# How the frp SSH Tunnel Gateway Works Without frpc

> Discover how the frp SSH tunnel gateway works without frpc. Learn how frps embeds an SSH server to create virtual frpc clients for seamless tunneling with standard OpenSSH.

- Repository: [fatedier/frp](https://github.com/fatedier/frp)
- Tags: deep-dive
- Published: 2026-02-26

---

**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`](https://github.com/fatedier/frp/blob/main/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`](https://github.com/fatedier/frp/blob/main/server/service.go), the `NewService` function checks if `sshTunnelGateway.bindPort` is greater than zero. When enabled, it calls `ssh.NewGateway` to instantiate the gateway:

```go
// 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`](https://github.com/fatedier/frp/blob/main/pkg/ssh/gateway.go). The `NewGateway` function constructs an `ssh.ServerConfig` with the following logic:

- **Private Key Loading**: Checks `privateKeyFile` first, then `autoGenPrivateKeyPath`. If neither is specified, generates a temporary RSA key.
- **Authentication Mode**: Sets `NoClientAuth` when `authorizedKeysFile` is 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`](https://github.com/fatedier/frp/blob/main/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`:

```go
// 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`](https://github.com/fatedier/frp/blob/main/pkg/ssh/server.go), the `handleConn` method calls `openConn` to create the work connection, then uses `libio.Join` to pipe data bidirectionally:

```go
// 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`](https://github.com/fatedier/frp/blob/main/frps.toml) with auto-generated keys:

```toml
sshTunnelGateway.bindPort = 2200

# Optional: sshTunnelGateway.authorizedKeysFile = "/etc/frp/authorized_keys"

```

### TCP Proxy via Standard SSH

Create a reverse tunnel without frpc installed:

```bash
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:
1. The SSH client requests forwarding port 9090 on the server to local port 8080
2. The gateway intercepts the `tcpip-forward` request and the `tcp` subcommand
3. It creates a virtual client registering a TCP proxy named `my-tcp`
4. Traffic to `frps.example.com:9090` tunnels to the SSH client's localhost:8080

### HTTP Proxy with Custom Domain

```bash
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:

```toml
sshTunnelGateway.bindPort = 2200
sshTunnelGateway.privateKeyFile = "/etc/frp/ssh_host_rsa_key"
sshTunnelGateway.authorizedKeysFile = "/etc/frp/authorized_keys"

```

Client connection (no token required):

```bash
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`](https://github.com/fatedier/frp/blob/main/pkg/ssh/gateway.go), listening on a configurable port.
- Upon connection, [`pkg/ssh/server.go`](https://github.com/fatedier/frp/blob/main/pkg/ssh/server.go) creates a **virtual client** using `virtual.NewClient` that registers proxies through the standard frps pipeline.
- **Standard SSH clients** can replace frpc by sending `tcpip-forward` requests that the gateway translates into proxy configurations.
- Authentication supports both **SSH public-key** (via `authorizedKeysFile`) and **frps token** validation, with `AlwaysAuthPass` controlling the virtual client authentication state.
- Data forwarding uses `libio.Join` to 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`](https://github.com/fatedier/frp/blob/main/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`](https://github.com/fatedier/frp/blob/main/frpc.toml). The SSH gateway adds a layer of SSH protocol authentication through [`pkg/ssh/gateway.go`](https://github.com/fatedier/frp/blob/main/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`](https://github.com/fatedier/frp/blob/main/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`](https://github.com/fatedier/frp/blob/main/pkg/ssh/gateway.go).