mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 23:29:08 +02:00
199 lines
6.6 KiB
Go
199 lines
6.6 KiB
Go
//go:build !js && !ios && !android
|
|
|
|
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// runHandshake drives both halves over an in-memory pipe and returns what each
|
|
// side concluded.
|
|
func runHandshake(t *testing.T, daemonToken, agentToken []byte, grant agentGrant) (daemonErr error, got agentGrant, agentErr error) {
|
|
t.Helper()
|
|
return runHandshakeOver(t, daemonToken, agentToken, grant, func(c net.Conn) net.Conn { return c })
|
|
}
|
|
|
|
// runHandshakeOver is runHandshake with the daemon's end of the connection
|
|
// wrapped, so a test can interfere with what the daemon sends.
|
|
func runHandshakeOver(t *testing.T, daemonToken, agentToken []byte, grant agentGrant, wrap func(net.Conn) net.Conn) (daemonErr error, got agentGrant, agentErr error) {
|
|
t.Helper()
|
|
|
|
daemonSide, agentSide := net.Pipe()
|
|
t.Cleanup(func() {
|
|
_ = daemonSide.Close()
|
|
_ = agentSide.Close()
|
|
})
|
|
|
|
type agentResult struct {
|
|
grant agentGrant
|
|
err error
|
|
}
|
|
agentDone := make(chan agentResult, 1)
|
|
go func() {
|
|
g, err := agentServerHandshake(agentSide, agentToken)
|
|
if err != nil {
|
|
// What the agent's caller does on rejection, so the daemon sees the
|
|
// close rather than waiting out its own deadline.
|
|
_ = agentSide.Close()
|
|
}
|
|
agentDone <- agentResult{g, err}
|
|
}()
|
|
|
|
daemonErr = agentClientHandshake(wrap(daemonSide), daemonToken, grant)
|
|
res := <-agentDone
|
|
return daemonErr, res.grant, res.err
|
|
}
|
|
|
|
func TestAgentHandshake_MatchingTokens(t *testing.T) {
|
|
token := bytes.Repeat([]byte{0xA5}, agentTokenLen)
|
|
|
|
for _, grant := range []agentGrant{
|
|
{viewOnly: false, peerAddr: "100.64.0.7:51234"},
|
|
{viewOnly: true, peerAddr: "[fd00:1234::2]:5900"},
|
|
{viewOnly: false, peerAddr: ""},
|
|
} {
|
|
dErr, got, aErr := runHandshake(t, token, token, grant)
|
|
require.NoError(t, dErr)
|
|
require.NoError(t, aErr)
|
|
assert.Equal(t, grant, got, "the agent must see the grant the daemon authenticated")
|
|
}
|
|
}
|
|
|
|
// The peer address is covered by the daemon's tag, so rewriting it in flight
|
|
// fails the handshake instead of misattributing the session.
|
|
func TestAgentHandshake_TamperedPeerAddrIsRefused(t *testing.T) {
|
|
token := bytes.Repeat([]byte{0x5A}, agentTokenLen)
|
|
grant := agentGrant{peerAddr: "100.64.0.7:51234"}
|
|
|
|
_, _, aErr := runHandshakeOver(t, token, token, grant, func(c net.Conn) net.Conn {
|
|
return &rewriteConn{Conn: c, from: []byte("100.64.0.7"), to: []byte("100.64.0.9")}
|
|
})
|
|
require.Error(t, aErr)
|
|
assert.Contains(t, aErr.Error(), "did not prove it holds the session token")
|
|
}
|
|
|
|
func TestAgentHandshake_OversizedPeerAddrIsRefused(t *testing.T) {
|
|
token := bytes.Repeat([]byte{0x6B}, agentTokenLen)
|
|
daemonSide, agentSide := net.Pipe()
|
|
defer daemonSide.Close()
|
|
defer agentSide.Close()
|
|
|
|
err := agentClientHandshake(daemonSide, token, agentGrant{peerAddr: string(bytes.Repeat([]byte{'a'}, maxAgentPeerAddrLen+1))})
|
|
require.Error(t, err, "an address longer than the length byte can carry must not be truncated silently")
|
|
}
|
|
|
|
// The point of the exchange: an impostor listening on the socket without the
|
|
// token cannot complete it, and the daemon refuses before proxying anything.
|
|
func TestAgentHandshake_ImpostorAgentIsRefused(t *testing.T) {
|
|
daemonToken := bytes.Repeat([]byte{0x01}, agentTokenLen)
|
|
impostorToken := bytes.Repeat([]byte{0x02}, agentTokenLen)
|
|
|
|
dErr, _, aErr := runHandshake(t, daemonToken, impostorToken, agentGrant{})
|
|
require.Error(t, aErr, "the impostor cannot verify the daemon's tag")
|
|
require.Error(t, dErr, "the daemon must not proceed against an unproven peer")
|
|
}
|
|
|
|
// And the other direction: something dialling the agent without the token gets
|
|
// nowhere either.
|
|
func TestAgentHandshake_ImpostorDaemonIsRefused(t *testing.T) {
|
|
agentToken := bytes.Repeat([]byte{0x03}, agentTokenLen)
|
|
impostorToken := bytes.Repeat([]byte{0x04}, agentTokenLen)
|
|
|
|
_, _, aErr := runHandshake(t, impostorToken, agentToken, agentGrant{})
|
|
require.Error(t, aErr)
|
|
assert.Contains(t, aErr.Error(), "did not prove it holds the session token")
|
|
}
|
|
|
|
// The token itself must never appear on the wire; that was the whole reason for
|
|
// replacing the plain preamble.
|
|
func TestAgentHandshake_TokenNeverSent(t *testing.T) {
|
|
token := bytes.Repeat([]byte{0x7E}, agentTokenLen)
|
|
|
|
daemonSide, agentSide := net.Pipe()
|
|
defer daemonSide.Close()
|
|
defer agentSide.Close()
|
|
|
|
// Tee both directions on the agent's end: what it reads is everything the
|
|
// daemon sent, what it writes is its challenge and reply. Either side
|
|
// leaking the token is the failure this test is for.
|
|
var wire bytes.Buffer
|
|
var mu sync.Mutex
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
_, _ = agentServerHandshake(&teeConn{Conn: agentSide, mu: &mu, read: &wire, written: &wire}, token)
|
|
}()
|
|
|
|
require.NoError(t, agentClientHandshake(daemonSide, token, agentGrant{}))
|
|
<-done
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
// bytes.Contains, not assert.NotContains: testify compares a []byte
|
|
// haystack element-wise, and a []byte is never an element of a []byte, so
|
|
// the assertion held whatever crossed the wire — including the whole token.
|
|
assert.False(t, bytes.Contains(wire.Bytes(), token), "the token must not cross the socket")
|
|
}
|
|
|
|
// A tag is bound to the nonce it answered, so replaying one against a fresh
|
|
// challenge fails.
|
|
func TestAgentMAC_IsBoundToNonceAndLabel(t *testing.T) {
|
|
token := bytes.Repeat([]byte{0x11}, agentTokenLen)
|
|
nonceA := bytes.Repeat([]byte{0x22}, agentNonceLen)
|
|
nonceB := bytes.Repeat([]byte{0x33}, agentNonceLen)
|
|
|
|
assert.NotEqual(t,
|
|
agentMAC(token, agentDaemonLabel, nonceA),
|
|
agentMAC(token, agentDaemonLabel, nonceB),
|
|
"a different challenge must produce a different tag")
|
|
|
|
assert.NotEqual(t,
|
|
agentMAC(token, agentDaemonLabel, nonceA),
|
|
agentMAC(token, agentAgentLabel, nonceA),
|
|
"the two directions must not share a tag, or one could be replayed as the other")
|
|
}
|
|
|
|
// teeConn records every byte read from and written to the wrapped connection.
|
|
type teeConn struct {
|
|
net.Conn
|
|
mu *sync.Mutex
|
|
read *bytes.Buffer
|
|
written *bytes.Buffer
|
|
}
|
|
|
|
func (c *teeConn) Read(b []byte) (int, error) {
|
|
n, err := c.Conn.Read(b)
|
|
if n > 0 && c.read != nil {
|
|
c.mu.Lock()
|
|
c.read.Write(b[:n])
|
|
c.mu.Unlock()
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
func (c *teeConn) Write(b []byte) (int, error) {
|
|
n, err := c.Conn.Write(b)
|
|
if n > 0 && c.written != nil {
|
|
c.mu.Lock()
|
|
c.written.Write(b[:n])
|
|
c.mu.Unlock()
|
|
}
|
|
return n, err
|
|
}
|
|
|
|
// rewriteConn replaces the first occurrence of from with to in what is written,
|
|
// standing in for something on the socket altering the daemon's bytes.
|
|
type rewriteConn struct {
|
|
net.Conn
|
|
from, to []byte
|
|
}
|
|
|
|
func (c *rewriteConn) Write(b []byte) (int, error) {
|
|
return c.Conn.Write(bytes.Replace(b, c.from, c.to, 1))
|
|
}
|