How to Use frp's Visitor Mode for P2P Connections with NAT Traversal
frp's visitor mode enables direct peer-to-peer connections between two NAT'd clients by using a central frps server to coordinate STUN-based NAT hole punching, allowing traffic to bypass the server after authentication.
The fatedier/frp project implements a visitor mechanism that eliminates the need to route traffic through a public relay server when both endpoints sit behind restrictive NATs. By configuring an xtcp proxy on the service side and a corresponding visitor on the client side, frp establishes authenticated P2P tunnels that minimize latency and reduce server bandwidth costs.
How frp Visitor Mode Works
frp's P2P architecture relies on three distinct roles: the public coordination server (frps), the service provider (frpc proxy), and the connecting client (frpc visitor). The server manages registration and authentication but does not forward payload data after the initial handshake.
The Three-Party Architecture
1. The frps Server (Coordinator)
In server/visitor/visitor.go (lines 49-73), the visitor manager maintains a registry of active P2P listeners. When a proxy registers with type = "xtcp", the manager creates an internal listener and stores the shared secretKey along with allowed user permissions. This listener acts as a rendezvous point but never handles application data.
2. The Proxy Client (Service Side)
Client A runs frpc with an [[proxies]] entry of type xtcp. This registers the service name and secret with frps, enabling the server to validate future visitor requests using util.GetAuthKey (lines 72-80 of the visitor manager).
3. The Visitor Client (Consumer Side)
Client B declares a [[visitors]] entry pointing to the proxy's serverName. Upon startup, it contacts frps to request a P2P session. The server validates the secretKey and user ACLs before initiating NAT traversal.
NAT Hole Punching Coordination
Once authenticated, frps delegates the connection to the nat-hole controller implemented in pkg/nathole/controller.go (lines 150-190). The controller uses STUN protocols to discover the external IP and port mappings of both clients. It then exchanges these addresses between the proxy and visitor sides, allowing them to open direct UDP/TCP channels to each other. After the hole is punched, all application traffic flows directly between the two frpc instances; frps only maintains the control channel for heartbeat and renegotiation.
Configuring the frps Server
The server requires no special P2P-specific settings beyond a standard configuration. Enable detailed error reporting to debug NAT traversal issues.
# frps.toml
bindPort = 7000
detailedErrorsToClient = true
Setting Up the P2P Proxy Client (Client A)
Create a proxy configuration that exposes your local service using the xtcp type. The secretKey acts as the pre-shared authentication credential required by any visitor.
# frpc_proxy.toml
serverAddr = "x.x.x.x"
serverPort = 7000
# Optional: specify an external STUN server if defaults fail
# natHoleStunServer = "stun.l.google.com:19302"
[[proxies]]
name = "p2p_ssh"
type = "xtcp"
secretKey = "s3cr3tK3y"
localIP = "127.0.0.1"
localPort = 22
Configuring the Visitor Client (Client B)
The visitor configuration must reference the exact serverName from the proxy config and supply the matching secretKey. The bindAddr and bindPort create a local listener that tunnels to the remote P2P service.
# frpc_visitor.toml
serverAddr = "x.x.x.x"
serverPort = 7000
[[visitors]]
name = "p2p_ssh_visitor"
type = "xtcp"
serverName = "p2p_ssh" # Must match the proxy name exactly
secretKey = "s3cr3tK3y"
bindAddr = "127.0.0.1"
bindPort = 6000
keepTunnelOpen = false # Set true to maintain the tunnel during idle periods
Establishing the P2P Connection
Start the components in order: server first, then the proxy client, followed by the visitor client.
# On the public server
./frps -c ./frps.toml
# On the machine hosting the service (Client A)
./frpc -c ./frpc_proxy.toml
# On the remote machine connecting to the service (Client B)
./frpc -c ./frpc_visitor.toml
# Connect through the local visitor port
ssh -p 6000 user@127.0.0.1
If NAT hole punching succeeds, the SSH connection routes directly between Client A and Client B. If the P2P negotiation fails, check that both clients can reach the configured STUN server and that firewalls allow outbound UDP traffic.
Summary
- frp visitor mode uses
xtcpproxies and visitors to create direct P2P tunnels between NAT'd clients, reducing latency and server load. - The visitor manager in
server/visitor/visitor.gohandles registration and secret-key authentication usingutil.GetAuthKey. - NAT traversal is coordinated by
pkg/nathole/controller.go, which exchanges external addresses to enable hole punching. - The proxy side defines the
secretKeyand service endpoint, while the visitor side specifiesserverName, matchingsecretKey, and localbindPort. - The
keepTunnelOpenflag controls whether the P2P session persists during idle periods.
Frequently Asked Questions
What is the difference between tcp and xtcp proxy types in frp?
The tcp type routes all traffic through the frps server, making it compatible with any network topology but consuming server bandwidth. The xtcp type attempts to establish a direct P2P connection using visitor mode and NAT hole punching; if successful, data flows directly between clients, though it requires both sides to support the STUN-based negotiation implemented in pkg/nathole/controller.go.
How does frp authenticate visitor connections?
frps validates visitors using a pre-shared secretKey defined in both the proxy and visitor configurations. In server/visitor/visitor.go (lines 72-80), the server computes an authentication key using util.GetAuthKey and compares it against the visitor's submitted credential. Additionally, the proxy can specify an allowUsers list to restrict which user accounts may initiate visitor connections.
Can multiple visitors connect to the same xtcp proxy simultaneously?
Yes. The visitor manager in frps creates a listener for each registered proxy that can accept multiple visitor connections. Each visitor must provide the correct secretKey and use a unique local bindPort on their respective machines. The proxy side sees multiple independent P2P sessions, each established through separate NAT hole punching operations.
What happens if NAT hole punching fails?
If the STUN-based negotiation in pkg/nathole/controller.go cannot establish a direct route—commonly due to symmetric NATs or aggressive firewall rules—the connection will timeout. Unlike standard tcp proxies, xtcp does not automatically fall back to server relay; the connection simply fails. In such scenarios, you must either adjust network configurations or use the standard tcp proxy type for reliable connectivity.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →