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:
Andrew Dunham
2025-10-20 11:40:30 -04:00
parent 653d0738f9
commit 3a41c0c585
16 changed files with 217 additions and 37 deletions
+72
View File
@@ -33,6 +33,7 @@ import (
"time"
"unicode/utf8"
"github.com/pires/go-proxyproto"
"go4.org/mem"
"tailscale.com/ipn"
"tailscale.com/net/netutil"
@@ -671,10 +672,81 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
})
}
var proxyHeader []byte
if ver := tcph.ProxyProtocol(); ver > 0 {
// backAddr is the final "destination" of the connection,
// which is the connection to the proxied-to backend.
backAddr := backConn.RemoteAddr().(*net.TCPAddr)
// We always want to format the PROXY protocol
// header based on the IPv4 or IPv6-ness of
// the client. The SourceAddr and
// DestinationAddr need to match in type, so we
// need to be careful to not e.g. set a
// SourceAddr of type IPv6 and DestinationAddr
// of type IPv4.
//
// If this is an IPv6-mapped IPv4 address,
// though, unmap it.
proxySrcAddr := srcAddr
if proxySrcAddr.Addr().Is4In6() {
proxySrcAddr = netip.AddrPortFrom(
proxySrcAddr.Addr().Unmap(),
proxySrcAddr.Port(),
)
}
is4 := proxySrcAddr.Addr().Is4()
var destAddr netip.Addr
if self := b.currentNode().Self(); self.Valid() {
if is4 {
destAddr = nodeIP(self, netip.Addr.Is4)
} else {
destAddr = nodeIP(self, netip.Addr.Is6)
}
}
if !destAddr.IsValid() {
// Pick a best-effort destination address of localhost.
if is4 {
destAddr = netip.AddrFrom4([4]byte{127, 0, 0, 1})
} else {
destAddr = netip.IPv6Loopback()
}
}
header := &proxyproto.Header{
Version: byte(ver),
Command: proxyproto.PROXY,
SourceAddr: net.TCPAddrFromAddrPort(proxySrcAddr),
DestinationAddr: &net.TCPAddr{
IP: destAddr.AsSlice(),
Port: backAddr.Port,
},
}
if is4 {
header.TransportProtocol = proxyproto.TCPv4
} else {
header.TransportProtocol = proxyproto.TCPv6
}
var err error
proxyHeader, err = header.Format()
if err != nil {
b.logf("localbackend: failed to format proxy protocol header for port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
}
}
// TODO(bradfitz): do the RegisterIPPortIdentity and
// UnregisterIPPortIdentity stuff that netstack does
errc := make(chan error, 1)
go func() {
if len(proxyHeader) > 0 {
if _, err := backConn.Write(proxyHeader); err != nil {
errc <- err
backConn.Close() // to ensure that the other side gets EOF
return
}
}
_, err := io.Copy(backConn, conn)
errc <- err
}()