mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 09:57:00 +02:00
plumb ECN through via relays
This commit is contained in:
@@ -144,10 +144,10 @@ type fakeConn struct {
|
|||||||
rebinds int
|
rebinds int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeConn) Rebind() error { c.rebinds++; return nil }
|
func (c *fakeConn) Rebind() error { c.rebinds++; return nil }
|
||||||
func (c *fakeConn) LocalAddr() (netip.AddrPort, error) { return netip.AddrPort{}, nil }
|
func (c *fakeConn) LocalAddr() (netip.AddrPort, error) { return netip.AddrPort{}, nil }
|
||||||
func (c *fakeConn) ListenOut(_ udp.EncReader, _ func()) error { return nil }
|
func (c *fakeConn) ListenOut(_ udp.EncReader, _ func()) error { return nil }
|
||||||
func (c *fakeConn) WriteTo(_ []byte, _ netip.AddrPort) error { return nil }
|
func (c *fakeConn) WriteTo(_ []byte, _ netip.AddrPort, _ byte) error { return nil }
|
||||||
func (c *fakeConn) WriteBatch(bufs [][]byte, _ []netip.AddrPort, _ []byte) (int, error) {
|
func (c *fakeConn) WriteBatch(bufs [][]byte, _ []netip.AddrPort, _ []byte) (int, error) {
|
||||||
return len(bufs), nil
|
return len(bufs), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
|
|||||||
var sentTo []netip.AddrPort
|
var sentTo []netip.AddrPort
|
||||||
hostinfo.remotes.ForEach(hm.mainHostMap.GetPreferredRanges(), func(addr netip.AddrPort, _ bool) {
|
hostinfo.remotes.ForEach(hm.mainHostMap.GetPreferredRanges(), func(addr netip.AddrPort, _ bool) {
|
||||||
hm.messageMetrics.Tx(header.Handshake, hh.machine.Subtype(), 1)
|
hm.messageMetrics.Tx(header.Handshake, hh.machine.Subtype(), 1)
|
||||||
err := hm.outside.WriteTo(stage0, addr)
|
err := hm.outside.WriteTo(stage0, addr, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// These repeat every attempt, so match the success log below and only shout when the remotes changed
|
// These repeat every attempt, so match the success log below and only shout when the remotes changed
|
||||||
level := slog.LevelDebug
|
level := slog.LevelDebug
|
||||||
@@ -1074,7 +1074,7 @@ func (hm *HandshakeManager) sendHandshakeResponse(via ViaSender, msg []byte, hos
|
|||||||
|
|
||||||
if !via.IsRelayed {
|
if !via.IsRelayed {
|
||||||
fields := append(logFields, "from", via)
|
fields := append(logFields, "from", via)
|
||||||
err := f.outside.WriteTo(msg, via.UdpAddr)
|
err := f.outside.WriteTo(msg, via.UdpAddr, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.l.Error("Failed to send handshake message", append(fields, "error", err)...)
|
f.l.Error("Failed to send handshake message", append(fields, "error", err)...)
|
||||||
} else {
|
} else {
|
||||||
@@ -1089,7 +1089,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.relay.LocalIndex, Established)
|
via.relayHI.relayState.UpdateRelayForByIdxState(via.relay.LocalIndex, Established)
|
||||||
f.SendVia(via.relayHI, via.relay, msg, make([]byte, 12), make([]byte, mtu), false)
|
f.SendVia(via.relayHI, via.relay, msg, make([]byte, 12), make([]byte, mtu), false, 0, 0)
|
||||||
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])...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func (mw *mockEncWriter) SendMessageToVpnAddr(_ header.MessageType, _ header.Mes
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mw *mockEncWriter) SendVia(_ *HostInfo, _ *Relay, _, _, _ []byte, _ bool) {
|
func (mw *mockEncWriter) SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool, outerECN byte, q int) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -520,21 +520,16 @@ func (f *Interface) prepareSendVia(via *HostInfo,
|
|||||||
// ad is the plaintext data to authenticate, but not encrypt
|
// ad is the plaintext data to authenticate, but not encrypt
|
||||||
// nb is a buffer used to store the nonce value, re-used for performance reasons.
|
// nb is a buffer used to store the nonce value, re-used for performance reasons.
|
||||||
// out is a buffer used to store the result of the Encrypt operation
|
// out is a buffer used to store the result of the Encrypt operation
|
||||||
|
// outerECN is the 2-bit codepoint to stamp on the carrier datagram (0 for control traffic).
|
||||||
// q indicates which writer to use to send the packet.
|
// q indicates which writer to use to send the packet.
|
||||||
func (f *Interface) SendVia(via *HostInfo,
|
func (f *Interface) SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool, outerECN byte, q int) {
|
||||||
relay *Relay,
|
|
||||||
ad,
|
|
||||||
nb,
|
|
||||||
out []byte,
|
|
||||||
nocopy bool,
|
|
||||||
) {
|
|
||||||
toSend, err := f.prepareSendVia(via, relay, ad, nb, out, nocopy)
|
toSend, err := f.prepareSendVia(via, relay, ad, nb, out, nocopy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// already logged by prepareSendVia
|
// already logged by prepareSendVia
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = f.writers[0].WriteTo(toSend, via.GetRemote())
|
err = f.writers[q].WriteTo(toSend, via.GetRemote(), outerECN)
|
||||||
if err != nil {
|
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)
|
||||||
}
|
}
|
||||||
@@ -595,8 +590,15 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data packets copy the inner packet's ECN codepoint onto the outer
|
||||||
|
// carrier per RFC 6040; control traffic stays Not-ECT.
|
||||||
|
var outerECN byte
|
||||||
|
if t == header.Message && f.ecnEnabled.Load() {
|
||||||
|
outerECN = innerECN(p)
|
||||||
|
}
|
||||||
|
|
||||||
if remote.IsValid() {
|
if remote.IsValid() {
|
||||||
err = f.writers[q].WriteTo(out, remote)
|
err = f.writers[q].WriteTo(out, remote, outerECN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hostinfo.logger(f.l).Error("Failed to write outgoing packet",
|
hostinfo.logger(f.l).Error("Failed to write outgoing packet",
|
||||||
"error", err,
|
"error", err,
|
||||||
@@ -604,7 +606,7 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if hr := hostinfo.GetRemote(); hr.IsValid() {
|
} else if hr := hostinfo.GetRemote(); hr.IsValid() {
|
||||||
err = f.writers[q].WriteTo(out, hr)
|
err = f.writers[q].WriteTo(out, hr, outerECN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hostinfo.logger(f.l).Error("Failed to write outgoing packet",
|
hostinfo.logger(f.l).Error("Failed to write outgoing packet",
|
||||||
"error", err,
|
"error", err,
|
||||||
@@ -623,7 +625,7 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
f.SendVia(relayHostInfo, relay, out, nb, fullOut[:header.Len+len(out)], true)
|
f.SendVia(relayHostInfo, relay, out, nb, fullOut[:header.Len+len(out)], true, outerECN, q)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-7
@@ -143,13 +143,7 @@ type Interface struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EncWriter interface {
|
type EncWriter interface {
|
||||||
SendVia(via *HostInfo,
|
SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool, outerECN byte, q int)
|
||||||
relay *Relay,
|
|
||||||
ad,
|
|
||||||
nb,
|
|
||||||
out []byte,
|
|
||||||
nocopy bool,
|
|
||||||
)
|
|
||||||
SendMessageToVpnAddr(t header.MessageType, st header.MessageSubType, vpnAddr netip.Addr, p, nb, out []byte)
|
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)
|
SendMessageToHostInfo(t header.MessageType, st header.MessageSubType, hostinfo *HostInfo, p, nb, out []byte)
|
||||||
Handshake(vpnAddr netip.Addr)
|
Handshake(vpnAddr netip.Addr)
|
||||||
|
|||||||
+1
-1
@@ -498,7 +498,7 @@ type testEncWriter struct {
|
|||||||
protocolVersion cert.Version
|
protocolVersion cert.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tw *testEncWriter) SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool) {
|
func (tw *testEncWriter) SendVia(via *HostInfo, relay *Relay, ad, nb, out []byte, nocopy bool, outerECN byte, q int) {
|
||||||
}
|
}
|
||||||
func (tw *testEncWriter) Handshake(vpnIp netip.Addr) {
|
func (tw *testEncWriter) Handshake(vpnIp netip.Addr) {
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -233,8 +233,12 @@ func (f *Interface) handleOutsideRelayPacket(hostinfo *HostInfo, via ViaSender,
|
|||||||
// Forward this packet through the relay tunnel, rebuilding it in place.
|
// Forward this packet through the relay tunnel, rebuilding it in place.
|
||||||
// Encode overwrites the old outer header, and the new AEAD tag lands where the old one was
|
// Encode overwrites the old outer header, and the new AEAD tag lands where the old one was
|
||||||
fwdBuf := packet[:0:len(packet)] // Cap to len(packet) to protect memory from a larger parent buffer
|
fwdBuf := packet[:0:len(packet)] // Cap to len(packet) to protect memory from a larger parent buffer
|
||||||
|
var fwdECN byte
|
||||||
|
if f.ecnEnabled.Load() {
|
||||||
|
fwdECN = meta.OuterECN
|
||||||
|
}
|
||||||
//todo it would potentially be nice to batch these
|
//todo it would potentially be nice to batch these
|
||||||
f.SendVia(targetHI, targetRelay, signedPayload, nb, fwdBuf, true)
|
f.SendVia(targetHI, targetRelay, signedPayload, nb, fwdBuf, true, fwdECN, q)
|
||||||
case TerminalType:
|
case TerminalType:
|
||||||
hostinfo.logger(f.l).Error("Unexpected Relay Type of Terminal")
|
hostinfo.logger(f.l).Error("Unexpected Relay Type of Terminal")
|
||||||
return
|
return
|
||||||
@@ -609,7 +613,7 @@ func (f *Interface) sendRecvError(endpoint netip.AddrPort, index uint32) {
|
|||||||
f.messageMetrics.Tx(header.RecvError, 0, 1)
|
f.messageMetrics.Tx(header.RecvError, 0, 1)
|
||||||
|
|
||||||
b := header.Encode(make([]byte, header.Len), header.Version, header.RecvError, 0, index, 0)
|
b := header.Encode(make([]byte, header.Len), header.Version, header.RecvError, 0, index, 0)
|
||||||
_ = f.outside.WriteTo(b, endpoint)
|
_ = f.outside.WriteTo(b, endpoint, 0)
|
||||||
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
if f.l.Enabled(context.Background(), slog.LevelDebug) {
|
||||||
f.l.Debug("Recv error sent",
|
f.l.Debug("Recv error sent",
|
||||||
"index", index,
|
"index", index,
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ func (p *Punchy) SendPunch(hostinfo *HostInfo) {
|
|||||||
p.sendPunchToAllRemotes(hostinfo)
|
p.sendPunchToAllRemotes(hostinfo)
|
||||||
} else if hr := hostinfo.GetRemote(); hr.IsValid() {
|
} else if hr := hostinfo.GetRemote(); hr.IsValid() {
|
||||||
p.metricPunchyTx.Inc(1)
|
p.metricPunchyTx.Inc(1)
|
||||||
p.punchConn.WriteTo([]byte{1}, hr)
|
p.punchConn.WriteTo([]byte{1}, hr, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ func (p *Punchy) SendPunchToAll(hostinfo *HostInfo) {
|
|||||||
func (p *Punchy) sendPunchToAllRemotes(hostinfo *HostInfo) {
|
func (p *Punchy) sendPunchToAllRemotes(hostinfo *HostInfo) {
|
||||||
hostinfo.remotes.ForEach(p.hm.GetPreferredRanges(), func(addr netip.AddrPort, preferred bool) {
|
hostinfo.remotes.ForEach(p.hm.GetPreferredRanges(), func(addr netip.AddrPort, preferred bool) {
|
||||||
p.metricPunchyTx.Inc(1)
|
p.metricPunchyTx.Inc(1)
|
||||||
p.punchConn.WriteTo([]byte{1}, addr)
|
p.punchConn.WriteTo([]byte{1}, addr, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ func (p *Punchy) Start(ctx context.Context, ifce EncWriter, hm *HostMap, lh ligh
|
|||||||
p.l.Debug("Punching", "target", job.target, "vpnAddr", job.vpnAddr)
|
p.l.Debug("Punching", "target", job.target, "vpnAddr", job.vpnAddr)
|
||||||
}
|
}
|
||||||
p.metricHolepunchTx.Inc(1)
|
p.metricHolepunchTx.Inc(1)
|
||||||
p.punchConn.WriteTo(empty, job.target)
|
p.punchConn.WriteTo(empty, job.target, 0)
|
||||||
case job.vpnAddr.IsValid():
|
case job.vpnAddr.IsValid():
|
||||||
// A nebula test packet to the host trying to contact us.
|
// A nebula test packet to the host trying to contact us.
|
||||||
// In the case of a double nat or other difficult scenario, this may help establish a tunnel.
|
// In the case of a double nat or other difficult scenario, this may help establish a tunnel.
|
||||||
|
|||||||
+1
-1
@@ -161,7 +161,7 @@ func (rm *relayManager) StartRelays(f *Interface, vpnIp netip.Addr, hh *Handshak
|
|||||||
switch existingRelay.State {
|
switch existingRelay.State {
|
||||||
case Established:
|
case Established:
|
||||||
hl.Log(context.Background(), level, "Send handshake via relay", "relay", relay.String())
|
hl.Log(context.Background(), level, "Send handshake via relay", "relay", relay.String())
|
||||||
f.SendVia(relayHostInfo, existingRelay, stage0, make([]byte, 12), make([]byte, mtu), false)
|
f.SendVia(relayHostInfo, existingRelay, stage0, make([]byte, 12), make([]byte, mtu), false, 0, 0)
|
||||||
case Disestablished:
|
case Disestablished:
|
||||||
// Mark this relay as 'requested'
|
// Mark this relay as 'requested'
|
||||||
relayHostInfo.relayState.UpdateRelayForByIpState(vpnIp, Requested)
|
relayHostInfo.relayState.UpdateRelayForByIpState(vpnIp, Requested)
|
||||||
|
|||||||
+10
-7
@@ -35,13 +35,16 @@ type EncReader func(
|
|||||||
type Conn interface {
|
type Conn interface {
|
||||||
Rebind() error
|
Rebind() error
|
||||||
LocalAddr() (netip.AddrPort, error)
|
LocalAddr() (netip.AddrPort, error)
|
||||||
// ListenOut invokes r for each received packet. On batch-capable
|
// ListenOut invokes r for each received packet.
|
||||||
// backends (recvmmsg), flush is called after each batch is fully
|
// On batch-capable backends (recvmmsg), flush is called after each batch is fully delivered.
|
||||||
// delivered — callers use it to flush per-batch accumulators such as
|
// Callers use it to flush per-batch accumulators such as TUN write coalescers.
|
||||||
// TUN write coalescers. Single-packet backends call flush after each
|
// Single-packet backends call flush after each packet. flush must not be nil.
|
||||||
// packet. flush must not be nil.
|
|
||||||
ListenOut(r EncReader, flush func()) error
|
ListenOut(r EncReader, flush func()) error
|
||||||
WriteTo(b []byte, addr netip.AddrPort) error
|
// WriteTo sends a single packet to addr.
|
||||||
|
// outerECN is the 2-bit IP-level ECN codepoint to stamp on the packet's outer IP header.
|
||||||
|
// 0 (Not-ECT) is the pass-through value.
|
||||||
|
// Linux attaches it as an IP_TOS / IPV6_TCLASS cmsg. Backends without per-packet ECN support ignore it.
|
||||||
|
WriteTo(b []byte, addr netip.AddrPort, outerECN byte) error
|
||||||
// WriteBatch sends a contiguous batch of packets, each with its own
|
// WriteBatch sends a contiguous batch of packets, each with its own
|
||||||
// destination. bufs and addrs must have the same length. outerECNs may
|
// destination. bufs and addrs must have the same length. outerECNs may
|
||||||
// be nil (treated as all-zero / Not-ECT); when non-nil it must have the
|
// be nil (treated as all-zero / Not-ECT); when non-nil it must have the
|
||||||
@@ -73,7 +76,7 @@ func (NoopConn) ListenOut(_ EncReader, _ func()) error {
|
|||||||
func (NoopConn) SupportsMultipleReaders() bool {
|
func (NoopConn) SupportsMultipleReaders() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
func (NoopConn) WriteTo(_ []byte, _ netip.AddrPort) error {
|
func (NoopConn) WriteTo(_ []byte, _ netip.AddrPort, _ byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (NoopConn) WriteBatch(bufs [][]byte, _ []netip.AddrPort, _ []byte) (int, error) {
|
func (NoopConn) WriteBatch(bufs [][]byte, _ []netip.AddrPort, _ []byte) (int, error) {
|
||||||
|
|||||||
+3
-2
@@ -89,7 +89,8 @@ func NewListenConfig(multi bool) net.ListenConfig {
|
|||||||
//go:noescape
|
//go:noescape
|
||||||
func sendto(s int, buf []byte, flags int, to unsafe.Pointer, addrlen int32) (err error)
|
func sendto(s int, buf []byte, flags int, to unsafe.Pointer, addrlen int32) (err error)
|
||||||
|
|
||||||
func (u *StdConn) WriteTo(b []byte, ap netip.AddrPort) error {
|
// WriteTo ignores outerECN; per-packet ECN marking is not implemented on darwin.
|
||||||
|
func (u *StdConn) WriteTo(b []byte, ap netip.AddrPort, _ byte) error {
|
||||||
var sa unsafe.Pointer
|
var sa unsafe.Pointer
|
||||||
var addrLen int32
|
var addrLen int32
|
||||||
|
|
||||||
@@ -147,7 +148,7 @@ func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, _ []byte) (i
|
|||||||
// writability on EAGAIN before giving up on the remainder.
|
// writability on EAGAIN before giving up on the remainder.
|
||||||
written := 0
|
written := 0
|
||||||
for i, b := range bufs {
|
for i, b := range bufs {
|
||||||
if err := u.WriteTo(b, addrs[i]); err == nil {
|
if err := u.WriteTo(b, addrs[i], 0); err == nil {
|
||||||
written++
|
written++
|
||||||
} else {
|
} else {
|
||||||
u.l.Debug("failed to write packet in batch", "udpAddr", addrs[i], "error", err)
|
u.l.Debug("failed to write packet in batch", "udpAddr", addrs[i], "error", err)
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
package udp
|
package udp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestPlanRunBreaksOnECNChange confirms that two same-destination, same-size
|
// TestPlanRunBreaksOnECNChange confirms that two same-destination, same-size
|
||||||
@@ -59,3 +62,130 @@ func TestPlanRunBreaksOnECNChange(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ecnReceiver is a raw UDP socket with IP_RECVTOS / IPV6_RECVTCLASS enabled,
|
||||||
|
// used to observe the outer ECN codepoint WriteTo stamps on the wire.
|
||||||
|
type ecnReceiver struct {
|
||||||
|
fd int
|
||||||
|
addr netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEcnReceiver(t *testing.T, v6 bool) *ecnReceiver {
|
||||||
|
t.Helper()
|
||||||
|
family := unix.AF_INET
|
||||||
|
if v6 {
|
||||||
|
family = unix.AF_INET6
|
||||||
|
}
|
||||||
|
fd, err := unix.Socket(family, unix.SOCK_DGRAM, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("socket: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { unix.Close(fd) })
|
||||||
|
|
||||||
|
var bindAddr netip.Addr
|
||||||
|
if v6 {
|
||||||
|
if err = unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_RECVTCLASS, 1); err != nil {
|
||||||
|
t.Fatalf("IPV6_RECVTCLASS: %v", err)
|
||||||
|
}
|
||||||
|
if err = unix.Bind(fd, &unix.SockaddrInet6{Addr: [16]byte{15: 1}}); err != nil {
|
||||||
|
t.Fatalf("bind ::1: %v", err)
|
||||||
|
}
|
||||||
|
bindAddr = netip.MustParseAddr("::1")
|
||||||
|
} else {
|
||||||
|
if err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVTOS, 1); err != nil {
|
||||||
|
t.Fatalf("IP_RECVTOS: %v", err)
|
||||||
|
}
|
||||||
|
if err = unix.Bind(fd, &unix.SockaddrInet4{Addr: [4]byte{127, 0, 0, 1}}); err != nil {
|
||||||
|
t.Fatalf("bind 127.0.0.1: %v", err)
|
||||||
|
}
|
||||||
|
bindAddr = netip.MustParseAddr("127.0.0.1")
|
||||||
|
}
|
||||||
|
tv := unix.Timeval{Sec: 5}
|
||||||
|
if err = unix.SetsockoptTimeval(fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv); err != nil {
|
||||||
|
t.Fatalf("SO_RCVTIMEO: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sa, err := unix.Getsockname(fd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getsockname: %v", err)
|
||||||
|
}
|
||||||
|
var port int
|
||||||
|
switch v := sa.(type) {
|
||||||
|
case *unix.SockaddrInet4:
|
||||||
|
port = v.Port
|
||||||
|
case *unix.SockaddrInet6:
|
||||||
|
port = v.Port
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected sockaddr %T", sa)
|
||||||
|
}
|
||||||
|
return &ecnReceiver{fd: fd, addr: netip.AddrPortFrom(bindAddr, uint16(port))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recvECN receives one datagram and returns the 2-bit ECN codepoint from its
|
||||||
|
// TOS / TCLASS cmsg.
|
||||||
|
func (r *ecnReceiver) recvECN(t *testing.T) byte {
|
||||||
|
t.Helper()
|
||||||
|
buf := make([]byte, 128)
|
||||||
|
oob := make([]byte, 128)
|
||||||
|
_, oobn, _, _, err := unix.Recvmsg(r.fd, buf, oob, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("recvmsg: %v", err)
|
||||||
|
}
|
||||||
|
cmsgs, err := unix.ParseSocketControlMessage(oob[:oobn])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse cmsg: %v", err)
|
||||||
|
}
|
||||||
|
for _, m := range cmsgs {
|
||||||
|
switch {
|
||||||
|
case m.Header.Level == unix.IPPROTO_IP && m.Header.Type == unix.IP_TOS:
|
||||||
|
return m.Data[0] & 0x03
|
||||||
|
case m.Header.Level == unix.IPPROTO_IPV6 && m.Header.Type == unix.IPV6_TCLASS:
|
||||||
|
return byte(binary.NativeEndian.Uint32(m.Data)) & 0x03
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatal("no TOS/TCLASS cmsg received")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWriteToStampsOuterECN sends single packets through StdConn.WriteTo and
|
||||||
|
// asserts the requested ECN codepoint lands on the outer IP header, for a
|
||||||
|
// v4 socket, a v6 socket, and the dual-stack case where a v4-mapped
|
||||||
|
// destination must be stamped via IP_TOS rather than IPV6_TCLASS.
|
||||||
|
func TestWriteToStampsOuterECN(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
bind string
|
||||||
|
recvV6 bool
|
||||||
|
sendECN byte
|
||||||
|
}{
|
||||||
|
{"v4_socket_to_v4", "127.0.0.1", false, 0x03},
|
||||||
|
{"v6_socket_to_v6", "::1", true, 0x01},
|
||||||
|
{"dualstack_v6_socket_to_v4_mapped", "::", false, 0x02},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c, err := NewListener(testLogger(), netip.MustParseAddr(tc.bind), 0, false, 8)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewListener: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
rx := newEcnReceiver(t, tc.recvV6)
|
||||||
|
|
||||||
|
if err = c.WriteTo([]byte("ecn"), rx.addr, tc.sendECN); err != nil {
|
||||||
|
t.Fatalf("WriteTo(ecn=%#02x): %v", tc.sendECN, err)
|
||||||
|
}
|
||||||
|
if got := rx.recvECN(t); got != tc.sendECN {
|
||||||
|
t.Errorf("outer ECN = %#02x, want %#02x", got, tc.sendECN)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The zero codepoint sends no TOS cmsg and must arrive Not-ECT
|
||||||
|
// (the socket-default TOS byte).
|
||||||
|
if err = c.WriteTo([]byte("ecn"), rx.addr, 0); err != nil {
|
||||||
|
t.Fatalf("WriteTo(ecn=0): %v", err)
|
||||||
|
}
|
||||||
|
if got := rx.recvECN(t); got != 0 {
|
||||||
|
t.Errorf("outer ECN = %#02x, want 0 (Not-ECT)", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -39,7 +39,8 @@ func NewGenericListener(l *slog.Logger, ip netip.Addr, port int, multi bool, bat
|
|||||||
return nil, fmt.Errorf("Unexpected PacketConn: %T %#v", pc, pc)
|
return nil, fmt.Errorf("Unexpected PacketConn: %T %#v", pc, pc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *GenericConn) WriteTo(b []byte, addr netip.AddrPort) error {
|
// WriteTo ignores outerECN; the stdlib UDPConn offers no per-packet TOS control.
|
||||||
|
func (u *GenericConn) WriteTo(b []byte, addr netip.AddrPort, _ byte) error {
|
||||||
_, err := u.UDPConn.WriteToUDPAddrPort(b, addr)
|
_, err := u.UDPConn.WriteToUDPAddrPort(b, addr)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-16
@@ -411,11 +411,11 @@ func parseRecvCmsg(hdr *msghdr, wantGRO, wantECN bool) (gso int, ecn byte) {
|
|||||||
return gso, ecn
|
return gso, ecn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) WriteTo(b []byte, ip netip.AddrPort) error {
|
func (u *StdConn) WriteTo(b []byte, ip netip.AddrPort, ecn byte) error {
|
||||||
return sendto(u.sysFd, b, ip, u.isV4)
|
return sendmsg(u.sysFd, b, ip, u.isV4, ecn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendto(fd int, b []byte, addr netip.AddrPort, isV4 bool) error {
|
func sendmsg(fd int, b []byte, addr netip.AddrPort, isV4 bool, ecn byte) error {
|
||||||
var rsa [unix.SizeofSockaddrInet6]byte
|
var rsa [unix.SizeofSockaddrInet6]byte
|
||||||
nlen, err := writeSockaddr(rsa[:], addr, isV4)
|
nlen, err := writeSockaddr(rsa[:], addr, isV4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -425,31 +425,48 @@ func sendto(fd int, b []byte, addr netip.AddrPort, isV4 bool) error {
|
|||||||
if len(b) > 0 {
|
if len(b) > 0 {
|
||||||
base = &b[0]
|
base = &b[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var iov iovec
|
||||||
|
iov.Base = base
|
||||||
|
setIovLen(&iov, len(b))
|
||||||
|
|
||||||
|
var hdr msghdr
|
||||||
|
hdr.Name = &rsa[0]
|
||||||
|
hdr.Namelen = uint32(nlen)
|
||||||
|
hdr.Iov = &iov
|
||||||
|
setMsgIovlen(&hdr, 1)
|
||||||
|
|
||||||
|
// Stack scratch for the ECN cmsg, typed as uint64s so its base is cmsg-aligned on every arch.
|
||||||
|
// CmsgSpace(4) needs 24 bytes on 64-bit linux, 16 on 32-bit.
|
||||||
|
var ctrl [3]uint64
|
||||||
|
if ecn != 0 {
|
||||||
|
buf := (*[24]byte)(unsafe.Pointer(&ctrl[0]))[:]
|
||||||
|
writeECNCmsg(buf, addr.Addr().Unmap().Is4(), ecn)
|
||||||
|
hdr.Control = &buf[0]
|
||||||
|
setMsgControllen(&hdr, unix.CmsgSpace(4))
|
||||||
|
}
|
||||||
|
|
||||||
_, _, errno := unix.Syscall6(
|
_, _, errno := unix.Syscall6(
|
||||||
unix.SYS_SENDTO,
|
unix.SYS_SENDMSG,
|
||||||
uintptr(fd),
|
uintptr(fd),
|
||||||
uintptr(unsafe.Pointer(base)),
|
uintptr(unsafe.Pointer(&hdr)),
|
||||||
uintptr(len(b)),
|
0, 0, 0, 0,
|
||||||
0,
|
|
||||||
uintptr(unsafe.Pointer(&rsa[0])),
|
|
||||||
uintptr(nlen),
|
|
||||||
)
|
)
|
||||||
if errno != 0 {
|
if errno != 0 {
|
||||||
return &net.OpError{Op: "sendto", Err: errno}
|
return &net.OpError{Op: "sendmsg", Err: errno}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteBatch sends bufs via sendmmsg(2), coalescing same-destination runs
|
// WriteBatch sends bufs via sendmmsg(2), coalescing same-destination runs into UDP-GSO superpackets when supported.
|
||||||
// into UDP-GSO superpackets when supported. See batchWriter in
|
// See batchWriter in udp_linux_writebatch.go for the mechanics.
|
||||||
// udp_linux_writebatch.go for the mechanics.
|
|
||||||
func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, ecns []byte) (int, error) {
|
func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, ecns []byte) (int, error) {
|
||||||
return u.bw.WriteBatch(bufs, addrs, ecns)
|
return u.bw.WriteBatch(bufs, addrs, ecns)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeSockaddr encodes addr into buf (which must be at least
|
// writeSockaddr encodes addr into buf (which must be at least SizeofSockaddrInet6 bytes).
|
||||||
// SizeofSockaddrInet6 bytes). Returns the number of bytes used. If isV4 is
|
// Returns the number of bytes used.
|
||||||
// true and addr is not a v4 (or v4-in-v6) address, returns an error.
|
// If isV4 is true and addr is not a v4 (or v4-in-v6) address, returns an error.
|
||||||
func writeSockaddr(buf []byte, addr netip.AddrPort, isV4 bool) (int, error) {
|
func writeSockaddr(buf []byte, addr netip.AddrPort, isV4 bool) (int, error) {
|
||||||
ap := addr.Addr().Unmap()
|
ap := addr.Addr().Unmap()
|
||||||
if isV4 {
|
if isV4 {
|
||||||
|
|||||||
+22
-16
@@ -385,14 +385,30 @@ func (w *batchWriter) planRun(bufs [][]byte, addrs []netip.AddrPort, ecns []byte
|
|||||||
return runLen, segSize
|
return runLen, segSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeECNCmsg fills the start of buf with one IP_TOS / IPV6_TCLASS cmsg
|
||||||
|
// carrying the 2-bit ECN codepoint. buf must be cmsg-aligned (the batch
|
||||||
|
// writer's heap slab is runtime-aligned; sendmsg passes uint64-backed stack
|
||||||
|
// scratch) and at least CmsgSpace(4) bytes. The cmsg family must match the
|
||||||
|
// socket: on the default dual-stack v6 bind, a v4-mapped destination takes
|
||||||
|
// the kernel's IPv4 path, which reads IP_TOS and ignores IPV6_TCLASS. The
|
||||||
|
// payload is a 4-byte int for both families, so the cmsg space is the same.
|
||||||
|
func writeECNCmsg(buf []byte, dstIsV4 bool, ecn byte) {
|
||||||
|
h := (*unix.Cmsghdr)(unsafe.Pointer(&buf[0]))
|
||||||
|
if dstIsV4 {
|
||||||
|
h.Level = int32(unix.IPPROTO_IP)
|
||||||
|
h.Type = int32(unix.IP_TOS)
|
||||||
|
} else {
|
||||||
|
h.Level = int32(unix.IPPROTO_IPV6)
|
||||||
|
h.Type = int32(unix.IPV6_TCLASS)
|
||||||
|
}
|
||||||
|
setCmsgLen(h, unix.CmsgLen(4))
|
||||||
|
dataOff := unix.CmsgLen(0)
|
||||||
|
binary.NativeEndian.PutUint32(buf[dataOff:dataOff+4], uint32(ecn))
|
||||||
|
}
|
||||||
|
|
||||||
// writeEntryCmsg writes one entry's cmsgs: the UDP_SEGMENT payload when
|
// writeEntryCmsg writes one entry's cmsgs: the UDP_SEGMENT payload when
|
||||||
// runLen >= 2, the IP_TOS/IPV6_TCLASS cmsg when ecn != 0, then points
|
// runLen >= 2, the IP_TOS/IPV6_TCLASS cmsg when ecn != 0, then points
|
||||||
// Hdr.Control at the smallest span covering the cmsgs in use.
|
// Hdr.Control at the smallest span covering the cmsgs in use.
|
||||||
//
|
|
||||||
// The ECN cmsg family must match the destination, not the socket: on the
|
|
||||||
// default dual-stack v6 bind, a v4-mapped destination takes the kernel's
|
|
||||||
// IPv4 path, which reads IP_TOS and ignores IPV6_TCLASS. The payload is a
|
|
||||||
// 4-byte int for both families, so the cmsg space is the same.
|
|
||||||
func (w *batchWriter) writeEntryCmsg(entry, runLen, segSize int, ecn byte, dstIsV4 bool) {
|
func (w *batchWriter) writeEntryCmsg(entry, runLen, segSize int, ecn byte, dstIsV4 bool) {
|
||||||
hdr := &w.msgs[entry].Hdr
|
hdr := &w.msgs[entry].Hdr
|
||||||
useSeg := runLen >= 2
|
useSeg := runLen >= 2
|
||||||
@@ -404,17 +420,7 @@ func (w *batchWriter) writeEntryCmsg(entry, runLen, segSize int, ecn byte, dstIs
|
|||||||
binary.NativeEndian.PutUint16(w.cmsg[dataOff:dataOff+2], uint16(segSize))
|
binary.NativeEndian.PutUint16(w.cmsg[dataOff:dataOff+2], uint16(segSize))
|
||||||
}
|
}
|
||||||
if useEcn {
|
if useEcn {
|
||||||
ecnHdr := (*unix.Cmsghdr)(unsafe.Pointer(&w.cmsg[base+w.cmsgSegSpace]))
|
writeECNCmsg(w.cmsg[base+w.cmsgSegSpace:], dstIsV4, ecn)
|
||||||
if dstIsV4 {
|
|
||||||
ecnHdr.Level = int32(unix.IPPROTO_IP)
|
|
||||||
ecnHdr.Type = int32(unix.IP_TOS)
|
|
||||||
} else {
|
|
||||||
ecnHdr.Level = int32(unix.IPPROTO_IPV6)
|
|
||||||
ecnHdr.Type = int32(unix.IPV6_TCLASS)
|
|
||||||
}
|
|
||||||
setCmsgLen(ecnHdr, unix.CmsgLen(4))
|
|
||||||
dataOff := base + w.cmsgSegSpace + unix.CmsgLen(0)
|
|
||||||
binary.NativeEndian.PutUint32(w.cmsg[dataOff:dataOff+4], uint32(ecn))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -99,3 +99,46 @@ func TestWriteBatchNoAllocs(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWriteToNoAllocs verifies the single-packet WriteTo path performs no
|
||||||
|
// heap allocations on the happy path, both without ancillary data and with
|
||||||
|
// an ECN cmsg (which is built in stack scratch, not a per-call slab).
|
||||||
|
func TestWriteToNoAllocs(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
addr string
|
||||||
|
}{
|
||||||
|
{"v4", "127.0.0.1"},
|
||||||
|
{"v6", "::1"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ip := netip.MustParseAddr(tc.addr)
|
||||||
|
newConn := func() Conn {
|
||||||
|
c, err := NewListener(testLogger(), ip, 0, false, 8)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewListener: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = c.Close() })
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
tx := newConn()
|
||||||
|
rx := newConn()
|
||||||
|
dst, err := rx.LocalAddr()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LocalAddr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := make([]byte, 512)
|
||||||
|
for _, ecn := range []byte{0, 0x03} {
|
||||||
|
allocs := testing.AllocsPerRun(100, func() {
|
||||||
|
if werr := tx.WriteTo(payload, dst, ecn); werr != nil {
|
||||||
|
t.Fatalf("WriteTo(ecn=%#02x): %v", ecn, werr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if allocs != 0 {
|
||||||
|
t.Errorf("ecn=%#02x: %v allocs per WriteTo, want 0", ecn, allocs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -254,7 +254,8 @@ retry:
|
|||||||
return n, ep, nil
|
return n, ep, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RIOConn) WriteTo(buf []byte, ip netip.AddrPort) error {
|
// WriteTo ignores outerECN; per-packet ECN marking is not implemented on windows.
|
||||||
|
func (u *RIOConn) WriteTo(buf []byte, ip netip.AddrPort, _ byte) error {
|
||||||
if !u.isOpen.Load() {
|
if !u.isOpen.Load() {
|
||||||
return net.ErrClosed
|
return net.ErrClosed
|
||||||
}
|
}
|
||||||
@@ -321,7 +322,7 @@ func (u *RIOConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, _ []byte) (i
|
|||||||
// An un-sendable destination costs its own packet, never the ones behind it in the batch.
|
// An un-sendable destination costs its own packet, never the ones behind it in the batch.
|
||||||
written := 0
|
written := 0
|
||||||
for i, b := range bufs {
|
for i, b := range bufs {
|
||||||
if err := u.WriteTo(b, addrs[i]); err == nil {
|
if err := u.WriteTo(b, addrs[i], 0); err == nil {
|
||||||
written++
|
written++
|
||||||
} else {
|
} else {
|
||||||
u.l.Debug("failed to write packet in batch", "udpAddr", addrs[i], "error", err)
|
u.l.Debug("failed to write packet in batch", "udpAddr", addrs[i], "error", err)
|
||||||
|
|||||||
+3
-2
@@ -153,7 +153,8 @@ func (u *TesterConn) Get(block bool) *Packet {
|
|||||||
// Below this is boilerplate implementation to make nebula actually work
|
// Below this is boilerplate implementation to make nebula actually work
|
||||||
//********************************************************************************************************************//
|
//********************************************************************************************************************//
|
||||||
|
|
||||||
func (u *TesterConn) WriteTo(b []byte, addr netip.AddrPort) error {
|
// WriteTo ignores outerECN; the in-memory tester carries no IP headers.
|
||||||
|
func (u *TesterConn) WriteTo(b []byte, addr netip.AddrPort, _ byte) error {
|
||||||
p := acquirePacket()
|
p := acquirePacket()
|
||||||
if cap(p.Data) < len(b) {
|
if cap(p.Data) < len(b) {
|
||||||
p.Data = make([]byte, len(b))
|
p.Data = make([]byte, len(b))
|
||||||
@@ -174,7 +175,7 @@ func (u *TesterConn) WriteTo(b []byte, addr netip.AddrPort) error {
|
|||||||
func (u *TesterConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, _ []byte) (int, error) {
|
func (u *TesterConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, _ []byte) (int, error) {
|
||||||
written := 0
|
written := 0
|
||||||
for i, b := range bufs {
|
for i, b := range bufs {
|
||||||
if err := u.WriteTo(b, addrs[i]); err == nil {
|
if err := u.WriteTo(b, addrs[i], 0); err == nil {
|
||||||
written++
|
written++
|
||||||
} else {
|
} else {
|
||||||
u.l.Debug("failed to write packet in batch", "udpAddr", addrs[i], "error", err)
|
u.l.Debug("failed to write packet in batch", "udpAddr", addrs[i], "error", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user