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

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 for the listener and 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 (lines 60-73), the server initializes the QUIC listener when quic_bind_port is non-zero.

Create or modify your frps.ini:

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

Alternatively, use command-line flags:

./frps --quic_bind_port 7001

Tuning QUIC Parameters

The QUICOptions struct in 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:

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

Configuring the frp Client (frpc) for QUIC

Enabling QUIC Protocol

In 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:

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

Or via command line:

./frpc --protocol quic

Matching Server Settings

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

[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 (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 – listener creation (lines 60-73) and QUIC handling (lines 77-78)

Client-Side QUIC Dialer

The defaultConnectorImpl in 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

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

Stream Management

The QuicStreamToNetConn function (referenced in 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)

[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)

[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:

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

Client:

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

Programmatic Go Example

For developers embedding frp directly:

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 (or --quic_bind_port flag) to enable UDP listening on a dedicated port
  • Configure the client with transport.protocol = "quic" in 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 and 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. During reconnection, existing proxy tunnels will temporarily disconnect and reconnect once the new QUIC connection is established and new streams are opened via OpenStreamSync.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →