ipn/ipnlocal: add PROXY protocol support to Funnel/Serve
This adds the --proxy-protocol flag to 'tailscale serve' and 'tailscale funnel', which tells the Tailscale client to prepend a PROXY protocol[1] header when making connections to the proxied-to backend. I've verified that this works with our existing funnel servers without additional work, since they pass along source address information via PeerAPI already. Updates #7747 [1]: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt Change-Id: I647c24d319375c1b33e995555a541b7615d2d203 Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
This commit is contained in:
@@ -85,6 +85,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
💣 github.com/modern-go/reflect2 from github.com/json-iterator/go
|
||||
github.com/munnerz/goautoneg from k8s.io/kube-openapi/pkg/handler3+
|
||||
github.com/opencontainers/go-digest from github.com/distribution/reference
|
||||
github.com/pires/go-proxyproto from tailscale.com/ipn/ipnlocal
|
||||
github.com/pkg/errors from github.com/evanphx/json-patch/v5+
|
||||
D github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
|
||||
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil from github.com/prometheus/client_golang/prometheus/promhttp
|
||||
|
||||
@@ -168,6 +168,7 @@ type serveEnv struct {
|
||||
http uint // HTTP port
|
||||
tcp uint // TCP port
|
||||
tlsTerminatedTCP uint // a TLS terminated TCP port
|
||||
proxyProtocol uint // PROXY protocol version (1 or 2)
|
||||
subcmd serveMode // subcommand
|
||||
yes bool // update without prompt
|
||||
service tailcfg.ServiceName // service name
|
||||
@@ -570,7 +571,7 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||
}
|
||||
|
||||
sc.SetTCPForwarding(srcPort, fwdAddr, terminateTLS, dnsName)
|
||||
sc.SetTCPForwarding(srcPort, fwdAddr, terminateTLS, 0 /* proxy proto */, dnsName)
|
||||
|
||||
if !reflect.DeepEqual(cursc, sc) {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
|
||||
@@ -240,6 +240,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
}
|
||||
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
|
||||
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
|
||||
fs.UintVar(&e.proxyProtocol, "proxy-protocol", 0, "PROXY protocol version (1 or 2) for TCP forwarding")
|
||||
fs.BoolVar(&e.yes, "yes", false, "Update without interactive prompts (default false)")
|
||||
fs.BoolVar(&e.tun, "tun", false, "Forward all traffic to the local machine (default false), only supported for services. Refer to docs for more information.")
|
||||
}),
|
||||
@@ -413,6 +414,14 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
return errHelpFunc(subcmd)
|
||||
}
|
||||
|
||||
if (srvType == serveTypeHTTP || srvType == serveTypeHTTPS) && e.proxyProtocol != 0 {
|
||||
return fmt.Errorf("PROXY protocol is only supported for TCP forwarding, not HTTP/HTTPS")
|
||||
}
|
||||
// Validate PROXY protocol version
|
||||
if e.proxyProtocol != 0 && e.proxyProtocol != 1 && e.proxyProtocol != 2 {
|
||||
return fmt.Errorf("invalid PROXY protocol version %d; must be 1 or 2", e.proxyProtocol)
|
||||
}
|
||||
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting serve config: %w", err)
|
||||
@@ -507,7 +516,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
if len(args) > 0 {
|
||||
target = args[0]
|
||||
}
|
||||
err = e.setServe(sc, dnsName, srvType, srvPort, mount, target, funnel, magicDNSSuffix, e.acceptAppCaps)
|
||||
err = e.setServe(sc, dnsName, srvType, srvPort, mount, target, funnel, magicDNSSuffix, e.acceptAppCaps, int(e.proxyProtocol))
|
||||
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -828,7 +837,7 @@ func (e *serveEnv) runServeSetConfig(ctx context.Context, args []string) (err er
|
||||
for name, details := range scf.Services {
|
||||
for ppr, ep := range details.Endpoints {
|
||||
if ep.Protocol == conffile.ProtoTUN {
|
||||
err := e.setServe(sc, name.String(), serveTypeTUN, 0, "", "", false, magicDNSSuffix, nil)
|
||||
err := e.setServe(sc, name.String(), serveTypeTUN, 0, "", "", false, magicDNSSuffix, nil, 0 /* proxy protocol */)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -850,7 +859,7 @@ func (e *serveEnv) runServeSetConfig(ctx context.Context, args []string) (err er
|
||||
portStr := fmt.Sprint(destPort)
|
||||
target = fmt.Sprintf("%s://%s", ep.Protocol, net.JoinHostPort(ep.Destination, portStr))
|
||||
}
|
||||
err := e.setServe(sc, name.String(), serveType, port, "/", target, false, magicDNSSuffix, nil)
|
||||
err := e.setServe(sc, name.String(), serveType, port, "/", target, false, magicDNSSuffix, nil, 0 /* proxy protocol */)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service %q: %w", name, err)
|
||||
}
|
||||
@@ -953,7 +962,7 @@ func serveFromPortHandler(tcp *ipn.TCPPortHandler) serveType {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool, mds string, caps []tailcfg.PeerCapability) error {
|
||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool, mds string, caps []tailcfg.PeerCapability, proxyProtocol int) error {
|
||||
// update serve config based on the type
|
||||
switch srvType {
|
||||
case serveTypeHTTPS, serveTypeHTTP:
|
||||
@@ -966,7 +975,7 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveTy
|
||||
if e.setPath != "" {
|
||||
return fmt.Errorf("cannot mount a path for TCP serve")
|
||||
}
|
||||
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
|
||||
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target, proxyProtocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply TCP serve: %w", err)
|
||||
}
|
||||
@@ -1092,6 +1101,9 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
if tcpHandler.TerminateTLS != "" {
|
||||
tlsStatus = "TLS terminated"
|
||||
}
|
||||
if ver := tcpHandler.ProxyProtocol; ver != 0 {
|
||||
tlsStatus = fmt.Sprintf("%s, PROXY protocol v%d", tlsStatus, ver)
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("|-- tcp://%s:%d (%s)\n", host, srvPort, tlsStatus))
|
||||
for _, a := range ips {
|
||||
@@ -1170,7 +1182,7 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType serveType, srcPort uint16, target string) error {
|
||||
func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType serveType, srcPort uint16, target string, proxyProtocol int) error {
|
||||
var terminateTLS bool
|
||||
switch srcType {
|
||||
case serveTypeTCP:
|
||||
@@ -1197,8 +1209,7 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d for %s", srcPort, dnsName)
|
||||
}
|
||||
|
||||
sc.SetTCPForwarding(srcPort, dstURL.Host, terminateTLS, dnsName)
|
||||
|
||||
sc.SetTCPForwarding(srcPort, dstURL.Host, terminateTLS, proxyProtocol, dnsName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -919,6 +919,73 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp_with_proxy_protocol_v1",
|
||||
steps: []step{{
|
||||
command: cmd("serve --tcp=8000 --proxy-protocol=1 --bg tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
8000: {
|
||||
TCPForward: "localhost:5432",
|
||||
ProxyProtocol: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "tls_terminated_tcp_with_proxy_protocol_v2",
|
||||
steps: []step{{
|
||||
command: cmd("serve --tls-terminated-tcp=443 --proxy-protocol=2 --bg tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "localhost:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
ProxyProtocol: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "tcp_update_to_add_proxy_protocol",
|
||||
steps: []step{
|
||||
{
|
||||
command: cmd("serve --tcp=8000 --bg tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
8000: {TCPForward: "localhost:5432"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
command: cmd("serve --tcp=8000 --proxy-protocol=1 --bg tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
8000: {
|
||||
TCPForward: "localhost:5432",
|
||||
ProxyProtocol: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp_proxy_protocol_invalid_version",
|
||||
steps: []step{{
|
||||
command: cmd("serve --tcp=8000 --proxy-protocol=3 --bg tcp://localhost:5432"),
|
||||
wantErr: anyErr(),
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "proxy_protocol_without_tcp",
|
||||
steps: []step{{
|
||||
command: cmd("serve --https=443 --proxy-protocol=1 --bg http://localhost:3000"),
|
||||
wantErr: anyErr(),
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
@@ -1889,18 +1956,19 @@ func TestSetServe(t *testing.T) {
|
||||
e := &serveEnv{}
|
||||
magicDNSSuffix := "test.ts.net"
|
||||
tests := []struct {
|
||||
name string
|
||||
desc string
|
||||
cfg *ipn.ServeConfig
|
||||
st *ipnstate.Status
|
||||
dnsName string
|
||||
srvType serveType
|
||||
srvPort uint16
|
||||
mountPath string
|
||||
target string
|
||||
allowFunnel bool
|
||||
expected *ipn.ServeConfig
|
||||
expectErr bool
|
||||
name string
|
||||
desc string
|
||||
cfg *ipn.ServeConfig
|
||||
st *ipnstate.Status
|
||||
dnsName string
|
||||
srvType serveType
|
||||
srvPort uint16
|
||||
mountPath string
|
||||
target string
|
||||
allowFunnel bool
|
||||
proxyProtocol int
|
||||
expected *ipn.ServeConfig
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "add new handler",
|
||||
@@ -2183,7 +2251,7 @@ func TestSetServe(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := e.setServe(tt.cfg, tt.dnsName, tt.srvType, tt.srvPort, tt.mountPath, tt.target, tt.allowFunnel, magicDNSSuffix, nil)
|
||||
err := e.setServe(tt.cfg, tt.dnsName, tt.srvType, tt.srvPort, tt.mountPath, tt.target, tt.allowFunnel, magicDNSSuffix, nil, tt.proxyProtocol)
|
||||
if err != nil && !tt.expectErr {
|
||||
t.Fatalf("got error: %v; did not expect error.", err)
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/pierrec/lz4/v4/internal/lz4errors from github.com/pierrec/lz4/v4+
|
||||
L github.com/pierrec/lz4/v4/internal/lz4stream from github.com/pierrec/lz4/v4
|
||||
L github.com/pierrec/lz4/v4/internal/xxh32 from github.com/pierrec/lz4/v4/internal/lz4stream
|
||||
github.com/pires/go-proxyproto from tailscale.com/ipn/ipnlocal
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
D github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
|
||||
|
||||
@@ -43,6 +43,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink+
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
github.com/pires/go-proxyproto from tailscale.com/ipn/ipnlocal
|
||||
D github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
|
||||
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
|
||||
Reference in New Issue
Block a user