# How to Set Up frp with QUIC Protocol for Modern Transport Optimization

> Optimize transport with frp and QUIC. Learn how to easily set up frp with QUIC protocol for faster, more reliable connections. Guide includes server and client configurations.

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

---

**To set up frp with QUIC protocol, configure `quic_bind_port` on the server and set `transport.protocol = "quic"` on the client, ensuring both sides use matching TLS settings and optional QUIC tuning parameters.**

The **fatedier/frp** repository implements a high-performance QUIC transport layer that reduces connection latency and improves throughput over traditional TCP. When you set up frp with QUIC protocol, you leverage UDP-based multiplexing with built-in TLS encryption, making it ideal for high-latency or lossy networks.

## Why Use QUIC with frp?

QUIC (Quick UDP Internet Connections) offers several advantages for reverse proxy tunnels:

- **0-RTT connection establishment** reduces latency compared to TCP+TLS handshakes
- **Built-in congestion control** and stream multiplexing over a single UDP port
- **Connection migration** support when client IP addresses change
- **Mandatory TLS encryption** for all traffic without additional configuration

According to the frp source code, the QUIC implementation resides in [`server/service.go`](https://github.com/fatedier/frp/blob/main/server/service.go) for the listener and [`client/connector.go`](https://github.com/fatedier/frp/blob/main/client/connector.go) for the dialer.

## Configuring the frp Server (frps) for QUIC

### Setting the QUIC Bind Port

The server requires a dedicated UDP port to accept QUIC connections. In [`server/service.go`](https://github.com/fatedier/frp/blob/main/server/service.go) (lines 60-73), the server initializes the QUIC listener when `quic_bind_port` is non-zero.

Create or modify your [`frps.ini`](https://github.com/fatedier/frp/blob/main/frps.ini):

```ini
[common]
bind_addr = 0.0.0.0
bind_port = 7000
quic_bind_port = 7001

```

Alternatively, use command-line flags:

```bash
./frps --quic_bind_port 7001

```

### Tuning QUIC Parameters

The `QUICOptions` struct in [`pkg/config/v1/common.go`](https://github.com/fatedier/frp/blob/main/pkg/config/v1/common.go) (lines 49-60) defines three tunable parameters:

- **keepalivePeriod**: Interval between keep-alive packets (default: 10s)
- **maxIdleTimeout**: Connection idle timeout (default: 30s)
- **maxIncomingStreams**: Maximum concurrent streams (default: 100,000)

Add these under the `[transport]` section in [`frps.ini`](https://github.com/fatedier/frp/blob/main/frps.ini):

```ini
[transport]
quic.keepalivePeriod = 10
quic.maxIdleTimeout = 30
quic.maxIncomingStreams = 100000

```

## Configuring the frp Client (frpc) for QUIC

### Enabling QUIC Protocol

In [`client/connector.go`](https://github.com/fatedier/frp/blob/main/client/connector.go) (lines 68-104), the client uses `quic.DialAddr` when `transport.protocol` is set to `"quic"`. The resulting `*quic.Conn` is stored and reused for subsequent streams.

Configure [`frpc.ini`](https://github.com/fatedier/frp/blob/main/frpc.ini):

```ini
[common]
server_addr = your.server.com
server_port = 7000
transport.protocol = quic

```

Or via command line:

```bash
./frpc --protocol quic

```

### Matching Server Settings

Ensure the client QUIC options match or are compatible with the server configuration:

```ini
[common]
server_addr = your.server.com
server_port = 7000
transport.protocol = quic

[transport]
quic.keepalivePeriod = 10
quic.maxIdleTimeout = 30
quic.maxIncomingStreams = 100000

# TLS is mandatory for QUIC but auto-configured by default

# transport.tls.enable = true

```

## Understanding the QUIC Implementation in frp Source Code

### Server-Side QUIC Listener

The server initialization in [`server/service.go`](https://github.com/fatedier/frp/blob/main/server/service.go) (lines 77-78) creates a `quic.Listener` using the configured TLS profile with `NextProtos: ["frp"]`. This listener accepts incoming UDP packets and handles the QUIC handshake transparently.

Key implementation details:
- The server disables receive-buffer warnings for QUIC sockets to prevent log spam
- Each accepted QUIC connection is handled by the same `HandleListener` logic used for TCP
- Source: [`server/service.go`](https://github.com/fatedier/frp/blob/main/server/service.go) – listener creation (lines 60-73) and QUIC handling (lines 77-78)

### Client-Side QUIC Dialer

The `defaultConnectorImpl` in [`client/connector.go`](https://github.com/fatedier/frp/blob/main/client/connector.go) manages QUIC connections through two phases:

1. **Connection Establishment** (lines 68-104): Calls `quic.DialAddr` with the server address and TLS config, storing the resulting `*quic.Conn`
2. **Stream Conversion** (lines 30-36): Each proxy connection calls `OpenStreamSync` on the QUIC connection, then wraps the stream using `QuicStreamToNetConn` from [`pkg/util/net/conn.go`](https://github.com/fatedier/frp/blob/main/pkg/util/net/conn.go)

This design allows multiple frp proxies to multiplex over a single QUIC connection.

### Stream Management

The `QuicStreamToNetConn` function (referenced in [`client/connector.go`](https://github.com/fatedier/frp/blob/main/client/connector.go)) adapts QUIC streams to the standard `net.Conn` interface, enabling the rest of the frp codebase to operate transparently regardless of the underlying transport.

## Complete Configuration Examples

### Server Configuration (INI)

```ini
[common]
bind_addr = 0.0.0.0
bind_port = 7000
quic_bind_port = 7001

[transport]
quic.keepalivePeriod = 10
quic.maxIdleTimeout = 30
quic.maxIncomingStreams = 100000

# Optional: custom TLS certificates

# tls.certFile = ./server.crt

# tls.keyFile = ./server.key

```

### Client Configuration (INI)

```ini
[common]
server_addr = your.server.com
server_port = 7000
transport.protocol = quic

[transport]
quic.keepalivePeriod = 10
quic.maxIdleTimeout = 30
quic.maxIncomingStreams = 100000

# TLS is enabled by default for QUIC

# transport.tls.enable = true

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000

```

### Command-Line Alternatives

**Server:**

```bash
./frps --quic_bind_port 7001 \
    --quic.keepalivePeriod 10 \
    --quic.maxIdleTimeout 30

```

**Client:**

```bash
./frpc --protocol quic \
    --server_addr your.server.com \
    --server_port 7000

```

### Programmatic Go Example

For developers embedding frp directly:

```go
import (
    "context"
    "github.com/fatedier/frp/pkg/config/v1"
    "github.com/fatedier/frp/client"
)

func runQUICClient() error {
    cfg := &v1.ClientConfig{
        ClientCommonConfig: v1.ClientCommonConfig{
            ServerAddr: "your.server.com",
            ServerPort: 7000,
            Transport: v1.ClientTransportConfig{
                Protocol: "quic",
                QUIC: v1.QUICOptions{
                    KeepalivePeriod:    10,
                    MaxIdleTimeout:     30,
                    MaxIncomingStreams: 100000,
                },
            },
        },
    }
    
    if err := cfg.Complete(); err != nil {
        return err
    }
    
    svc, err := client.NewService(cfg)
    if err != nil {
        return err
    }
    defer svc.Close()
    
    return svc.Run(context.Background())
}

```

## Summary

Setting up frp with QUIC protocol involves three core steps:

- **Configure the server** with `quic_bind_port` in [`frps.ini`](https://github.com/fatedier/frp/blob/main/frps.ini) (or `--quic_bind_port` flag) to enable UDP listening on a dedicated port
- **Configure the client** with `transport.protocol = "quic"` in [`frpc.ini`](https://github.com/fatedier/frp/blob/main/frpc.ini) (or `--protocol quic` flag) to initiate QUIC connections
- **Align QUIC parameters** such as `keepalivePeriod`, `maxIdleTimeout`, and `maxIncomingStreams` on both sides to ensure stable multiplexing over the UDP transport

The implementation in [`server/service.go`](https://github.com/fatedier/frp/blob/main/server/service.go) and [`client/connector.go`](https://github.com/fatedier/frp/blob/main/client/connector.go) handles TLS automatically, converts QUIC streams to standard `net.Conn` interfaces, and supports up to 100,000 concurrent streams per connection by default.

## Frequently Asked Questions

### Is QUIC faster than TCP in frp?

QUIC typically reduces connection establishment latency compared to TCP+TLS because it combines the transport and encryption handshakes into a single 0-RTT or 1-RTT exchange. In high-latency networks, this can significantly improve initial connection speed. However, throughput depends on network conditions and the `maxIncomingStreams` setting, which defaults to 100,000 concurrent streams.

### Does QUIC require specific firewall rules?

Yes, because QUIC uses UDP instead of TCP. You must open the UDP port specified by `quic_bind_port` (default 7001 in examples) on your server firewall. Unlike TCP, QUIC handles its own congestion control and reliability, so only the single UDP port needs to be exposed, though you should also keep the TCP `bind_port` open for control channel compatibility if running mixed transports.

### Can I use custom TLS certificates with QUIC?

Absolutely. While frp automatically generates self-signed TLS certificates when `transport.tls.enable` is true (the default for QUIC), you can specify custom certificates using `transport.tls.certFile`, `transport.tls.keyFile`, and `transport.tls.trustedCaFile` in both client and server configurations. This is essential for production environments where certificate validation is required.

### What happens if the QUIC connection drops?

The frp client implements automatic reconnection logic. If the QUIC connection drops due to network interruption or timeout (governed by `maxIdleTimeout`), the client will attempt to redial the server using `quic.DialAddr` as implemented in [`client/connector.go`](https://github.com/fatedier/frp/blob/main/client/connector.go). During reconnection, existing proxy tunnels will temporarily disconnect and reconnect once the new QUIC connection is established and new streams are opened via `OpenStreamSync`.