How the frp SSH Tunnel Gateway Works Without frpc
The frp SSH tunnel gateway embeds an SSH server directly into frps that spawns a virtual frpc client internally, allowing standard OpenSSH clients to establish tunnels without installing the frpc binary.
The fatedier/frp project implements a server-side SSH tunnel gateway that transforms the frps process into a functional SSH daemon. This architecture enables any standard SSH client to behave as a tunnel endpoint by translating SSH protocol commands into internal virtual client registrations, completely removing the requirement for the frpc binary on client machines.
Gateway Architecture Overview
The SSH Tunnel Gateway is a server-side component that turns frps into an SSH server capable of accepting standard ssh -R connections. When an SSH client connects to the configured port, the server instantiates a virtual client inside frps that mimics a normal frpc instance. The entire handshake, authentication, and data forwarding pipeline occurs over the SSH connection, meaning the heavy lifting happens inside frps while the remote side only needs a standard SSH client.
Configuration and Service Initialization
The gateway is enabled through the frps configuration file and initialized during the service startup sequence.
In pkg/config/v1/server.go, the SSHTunnelGateway struct defines the configuration parameters including bindPort, privateKeyFile, autoGenPrivateKeyPath, and authorizedKeysFile. The Complete() method validates these settings.
During service creation in server/service.go, the NewService function checks if sshTunnelGateway.bindPort is greater than zero. When enabled, it calls ssh.NewGateway to instantiate the gateway:
// From server/service.go - NewService
if cfg.SSHTunnelGateway.BindPort > 0 {
sshGateway, err := ssh.NewGateway(cfg.SSHTunnelGateway)
// ... error handling and service registration
}
SSH Server Implementation
The gateway implementation resides in pkg/ssh/gateway.go. The NewGateway function constructs an ssh.ServerConfig with the following logic:
- Private Key Loading: Checks
privateKeyFilefirst, thenautoGenPrivateKeyPath. If neither is specified, generates a temporary RSA key. - Authentication Mode: Sets
NoClientAuthwhenauthorizedKeysFileis empty, allowing connections without SSH key authentication. Otherwise, loads the specified authorized keys for public-key authentication. - Listener Creation: Opens a TCP listener on the configured bind address and port.
The Run method accepts incoming TCP connections and spawns handleConn goroutines for each client. The handleConn function creates a new TunnelServer instance via ssh.NewTunnelServer to manage the individual SSH session.
Virtual Client Construction
The mechanism that eliminates the need for frpc occurs in pkg/ssh/server.go. The TunnelServer.Run method performs the SSH handshake using ssh.NewServerConn, then processes the tcpip-forward request containing the tunnel parameters (remote port, proxy type, local address).
After parsing the proxy configuration from the SSH request, the server creates a virtual client using virtual.NewClient:
// Conceptual flow from pkg/ssh/server.go
vc := virtual.NewClient(virtual.ClientOptions{
// ... configuration derived from SSH parameters
AlwaysAuthPass: !sshConfig.NoClientAuth,
})
The AlwaysAuthPass parameter determines authentication behavior:
- If SSH public-key authentication succeeded (
NoClientAuth = false), the virtual client is considered pre-authenticated. - If connecting without SSH authentication, the virtual client requires a token to pass frps authentication.
The virtual client then registers the requested proxy (TCP, HTTP, HTTPS, STCP, or TCPMUX) using the same internal pipeline as standard frpc connections, calling vc.UpdateProxyConfigurer with a ProxyConfigurer built from the parsed SSH flags.
Data Forwarding Mechanics
Once the virtual client registers the proxy, the TunnelServer establishes data flow between the SSH channel and frps work connections. In pkg/ssh/server.go, the handleConn method calls openConn to create the work connection, then uses libio.Join to pipe data bidirectionally:
// From the connection handling logic
go libio.Join(localConn, workConn)
This joins the SSH channel with the frps work connection, effectively acting as an frpc forwarder without the binary. When the SSH session terminates (through logout, Ctrl-C, or connection drop), the TunnelServer cleans up the virtual client and removes the proxy registration.
Security Configuration
The gateway supports multiple authentication layers independent of standard frpc token validation:
| Feature | Implementation | Default Behavior |
|---|---|---|
| Server Identity | privateKeyFile (custom) or autoGenPrivateKeyPath (persistent) |
Auto-generates .autogen_ssh_key if unspecified |
| Client Authentication | authorizedKeysFile enables public-key auth |
NoClientAuth mode (no SSH auth required) |
| frps Token Validation | Required when SSH auth is disabled or for additional security | Recommended but optional |
| User Mapping | SSH public key comment populates clientCfg.User |
Unset when NoClientAuth is active |
When authorizedKeysFile is configured, the gateway enforces key-based authentication and extracts the username from the key comment into ssh.Permissions.Extensions["user"], which becomes the clientCfg.User value for the virtual client.
Practical Usage Examples
Minimal Server Configuration
Enable the gateway in frps.toml with auto-generated keys:
sshTunnelGateway.bindPort = 2200
# Optional: sshTunnelGateway.authorizedKeysFile = "/etc/frp/authorized_keys"
TCP Proxy via Standard SSH
Create a reverse tunnel without frpc installed:
ssh -R :9090:127.0.0.1:8080 v0@frps.example.com -p 2200 tcp \
--proxy_name my-tcp \
--token my-secret-token
The command parses as follows:
- The SSH client requests forwarding port 9090 on the server to local port 8080
- The gateway intercepts the
tcpip-forwardrequest and thetcpsubcommand - It creates a virtual client registering a TCP proxy named
my-tcp - Traffic to
frps.example.com:9090tunnels to the SSH client's localhost:8080
HTTP Proxy with Custom Domain
ssh -R :80:127.0.0.1:8080 v0@frps.example.com -p 2200 http \
--proxy_name my-http \
--custom_domain app.example.com \
--token my-secret-token
Key-Based Authentication
For deployments requiring SSH key authentication without tokens:
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.privateKeyFile = "/etc/frp/ssh_host_rsa_key"
sshTunnelGateway.authorizedKeysFile = "/etc/frp/authorized_keys"
Client connection (no token required):
ssh -i ~/.ssh/id_frp -R :9090:127.0.0.1:8080 myuser@frps.example.com -p 2200 tcp \
--proxy_name authenticated-tcp
Summary
- The SSH Tunnel Gateway embeds an SSH server into frps via
pkg/ssh/gateway.go, listening on a configurable port. - Upon connection,
pkg/ssh/server.gocreates a virtual client usingvirtual.NewClientthat registers proxies through the standard frps pipeline. - Standard SSH clients can replace frpc by sending
tcpip-forwardrequests that the gateway translates into proxy configurations. - Authentication supports both SSH public-key (via
authorizedKeysFile) and frps token validation, withAlwaysAuthPasscontrolling the virtual client authentication state. - Data forwarding uses
libio.Jointo connect SSH channels directly to frps work connections, achieving zero-binary tunneling on the client side.
Frequently Asked Questions
Do I need the frpc binary installed to use the SSH tunnel gateway?
No. The SSH tunnel gateway eliminates the need for frpc on the client side. According to the implementation in pkg/ssh/server.go, the gateway creates a virtual frpc client internally when an SSH connection is established. You only need a standard OpenSSH client (or any SSH client supporting reverse tunnels) to connect to the gateway port configured in sshTunnelGateway.bindPort.
How does authentication differ between SSH and standard frpc connections?
Standard frpc uses token-based authentication configured in frpc.toml. The SSH gateway adds a layer of SSH protocol authentication through pkg/ssh/gateway.go. When authorizedKeysFile is configured, the gateway enforces public-key authentication and sets AlwaysAuthPass = true for the virtual client, bypassing token checks. When authorizedKeysFile is empty (NoClientAuth mode), the SSH connection succeeds without authentication, but the virtual client requires a valid --token parameter to register with frps.
What proxy types are supported through the SSH gateway?
The gateway supports the same proxy types as standard frpc: TCP, HTTP, HTTPS, STCP, and TCPMUX. As implemented in pkg/ssh/server.go, the proxy type is determined by parsing the SSH command arguments (e.g., tcp, http, https) and constructing the appropriate ProxyConfigurer for the virtual client. Each type accepts corresponding parameters like --custom_domain for HTTP/HTTPS or --remote_port for TCP.
Is the auto-generated SSH key persistent across frps restarts?
By default, frps generates a temporary RSA key if neither privateKeyFile nor autoGenPrivateKeyPath is specified. However, when autoGenPrivateKeyPath is configured (defaulting to .autogen_ssh_key), the gateway loads or generates a persistent key at that path. This prevents SSH clients from seeing "host key changed" warnings after frps restarts, as documented in the NewGateway function within pkg/ssh/gateway.go.
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 →