mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-14 06:57:31 +00:00
cmd/derper: fix mesh auth for DERP servers (#16061)
To authenticate mesh keys, the DERP servers used a simple == comparison, which is susceptible to a side channel timing attack. By extracting the mesh key for a DERP server, an attacker could DoS it by forcing disconnects using derp.Client.ClosePeer. They could also enumerate the public Wireguard keys, IP addresses and ports for nodes connected to that DERP server. DERP servers configured without mesh keys deny all such requests. This patch also extracts the mesh key logic into key.DERPMesh, to prevent this from happening again. Security bulletin: https://tailscale.com/security-bulletins#ts-2025-003 Fixes tailscale/corp#28720 Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
@@ -30,7 +30,7 @@ type Client struct {
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
meshKey key.DERPMesh
|
||||
canAckPings bool
|
||||
isProber bool
|
||||
|
||||
@@ -56,7 +56,7 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
MeshKey key.DERPMesh
|
||||
ServerPub key.NodePublic
|
||||
CanAckPings bool
|
||||
IsProber bool
|
||||
@@ -66,7 +66,7 @@ type clientOpt struct {
|
||||
// access to join the mesh.
|
||||
//
|
||||
// An empty key means to not use a mesh key.
|
||||
func MeshKey(key string) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = key }) }
|
||||
func MeshKey(k key.DERPMesh) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = k }) }
|
||||
|
||||
// IsProber returns a ClientOpt to pass to the DERP server during connect to
|
||||
// declare that this client is a a prober.
|
||||
@@ -182,7 +182,7 @@ type clientInfo struct {
|
||||
func (c *Client) sendClientKey() error {
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
MeshKey: c.meshKey.String(),
|
||||
CanAckPings: c.canAckPings,
|
||||
IsProber: c.isProber,
|
||||
})
|
||||
|
@@ -134,7 +134,7 @@ type Server struct {
|
||||
publicKey key.NodePublic
|
||||
logf logger.Logf
|
||||
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
|
||||
meshKey string
|
||||
meshKey key.DERPMesh
|
||||
limitedLogf logger.Logf
|
||||
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
||||
dupPolicy dupPolicy
|
||||
@@ -464,8 +464,13 @@ func genDroppedCounters() {
|
||||
// amongst themselves.
|
||||
//
|
||||
// It must be called before serving begins.
|
||||
func (s *Server) SetMeshKey(v string) {
|
||||
s.meshKey = v
|
||||
func (s *Server) SetMeshKey(v string) error {
|
||||
k, err := key.ParseDERPMesh(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.meshKey = k
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetVerifyClients sets whether this DERP server verifies clients through tailscaled.
|
||||
@@ -506,10 +511,10 @@ func (s *Server) SetTCPWriteTimeout(d time.Duration) {
|
||||
}
|
||||
|
||||
// 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.IsZero() }
|
||||
|
||||
// MeshKey returns the configured mesh key, if any.
|
||||
func (s *Server) MeshKey() string { return s.meshKey }
|
||||
func (s *Server) MeshKey() key.DERPMesh { return s.meshKey }
|
||||
|
||||
// PrivateKey returns the server's private key.
|
||||
func (s *Server) PrivateKey() key.NodePrivate { return s.privateKey }
|
||||
@@ -1355,7 +1360,18 @@ 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 {
|
||||
return info != nil && info.MeshKey != "" && info.MeshKey == s.meshKey
|
||||
// Compare mesh keys in constant time to prevent timing attacks.
|
||||
// Since mesh keys are a fixed length, we don’t need to be concerned
|
||||
// about timing attacks on client mesh keys that are the wrong length.
|
||||
// See https://github.com/tailscale/corp/issues/28720
|
||||
if info == nil || info.MeshKey == "" {
|
||||
return false
|
||||
}
|
||||
k, err := key.ParseDERPMesh(info.MeshKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return s.meshKey.Equal(k)
|
||||
}
|
||||
|
||||
// verifyClient checks whether the client is allowed to connect to the derper,
|
||||
|
@@ -511,11 +511,13 @@ func (ts *testServer) close(t *testing.T) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const testMeshKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
func newTestServer(t *testing.T, ctx context.Context) *testServer {
|
||||
t.Helper()
|
||||
logf := logger.WithPrefix(t.Logf, "derp-server: ")
|
||||
s := NewServer(key.NewNode(), logf)
|
||||
s.SetMeshKey("mesh-key")
|
||||
s.SetMeshKey(testMeshKey)
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -591,8 +593,12 @@ func newRegularClient(t *testing.T, ts *testServer, name string) *testClient {
|
||||
|
||||
func newTestWatcher(t *testing.T, ts *testServer, name string) *testClient {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.NodePrivate, logf logger.Logf) (*Client, error) {
|
||||
mk, err := key.ParseDERPMesh(testMeshKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
c, err := NewClient(priv, nc, brw, logf, MeshKey("mesh-key"))
|
||||
c, err := NewClient(priv, nc, brw, logf, MeshKey(mk))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1627,3 +1633,96 @@ func TestGetPerClientSendQueueDepth(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMeshKey(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
key string
|
||||
want key.DERPMesh
|
||||
wantErr bool
|
||||
}{
|
||||
"clobber": {
|
||||
key: testMeshKey,
|
||||
wantErr: false,
|
||||
},
|
||||
"invalid": {
|
||||
key: "badf00d",
|
||||
wantErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
err := s.SetMeshKey(tt.key)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected err")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
|
||||
want, err := key.ParseDERPMesh(tt.key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !s.meshKey.Equal(want) {
|
||||
t.Fatalf("got %v, want %v", s.meshKey, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsMeshPeer(t *testing.T) {
|
||||
s := &Server{}
|
||||
err := s.SetMeshKey(testMeshKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, tt := range map[string]struct {
|
||||
info *clientInfo
|
||||
want bool
|
||||
wantAllocs float64
|
||||
}{
|
||||
"nil": {
|
||||
info: nil,
|
||||
want: false,
|
||||
wantAllocs: 0,
|
||||
},
|
||||
"empty": {
|
||||
info: &clientInfo{MeshKey: ""},
|
||||
want: false,
|
||||
wantAllocs: 0,
|
||||
},
|
||||
"invalid": {
|
||||
info: &clientInfo{MeshKey: "invalid"},
|
||||
want: false,
|
||||
wantAllocs: 2, // error message
|
||||
},
|
||||
"mismatch": {
|
||||
info: &clientInfo{MeshKey: "0badf00d00000000000000000000000000000000000000000000000000000000"},
|
||||
want: false,
|
||||
wantAllocs: 1,
|
||||
},
|
||||
"match": {
|
||||
info: &clientInfo{MeshKey: testMeshKey},
|
||||
want: true,
|
||||
wantAllocs: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var got bool
|
||||
allocs := testing.AllocsPerRun(1, func() {
|
||||
got = s.isMeshPeer(tt.info)
|
||||
})
|
||||
if got != tt.want {
|
||||
t.Fatalf("got %t, want %t: info = %#v", got, tt.want, tt.info)
|
||||
}
|
||||
|
||||
if allocs != tt.wantAllocs && tt.want {
|
||||
t.Errorf("%f allocations, want %f", allocs, tt.wantAllocs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -57,7 +57,7 @@ type Client struct {
|
||||
TLSConfig *tls.Config // optional; nil means default
|
||||
HealthTracker *health.Tracker // optional; used if non-nil only
|
||||
DNSCache *dnscache.Resolver // optional; nil means no caching
|
||||
MeshKey string // optional; for trusted clients
|
||||
MeshKey key.DERPMesh // optional; for trusted clients
|
||||
IsProber bool // optional; for probers to optional declare themselves as such
|
||||
|
||||
// WatchConnectionChanges is whether the client wishes to subscribe to
|
||||
|
@@ -212,6 +212,8 @@ func TestPing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
const testMeshKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
func newTestServer(t *testing.T, k key.NodePrivate) (serverURL string, s *derp.Server) {
|
||||
s = derp.NewServer(k, t.Logf)
|
||||
httpsrv := &http.Server{
|
||||
@@ -224,7 +226,7 @@ func newTestServer(t *testing.T, k key.NodePrivate) (serverURL string, s *derp.S
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverURL = "http://" + ln.Addr().String()
|
||||
s.SetMeshKey("1234")
|
||||
s.SetMeshKey(testMeshKey)
|
||||
|
||||
go func() {
|
||||
if err := httpsrv.Serve(ln); err != nil {
|
||||
@@ -243,7 +245,11 @@ func newWatcherClient(t *testing.T, watcherPrivateKey key.NodePrivate, serverToW
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.MeshKey = "1234"
|
||||
k, err := key.ParseDERPMesh(testMeshKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.MeshKey = k
|
||||
return
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user