How frp Handles X-Forwarded-For Headers to Preserve Client IP Addresses
frp preserves original client IPs by copying inbound X-Forwarded-For headers and appending the current client address using Go's SetXForwarded() method before forwarding requests to backend services.
When tunneling HTTP/HTTPS traffic through fatedier/frp, maintaining visibility of the original client IP address is critical for logging, rate limiting, and security analytics. The frp project handles this by explicitly managing the X-Forwarded-For header across its reverse proxy and plugin implementations, ensuring downstream services receive accurate client IP chains.
How frp Propagates X-Forwarded-For Headers
Core Header Handling in the Reverse Proxy
The primary logic resides in pkg/util/vhost/http.go. During the request rewrite phase, frp explicitly copies the incoming header to the outbound request:
r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
(see pkg/util/vhost/http.go, lines 63-66).
Immediately after copying, the code invokes r.SetXForwarded(), a method provided by Go's httputil.ProxyRequest. This appends the remote address of the current frp connection to the header value, creating a comma-separated chain such as X-Forwarded-For: 203.0.113.4, 10.0.0.5, where the first IP represents the original client and subsequent entries denote intermediate proxies.
Consistent Behavior Across Plugin Proxies
frp extends this header handling to its client-side plugin proxies, including HTTPS2HTTPS, HTTPS2HTTP, and HTTP2HTTPS modes. Each plugin's rewrite function implements identical logic to ensure uniform client IP preservation regardless of the encryption layer.
For example, in pkg/plugin/client/https2https.go (lines 63-66), the plugin copies the header and calls SetXForwarded() just like the core HTTP proxy. The same pattern appears in https2http.go and http2https.go, guaranteeing that backend services receive consistent X-Forwarded-For values whether the tunnel terminates TLS or passes it through.
Verifying Client IP Preservation
The frp codebase includes end-to-end tests in test/e2e/v1/features/real_ip.go that verify header propagation. These tests confirm that the X-Forwarded-For header reaches the backend service containing the expected IP chain, validating that the reverse proxy logic functions correctly across different network configurations.
Configuring frp to Preserve Client IP Addresses
To enable X-Forwarded-For header propagation in your frp deployment, configure an HTTP or HTTPS proxy on both the server and client sides.
Server configuration (frps.ini):
[common]
bind_port = 7000
[web]
type = http
local_port = 8080
custom_domains = example.com
Client configuration (frpc.ini):
[common]
server_addr = x.x.x.x
server_port = 7000
[web]
type = http
local_port = 8080
custom_domains = example.com
When a request traverses this tunnel, frp copies any existing X-Forwarded-For header from the client, appends the current connection's IP via SetXForwarded(), and forwards the enriched request to your local service on port 8080.
To verify the header on your backend, inspect the request headers in your application code. The following Go example reads and displays the X-Forwarded-For value:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
xff := r.Header.Get("X-Forwarded-For")
fmt.Fprintf(w, "X-Forwarded-For: %s\n", xff)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
Running this behind the configured frp tunnel outputs a header string such as 203.0.113.42, 10.0.0.5, where 203.0.113.42 is the original client IP and 10.0.0.5 represents the frp client or an upstream proxy.
Summary
- frp copies the inbound
X-Forwarded-Forheader to outbound requests inpkg/util/vhost/http.gobefore invokingSetXForwarded(). - The Go standard library's
SetXForwarded()method appends the current client IP to the header chain. - Client-side plugins (HTTPS2HTTPS, HTTPS2HTTP, HTTP2HTTPS) implement identical header handling for consistent behavior.
- End-to-end tests in
test/e2e/v1/features/real_ip.govalidate that backend services receive correct client IP chains.
Frequently Asked Questions
Does frp automatically add X-Forwarded-For headers if they don't exist?
Yes. According to the implementation in pkg/util/vhost/http.go, frp first copies any existing header values, then calls r.SetXForwarded(). If no header exists, the copy operation results in an empty value, and SetXForwarded() will initialize the header with the current client IP address.
How does frp handle multiple proxy layers in the X-Forwarded-For chain?
frp appends the immediate client's IP address to the existing comma-separated list using Go's httputil.ProxyRequest.SetXForwarded() method. This preserves the complete proxy chain, allowing downstream applications to identify both the original client (first IP) and any intermediate proxies (subsequent IPs) including the frp tunnel itself.
Is X-Forwarded-For handling available in HTTPS plugin modes?
Yes. The frp project implements identical header propagation logic across all HTTP-based plugins. Files such as pkg/plugin/client/https2https.go, https2http.go, and http2https.go each copy the X-Forwarded-For header and invoke SetXForwarded(), ensuring client IP preservation regardless of TLS termination points.
Can I trust the X-Forwarded-For header values from frp?
The header values reflect the connection chain as observed by frp. While frp accurately appends the real client IP it observes, administrators should validate that direct access to backend services is restricted to frp tunnels only, preventing clients from spoofing the header by connecting directly to the backend and bypassing the proxy.
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 →