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)")
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.")
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.")
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")
@ -203,6 +204,13 @@ func main() {
s.SetVerifyClientURL(*verifyClientURL)
s.SetVerifyClientURLFailOpen(*verifyFailOpen)
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
if *dev {
@ -345,9 +353,23 @@ func main() {
ReadTimeout: 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() {
<-ctx.Done()
httpsrv.Shutdown(ctx)
if meshSrv != nil {
meshSrv.Shutdown(ctx)
}
}()
if serveTLS {
@ -359,7 +381,7 @@ func main() {
}
httpsrv.TLSConfig = certManager.TLSConfig()
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)
if err != nil {
return nil, err
@ -367,9 +389,10 @@ func main() {
cert.Certificate = append(cert.Certificate, s.MetaCert())
return cert, nil
}
httpsrv.TLSConfig.GetCertificate = certFunc
// Disable TLS 1.0 and 1.1, which are obsolete and have security issues.
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 {
label := "unknown"
switch r.TLS.Version {
@ -389,6 +412,17 @@ func main() {
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 {
go func() {
port80mux := http.NewServeMux()
@ -420,6 +454,17 @@ func main() {
}
err = rateLimitedListenAndServeTLS(httpsrv, &lc)
} 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)
var ln net.Listener
ln, err = lc.Listen(context.Background(), "tcp", httpsrv.Addr)

View File

@ -23,6 +23,7 @@ import (
"math"
"math/big"
"math/rand/v2"
"net"
"net/http"
"net/netip"
"os"
@ -133,6 +134,7 @@ type Server struct {
logf logger.Logf
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
meshKey string
meshPort string
limitedLogf logger.Logf
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
dupPolicy dupPolicy
@ -503,6 +505,10 @@ func (s *Server) SetTCPWriteTimeout(d time.Duration) {
s.tcpWriteTimeout = d
}
func (s *Server) SetMeshPort(port string) {
s.meshPort = port
}
// HasMeshKey reports whether the server is configured with a mesh key.
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)
}
_, localPort, _ := net.SplitHostPort(nc.LocalAddr().String())
remoteIPPort, _ := netip.ParseAddrPort(remoteAddr)
if err := s.verifyClient(ctx, clientKey, clientInfo, remoteIPPort.Addr()); err != nil {
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),
sendPongCh: make(chan [8]byte, 1),
peerGone: make(chan peerGoneMsg),
canMesh: s.isMeshPeer(clientInfo),
canMesh: s.hasMeshPSK(clientInfo) && s.usingMeshPort(localPort),
isNotIdealConn: IdealNodeContextKey.Value(ctx) != "",
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
// node in the DERP region.
func (s *Server) isMeshPeer(info *clientInfo) bool {
// hasMeshPSK reports whether the client provides
// a matching pre-shared key and can be trusted.
func (s *Server) hasMeshPSK(info *clientInfo) bool {
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,
// 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 {
if s.isMeshPeer(info) {
if s.hasMeshPSK(info) {
// 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
// likely the admission control URL wouldn't know about it.