fatedier / frp

A fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet.
Apache License 2.0
80.5k stars 12.74k forks source link

x-forwarded-proto gets stripped away in http proxies #4168

Closed panta82 closed 4 weeks ago

panta82 commented 4 weeks ago

Bug Description

I'm using setup: nginx[443] (SSL termination) -> proxy_pass[http] -> frps[http] -> ...tunnel... -> frpc[http] -> local service[http]

This works, however on the other side, X-Forwarded-Proto header gets changed, which creates problems for cookies (the app thinks it's not behind HTTPS, which is incorrect).

Here's the diff: image

Left side: What is sent from nginx to frps Right side: What is sent from frpc to the client

Frps adds X-Forwarded-Host (good), but changes X-Forwarded-Proto from https to http.

frpc Version

0.57.0

frps Version

0.57.0

System Architecture

linux/amd64

Configurations

frps.toml:

# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.

# A literal address or host name for IPv6 must be enclosed
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
# For single "bindAddr" field, no need square brackets, like `bindAddr = "::"`.
bindAddr = "0.0.0.0"
bindPort = 7852

# udp port used for kcp protocol, it can be same with 'bindPort'.
# if not set, kcp is disabled in frps.
kcpBindPort = 7852

# Pool count in each proxy will keep no more than maxPoolCount.
transport.maxPoolCount = 5

# If tcp stream multiplexing is used, default is true
transport.tcpMux = true

# If you want to support virtual host, you must set the http port for listening (optional)
# Note: http port and https port can be same with bindPort
vhostHTTPPort = 7952
#vhostHTTPSPort = 7952

# Configure the web server to enable the dashboard for frps.
# dashboard is available only if webServer.port is set.
webServer.addr = "127.0.0.1"
webServer.port = 8052
webServer.user = "admin"
webServer.password = "admin"

# console or real logFile path like ./frps.log
#log.to = "./frps.log"
# trace, debug, info, warn, error
#log.level = "info"
#log.maxDays = 3
# disable log colors when log.to is console, default is false
log.disablePrintColor = false

# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true.
detailedErrorsToClient = true

auth.method = "token"
auth.token = "xxx"

# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file
# When subdomain is test, the host used by routing is test.frps.com
subDomainHost = "client.example.com"

frpc.toml:

# your proxy name will be changed to {user}.{proxy}
#user = "your_name"

# A literal address or host name for IPv6 must be enclosed
# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80"
# For single serverAddr field, no need square brackets, like serverAddr = "::".
serverAddr = "server.example.com"
serverPort = 7852

# Decide if exit program when first login failed, otherwise continuous relogin to frps
# default is true
loginFailExit = true

# console or real logFile path like ./frpc.log
#log.to = "./frpc.log"
# trace, debug, info, warn, error
log.level = "info"
log.maxDays = 3
# disable log colors when log.to is console, default is false
log.disablePrintColor = false

auth.method = "token"
auth.token = "xxx"

# connections will be established in advance, default value is zero
transport.poolCount = 5

# If tcp stream multiplexing is used, default is true, it must be same with frps
# transport.tcpMux = true

# Communication protocol used to connect to server
# supports tcp, kcp, quic, websocket and wss now, default is tcp
transport.protocol = "tcp"

# If tls.enable is true, frpc will connect frps by tls.
# Since v0.50.0, the default value has been changed to true, and tls is enabled by default.
transport.tls.enable = true

[[proxies]]
name = "app"
type = "http"
localIP = "127.0.0.1"
localPort = 8000
subdomain = "app"

client.example.com.conf (nginx):

server {
    listen 443 ssl;

    ## Server name
    server_name client.example.com;

    ## SSL Certificate
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ## Do not display server info in responses
    server_tokens off;

    ## SSL security
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    ## Mozilla Intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    ## Increase max body size, to prevent strange errors
    client_max_body_size 200M;

    ## Prevent problems with JWT token size
    large_client_header_buffers 8 64k;
    proxy_buffers               8 32k;
    proxy_buffer_size             32k;

    ## Proxy pass
    location / {
        proxy_pass http://127.0.0.1:7952;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $http_host;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        #proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Nginx-Proxy true;

        proxy_redirect off;
    }
}

Logs

No response

Steps to reproduce

???

Affected area

fatedier commented 4 weeks ago

This should be expected, otherwise the client can pass any untrusted content.

In your scenario, you can test whether the issue can be resolved by using requestHeaders.set.x-forwarded-proto = "https".

panta82 commented 4 weeks ago

Understood. I can certainly hack it in.

It might be useful though for frps to have some kind of "behind proxy" mode, where this header is trusted. Because I assume this will be a 99% use case for the tool.

(also, should x-real-ip then be passed through?)

fatedier commented 4 weeks ago

More about the configuration of the 7th layer HTTP will be planned for the future v2 version, and currently, no additional configuration options will be provided.

x-real-ip is not a standard header defined by the RFC. The current code uses the native handling logic of Go and does not perform any additional processing.

panta82 commented 4 weeks ago

Ok, that's fair. Closing the issue.