Merge remote-tracking branch 'origin/master' into fips140

This commit is contained in:
Wade Simmons
2026-06-18 12:07:19 -04:00
20 changed files with 936 additions and 107 deletions
+3
View File
@@ -28,6 +28,9 @@ jobs:
- name: Smoke Docker
run: make smoke-docker-race
- name: Smoke Docker IPv6 overlay
run: make smoke-docker-ipv6
- name: Smoke Relay Docker
run: make smoke-relay-docker
+20 -7
View File
@@ -5,6 +5,19 @@ set -e -x
rm -rf ./build
mkdir ./build
if [ "$SMOKE_OVERLAY_IPV6" ]
then
LIGHTHOUSE_NIP="fd00:4242:0:0:0:ffff:c0a8:6401"
HOST2_NIP="fd00:4242:0:0:0:ffff:c0a8:6402"
HOST3_NIP="fd00:4242:0:0:0:ffff:c0a8:6403"
HOST4_NIP="fd00:4242:0:0:0:ffff:c0a8:6404"
else
LIGHTHOUSE_NIP="192.168.100.1"
HOST2_NIP="192.168.100.2"
HOST3_NIP="192.168.100.3"
HOST4_NIP="192.168.100.4"
fi
# Smoke containers run on a dedicated docker network whose subnet is allocated
# at smoke time, not known at build time. Configs are written with TEST-NET-3
# placeholder IPs (RFC 5737) and smoke.sh / smoke-vagrant.sh / smoke-relay.sh
@@ -31,24 +44,24 @@ LIGHTHOUSE_IP="203.0.113.2"
../genconfig.sh >lighthouse1.yml
HOST="host2" \
LIGHTHOUSES="192.168.100.1 $LIGHTHOUSE_IP:4242" \
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
../genconfig.sh >host2.yml
HOST="host3" \
LIGHTHOUSES="192.168.100.1 $LIGHTHOUSE_IP:4242" \
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
INBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
../genconfig.sh >host3.yml
HOST="host4" \
LIGHTHOUSES="192.168.100.1 $LIGHTHOUSE_IP:4242" \
LIGHTHOUSES="$LIGHTHOUSE_NIP $LIGHTHOUSE_IP:4242" \
OUTBOUND='[{"port": "any", "proto": "icmp", "group": "lighthouse"}]' \
../genconfig.sh >host4.yml
../../../../nebula-cert ca -curve "${CURVE:-25519}" -name "Smoke Test"
../../../../nebula-cert sign -name "lighthouse1" -groups "lighthouse,lighthouse1" -ip "192.168.100.1/24"
../../../../nebula-cert sign -name "host2" -groups "host,host2" -ip "192.168.100.2/24"
../../../../nebula-cert sign -name "host3" -groups "host,host3" -ip "192.168.100.3/24"
../../../../nebula-cert sign -name "host4" -groups "host,host4" -ip "192.168.100.4/24"
../../../../nebula-cert sign -name "lighthouse1" -groups "lighthouse,lighthouse1" -ip "$LIGHTHOUSE_NIP/24"
../../../../nebula-cert sign -name "host2" -groups "host,host2" -ip "$HOST2_NIP/24"
../../../../nebula-cert sign -name "host3" -groups "host,host3" -ip "$HOST3_NIP/24"
../../../../nebula-cert sign -name "host4" -groups "host,host4" -ip "$HOST4_NIP/24"
)
docker build -t "nebula:${NAME:-smoke}" .
+36 -23
View File
@@ -47,6 +47,19 @@ HOST2_IP="$PREFIX.3"
HOST3_IP="$PREFIX.4"
HOST4_IP="$PREFIX.5"
if [ "$SMOKE_OVERLAY_IPV6" ]
then
LIGHTHOUSE_NIP="fd00:4242:0:0:0:ffff:c0a8:6401"
HOST2_NIP="fd00:4242:0:0:0:ffff:c0a8:6402"
HOST3_NIP="fd00:4242:0:0:0:ffff:c0a8:6403"
HOST4_NIP="fd00:4242:0:0:0:ffff:c0a8:6404"
else
LIGHTHOUSE_NIP="192.168.100.1"
HOST2_NIP="192.168.100.2"
HOST3_NIP="192.168.100.3"
HOST4_NIP="192.168.100.4"
fi
# Sed the placeholder TEST-NET-3 IPs in the host configs to the real ones.
# build/lighthouse1.yml has no IPs to rewrite so it's skipped.
for f in build/host2.yml build/host3.yml build/host4.yml; do
@@ -80,28 +93,28 @@ docker exec host3 tcpdump -i eth0 -q -w - -U 2>logs/host3.outside.log >logs/host
docker exec host4 tcpdump -i tun0 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap &
docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap &
docker exec host2 ncat -nklv 0.0.0.0 2000 &
docker exec host3 ncat -nklv 0.0.0.0 2000 &
docker exec host4 ncat -e '/usr/bin/echo helloagainfromhost4' -nkluv 0.0.0.0 4000 &
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 &
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 &
docker exec host2 ncat -nklv 2000 &
docker exec host3 ncat -nklv 2000 &
docker exec host4 ncat -e '/usr/bin/echo helloagainfromhost4' -nkluv 4000 &
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 3000 &
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 3000 &
set +x
echo
echo " *** Testing ping from lighthouse1"
echo
set -x
docker exec lighthouse1 ping -c1 192.168.100.2
docker exec lighthouse1 ping -c1 192.168.100.3
docker exec lighthouse1 ping -c1 $HOST2_NIP
docker exec lighthouse1 ping -c1 $HOST3_NIP
set +x
echo
echo " *** Testing ping from host2"
echo
set -x
docker exec host2 ping -c1 192.168.100.1
docker exec host2 ping -c1 $LIGHTHOUSE_NIP
# Should fail because not allowed by host3 inbound firewall
! docker exec host2 ping -c1 192.168.100.3 -w5 || exit 1
! docker exec host2 ping -c1 $HOST3_NIP -w5 || exit 1
set +x
echo
@@ -109,34 +122,34 @@ echo " *** Testing ncat from host2"
echo
set -x
# Should fail because not allowed by host3 inbound firewall
! docker exec host2 ncat -nzv -w5 192.168.100.3 2000 || exit 1
! docker exec host2 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
! docker exec host2 ncat -nzv -w5 $HOST3_NIP 2000 || exit 1
! docker exec host2 ncat -nzuv -w5 $HOST3_NIP 3000 | grep -q host3 || exit 1
set +x
echo
echo " *** Testing ping from host3"
echo
set -x
docker exec host3 ping -c1 192.168.100.1
docker exec host3 ping -c1 192.168.100.2
docker exec host3 ping -c1 $LIGHTHOUSE_NIP
docker exec host3 ping -c1 $HOST2_NIP
set +x
echo
echo " *** Testing ncat from host3"
echo
set -x
docker exec host3 ncat -nzv -w5 192.168.100.2 2000
docker exec host3 ncat -nzuv -w5 192.168.100.2 3000 | grep -q host2
docker exec host3 ncat -nzv -w5 $HOST2_NIP 2000
docker exec host3 ncat -nzuv -w5 $HOST2_NIP 3000 | grep -q host2
set +x
echo
echo " *** Testing ping from host4"
echo
set -x
docker exec host4 ping -c1 192.168.100.1
docker exec host4 ping -c1 $LIGHTHOUSE_NIP
# Should fail because not allowed by host4 outbound firewall
! docker exec host4 ping -c1 192.168.100.2 -w5 || exit 1
! docker exec host4 ping -c1 192.168.100.3 -w5 || exit 1
! docker exec host4 ping -c1 $HOST2_NIP -w5 || exit 1
! docker exec host4 ping -c1 $HOST3_NIP -w5 || exit 1
set +x
echo
@@ -144,10 +157,10 @@ echo " *** Testing ncat from host4"
echo
set -x
# Should fail because not allowed by host4 outbound firewall
! docker exec host4 ncat -nzv -w5 192.168.100.2 2000 || exit 1
! docker exec host4 ncat -nzv -w5 192.168.100.3 2000 || exit 1
! docker exec host4 ncat -nzuv -w5 192.168.100.2 3000 | grep -q host2 || exit 1
! docker exec host4 ncat -nzuv -w5 192.168.100.3 3000 | grep -q host3 || exit 1
! docker exec host4 ncat -nzv -w5 $HOST2_NIP 2000 || exit 1
! docker exec host4 ncat -nzv -w5 $HOST3_NIP 2000 || exit 1
! docker exec host4 ncat -nzuv -w5 $HOST2_NIP 3000 | grep -q host2 || exit 1
! docker exec host4 ncat -nzuv -w5 $HOST3_NIP 3000 | grep -q host3 || exit 1
set +x
echo
@@ -159,7 +172,7 @@ set -x
# cannot initiate UDP to host2. Once host2 initiates a flow to host4:4000,
# conntrack must let host4's listener reply on that flow. If it doesn't,
# the echo back from host4 never reaches host2.
docker exec host2 sh -c "(/usr/bin/echo host2; sleep 2) | ncat -nuv 192.168.100.4 4000" | grep -q helloagainfromhost4
docker exec host2 sh -c "(/usr/bin/echo host2; sleep 2) | ncat -nuv $HOST4_NIP 4000" | grep -q helloagainfromhost4
docker exec host4 sh -c 'kill 1'
docker exec host3 sh -c 'kill 1'
+3
View File
@@ -328,6 +328,9 @@ smoke-relay-docker: bin-docker
cd .github/workflows/smoke/ && $(GOENV) ./build-relay.sh
cd .github/workflows/smoke/ && $(GOENV) ./smoke-relay.sh
smoke-docker-ipv6: export SMOKE_OVERLAY_IPV6 = 1
smoke-docker-ipv6: smoke-docker
smoke-docker-race: BUILD_ARGS += -race
smoke-docker-race: GOENV += CGO_ENABLED=1
smoke-docker-race: smoke-docker
+2 -10
View File
@@ -136,14 +136,6 @@ func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time)
return in, out
}
// AddTrafficWatch must be called for every new HostInfo.
// We will continue to monitor the HostInfo until the tunnel is dropped.
func (cm *connectionManager) AddTrafficWatch(h *HostInfo) {
if h.out.Swap(true) == false {
cm.trafficTimer.Add(h.localIndexId, cm.checkInterval)
}
}
func (cm *connectionManager) Start(ctx context.Context) {
clockSource := time.NewTicker(cm.trafficTimer.t.tickDuration)
defer clockSource.Stop()
@@ -306,8 +298,8 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
} else {
cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, make([]byte, 12), make([]byte, mtu))
cm.l.Info("send CreateRelayRequest",
"relayFrom", req.RelayFromAddr,
"relayTo", req.RelayToAddr,
"relayFrom", relayFrom,
"relayTo", relayTo,
"initiatorRelayIndex", req.InitiatorRelayIndex,
"responderRelayIndex", req.ResponderRelayIndex,
"vpnAddrs", newhostinfo.vpnAddrs,
+95
View File
@@ -15,6 +15,7 @@ import (
"github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/udp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
@@ -373,6 +374,100 @@ func TestCrossStackRelaysWork(t *testing.T) {
//relayControl.Stop()
}
// TestRelayReplayProtection asserts that a relay (forwarding-type) node rejects
// replayed relay frames. A captured relay frame, re-injected with the same
// message counter, must be dropped by the replay window rather than re-forwarded
// to the relay target. Before the fix, handleOutsideRelayPacket authenticated the
// frame but never advanced the replay window, so every replay was re-forwarded.
func TestRelayReplayProtection(t *testing.T) {
t.Parallel()
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version2, ca, caKey, "me ", "10.128.0.1/24,fc00::1/64", m{"relay": m{"use_relays": true}})
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "relay ", "10.128.0.128/24,fc00::128/64", m{"relay": m{"am_relay": true}})
theirUdp := netip.MustParseAddrPort("10.0.0.2:4242")
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServerWithUdp(cert.Version2, ca, caKey, "them ", "fc00::2/64", theirUdp, m{"relay": m{"use_relays": true}})
myVpnV6 := myVpnIpNet[1]
relayVpnV4 := relayVpnIpNet[0]
relayVpnV6 := relayVpnIpNet[1]
theirVpnV6 := theirVpnIpNet[0]
// Teach me how to reach the relay and that them is reachable via the relay
myControl.InjectLightHouseAddr(relayVpnV4.Addr(), relayUdpAddr)
myControl.InjectLightHouseAddr(relayVpnV6.Addr(), relayUdpAddr)
myControl.InjectRelays(theirVpnV6.Addr(), []netip.Addr{relayVpnV6.Addr()})
relayControl.InjectLightHouseAddr(theirVpnV6.Addr(), theirUdpAddr)
r := router.NewR(t, myControl, relayControl, theirControl)
defer r.RenderFlow()
myControl.Start()
relayControl.Start()
theirControl.Start()
// Establish the relayed tunnel in both directions so all handshakes complete.
t.Log("Establish the relayed tunnel")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnV6.Addr(), 80, myVpnV6.Addr(), 80, []byte("Hi from me")))
p := r.RouteForAllUntilTxTun(theirControl)
assertUdpPacket(t, []byte("Hi from me"), p, myVpnV6.Addr(), theirVpnV6.Addr(), 80, 80)
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnV6.Addr(), 80, theirVpnV6.Addr(), 80, []byte("Hi from them")))
p = r.RouteForAllUntilTxTun(myControl)
assertUdpPacket(t, []byte("Hi from them"), p, theirVpnV6.Addr(), myVpnV6.Addr(), 80, 80)
// Drain anything still queued on me's UDP tx so the next packet we pull is the
// relay frame we are about to generate.
for myControl.GetFromUDP(false) != nil {
}
// Capture a single legitimate relay frame that me transmits toward the relay.
t.Log("Capture a relay frame from me -> relay")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnV6.Addr(), 80, myVpnV6.Addr(), 80, []byte("replay me")))
relayFrame := myControl.GetFromUDP(true)
require.Equal(t, relayUdpAddr, relayFrame.To, "captured frame should be addressed to the relay")
var fh header.H
require.NoError(t, fh.Parse(relayFrame.Data))
require.Equal(t, header.Message, fh.Type)
require.Equal(t, header.MessageRelay, fh.Subtype)
// drainForwards counts relay frames the relay forwards toward them within the
// settle window. We match on destination + (Message, MessageRelay) so the
// relay's own direct traffic to them can't be miscounted.
drainForwards := func(settle time.Duration) int {
ch := relayControl.GetUDPTxChan()
count := 0
for {
select {
case pkt := <-ch:
var ph header.H
if pkt.To == theirUdpAddr && ph.Parse(pkt.Data) == nil &&
ph.Type == header.Message && ph.Subtype == header.MessageRelay {
count++
}
pkt.Release()
case <-time.After(settle):
return count
}
}
}
// First delivery of the captured frame: the relay should forward it once.
t.Log("Deliver the captured frame once; relay forwards it to them")
relayControl.InjectUDPPacket(relayFrame)
require.Equal(t, 1, drainForwards(200*time.Millisecond), "relay should forward the first, legitimate copy")
// Replay the exact same frame several times. A correct replay window rejects
// these duplicates so the relay forwards none of them.
t.Log("Replay the captured frame; relay must drop the duplicates")
const replays = 3
for i := 0; i < replays; i++ {
relayControl.InjectUDPPacket(relayFrame)
}
forwarded := drainForwards(200 * time.Millisecond)
assert.Equal(t, 0, forwarded, "relay re-forwarded %d/%d replayed relay frames; replay protection is ineffective on relay tunnels", forwarded, replays)
r.RenderHostmaps("Final hostmaps", myControl, relayControl, theirControl)
}
func TestCloseTunnelAuthenticated(t *testing.T) {
t.Parallel()
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
+1 -1
View File
@@ -397,7 +397,7 @@ firewall:
# `drop` (default): silently drop the packet.
# `reject`: send a reject reply.
# - For TCP, this will be a RST "Connection Reset" packet.
# - For other protocols, this will be an ICMP port unreachable packet.
# - For other protocols, this will be an ICMP "Destination unreachable: Communication administratively prohibited" packet.
outbound_action: drop
inbound_action: drop
+6 -6
View File
@@ -9,7 +9,7 @@ require (
github.com/armon/go-radix v1.0.0
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432
github.com/flynn/noise v1.1.0
github.com/gaissmai/bart v0.27.1
github.com/gaissmai/bart v0.28.0
github.com/gogo/protobuf v1.3.2
github.com/google/gopacket v1.1.19
github.com/kardianos/service v1.2.4
@@ -24,12 +24,12 @@ require (
github.com/vishvananda/netlink v1.3.1
go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.51.0
golang.org/x/crypto v0.53.0
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
golang.org/x/net v0.54.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.44.0
golang.org/x/term v0.43.0
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.0
golang.org/x/term v0.44.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
golang.zx2c4.com/wireguard/windows v0.6.1
+12 -12
View File
@@ -26,8 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
github.com/gaissmai/bart v0.27.1 h1:FysPzqETMJa8q9rNkLW5peT1hq25nLOz8ksHbSVoiAk=
github.com/gaissmai/bart v0.27.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/gaissmai/bart v0.28.0 h1:89yZLo8NmyqD0RYgJ3QO9HhqqGGw+oWhf90cZm69Lko=
github.com/gaissmai/bart v0.28.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -162,8 +162,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -182,8 +182,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -191,8 +191,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -208,11 +208,11 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+1
View File
@@ -13,6 +13,7 @@ var (
ErrUnknownSubtype = errors.New("unknown handshake subtype")
ErrMissingContent = errors.New("expected handshake content but message was empty")
ErrUnexpectedContent = errors.New("received unexpected handshake content")
ErrInvalidRemoteIndex = errors.New("peer sent an invalid index in handshake payload")
ErrIndexAllocation = errors.New("failed to allocate local index")
ErrNoCredential = errors.New("no handshake credential available for cert version")
ErrAsymmetricCipherKeys = errors.New("noise produced only one cipher key")
+10 -2
View File
@@ -312,11 +312,19 @@ func (m *Machine) processPayload(msg []byte, flags msgFlags) error {
// Process payload
if flags.expectsPayload {
var remoteIndex uint32
if m.result.Initiator {
m.result.RemoteIndex = payload.ResponderIndex
remoteIndex = payload.ResponderIndex
} else {
m.result.RemoteIndex = payload.InitiatorIndex
remoteIndex = payload.InitiatorIndex
}
// The payload presence check above can be satisfied by Time alone, so a payload
// could still carry a zero index here. We need to reject it.
if remoteIndex == 0 {
m.failed = true
return ErrInvalidRemoteIndex
}
m.result.RemoteIndex = remoteIndex
m.result.HandshakeTime = payload.Time
m.payloadSet = true
}
+18
View File
@@ -229,6 +229,24 @@ func TestMachineProcessPayload(t *testing.T) {
require.ErrorIs(t, err, ErrUnexpectedContent)
assert.True(t, m.Failed())
})
t.Run("zero initiator index on responder is fatal", func(t *testing.T) {
m := newTestMachine(t, cs, v, false, 100)
bytes := MarshalPayload(nil, Payload{InitiatorIndex: 0, Time: 1})
err := m.processPayload(bytes, msgFlags{expectsPayload: true})
require.ErrorIs(t, err, ErrInvalidRemoteIndex)
assert.True(t, m.Failed())
assert.Zero(t, m.result.RemoteIndex)
})
t.Run("zero responder index on initiator is fatal", func(t *testing.T) {
m := newTestMachine(t, cs, v, true, 100)
bytes := MarshalPayload(nil, Payload{InitiatorIndex: 100, ResponderIndex: 0, Time: 1})
err := m.processPayload(bytes, msgFlags{expectsPayload: true})
require.ErrorIs(t, err, ErrInvalidRemoteIndex)
assert.True(t, m.Failed())
assert.Zero(t, m.result.RemoteIndex)
})
}
// TestMachineRequireComplete checks the fail-on-incomplete-handshake path
-2
View File
@@ -796,7 +796,6 @@ func (hm *HandshakeManager) beginHandshake(via ViaSender, packet []byte, h *head
}
hm.sendHandshakeResponse(via, response, hostinfo, false)
f.connectionManager.AddTrafficWatch(hostinfo)
hostinfo.remotes.RefreshFromHandshake(vpnAddrs)
// Don't wait for UpdateWorker
@@ -963,7 +962,6 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
hostinfo.buildNetworks(f.myVpnNetworksTable, remoteCert.Certificate)
hm.Complete(hostinfo, f)
f.connectionManager.AddTrafficWatch(hostinfo)
if len(hh.packetStore) > 0 {
if f.l.Enabled(context.Background(), slog.LevelDebug) {
+6 -1
View File
@@ -138,9 +138,9 @@ func (rs *RelayState) InsertRelayTo(ip netip.Addr) {
}
func (rs *RelayState) CopyRelayIps() []netip.Addr {
ret := make([]netip.Addr, len(rs.relays))
rs.RLock()
defer rs.RUnlock()
ret := make([]netip.Addr, len(rs.relays))
copy(ret, rs.relays)
return ret
}
@@ -623,6 +623,11 @@ func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
hm.Indexes[hostinfo.localIndexId] = hostinfo
hm.RemoteIndexes[hostinfo.remoteIndexId] = hostinfo
hostinfo.out.Store(true)
if f.connectionManager != nil { // f.connectionManager is only nil in some unit tests
f.connectionManager.trafficTimer.Add(hostinfo.localIndexId, f.connectionManager.checkInterval)
}
if hm.l.Enabled(context.Background(), slog.LevelDebug) {
hm.l.Debug("Hostmap vpnIp added",
"hostMap", m{"vpnAddrs": hostinfo.vpnAddrs, "mapTotalSize": len(hm.Hosts),
+290 -19
View File
@@ -4,26 +4,54 @@ import (
"encoding/binary"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
const (
// Need 96 bytes for the largest reject packet:
// MaxIPv4RejectPacketSize is the largest IPv4 reject packet:
// - 20 byte ipv4 header
// - 8 byte icmpv4 header
// - 68 byte body (60 byte max orig ipv4 header + 8 byte orig icmpv4 header)
MaxRejectPacketSize = ipv4.HeaderLen + 8 + 60 + 8
maxIPv4RejectPacketSize = ipv4.HeaderLen + 8 + 60 + 8
// MaxRejectPacketSize is sized for the largest possible reject packet (IPv6):
// - 40 byte ipv6 header
// - 8 byte icmpv6 header
// - up to 1000 byte body (original packet, possibly truncated. We want to stay
// under the MTU with Nebula overhead included)
maxIPv6RejectPacketSize = ipv6.HeaderLen + 8 + 1000
MaxRejectPacketSize = maxIPv6RejectPacketSize
)
func CreateRejectPacket(packet []byte, out []byte) []byte {
if len(packet) < ipv4.HeaderLen || int(packet[0]>>4) != ipv4.Version {
if len(packet) < 1 {
return nil
}
switch packet[9] {
case 6: // tcp
return ipv4CreateRejectTCPPacket(packet, out)
version := int(packet[0] >> 4)
switch version {
case ipv4.Version:
if len(packet) < ipv4.HeaderLen {
return nil
}
// Do not send reject packets for non-first fragments
if packet[6]&0x1f != 0 || packet[7] != 0 {
return nil
}
switch packet[9] {
case 6: // tcp
return ipv4CreateRejectTCPPacket(packet, out)
default:
return ipv4CreateRejectICMPPacket(packet, out)
}
case ipv6.Version:
if len(packet) < ipv6.HeaderLen {
return nil
}
return ipv6CreateRejectPacket(packet, out)
default:
return ipv4CreateRejectICMPPacket(packet, out)
return nil
}
}
@@ -35,12 +63,17 @@ func ipv4CreateRejectICMPPacket(packet []byte, out []byte) []byte {
return nil
}
// ICMP reply includes original header and first 8 bytes of the packet
packetLen := len(packet)
if packetLen > ihl+8 {
packetLen = ihl + 8
// Do not generate ICMP errors in response to ICMP error packets
if packet[9] == 1 && len(packet) > ihl {
icmpType := packet[ihl]
if icmpType == 3 || icmpType == 4 || icmpType == 5 || icmpType == 11 || icmpType == 12 {
return nil
}
}
// ICMP reply includes original header and first 8 bytes of the packet
packetLen := min(len(packet), ihl+8)
outLen := ipv4.HeaderLen + 8 + packetLen
if outLen > cap(out) {
return nil
@@ -71,14 +104,14 @@ func ipv4CreateRejectICMPPacket(packet []byte, out []byte) []byte {
// ICMP Destination Unreachable
icmpOut := out[ipv4.HeaderLen:]
icmpOut[0] = 3 // type (Destination unreachable)
icmpOut[1] = 3 // code (Port unreachable error)
icmpOut[2] = 0 // checksum
icmpOut[3] = 0 // .
icmpOut[4] = 0 // unused
icmpOut[5] = 0 // .
icmpOut[6] = 0 // .
icmpOut[7] = 0 // .
icmpOut[0] = 3 // type (Destination unreachable)
icmpOut[1] = 13 // code (Communication administratively prohibited)
icmpOut[2] = 0 // checksum
icmpOut[3] = 0 // .
icmpOut[4] = 0 // unused
icmpOut[5] = 0 // .
icmpOut[6] = 0 // .
icmpOut[7] = 0 // .
// Copy original IP header and first 8 bytes as body
copy(icmpOut[8:], packet[:packetLen])
@@ -165,7 +198,193 @@ func ipv4CreateRejectTCPPacket(packet []byte, out []byte) []byte {
return out
}
func ipv6CreateRejectPacket(packet []byte, out []byte) []byte {
proto, offset, isFragment := ipv6FindUpperProtocol(packet)
if isFragment {
return nil
}
switch proto {
case 6: // tcp
return ipv6CreateRejectTCPPacket(packet, out, offset)
default:
return ipv6CreateRejectICMPPacket(packet, out, proto, offset)
}
}
func ipv6CreateRejectICMPPacket(packet []byte, out []byte, proto uint8, offset int) []byte {
// Do not generate ICMPv6 errors in response to ICMPv6 error packets
if proto == 58 && len(packet) > offset {
icmpType := packet[offset]
if icmpType >= 1 && icmpType <= 4 {
return nil
}
}
// Include as much of the original packet as possible, up to 1000 bytes,
// so the response fits comfortably within any tunnel MTU.
packetLen := min(len(packet), 1000)
outLen := ipv6.HeaderLen + 8 + packetLen
if outLen > cap(out) {
return nil
}
out = out[:outLen]
// IPv6 header
ipHdr := out[0:ipv6.HeaderLen]
ipHdr[0] = ipv6.Version << 4 // version, traffic class (high bits)
ipHdr[1] = 0 // traffic class (low bits), flow label (high bits)
ipHdr[2] = 0 // flow label
ipHdr[3] = 0 // flow label
payloadLen := uint16(outLen - ipv6.HeaderLen)
binary.BigEndian.PutUint16(ipHdr[4:], payloadLen) // payload length
ipHdr[6] = 58 // next header (ICMPv6)
ipHdr[7] = 64 // hop limit
// Swap dest / src IPs (each 16 bytes, src at 8, dst at 24)
copy(ipHdr[8:24], packet[24:40])
copy(ipHdr[24:40], packet[8:24])
// ICMPv6 Destination Unreachable
icmpOut := out[ipv6.HeaderLen:]
icmpOut[0] = 1 // type (Destination Unreachable)
icmpOut[1] = 1 // code (Communication with destination administratively prohibited)
icmpOut[2] = 0 // checksum
icmpOut[3] = 0 // .
icmpOut[4] = 0 // unused
icmpOut[5] = 0 // .
icmpOut[6] = 0 // .
icmpOut[7] = 0 // .
copy(icmpOut[8:], packet[:packetLen])
// ICMPv6 checksum uses a pseudo-header
csum := ipv6PseudoheaderChecksum(ipHdr[8:24], ipHdr[24:40], 58, uint32(payloadLen))
binary.BigEndian.PutUint16(icmpOut[2:], tcpipChecksum(icmpOut, csum))
return out
}
func ipv6CreateRejectTCPPacket(packet []byte, out []byte, offset int) []byte {
const tcpLen = 20
if len(packet) < offset+tcpLen {
return nil
}
outLen := ipv6.HeaderLen + tcpLen
if outLen > cap(out) {
return nil
}
out = out[:outLen]
// IPv6 header
ipHdr := out[0:ipv6.HeaderLen]
ipHdr[0] = ipv6.Version << 4 // version, traffic class (high bits)
ipHdr[1] = 0 // traffic class (low bits), flow label (high bits)
ipHdr[2] = 0 // flow label
ipHdr[3] = 0 // flow label
binary.BigEndian.PutUint16(ipHdr[4:], tcpLen) // payload length
ipHdr[6] = 6 // next header (TCP)
ipHdr[7] = 64 // hop limit
// Swap dest / src IPs
copy(ipHdr[8:24], packet[24:40])
copy(ipHdr[24:40], packet[8:24])
// TCP RST
tcpIn := packet[offset:]
var ackSeq, seq uint32
outFlags := byte(0b00000100) // RST
inAck := tcpIn[13]&0b00010000 != 0
if inAck {
seq = binary.BigEndian.Uint32(tcpIn[8:])
} else {
inSyn := uint32((tcpIn[13] & 0b00000010) >> 1)
inFin := uint32(tcpIn[13] & 0b00000001)
ackSeq = binary.BigEndian.Uint32(tcpIn[4:]) + inSyn + inFin + uint32(len(tcpIn)) - uint32(tcpIn[12]>>4)<<2
outFlags |= 0b00010000 // ACK
}
tcpOut := out[ipv6.HeaderLen:]
// Swap dest / src ports
copy(tcpOut[0:2], tcpIn[2:4])
copy(tcpOut[2:4], tcpIn[0:2])
binary.BigEndian.PutUint32(tcpOut[4:], seq)
binary.BigEndian.PutUint32(tcpOut[8:], ackSeq)
tcpOut[12] = (tcpLen >> 2) << 4 // data offset, reserved, NS
tcpOut[13] = outFlags // CWR, ECE, URG, ACK, PSH, RST, SYN, FIN
tcpOut[14] = 0 // window size
tcpOut[15] = 0 // .
tcpOut[16] = 0 // checksum
tcpOut[17] = 0 // .
tcpOut[18] = 0 // URG Pointer
tcpOut[19] = 0 // .
// Calculate checksum with IPv6 pseudo-header
csum := ipv6PseudoheaderChecksum(ipHdr[8:24], ipHdr[24:40], 6, tcpLen)
binary.BigEndian.PutUint16(tcpOut[16:], tcpipChecksum(tcpOut, csum))
return out
}
func ipv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragment bool) {
nextHeader = packet[6]
offset = ipv6.HeaderLen
for {
switch nextHeader {
case 0, 43, 60: // Hop-by-Hop, Routing, Destination
if len(packet) < offset+2 {
return nextHeader, offset, isFragment
}
nextHeader = packet[offset]
offset += int(packet[offset+1]+1) << 3
case 44: // Fragment
if len(packet) < offset+8 {
return nextHeader, offset, isFragment
}
if packet[offset+2] != 0 || packet[offset+3]&0xf8 != 0 {
isFragment = true
}
nextHeader = packet[offset]
offset += 8
case 51: // AH
if len(packet) < offset+2 {
return nextHeader, offset, isFragment
}
nextHeader = packet[offset]
offset += int(packet[offset+1]+2) << 2
default:
return nextHeader, offset, isFragment
}
}
}
func CreateICMPEchoResponse(packet, out []byte) []byte {
if len(packet) < 1 {
return nil
}
switch packet[0] >> 4 {
case 4:
return createICMPv4EchoResponse(packet, out)
case 6:
return createICMPv6EchoResponse(packet, out)
default:
return nil
}
}
func createICMPv4EchoResponse(packet, out []byte) []byte {
// Return early if this is not a simple ICMP Echo Request
//TODO: make constants out of these
if !(len(packet) >= 28 && len(packet) <= 9001 && packet[0] == 0x45 && packet[9] == 0x01 && packet[20] == 0x08) {
@@ -199,6 +418,43 @@ func CreateICMPEchoResponse(packet, out []byte) []byte {
return out
}
func createICMPv6EchoResponse(packet, out []byte) []byte {
// IPv6 header (40 bytes) + ICMPv6 header (8 bytes minimum)
if len(packet) < ipv6.HeaderLen+8 || len(packet) > 9001 {
return nil
}
// Next Header must be ICMPv6 (58)
if packet[6] != 58 {
return nil
}
// ICMPv6 type must be Echo Request (128)
if packet[ipv6.HeaderLen] != 128 {
return nil
}
out = out[:len(packet)]
copy(out, packet)
// Swap src/dst addresses (bytes 8-23 and 24-39)
copy(out[8:24], packet[24:40])
copy(out[24:40], packet[8:24])
// Change ICMPv6 type to Echo Reply (129)
icmp := out[ipv6.HeaderLen:]
icmp[0] = 129
icmp[2] = 0
icmp[3] = 0
// ICMPv6 checksum uses a pseudo-header with src, dst, length, and next header
payloadLen := uint32(len(icmp))
csum := ipv6PseudoheaderChecksum(out[8:24], out[24:40], 58, payloadLen)
binary.BigEndian.PutUint16(icmp[2:], tcpipChecksum(icmp, csum))
return out
}
// calculates the TCP/IP checksum defined in rfc1071. The passed-in
// csum is any initial checksum data that's already been computed.
//
@@ -236,3 +492,18 @@ func ipv4PseudoheaderChecksum(src, dst []byte, proto, length uint32) (csum uint3
csum += length >> 16
return csum
}
// based on:
// - https://github.com/google/gopacket/blob/v1.1.19/layers/tcpip.go#L37-L48
func ipv6PseudoheaderChecksum(src, dst []byte, proto, length uint32) (csum uint32) {
for i := 0; i < 16; i += 2 {
csum += uint32(src[i]) << 8
csum += uint32(src[i+1])
csum += uint32(dst[i]) << 8
csum += uint32(dst[i+1])
}
csum += proto
csum += length & 0xffff
csum += length >> 16
return csum
}
+404 -1
View File
@@ -1,11 +1,13 @@
package iputil
import (
"encoding/binary"
"net"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
func Test_CreateRejectPacket(t *testing.T) {
@@ -43,7 +45,7 @@ func Test_CreateRejectPacket(t *testing.T) {
}
b = append(b, []byte{0, 3, 0, 4, 0, 0, 0, 0}...)
expectedLen = MaxRejectPacketSize
expectedLen = maxIPv4RejectPacketSize
out = make([]byte, MaxRejectPacketSize)
rejectPacket = CreateRejectPacket(b, out)
assert.NotNil(t, rejectPacket)
@@ -71,3 +73,404 @@ func Test_CreateRejectPacket(t *testing.T) {
assert.NotNil(t, rejectPacket)
assert.Len(t, rejectPacket, expectedLen)
}
func Test_CreateRejectPacket_NoFragment(t *testing.T) {
out := make([]byte, MaxRejectPacketSize)
// IPv4: non-zero fragment offset should not generate reject packet
h := ipv4.Header{
Len: 20,
Src: net.IPv4(10, 0, 0, 1),
Dst: net.IPv4(10, 0, 0, 2),
Protocol: 17, // UDP
}
b, err := h.Marshal()
if err != nil {
t.Fatalf("h.Marshal: %v", err)
}
b = append(b, make([]byte, 8)...)
// Set fragment offset to non-zero (byte 6-7, offset in 8-byte units)
b[6] = 0x00
b[7] = 0x01
assert.Nil(t, CreateRejectPacket(b, out))
// MF flag with zero offset (first fragment) should still generate reject
b[6] = 0x20 // MF flag set
b[7] = 0x00
assert.NotNil(t, CreateRejectPacket(b, out))
// Non-fragment should still generate reject packet
b[6] = 0x00
b[7] = 0x00
assert.NotNil(t, CreateRejectPacket(b, out))
// DF flag only (not a fragment) should still generate reject packet
b[6] = 0x40
b[7] = 0x00
assert.NotNil(t, CreateRejectPacket(b, out))
}
func Test_CreateRejectPacketIPv6_NoFragment(t *testing.T) {
src := net.ParseIP("fd00::1")
dst := net.ParseIP("fd00::2")
out := make([]byte, MaxRejectPacketSize)
// IPv6 with Fragment header and non-zero offset should not generate reject
fragHeader := []byte{
17, // next header: UDP
0, // reserved
0, 9, // fragment offset=1 (shifted left 3), M=1
0, 0, 0, 1, // identification
}
udpPayload := make([]byte, 8)
payload := append(fragHeader, udpPayload...)
packet := makeIPv6Packet(src, dst, 44, payload) // next header 44 = Fragment
assert.Nil(t, CreateRejectPacket(packet, out))
// Fragment header with zero offset (first fragment) should still generate reject
fragHeader[2] = 0
fragHeader[3] = 1 // offset=0, M=1
payload = append(fragHeader, udpPayload...)
packet = makeIPv6Packet(src, dst, 44, payload)
assert.NotNil(t, CreateRejectPacket(packet, out))
}
func Test_CreateRejectPacket_NoICMPError(t *testing.T) {
out := make([]byte, MaxRejectPacketSize)
// ICMP error types should not generate reject packets
icmpErrorTypes := []byte{3, 4, 5, 11, 12}
for _, icmpType := range icmpErrorTypes {
h := ipv4.Header{
Len: 20,
Src: net.IPv4(10, 0, 0, 1),
Dst: net.IPv4(10, 0, 0, 2),
Protocol: 1, // ICMP
}
b, err := h.Marshal()
if err != nil {
t.Fatalf("h.Marshal: %v", err)
}
b = append(b, icmpType, 0, 0, 0, 0, 0, 0, 0)
rejectPacket := CreateRejectPacket(b, out)
assert.Nil(t, rejectPacket, "ICMP type %d should not generate a reject packet", icmpType)
}
// ICMP non-error types should still generate reject packets
icmpNonErrorTypes := []byte{0, 8, 13, 14}
for _, icmpType := range icmpNonErrorTypes {
h := ipv4.Header{
Len: 20,
Src: net.IPv4(10, 0, 0, 1),
Dst: net.IPv4(10, 0, 0, 2),
Protocol: 1, // ICMP
}
b, err := h.Marshal()
if err != nil {
t.Fatalf("h.Marshal: %v", err)
}
b = append(b, icmpType, 0, 0, 0, 0, 0, 0, 0)
rejectPacket := CreateRejectPacket(b, out)
assert.NotNil(t, rejectPacket, "ICMP type %d should generate a reject packet", icmpType)
}
}
func makeIPv6Packet(src, dst net.IP, nextHeader uint8, payload []byte) []byte {
b := make([]byte, ipv6.HeaderLen+len(payload))
b[0] = ipv6.Version << 4
binary.BigEndian.PutUint16(b[4:], uint16(len(payload)))
b[6] = nextHeader
b[7] = 64
copy(b[8:24], src.To16())
copy(b[24:40], dst.To16())
copy(b[ipv6.HeaderLen:], payload)
return b
}
func Test_CreateRejectPacketIPv6_ICMP(t *testing.T) {
src := net.ParseIP("fd00::1")
dst := net.ParseIP("fd00::2")
// Small UDP packet: entire original included in body
udpPayload := make([]byte, 20)
udpPayload[0] = 0x00 // src port high
udpPayload[1] = 0x50 // src port low (80)
udpPayload[2] = 0x01 // dst port high
udpPayload[3] = 0xBB // dst port low (443)
packet := makeIPv6Packet(src, dst, 17, udpPayload)
out := make([]byte, MaxRejectPacketSize)
rejectPacket := CreateRejectPacket(packet, out)
assert.NotNil(t, rejectPacket)
// Small packet fits entirely: 40 (ipv6 hdr) + 8 (icmpv6 hdr) + 60 (original)
expectedLen := ipv6.HeaderLen + 8 + len(packet)
assert.Len(t, rejectPacket, expectedLen)
// Verify version
assert.Equal(t, byte(ipv6.Version<<4), rejectPacket[0]&0xf0)
// Verify next header is ICMPv6 (58)
assert.Equal(t, byte(58), rejectPacket[6])
// Verify src/dst are swapped
assert.Equal(t, dst.To16(), net.IP(rejectPacket[8:24]))
assert.Equal(t, src.To16(), net.IP(rejectPacket[24:40]))
// Verify ICMPv6 type=1 (Dest Unreachable), code=1 (Administratively prohibited)
assert.Equal(t, byte(1), rejectPacket[ipv6.HeaderLen])
assert.Equal(t, byte(1), rejectPacket[ipv6.HeaderLen+1])
// Verify entire original packet is included in body
assert.Equal(t, packet, rejectPacket[ipv6.HeaderLen+8:])
// Large packet: body is truncated to 1000 bytes
largePkt := makeIPv6Packet(src, dst, 17, make([]byte, 1200))
rejectPacket = CreateRejectPacket(largePkt, out)
assert.NotNil(t, rejectPacket)
assert.Len(t, rejectPacket, ipv6.HeaderLen+8+1000)
assert.Equal(t, largePkt[:1000], rejectPacket[ipv6.HeaderLen+8:])
}
func Test_CreateRejectPacketIPv6_TCP(t *testing.T) {
src := net.ParseIP("fd00::1")
dst := net.ParseIP("fd00::2")
// TCP SYN packet (next header 6)
tcpPayload := make([]byte, 20)
tcpPayload[0] = 0x00 // src port high
tcpPayload[1] = 0x50 // src port low (80)
tcpPayload[2] = 0x01 // dst port high
tcpPayload[3] = 0xBB // dst port low (443)
binary.BigEndian.PutUint32(tcpPayload[4:], 1000) // seq
binary.BigEndian.PutUint32(tcpPayload[8:], 0) // ack seq
tcpPayload[12] = (20 >> 2) << 4 // data offset
tcpPayload[13] = 0b00000010 // SYN flag
packet := makeIPv6Packet(src, dst, 6, tcpPayload)
out := make([]byte, MaxRejectPacketSize)
rejectPacket := CreateRejectPacket(packet, out)
assert.NotNil(t, rejectPacket)
// Expected: 40 (ipv6 hdr) + 20 (tcp RST)
expectedLen := ipv6.HeaderLen + 20
assert.Len(t, rejectPacket, expectedLen)
// Verify version
assert.Equal(t, byte(ipv6.Version<<4), rejectPacket[0]&0xf0)
// Verify next header is TCP (6)
assert.Equal(t, byte(6), rejectPacket[6])
// Verify src/dst are swapped
assert.Equal(t, dst.To16(), net.IP(rejectPacket[8:24]))
assert.Equal(t, src.To16(), net.IP(rejectPacket[24:40]))
// Verify ports are swapped
tcpOut := rejectPacket[ipv6.HeaderLen:]
assert.Equal(t, uint16(443), binary.BigEndian.Uint16(tcpOut[0:2]))
assert.Equal(t, uint16(80), binary.BigEndian.Uint16(tcpOut[2:4]))
// RST+ACK flags (since input was SYN without ACK)
assert.Equal(t, byte(0b00010100), tcpOut[13])
// ack_seq = original seq (1000) + SYN (1) + FIN (0) + segment data (0)
assert.Equal(t, uint32(1001), binary.BigEndian.Uint32(tcpOut[8:]))
}
func Test_CreateRejectPacketIPv6_TCPWithACK(t *testing.T) {
src := net.ParseIP("fd00::1")
dst := net.ParseIP("fd00::2")
// TCP packet with ACK set
tcpPayload := make([]byte, 20)
tcpPayload[0] = 0x00
tcpPayload[1] = 0x50
tcpPayload[2] = 0x01
tcpPayload[3] = 0xBB
binary.BigEndian.PutUint32(tcpPayload[4:], 1000) // seq
binary.BigEndian.PutUint32(tcpPayload[8:], 2000) // ack seq
tcpPayload[12] = (20 >> 2) << 4 // data offset
tcpPayload[13] = 0b00010000 // ACK flag
packet := makeIPv6Packet(src, dst, 6, tcpPayload)
out := make([]byte, MaxRejectPacketSize)
rejectPacket := CreateRejectPacket(packet, out)
assert.NotNil(t, rejectPacket)
tcpOut := rejectPacket[ipv6.HeaderLen:]
// RST only (no ACK) since input had ACK
assert.Equal(t, byte(0b00000100), tcpOut[13])
// seq = original ack_seq
assert.Equal(t, uint32(2000), binary.BigEndian.Uint32(tcpOut[4:]))
}
func Test_CreateRejectPacketIPv6_NoICMPError(t *testing.T) {
src := net.ParseIP("fd00::1")
dst := net.ParseIP("fd00::2")
out := make([]byte, MaxRejectPacketSize)
// ICMPv6 error types (1-4) should not generate reject packets
for icmpType := byte(1); icmpType <= 4; icmpType++ {
payload := make([]byte, 8)
payload[0] = icmpType
packet := makeIPv6Packet(src, dst, 58, payload)
rejectPacket := CreateRejectPacket(packet, out)
assert.Nil(t, rejectPacket, "ICMPv6 type %d should not generate a reject packet", icmpType)
}
// ICMPv6 non-error types should still generate reject packets
nonErrorTypes := []byte{128, 129, 133, 134}
for _, icmpType := range nonErrorTypes {
payload := make([]byte, 8)
payload[0] = icmpType
packet := makeIPv6Packet(src, dst, 58, payload)
rejectPacket := CreateRejectPacket(packet, out)
assert.NotNil(t, rejectPacket, "ICMPv6 type %d should generate a reject packet", icmpType)
}
}
func Test_CreateRejectPacketIPv6_TooShort(t *testing.T) {
// Packet too short to be valid IPv6
out := make([]byte, MaxRejectPacketSize)
assert.Nil(t, CreateRejectPacket([]byte{0x60}, out))
assert.Nil(t, CreateRejectPacket(make([]byte, 39), out))
}
func Test_CreateRejectPacketIPv6_ExtensionHeaders(t *testing.T) {
src := net.ParseIP("fd00::1")
dst := net.ParseIP("fd00::2")
// IPv6 + Hop-by-Hop extension header + TCP
hopByHop := []byte{
6, // next header: TCP
0, // length (8 bytes total)
0, 0, // padding
0, 0, 0, 0,
}
tcpPayload := make([]byte, 20)
tcpPayload[0] = 0x00
tcpPayload[1] = 0x50
tcpPayload[2] = 0x01
tcpPayload[3] = 0xBB
binary.BigEndian.PutUint32(tcpPayload[4:], 1000)
binary.BigEndian.PutUint32(tcpPayload[8:], 2000)
tcpPayload[12] = (20 >> 2) << 4
tcpPayload[13] = 0b00010000 // ACK
payload := append(hopByHop, tcpPayload...)
packet := makeIPv6Packet(src, dst, 0, payload) // next header 0 = Hop-by-Hop
out := make([]byte, MaxRejectPacketSize)
rejectPacket := CreateRejectPacket(packet, out)
assert.NotNil(t, rejectPacket)
// Should produce TCP RST
expectedLen := ipv6.HeaderLen + 20
assert.Len(t, rejectPacket, expectedLen)
assert.Equal(t, byte(6), rejectPacket[6]) // next header is TCP
tcpOut := rejectPacket[ipv6.HeaderLen:]
assert.Equal(t, byte(0b00000100), tcpOut[13]) // RST only
}
func TestCreateICMPEchoResponse_IPv4(t *testing.T) {
// Build a simple IPv4 ICMP Echo Request
packet := make([]byte, 28)
packet[0] = 0x45 // version 4, IHL 5
binary.BigEndian.PutUint16(packet[2:], uint16(28)) // total length
packet[8] = 64 // TTL
packet[9] = 1 // protocol ICMP
copy(packet[12:16], net.IPv4(10, 0, 0, 1).To4()) // src
copy(packet[16:20], net.IPv4(10, 0, 0, 2).To4()) // dst
packet[20] = 8 // ICMP Echo Request
out := make([]byte, len(packet))
result := CreateICMPEchoResponse(packet, out)
assert.NotNil(t, result)
assert.Equal(t, byte(0x45), result[0])
// src/dst swapped
assert.Equal(t, net.IPv4(10, 0, 0, 2).To4(), net.IP(result[12:16]))
assert.Equal(t, net.IPv4(10, 0, 0, 1).To4(), net.IP(result[16:20]))
// ICMP Echo Reply
assert.Equal(t, byte(0), result[20])
}
func TestCreateICMPEchoResponse_IPv6(t *testing.T) {
src := net.ParseIP("fd00::1").To16()
dst := net.ParseIP("fd00::2").To16()
// Build an IPv6 ICMPv6 Echo Request packet
// IPv6 header (40 bytes) + ICMPv6 (8 bytes)
packet := make([]byte, 48)
packet[0] = 0x60 // version 6
payloadLen := uint16(8) // ICMPv6 header only
binary.BigEndian.PutUint16(packet[4:], payloadLen)
packet[6] = 58 // Next Header: ICMPv6
packet[7] = 64 // Hop Limit
copy(packet[8:24], src) // src address
copy(packet[24:40], dst) // dst address
// ICMPv6 Echo Request
icmp := packet[40:]
icmp[0] = 128 // type: Echo Request
icmp[1] = 0 // code
binary.BigEndian.PutUint16(icmp[4:], 1) // identifier
binary.BigEndian.PutUint16(icmp[6:], 1) // sequence number
// Compute correct checksum for the request
csum := ipv6PseudoheaderChecksum(src, dst, 58, uint32(payloadLen))
binary.BigEndian.PutUint16(icmp[2:], tcpipChecksum(icmp, csum))
out := make([]byte, len(packet))
result := CreateICMPEchoResponse(packet, out)
assert.NotNil(t, result)
// Version should still be 6
assert.Equal(t, byte(6), result[0]>>4)
// src/dst swapped
assert.Equal(t, dst, net.IP(result[8:24]))
assert.Equal(t, src, net.IP(result[24:40]))
// ICMPv6 Echo Reply type
assert.Equal(t, byte(129), result[40])
// Verify checksum is valid (tcpipChecksum returns 0 when data+checksum is correct)
respIcmp := result[40:]
verifyCsum := ipv6PseudoheaderChecksum(result[8:24], result[24:40], 58, uint32(payloadLen))
assert.Equal(t, uint16(0), tcpipChecksum(respIcmp, verifyCsum))
}
func TestCreateICMPEchoResponse_IPv6_NotEchoRequest(t *testing.T) {
src := net.ParseIP("fd00::1").To16()
dst := net.ParseIP("fd00::2").To16()
packet := make([]byte, 48)
packet[0] = 0x60
binary.BigEndian.PutUint16(packet[4:], 8)
packet[6] = 58
packet[7] = 64
copy(packet[8:24], src)
copy(packet[24:40], dst)
// ICMPv6 type 1 (Destination Unreachable) - not Echo Request
packet[40] = 1
out := make([]byte, len(packet))
result := CreateICMPEchoResponse(packet, out)
assert.Nil(t, result)
}
func TestCreateICMPEchoResponse_IPv6_NotICMPv6(t *testing.T) {
src := net.ParseIP("fd00::1").To16()
dst := net.ParseIP("fd00::2").To16()
packet := make([]byte, 48)
packet[0] = 0x60
binary.BigEndian.PutUint16(packet[4:], 8)
packet[6] = 6 // TCP, not ICMPv6
packet[7] = 64
copy(packet[8:24], src)
copy(packet[24:40], dst)
out := make([]byte, len(packet))
result := CreateICMPEchoResponse(packet, out)
assert.Nil(t, result)
}
+7
View File
@@ -181,6 +181,13 @@ func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender,
if err != nil {
return
}
// Advance the replay window now that the frame is authenticated
if !hostinfo.ConnectionState.window.Update(f.l, h.MessageCounter) {
if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debug("dropping out of window relay packet", "header", h)
}
return
}
// Successfully validated the thing. Get rid of the Relay header.
signedPayload = signedPayload[header.Len:]
// Pull the Roaming parts up here, and return in all call paths.
+20 -21
View File
@@ -318,17 +318,16 @@ func (rm *relayManager) HandleControlMsg(h *HostInfo, d []byte, f *Interface) {
}
func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f *Interface, m *NebulaControl) {
relayFrom := protoAddrToNetAddr(m.RelayFromAddr)
relayTo := protoAddrToNetAddr(m.RelayToAddr)
rm.l.Info("handleCreateRelayResponse",
"relayFrom", protoAddrToNetAddr(m.RelayFromAddr),
"relayTo", protoAddrToNetAddr(m.RelayToAddr),
"relayFrom", relayFrom,
"relayTo", relayTo,
"initiatorRelayIndex", m.InitiatorRelayIndex,
"responderRelayIndex", m.ResponderRelayIndex,
"vpnAddrs", h.vpnAddrs,
)
target := m.RelayToAddr
targetAddr := protoAddrToNetAddr(target)
relay, err := rm.EstablishRelay(h, m)
if err != nil {
rm.l.Error("Failed to update relay for relayTo", "error", err)
@@ -344,7 +343,7 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
rm.l.Error("Can't find a HostInfo for peer", "relayTo", relay.PeerAddr)
return
}
peerRelay, ok := peerHostInfo.relayState.QueryRelayForByIp(targetAddr)
peerRelay, ok := peerHostInfo.relayState.QueryRelayForByIp(relayTo)
if !ok {
rm.l.Error("peerRelay does not have Relay state for relayTo", "relayTo", peerHostInfo.vpnAddrs[0])
return
@@ -354,19 +353,19 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
// I initiated the request to this peer, but haven't heard back from the peer yet. I must wait for this peer
// to respond to complete the connection.
case PeerRequested, Disestablished, Established:
peerHostInfo.relayState.UpdateRelayForByIpState(targetAddr, Established)
peerHostInfo.relayState.UpdateRelayForByIpState(relayTo, Established)
resp := NebulaControl{
Type: NebulaControl_CreateRelayResponse,
ResponderRelayIndex: peerRelay.LocalIndex,
InitiatorRelayIndex: peerRelay.RemoteIndex,
}
peer := peerHostInfo.vpnAddrs[0]
if v == cert.Version1 {
peer := peerHostInfo.vpnAddrs[0]
if !peer.Is4() {
rm.l.Error("Refusing to CreateRelayResponse for a v1 relay with an ipv6 address",
"relayFrom", peer,
"relayTo", target,
"relayTo", relayTo,
"initiatorRelayIndex", resp.InitiatorRelayIndex,
"responderRelayIndex", resp.ResponderRelayIndex,
"vpnAddrs", peerHostInfo.vpnAddrs,
@@ -376,26 +375,26 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
b := peer.As4()
resp.OldRelayFromAddr = binary.BigEndian.Uint32(b[:])
b = targetAddr.As4()
b = relayTo.As4()
resp.OldRelayToAddr = binary.BigEndian.Uint32(b[:])
} else {
resp.RelayFromAddr = netAddrToProtoAddr(peerHostInfo.vpnAddrs[0])
resp.RelayToAddr = target
resp.RelayFromAddr = netAddrToProtoAddr(peer)
resp.RelayToAddr = m.RelayToAddr
}
msg, err := resp.Marshal()
if err != nil {
rm.l.Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay", "error", err)
} else {
f.SendMessageToHostInfo(header.Control, 0, peerHostInfo, msg, make([]byte, 12), make([]byte, mtu))
rm.l.Info("send CreateRelayResponse",
"relayFrom", resp.RelayFromAddr,
"relayTo", resp.RelayToAddr,
"initiatorRelayIndex", resp.InitiatorRelayIndex,
"responderRelayIndex", resp.ResponderRelayIndex,
"vpnAddrs", peerHostInfo.vpnAddrs,
)
return
}
f.SendMessageToHostInfo(header.Control, 0, peerHostInfo, msg, make([]byte, 12), make([]byte, mtu))
rm.l.Info("send CreateRelayResponse",
"relayFrom", peer,
"relayTo", relayTo,
"initiatorRelayIndex", resp.InitiatorRelayIndex,
"responderRelayIndex", resp.ResponderRelayIndex,
"vpnAddrs", peerHostInfo.vpnAddrs,
)
}
}
+1 -1
View File
@@ -335,7 +335,7 @@ func loadStatsConfig(c *config.C) (statsConfig, error) {
}
cfg.interval = c.GetDuration("stats.interval", 0)
if cfg.interval == 0 {
if cfg.interval <= 0 {
return cfg, fmt.Errorf("stats.interval was an invalid duration: %s", c.GetString("stats.interval", ""))
}
+1 -1
View File
@@ -175,8 +175,8 @@ func (u *StdConn) ListenOut(r EncReader) error {
if errors.Is(err, net.ErrClosed) {
return err
}
u.l.Error("unexpected udp socket receive error", "error", err)
continue
}
r(netip.AddrPortFrom(rua.Addr().Unmap(), rua.Port()), buffer[:n])