Compare commits

..

1 Commits

Author SHA1 Message Date
Nate Brown 16a836a73f PMTUD exploration, start small then grow 2026-05-05 17:05:50 -05:00
52 changed files with 1775 additions and 1119 deletions
+15 -8
View File
@@ -145,6 +145,7 @@ func (cm *connectionManager) getAndResetTrafficCheck(h *HostInfo, now time.Time)
func (cm *connectionManager) AddTrafficWatch(h *HostInfo) { func (cm *connectionManager) AddTrafficWatch(h *HostInfo) {
if h.out.Swap(true) == false { if h.out.Swap(true) == false {
cm.trafficTimer.Add(h.localIndexId, cm.checkInterval) cm.trafficTimer.Add(h.localIndexId, cm.checkInterval)
cm.intf.pmtudManager.OnTunnelUp(h)
} }
} }
@@ -153,8 +154,8 @@ func (cm *connectionManager) Start(ctx context.Context) {
defer clockSource.Stop() defer clockSource.Stop()
p := []byte("") p := []byte("")
// Long-lived buf for the traffic-check goroutine; never released. nb := make([]byte, 12, 12)
buf := cm.intf.bufAlloc.Acquire() out := make([]byte, mtu)
for { for {
select { select {
@@ -169,17 +170,18 @@ func (cm *connectionManager) Start(ctx context.Context) {
break break
} }
cm.doTrafficCheck(localIndex, p, buf, now) cm.doTrafficCheck(localIndex, p, nb, out, now)
} }
} }
} }
} }
func (cm *connectionManager) doTrafficCheck(localIndex uint32, p []byte, buf *WireBuffer, now time.Time) { func (cm *connectionManager) doTrafficCheck(localIndex uint32, p, nb, out []byte, now time.Time) {
decision, hostinfo, primary := cm.makeTrafficDecision(localIndex, now) decision, hostinfo, primary := cm.makeTrafficDecision(localIndex, now)
switch decision { switch decision {
case deleteTunnel: case deleteTunnel:
cm.intf.pmtudManager.OnTunnelDown(hostinfo)
if cm.hostMap.DeleteHostInfo(hostinfo) { if cm.hostMap.DeleteHostInfo(hostinfo) {
// Only clearing the lighthouse cache if this is the last hostinfo for this vpn ip in the hostmap // Only clearing the lighthouse cache if this is the last hostinfo for this vpn ip in the hostmap
cm.intf.lightHouse.DeleteVpnAddrs(hostinfo.vpnAddrs) cm.intf.lightHouse.DeleteVpnAddrs(hostinfo.vpnAddrs)
@@ -199,7 +201,14 @@ func (cm *connectionManager) doTrafficCheck(localIndex uint32, p []byte, buf *Wi
cm.tryRehandshake(hostinfo) cm.tryRehandshake(hostinfo)
case sendTestPacket: case sendTestPacket:
cm.intf.SendMessageToHostInfo(header.Test, header.TestRequest, hostinfo, p, buf) // Defer to pmtud if it has a confirmed PMTU > floor for this peer:
// the probe at the confirmed size verifies both liveness AND that
// the discovered PMTU still fits, so we don't burn a separate test
// packet on top of it. If pmtud declines (disabled, peer unsupported,
// or no confirmed size yet) we fall back to the regular test.
if !cm.intf.pmtudManager.MaybeProbeAsTest(hostinfo) {
cm.intf.SendMessageToHostInfo(header.Test, header.TestRequest, hostinfo, p, nb, out)
}
} }
cm.resetRelayTrafficCheck(hostinfo) cm.resetRelayTrafficCheck(hostinfo)
@@ -308,9 +317,7 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
if err != nil { if err != nil {
cm.l.Error("failed to marshal Control message to migrate relay", "error", err) cm.l.Error("failed to marshal Control message to migrate relay", "error", err)
} else { } else {
migBuf := cm.intf.bufAlloc.Acquire() cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, make([]byte, 12), make([]byte, mtu))
cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, migBuf)
cm.intf.bufAlloc.Release(migBuf)
cm.l.Info("send CreateRelayRequest", cm.l.Info("send CreateRelayRequest",
"relayFrom", req.RelayFromAddr, "relayFrom", req.RelayFromAddr,
"relayTo", req.RelayToAddr, "relayTo", req.RelayToAddr,
+16 -10
View File
@@ -67,9 +67,11 @@ func Test_NewConnectionManagerTest(t *testing.T) {
punchy := NewPunchyFromConfig(test.NewLogger(), conf) punchy := NewPunchyFromConfig(test.NewLogger(), conf)
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy) nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
nc.intf = ifce nc.intf = ifce
ifce.pmtudManager = newPMTUDManagerFromConfig(test.NewLogger(), conf, ifce.inside)
ifce.pmtudManager.intf = ifce
p := []byte("") p := []byte("")
buf := NewWireBuffer(mtu, 0) nb := make([]byte, 12, 12)
out := make([]byte, mtu)
// Add an ip we have established a connection w/ to hostmap // Add an ip we have established a connection w/ to hostmap
hostinfo := &HostInfo{ hostinfo := &HostInfo{
@@ -92,7 +94,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
assert.True(t, hostinfo.in.Load()) assert.True(t, hostinfo.in.Load())
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded // Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
nc.doTrafficCheck(hostinfo.localIndexId, p, buf, time.Now()) nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
assert.False(t, hostinfo.pendingDeletion.Load()) assert.False(t, hostinfo.pendingDeletion.Load())
assert.False(t, hostinfo.out.Load()) assert.False(t, hostinfo.out.Load())
assert.False(t, hostinfo.in.Load()) assert.False(t, hostinfo.in.Load())
@@ -100,7 +102,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
// Do another traffic check tick, this host should be pending deletion now // Do another traffic check tick, this host should be pending deletion now
nc.Out(hostinfo) nc.Out(hostinfo)
assert.True(t, hostinfo.out.Load()) assert.True(t, hostinfo.out.Load())
nc.doTrafficCheck(hostinfo.localIndexId, p, buf, time.Now()) nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
assert.True(t, hostinfo.pendingDeletion.Load()) assert.True(t, hostinfo.pendingDeletion.Load())
assert.False(t, hostinfo.out.Load()) assert.False(t, hostinfo.out.Load())
assert.False(t, hostinfo.in.Load()) assert.False(t, hostinfo.in.Load())
@@ -108,7 +110,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0]) assert.Contains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs[0])
// Do a final traffic check tick, the host should now be removed // Do a final traffic check tick, the host should now be removed
nc.doTrafficCheck(hostinfo.localIndexId, p, buf, time.Now()) nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
assert.NotContains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs) assert.NotContains(t, nc.hostMap.Hosts, hostinfo.vpnAddrs)
assert.NotContains(t, nc.hostMap.Indexes, hostinfo.localIndexId) assert.NotContains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
} }
@@ -149,9 +151,11 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
punchy := NewPunchyFromConfig(test.NewLogger(), conf) punchy := NewPunchyFromConfig(test.NewLogger(), conf)
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy) nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
nc.intf = ifce nc.intf = ifce
ifce.pmtudManager = newPMTUDManagerFromConfig(test.NewLogger(), conf, ifce.inside)
ifce.pmtudManager.intf = ifce
p := []byte("") p := []byte("")
buf := NewWireBuffer(mtu, 0) nb := make([]byte, 12, 12)
out := make([]byte, mtu)
// Add an ip we have established a connection w/ to hostmap // Add an ip we have established a connection w/ to hostmap
hostinfo := &HostInfo{ hostinfo := &HostInfo{
@@ -174,14 +178,14 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId) assert.Contains(t, nc.hostMap.Indexes, hostinfo.localIndexId)
// Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded // Do a traffic check tick, should not be pending deletion but should not have any in/out packets recorded
nc.doTrafficCheck(hostinfo.localIndexId, p, buf, time.Now()) nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
assert.False(t, hostinfo.pendingDeletion.Load()) assert.False(t, hostinfo.pendingDeletion.Load())
assert.False(t, hostinfo.out.Load()) assert.False(t, hostinfo.out.Load())
assert.False(t, hostinfo.in.Load()) assert.False(t, hostinfo.in.Load())
// Do another traffic check tick, this host should be pending deletion now // Do another traffic check tick, this host should be pending deletion now
nc.Out(hostinfo) nc.Out(hostinfo)
nc.doTrafficCheck(hostinfo.localIndexId, p, buf, time.Now()) nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
assert.True(t, hostinfo.pendingDeletion.Load()) assert.True(t, hostinfo.pendingDeletion.Load())
assert.False(t, hostinfo.out.Load()) assert.False(t, hostinfo.out.Load())
assert.False(t, hostinfo.in.Load()) assert.False(t, hostinfo.in.Load())
@@ -190,7 +194,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
// We saw traffic, should no longer be pending deletion // We saw traffic, should no longer be pending deletion
nc.In(hostinfo) nc.In(hostinfo)
nc.doTrafficCheck(hostinfo.localIndexId, p, buf, time.Now()) nc.doTrafficCheck(hostinfo.localIndexId, p, nb, out, time.Now())
assert.False(t, hostinfo.pendingDeletion.Load()) assert.False(t, hostinfo.pendingDeletion.Load())
assert.False(t, hostinfo.out.Load()) assert.False(t, hostinfo.out.Load())
assert.False(t, hostinfo.in.Load()) assert.False(t, hostinfo.in.Load())
@@ -361,6 +365,8 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
punchy := NewPunchyFromConfig(test.NewLogger(), conf) punchy := NewPunchyFromConfig(test.NewLogger(), conf)
nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy) nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
nc.intf = ifce nc.intf = ifce
ifce.pmtudManager = newPMTUDManagerFromConfig(test.NewLogger(), conf, ifce.inside)
ifce.pmtudManager.intf = ifce
ifce.connectionManager = nc ifce.connectionManager = nc
hostinfo := &HostInfo{ hostinfo := &HostInfo{
+14 -7
View File
@@ -54,6 +54,7 @@ type Control struct {
dnsStart func() dnsStart func()
lighthouseStart func() lighthouseStart func()
connectionManagerStart func(context.Context) connectionManagerStart func(context.Context)
pmtudManagerStart func(context.Context)
} }
type ControlHostInfo struct { type ControlHostInfo struct {
@@ -107,6 +108,9 @@ func (c *Control) Start() (func() error, error) {
if c.connectionManagerStart != nil { if c.connectionManagerStart != nil {
go c.connectionManagerStart(c.ctx) go c.connectionManagerStart(c.ctx)
} }
if c.pmtudManagerStart != nil {
go c.pmtudManagerStart(c.ctx)
}
if c.lighthouseStart != nil { if c.lighthouseStart != nil {
c.lighthouseStart() c.lighthouseStart()
} }
@@ -278,9 +282,15 @@ func (c *Control) CloseTunnel(vpnIp netip.Addr, localOnly bool) bool {
} }
if !localOnly { if !localOnly {
buf := c.f.bufAlloc.Acquire() c.f.send(
c.f.send(header.CloseTunnel, 0, hostInfo.ConnectionState, hostInfo, []byte{}, buf) header.CloseTunnel,
c.f.bufAlloc.Release(buf) 0,
hostInfo.ConnectionState,
hostInfo,
[]byte{},
make([]byte, 12, 12),
make([]byte, mtu),
)
} }
c.f.closeTunnel(hostInfo) c.f.closeTunnel(hostInfo)
@@ -290,14 +300,11 @@ func (c *Control) CloseTunnel(vpnIp netip.Addr, localOnly bool) bool {
// CloseAllTunnels is just like CloseTunnel except it goes through and shuts them all down, optionally you can avoid shutting down lighthouse tunnels // CloseAllTunnels is just like CloseTunnel except it goes through and shuts them all down, optionally you can avoid shutting down lighthouse tunnels
// the int returned is a count of tunnels closed // the int returned is a count of tunnels closed
func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) { func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
// One WireBuffer for the whole shutdown loop.
buf := c.f.bufAlloc.Acquire()
defer c.f.bufAlloc.Release(buf)
shutdown := func(h *HostInfo) { shutdown := func(h *HostInfo) {
if excludeLighthouses && c.f.lightHouse.IsAnyLighthouseAddr(h.vpnAddrs) { if excludeLighthouses && c.f.lightHouse.IsAnyLighthouseAddr(h.vpnAddrs) {
return return
} }
c.f.send(header.CloseTunnel, 0, h.ConnectionState, h, []byte{}, buf) c.f.send(header.CloseTunnel, 0, h.ConnectionState, h, []byte{}, make([]byte, 12, 12), make([]byte, mtu))
c.f.closeTunnel(h) c.f.closeTunnel(h)
c.l.Debug("Sending close tunnel message", c.l.Debug("Sending close tunnel message",
+60 -12
View File
@@ -5,6 +5,8 @@ package nebula
import ( import (
"net/netip" "net/netip"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/overlay" "github.com/slackhq/nebula/overlay"
"github.com/slackhq/nebula/udp" "github.com/slackhq/nebula/udp"
@@ -20,9 +22,7 @@ func (c *Control) WaitForType(msgType header.MessageType, subType header.Message
panic(err) panic(err)
} }
pipeTo.InjectUDPPacket(p) pipeTo.InjectUDPPacket(p)
match := h.Type == msgType && h.Subtype == subType if h.Type == msgType && h.Subtype == subType {
p.Release()
if match {
return return
} }
} }
@@ -38,9 +38,7 @@ func (c *Control) WaitForTypeByIndex(toIndex uint32, msgType header.MessageType,
panic(err) panic(err)
} }
pipeTo.InjectUDPPacket(p) pipeTo.InjectUDPPacket(p)
match := h.RemoteIndex == toIndex && h.Type == msgType && h.Subtype == subType if h.RemoteIndex == toIndex && h.Type == msgType && h.Subtype == subType {
p.Release()
if match {
return return
} }
} }
@@ -92,15 +90,65 @@ func (c *Control) GetTunTxChan() <-chan []byte {
return c.f.inside.(*overlay.TestTun).TxPackets return c.f.inside.(*overlay.TestTun).TxPackets
} }
// InjectUDPPacket injects a packet into the udp side. We copy internally so the caller keeps ownership of p. // InjectUDPPacket will inject a packet into the udp side of nebula
// The copy comes from the freelist so steady-state alloc is zero.
func (c *Control) InjectUDPPacket(p *udp.Packet) { func (c *Control) InjectUDPPacket(p *udp.Packet) {
c.f.outside.(*udp.TesterConn).Send(p.Copy()) c.f.outside.(*udp.TesterConn).Send(p)
} }
// InjectTunPacket pushes an IP packet onto the tun interface. // InjectTunUDPPacket puts a udp packet on the tun interface. Using UDP here because it's a simpler protocol
func (c *Control) InjectTunPacket(packet []byte) { func (c *Control) InjectTunUDPPacket(toAddr netip.Addr, toPort uint16, fromAddr netip.Addr, fromPort uint16, data []byte) {
c.f.inside.(*overlay.TestTun).Send(packet) serialize := make([]gopacket.SerializableLayer, 0)
var netLayer gopacket.NetworkLayer
if toAddr.Is6() {
if !fromAddr.Is6() {
panic("Cant send ipv6 to ipv4")
}
ip := &layers.IPv6{
Version: 6,
NextHeader: layers.IPProtocolUDP,
SrcIP: fromAddr.Unmap().AsSlice(),
DstIP: toAddr.Unmap().AsSlice(),
}
serialize = append(serialize, ip)
netLayer = ip
} else {
if !fromAddr.Is4() {
panic("Cant send ipv4 to ipv6")
}
ip := &layers.IPv4{
Version: 4,
TTL: 64,
Protocol: layers.IPProtocolUDP,
SrcIP: fromAddr.Unmap().AsSlice(),
DstIP: toAddr.Unmap().AsSlice(),
}
serialize = append(serialize, ip)
netLayer = ip
}
udp := layers.UDP{
SrcPort: layers.UDPPort(fromPort),
DstPort: layers.UDPPort(toPort),
}
err := udp.SetNetworkLayerForChecksum(netLayer)
if err != nil {
panic(err)
}
buffer := gopacket.NewSerializeBuffer()
opt := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
serialize = append(serialize, &udp, gopacket.Payload(data))
err = gopacket.SerializeLayers(buffer, opt, serialize...)
if err != nil {
panic(err)
}
c.f.inside.(*overlay.TestTun).Send(buffer.Bytes())
} }
func (c *Control) GetVpnAddrs() []netip.Addr { func (c *Control) GetVpnAddrs() []netip.Addr {
-68
View File
@@ -1,68 +0,0 @@
//go:build e2e_testing
// +build e2e_testing
package e2e
import (
"testing"
"time"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/e2e/router"
)
// BenchmarkHandshake measures end-to-end tunnel establishment time. The two
// nodes and the router are constructed once before the loop so the timed window
// is just the handshake itself: trigger packet -> handshake1 -> handshake2 ->
// cached packet replay -> arrival on the remote TUN. Between iterations we
// tear down both sides locally (no CloseTunnel notification on the wire) and
// re-inject the lighthouse address that closeTunnel cleared, so the next
// iteration runs through a fresh handshake against the same harness.
func BenchmarkHandshake(b *testing.B) {
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
// Default try_interval is 100ms. The handshake manager schedules handshake1
// on its OutboundHandshakeTimer rather than firing immediately on trigger
// (the trigger channel only fast-paths static hosts), so a 100ms default
// drowns the actual handshake cost. Drop it to 1ms so the bench reflects
// the computation, not the wheel cadence.
bovr := m{"handshakes": m{"try_interval": "1ms"}}
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.1/24", bovr)
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.2/24", bovr)
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
myControl.Start()
theirControl.Start()
defer myControl.Stop()
defer theirControl.Stop()
r := router.NewR(b, myControl, theirControl)
r.CancelFlowLogs()
r.EnableFanIn()
trigger := BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
myControl.InjectTunPacket(trigger)
// RouteForAllUntilTxTun returns the moment the cached packet arrives at
// the remote TUN, which is also when both sides are fully established.
_ = r.RouteForAllUntilTxTun(theirControl)
b.StopTimer()
// Local-only close removes hostmap state on both sides without putting a
// CloseTunnel packet on the wire that we'd then have to drain. The
// closeTunnel path also clears learned lighthouse state for the peer
// when the last hostinfo for that addr goes away, so we re-inject.
myControl.CloseTunnel(theirVpnIpNet[0].Addr(), true)
theirControl.CloseTunnel(myVpnIpNet[0].Addr(), true)
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
theirControl.InjectLightHouseAddr(myVpnIpNet[0].Addr(), myUdpAddr)
b.StartTimer()
}
}
+12 -12
View File
@@ -47,7 +47,7 @@ func TestHandshakeRetransmitDuplicate(t *testing.T) {
defer r.RenderFlow() defer r.RenderFlow()
t.Log("Trigger handshake from me to them") t.Log("Trigger handshake from me to them")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))
t.Log("Grab my msg1") t.Log("Grab my msg1")
msg1 := myControl.GetFromUDP(true) msg1 := myControl.GetFromUDP(true)
@@ -97,7 +97,7 @@ func TestHandshakeTruncatedPacketRecovery(t *testing.T) {
defer r.RenderFlow() defer r.RenderFlow()
t.Log("Trigger handshake") t.Log("Trigger handshake")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))
t.Log("Get msg1 and deliver to responder") t.Log("Get msg1 and deliver to responder")
msg1 := myControl.GetFromUDP(true) msg1 := myControl.GetFromUDP(true)
@@ -146,7 +146,7 @@ func TestHandshakeOrphanedMsg2Dropped(t *testing.T) {
defer r.RenderFlow() defer r.RenderFlow()
t.Log("Complete a normal handshake") t.Log("Complete a normal handshake")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))
r.RouteForAllUntilTxTun(theirControl) r.RouteForAllUntilTxTun(theirControl)
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r) assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
@@ -248,7 +248,7 @@ func TestHandshakeLateResponse(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger handshake from me") t.Log("Trigger handshake from me")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))
t.Log("Grab msg1 but don't deliver") t.Log("Grab msg1 but don't deliver")
msg1 := myControl.GetFromUDP(true) msg1 := myControl.GetFromUDP(true)
@@ -292,7 +292,7 @@ func TestHandshakeSelfConnectionRejected(t *testing.T) {
myControl.Start() myControl.Start()
t.Log("Trigger handshake from me") t.Log("Trigger handshake from me")
myControl.InjectTunPacket(BuildTunUDPPacket(netip.MustParseAddr("10.128.0.2"), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))) myControl.InjectTunUDPPacket(netip.MustParseAddr("10.128.0.2"), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))
msg1 := myControl.GetFromUDP(true) msg1 := myControl.GetFromUDP(true)
t.Log("Drain any handshake retransmits before injecting") t.Log("Drain any handshake retransmits before injecting")
@@ -375,7 +375,7 @@ func TestHandshakeRemoteAllowList(t *testing.T) {
defer r.RenderFlow() defer r.RenderFlow()
t.Log("Trigger handshake from them") t.Log("Trigger handshake from them")
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi"))
msg1 := theirControl.GetFromUDP(true) msg1 := theirControl.GetFromUDP(true)
t.Log("Rewrite the source to a blocked IP and inject") t.Log("Rewrite the source to a blocked IP and inject")
@@ -426,7 +426,7 @@ func TestHandshakeAlreadySeenPreferredRemote(t *testing.T) {
defer r.RenderFlow() defer r.RenderFlow()
t.Log("Complete a normal handshake via the router") t.Log("Complete a normal handshake via the router")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi"))
r.RouteForAllUntilTxTun(theirControl) r.RouteForAllUntilTxTun(theirControl)
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r) assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
@@ -437,7 +437,7 @@ func TestHandshakeAlreadySeenPreferredRemote(t *testing.T) {
originalRemote := hi.CurrentRemote originalRemote := hi.CurrentRemote
t.Log("Re-trigger traffic to cause a new handshake attempt (ErrAlreadySeen)") t.Log("Re-trigger traffic to cause a new handshake attempt (ErrAlreadySeen)")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("roam"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("roam"))
r.RouteForAllUntilTxTun(theirControl) r.RouteForAllUntilTxTun(theirControl)
t.Log("Verify tunnel still works") t.Log("Verify tunnel still works")
@@ -475,8 +475,8 @@ func TestHandshakeWrongResponderPacketStore(t *testing.T) {
evilControl.Start() evilControl.Start()
t.Log("Send multiple packets to them (cached during handshake)") t.Log("Send multiple packets to them (cached during handshake)")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("packet1"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("packet1"))
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("packet2"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("packet2"))
t.Log("Route until evil tunnel is closed") t.Log("Route until evil tunnel is closed")
h := &header.H{} h := &header.H{}
@@ -540,7 +540,7 @@ func TestHandshakeRelayComplete(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger handshake via relay") t.Log("Trigger handshake via relay")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi via relay"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi via relay"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
assertUdpPacket(t, []byte("Hi via relay"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80) assertUdpPacket(t, []byte("Hi via relay"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
@@ -568,7 +568,7 @@ func TestHandshakeRelayComplete(t *testing.T) {
} }
// NOTE: Relay V1 cert + IPv6 rejection is not tested here because // NOTE: Relay V1 cert + IPv6 rejection is not tested here because
// BuildTunUDPPacket from a V4 node to a V6 address panics in the test // InjectTunUDPPacket from a V4 node to a V6 address panics in the test
// framework. The check is in handshake_manager.go handleOutbound relay // framework. The check is in handshake_manager.go handleOutbound relay
// logic (lines ~304-313): if the relay host has a V1 cert and either // logic (lines ~304-313): if the relay host has a V1 cert and either
// address is IPv6, the relay is skipped. // address is IPv6, the relay is skipped.
+31 -47
View File
@@ -16,7 +16,6 @@ import (
"github.com/slackhq/nebula/cert_test" "github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/e2e/router" "github.com/slackhq/nebula/e2e/router"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/overlay"
"github.com/slackhq/nebula/udp" "github.com/slackhq/nebula/udp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -40,22 +39,11 @@ func BenchmarkHotPath(b *testing.B) {
r.CancelFlowLogs() r.CancelFlowLogs()
assertTunnel(b, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r) assertTunnel(b, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
// Pre-build the IP packet bytes once so the bench measures the data plane,
// not gopacket SerializeLayers overhead.
prebuilt := BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
// EnableFanIn switches the router to a 0-alloc routing path. Required
// for hot-path benchmarks; would conflict with GetFromUDP-using tests.
r.EnableFanIn()
b.ResetTimer() b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
myControl.InjectTunPacket(prebuilt) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
// Release the TUN-side bytes back to the harness freelist; the bench _ = r.RouteForAllUntilTxTun(theirControl)
// just confirms a packet arrived, the contents aren't inspected.
overlay.ReleaseTunBuf(r.RouteForAllUntilTxTun(theirControl))
} }
myControl.Stop() myControl.Stop()
@@ -83,15 +71,11 @@ func BenchmarkHotPathRelay(b *testing.B) {
theirControl.Start() theirControl.Start()
assertTunnel(b, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r) assertTunnel(b, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
prebuilt := BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
r.EnableFanIn()
b.ResetTimer() b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
myControl.InjectTunPacket(prebuilt) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
overlay.ReleaseTunBuf(r.RouteForAllUntilTxTun(theirControl)) _ = r.RouteForAllUntilTxTun(theirControl)
} }
myControl.Stop() myControl.Stop()
@@ -113,7 +97,7 @@ func TestGoodHandshake(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Send a udp packet through to begin standing up the tunnel, this should come out the other side") t.Log("Send a udp packet through to begin standing up the tunnel, this should come out the other side")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
t.Log("Have them consume my stage 0 packet. They have a tunnel now") t.Log("Have them consume my stage 0 packet. They have a tunnel now")
theirControl.InjectUDPPacket(myControl.GetFromUDP(true)) theirControl.InjectUDPPacket(myControl.GetFromUDP(true))
@@ -165,7 +149,7 @@ func TestGoodHandshakeNoOverlap(t *testing.T) {
empty := []byte{} empty := []byte{}
t.Log("do something to cause a handshake") t.Log("do something to cause a handshake")
myControl.GetF().SendMessageToVpnAddr(header.Test, header.MessageNone, theirVpnIpNet[0].Addr(), empty, nebula.NewWireBuffer(9001, 0)) myControl.GetF().SendMessageToVpnAddr(header.Test, header.MessageNone, theirVpnIpNet[0].Addr(), empty, empty, empty)
t.Log("Have them consume my stage 0 packet. They have a tunnel now") t.Log("Have them consume my stage 0 packet. They have a tunnel now")
theirControl.InjectUDPPacket(myControl.GetFromUDP(true)) theirControl.InjectUDPPacket(myControl.GetFromUDP(true))
@@ -207,7 +191,7 @@ func TestWrongResponderHandshake(t *testing.T) {
evilControl.Start() evilControl.Start()
t.Log("Start the handshake process, we will route until we see the evil tunnel closed") t.Log("Start the handshake process, we will route until we see the evil tunnel closed")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
h := &header.H{} h := &header.H{}
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType { r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
@@ -289,7 +273,7 @@ func TestWrongResponderHandshakeStaticHostMap(t *testing.T) {
evilControl.Start() evilControl.Start()
t.Log("Start the handshake process, we will route until we see the evil tunnel closed") t.Log("Start the handshake process, we will route until we see the evil tunnel closed")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
h := &header.H{} h := &header.H{}
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType { r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
@@ -368,8 +352,8 @@ func TestStage1Race(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger a handshake to start on both me and them") t.Log("Trigger a handshake to start on both me and them")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
t.Log("Get both stage 1 handshake packets") t.Log("Get both stage 1 handshake packets")
myHsForThem := myControl.GetFromUDP(true) myHsForThem := myControl.GetFromUDP(true)
@@ -446,7 +430,7 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
theirControl.Start() theirControl.Start()
r.Log("Trigger a handshake from me to them") r.Log("Trigger a handshake from me to them")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80) assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
@@ -457,7 +441,7 @@ func TestUncleanShutdownRaceLoser(t *testing.T) {
myHostmap.Indexes = map[uint32]*nebula.HostInfo{} myHostmap.Indexes = map[uint32]*nebula.HostInfo{}
myHostmap.RemoteIndexes = map[uint32]*nebula.HostInfo{} myHostmap.RemoteIndexes = map[uint32]*nebula.HostInfo{}
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me again"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me again"))
p = r.RouteForAllUntilTxTun(theirControl) p = r.RouteForAllUntilTxTun(theirControl)
assertUdpPacket(t, []byte("Hi from me again"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80) assertUdpPacket(t, []byte("Hi from me again"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
@@ -496,7 +480,7 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
theirControl.Start() theirControl.Start()
r.Log("Trigger a handshake from me to them") r.Log("Trigger a handshake from me to them")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80) assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
@@ -508,7 +492,7 @@ func TestUncleanShutdownRaceWinner(t *testing.T) {
theirHostmap.Indexes = map[uint32]*nebula.HostInfo{} theirHostmap.Indexes = map[uint32]*nebula.HostInfo{}
theirHostmap.RemoteIndexes = map[uint32]*nebula.HostInfo{} theirHostmap.RemoteIndexes = map[uint32]*nebula.HostInfo{}
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them again"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them again"))
p = r.RouteForAllUntilTxTun(myControl) p = r.RouteForAllUntilTxTun(myControl)
assertUdpPacket(t, []byte("Hi from them again"), p, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), 80, 80) assertUdpPacket(t, []byte("Hi from them again"), p, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), 80, 80)
r.RenderHostmaps("Derp hostmaps", myControl, theirControl) r.RenderHostmaps("Derp hostmaps", myControl, theirControl)
@@ -551,7 +535,7 @@ func TestRelays(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger a handshake from me to them via the relay") t.Log("Trigger a handshake from me to them via the relay")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
@@ -581,7 +565,7 @@ func TestRelaysDontCareAboutIps(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger a handshake from me to them via the relay") t.Log("Trigger a handshake from me to them via the relay")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
@@ -611,14 +595,14 @@ func TestReestablishRelays(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger a handshake from me to them via the relay") t.Log("Trigger a handshake from me to them via the relay")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80) assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
t.Log("Ensure packet traversal from them to me via the relay") t.Log("Ensure packet traversal from them to me via the relay")
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
p = r.RouteForAllUntilTxTun(myControl) p = r.RouteForAllUntilTxTun(myControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
@@ -633,7 +617,7 @@ func TestReestablishRelays(t *testing.T) {
for curIndexes >= start { for curIndexes >= start {
curIndexes = len(myControl.GetHostmap().Indexes) curIndexes = len(myControl.GetHostmap().Indexes)
r.Logf("Wait for the dead index to go away:start=%v indexes, current=%v indexes", start, curIndexes) r.Logf("Wait for the dead index to go away:start=%v indexes, current=%v indexes", start, curIndexes)
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me should fail"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me should fail"))
r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType { r.RouteForAllExitFunc(func(p *udp.Packet, c *nebula.Control) router.ExitType {
return router.RouteAndExit return router.RouteAndExit
@@ -650,7 +634,7 @@ func TestReestablishRelays(t *testing.T) {
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr) myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()}) myControl.InjectRelays(theirVpnIpNet[0].Addr(), []netip.Addr{relayVpnIpNet[0].Addr()})
relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr) relayControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
p = r.RouteForAllUntilTxTun(theirControl) p = r.RouteForAllUntilTxTun(theirControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
@@ -685,7 +669,7 @@ func TestReestablishRelays(t *testing.T) {
t.Log("Assert the tunnel works the other way, too") t.Log("Assert the tunnel works the other way, too")
for { for {
t.Log("RouteForAllUntilTxTun") t.Log("RouteForAllUntilTxTun")
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
p = r.RouteForAllUntilTxTun(myControl) p = r.RouteForAllUntilTxTun(myControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
@@ -755,8 +739,8 @@ func TestStage1RaceRelays(t *testing.T) {
assertTunnel(t, theirVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), theirControl, relayControl, r) assertTunnel(t, theirVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), theirControl, relayControl, r)
r.Log("Trigger a handshake from both them and me via relay to them and me") r.Log("Trigger a handshake from both them and me via relay to them and me")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
r.Log("Wait for a packet from them to me") r.Log("Wait for a packet from them to me")
p := r.RouteForAllUntilTxTun(myControl) p := r.RouteForAllUntilTxTun(myControl)
@@ -803,8 +787,8 @@ func TestStage1RaceRelays2(t *testing.T) {
assertTunnel(t, theirVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), theirControl, relayControl, r) assertTunnel(t, theirVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), theirControl, relayControl, r)
r.Log("Trigger a handshake from both them and me via relay to them and me") r.Log("Trigger a handshake from both them and me via relay to them and me")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
//r.RouteUntilAfterMsgType(myControl, header.Control, header.MessageNone) //r.RouteUntilAfterMsgType(myControl, header.Control, header.MessageNone)
//r.RouteUntilAfterMsgType(theirControl, header.Control, header.MessageNone) //r.RouteUntilAfterMsgType(theirControl, header.Control, header.MessageNone)
@@ -868,7 +852,7 @@ func TestRehandshakingRelays(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger a handshake from me to them via the relay") t.Log("Trigger a handshake from me to them via the relay")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
@@ -973,7 +957,7 @@ func TestRehandshakingRelaysPrimary(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger a handshake from me to them via the relay") t.Log("Trigger a handshake from me to them via the relay")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
@@ -1275,8 +1259,8 @@ func TestRaceRegression(t *testing.T) {
//them rx stage:2 initiatorIndex=120607833 responderIndex=4209862089 //them rx stage:2 initiatorIndex=120607833 responderIndex=4209862089
t.Log("Start both handshakes") t.Log("Start both handshakes")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, theirVpnIpNet[0].Addr(), 80, []byte("Hi from them"))
t.Log("Get both stage 1") t.Log("Get both stage 1")
myStage1ForThem := myControl.GetFromUDP(true) myStage1ForThem := myControl.GetFromUDP(true)
@@ -1492,7 +1476,7 @@ func TestGoodHandshakeUnsafeDest(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Send a udp packet through to begin standing up the tunnel, this should come out the other side") t.Log("Send a udp packet through to begin standing up the tunnel, this should come out the other side")
myControl.InjectTunPacket(BuildTunUDPPacket(spookyDest, 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(spookyDest, 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me"))
t.Log("Have them consume my stage 0 packet. They have a tunnel now") t.Log("Have them consume my stage 0 packet. They have a tunnel now")
theirControl.InjectUDPPacket(myControl.GetFromUDP(true)) theirControl.InjectUDPPacket(myControl.GetFromUDP(true))
@@ -1520,7 +1504,7 @@ func TestGoodHandshakeUnsafeDest(t *testing.T) {
assertUdpPacket(t, []byte("Hi from me"), myCachedPacket, myVpnIpNet[0].Addr(), spookyDest, 80, 80) assertUdpPacket(t, []byte("Hi from me"), myCachedPacket, myVpnIpNet[0].Addr(), spookyDest, 80, 80)
//reply //reply
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnIpNet[0].Addr(), 80, spookyDest, 80, []byte("Hi from the spookyman"))) theirControl.InjectTunUDPPacket(myVpnIpNet[0].Addr(), 80, spookyDest, 80, []byte("Hi from the spookyman"))
//wait for reply //wait for reply
theirControl.WaitForType(1, 0, myControl) theirControl.WaitForType(1, 0, myControl)
theirCachedPacket := myControl.GetFromTun(true) theirCachedPacket := myControl.GetFromTun(true)
+2 -57
View File
@@ -294,12 +294,12 @@ func deadline(t *testing.T, seconds time.Duration) doneCb {
func assertTunnel(t testing.TB, vpnIpA, vpnIpB netip.Addr, controlA, controlB *nebula.Control, r *router.R) { func assertTunnel(t testing.TB, vpnIpA, vpnIpB netip.Addr, controlA, controlB *nebula.Control, r *router.R) {
// Send a packet from them to me // Send a packet from them to me
controlB.InjectTunPacket(BuildTunUDPPacket(vpnIpA, 80, vpnIpB, 90, []byte("Hi from B"))) controlB.InjectTunUDPPacket(vpnIpA, 80, vpnIpB, 90, []byte("Hi from B"))
bPacket := r.RouteForAllUntilTxTun(controlA) bPacket := r.RouteForAllUntilTxTun(controlA)
assertUdpPacket(t, []byte("Hi from B"), bPacket, vpnIpB, vpnIpA, 90, 80) assertUdpPacket(t, []byte("Hi from B"), bPacket, vpnIpB, vpnIpA, 90, 80)
// And once more from me to them // And once more from me to them
controlA.InjectTunPacket(BuildTunUDPPacket(vpnIpB, 80, vpnIpA, 90, []byte("Hello from A"))) controlA.InjectTunUDPPacket(vpnIpB, 80, vpnIpA, 90, []byte("Hello from A"))
aPacket := r.RouteForAllUntilTxTun(controlB) aPacket := r.RouteForAllUntilTxTun(controlB)
assertUdpPacket(t, []byte("Hello from A"), aPacket, vpnIpA, vpnIpB, 90, 80) assertUdpPacket(t, []byte("Hello from A"), aPacket, vpnIpA, vpnIpB, 90, 80)
} }
@@ -408,58 +408,3 @@ func testLogLevelName() string {
} }
return "info" return "info"
} }
// BuildTunUDPPacket assembles an IP+UDP packet suitable for Control.InjectTunPacket.
// Using UDP here because it's a simpler protocol.
func BuildTunUDPPacket(toAddr netip.Addr, toPort uint16, fromAddr netip.Addr, fromPort uint16, data []byte) []byte {
serialize := make([]gopacket.SerializableLayer, 0)
var netLayer gopacket.NetworkLayer
if toAddr.Is6() {
if !fromAddr.Is6() {
panic("Cant send ipv6 to ipv4")
}
ip := &layers.IPv6{
Version: 6,
NextHeader: layers.IPProtocolUDP,
SrcIP: fromAddr.Unmap().AsSlice(),
DstIP: toAddr.Unmap().AsSlice(),
}
serialize = append(serialize, ip)
netLayer = ip
} else {
if !fromAddr.Is4() {
panic("Cant send ipv4 to ipv6")
}
ip := &layers.IPv4{
Version: 4,
TTL: 64,
Protocol: layers.IPProtocolUDP,
SrcIP: fromAddr.Unmap().AsSlice(),
DstIP: toAddr.Unmap().AsSlice(),
}
serialize = append(serialize, ip)
netLayer = ip
}
udp := layers.UDP{
SrcPort: layers.UDPPort(fromPort),
DstPort: layers.UDPPort(toPort),
}
if err := udp.SetNetworkLayerForChecksum(netLayer); err != nil {
panic(err)
}
buffer := gopacket.NewSerializeBuffer()
opt := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
serialize = append(serialize, &udp, gopacket.Payload(data))
if err := gopacket.SerializeLayers(buffer, opt, serialize...); err != nil {
panic(err)
}
return buffer.Bytes()
}
+54 -188
View File
@@ -13,7 +13,6 @@ import (
"regexp" "regexp"
"sort" "sort"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -25,19 +24,6 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
// outNatKey is the (from, to) pair used by outNat. Comparable struct, so it works as a map key without the
// allocation cost of a string-concat key.
type outNatKey struct {
from, to netip.AddrPort
}
// fannedPacket pairs a UDP TX packet with its source control so the router can route it after popping from
// the fan-in channel.
type fannedPacket struct {
from *nebula.Control
pkt *udp.Packet
}
type R struct { type R struct {
// Simple map of the ip:port registered on a control to the control // Simple map of the ip:port registered on a control to the control
// Basically a router, right? // Basically a router, right?
@@ -48,28 +34,12 @@ type R struct {
// A last used map, if an inbound packet hit the inNat map then // A last used map, if an inbound packet hit the inNat map then
// all return packets should use the same last used inbound address for the outbound sender // all return packets should use the same last used inbound address for the outbound sender
outNat map[outNatKey]netip.AddrPort // map[from address + ":" + to address] => ip:port to rewrite in the udp packet to receiver
outNat map[string]netip.AddrPort
// A map of vpn ip to the nebula control it belongs to // A map of vpn ip to the nebula control it belongs to
vpnControls map[netip.Addr]*nebula.Control vpnControls map[netip.Addr]*nebula.Control
// Cached select infrastructure for RouteForAllUntilTxTun.
// The controls map is immutable after NewR so the cases are good for the test lifetime.
// We only rebuild if a different receiver is asked.
selRecvCtl *nebula.Control
selCases []reflect.SelectCase
selCtls []*nebula.Control
// Optional fan-in mode for hot-path benchmarks: one forwarder goroutine per control drains UDP TX into udpFanIn,
// so RouteForAllUntilTxTun can do a fixed 2-way native select instead of paying reflect.Select per call.
// Off by default (would otherwise interleave with tests that use GetFromUDP directly on the same control).
// Enabled by EnableFanIn.
udpFanIn chan fannedPacket
stopFanIn chan struct{}
fanInWG sync.WaitGroup
fanInMu sync.Mutex
fanInOn atomic.Bool
ignoreFlows []ignoreFlow ignoreFlows []ignoreFlow
flow []flowEntry flow []flowEntry
@@ -149,7 +119,7 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
controls: make(map[netip.AddrPort]*nebula.Control), controls: make(map[netip.AddrPort]*nebula.Control),
vpnControls: make(map[netip.Addr]*nebula.Control), vpnControls: make(map[netip.Addr]*nebula.Control),
inNat: make(map[netip.AddrPort]*nebula.Control), inNat: make(map[netip.AddrPort]*nebula.Control),
outNat: make(map[outNatKey]netip.AddrPort), outNat: make(map[string]netip.AddrPort),
flow: []flowEntry{}, flow: []flowEntry{},
ignoreFlows: []ignoreFlow{}, ignoreFlows: []ignoreFlow{},
fn: filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name())), fn: filepath.Join("mermaid", fmt.Sprintf("%s.md", t.Name())),
@@ -183,10 +153,8 @@ func NewR(t testing.TB, controls ...*nebula.Control) *R {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-clockSource.C: case <-clockSource.C:
r.Lock()
r.renderHostmaps("clock tick") r.renderHostmaps("clock tick")
r.renderFlow() r.renderFlow()
r.Unlock()
} }
} }
}() }()
@@ -212,21 +180,15 @@ func (r *R) AddRoute(ip netip.Addr, port uint16, c *nebula.Control) {
// RenderFlow renders the packet flow seen up until now and stops further automatic renders from happening. // RenderFlow renders the packet flow seen up until now and stops further automatic renders from happening.
func (r *R) RenderFlow() { func (r *R) RenderFlow() {
r.cancelRender() r.cancelRender()
r.Lock()
defer r.Unlock()
r.renderFlow() r.renderFlow()
} }
// CancelFlowLogs stops flow logs from being tracked and destroys any logs already collected // CancelFlowLogs stops flow logs from being tracked and destroys any logs already collected
func (r *R) CancelFlowLogs() { func (r *R) CancelFlowLogs() {
r.cancelRender() r.cancelRender()
r.Lock()
r.flow = nil r.flow = nil
r.Unlock()
} }
// renderFlow writes the flow log to disk. Caller must hold r.Lock. renderFlow reads r.flow / r.additionalGraphs and
// the *packet pointers stashed inside, all of which are mutated under the same lock by routing paths.
func (r *R) renderFlow() { func (r *R) renderFlow() {
if r.flow == nil { if r.flow == nil {
return return
@@ -472,157 +434,68 @@ func (r *R) RouteUntilTxTun(sender *nebula.Control, receiver *nebula.Control) []
panic("No control for udp tx " + a.String()) panic("No control for udp tx " + a.String())
} }
fp := r.unlockedInjectFlow(sender, c, p, false) fp := r.unlockedInjectFlow(sender, c, p, false)
c.InjectUDPPacket(p) // copies internally; original is ours to release c.InjectUDPPacket(p)
fp.WasReceived() fp.WasReceived()
r.Unlock() r.Unlock()
p.Release()
} }
} }
} }
// RouteForAllUntilTxTun will route for everyone and return when a packet is seen on the receiver's tun. // RouteForAllUntilTxTun will route for everyone and return when a packet is seen on receivers tun
// If a control's UDP TX address can't be matched to a registered control, we panic. // If the router doesn't have the nebula controller for that address, we panic
//
// For allocation-sensitive callers (hot-path benchmarks, in particular relay
// benches with 3+ controls), call EnableFanIn() first.
func (r *R) RouteForAllUntilTxTun(receiver *nebula.Control) []byte { func (r *R) RouteForAllUntilTxTun(receiver *nebula.Control) []byte {
if r.fanInOn.Load() {
return r.routeFanIn(receiver)
}
return r.routeReflect(receiver)
}
// routeFanIn is the alloc-free path used when EnableFanIn is in effect.
func (r *R) routeFanIn(receiver *nebula.Control) []byte {
tunTx := receiver.GetTunTxChan()
for {
select {
case p := <-tunTx:
r.Lock()
if r.flow != nil {
np := udp.Packet{Data: make([]byte, len(p))}
copy(np.Data, p)
r.unlockedInjectFlow(receiver, receiver, &np, true)
}
r.Unlock()
return p
case fp := <-r.udpFanIn:
r.routeUDP(fp.from, fp.pkt)
}
}
}
// routeReflect is the default reflect.Select-based path. Pays the boxing allocation per call but doesn't interfere
// with tests that pull packets directly from controls' UDP TX channels via GetFromUDP.
func (r *R) routeReflect(receiver *nebula.Control) []byte {
sc, cm := r.selectCasesFor(receiver)
for {
x, rx, _ := reflect.Select(sc)
if x == 0 {
p := rx.Interface().([]byte)
r.Lock()
if r.flow != nil {
np := udp.Packet{Data: make([]byte, len(p))}
copy(np.Data, p)
r.unlockedInjectFlow(cm[x], cm[x], &np, true)
}
r.Unlock()
return p
}
r.routeUDP(cm[x], rx.Interface().(*udp.Packet))
}
}
// EnableFanIn switches RouteForAllUntilTxTun to the alloc-free fan-in path.
// One forwarder goroutine per registered control drains UDP TX into a shared channel that RouteForAllUntilTxTun selects
// on alongside the receiver's TUN TX channel.
func (r *R) EnableFanIn() {
r.fanInMu.Lock()
defer r.fanInMu.Unlock()
if r.fanInOn.Load() {
return
}
r.udpFanIn = make(chan fannedPacket, 32)
r.stopFanIn = make(chan struct{})
for _, c := range r.controls {
r.startFanInWorker(c)
}
r.fanInOn.Store(true)
r.t.Cleanup(r.stopFanInWorkers)
}
// startFanInWorker spawns a goroutine that drains c's UDP TX into r.udpFanIn.
func (r *R) startFanInWorker(c *nebula.Control) {
r.fanInWG.Add(1)
udpTx := c.GetUDPTxChan()
go func() {
defer r.fanInWG.Done()
for {
select {
case <-r.stopFanIn:
return
case p := <-udpTx:
select {
case <-r.stopFanIn:
p.Release()
return
case r.udpFanIn <- fannedPacket{from: c, pkt: p}:
}
}
}
}()
}
// stopFanInWorkers signals the fan-in goroutines to exit and waits for them.
func (r *R) stopFanInWorkers() {
r.fanInMu.Lock()
wasOn := r.fanInOn.Swap(false)
r.fanInMu.Unlock()
if !wasOn {
return
}
close(r.stopFanIn)
r.fanInWG.Wait()
}
// routeUDP forwards a UDP TX packet from the named source control to the destination control derived from p.To,
// releasing the source packet after InjectUDPPacket has copied its bytes into a fresh pool slot.
func (r *R) routeUDP(from *nebula.Control, p *udp.Packet) {
r.Lock()
defer r.Unlock()
a := from.GetUDPAddr()
c := r.getControl(a, p.To, p)
if c == nil {
panic(fmt.Sprintf("No control for udp tx %s", p.To))
}
fp := r.unlockedInjectFlow(from, c, p, false)
c.InjectUDPPacket(p) // copies internally; original is ours to release
fp.WasReceived()
p.Release()
}
// selectCasesFor returns the SelectCase array used by routeReflect: one slot for the receiver's TUN TX channel followed
// by one per control's UDP TX channel. Cached for the test lifetime, only rebuilt if the receiver changes.
func (r *R) selectCasesFor(receiver *nebula.Control) ([]reflect.SelectCase, []*nebula.Control) {
r.Lock()
defer r.Unlock()
if r.selRecvCtl == receiver && r.selCases != nil {
return r.selCases, r.selCtls
}
sc := make([]reflect.SelectCase, len(r.controls)+1) sc := make([]reflect.SelectCase, len(r.controls)+1)
cm := make([]*nebula.Control, len(r.controls)+1) cm := make([]*nebula.Control, len(r.controls)+1)
sc[0] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(receiver.GetTunTxChan())}
cm[0] = receiver i := 0
i := 1 sc[i] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(receiver.GetTunTxChan()),
Send: reflect.Value{},
}
cm[i] = receiver
i++
for _, c := range r.controls { for _, c := range r.controls {
sc[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c.GetUDPTxChan())} sc[i] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(c.GetUDPTxChan()),
Send: reflect.Value{},
}
cm[i] = c cm[i] = c
i++ i++
} }
r.selRecvCtl = receiver
r.selCases = sc for {
r.selCtls = cm x, rx, _ := reflect.Select(sc)
return sc, cm r.Lock()
if x == 0 {
// we are the tun tx, we can exit
p := rx.Interface().([]byte)
np := udp.Packet{Data: make([]byte, len(p))}
copy(np.Data, p)
r.unlockedInjectFlow(cm[x], cm[x], &np, true)
r.Unlock()
return p
} else {
// we are a udp tx, route and continue
p := rx.Interface().(*udp.Packet)
a := cm[x].GetUDPAddr()
c := r.getControl(a, p.To, p)
if c == nil {
r.Unlock()
panic(fmt.Sprintf("No control for udp tx %s", p.To))
}
fp := r.unlockedInjectFlow(cm[x], c, p, false)
c.InjectUDPPacket(p)
fp.WasReceived()
}
r.Unlock()
}
} }
// RouteExitFunc will call the whatDo func with each udp packet from sender. // RouteExitFunc will call the whatDo func with each udp packet from sender.
@@ -649,7 +522,6 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
switch e { switch e {
case ExitNow: case ExitNow:
r.Unlock() r.Unlock()
p.Release()
return return
case RouteAndExit: case RouteAndExit:
@@ -657,7 +529,6 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
receiver.InjectUDPPacket(p) receiver.InjectUDPPacket(p)
fp.WasReceived() fp.WasReceived()
r.Unlock() r.Unlock()
p.Release()
return return
case KeepRouting: case KeepRouting:
@@ -670,7 +541,6 @@ func (r *R) RouteExitFunc(sender *nebula.Control, whatDo ExitFunc) {
} }
r.Unlock() r.Unlock()
p.Release()
} }
} }
@@ -771,7 +641,6 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
switch e { switch e {
case ExitNow: case ExitNow:
r.Unlock() r.Unlock()
p.Release()
return return
case RouteAndExit: case RouteAndExit:
@@ -779,7 +648,6 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
receiver.InjectUDPPacket(p) receiver.InjectUDPPacket(p)
fp.WasReceived() fp.WasReceived()
r.Unlock() r.Unlock()
p.Release()
return return
case KeepRouting: case KeepRouting:
@@ -791,7 +659,6 @@ func (r *R) RouteForAllExitFunc(whatDo ExitFunc) {
panic(fmt.Sprintf("Unknown exitFunc return: %v", e)) panic(fmt.Sprintf("Unknown exitFunc return: %v", e))
} }
r.Unlock() r.Unlock()
p.Release()
} }
} }
@@ -835,20 +702,19 @@ func (r *R) FlushAll() {
} }
receiver.InjectUDPPacket(p) receiver.InjectUDPPacket(p)
r.Unlock() r.Unlock()
p.Release()
} }
} }
// getControl performs or seeds NAT translation and returns the control for toAddr, p from fields may change // getControl performs or seeds NAT translation and returns the control for toAddr, p from fields may change
// This is an internal router function, the caller must hold the lock // This is an internal router function, the caller must hold the lock
func (r *R) getControl(fromAddr, toAddr netip.AddrPort, p *udp.Packet) *nebula.Control { func (r *R) getControl(fromAddr, toAddr netip.AddrPort, p *udp.Packet) *nebula.Control {
if newAddr, ok := r.outNat[outNatKey{from: fromAddr, to: toAddr}]; ok { if newAddr, ok := r.outNat[fromAddr.String()+":"+toAddr.String()]; ok {
p.From = newAddr p.From = newAddr
} }
c, ok := r.inNat[toAddr] c, ok := r.inNat[toAddr]
if ok { if ok {
r.outNat[outNatKey{from: c.GetUDPAddr(), to: fromAddr}] = toAddr r.outNat[c.GetUDPAddr().String()+":"+fromAddr.String()] = toAddr
return c return c
} }
+2 -2
View File
@@ -355,14 +355,14 @@ func TestCrossStackRelaysWork(t *testing.T) {
theirControl.Start() theirControl.Start()
t.Log("Trigger a handshake from me to them via the relay") t.Log("Trigger a handshake from me to them via the relay")
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnV6.Addr(), 80, myVpnV6.Addr(), 80, []byte("Hi from me"))) myControl.InjectTunUDPPacket(theirVpnV6.Addr(), 80, myVpnV6.Addr(), 80, []byte("Hi from me"))
p := r.RouteForAllUntilTxTun(theirControl) p := r.RouteForAllUntilTxTun(theirControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
assertUdpPacket(t, []byte("Hi from me"), p, myVpnV6.Addr(), theirVpnV6.Addr(), 80, 80) assertUdpPacket(t, []byte("Hi from me"), p, myVpnV6.Addr(), theirVpnV6.Addr(), 80, 80)
t.Log("reply?") t.Log("reply?")
theirControl.InjectTunPacket(BuildTunUDPPacket(myVpnV6.Addr(), 80, theirVpnV6.Addr(), 80, []byte("Hi from them"))) theirControl.InjectTunUDPPacket(myVpnV6.Addr(), 80, theirVpnV6.Addr(), 80, []byte("Hi from them"))
p = r.RouteForAllUntilTxTun(myControl) p = r.RouteForAllUntilTxTun(myControl)
assertUdpPacket(t, []byte("Hi from them"), p, theirVpnV6.Addr(), myVpnV6.Addr(), 80, 80) assertUdpPacket(t, []byte("Hi from them"), p, theirVpnV6.Addr(), myVpnV6.Addr(), 80, 80)
+6 -12
View File
@@ -971,11 +971,11 @@ func (hm *HandshakeManager) continueHandshake(via ViaSender, hh *HandshakeHostIn
if f.l.Enabled(context.Background(), slog.LevelDebug) { if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debug("Sending stored packets", "count", len(hh.packetStore)) hostinfo.logger(f.l).Debug("Sending stored packets", "count", len(hh.packetStore))
} }
buf := f.bufAlloc.Acquire() nb := make([]byte, 12, 12)
out := make([]byte, mtu)
for _, cp := range hh.packetStore { for _, cp := range hh.packetStore {
cp.callback(cp.messageType, cp.messageSubType, hostinfo, cp.packet, buf) cp.callback(cp.messageType, cp.messageSubType, hostinfo, cp.packet, nb, out)
} }
f.bufAlloc.Release(buf)
f.cachedPacketMetrics.sent.Inc(int64(len(hh.packetStore))) f.cachedPacketMetrics.sent.Inc(int64(len(hh.packetStore)))
} }
@@ -1085,9 +1085,7 @@ func (hm *HandshakeManager) sendHandshakeResponse(via ViaSender, msg []byte, hos
// We received a valid handshake on this relay, so make sure the relay // We received a valid handshake on this relay, so make sure the relay
// state reflects that, in case it had been marked Disestablished. // state reflects that, in case it had been marked Disestablished.
via.relayHI.relayState.UpdateRelayForByIdxState(via.remoteIdx, Established) via.relayHI.relayState.UpdateRelayForByIdxState(via.remoteIdx, Established)
buf := f.bufAlloc.Acquire() f.SendVia(via.relayHI, via.relay, msg, make([]byte, 12), make([]byte, mtu), false)
f.SendVia(via.relayHI, via.relay, msg, buf)
f.bufAlloc.Release(buf)
f.l.Info("Handshake message sent", append(logFields, "relay", via.relayHI.vpnAddrs[0])...) f.l.Info("Handshake message sent", append(logFields, "relay", via.relayHI.vpnAddrs[0])...)
} }
} }
@@ -1104,9 +1102,7 @@ func (hm *HandshakeManager) handleCheckAndCompleteError(err error, existing, hos
switch err { switch err {
case ErrAlreadySeen: case ErrAlreadySeen:
if existing.SetRemoteIfPreferred(f.hostMap, via) { if existing.SetRemoteIfPreferred(f.hostMap, via) {
buf := f.bufAlloc.Acquire() f.SendMessageToVpnAddr(header.Test, header.TestRequest, hostinfo.vpnAddrs[0], []byte(""), make([]byte, 12, 12), make([]byte, mtu))
f.SendMessageToVpnAddr(header.Test, header.TestRequest, hostinfo.vpnAddrs[0], []byte(""), buf)
f.bufAlloc.Release(buf)
} }
// Resend the original response. The peer is committed to that response's // Resend the original response. The peer is committed to that response's
// ephemeral keys; a freshly-built one would have different keys and break // ephemeral keys; a freshly-built one would have different keys and break
@@ -1129,9 +1125,7 @@ func (hm *HandshakeManager) handleCheckAndCompleteError(err error, existing, hos
"responderIndex", hostinfo.localIndexId, "responderIndex", hostinfo.localIndexId,
"handshake", hsFields, "handshake", hsFields,
) )
buf := f.bufAlloc.Acquire() f.SendMessageToVpnAddr(header.Test, header.TestRequest, hostinfo.vpnAddrs[0], []byte(""), make([]byte, 12, 12), make([]byte, mtu))
f.SendMessageToVpnAddr(header.Test, header.TestRequest, hostinfo.vpnAddrs[0], []byte(""), buf)
f.bufAlloc.Release(buf)
case ErrLocalIndexCollision: case ErrLocalIndexCollision:
f.l.Error("Failed to add HostInfo due to localIndex collision", f.l.Error("Failed to add HostInfo due to localIndex collision",
+3 -3
View File
@@ -80,15 +80,15 @@ func testCountTimerWheelEntries(tw *LockingTimerWheel[netip.Addr]) (c int) {
type mockEncWriter struct { type mockEncWriter struct {
} }
func (mw *mockEncWriter) SendMessageToVpnAddr(_ header.MessageType, _ header.MessageSubType, _ netip.Addr, _ []byte, _ *WireBuffer) { func (mw *mockEncWriter) SendMessageToVpnAddr(_ header.MessageType, _ header.MessageSubType, _ netip.Addr, _, _, _ []byte) {
return return
} }
func (mw *mockEncWriter) SendVia(_ *HostInfo, _ *Relay, _ []byte, _ *WireBuffer) { func (mw *mockEncWriter) SendVia(_ *HostInfo, _ *Relay, _, _, _ []byte, _ bool) {
return return
} }
func (mw *mockEncWriter) SendMessageToHostInfo(_ header.MessageType, _ header.MessageSubType, _ *HostInfo, _ []byte, _ *WireBuffer) { func (mw *mockEncWriter) SendMessageToHostInfo(_ header.MessageType, _ header.MessageSubType, _ *HostInfo, _, _, _ []byte) {
return return
} }
+4
View File
@@ -57,6 +57,8 @@ const (
const ( const (
TestRequest MessageSubType = 0 TestRequest MessageSubType = 0
TestReply MessageSubType = 1 TestReply MessageSubType = 1
MTUDProbeRequest MessageSubType = 2
MTUDProbeReply MessageSubType = 3
) )
const ( const (
@@ -69,6 +71,8 @@ var ErrHeaderTooShort = errors.New("header is too short")
var subTypeTestMap = map[MessageSubType]string{ var subTypeTestMap = map[MessageSubType]string{
TestRequest: "testRequest", TestRequest: "testRequest",
TestReply: "testReply", TestReply: "testReply",
MTUDProbeRequest: "mtudProbeRequest",
MTUDProbeReply: "mtudProbeReply",
} }
var subTypeNoneMap = map[MessageSubType]string{0: "none"} var subTypeNoneMap = map[MessageSubType]string{0: "none"}
+2 -4
View File
@@ -308,7 +308,7 @@ type cachedPacket struct {
packet []byte packet []byte
} }
type packetCallback func(t header.MessageType, st header.MessageSubType, h *HostInfo, p []byte, buf *WireBuffer) type packetCallback func(t header.MessageType, st header.MessageSubType, h *HostInfo, p, nb, out []byte)
type cachedPacketMetrics struct { type cachedPacketMetrics struct {
sent metrics.Counter sent metrics.Counter
@@ -691,7 +691,6 @@ func (i *HostInfo) TryPromoteBest(preferredRanges []netip.Prefix, ifce *Interfac
} }
} }
buf := ifce.bufAlloc.Acquire()
i.remotes.ForEach(preferredRanges, func(addr netip.AddrPort, preferred bool) { i.remotes.ForEach(preferredRanges, func(addr netip.AddrPort, preferred bool) {
if remote.IsValid() && (!addr.IsValid() || !preferred) { if remote.IsValid() && (!addr.IsValid() || !preferred) {
return return
@@ -699,9 +698,8 @@ func (i *HostInfo) TryPromoteBest(preferredRanges []netip.Prefix, ifce *Interfac
// Try to send a test packet to that host, this should // Try to send a test packet to that host, this should
// cause it to detect a roaming event and switch remotes // cause it to detect a roaming event and switch remotes
ifce.sendTo(header.Test, header.TestRequest, i.ConnectionState, i, addr, []byte(""), buf) ifce.sendTo(header.Test, header.TestRequest, i.ConnectionState, i, addr, []byte(""), make([]byte, 12, 12), make([]byte, mtu))
}) })
ifce.bufAlloc.Release(buf)
} }
// Re query our lighthouses for new remotes occasionally // Re query our lighthouses for new remotes occasionally
+122 -75
View File
@@ -8,13 +8,12 @@ import (
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/iputil" "github.com/slackhq/nebula/iputil"
"github.com/slackhq/nebula/noiseutil"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
func (f *Interface) consumeInsidePacket(buf *WireBuffer, q int, localCache firewall.ConntrackCache) { func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet, nb, out []byte, q int, localCache firewall.ConntrackCache) {
packet := buf.IPPacket() err := newPacket(packet, false, fwPacket)
err := newPacket(packet, false, buf.FwPacket)
if err != nil { if err != nil {
if f.l.Enabled(context.Background(), slog.LevelDebug) { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.Debug("Error while validating outbound packet", f.l.Debug("Error while validating outbound packet",
@@ -27,12 +26,12 @@ func (f *Interface) consumeInsidePacket(buf *WireBuffer, q int, localCache firew
// Ignore local broadcast packets // Ignore local broadcast packets
if f.dropLocalBroadcast { if f.dropLocalBroadcast {
if f.myBroadcastAddrsTable.Contains(buf.FwPacket.RemoteAddr) { if f.myBroadcastAddrsTable.Contains(fwPacket.RemoteAddr) {
return return
} }
} }
if f.myVpnAddrsTable.Contains(buf.FwPacket.RemoteAddr) { if f.myVpnAddrsTable.Contains(fwPacket.RemoteAddr) {
// Immediately forward packets from self to self. // Immediately forward packets from self to self.
// This should only happen on Darwin-based and FreeBSD hosts, which // This should only happen on Darwin-based and FreeBSD hosts, which
// routes packets from the Nebula addr to the Nebula addr through the Nebula // routes packets from the Nebula addr to the Nebula addr through the Nebula
@@ -49,20 +48,20 @@ func (f *Interface) consumeInsidePacket(buf *WireBuffer, q int, localCache firew
} }
// Ignore multicast packets // Ignore multicast packets
if f.dropMulticast && buf.FwPacket.RemoteAddr.IsMulticast() { if f.dropMulticast && fwPacket.RemoteAddr.IsMulticast() {
return return
} }
hostinfo, ready := f.getOrHandshakeConsiderRouting(buf.FwPacket, func(hh *HandshakeHostInfo) { hostinfo, ready := f.getOrHandshakeConsiderRouting(fwPacket, func(hh *HandshakeHostInfo) {
hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics) hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics)
}) })
if hostinfo == nil { if hostinfo == nil {
f.rejectInside(packet, buf.Out, q) f.rejectInside(packet, out, q)
if f.l.Enabled(context.Background(), slog.LevelDebug) { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.Debug("dropping outbound packet, vpnAddr not in our vpn networks or in unsafe networks", f.l.Debug("dropping outbound packet, vpnAddr not in our vpn networks or in unsafe networks",
"vpnAddr", buf.FwPacket.RemoteAddr, "vpnAddr", fwPacket.RemoteAddr,
"fwPacket", buf.FwPacket, "fwPacket", fwPacket,
) )
} }
return return
@@ -72,15 +71,15 @@ func (f *Interface) consumeInsidePacket(buf *WireBuffer, q int, localCache firew
return return
} }
dropReason := f.firewall.Drop(*buf.FwPacket, false, hostinfo, f.pki.GetCAPool(), localCache) dropReason := f.firewall.Drop(*fwPacket, false, hostinfo, f.pki.GetCAPool(), localCache)
if dropReason == nil { if dropReason == nil {
f.sendNoMetrics(header.Message, 0, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, packet, buf, q) f.sendNoMetrics(header.Message, 0, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, packet, nb, out, q)
} else { } else {
f.rejectInside(packet, buf.Out, q) f.rejectInside(packet, out, q)
if f.l.Enabled(context.Background(), slog.LevelDebug) { if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debug("dropping outbound packet", hostinfo.logger(f.l).Debug("dropping outbound packet",
"fwPacket", buf.FwPacket, "fwPacket", fwPacket,
"reason", dropReason, "reason", dropReason,
) )
} }
@@ -103,27 +102,27 @@ func (f *Interface) rejectInside(packet []byte, out []byte, q int) {
} }
} }
func (f *Interface) rejectOutside(packet []byte, ci *ConnectionState, hostinfo *HostInfo, scratch []byte, buf *WireBuffer, q int) { func (f *Interface) rejectOutside(packet []byte, ci *ConnectionState, hostinfo *HostInfo, nb, out []byte, q int) {
if !f.firewall.OutSendReject { if !f.firewall.OutSendReject {
return return
} }
rejectIP := iputil.CreateRejectPacket(packet, scratch) out = iputil.CreateRejectPacket(packet, out)
if len(rejectIP) == 0 { if len(out) == 0 {
return return
} }
if len(rejectIP) > iputil.MaxRejectPacketSize { if len(out) > iputil.MaxRejectPacketSize {
if f.l.Enabled(context.Background(), slog.LevelInfo) { if f.l.Enabled(context.Background(), slog.LevelInfo) {
f.l.Info("rejectOutside: packet too big, not sending", f.l.Info("rejectOutside: packet too big, not sending",
"packet", packet, "packet", packet,
"outPacket", rejectIP, "outPacket", out,
) )
} }
return return
} }
f.sendNoMetrics(header.Message, 0, ci, hostinfo, netip.AddrPort{}, rejectIP, buf, q) f.sendNoMetrics(header.Message, 0, ci, hostinfo, netip.AddrPort{}, out, nb, packet, q)
} }
// Handshake will attempt to initiate a tunnel with the provided vpn address. This is a no-op if the tunnel is already established or being established // Handshake will attempt to initiate a tunnel with the provided vpn address. This is a no-op if the tunnel is already established or being established
@@ -216,7 +215,7 @@ func (f *Interface) getOrHandshakeConsiderRouting(fwPacket *firewall.Packet, cac
} }
func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p []byte, buf *WireBuffer) { func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, nb, out []byte) {
fp := &firewall.Packet{} fp := &firewall.Packet{}
err := newPacket(p, false, fp) err := newPacket(p, false, fp)
if err != nil { if err != nil {
@@ -236,12 +235,12 @@ func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubTyp
return return
} }
f.sendNoMetrics(header.Message, st, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, p, buf, 0) f.sendNoMetrics(header.Message, st, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, p, nb, out, 0)
} }
// SendMessageToVpnAddr handles real addr:port lookup and sends to the current best known address for vpnAddr. // SendMessageToVpnAddr handles real addr:port lookup and sends to the current best known address for vpnAddr.
// This function ignores myVpnNetworksTable, and will always attempt to treat the address as a vpnAddr // This function ignores myVpnNetworksTable, and will always attempt to treat the address as a vpnAddr
func (f *Interface) SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnAddr netip.Addr, p []byte, buf *WireBuffer) { func (f *Interface) SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnAddr netip.Addr, p, nb, out []byte) {
hostInfo, ready := f.handshakeManager.GetOrHandshake(vpnAddr, func(hh *HandshakeHostInfo) { hostInfo, ready := f.handshakeManager.GetOrHandshake(vpnAddr, func(hh *HandshakeHostInfo) {
hh.cachePacket(f.l, t, st, p, f.SendMessageToHostInfo, f.cachedPacketMetrics) hh.cachePacket(f.l, t, st, p, f.SendMessageToHostInfo, f.cachedPacketMetrics)
}) })
@@ -259,73 +258,113 @@ func (f *Interface) SendMessageToVpnAddr(t header.MessageType, st header.Message
return return
} }
f.SendMessageToHostInfo(t, st, hostInfo, p, buf) f.SendMessageToHostInfo(t, st, hostInfo, p, nb, out)
} }
func (f *Interface) SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hi *HostInfo, p []byte, buf *WireBuffer) { func (f *Interface) SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hi *HostInfo, p, nb, out []byte) {
f.send(t, st, hi.ConnectionState, hi, p, buf) f.send(t, st, hi.ConnectionState, hi, p, nb, out)
} }
func (f *Interface) send(t header.MessageType, st header.MessageSubType, ci *ConnectionState, hostinfo *HostInfo, p []byte, buf *WireBuffer) { func (f *Interface) send(t header.MessageType, st header.MessageSubType, ci *ConnectionState, hostinfo *HostInfo, p, nb, out []byte) {
f.messageMetrics.Tx(t, st, 1) f.messageMetrics.Tx(t, st, 1)
f.sendNoMetrics(t, st, ci, hostinfo, netip.AddrPort{}, p, buf, 0) f.sendNoMetrics(t, st, ci, hostinfo, netip.AddrPort{}, p, nb, out, 0)
} }
func (f *Interface) sendTo(t header.MessageType, st header.MessageSubType, ci *ConnectionState, hostinfo *HostInfo, remote netip.AddrPort, p []byte, buf *WireBuffer) { func (f *Interface) sendTo(t header.MessageType, st header.MessageSubType, ci *ConnectionState, hostinfo *HostInfo, remote netip.AddrPort, p, nb, out []byte) {
f.messageMetrics.Tx(t, st, 1) f.messageMetrics.Tx(t, st, 1)
f.sendNoMetrics(t, st, ci, hostinfo, remote, p, buf, 0) f.sendNoMetrics(t, st, ci, hostinfo, remote, p, nb, out, 0)
} }
// SendVia sends a payload through a Relay tunnel. No authentication or encryption is done // SendVia sends a payload through a Relay tunnel. No authentication or encryption is done
// to the payload for the ultimate target host, making this a useful method for sending // to the payload for the ultimate target host, making this a useful method for sending
// handshake messages to peers through relay tunnels. // handshake messages to peers through relay tunnels.
// // via is the HostInfo through which the message is relayed.
// via is the HostInfo through which the message is relayed. ad is staged into // ad is the plaintext data to authenticate, but not encrypt
// the inner-payload slot of buf and then AAD-only sealed under via's key by // nb is a buffer used to store the nonce value, re-used for performance reasons.
// SealRelayInPlace. The sendNoMetrics relay-forward path skips this entry // out is a buffer used to store the result of the Encrypt operation
// point and calls sendViaInPlace directly because its inner ciphertext is // q indicates which writer to use to send the packet.
// already in place from the encrypt step. func (f *Interface) SendVia(via *HostInfo,
func (f *Interface) SendVia(via *HostInfo, relay *Relay, ad []byte, buf *WireBuffer) { relay *Relay,
if header.Len+len(ad)+via.ConnectionState.eKey.Overhead() > cap(buf.Out) { ad,
nb,
out []byte,
nocopy bool,
) {
if noiseutil.EncryptLockNeeded {
// NOTE: for goboring AESGCMTLS we need to lock because of the nonce check
via.ConnectionState.writeLock.Lock()
}
c := via.ConnectionState.messageCounter.Add(1)
out = header.Encode(out, header.Version, header.Message, header.MessageRelay, relay.RemoteIndex, c)
f.connectionManager.Out(via)
// Authenticate the header and payload, but do not encrypt for this message type.
// The payload consists of the inner, unencrypted Nebula header, as well as the end-to-end encrypted payload.
if len(out)+len(ad)+via.ConnectionState.eKey.Overhead() > cap(out) {
if noiseutil.EncryptLockNeeded {
via.ConnectionState.writeLock.Unlock()
}
via.logger(f.l).Error("SendVia out buffer not large enough for relay", via.logger(f.l).Error("SendVia out buffer not large enough for relay",
"outCap", cap(buf.Out), "outCap", cap(out),
"payloadLen", len(ad), "payloadLen", len(ad),
"headerLen", header.Len, "headerLen", len(out),
"cipherOverhead", via.ConnectionState.eKey.Overhead(), "cipherOverhead", via.ConnectionState.eKey.Overhead(),
) )
return return
} }
buf.StageRelayInner(ad)
f.sendViaInPlace(via, relay, len(ad), buf)
}
// sendViaInPlace stamps the outer relay header, AAD-seals over the [outer // The header bytes are written to the 'out' slice; Grow the slice to hold the header and associated data payload.
// header | inner-already-staged] region, and writes the result to via.remote. offset := len(out)
// Called from SendVia (after staging ad) and from sendNoMetrics' relay-forward out = out[:offset+len(ad)]
// path (where the inner ciphertext is already in place from SealForRelay).
func (f *Interface) sendViaInPlace(via *HostInfo, relay *Relay, innerLen int, buf *WireBuffer) { // In one call path, the associated data _is_ already stored in out. In other call paths, the associated data must
f.connectionManager.Out(via) // be copied into 'out'.
out, err := buf.SealRelayInPlace(via.ConnectionState, relay.RemoteIndex, innerLen) if !nocopy {
copy(out[offset:], ad)
}
var err error
out, err = via.ConnectionState.eKey.EncryptDanger(out, out, nil, c, nb)
if noiseutil.EncryptLockNeeded {
via.ConnectionState.writeLock.Unlock()
}
if err != nil { if err != nil {
via.logger(f.l).Info("Failed to EncryptDanger in sendVia", "error", err) via.logger(f.l).Info("Failed to EncryptDanger in sendVia", "error", err)
return return
} }
if err := f.writers[0].WriteTo(out, via.remote); err != nil { err = f.writers[0].WriteTo(out, via.remote)
if err != nil {
via.logger(f.l).Info("Failed to WriteTo in sendVia", "error", err) via.logger(f.l).Info("Failed to WriteTo in sendVia", "error", err)
} }
f.connectionManager.RelayUsed(relay.LocalIndex) f.connectionManager.RelayUsed(relay.LocalIndex)
} }
// sendNoMetrics encrypts and writes one outbound nebula packet (data, control, func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType, ci *ConnectionState, hostinfo *HostInfo, remote netip.AddrPort, p, nb, out []byte, q int) {
// lighthouse, etc) using buf as the per-call wire scratch. When the hostinfo
// has no direct remote we encrypt into the relay-reserved slot via
// SealForRelay so sendViaInPlace can wrap it without an extra copy.
func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType, ci *ConnectionState, hostinfo *HostInfo, remote netip.AddrPort, p []byte, buf *WireBuffer, q int) {
if ci.eKey == nil { if ci.eKey == nil {
return return
} }
useRelay := !remote.IsValid() && !hostinfo.remote.IsValid() useRelay := !remote.IsValid() && !hostinfo.remote.IsValid()
fullOut := out
if useRelay {
if len(out) < header.Len {
// out always has a capacity of mtu, but not always a length greater than the header.Len.
// Grow it to make sure the next operation works.
out = out[:header.Len]
}
// Save a header's worth of data at the front of the 'out' buffer.
out = out[header.Len:]
}
if noiseutil.EncryptLockNeeded {
// NOTE: for goboring AESGCMTLS we need to lock because of the nonce check
ci.writeLock.Lock()
}
c := ci.messageCounter.Add(1)
//l.WithField("trace", string(debug.Stack())).Error("out Header ", &Header{Version, t, st, 0, hostinfo.remoteIndexId, c}, p)
out = header.Encode(out, header.Version, t, st, hostinfo.remoteIndexId, c)
f.connectionManager.Out(hostinfo) f.connectionManager.Out(hostinfo)
// Query our LH if we haven't since the last time we've been rebound, this will cause the remote to punch against // Query our LH if we haven't since the last time we've been rebound, this will cause the remote to punch against
@@ -342,42 +381,50 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
} }
} }
var out []byte
var err error var err error
if useRelay { out, err = ci.eKey.EncryptDanger(out, out, p, c, nb)
out, err = buf.SealForRelay(ci, t, st, hostinfo.remoteIndexId, p) if noiseutil.EncryptLockNeeded {
} else { ci.writeLock.Unlock()
out, err = buf.Seal(ci, t, st, hostinfo.remoteIndexId, p)
} }
if err != nil { if err != nil {
hostinfo.logger(f.l).Error("Failed to encrypt outgoing packet", hostinfo.logger(f.l).Error("Failed to encrypt outgoing packet",
"error", err, "error", err,
"udpAddr", remote, "udpAddr", remote,
"counter", c,
"attemptedCounter", c,
) )
return return
} }
switch { if remote.IsValid() {
case remote.IsValid(): err = f.writers[q].WriteTo(out, remote)
if err := f.writers[q].WriteTo(out, remote); err != nil { if err != nil {
hostinfo.logger(f.l).Error("Failed to write outgoing packet", "error", err, "udpAddr", remote) hostinfo.logger(f.l).Error("Failed to write outgoing packet",
"error", err,
"udpAddr", remote,
)
} }
case hostinfo.remote.IsValid(): } else if hostinfo.remote.IsValid() {
if err := f.writers[q].WriteTo(out, hostinfo.remote); err != nil { err = f.writers[q].WriteTo(out, hostinfo.remote)
hostinfo.logger(f.l).Error("Failed to write outgoing packet", "error", err, "udpAddr", hostinfo.remote) if err != nil {
hostinfo.logger(f.l).Error("Failed to write outgoing packet",
"error", err,
"udpAddr", remote,
)
} }
default: } else {
// SealForRelay placed the inner ciphertext at buf.Out[header.Len:], // Try to send via a relay
// so sendViaInPlace can wrap it with the outer relay header without
// an extra copy.
for _, relayIP := range hostinfo.relayState.CopyRelayIps() { for _, relayIP := range hostinfo.relayState.CopyRelayIps() {
relayHostInfo, relay, err := f.hostMap.QueryVpnAddrsRelayFor(hostinfo.vpnAddrs, relayIP) relayHostInfo, relay, err := f.hostMap.QueryVpnAddrsRelayFor(hostinfo.vpnAddrs, relayIP)
if err != nil { if err != nil {
hostinfo.relayState.DeleteRelay(relayIP) hostinfo.relayState.DeleteRelay(relayIP)
hostinfo.logger(f.l).Info("sendNoMetrics failed to find HostInfo", "relay", relayIP, "error", err) hostinfo.logger(f.l).Info("sendNoMetrics failed to find HostInfo",
"relay", relayIP,
"error", err,
)
continue continue
} }
f.sendViaInPlace(relayHostInfo, relay, len(out), buf) f.SendVia(relayHostInfo, relay, out, nb, fullOut[:header.Len+len(out)], true)
break break
} }
} }
+25 -18
View File
@@ -34,6 +34,7 @@ type InterfaceConfig struct {
HandshakeManager *HandshakeManager HandshakeManager *HandshakeManager
lightHouse *LightHouse lightHouse *LightHouse
connectionManager *connectionManager connectionManager *connectionManager
pmtudManager *pmtudManager
DropLocalBroadcast bool DropLocalBroadcast bool
DropMulticast bool DropMulticast bool
routines int routines int
@@ -57,6 +58,7 @@ type Interface struct {
pki *PKI pki *PKI
firewall *Firewall firewall *Firewall
connectionManager *connectionManager connectionManager *connectionManager
pmtudManager *pmtudManager
handshakeManager *HandshakeManager handshakeManager *HandshakeManager
dnsServer *dnsServer dnsServer *dnsServer
createTime time.Time createTime time.Time
@@ -101,19 +103,19 @@ type Interface struct {
messageMetrics *MessageMetrics messageMetrics *MessageMetrics
cachedPacketMetrics *cachedPacketMetrics cachedPacketMetrics *cachedPacketMetrics
// bufAlloc hands out reusable WireBuffers sized for this interface's
// inside Device. All buf consumers (hot-path data-plane goroutines,
// long-lived workers, and cold callers) acquire from here so sizing
// is centralized and consistent. Long-lived owners just don't release.
bufAlloc WireBufferAllocator
l *slog.Logger l *slog.Logger
} }
type EncWriter interface { type EncWriter interface {
SendVia(via *HostInfo, relay *Relay, ad []byte, buf *WireBuffer) SendVia(via *HostInfo,
SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnAddr netip.Addr, p []byte, buf *WireBuffer) relay *Relay,
SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p []byte, buf *WireBuffer) ad,
nb,
out []byte,
nocopy bool,
)
SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnAddr netip.Addr, p, nb, out []byte)
SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, nb, out []byte)
Handshake(vpnAddr netip.Addr) Handshake(vpnAddr netip.Addr)
GetHostInfo(vpnAddr netip.Addr) *HostInfo GetHostInfo(vpnAddr netip.Addr) *HostInfo
GetCertState() *CertState GetCertState() *CertState
@@ -195,6 +197,7 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
myBroadcastAddrsTable: cs.myVpnBroadcastAddrsTable, myBroadcastAddrsTable: cs.myVpnBroadcastAddrsTable,
relayManager: c.relayManager, relayManager: c.relayManager,
connectionManager: c.connectionManager, connectionManager: c.connectionManager,
pmtudManager: c.pmtudManager,
conntrackCacheTimeout: c.ConntrackCacheTimeout, conntrackCacheTimeout: c.ConntrackCacheTimeout,
metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)), metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)),
@@ -204,8 +207,6 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
dropped: metrics.GetOrRegisterCounter("hostinfo.cached_packets.dropped", nil), dropped: metrics.GetOrRegisterCounter("hostinfo.cached_packets.dropped", nil),
}, },
bufAlloc: NewWireBufferPool(mtu, c.Inside.TunPrefixLen()),
l: c.l, l: c.l,
} }
@@ -214,6 +215,7 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
ifce.reQueryWait.Store(int64(c.reQueryWait)) ifce.reQueryWait.Store(int64(c.reQueryWait))
ifce.connectionManager.intf = ifce ifce.connectionManager.intf = ifce
ifce.pmtudManager.intf = ifce
return ifce, nil return ifce, nil
} }
@@ -313,11 +315,13 @@ func (f *Interface) listenOut(i int) {
ctCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout) ctCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
lhh := f.lightHouse.NewRequestHandler() lhh := f.lightHouse.NewRequestHandler()
// Long-lived per-receive-goroutine buf; never released back to the pool. plaintext := make([]byte, udp.MTU)
buf := f.bufAlloc.Acquire() h := &header.H{}
fwPacket := &firewall.Packet{}
nb := make([]byte, 12, 12)
err := li.ListenOut(func(fromUdpAddr netip.AddrPort, payload []byte) { err := li.ListenOut(func(fromUdpAddr netip.AddrPort, payload []byte) {
f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, buf, payload, lhh, i, ctCache.Get()) f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, plaintext[:0], payload, h, fwPacket, lhh, nb, i, ctCache.Get())
}) })
if err != nil && !f.closed.Load() { if err != nil && !f.closed.Load() {
@@ -329,12 +333,15 @@ func (f *Interface) listenOut(i int) {
} }
func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) { func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
// Long-lived per-tun-reader buf; never released back to the pool. packet := make([]byte, mtu)
buf := f.bufAlloc.Acquire() out := make([]byte, mtu)
fwPacket := &firewall.Packet{}
nb := make([]byte, 12, 12)
conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout) conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
for { for {
_, err := buf.ReadIPFromTUN(reader) n, err := reader.Read(packet)
if err != nil { if err != nil {
if !f.closed.Load() { if !f.closed.Load() {
f.l.Error("Error while reading outbound packet, closing", "error", err, "reader", i) f.l.Error("Error while reading outbound packet, closing", "error", err, "reader", i)
@@ -343,7 +350,7 @@ func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
break break
} }
f.consumeInsidePacket(buf, i, conntrackCache.Get()) f.consumeInsidePacket(packet[:n], fwPacket, nb, out, i, conntrackCache.Get())
} }
f.l.Debug("overlay reader is done", "reader", i) f.l.Debug("overlay reader is done", "reader", i)
+23 -46
View File
@@ -63,10 +63,6 @@ type LightHouse struct {
interval atomic.Int64 interval atomic.Int64
updateCancel context.CancelFunc updateCancel context.CancelFunc
ifce EncWriter ifce EncWriter
// bufAlloc lets the lighthouse query/update workers, request handlers
// and punchback goroutines acquire correctly sized WireBuffers from
// the same pool as the data plane. Set by main.go alongside ifce.
bufAlloc WireBufferAllocator
nebulaPort uint32 // 32 bits because protobuf does not have a uint16 nebulaPort uint32 // 32 bits because protobuf does not have a uint16
advertiseAddrs atomic.Pointer[[]netip.AddrPort] advertiseAddrs atomic.Pointer[[]netip.AddrPort]
@@ -113,10 +109,6 @@ func NewLightHouseFromConfig(ctx context.Context, l *slog.Logger, c *config.C, c
punchy: p, punchy: p,
updateTrigger: make(chan struct{}, 1), updateTrigger: make(chan struct{}, 1),
queryChan: make(chan netip.Addr, c.GetUint32("handshakes.query_buffer", 64)), queryChan: make(chan netip.Addr, c.GetUint32("handshakes.query_buffer", 64)),
// Default to a no-prefix pool so the query/update workers and
// request handlers have a working WireBufferAllocator before
// main.go wires up the real one from the Interface.
bufAlloc: NewWireBufferPool(mtu, 0),
l: l, l: l,
} }
lighthouses := make([]netip.Addr, 0) lighthouses := make([]netip.Addr, 0)
@@ -766,22 +758,21 @@ func (lh *LightHouse) startQueryWorker() {
} }
go func() { go func() {
// Long-lived per-worker WireBuffer; reused for every lighthouse query nb := make([]byte, 12, 12)
// this worker issues for the life of the goroutine. out := make([]byte, mtu)
buf := lh.bufAlloc.Acquire()
for { for {
select { select {
case <-lh.ctx.Done(): case <-lh.ctx.Done():
return return
case addr := <-lh.queryChan: case addr := <-lh.queryChan:
lh.innerQueryServer(addr, buf) lh.innerQueryServer(addr, nb, out)
} }
} }
}() }()
} }
func (lh *LightHouse) innerQueryServer(addr netip.Addr, buf *WireBuffer) { func (lh *LightHouse) innerQueryServer(addr netip.Addr, nb, out []byte) {
if lh.IsLighthouseAddr(addr) { if lh.IsLighthouseAddr(addr) {
return return
} }
@@ -830,7 +821,7 @@ func (lh *LightHouse) innerQueryServer(addr netip.Addr, buf *WireBuffer) {
} }
} }
lh.ifce.SendMessageToVpnAddr(header.LightHouse, 0, lhVpnAddr, v1Query, buf) lh.ifce.SendMessageToVpnAddr(header.LightHouse, 0, lhVpnAddr, v1Query, nb, out)
queried++ queried++
} else if v == cert.Version2 { } else if v == cert.Version2 {
@@ -849,7 +840,7 @@ func (lh *LightHouse) innerQueryServer(addr netip.Addr, buf *WireBuffer) {
} }
} }
lh.ifce.SendMessageToVpnAddr(header.LightHouse, 0, lhVpnAddr, v2Query, buf) lh.ifce.SendMessageToVpnAddr(header.LightHouse, 0, lhVpnAddr, v2Query, nb, out)
queried++ queried++
} else { } else {
@@ -878,12 +869,8 @@ func (lh *LightHouse) StartUpdateWorker() {
go func() { go func() {
defer clockSource.Stop() defer clockSource.Stop()
// Long-lived per-worker WireBuffer; reused across every periodic
// update for the life of this goroutine.
buf := lh.bufAlloc.Acquire()
for { for {
lh.sendUpdate(buf) lh.SendUpdate()
select { select {
case <-updateCtx.Done(): case <-updateCtx.Done():
@@ -897,15 +884,6 @@ func (lh *LightHouse) StartUpdateWorker() {
}() }()
} }
// SendUpdate is the public entry point that triggers a one-shot lighthouse
// update outside the worker loop (e.g. tests or reload paths). It allocates
// its own WireBuffer since callers don't already own one.
func (lh *LightHouse) SendUpdate() {
buf := lh.bufAlloc.Acquire()
defer lh.bufAlloc.Release(buf)
lh.sendUpdate(buf)
}
// TriggerUpdate requests an immediate lighthouse update. This is a non-blocking // TriggerUpdate requests an immediate lighthouse update. This is a non-blocking
// operation intended to be called after a handshake completes with a lighthouse, // operation intended to be called after a handshake completes with a lighthouse,
// so the lighthouse has our current addresses without waiting for the next // so the lighthouse has our current addresses without waiting for the next
@@ -917,7 +895,7 @@ func (lh *LightHouse) TriggerUpdate() {
} }
} }
func (lh *LightHouse) sendUpdate(buf *WireBuffer) { func (lh *LightHouse) SendUpdate() {
var v4 []*V4AddrPort var v4 []*V4AddrPort
var v6 []*V6AddrPort var v6 []*V6AddrPort
@@ -943,6 +921,9 @@ func (lh *LightHouse) sendUpdate(buf *WireBuffer) {
} }
} }
nb := make([]byte, 12, 12)
out := make([]byte, mtu)
var v1Update, v2Update []byte var v1Update, v2Update []byte
var err error var err error
updated := 0 updated := 0
@@ -993,7 +974,7 @@ func (lh *LightHouse) sendUpdate(buf *WireBuffer) {
} }
} }
lh.ifce.SendMessageToVpnAddr(header.LightHouse, 0, lhVpnAddr, v1Update, buf) lh.ifce.SendMessageToVpnAddr(header.LightHouse, 0, lhVpnAddr, v1Update, nb, out)
updated++ updated++
} else if v == cert.Version2 { } else if v == cert.Version2 {
@@ -1022,7 +1003,7 @@ func (lh *LightHouse) sendUpdate(buf *WireBuffer) {
} }
} }
lh.ifce.SendMessageToVpnAddr(header.LightHouse, 0, lhVpnAddr, v2Update, buf) lh.ifce.SendMessageToVpnAddr(header.LightHouse, 0, lhVpnAddr, v2Update, nb, out)
updated++ updated++
} else { } else {
@@ -1039,10 +1020,8 @@ func (lh *LightHouse) sendUpdate(buf *WireBuffer) {
type LightHouseHandler struct { type LightHouseHandler struct {
lh *LightHouse lh *LightHouse
// buf is the long-lived per-handler wire scratch. NewRequestHandler is nb []byte
// called once per data-plane receive goroutine, so buf is owned by that out []byte
// goroutine and reused for every lighthouse send the handler issues.
buf *WireBuffer
pb []byte pb []byte
meta *NebulaMeta meta *NebulaMeta
l *slog.Logger l *slog.Logger
@@ -1051,7 +1030,8 @@ type LightHouseHandler struct {
func (lh *LightHouse) NewRequestHandler() *LightHouseHandler { func (lh *LightHouse) NewRequestHandler() *LightHouseHandler {
lhh := &LightHouseHandler{ lhh := &LightHouseHandler{
lh: lh, lh: lh,
buf: lh.bufAlloc.Acquire(), nb: make([]byte, 12, 12),
out: make([]byte, mtu),
l: lh.l, l: lh.l,
pb: make([]byte, mtu), pb: make([]byte, mtu),
@@ -1188,7 +1168,7 @@ func (lhh *LightHouseHandler) handleHostQuery(n *NebulaMeta, fromVpnAddrs []neti
} }
lhh.lh.metricTx(NebulaMeta_HostQueryReply, 1) lhh.lh.metricTx(NebulaMeta_HostQueryReply, 1)
w.SendMessageToVpnAddr(header.LightHouse, 0, fromVpnAddrs[0], lhh.pb[:ln], lhh.buf) w.SendMessageToVpnAddr(header.LightHouse, 0, fromVpnAddrs[0], lhh.pb[:ln], lhh.nb, lhh.out[:0])
lhh.sendHostPunchNotification(n, fromVpnAddrs, queryVpnAddr, w) lhh.sendHostPunchNotification(n, fromVpnAddrs, queryVpnAddr, w)
} }
@@ -1248,7 +1228,7 @@ func (lhh *LightHouseHandler) sendHostPunchNotification(n *NebulaMeta, fromVpnAd
} }
lhh.lh.metricTx(NebulaMeta_HostPunchNotification, 1) lhh.lh.metricTx(NebulaMeta_HostPunchNotification, 1)
w.SendMessageToVpnAddr(header.LightHouse, 0, punchNotifDest, lhh.pb[:ln], lhh.buf) w.SendMessageToVpnAddr(header.LightHouse, 0, punchNotifDest, lhh.pb[:ln], lhh.nb, lhh.out[:0])
} }
func (lhh *LightHouseHandler) coalesceAnswers(v cert.Version, c *cache, n *NebulaMeta) { func (lhh *LightHouseHandler) coalesceAnswers(v cert.Version, c *cache, n *NebulaMeta) {
@@ -1405,7 +1385,7 @@ func (lhh *LightHouseHandler) handleHostUpdateNotification(n *NebulaMeta, fromVp
} }
lhh.lh.metricTx(NebulaMeta_HostUpdateNotificationAck, 1) lhh.lh.metricTx(NebulaMeta_HostUpdateNotificationAck, 1)
w.SendMessageToVpnAddr(header.LightHouse, 0, fromVpnAddrs[0], lhh.pb[:ln], lhh.buf) w.SendMessageToVpnAddr(header.LightHouse, 0, fromVpnAddrs[0], lhh.pb[:ln], lhh.nb, lhh.out[:0])
} }
func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpnAddrs []netip.Addr, w EncWriter) { func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpnAddrs []netip.Addr, w EncWriter) {
@@ -1472,13 +1452,10 @@ func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpn
"vpnAddr", detailsVpnAddr, "vpnAddr", detailsVpnAddr,
) )
} }
// We acquire and release a fresh buf within this goroutine so it //NOTE: we have to allocate a new output buffer here since we are spawning a new goroutine
// returns to the pool once the punchback send completes. We // for each punchBack packet. We should move this into a timerwheel or a single goroutine
// should move this into a timerwheel or a single goroutine
// managed by a channel. // managed by a channel.
pbuf := lhh.lh.bufAlloc.Acquire() w.SendMessageToVpnAddr(header.Test, header.TestRequest, detailsVpnAddr, []byte(""), make([]byte, 12, 12), make([]byte, mtu))
defer lhh.lh.bufAlloc.Release(pbuf)
w.SendMessageToVpnAddr(header.Test, header.TestRequest, detailsVpnAddr, []byte(""), pbuf)
}() }()
} }
} }
+3 -3
View File
@@ -372,12 +372,12 @@ type testEncWriter struct {
protocolVersion cert.Version protocolVersion cert.Version
} }
func (tw *testEncWriter) SendVia(via *HostInfo, relay *Relay, ad []byte, buf *WireBuffer) { func (tw *testEncWriter) SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool) {
} }
func (tw *testEncWriter) Handshake(vpnIp netip.Addr) { func (tw *testEncWriter) Handshake(vpnIp netip.Addr) {
} }
func (tw *testEncWriter) SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p []byte, _ *WireBuffer) { func (tw *testEncWriter) SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, _, _ []byte) {
msg := &NebulaMeta{} msg := &NebulaMeta{}
err := msg.Unmarshal(p) err := msg.Unmarshal(p)
if tw.metaFilter == nil || msg.Type == *tw.metaFilter { if tw.metaFilter == nil || msg.Type == *tw.metaFilter {
@@ -394,7 +394,7 @@ func (tw *testEncWriter) SendMessageToHostInfo(t header.MessageType, st header.M
} }
} }
func (tw *testEncWriter) SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnIp netip.Addr, p []byte, _ *WireBuffer) { func (tw *testEncWriter) SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnIp netip.Addr, p, _, _ []byte) {
msg := &NebulaMeta{} msg := &NebulaMeta{}
err := msg.Unmarshal(p) err := msg.Unmarshal(p)
if tw.metaFilter == nil || msg.Type == *tw.metaFilter { if tw.metaFilter == nil || msg.Type == *tw.metaFilter {
+3 -1
View File
@@ -172,6 +172,7 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
hostMap := NewHostMapFromConfig(l, c) hostMap := NewHostMapFromConfig(l, c)
punchy := NewPunchyFromConfig(l, c) punchy := NewPunchyFromConfig(l, c)
connManager := newConnectionManagerFromConfig(l, c, hostMap, punchy) connManager := newConnectionManagerFromConfig(l, c, hostMap, punchy)
pmtudMgr := newPMTUDManagerFromConfig(l, c, tun)
lightHouse, err := NewLightHouseFromConfig(ctx, l, c, pki.getCertState(), udpConns[0], punchy) lightHouse, err := NewLightHouseFromConfig(ctx, l, c, pki.getCertState(), udpConns[0], punchy)
if err != nil { if err != nil {
return nil, util.ContextualizeIfNeeded("Failed to initialize lighthouse handler", err) return nil, util.ContextualizeIfNeeded("Failed to initialize lighthouse handler", err)
@@ -208,6 +209,7 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
DnsServer: ds, DnsServer: ds,
HandshakeManager: handshakeManager, HandshakeManager: handshakeManager,
connectionManager: connManager, connectionManager: connManager,
pmtudManager: pmtudMgr,
lightHouse: lightHouse, lightHouse: lightHouse,
tryPromoteEvery: c.GetUint32("counters.try_promote", defaultPromoteEvery), tryPromoteEvery: c.GetUint32("counters.try_promote", defaultPromoteEvery),
reQueryEvery: c.GetUint32("counters.requery_every_packets", defaultReQueryEvery), reQueryEvery: c.GetUint32("counters.requery_every_packets", defaultReQueryEvery),
@@ -232,7 +234,6 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
ifce.writers = udpConns ifce.writers = udpConns
lightHouse.ifce = ifce lightHouse.ifce = ifce
lightHouse.bufAlloc = ifce.bufAlloc
ifce.RegisterConfigChangeCallbacks(c) ifce.RegisterConfigChangeCallbacks(c)
ifce.reloadDisconnectInvalid(c) ifce.reloadDisconnectInvalid(c)
@@ -267,6 +268,7 @@ func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, dev
dnsStart: ds.Start, dnsStart: ds.Start,
lighthouseStart: lightHouse.StartUpdateWorker, lighthouseStart: lightHouse.StartUpdateWorker,
connectionManagerStart: connManager.Start, connectionManagerStart: connManager.Start,
pmtudManagerStart: pmtudMgr.Start,
}, nil }, nil
} }
-13
View File
@@ -14,19 +14,6 @@ type endianness interface {
var noiseEndianness endianness = binary.BigEndian var noiseEndianness endianness = binary.BigEndian
// NonceSize is the AEAD nonce length used by all ciphers nebula supports
// today (AES-GCM and ChaCha20-Poly1305 both use 96-bit nonces). Encrypt-
// and DecryptDanger lay out the nonce as 4 zero bytes followed by an 8-byte
// big-endian counter; if a future cipher with a different nonce size is
// added, this constant and those layouts must change together.
const NonceSize = 12
// AEADOverhead is the AEAD authentication tag length the ciphers nebula
// supports append to ciphertext. Both AES-GCM and ChaCha20-Poly1305 use
// 128-bit tags. NebulaCipherState.Overhead() returns this dynamically from
// the cipher; the constant is for sizing buffers at construction time.
const AEADOverhead = 16
type NebulaCipherState struct { type NebulaCipherState struct {
c cipher.AEAD c cipher.AEAD
} }
+45 -36
View File
@@ -20,8 +20,7 @@ const (
minFwPacketLen = 4 minFwPacketLen = 4
) )
func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []byte, lhf *LightHouseHandler, q int, localCache firewall.ConntrackCache) { func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte, h *header.H, fwPacket *firewall.Packet, lhf *LightHouseHandler, nb []byte, q int, localCache firewall.ConntrackCache) {
h := buf.H
err := h.Parse(packet) err := h.Parse(packet)
if err != nil { if err != nil {
// Hole punch packets are 0 or 1 byte big, so lets ignore printing those errors // Hole punch packets are 0 or 1 byte big, so lets ignore printing those errors
@@ -66,7 +65,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
switch h.Subtype { switch h.Subtype {
case header.MessageNone: case header.MessageNone:
if !f.decryptToTun(hostinfo, h.MessageCounter, buf, packet, q, localCache) { if !f.decryptToTun(hostinfo, h.MessageCounter, out, packet, fwPacket, nb, q, localCache) {
return return
} }
case header.MessageRelay: case header.MessageRelay:
@@ -77,9 +76,8 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
// which will gracefully fail in the DecryptDanger call. // which will gracefully fail in the DecryptDanger call.
signedPayload := packet[:len(packet)-hostinfo.ConnectionState.dKey.Overhead()] signedPayload := packet[:len(packet)-hostinfo.ConnectionState.dKey.Overhead()]
signatureValue := packet[len(packet)-hostinfo.ConnectionState.dKey.Overhead():] signatureValue := packet[len(packet)-hostinfo.ConnectionState.dKey.Overhead():]
// AAD-only validation: passing dst=nil since there's no plaintext out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, signedPayload, signatureValue, h.MessageCounter, nb)
// to recover (ciphertext is just the trailing AEAD tag). if err != nil {
if _, err = hostinfo.ConnectionState.dKey.DecryptDanger(nil, signedPayload, signatureValue, h.MessageCounter, buf.NB); err != nil {
return return
} }
// Successfully validated the thing. Get rid of the Relay header. // Successfully validated the thing. Get rid of the Relay header.
@@ -112,8 +110,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
relay: relay, relay: relay,
IsRelayed: true, IsRelayed: true,
} }
buf.Reset() f.readOutsidePackets(via, out[:0], signedPayload, h, fwPacket, lhf, nb, q, localCache)
f.readOutsidePackets(via, buf, signedPayload, lhf, q, localCache)
return return
case ForwardingType: case ForwardingType:
// Find the target HostInfo relay object // Find the target HostInfo relay object
@@ -133,7 +130,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
case ForwardingType: case ForwardingType:
// Forward this packet through the relay tunnel // Forward this packet through the relay tunnel
// Find the target HostInfo // Find the target HostInfo
f.SendVia(targetHI, targetRelay, signedPayload, buf) f.SendVia(targetHI, targetRelay, signedPayload, nb, out, false)
return return
case TerminalType: case TerminalType:
hostinfo.logger(f.l).Error("Unexpected Relay Type of Terminal") hostinfo.logger(f.l).Error("Unexpected Relay Type of Terminal")
@@ -155,7 +152,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
return return
} }
d, err := f.decrypt(hostinfo, h.MessageCounter, buf, packet, h) d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).Error("Failed to decrypt lighthouse packet", hostinfo.logger(f.l).Error("Failed to decrypt lighthouse packet",
"error", err, "error", err,
@@ -176,7 +173,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
return return
} }
d, err := f.decrypt(hostinfo, h.MessageCounter, buf, packet, h) d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).Error("Failed to decrypt test packet", hostinfo.logger(f.l).Error("Failed to decrypt test packet",
"error", err, "error", err,
@@ -186,11 +183,20 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
return return
} }
if h.Subtype == header.TestRequest { switch h.Subtype {
case header.TestRequest:
// This testRequest might be from TryPromoteBest, so we should roam // This testRequest might be from TryPromoteBest, so we should roam
// to the new IP address before responding. // to the new IP address before responding
f.handleHostRoaming(hostinfo, via) f.handleHostRoaming(hostinfo, via)
f.send(header.Test, header.TestReply, ci, hostinfo, d, buf) f.send(header.Test, header.TestReply, ci, hostinfo, d, nb, out)
case header.MTUDProbeRequest:
// Reply with just the 8-byte ack header so the reverse path doesn't have to
// carry the full probe size; we only verify the forward direction.
if len(d) >= 8 {
f.send(header.Test, header.MTUDProbeReply, ci, hostinfo, d[:8], nb, out)
}
case header.MTUDProbeReply:
f.pmtudManager.HandleReply(hostinfo.localIndexId, d)
} }
// Fallthrough to the bottom to record incoming traffic // Fallthrough to the bottom to record incoming traffic
@@ -213,7 +219,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
if !f.handleEncrypted(ci, via, h) { if !f.handleEncrypted(ci, via, h) {
return return
} }
_, err = f.decrypt(hostinfo, h.MessageCounter, buf, packet, h) _, err = f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).Error("Failed to decrypt CloseTunnel packet", hostinfo.logger(f.l).Error("Failed to decrypt CloseTunnel packet",
"error", err, "error", err,
@@ -233,7 +239,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
return return
} }
d, err := f.decrypt(hostinfo, h.MessageCounter, buf, packet, h) d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).Error("Failed to decrypt Control packet", hostinfo.logger(f.l).Error("Failed to decrypt Control packet",
"error", err, "error", err,
@@ -260,6 +266,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, buf *WireBuffer, packet []
// closeTunnel closes a tunnel locally, it does not send a closeTunnel packet to the remote // closeTunnel closes a tunnel locally, it does not send a closeTunnel packet to the remote
func (f *Interface) closeTunnel(hostInfo *HostInfo) { func (f *Interface) closeTunnel(hostInfo *HostInfo) {
f.pmtudManager.OnTunnelDown(hostInfo)
final := f.hostMap.DeleteHostInfo(hostInfo) final := f.hostMap.DeleteHostInfo(hostInfo)
if final { if final {
// We no longer have any tunnels with this vpn addr, clear learned lighthouse state to lower memory usage // We no longer have any tunnels with this vpn addr, clear learned lighthouse state to lower memory usage
@@ -269,9 +276,7 @@ func (f *Interface) closeTunnel(hostInfo *HostInfo) {
// sendCloseTunnel is a helper function to send a proper close tunnel packet to a remote // sendCloseTunnel is a helper function to send a proper close tunnel packet to a remote
func (f *Interface) sendCloseTunnel(h *HostInfo) { func (f *Interface) sendCloseTunnel(h *HostInfo) {
buf := f.bufAlloc.Acquire() f.send(header.CloseTunnel, 0, h.ConnectionState, h, []byte{}, make([]byte, 12, 12), make([]byte, mtu))
defer f.bufAlloc.Release(buf)
f.send(header.CloseTunnel, 0, h.ConnectionState, h, []byte{}, buf)
} }
func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) { func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
@@ -301,6 +306,7 @@ func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
hostinfo.lastRoam = time.Now() hostinfo.lastRoam = time.Now()
hostinfo.lastRoamRemote = hostinfo.remote hostinfo.lastRoamRemote = hostinfo.remote
hostinfo.SetRemote(via.UdpAddr) hostinfo.SetRemote(via.UdpAddr)
f.pmtudManager.OnRoam(hostinfo)
} }
} }
@@ -520,8 +526,9 @@ func parseV4(data []byte, incoming bool, fp *firewall.Packet) error {
return nil return nil
} }
func (f *Interface) decrypt(hostinfo *HostInfo, mc uint64, buf *WireBuffer, packet []byte, h *header.H) ([]byte, error) { func (f *Interface) decrypt(hostinfo *HostInfo, mc uint64, out []byte, packet []byte, h *header.H, nb []byte) ([]byte, error) {
plaintext, err := buf.DecryptForHandler(hostinfo.ConnectionState, packet, mc) var err error
out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], mc, nb)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -533,41 +540,42 @@ func (f *Interface) decrypt(hostinfo *HostInfo, mc uint64, buf *WireBuffer, pack
return nil, errors.New("out of window packet") return nil, errors.New("out of window packet")
} }
return plaintext, nil return out, nil
} }
func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, buf *WireBuffer, packet []byte, q int, localCache firewall.ConntrackCache) bool { func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, out []byte, packet []byte, fwPacket *firewall.Packet, nb []byte, q int, localCache firewall.ConntrackCache) bool {
if err := buf.DecryptDatagram(hostinfo.ConnectionState, packet, messageCounter); err != nil { var err error
out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], messageCounter, nb)
if err != nil {
hostinfo.logger(f.l).Error("Failed to decrypt packet", "error", err) hostinfo.logger(f.l).Error("Failed to decrypt packet", "error", err)
return false return false
} }
ipPacket := buf.IPPacket() err = newPacket(out, true, fwPacket)
if err := newPacket(ipPacket, true, buf.FwPacket); err != nil { if err != nil {
hostinfo.logger(f.l).Warn("Error while validating inbound packet", hostinfo.logger(f.l).Warn("Error while validating inbound packet",
"error", err, "error", err,
"packet", ipPacket, "packet", out,
) )
return false return false
} }
if !hostinfo.ConnectionState.window.Update(f.l, messageCounter) { if !hostinfo.ConnectionState.window.Update(f.l, messageCounter) {
if f.l.Enabled(context.Background(), slog.LevelDebug) { if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debug("dropping out of window packet", "fwPacket", buf.FwPacket) hostinfo.logger(f.l).Debug("dropping out of window packet", "fwPacket", fwPacket)
} }
return false return false
} }
dropReason := f.firewall.Drop(*buf.FwPacket, true, hostinfo, f.pki.GetCAPool(), localCache) dropReason := f.firewall.Drop(*fwPacket, true, hostinfo, f.pki.GetCAPool(), localCache)
if dropReason != nil { if dropReason != nil {
// NOTE: We hand `packet` (the original UDP ciphertext we already // NOTE: We give `packet` as the `out` here since we already decrypted from it and we don't need it anymore
// decrypted from) as the reject-IP scratch since we no longer // This gives us a buffer to build the reject packet in
// need its ciphertext, and it's disjoint from buf.Out where f.rejectOutside(out, hostinfo.ConnectionState, hostinfo, nb, packet, q)
// sendNoMetrics will encrypt the wire packet.
f.rejectOutside(ipPacket, hostinfo.ConnectionState, hostinfo, packet, buf, q)
if f.l.Enabled(context.Background(), slog.LevelDebug) { if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debug("dropping inbound packet", hostinfo.logger(f.l).Debug("dropping inbound packet",
"fwPacket", buf.FwPacket, "fwPacket", fwPacket,
"reason", dropReason, "reason", dropReason,
) )
} }
@@ -575,7 +583,8 @@ func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, buf
} }
f.connectionManager.In(hostinfo) f.connectionManager.In(hostinfo)
if _, err := buf.WriteIPToTUN(f.readers[q]); err != nil { _, err = f.readers[q].Write(out)
if err != nil {
f.l.Error("Failed to write to tun", "error", err) f.l.Error("Failed to write to tun", "error", err)
} }
return true return true
+9 -3
View File
@@ -15,7 +15,13 @@ type Device interface {
RoutesFor(netip.Addr) routing.Gateways RoutesFor(netip.Addr) routing.Gateways
SupportsMultiqueue() bool SupportsMultiqueue() bool
NewMultiQueueReader() (io.ReadWriteCloser, error) NewMultiQueueReader() (io.ReadWriteCloser, error)
// TunPrefixLen reports the number of bytes the device prepends to every IP packet on the wire. // SupportsPerPeerMTU reports whether SetPeerMTU is implemented for real on
// Currently only non zero for the BSD tun devices. // this platform. PMTUD requires this; the manager will refuse to enable when
TunPrefixLen() int // false even if the operator set tun.max_mtu, because a discovered MTU we
// can't actually install does the operator no good.
SupportsPerPeerMTU() bool
// SetPeerMTU installs a per-peer MTU on the routing table so the kernel will
// surface PTB / EMSGSIZE for inside packets to that peer that would exceed mtu.
// Pass mtu=0 to remove the override and let the device default apply.
SetPeerMTU(addr netip.Addr, mtu int) error
} }
+8 -2
View File
@@ -39,6 +39,14 @@ func (NoopTun) Write([]byte) (int, error) {
return 0, nil return 0, nil
} }
func (NoopTun) SupportsPerPeerMTU() bool {
return false
}
func (NoopTun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (NoopTun) SupportsMultiqueue() bool { func (NoopTun) SupportsMultiqueue() bool {
return false return false
} }
@@ -50,5 +58,3 @@ func (NoopTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
func (NoopTun) Close() error { func (NoopTun) Close() error {
return nil return nil
} }
func (NoopTun) TunPrefixLen() int { return 0 }
+8 -2
View File
@@ -95,6 +95,14 @@ func (t *tun) Name() string {
return "android" return "android"
} }
func (t *tun) SupportsPerPeerMTU() bool {
return false
}
func (t *tun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *tun) SupportsMultiqueue() bool { func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
@@ -102,5 +110,3 @@ func (t *tun) SupportsMultiqueue() bool {
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for android") return nil, fmt.Errorf("TODO: multiqueue not implemented for android")
} }
func (t *tun) TunPrefixLen() int { return 0 }
-29
View File
@@ -1,29 +0,0 @@
//go:build (darwin || ios || freebsd || openbsd || netbsd) && !e2e_testing
package overlay
import (
"fmt"
"syscall"
)
// StampTunPrefix writes the 4-byte AF_INET / AF_INET6 protocol-family marker into buf[0:4] in place,
// picking the family from the first byte of the IP packet at buf[4].
func StampTunPrefix(buf []byte) error {
if len(buf) < 5 {
return fmt.Errorf("tun write buffer too small for prefix")
}
ipVer := buf[4] >> 4
buf[0] = 0
buf[1] = 0
buf[2] = 0
switch ipVer {
case 4:
buf[3] = syscall.AF_INET
case 6:
buf[3] = syscall.AF_INET6
default:
return fmt.Errorf("unable to determine IP version from packet")
}
return nil
}
+50 -4
View File
@@ -11,6 +11,7 @@ import (
"net/netip" "net/netip"
"os" "os"
"sync/atomic" "sync/atomic"
"syscall"
"unsafe" "unsafe"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
@@ -30,6 +31,9 @@ type tun struct {
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
linkAddr *netroute.LinkAddr linkAddr *netroute.LinkAddr
l *slog.Logger l *slog.Logger
// cache out buffer since we need to prepend 4 bytes for tun metadata
out []byte
} }
type ifReq struct { type ifReq struct {
@@ -498,6 +502,44 @@ func delRoute(prefix netip.Prefix, gateway netroute.Addr) error {
return nil return nil
} }
func (t *tun) Read(to []byte) (int, error) {
buf := make([]byte, len(to)+4)
n, err := t.ReadWriteCloser.Read(buf)
copy(to, buf[4:])
return n - 4, err
}
// Write is only valid for single threaded use
func (t *tun) Write(from []byte) (int, error) {
buf := t.out
if cap(buf) < len(from)+4 {
buf = make([]byte, len(from)+4)
t.out = buf
}
buf = buf[:len(from)+4]
if len(from) == 0 {
return 0, syscall.EIO
}
// Determine the IP Family for the NULL L2 Header
ipVer := from[0] >> 4
if ipVer == 4 {
buf[3] = syscall.AF_INET
} else if ipVer == 6 {
buf[3] = syscall.AF_INET6
} else {
return 0, fmt.Errorf("unable to determine IP version from packet")
}
copy(buf[4:], from)
n, err := t.ReadWriteCloser.Write(buf)
return n - 4, err
}
func (t *tun) Networks() []netip.Prefix { func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
@@ -506,6 +548,14 @@ func (t *tun) Name() string {
return t.Device return t.Device
} }
func (t *tun) SupportsPerPeerMTU() bool {
return false
}
func (t *tun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *tun) SupportsMultiqueue() bool { func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
@@ -513,7 +563,3 @@ func (t *tun) SupportsMultiqueue() bool {
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for darwin") return nil, fmt.Errorf("TODO: multiqueue not implemented for darwin")
} }
// TunPrefixLen reports the 4-byte BSD AF_INET / AF_INET6 protocol-family
// marker the kernel prepends on read and expects on write.
func (t *tun) TunPrefixLen() int { return 4 }
+8 -2
View File
@@ -106,6 +106,14 @@ func (t *disabledTun) Write(b []byte) (int, error) {
return len(b), nil return len(b), nil
} }
func (t *disabledTun) SupportsPerPeerMTU() bool {
return false
}
func (t *disabledTun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *disabledTun) SupportsMultiqueue() bool { func (t *disabledTun) SupportsMultiqueue() bool {
return true return true
} }
@@ -136,5 +144,3 @@ func (p prettyPacket) String() string {
return s.String() return s.String()
} }
func (t *disabledTun) TunPrefixLen() int { return 0 }
+55 -20
View File
@@ -158,43 +158,74 @@ func (t *tun) blockOnWrite() error {
} }
func (t *tun) Read(to []byte) (int, error) { func (t *tun) Read(to []byte) (int, error) {
for { // first 4 bytes is protocol family, in network byte order
n, err := unix.Read(t.fd, to) var head [4]byte
if err == nil { iovecs := [2]syscall.Iovec{
return n, nil {&head[0], 4},
{&to[0], uint64(len(to))},
} }
switch err { for {
n, _, errno := syscall.Syscall(syscall.SYS_READV, uintptr(t.fd), uintptr(unsafe.Pointer(&iovecs[0])), 2)
if errno == 0 {
bytesRead := int(n)
if bytesRead < 4 {
return 0, nil
}
return bytesRead - 4, nil
}
switch errno {
case unix.EAGAIN: case unix.EAGAIN:
if berr := t.blockOnRead(); berr != nil { if err := t.blockOnRead(); err != nil {
return 0, berr return 0, err
} }
case unix.EINTR: case unix.EINTR:
// retry // retry
case unix.EBADF: case unix.EBADF:
return 0, os.ErrClosed return 0, os.ErrClosed
default: default:
return 0, err return 0, errno
} }
} }
} }
// Write is only valid for single threaded use
func (t *tun) Write(from []byte) (int, error) { func (t *tun) Write(from []byte) (int, error) {
for { if len(from) <= 1 {
n, err := unix.Write(t.fd, from) return 0, syscall.EIO
if err == nil {
return n, nil
} }
switch err {
ipVer := from[0] >> 4
var head [4]byte
// first 4 bytes is protocol family, in network byte order
switch ipVer {
case 4:
head[3] = syscall.AF_INET
case 6:
head[3] = syscall.AF_INET6
default:
return 0, fmt.Errorf("unable to determine IP version from packet")
}
iovecs := [2]syscall.Iovec{
{&head[0], 4},
{&from[0], uint64(len(from))},
}
for {
n, _, errno := syscall.Syscall(syscall.SYS_WRITEV, uintptr(t.fd), uintptr(unsafe.Pointer(&iovecs[0])), 2)
if errno == 0 {
return int(n) - 4, nil
}
switch errno {
case unix.EAGAIN: case unix.EAGAIN:
if berr := t.blockOnWrite(); berr != nil { if err := t.blockOnWrite(); err != nil {
return 0, berr return 0, err
} }
case unix.EINTR: case unix.EINTR:
// retry // retry
case unix.EBADF: case unix.EBADF:
return 0, os.ErrClosed return 0, os.ErrClosed
default: default:
return 0, err return 0, errno
} }
} }
} }
@@ -530,6 +561,14 @@ func (t *tun) Name() string {
return t.Device return t.Device
} }
func (t *tun) SupportsPerPeerMTU() bool {
return false
}
func (t *tun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *tun) SupportsMultiqueue() bool { func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
@@ -701,7 +740,3 @@ func getLinkAddr(name string) (*netroute.LinkAddr, error) {
return nil, nil return nil, nil
} }
// TunPrefixLen reports the 4-byte BSD AF_INET / AF_INET6 protocol-family
// marker the kernel prepends on read and expects on write.
func (t *tun) TunPrefixLen() int { return 4 }
+70 -5
View File
@@ -4,12 +4,15 @@
package overlay package overlay
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"net/netip" "net/netip"
"os" "os"
"sync"
"sync/atomic" "sync/atomic"
"syscall"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
@@ -33,7 +36,7 @@ func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip
file := os.NewFile(uintptr(deviceFd), "/dev/tun") file := os.NewFile(uintptr(deviceFd), "/dev/tun")
t := &tun{ t := &tun{
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
ReadWriteCloser: file, ReadWriteCloser: &tunReadCloser{f: file},
l: l, l: l,
} }
@@ -82,6 +85,64 @@ func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
return r return r
} }
// The following is hoisted up from water, we do this so we can inject our own fd on iOS
type tunReadCloser struct {
f io.ReadWriteCloser
rMu sync.Mutex
rBuf []byte
wMu sync.Mutex
wBuf []byte
}
func (tr *tunReadCloser) Read(to []byte) (int, error) {
tr.rMu.Lock()
defer tr.rMu.Unlock()
if cap(tr.rBuf) < len(to)+4 {
tr.rBuf = make([]byte, len(to)+4)
}
tr.rBuf = tr.rBuf[:len(to)+4]
n, err := tr.f.Read(tr.rBuf)
copy(to, tr.rBuf[4:])
return n - 4, err
}
func (tr *tunReadCloser) Write(from []byte) (int, error) {
if len(from) == 0 {
return 0, syscall.EIO
}
tr.wMu.Lock()
defer tr.wMu.Unlock()
if cap(tr.wBuf) < len(from)+4 {
tr.wBuf = make([]byte, len(from)+4)
}
tr.wBuf = tr.wBuf[:len(from)+4]
// Determine the IP Family for the NULL L2 Header
ipVer := from[0] >> 4
if ipVer == 4 {
tr.wBuf[3] = syscall.AF_INET
} else if ipVer == 6 {
tr.wBuf[3] = syscall.AF_INET6
} else {
return 0, errors.New("unable to determine IP version from packet")
}
copy(tr.wBuf[4:], from)
n, err := tr.f.Write(tr.wBuf)
return n - 4, err
}
func (tr *tunReadCloser) Close() error {
return tr.f.Close()
}
func (t *tun) Networks() []netip.Prefix { func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
@@ -90,6 +151,14 @@ func (t *tun) Name() string {
return "iOS" return "iOS"
} }
func (t *tun) SupportsPerPeerMTU() bool {
return false
}
func (t *tun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *tun) SupportsMultiqueue() bool { func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
@@ -97,7 +166,3 @@ func (t *tun) SupportsMultiqueue() bool {
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for ios") return nil, fmt.Errorf("TODO: multiqueue not implemented for ios")
} }
// TunPrefixLen reports the 4-byte BSD AF_INET / AF_INET6 protocol-family
// marker the kernel prepends on read and expects on write.
func (t *tun) TunPrefixLen() int { return 4 }
+63 -7
View File
@@ -368,6 +368,13 @@ func (t *tun) reload(c *config.C, initial bool) error {
} }
} }
// tun.max_mtu raises the device MTU above tun.mtu so PMTUD has headroom to
// install per-peer routes between tun.mtu (floor) and tun.max_mtu (ceiling).
// When unset (default 0) the device MTU is unchanged from existing behavior.
if pmtudCeiling := c.GetInt("tun.max_mtu", 0); pmtudCeiling > newMaxMTU {
newMaxMTU = pmtudCeiling
}
t.MaxMTU = newMaxMTU t.MaxMTU = newMaxMTU
t.DefaultMTU = newDefaultMTU t.DefaultMTU = newDefaultMTU
@@ -596,7 +603,7 @@ func (t *tun) setDefaultRoute(cidr netip.Prefix) error {
LinkIndex: t.deviceIndex, LinkIndex: t.deviceIndex,
Dst: dr, Dst: dr,
MTU: t.DefaultMTU, MTU: t.DefaultMTU,
AdvMSS: t.advMSS(Route{}), AdvMSS: t.advMSS(Route{Cidr: cidr}),
Scope: unix.RT_SCOPE_LINK, Scope: unix.RT_SCOPE_LINK,
Src: net.IP(cidr.Addr().AsSlice()), Src: net.IP(cidr.Addr().AsSlice()),
Protocol: unix.RTPROT_KERNEL, Protocol: unix.RTPROT_KERNEL,
@@ -705,17 +712,68 @@ func (t *tun) Name() string {
return t.Device return t.Device
} }
func (t *tun) SupportsPerPeerMTU() bool {
return true
}
// SetPeerMTU installs a host route (/32 for an IPv4 vpn address, /128 for an IPv6
// vpn address) to addr through this tun device with the given MTU. This causes
// the kernel to reject (or surface PTB to apps for) inside packets to addr that
// would exceed mtu. Pass mtu=0 to remove the override and let the per-vpn-network
// route apply again. PoC: assumes addr is reachable directly via this device.
func (t *tun) SetPeerMTU(addr netip.Addr, mtu int) error {
bits := addr.BitLen()
prefix := netip.PrefixFrom(addr, bits)
dr := &net.IPNet{
IP: addr.AsSlice(),
Mask: net.CIDRMask(bits, bits),
}
if mtu == 0 {
nr := netlink.Route{
LinkIndex: t.deviceIndex,
Dst: dr,
Scope: unix.RT_SCOPE_LINK,
}
if err := netlink.RouteDel(&nr); err != nil {
return fmt.Errorf("failed to remove per-peer mtu route %v: %w", prefix, err)
}
return nil
}
nr := netlink.Route{
LinkIndex: t.deviceIndex,
Dst: dr,
MTU: mtu,
AdvMSS: t.advMSS(Route{Cidr: prefix, MTU: mtu}),
Scope: unix.RT_SCOPE_LINK,
}
if err := netlink.RouteReplace(&nr); err != nil {
return fmt.Errorf("failed to set per-peer mtu route %v mtu=%d: %w", prefix, mtu, err)
}
return nil
}
func (t *tun) advMSS(r Route) int { func (t *tun) advMSS(r Route) int {
mtu := r.MTU mtu := r.MTU
if r.MTU == 0 { if r.MTU == 0 {
mtu = t.DefaultMTU mtu = t.DefaultMTU
} }
// We only need to set advmss if the route MTU does not match the device MTU // We only need to set advmss if the route MTU does not match the device MTU.
if mtu != t.MaxMTU { if mtu == t.MaxMTU {
return mtu - 40
}
return 0 return 0
}
// MSS = MTU - (IP header + TCP header). TCP is always 20 bytes; IP is 20 for
// v4 and 40 for v6. r.Cidr is the route destination so it tells us which
// family this route is in. If Cidr is unset (empty Route) we default to v4.
addr := r.Cidr.Addr()
if addr.Is6() && !addr.Is4In6() {
return mtu - 60
}
return mtu - 40
} }
func (t *tun) watchRoutes() { func (t *tun) watchRoutes() {
@@ -907,5 +965,3 @@ func (t *tun) Close() error {
} }
return err return err
} }
func (t *tun) TunPrefixLen() int { return 0 }
+106 -9
View File
@@ -58,13 +58,13 @@ type addrLifetime struct {
} }
type tun struct { type tun struct {
io.ReadWriteCloser
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
MTU int MTU int
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *slog.Logger l *slog.Logger
f *os.File
fd int fd int
} }
@@ -96,7 +96,7 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
} }
t := &tun{ t := &tun{
ReadWriteCloser: os.NewFile(uintptr(fd), ""), f: os.NewFile(uintptr(fd), ""),
fd: fd, fd: fd,
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
@@ -120,12 +120,12 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
} }
func (t *tun) Close() error { func (t *tun) Close() error {
if t.ReadWriteCloser != nil { if t.f != nil {
if err := t.ReadWriteCloser.Close(); err != nil { if err := t.f.Close(); err != nil {
return fmt.Errorf("error closing tun file: %w", err) return fmt.Errorf("error closing tun file: %w", err)
} }
// Close on the os.File should have handled the fd for us but let's be extra sure // t.f.Close should have handled it for us but let's be extra sure
_ = unix.Close(t.fd) _ = unix.Close(t.fd)
s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP) s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP)
@@ -141,6 +141,99 @@ func (t *tun) Close() error {
return nil return nil
} }
func (t *tun) Read(to []byte) (int, error) {
rc, err := t.f.SyscallConn()
if err != nil {
return 0, fmt.Errorf("failed to get syscall conn for tun: %w", err)
}
var errno syscall.Errno
var n uintptr
err = rc.Read(func(fd uintptr) bool {
// first 4 bytes is protocol family, in network byte order
head := [4]byte{}
iovecs := []syscall.Iovec{
{&head[0], 4},
{&to[0], uint64(len(to))},
}
n, _, errno = syscall.Syscall(syscall.SYS_READV, fd, uintptr(unsafe.Pointer(&iovecs[0])), uintptr(2))
if errno.Temporary() {
// We got an EAGAIN, EINTR, or EWOULDBLOCK, go again
return false
}
return true
})
if err != nil {
if err == syscall.EBADF || err.Error() == "use of closed file" {
// Go doesn't export poll.ErrFileClosing but happily reports it to us so here we are
// https://github.com/golang/go/blob/master/src/internal/poll/fd_poll_runtime.go#L121
return 0, os.ErrClosed
}
return 0, fmt.Errorf("failed to make read call for tun: %w", err)
}
if errno != 0 {
return 0, fmt.Errorf("failed to make inner read call for tun: %w", errno)
}
// fix bytes read number to exclude header
bytesRead := int(n)
if bytesRead < 0 {
return bytesRead, nil
} else if bytesRead < 4 {
return 0, nil
} else {
return bytesRead - 4, nil
}
}
// Write is only valid for single threaded use
func (t *tun) Write(from []byte) (int, error) {
if len(from) <= 1 {
return 0, syscall.EIO
}
ipVer := from[0] >> 4
var head [4]byte
// first 4 bytes is protocol family, in network byte order
if ipVer == 4 {
head[3] = syscall.AF_INET
} else if ipVer == 6 {
head[3] = syscall.AF_INET6
} else {
return 0, fmt.Errorf("unable to determine IP version from packet")
}
rc, err := t.f.SyscallConn()
if err != nil {
return 0, err
}
var errno syscall.Errno
var n uintptr
err = rc.Write(func(fd uintptr) bool {
iovecs := []syscall.Iovec{
{&head[0], 4},
{&from[0], uint64(len(from))},
}
n, _, errno = syscall.Syscall(syscall.SYS_WRITEV, fd, uintptr(unsafe.Pointer(&iovecs[0])), uintptr(2))
// According to NetBSD documentation for TUN, writes will only return errors in which
// this packet will never be delivered so just go on living life.
return true
})
if err != nil {
return 0, err
}
if errno != 0 {
return 0, errno
}
return int(n) - 4, err
}
func (t *tun) addIp(cidr netip.Prefix) error { func (t *tun) addIp(cidr netip.Prefix) error {
if cidr.Addr().Is4() { if cidr.Addr().Is4() {
var req ifreqAlias4 var req ifreqAlias4
@@ -297,6 +390,14 @@ func (t *tun) Name() string {
return t.Device return t.Device
} }
func (t *tun) SupportsPerPeerMTU() bool {
return false
}
func (t *tun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *tun) SupportsMultiqueue() bool { func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
@@ -458,7 +559,3 @@ func delRoute(prefix netip.Prefix, gateways []netip.Prefix) error {
return nil return nil
} }
// TunPrefixLen reports the 4-byte BSD AF_INET / AF_INET6 protocol-family
// marker the kernel prepends on read and expects on write.
func (t *tun) TunPrefixLen() int { return 4 }
-10
View File
@@ -1,10 +0,0 @@
//go:build (!darwin && !ios && !freebsd && !openbsd && !netbsd) || e2e_testing
package overlay
// StampTunPrefix is a no-op on platforms whose tun devices have no
// protocol-family marker. WireBuffer only invokes it when its prefixLen
// is non-zero, so this should never be reached on these platforms.
func StampTunPrefix(buf []byte) error {
return nil
}
+53 -9
View File
@@ -49,14 +49,16 @@ type ifreq struct {
} }
type tun struct { type tun struct {
io.ReadWriteCloser
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
MTU int MTU int
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *slog.Logger l *slog.Logger
f *os.File
fd int fd int
// cache out buffer since we need to prepend 4 bytes for tun metadata
out []byte
} }
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`) var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
@@ -87,7 +89,7 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
} }
t := &tun{ t := &tun{
ReadWriteCloser: os.NewFile(uintptr(fd), ""), f: os.NewFile(uintptr(fd), ""),
fd: fd, fd: fd,
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
@@ -111,17 +113,55 @@ func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*t
} }
func (t *tun) Close() error { func (t *tun) Close() error {
if t.ReadWriteCloser != nil { if t.f != nil {
if err := t.ReadWriteCloser.Close(); err != nil { if err := t.f.Close(); err != nil {
return fmt.Errorf("error closing tun file: %w", err) return fmt.Errorf("error closing tun file: %w", err)
} }
// Close on the os.File should have handled the fd for us but let's be extra sure // t.f.Close should have handled it for us but let's be extra sure
_ = unix.Close(t.fd) _ = unix.Close(t.fd)
} }
return nil return nil
} }
func (t *tun) Read(to []byte) (int, error) {
buf := make([]byte, len(to)+4)
n, err := t.f.Read(buf)
copy(to, buf[4:])
return n - 4, err
}
// Write is only valid for single threaded use
func (t *tun) Write(from []byte) (int, error) {
buf := t.out
if cap(buf) < len(from)+4 {
buf = make([]byte, len(from)+4)
t.out = buf
}
buf = buf[:len(from)+4]
if len(from) == 0 {
return 0, syscall.EIO
}
// Determine the IP Family for the NULL L2 Header
ipVer := from[0] >> 4
if ipVer == 4 {
buf[3] = syscall.AF_INET
} else if ipVer == 6 {
buf[3] = syscall.AF_INET6
} else {
return 0, fmt.Errorf("unable to determine IP version from packet")
}
copy(buf[4:], from)
n, err := t.f.Write(buf)
return n - 4, err
}
func (t *tun) addIp(cidr netip.Prefix) error { func (t *tun) addIp(cidr netip.Prefix) error {
if cidr.Addr().Is4() { if cidr.Addr().Is4() {
var req ifreqAlias4 var req ifreqAlias4
@@ -270,6 +310,14 @@ func (t *tun) Name() string {
return t.Device return t.Device
} }
func (t *tun) SupportsPerPeerMTU() bool {
return false
}
func (t *tun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *tun) SupportsMultiqueue() bool { func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
@@ -431,7 +479,3 @@ func delRoute(prefix netip.Prefix, gateways []netip.Prefix) error {
return nil return nil
} }
// TunPrefixLen reports the 4-byte BSD AF_INET / AF_INET6 protocol-family
// marker the kernel prepends on read and expects on write.
func (t *tun) TunPrefixLen() int { return 4 }
+13 -51
View File
@@ -15,7 +15,6 @@ import (
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/udp"
) )
type TestTun struct { type TestTun struct {
@@ -55,12 +54,9 @@ func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*TestTu
return nil, fmt.Errorf("newTunFromFd not supported") return nil, fmt.Errorf("newTunFromFd not supported")
} }
// Send will place a byte array onto the receive queue for nebula to consume. // Send will place a byte array onto the receive queue for nebula to consume
// These are unencrypted ip layer frames destined for another nebula node. // These are unencrypted ip layer frames destined for another nebula node.
// packets should exit the udp side, capture them with udpConn.Get. // packets should exit the udp side, capture them with udpConn.Get
//
// Send copies the input via the freelist, so the caller is free to mutate
// or reuse it after the call returns.
func (t *TestTun) Send(packet []byte) { func (t *TestTun) Send(packet []byte) {
if t.closed.Load() { if t.closed.Load() {
return return
@@ -69,9 +65,7 @@ func (t *TestTun) Send(packet []byte) {
if t.l.Enabled(context.Background(), slog.LevelDebug) { if t.l.Enabled(context.Background(), slog.LevelDebug) {
t.l.Debug("Tun receiving injected packet", "dataLen", len(packet)) t.l.Debug("Tun receiving injected packet", "dataLen", len(packet))
} }
buf := acquireTunBuf(len(packet)) t.rxPackets <- packet
copy(buf, packet)
t.rxPackets <- buf
} }
// Get will pull an unencrypted ip layer frame from the transmit queue // Get will pull an unencrypted ip layer frame from the transmit queue
@@ -111,49 +105,25 @@ func (t *TestTun) Name() string {
return t.Device return t.Device
} }
func (t *TestTun) SupportsPerPeerMTU() bool {
return false
}
func (t *TestTun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *TestTun) Write(b []byte) (n int, err error) { func (t *TestTun) Write(b []byte) (n int, err error) {
if t.closed.Load() { if t.closed.Load() {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
} }
packet := acquireTunBuf(len(b)) packet := make([]byte, len(b), len(b))
copy(packet, b) copy(packet, b)
t.TxPackets <- packet t.TxPackets <- packet
return len(b), nil return len(b), nil
} }
// ReleaseTunBuf returns a slice from TxPackets to the harness freelist, don't use the bytes after the call.
// Channel-backed instead of sync.Pool because putting a []byte in a sync.Pool escapes the slice header to heap.
func ReleaseTunBuf(b []byte) {
if b == nil {
return
}
select {
case tunBufFreelist <- b:
default:
// Freelist full; drop the buffer for the GC.
}
}
// tunBufFreelist retains the backing arrays for TestTun.Write so steady-state allocation drops to zero once the
// freelist has saturated for the current MTU.
var tunBufFreelist = make(chan []byte, 64)
func acquireTunBuf(n int) []byte {
var b []byte
select {
case b = <-tunBufFreelist:
default:
b = make([]byte, 0, udp.MTU)
}
if cap(b) < n {
b = make([]byte, n)
} else {
b = b[:n]
}
return b
}
func (t *TestTun) Close() error { func (t *TestTun) Close() error {
if t.closed.CompareAndSwap(false, true) { if t.closed.CompareAndSwap(false, true) {
close(t.rxPackets) close(t.rxPackets)
@@ -167,14 +137,8 @@ func (t *TestTun) Read(b []byte) (int, error) {
if !ok { if !ok {
return 0, os.ErrClosed return 0, os.ErrClosed
} }
n := len(p)
copy(b, p) copy(b, p)
// Send always pushes a freelist-acquired slice, return it once we've copied the bytes into the caller's buffer. return len(p), nil
select {
case tunBufFreelist <- p:
default:
}
return n, nil
} }
func (t *TestTun) SupportsMultiqueue() bool { func (t *TestTun) SupportsMultiqueue() bool {
@@ -184,5 +148,3 @@ func (t *TestTun) SupportsMultiqueue() bool {
func (t *TestTun) NewMultiQueueReader() (io.ReadWriteCloser, error) { func (t *TestTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented") return nil, fmt.Errorf("TODO: multiqueue not implemented")
} }
func (t *TestTun) TunPrefixLen() int { return 0 }
+8 -2
View File
@@ -229,6 +229,14 @@ func (t *winTun) Name() string {
return t.Device return t.Device
} }
func (t *winTun) SupportsPerPeerMTU() bool {
return false
}
func (t *winTun) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (t *winTun) Read(b []byte) (int, error) { func (t *winTun) Read(b []byte) (int, error) {
return t.tun.Read(b, 0) return t.tun.Read(b, 0)
} }
@@ -296,5 +304,3 @@ func checkWinTunExists() error {
_, err = syscall.LoadDLL(filepath.Join(filepath.Dir(myPath), "dist", "windows", "wintun", "bin", arch, "wintun.dll")) _, err = syscall.LoadDLL(filepath.Join(filepath.Dir(myPath), "dist", "windows", "wintun", "bin", arch, "wintun.dll"))
return err return err
} }
func (t *winTun) TunPrefixLen() int { return 0 }
+8 -2
View File
@@ -46,6 +46,14 @@ func (d *UserDevice) RoutesFor(ip netip.Addr) routing.Gateways {
return routing.Gateways{routing.NewGateway(ip, 1)} return routing.Gateways{routing.NewGateway(ip, 1)}
} }
func (d *UserDevice) SupportsPerPeerMTU() bool {
return false
}
func (d *UserDevice) SetPeerMTU(addr netip.Addr, mtu int) error {
return nil
}
func (d *UserDevice) SupportsMultiqueue() bool { func (d *UserDevice) SupportsMultiqueue() bool {
return true return true
} }
@@ -69,5 +77,3 @@ func (d *UserDevice) Close() error {
d.outboundWriter.Close() d.outboundWriter.Close()
return nil return nil
} }
func (d *UserDevice) TunPrefixLen() int { return 0 }
+623
View File
@@ -0,0 +1,623 @@
package nebula
import (
"context"
"encoding/binary"
"log/slog"
"math/rand/v2"
"net/netip"
"sync"
"sync/atomic"
"time"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/overlay"
)
// PMTUD PoC: discover the path MTU per-tunnel via authenticated probes that ride
// the existing crypto session. We follow RFC 8899 PLPMTUD: a binary search
// between a known-good floor and a configured ceiling, with N consecutive probe
// losses at a size treated as "doesn't fit." Confirmed PMTU is pushed to the
// overlay device, which on Linux installs a per-host route with the discovered
// MTU. The kernel then surfaces EMSGSIZE / PTB to apps writing to the tun.
//
// Probe payload format (request):
//
// [magic uint32 BE][probeID uint32 BE][padding 0x00...]
//
// Reply is a small ack with the same magic and probeID and no padding. We do not
// verify the reverse-path MTU; only the forward direction matters for the
// receiver's MTU on the inside.
const (
pmtudMagic uint32 = 0x504D5544 // 'P' 'M' 'U' 'D'
pmtudFloor = 1280 // IPv6 minimum payload, also a safe internet MTU floor
// pmtudConverged is the bytes-tolerance for stopping the search.
pmtudConverged = 8
// pmtudMaxLoss matches RFC 8899 MAX_PROBES (default 3).
pmtudMaxLoss = 3
// pmtudProbeInterval is the time between probe ticks during the search phase.
// Once a peer converges the wheel stops ticking it; re-validation is driven
// by connection_manager via MaybeProbeAsTest at its natural test cadence.
pmtudProbeInterval = 500 * time.Millisecond
// pmtudWheelMax is the wheel's maximum supported scheduling duration. We
// only ever schedule at pmtudProbeInterval today, but the wheel needs a
// max greater than its tick to allocate its slot ring sensibly.
pmtudWheelMax = 5 * time.Second
// pmtudOverheadPessimistic assumes IPv6 underlay + relay framing:
// IPv6(40) + UDP(8) + outer nebula(16) + outer AEAD tag(16)
// + inner nebula(16) + inner AEAD tag(16) = 112 bytes.
// TODO: track underlay address family and per-peer relay state on the HostInfo
// so the manager can use the actual overhead for that tunnel and recover the
// 32 bytes we pessimistically give up on direct IPv6 paths and the 52 bytes on
// direct IPv4 paths.
pmtudOverheadPessimistic = 112
// pmtudUnsupportedAfter is the number of consecutive lost probes (across any
// sizes) without ever receiving a reply that we treat as evidence the peer
// does not understand the MTUDProbeRequest subtype (i.e. it's running an
// older nebula). After this many failures with everReplied=false we mark the
// peer pmtud-unsupported and stop scheduling probes. K is small enough that
// it fires before the binary search would naturally converge to floor (which
// would otherwise be ~30 wasted probes), but large enough to absorb a few
// transient probe losses on a path that's just starting to settle.
pmtudUnsupportedAfter = 5
)
// pmtudPeer tracks the binary-search state for one tunnel.
type pmtudPeer struct {
mu sync.Mutex
addr netip.Addr
localIdx uint32
// low is the largest outer IP packet size we have a confirmed ack for.
// high is the smallest size we believe fails (the search ceiling to start).
low, high int
// inFlightSize is the outer IP packet size of the probe currently awaiting
// an ack. 0 means no probe in flight.
inFlightSize int
// inFlightID matches the probeID echoed in the reply.
inFlightID uint32
// losses counts consecutive failures at inFlightSize.
losses int
// firstProbe is true until we have sent the first probe of a search. The
// first probe targets the ceiling directly (RFC 8899 permits this Search
// Algorithm choice); operators who set tun.max_mtu typically have a path
// that supports it, so we converge in one probe in the common case.
firstProbe bool
// everReplied is true once we have ever received any MTUDProbeReply from
// this peer. Combined with consecutiveFailures, this lets us detect peers
// that don't understand the new subtype and stop probing them.
everReplied bool
// consecutiveFailures counts probes lost without an intervening reply.
// Resets to 0 on any successful reply.
consecutiveFailures int
// unsupported is set true once we conclude the peer doesn't speak PMTUD.
// The manager skips probes for unsupported peers.
unsupported bool
// converged means we have a confirmed PMTU and are in the slow re-validation phase.
converged bool
// applied is the inner MTU we last pushed to the overlay device (0 if never).
applied int
}
func (p *pmtudPeer) overhead() int {
// TODO: branch on actual underlay family + relay state for this peer.
return pmtudOverheadPessimistic
}
func (p *pmtudPeer) midpoint() int {
return (p.low + p.high) / 2
}
type pmtudManager struct {
intf *Interface
device overlay.Device
// peers is keyed by HostInfo.localIndexId.
peers sync.Map // map[uint32]*pmtudPeer
wheel *LockingTimerWheel[uint32]
// floor is the always-safe inner MTU (= tun.mtu). Per-peer routes start here
// on tunnel-up so unprobed traffic is always small enough to fit. Stored as
// atomic int64 so reload can update it without coordinating with the readers
// in tick/HandleReply/OnTunnelUp.
floor atomic.Int64
// ceiling is the search ceiling expressed as an outer IP packet size, derived
// from tun.max_mtu (which is the kernel's device MTU on the tun) plus our
// pessimistic overhead. PMTUD will not probe larger than this.
ceiling atomic.Int64
enabled atomic.Bool
l *slog.Logger
}
func newPMTUDManagerFromConfig(l *slog.Logger, c *config.C, device overlay.Device) *pmtudManager {
m := &pmtudManager{
device: device,
wheel: NewLockingTimerWheel[uint32](pmtudProbeInterval, pmtudWheelMax),
l: l,
}
c.RegisterReloadCallback(func(c *config.C) { m.reload(c, false) })
m.reload(c, true)
return m
}
// reload applies tun.mtu / tun.max_mtu changes to the manager. On the initial
// call (during construction) it just snapshots state; on a live reload it also
// transitions in-flight peers to match the new bounds: clearing per-peer routes
// when newly disabled, seeding peers from the hostmap and flipping DF on
// outside sockets when newly enabled, and rebounding existing searches in
// place when only the ceiling moved.
func (m *pmtudManager) reload(c *config.C, initial bool) {
if !initial && !c.HasChanged("tun.mtu") && !c.HasChanged("tun.max_mtu") {
return
}
floor := c.GetInt("tun.mtu", overlay.DefaultMTU)
maxMTU := c.GetInt("tun.max_mtu", 0)
enable := maxMTU > floor && m.device.SupportsPerPeerMTU()
var ceiling int
if enable {
ceiling = maxMTU + pmtudOverheadPessimistic
}
if initial {
m.floor.Store(int64(floor))
m.ceiling.Store(int64(ceiling))
m.enabled.Store(enable)
switch {
case enable:
m.l.Info("pmtud enabled", "floor", floor, "ceiling", ceiling, "tun.max_mtu", maxMTU)
case maxMTU > floor:
m.l.Warn("pmtud disabled: this platform does not yet support per-peer MTU routes",
"tun.max_mtu", maxMTU)
}
return
}
wasEnabled := m.enabled.Load()
m.floor.Store(int64(floor))
m.ceiling.Store(int64(ceiling))
m.enabled.Store(enable)
switch {
case wasEnabled && !enable:
m.disableLive(floor, maxMTU)
case !wasEnabled && enable:
m.enableLive(floor, ceiling, maxMTU)
case wasEnabled && enable:
m.reboundLive(floor, ceiling, maxMTU)
}
}
// disableLive clears per-peer routes and drops all peer state. We do not
// disable DF on the outside sockets; once on, it stays on for the life of the
// process. Operators flipping pmtud off live get correct routing behavior; if
// they want the historical no-DF behavior back they need to restart.
func (m *pmtudManager) disableLive(floor, maxMTU int) {
m.peers.Range(func(k, v any) bool {
p := v.(*pmtudPeer)
p.mu.Lock()
applied := p.applied
addr := p.addr
p.applied = 0
p.mu.Unlock()
if applied != 0 {
if err := m.device.SetPeerMTU(addr, 0); err != nil {
m.l.Warn("pmtud: failed to clear per-peer mtu on disable", "addr", addr, "error", err)
}
}
m.peers.Delete(k)
return true
})
m.l.Info("pmtud disabled (tun.max_mtu <= tun.mtu)", "tun.mtu", floor, "tun.max_mtu", maxMTU)
}
// enableLive flips DF on every outside socket. We don't pre-seed existing
// tunnels here; connection_manager's normal test cadence will eventually call
// MaybeProbeAsTest for each peer, which seeds on miss and lets the wheel pick
// up the search from there. New tunnels established after this point still
// take the OnTunnelUp fast path.
func (m *pmtudManager) enableLive(floor, ceiling, maxMTU int) {
m.enableDF()
m.l.Info("pmtud enabled", "floor", floor, "ceiling", ceiling, "tun.max_mtu", maxMTU)
}
// reboundLive resets each peer's search state to the new bounds. Peers whose
// confirmed PMTU still fits under the new ceiling keep their applied route in
// place during the new search; peers whose confirmed PMTU exceeds the new
// ceiling get cleared back to floor and re-search from scratch. The unsupported
// flag is preserved because peer software version doesn't change on reload.
func (m *pmtudManager) reboundLive(floor, ceiling, maxMTU int) {
overhead := pmtudOverheadPessimistic
m.peers.Range(func(k, v any) bool {
p := v.(*pmtudPeer)
p.mu.Lock()
if p.applied > 0 && p.applied+overhead > ceiling {
if err := m.device.SetPeerMTU(p.addr, 0); err != nil {
m.l.Warn("pmtud: failed to clear per-peer mtu on rebound", "addr", p.addr, "error", err)
} else {
p.applied = 0
}
}
p.low = floor + overhead
p.high = ceiling
p.inFlightSize = 0
p.inFlightID = 0
p.losses = 0
p.firstProbe = !p.unsupported
p.converged = false
idx := p.localIdx
unsupported := p.unsupported
p.mu.Unlock()
if !unsupported {
m.wheel.Add(idx, pmtudProbeInterval)
}
return true
})
m.l.Info("pmtud reloaded", "floor", floor, "ceiling", ceiling, "tun.max_mtu", maxMTU)
}
// enableDF asks every outside socket to set the don't-fragment bit on outbound
// packets. Idempotent: safe to call from both Start (initial enable) and from a
// live reload that flips pmtud on.
func (m *pmtudManager) enableDF() {
for i, w := range m.intf.writers {
if err := w.EnablePathMTUDiscovery(); err != nil {
m.l.Warn("pmtud: failed to enable path mtu discovery on outside socket; pmtud will not work correctly",
"writer", i, "error", err)
}
}
}
// Start runs the probe scheduler until ctx is done. The loop runs even when PMTUD
// is disabled at startup so a hot reload can turn it on without restarting nebula.
//
// When PMTUD is enabled at startup we ask each outside socket to enable
// path-MTU discovery (DF on every send). This is intentionally gated on the
// feature being on so that operators who haven't opted in keep the historical
// behavior where the kernel may fragment outbound nebula UDP packets. A live
// reload from disabled to enabled will also flip DF on via enableLive; the
// reverse direction does not turn DF off, so flipping pmtud back off live
// keeps DF on until restart.
func (m *pmtudManager) Start(ctx context.Context) {
if m.enabled.Load() {
m.enableDF()
}
ticker := time.NewTicker(m.wheel.t.tickDuration)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case now := <-ticker.C:
m.wheel.Advance(now)
for {
idx, has := m.wheel.Purge()
if !has {
break
}
m.tick(idx)
}
}
}
}
// OnTunnelUp is called when a HostInfo becomes traffic-watched. The kernel
// already routes packets to this peer through the per-vpn-network route (mtu =
// tun.mtu), so the floor is in effect implicitly. We just kick off the search
// here; HandleReply will install a per-host /32 (or /128) route once a larger
// size is confirmed.
func (m *pmtudManager) OnTunnelUp(hi *HostInfo) {
if !m.enabled.Load() {
return
}
m.seedPeer(hi)
}
// seedPeer is the shared body of OnTunnelUp and the live-reload enable path.
// LoadOrStore protects against double-seeding the same localIndexId from a
// race between OnTunnelUp and a reload-driven hostmap walk.
func (m *pmtudManager) seedPeer(hi *HostInfo) {
if hi == nil || len(hi.vpnAddrs) == 0 {
return
}
floor := int(m.floor.Load())
ceiling := int(m.ceiling.Load())
p := &pmtudPeer{
addr: hi.vpnAddrs[0],
localIdx: hi.localIndexId,
low: floor + pmtudOverheadPessimistic,
high: ceiling,
firstProbe: true,
}
if _, loaded := m.peers.LoadOrStore(hi.localIndexId, p); loaded {
return
}
m.wheel.Add(hi.localIndexId, pmtudProbeInterval)
}
// OnTunnelDown is called when a HostInfo is being torn down. Removes any per-host
// MTU override so the device default applies again.
func (m *pmtudManager) OnTunnelDown(hi *HostInfo) {
if hi == nil {
return
}
v, ok := m.peers.LoadAndDelete(hi.localIndexId)
if !ok {
return
}
p := v.(*pmtudPeer)
p.mu.Lock()
applied := p.applied
addr := p.addr
p.applied = 0
p.mu.Unlock()
if applied != 0 {
if err := m.device.SetPeerMTU(addr, 0); err != nil {
m.l.Warn("pmtud: failed to clear per-peer mtu", "addr", addr, "error", err)
}
}
}
// OnRoam is called when a HostInfo's remote underlay address changes. The path
// MTU may now be different; drop the per-host route so the kernel falls back to
// the per-vpn-network route (mtu = tun.mtu floor), then restart the search.
// We do not reset the unsupported flag: peer software version doesn't change on
// roam, so once we've decided a peer doesn't speak PMTUD we stay decided.
func (m *pmtudManager) OnRoam(hi *HostInfo) {
if !m.enabled.Load() || hi == nil {
return
}
v, ok := m.peers.Load(hi.localIndexId)
if !ok {
return
}
p := v.(*pmtudPeer)
p.mu.Lock()
if p.unsupported {
p.mu.Unlock()
return
}
p.low = int(m.floor.Load()) + pmtudOverheadPessimistic
p.high = int(m.ceiling.Load())
p.inFlightSize = 0
p.inFlightID = 0
p.losses = 0
p.consecutiveFailures = 0
p.firstProbe = true
p.converged = false
if p.applied != 0 {
if err := m.device.SetPeerMTU(p.addr, 0); err != nil {
m.l.Warn("pmtud: failed to clear per-peer mtu on roam", "addr", p.addr, "error", err)
} else {
p.applied = 0
}
}
p.mu.Unlock()
m.wheel.Add(hi.localIndexId, pmtudProbeInterval)
}
// MaybeProbeAsTest is called by connection_manager when it would otherwise send
// a TestRequest because a tunnel has gone silent. If we have a confirmed PMTU
// for this peer that's larger than the floor, we send a probe at that size
// instead. The reply confirms both liveness (consumed by connection_manager via
// the existing inbound traffic accounting fallthrough in outside.go) and that
// the confirmed PMTU still fits (consumed by HandleReply here). One synthetic
// packet does the work of two.
//
// Returns true if a probe was sent. False means the caller should send a
// regular TestRequest at the floor.
//
// On probe failure, connection_manager's existing pendingDeletion timeout will
// tear the tunnel down. Heavy hammer, but correct: a re-handshake re-runs PMTUD
// discovery against the now-shrunken path. A future EMSGSIZE-capture followup
// can replace this with a soft-drop-and-research flow.
func (m *pmtudManager) MaybeProbeAsTest(hi *HostInfo) bool {
if !m.enabled.Load() || hi == nil {
return false
}
v, ok := m.peers.Load(hi.localIndexId)
if !ok {
// Tunnel pre-dates the manager being aware of it (e.g. pmtud was just
// enabled live, or AddTrafficWatch fired before this call). Seed the
// peer so the wheel picks up the search; let connection_manager send
// its regular TestRequest this cycle.
m.seedPeer(hi)
return false
}
p := v.(*pmtudPeer)
p.mu.Lock()
if p.unsupported || p.applied == 0 {
p.mu.Unlock()
return false
}
overhead := p.overhead()
size := p.applied + overhead
id := rand.Uint32()
p.inFlightSize = size
p.inFlightID = id
p.mu.Unlock()
m.sendProbe(hi, size, id, overhead)
return true
}
// HandleReply consumes an MTUDProbeReply payload from the receive path.
func (m *pmtudManager) HandleReply(localIdx uint32, payload []byte) {
if !m.enabled.Load() {
return
}
if len(payload) < 8 {
return
}
if binary.BigEndian.Uint32(payload[0:4]) != pmtudMagic {
return
}
id := binary.BigEndian.Uint32(payload[4:8])
v, ok := m.peers.Load(localIdx)
if !ok {
return
}
p := v.(*pmtudPeer)
p.mu.Lock()
defer p.mu.Unlock()
if p.inFlightSize == 0 || p.inFlightID != id {
return
}
confirmed := p.inFlightSize
p.low = confirmed
p.inFlightSize = 0
p.losses = 0
p.everReplied = true
p.consecutiveFailures = 0
innerMTU := confirmed - p.overhead()
// Only install a /32 override when it would actually raise the MTU above the
// per-vpn-network floor route. If the discovered MTU is <= floor, the /24
// already covers it; installing a /32 at floor would just create roam churn.
if innerMTU > int(m.floor.Load()) && p.applied != innerMTU {
if err := m.device.SetPeerMTU(p.addr, innerMTU); err != nil {
m.l.Warn("pmtud: failed to apply per-peer mtu", "addr", p.addr, "innerMTU", innerMTU, "error", err)
} else {
m.l.Info("pmtud probe confirmed",
"addr", p.addr,
"outerMTU", confirmed,
"innerMTU", innerMTU,
"low", p.low,
"high", p.high,
)
p.applied = innerMTU
}
}
if p.high-p.low <= pmtudConverged {
p.converged = true
} else {
p.converged = false
}
}
// tick handles one wheel firing for a single peer.
func (m *pmtudManager) tick(localIdx uint32) {
v, ok := m.peers.Load(localIdx)
if !ok {
return
}
p := v.(*pmtudPeer)
p.mu.Lock()
if p.unsupported {
p.mu.Unlock()
return
}
// If a probe was outstanding, this tick is the loss timeout.
if p.inFlightSize != 0 {
p.losses++
p.consecutiveFailures++
if p.losses >= pmtudMaxLoss {
p.high = p.inFlightSize
p.inFlightSize = 0
p.losses = 0
if p.high-p.low <= pmtudConverged {
p.converged = true
}
}
}
// If we've never gotten a reply from this peer and we've burned through our
// failure budget, conclude the peer doesn't understand the MTUDProbeRequest
// subtype and stop scheduling probes for it.
if !p.everReplied && p.consecutiveFailures >= pmtudUnsupportedAfter {
p.unsupported = true
addr := p.addr
p.mu.Unlock()
m.l.Info("pmtud: peer not responding to probes, marking unsupported",
"addr", addr, "failures", pmtudUnsupportedAfter)
return
}
hi := m.intf.hostMap.QueryIndex(localIdx)
if hi == nil {
p.mu.Unlock()
m.peers.Delete(localIdx)
return
}
// Once a peer converges, the wheel stops scheduling for it. Re-validation
// (and the resulting black hole detection) is driven by connection_manager
// via MaybeProbeAsTest at its natural test cadence, so a converged peer
// has nothing for the wheel to do until OnRoam or a tunnel down/up cycle
// triggers a fresh search.
if p.converged {
p.mu.Unlock()
return
}
ceiling := int(m.ceiling.Load())
var size int
switch {
case p.firstProbe:
// Probe the ceiling directly. If the path supports it (the common case
// when an operator has explicitly configured tun.max_mtu), we converge
// in one round trip. If it fails, the standard binary search resumes
// on the next tick from the (low, ceiling) bounds.
size = ceiling
p.firstProbe = false
case p.losses > 0 && p.inFlightSize != 0:
size = p.inFlightSize
default:
size = p.midpoint()
}
if size < pmtudFloor {
size = pmtudFloor
}
if size > ceiling {
size = ceiling
}
id := rand.Uint32()
p.inFlightSize = size
p.inFlightID = id
overhead := p.overhead()
p.mu.Unlock()
m.sendProbe(hi, size, id, overhead)
m.wheel.Add(localIdx, pmtudProbeInterval)
}
// sendProbe builds an MTUDProbeRequest payload that will produce an outer IP
// packet of approximately `outerSize` bytes, then sends it.
func (m *pmtudManager) sendProbe(hi *HostInfo, outerSize int, id uint32, overhead int) {
payloadLen := outerSize - overhead
if payloadLen < 8 {
payloadLen = 8
}
p := make([]byte, payloadLen)
binary.BigEndian.PutUint32(p[0:4], pmtudMagic)
binary.BigEndian.PutUint32(p[4:8], id)
// remaining bytes are zero-padding
nb := make([]byte, 12)
out := make([]byte, outerSize+128) // headroom for header/tag/relay framing
m.intf.SendMessageToHostInfo(header.Test, header.MTUDProbeRequest, hi, p, nb, out)
}
+6 -15
View File
@@ -63,9 +63,6 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
} }
hostinfo.logger(rm.l).Info("Attempt to relay through hosts", "relays", hostinfo.remotes.relays) hostinfo.logger(rm.l).Info("Attempt to relay through hosts", "relays", hostinfo.remotes.relays)
// One WireBuffer for the whole relay-fanout loop.
buf := f.bufAlloc.Acquire()
defer f.bufAlloc.Release(buf)
// Send a RelayRequest to all known Relay IP's // Send a RelayRequest to all known Relay IP's
for _, relay := range hostinfo.remotes.relays { for _, relay := range hostinfo.remotes.relays {
// Don't relay through the host I'm trying to connect to // Don't relay through the host I'm trying to connect to
@@ -127,7 +124,7 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
if err != nil { if err != nil {
hostinfo.logger(rm.l).Error("Failed to marshal Control message to create relay", "error", err) hostinfo.logger(rm.l).Error("Failed to marshal Control message to create relay", "error", err)
} else { } else {
f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, buf) f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, make([]byte, 12), make([]byte, mtu))
rm.l.Info("send CreateRelayRequest", rm.l.Info("send CreateRelayRequest",
"relayFrom", f.myVpnAddrs[0], "relayFrom", f.myVpnAddrs[0],
"relayTo", vpnIp, "relayTo", vpnIp,
@@ -142,7 +139,7 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
switch existingRelay.State { switch existingRelay.State {
case Established: case Established:
hostinfo.logger(rm.l).Info("Send handshake via relay", "relay", relay.String()) hostinfo.logger(rm.l).Info("Send handshake via relay", "relay", relay.String())
f.SendVia(relayHostInfo, existingRelay, stage0, buf) f.SendVia(relayHostInfo, existingRelay, stage0, make([]byte, 12), make([]byte, mtu), false)
case Disestablished: case Disestablished:
// Mark this relay as 'requested' // Mark this relay as 'requested'
relayHostInfo.relayState.UpdateRelayForByIpState(vpnIp, Requested) relayHostInfo.relayState.UpdateRelayForByIpState(vpnIp, Requested)
@@ -183,7 +180,7 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hostinfo *Ho
hostinfo.logger(rm.l).Error("Failed to marshal Control message to create relay", "error", err) hostinfo.logger(rm.l).Error("Failed to marshal Control message to create relay", "error", err)
} else { } else {
// This must send over the hostinfo, not over hm.Hosts[ip] // This must send over the hostinfo, not over hm.Hosts[ip]
f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, buf) f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, make([]byte, 12), make([]byte, mtu))
rm.l.Info("send CreateRelayRequest", rm.l.Info("send CreateRelayRequest",
"relayFrom", f.myVpnAddrs[0], "relayFrom", f.myVpnAddrs[0],
"relayTo", vpnIp, "relayTo", vpnIp,
@@ -371,9 +368,7 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
if err != nil { if err != nil {
rm.l.Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay", "error", err) rm.l.Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay", "error", err)
} else { } else {
buf := f.bufAlloc.Acquire() f.SendMessageToHostInfo(header.Control, 0, peerHostInfo, msg, make([]byte, 12), make([]byte, mtu))
f.SendMessageToHostInfo(header.Control, 0, peerHostInfo, msg, buf)
f.bufAlloc.Release(buf)
rm.l.Info("send CreateRelayResponse", rm.l.Info("send CreateRelayResponse",
"relayFrom", resp.RelayFromAddr, "relayFrom", resp.RelayFromAddr,
"relayTo", resp.RelayToAddr, "relayTo", resp.RelayToAddr,
@@ -473,9 +468,7 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
if err != nil { if err != nil {
logMsg.Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay", "error", err) logMsg.Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay", "error", err)
} else { } else {
buf := f.bufAlloc.Acquire() f.SendMessageToHostInfo(header.Control, 0, h, msg, make([]byte, 12), make([]byte, mtu))
f.SendMessageToHostInfo(header.Control, 0, h, msg, buf)
f.bufAlloc.Release(buf)
rm.l.Info("send CreateRelayResponse", rm.l.Info("send CreateRelayResponse",
"relayFrom", from, "relayFrom", from,
"relayTo", target, "relayTo", target,
@@ -545,9 +538,7 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
if err != nil { if err != nil {
logMsg.Error("relayManager Failed to marshal Control message to create relay", "error", err) logMsg.Error("relayManager Failed to marshal Control message to create relay", "error", err)
} else { } else {
buf := f.bufAlloc.Acquire() f.SendMessageToHostInfo(header.Control, 0, peer, msg, make([]byte, 12), make([]byte, mtu))
f.SendMessageToHostInfo(header.Control, 0, peer, msg, buf)
f.bufAlloc.Release(buf)
rm.l.Info("send CreateRelayRequest", rm.l.Info("send CreateRelayRequest",
"relayFrom", h.vpnAddrs[0], "relayFrom", h.vpnAddrs[0],
"relayTo", target, "relayTo", target,
+9 -3
View File
@@ -632,9 +632,15 @@ func sshCloseTunnel(ifce *Interface, fs any, a []string, w sshd.StringWriter) er
} }
if !flags.LocalOnly { if !flags.LocalOnly {
buf := ifce.bufAlloc.Acquire() ifce.send(
ifce.send(header.CloseTunnel, 0, hostInfo.ConnectionState, hostInfo, []byte{}, buf) header.CloseTunnel,
ifce.bufAlloc.Release(buf) 0,
hostInfo.ConnectionState,
hostInfo,
[]byte{},
make([]byte, 12, 12),
make([]byte, mtu),
)
} }
ifce.closeTunnel(hostInfo) ifce.closeTunnel(hostInfo)
+9
View File
@@ -20,6 +20,12 @@ type Conn interface {
WriteTo(b []byte, addr netip.AddrPort) error WriteTo(b []byte, addr netip.AddrPort) error
ReloadConfig(c *config.C) ReloadConfig(c *config.C)
SupportsMultipleReaders() bool SupportsMultipleReaders() bool
// EnablePathMTUDiscovery sets the don't-fragment bit on outgoing packets for
// this socket. Called by the pmtud manager when PMTUD is enabled. A no-op on
// platforms that don't support it; nebula's default behavior (no DF, kernel
// fragmentation allowed) is preserved on those platforms and on this one when
// PMTUD is disabled.
EnablePathMTUDiscovery() error
Close() error Close() error
} }
@@ -43,6 +49,9 @@ func (NoopConn) WriteTo(_ []byte, _ netip.AddrPort) error {
func (NoopConn) ReloadConfig(_ *config.C) { func (NoopConn) ReloadConfig(_ *config.C) {
return return
} }
func (NoopConn) EnablePathMTUDiscovery() error {
return nil
}
func (NoopConn) Close() error { func (NoopConn) Close() error {
return nil return nil
} }
+14
View File
@@ -44,3 +44,17 @@ func NewListenConfig(multi bool) net.ListenConfig {
func (u *GenericConn) Rebind() error { func (u *GenericConn) Rebind() error {
return nil return nil
} }
// EnablePathMTUDiscovery sets the don't-fragment bit on outbound packets.
// Android is Linux underneath, so we use IP_PMTUDISC_PROBE (kernel sets DF but
// does not consume incoming ICMP frag-needed for its PMTU cache; the manager
// drives discovery via authenticated probes).
func (u *GenericConn) EnablePathMTUDiscovery() error {
v4 := u.isV4Socket()
return u.controlFD(func(fd uintptr) error {
if v4 {
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_MTU_DISCOVER, unix.IP_PMTUDISC_PROBE)
}
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_MTU_DISCOVER, unix.IPV6_PMTUDISC_PROBE)
})
}
+5
View File
@@ -47,3 +47,8 @@ func NewListenConfig(multi bool) net.ListenConfig {
func (u *GenericConn) Rebind() error { func (u *GenericConn) Rebind() error {
return nil return nil
} }
// EnablePathMTUDiscovery is split into per-OS files: udp_freebsd.go handles
// FreeBSD (which has both IP_DONTFRAG and IPV6_DONTFRAG in the unix package);
// udp_openbsd.go handles OpenBSD (v6 only; the kernel doesn't expose a v4 DF
// sockopt).
+11
View File
@@ -187,6 +187,17 @@ func (u *StdConn) SupportsMultipleReaders() bool {
return false return false
} }
// EnablePathMTUDiscovery sets the don't-fragment bit on every outbound packet.
// On darwin we use IP_DONTFRAG (v4) / IPV6_DONTFRAG (v6). The kernel will return
// EMSGSIZE for sends that exceed the local interface MTU; ICMP-driven PMTU
// updates from upstream routers are processed by the kernel as usual.
func (u *StdConn) EnablePathMTUDiscovery() error {
if u.isV4 {
return syscall.SetsockoptInt(int(u.sysFd), syscall.IPPROTO_IP, unix.IP_DONTFRAG, 1)
}
return syscall.SetsockoptInt(int(u.sysFd), syscall.IPPROTO_IPV6, unix.IPV6_DONTFRAG, 1)
}
func (u *StdConn) Rebind() error { func (u *StdConn) Rebind() error {
var err error var err error
if u.isV4 { if u.isV4 {
+25
View File
@@ -0,0 +1,25 @@
//go:build freebsd && !e2e_testing
// +build freebsd,!e2e_testing
package udp
import (
"golang.org/x/sys/unix"
)
// EnablePathMTUDiscovery sets the don't-fragment bit on outbound packets.
// FreeBSD exposes IP_DONTFRAG (v4) and IPV6_DONTFRAG (v6) in golang.org/x/sys/unix.
// Unlike Linux, BSDs don't have an explicit "don't consume incoming ICMP
// frag-needed" knob for unconnected UDP sockets; the kernel's PMTU cache will
// be updated from ICMP, which is benign for our usage (the cache only affects
// what EMSGSIZE gets surfaced for; the manager drives its own discovery via
// authenticated probes).
func (u *GenericConn) EnablePathMTUDiscovery() error {
v4 := u.isV4Socket()
return u.controlFD(func(fd uintptr) error {
if v4 {
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_DONTFRAG, 1)
}
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_DONTFRAG, 1)
})
}
+38
View File
@@ -100,3 +100,41 @@ func (u *GenericConn) ListenOut(r EncReader) error {
func (u *GenericConn) SupportsMultipleReaders() bool { func (u *GenericConn) SupportsMultipleReaders() bool {
return false return false
} }
// EnablePathMTUDiscovery is implemented per-platform alongside Rebind, in
// udp_android.go / udp_bsd.go / udp_netbsd.go / udp_windows.go.
// controlFD invokes f with the underlying UDP socket file descriptor (or
// handle, on Windows). Used by platform files for setsockopt calls that the
// stdlib net.UDPConn does not expose directly.
func (u *GenericConn) controlFD(f func(fd uintptr) error) error {
rc, err := u.UDPConn.SyscallConn()
if err != nil {
return err
}
var sockErr error
err = rc.Control(func(fd uintptr) {
sockErr = f(fd)
})
if err != nil {
return err
}
return sockErr
}
// isV4Socket reports whether the local bind address looks like an IPv4 socket.
// Used by EnablePathMTUDiscovery to pick IPPROTO_IP vs IPPROTO_IPV6 socket
// options. Assumes pure-v4 or pure-v6 sockets; a dual-stack v6 socket bound to
// :: will be treated as v6 (correct: setting IPV6_DONTFRAG covers v4-mapped
// traffic too on most stacks).
func (u *GenericConn) isV4Socket() bool {
la := u.UDPConn.LocalAddr()
if la == nil {
return false
}
ua, ok := la.(*net.UDPAddr)
if !ok {
return false
}
return ua.IP.To4() != nil
}
+30
View File
@@ -73,6 +73,21 @@ func NewListener(l *slog.Logger, ip netip.Addr, port int, multi bool, batch int)
return out, nil return out, nil
} }
// EnablePathMTUDiscovery sets IP_MTU_DISCOVER=IP_PMTUDISC_PROBE (IPV6 equivalent
// for v6 sockets). This sets the don't-fragment bit on every outbound packet but
// tells the kernel not to consume incoming ICMP frag-needed for its own PMTU
// cache; we drive PMTU discovery from the application via authenticated probes
// (RFC 8899). Called by the pmtud manager when PMTUD is enabled. Without this
// call the socket retains nebula's historical behavior (no DF, kernel may
// fragment), preserving compatibility with deployments that depend on UDP
// fragmentation.
func (u *StdConn) EnablePathMTUDiscovery() error {
if u.isV4 {
return u.setSockOptIPInt(unix.IPPROTO_IP, unix.IP_MTU_DISCOVER, unix.IP_PMTUDISC_PROBE)
}
return u.setSockOptIPInt(unix.IPPROTO_IPV6, unix.IPV6_MTU_DISCOVER, unix.IPV6_PMTUDISC_PROBE)
}
func (u *StdConn) SupportsMultipleReaders() bool { func (u *StdConn) SupportsMultipleReaders() bool {
return true return true
} }
@@ -110,6 +125,21 @@ func (u *StdConn) setSockOptInt(opt int, n int) error {
return opErr return opErr
} }
// setSockOptIPInt sets a socket option at a non-SOL_SOCKET level (e.g. IPPROTO_IP).
func (u *StdConn) setSockOptIPInt(level, opt, n int) error {
if u.rawConn == nil {
return fmt.Errorf("no UDP connection")
}
var opErr error
err := u.rawConn.Control(func(fd uintptr) {
opErr = unix.SetsockoptInt(int(fd), level, opt, n)
})
if err != nil {
return err
}
return opErr
}
func (u *StdConn) SetRecvBuffer(n int) error { func (u *StdConn) SetRecvBuffer(n int) error {
return u.setSockOptInt(unix.SO_RCVBUFFORCE, n) return u.setSockOptInt(unix.SO_RCVBUFFORCE, n)
} }
+15
View File
@@ -46,3 +46,18 @@ func NewListenConfig(multi bool) net.ListenConfig {
func (u *GenericConn) Rebind() error { func (u *GenericConn) Rebind() error {
return nil return nil
} }
// EnablePathMTUDiscovery sets the don't-fragment bit on outbound packets.
// NetBSD exposes IPV6_DONTFRAG via golang.org/x/sys/unix but the kernel does
// not provide a socket-level knob for setting DF on v4 UDP. The only IP-layer
// constant exposed is IP_DF, which is the wire header flag, not a sockopt.
// quic-go skips NetBSD for the same reason. So v4 sockets stay at nebula's
// historical behavior (kernel may fragment); v6 gets DF.
func (u *GenericConn) EnablePathMTUDiscovery() error {
if u.isV4Socket() {
return nil
}
return u.controlFD(func(fd uintptr) error {
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_DONTFRAG, 1)
})
}
+23
View File
@@ -0,0 +1,23 @@
//go:build openbsd && !e2e_testing
// +build openbsd,!e2e_testing
package udp
import (
"golang.org/x/sys/unix"
)
// EnablePathMTUDiscovery sets the don't-fragment bit on outbound packets.
// OpenBSD exposes IPV6_DONTFRAG via golang.org/x/sys/unix but the kernel does
// not provide a socket-level knob for setting DF on v4 UDP. The only IP-layer
// constant exposed is IP_DF, which is the wire header flag, not a sockopt.
// quic-go skips OpenBSD for the same reason. So v4 sockets stay at nebula's
// historical behavior (kernel may fragment); v6 gets DF.
func (u *GenericConn) EnablePathMTUDiscovery() error {
if u.isV4Socket() {
return nil
}
return u.controlFD(func(fd uintptr) error {
return unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_DONTFRAG, 1)
})
}
+6
View File
@@ -335,6 +335,12 @@ func (u *RIOConn) Rebind() error {
return nil return nil
} }
// EnablePathMTUDiscovery is a no-op on Windows for now. PMTUD is Linux-only in
// the initial PoC; Windows support would set IP_DONTFRAGMENT here.
func (u *RIOConn) EnablePathMTUDiscovery() error {
return nil
}
func (u *RIOConn) ReloadConfig(*config.C) {} func (u *RIOConn) ReloadConfig(*config.C) {}
func (u *RIOConn) Close() error { func (u *RIOConn) Close() error {
+17 -50
View File
@@ -21,48 +21,17 @@ type Packet struct {
Data []byte Data []byte
} }
// Copy returns a fresh *Packet (from the freelist) with a duplicate Data buffer.
func (u *Packet) Copy() *Packet { func (u *Packet) Copy() *Packet {
n := acquirePacket() n := &Packet{
n.To = u.To To: u.To,
n.From = u.From From: u.From,
if cap(n.Data) < len(u.Data) { Data: make([]byte, len(u.Data)),
n.Data = make([]byte, len(u.Data))
} else {
n.Data = n.Data[:len(u.Data)]
} }
copy(n.Data, u.Data) copy(n.Data, u.Data)
return n return n
} }
// Release returns p to the harness packet freelist.
// Callers that pull a *Packet from Get / TxPackets must Release when done.
// Channel-backed instead of sync.Pool because sync.Pool's per-P caches drain badly under cross-goroutine Get/Put,
// and putting a []byte in a Pool escapes the slice header to heap.
func (p *Packet) Release() {
if p == nil {
return
}
p.Data = p.Data[:0]
select {
case packetFreelist <- p:
default:
// Freelist full; drop the *Packet for the GC.
}
}
// packetFreelist retains *Packet structs (and their backing Data arrays) so steady-state allocation drops to zero.
var packetFreelist = make(chan *Packet, 64)
func acquirePacket() *Packet {
select {
case p := <-packetFreelist:
return p
default:
return &Packet{}
}
}
type TesterConn struct { type TesterConn struct {
Addr netip.AddrPort Addr netip.AddrPort
@@ -95,15 +64,13 @@ func NewListener(l *slog.Logger, ip netip.Addr, port int, _ bool, _ int) (Conn,
// this is an encrypted packet or a handshake message in most cases // this is an encrypted packet or a handshake message in most cases
// packets were transmitted from another nebula node, you can send them with Tun.Send // packets were transmitted from another nebula node, you can send them with Tun.Send
func (u *TesterConn) Send(packet *Packet) { func (u *TesterConn) Send(packet *Packet) {
if u.l.Enabled(context.Background(), slog.LevelDebug) { h := &header.H{}
// Parse the header only under debug logging, otherwise the
// allocation would show up in every Send call.
var h header.H
if err := h.Parse(packet.Data); err != nil { if err := h.Parse(packet.Data); err != nil {
panic(err) panic(err)
} }
if u.l.Enabled(context.Background(), slog.LevelDebug) {
u.l.Debug("UDP receiving injected packet", u.l.Debug("UDP receiving injected packet",
"header", &h, "header", h,
"udpAddr", packet.From, "udpAddr", packet.From,
"dataLen", len(packet.Data), "dataLen", len(packet.Data),
) )
@@ -140,18 +107,15 @@ func (u *TesterConn) Get(block bool) *Packet {
//********************************************************************************************************************// //********************************************************************************************************************//
func (u *TesterConn) WriteTo(b []byte, addr netip.AddrPort) error { func (u *TesterConn) WriteTo(b []byte, addr netip.AddrPort) error {
p := acquirePacket() p := &Packet{
if cap(p.Data) < len(b) { Data: make([]byte, len(b), len(b)),
p.Data = make([]byte, len(b)) From: u.Addr,
} else { To: addr,
p.Data = p.Data[:len(b)]
} }
copy(p.Data, b) copy(p.Data, b)
p.From = u.Addr
p.To = addr
select { select {
case <-u.done: case <-u.done:
p.Release()
return io.ErrClosedPipe return io.ErrClosedPipe
case u.TxPackets <- p: case u.TxPackets <- p:
return nil return nil
@@ -165,7 +129,6 @@ func (u *TesterConn) ListenOut(r EncReader) error {
return os.ErrClosed return os.ErrClosed
case p := <-u.RxPackets: case p := <-u.RxPackets:
r(p.From, p.Data) r(p.From, p.Data)
p.Release()
} }
} }
} }
@@ -189,6 +152,10 @@ func (u *TesterConn) Rebind() error {
return nil return nil
} }
func (u *TesterConn) EnablePathMTUDiscovery() error {
return nil
}
func (u *TesterConn) Close() error { func (u *TesterConn) Close() error {
u.closeOnce.Do(func() { u.closeOnce.Do(func() {
close(u.done) close(u.done)
+26
View File
@@ -9,6 +9,8 @@ import (
"net" "net"
"net/netip" "net/netip"
"syscall" "syscall"
"golang.org/x/sys/windows"
) )
func NewListener(l *slog.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) { func NewListener(l *slog.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) {
@@ -44,3 +46,27 @@ func NewListenConfig(multi bool) net.ListenConfig {
func (u *GenericConn) Rebind() error { func (u *GenericConn) Rebind() error {
return nil return nil
} }
// Windows IP_DONTFRAGMENT and IPV6_DONTFRAG are not exposed in the
// golang.org/x/sys/windows package. Defined locally per the values in
// ws2ipdef.h / ws2tcpip.h. These are stable Win32 constants that have not
// changed since at least Windows Vista.
const (
winIPDontFragment = 14
winIPv6DontFrag = 14
)
// EnablePathMTUDiscovery sets the don't-fragment bit on outbound packets.
// Windows uses IP_DONTFRAGMENT (v4) and IPV6_DONTFRAG (v6) at IPPROTO_IP /
// IPPROTO_IPV6 respectively. Note: this only enables DF on the GenericConn
// fallback path. The RIO path (RIOConn) has its own EnablePathMTUDiscovery
// in udp_rio_windows.go and is currently a no-op pending RIO-specific work.
func (u *GenericConn) EnablePathMTUDiscovery() error {
v4 := u.isV4Socket()
return u.controlFD(func(fd uintptr) error {
if v4 {
return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, winIPDontFragment, 1)
}
return windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, winIPv6DontFrag, 1)
})
}
-255
View File
@@ -1,255 +0,0 @@
package nebula
import (
"io"
"sync"
"github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/noiseutil"
"github.com/slackhq/nebula/overlay"
)
// WireBuffer is the per-goroutine working set for processing one IP packet
// through the data plane. It owns:
//
// - The IP-payload byte buffer used to hold the current inbound or
// outbound packet, with prefixLen bytes of slack at the front for
// the BSD AF_INET protocol-family marker.
// - The fwPacket scratch parsed by newPacket().
// - The 12-byte AEAD nonce scratch.
// - The header.H parse target used by the receive path.
// - An mtu-sized wire-output scratch for sendNoMetrics and for building
// reject packets.
//
// One WireBuffer is allocated per data-plane goroutine (listenIn for the
// TUN-side, listenOut for the UDP-side) and reused for every packet. No
// per-packet allocation. Future GRO/GSO/TSO and reliable-transport work
// will likely extend this to carry batch state and fragment metadata.
//
// The TUN protocol-family prefix is handled here, not in the overlay
// package. On BSDs the kernel writes the 4-byte marker into the slack on
// read, and we stamp it into the slack before write. On linux/windows
// /userspace devices prefixLen is 0 and the slack is empty.
type WireBuffer struct {
// FwPacket is the parsed IP packet metadata (5-tuple, fragment flags,
// etc.) populated by newPacket().
FwPacket *firewall.Packet
// NB is a 12-byte scratch the AEAD uses for the nonce; reused so we
// don't allocate one per encrypt/decrypt.
NB []byte
// H is the parse target for inbound nebula headers. Receive path only.
H *header.H
// Out is an mtu-sized wire-output scratch passed to sendNoMetrics and
// rejectInside / rejectOutside. Sized to fit any single wire packet.
Out []byte
// ip is the IP-payload region: a slice of len 0, cap linkMTU sliced
// from raw at offset prefixLen. The current packet (if any) is
// ip[:bodyN]. The TUN prefix slack lives at raw[0:prefixLen] just
// before ip.
ip []byte
// raw is the backing slab. Layout:
// [prefixLen bytes prefix slack | linkMTU bytes IP region | outSize bytes Out scratch]
// Holding it lets ReadIPFromTUN / WriteIPToTUN address the slack
// region directly.
raw []byte
prefixLen int
bodyN int
}
// NewWireBuffer returns a buffer sized to hold any single IP packet up to
// linkMTU, plus a disjoint wire-output scratch sliced from the same backing
// slab (the AEAD's Seal contract requires plaintext and dst not to partially
// overlap, and keeping them in one slab gives a single allocation per
// goroutine). Out is sized for the relay worst case
// (linkMTU + 2*header.Len + 2*AEADOverhead).
//
// prefixLen is the number of bytes the destination tun device prepends/
// expects on each IP packet (overlay.Device.TunPrefixLen). On BSDs this
// is 4 (AF_INET marker); on linux/windows/userspace devices it is 0.
func NewWireBuffer(linkMTU, prefixLen int) *WireBuffer {
outSize := linkMTU + 2*header.Len + 2*AEADOverhead
raw := make([]byte, prefixLen+linkMTU+outSize)
outStart := prefixLen + linkMTU
return &WireBuffer{
FwPacket: &firewall.Packet{},
NB: make([]byte, NonceSize),
H: &header.H{},
Out: raw[outStart : outStart : outStart+outSize],
ip: raw[prefixLen:prefixLen:outStart],
raw: raw,
prefixLen: prefixLen,
}
}
// Reset clears the body-length record so the buffer is ready for another
// recv (e.g. relay-receive recursion before a nested decrypt).
func (b *WireBuffer) Reset() { b.bodyN = 0 }
// IPPacket returns the IP packet currently held in the payload region (after
// a successful ReadIPFromTUN or DecryptDatagram). The slice aliases the
// buffer; do not retain past the next operation.
func (b *WireBuffer) IPPacket() []byte {
return b.ip[:b.bodyN]
}
// Seal stamps a nebula header at the front of buf.Out and AEAD-seals p as the
// payload, treating the header as additional authenticated data. The lock
// scope around counter increment + encrypt matches what goboring AESGCMTLS
// requires; non-boring builds skip the lock.
//
// Returns the wire bytes (header || ciphertext || tag), aliased to buf.Out.
// The slice is invalidated by the next Seal* call on this buffer.
func (b *WireBuffer) Seal(ci *ConnectionState, t header.MessageType, st header.MessageSubType, remoteIndex uint32, p []byte) ([]byte, error) {
return b.sealInto(b.Out[:cap(b.Out)], ci, t, st, remoteIndex, p)
}
// SealForRelay is like Seal but reserves header.Len bytes of slack at the front
// of buf.Out for an outer relay header. The inner header + ciphertext lands at
// offset header.Len so a follow-up SealRelayInPlace can stamp the outer header
// without copying. Use this when the caller may need to wrap the result in a
// relay envelope after the fact.
func (b *WireBuffer) SealForRelay(ci *ConnectionState, t header.MessageType, st header.MessageSubType, remoteIndex uint32, p []byte) ([]byte, error) {
return b.sealInto(b.Out[header.Len:cap(b.Out)], ci, t, st, remoteIndex, p)
}
func (b *WireBuffer) sealInto(out []byte, ci *ConnectionState, t header.MessageType, st header.MessageSubType, remoteIndex uint32, p []byte) ([]byte, error) {
if noiseutil.EncryptLockNeeded {
ci.writeLock.Lock()
}
c := ci.messageCounter.Add(1)
out = header.Encode(out, header.Version, t, st, remoteIndex, c)
out, err := ci.eKey.EncryptDanger(out, out, p, c, b.NB)
if noiseutil.EncryptLockNeeded {
ci.writeLock.Unlock()
}
return out, err
}
// SealRelayInPlace wraps an inner message that is already staged at
// buf.Out[header.Len:header.Len+innerLen] (either from a SealForRelay encrypt
// or from a copy via the SendVia entry point). It stamps the outer relay
// header into buf.Out[:header.Len] and AAD-only seals over the entire region,
// producing the wire bytes for the relay tunnel.
//
// Returns the wire bytes aliased to buf.Out; invalidated by the next Seal*
// call on this buffer.
func (b *WireBuffer) SealRelayInPlace(ci *ConnectionState, remoteIndex uint32, innerLen int) ([]byte, error) {
if noiseutil.EncryptLockNeeded {
ci.writeLock.Lock()
}
c := ci.messageCounter.Add(1)
out := b.Out[:cap(b.Out)]
out = header.Encode(out, header.Version, header.Message, header.MessageRelay, remoteIndex, c)
out = out[:header.Len+innerLen]
out, err := ci.eKey.EncryptDanger(out, out, nil, c, b.NB)
if noiseutil.EncryptLockNeeded {
ci.writeLock.Unlock()
}
return out, err
}
// StageRelayInner copies ad into the inner-payload slot at buf.Out[header.Len:]
// so SealRelayInPlace can wrap it on the next call. Used by SendVia when ad
// did not come from a prior SealForRelay (e.g. a handshake message being
// forwarded through a relay tunnel without our own encryption).
func (b *WireBuffer) StageRelayInner(ad []byte) int {
return copy(b.Out[header.Len:cap(b.Out)], ad)
}
// ReadIPFromTUN reads one IP packet from r into the payload region and
// updates bodyN. On BSDs the kernel writes its 4-byte protocol-family
// marker into the slack at raw[0:prefixLen] and the IP packet at
// raw[prefixLen:prefixLen+n]; we hand it the slack-prefixed slice so
// the kernel can do this in one syscall with no copy. On linux/windows/
// userspace devices prefixLen is 0 and the slack is empty.
func (b *WireBuffer) ReadIPFromTUN(r io.Reader) (int, error) {
n, err := r.Read(b.raw[:b.prefixLen+cap(b.ip)])
if err != nil {
b.bodyN = 0
return 0, err
}
if n < b.prefixLen {
b.bodyN = 0
return 0, nil
}
b.bodyN = n - b.prefixLen
return b.bodyN, nil
}
// WriteIPToTUN writes the IP packet currently in the payload region to w.
// On BSDs we stamp the protocol-family marker into the slack at
// raw[0:prefixLen] in place and write the entire slack+IP region in a
// single syscall, so the kernel sees [marker][ip] back to back without a
// userspace copy. On linux/windows/userspace devices the slack is empty
// and we just write the IP region.
func (b *WireBuffer) WriteIPToTUN(w io.Writer) (int, error) {
out := b.raw[:b.prefixLen+b.bodyN]
if b.prefixLen > 0 {
if err := overlay.StampTunPrefix(out); err != nil {
return 0, err
}
}
return w.Write(out)
}
// DecryptDatagram decrypts an inbound UDP packet into the payload region.
func (b *WireBuffer) DecryptDatagram(ci *ConnectionState, packet []byte, mc uint64) error {
dst, err := ci.dKey.DecryptDanger(b.ip[:0], packet[:header.Len], packet[header.Len:], mc, b.NB)
if err != nil {
b.bodyN = 0
return err
}
b.bodyN = len(dst)
return nil
}
// DecryptForHandler decrypts an inbound UDP packet (lighthouse, test,
// control, close-tunnel) into the payload region and returns the plaintext
// slice for the in-process handler. Returned slice aliases the buffer.
func (b *WireBuffer) DecryptForHandler(ci *ConnectionState, packet []byte, mc uint64) ([]byte, error) {
dst, err := ci.dKey.DecryptDanger(b.ip[:0], packet[:header.Len], packet[header.Len:], mc, b.NB)
if err != nil {
b.bodyN = 0
return nil, err
}
b.bodyN = len(dst)
return dst, nil
}
// WireBufferAllocator hands out reusable WireBuffers for cold callers that
// don't own a long-lived per-goroutine buffer (control plane, relay manager,
// connection manager teardown, etc.). Hot-path goroutines hold their own
// buffer for the life of the goroutine and don't need to acquire one.
type WireBufferAllocator interface {
Acquire() *WireBuffer
Release(*WireBuffer)
}
// wireBufferPool is a sync.Pool-backed WireBufferAllocator. The pool is
// keyed off a single linkMTU and prefixLen; cold callers send across the
// data-plane mtu and target the same Device, so we size the pool's
// buffers the same way.
type wireBufferPool struct {
pool sync.Pool
}
func NewWireBufferPool(linkMTU, prefixLen int) *wireBufferPool {
return &wireBufferPool{
pool: sync.Pool{
New: func() any {
return NewWireBuffer(linkMTU, prefixLen)
},
},
}
}
func (p *wireBufferPool) Acquire() *WireBuffer {
return p.pool.Get().(*WireBuffer)
}
func (p *wireBufferPool) Release(b *WireBuffer) {
b.Reset()
p.pool.Put(b)
}