cmd/derper,derp: add DERP mesh port isolation

Add support to only allow derp mesh frames
a specific port.

Fixes #26312

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
This commit is contained in:
Mike O'Driscoll 2025-04-01 14:36:47 -04:00
parent 886ab4fad4
commit 34e459ac1e
No known key found for this signature in database
2 changed files with 70 additions and 7 deletions

View File

@ -58,6 +58,7 @@ var (
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)") dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
versionFlag = flag.Bool("version", false, "print version and exit") versionFlag = flag.Bool("version", false, "print version and exit")
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.") addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
meshAddr = flag.String("mesh-addr", "", "optional, Specify a separate http/https listen address for mesh connections, if unset, mesh will use address specified in 'a'")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.") httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.") stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path") configPath = flag.String("c", "", "config file path")
@ -203,6 +204,13 @@ func main() {
s.SetVerifyClientURL(*verifyClientURL) s.SetVerifyClientURL(*verifyClientURL)
s.SetVerifyClientURLFailOpen(*verifyFailOpen) s.SetVerifyClientURLFailOpen(*verifyFailOpen)
s.SetTCPWriteTimeout(*tcpWriteTimeout) s.SetTCPWriteTimeout(*tcpWriteTimeout)
if *meshAddr != "" {
_, port, err := net.SplitHostPort(*meshAddr)
if err != nil {
log.Fatalf("failed to parse mesh port addr %s: %v", *meshAddr, err)
}
s.SetMeshPort(port)
}
var meshKey string var meshKey string
if *dev { if *dev {
@ -345,9 +353,23 @@ func main() {
ReadTimeout: 30 * time.Second, ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second,
} }
var meshSrv *http.Server
if *meshAddr != "" {
// Same as httpsrv above, different port.
meshSrv = &http.Server{
Addr: *meshAddr,
Handler: mux,
ErrorLog: quietLogger,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
}
go func() { go func() {
<-ctx.Done() <-ctx.Done()
httpsrv.Shutdown(ctx) httpsrv.Shutdown(ctx)
if meshSrv != nil {
meshSrv.Shutdown(ctx)
}
}() }()
if serveTLS { if serveTLS {
@ -359,7 +381,7 @@ func main() {
} }
httpsrv.TLSConfig = certManager.TLSConfig() httpsrv.TLSConfig = certManager.TLSConfig()
getCert := httpsrv.TLSConfig.GetCertificate getCert := httpsrv.TLSConfig.GetCertificate
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { certFunc := func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := getCert(hi) cert, err := getCert(hi)
if err != nil { if err != nil {
return nil, err return nil, err
@ -367,9 +389,10 @@ func main() {
cert.Certificate = append(cert.Certificate, s.MetaCert()) cert.Certificate = append(cert.Certificate, s.MetaCert())
return cert, nil return cert, nil
} }
httpsrv.TLSConfig.GetCertificate = certFunc
// Disable TLS 1.0 and 1.1, which are obsolete and have security issues. // Disable TLS 1.0 and 1.1, which are obsolete and have security issues.
httpsrv.TLSConfig.MinVersion = tls.VersionTLS12 httpsrv.TLSConfig.MinVersion = tls.VersionTLS12
httpsrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil { if r.TLS != nil {
label := "unknown" label := "unknown"
switch r.TLS.Version { switch r.TLS.Version {
@ -389,6 +412,17 @@ func main() {
mux.ServeHTTP(w, r) mux.ServeHTTP(w, r)
}) })
httpsrv.Handler = handlerFunc
if meshSrv != nil {
go func() {
meshSrv.TLSConfig = certManager.TLSConfig()
meshSrv.TLSConfig.GetCertificate = certFunc
meshSrv.TLSConfig.MinVersion = tls.VersionTLS12
meshSrv.Handler = handlerFunc
err = rateLimitedListenAndServeTLS(meshSrv, &lc)
}()
}
if *httpPort > -1 { if *httpPort > -1 {
go func() { go func() {
port80mux := http.NewServeMux() port80mux := http.NewServeMux()
@ -420,6 +454,17 @@ func main() {
} }
err = rateLimitedListenAndServeTLS(httpsrv, &lc) err = rateLimitedListenAndServeTLS(httpsrv, &lc)
} else { } else {
if meshSrv != nil {
go func() {
log.Printf("derper: starting mesh on %s", *meshAddr)
var lnm net.Listener
lnm, err = lc.Listen(context.Background(), "tcp", *meshAddr)
if err != nil {
log.Fatal(err)
}
err = meshSrv.Serve(lnm)
}()
}
log.Printf("derper: serving on %s", *addr) log.Printf("derper: serving on %s", *addr)
var ln net.Listener var ln net.Listener
ln, err = lc.Listen(context.Background(), "tcp", httpsrv.Addr) ln, err = lc.Listen(context.Background(), "tcp", httpsrv.Addr)

View File

@ -23,6 +23,7 @@ import (
"math" "math"
"math/big" "math/big"
"math/rand/v2" "math/rand/v2"
"net"
"net/http" "net/http"
"net/netip" "net/netip"
"os" "os"
@ -133,6 +134,7 @@ type Server struct {
logf logger.Logf logf logger.Logf
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish) memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
meshKey string meshKey string
meshPort string
limitedLogf logger.Logf limitedLogf logger.Logf
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
dupPolicy dupPolicy dupPolicy dupPolicy
@ -503,6 +505,10 @@ func (s *Server) SetTCPWriteTimeout(d time.Duration) {
s.tcpWriteTimeout = d s.tcpWriteTimeout = d
} }
func (s *Server) SetMeshPort(port string) {
s.meshPort = port
}
// HasMeshKey reports whether the server is configured with a mesh key. // HasMeshKey reports whether the server is configured with a mesh key.
func (s *Server) HasMeshKey() bool { return s.meshKey != "" } func (s *Server) HasMeshKey() bool { return s.meshKey != "" }
@ -900,6 +906,8 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
return fmt.Errorf("receive client key: %v", err) return fmt.Errorf("receive client key: %v", err)
} }
_, localPort, _ := net.SplitHostPort(nc.LocalAddr().String())
remoteIPPort, _ := netip.ParseAddrPort(remoteAddr) remoteIPPort, _ := netip.ParseAddrPort(remoteAddr)
if err := s.verifyClient(ctx, clientKey, clientInfo, remoteIPPort.Addr()); err != nil { if err := s.verifyClient(ctx, clientKey, clientInfo, remoteIPPort.Addr()); err != nil {
return fmt.Errorf("client %v rejected: %v", clientKey, err) return fmt.Errorf("client %v rejected: %v", clientKey, err)
@ -926,7 +934,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
discoSendQueue: make(chan pkt, s.perClientSendQueueDepth), discoSendQueue: make(chan pkt, s.perClientSendQueueDepth),
sendPongCh: make(chan [8]byte, 1), sendPongCh: make(chan [8]byte, 1),
peerGone: make(chan peerGoneMsg), peerGone: make(chan peerGoneMsg),
canMesh: s.isMeshPeer(clientInfo), canMesh: s.hasMeshPSK(clientInfo) && s.usingMeshPort(localPort),
isNotIdealConn: IdealNodeContextKey.Value(ctx) != "", isNotIdealConn: IdealNodeContextKey.Value(ctx) != "",
peerGoneLim: rate.NewLimiter(rate.Every(time.Second), 3), peerGoneLim: rate.NewLimiter(rate.Every(time.Second), 3),
} }
@ -1331,16 +1339,26 @@ func (c *sclient) requestMeshUpdate() {
} }
} }
// isMeshPeer reports whether the client is a trusted mesh peer // hasMeshPSK reports whether the client provides
// node in the DERP region. // a matching pre-shared key and can be trusted.
func (s *Server) isMeshPeer(info *clientInfo) bool { func (s *Server) hasMeshPSK(info *clientInfo) bool {
return info != nil && info.MeshKey != "" && info.MeshKey == s.meshKey return info != nil && info.MeshKey != "" && info.MeshKey == s.meshKey
} }
// usingMeshPort reports if the given port is
// authorized for mesh connections.
func (s *Server) usingMeshPort(port string) bool {
if s.meshPort == "" {
return true
}
return s.meshPort == port
}
// verifyClient checks whether the client is allowed to connect to the derper, // verifyClient checks whether the client is allowed to connect to the derper,
// depending on how & whether the server's been configured to verify. // depending on how & whether the server's been configured to verify.
func (s *Server) verifyClient(ctx context.Context, clientKey key.NodePublic, info *clientInfo, clientIP netip.Addr) error { func (s *Server) verifyClient(ctx context.Context, clientKey key.NodePublic, info *clientInfo, clientIP netip.Addr) error {
if s.isMeshPeer(info) { if s.hasMeshPSK(info) {
// Trusted mesh peer. No need to verify further. In fact, verifying // Trusted mesh peer. No need to verify further. In fact, verifying
// further wouldn't work: it's not part of the tailnet so tailscaled and // further wouldn't work: it's not part of the tailnet so tailscaled and
// likely the admission control URL wouldn't know about it. // likely the admission control URL wouldn't know about it.