| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- //go:build linux && !android && !e2e_testing
- package nebula
- import (
- "fmt"
- "net/netip"
- "sync"
- "time"
- "os"
- "os/exec"
- "testing"
- "github.com/slackhq/nebula/overlay/tio"
- "github.com/flynn/noise"
- "github.com/slackhq/nebula/header"
- "github.com/slackhq/nebula/udp"
- "github.com/slackhq/nebula/util"
- "github.com/stretchr/testify/require"
- )
- // queuedWire models two CPU-selected NIC queues with independent completion: the reader's
- // queue is congested, while another CPU's queue drains immediately. WriteBatch returning only
- // means the kernel accepted the packets; draining staged slots does not drain a NIC queue.
- // The sampled affinity makes the schedule deterministic without a real socket or NIC. An
- // unpinned sender alternates between allowed CPUs, even if it happened not to in this run.
- func (c *affinityConn) queuedWire(readerCPU int) [][]byte {
- var slow, fast [][]byte
- wire := c.captured()
- off := 0
- for i, n := range c.sizes {
- mask := c.masks[i]
- if mask.IsSet(readerCPU) && (mask.Count() == 1 || i%2 == 0) {
- slow = append(slow, wire[off:off+n]...)
- } else {
- fast = append(fast, wire[off:off+n]...)
- }
- off += n
- }
- return append(fast, slow...)
- }
- // TestFlowStagedTXRelayQueuedWire reproduces reorder after successful, ordered socket
- // submissions. Mixed reads switch the sticky gate with an empty software pipeline; relayed
- // data must stay on the reader throughout, without moving the direct sender. Each routine owns a flow.
- func TestFlowStagedTXRelayQueuedWire(t *testing.T) {
- if os.Getenv("NEBULA_FLOW_RING_TEST") != "1" {
- cmd := exec.Command(os.Args[0], "-test.run=^TestFlowStagedTXRelayQueuedWire$")
- cmd.Env = append(os.Environ(), "NEBULA_FLOW_RING_TEST=1")
- out, err := cmd.CombinedOutput()
- require.NoError(t, err, "%s", out)
- return
- }
- allowed, err := util.AllowedCPUs()
- require.NoError(t, err)
- if len(allowed) < 2 {
- t.Skip("requires two allowed CPUs")
- }
- for _, mode := range []string{"gate", "always-staged", "oversized"} {
- for _, explicit := range []bool{false, true} {
- for _, routines := range []int{1, 2} {
- for q := range routines {
- t.Run(fmt.Sprintf("%s/explicit=%t/routines=%d/q=%d", mode, explicit, routines, q), func(t *testing.T) {
- sc := gated(stagedGateHold)
- if mode != "gate" {
- sc.inlineBelow = 0
- }
- sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, sc)
- f := sender.f
- f.pinThreads, f.routines = true, routines
- if explicit {
- f.cpuAffinity = allowed[:2]
- }
- _, relayHI, relayPeer := addTestRelay(t, sender, ReplayWindow)
- conn := &affinityConn{}
- f.writers = make([]udp.Conn, routines)
- for i := range f.writers {
- f.writers[i] = conn
- }
- shape := append(repeatBool(false, 2), repeatBool(true, stagedGateHold+2)...)
- shape = append(shape, repeatBool(false, stagedGateHold+2)...)
- if mode != "gate" {
- shape = []bool{false, true, false, true}
- }
- reads, want := gateReads(shape...)
- if mode == "oversized" {
- // A deliberately tiny outer budget alone forces the oversized fallback.
- relayHI.ConnectionState.window = NewBits(16)
- }
- queue := &affinityQueue{flowBatchQueue: newFlowBatchQueue(reads), conn: conn}
- // Each Read waits for all previous socket submissions, making software drain
- // irrelevant to the independently queued packets in queuedWire.
- f.listenIn(queue, q)
- unwrap := func(wire [][]byte) [][]byte {
- inner := make([][]byte, 0, len(wire))
- for _, pkt := range wire {
- inner = append(inner, pkt[header.Len:len(pkt)-relayPeer.dKey.Overhead()])
- }
- return inner
- }
- require.Equal(t, want, decryptWire(t, receiver, unwrap(conn.captured())), "socket submission order")
- require.Equal(t, want, decryptWire(t, receiver, unwrap(conn.queuedWire(f.threadCPU(q)))), "NIC queue completion order")
- })
- }
- }
- }
- }
- }
- // Mixed reads must keep direct sends on the original staged sender, including when relay data is present.
- func TestFlowStagedTXMixedPlacement(t *testing.T) {
- if os.Getenv("NEBULA_FLOW_MIXED_TEST") != "1" {
- cmd := exec.Command(os.Args[0], "-test.run=^TestFlowStagedTXMixedPlacement$")
- cmd.Env = append(os.Environ(), "NEBULA_FLOW_MIXED_TEST=1")
- out, err := cmd.CombinedOutput()
- require.NoError(t, err, "%s", out)
- return
- }
- allowed, err := util.AllowedCPUs()
- require.NoError(t, err)
- if len(allowed) < 2 {
- t.Skip("requires two allowed CPUs")
- }
- for _, explicit := range []bool{false, true} {
- t.Run(fmt.Sprintf("explicit=%t", explicit), func(t *testing.T) {
- sender, receiver := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(2))
- _, _, relayPeer := addTestRelay(t, sender, ReplayWindow)
- direct, directReceiver := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(2))
- directAddr := netip.MustParseAddr("10.0.0.4")
- direct.hostinfo.vpnAddrs = []netip.Addr{directAddr}
- f := sender.f
- f.hostMap.Hosts[directAddr] = direct.hostinfo
- f.pinThreads, f.routines = true, 2
- sendCPU := -1
- if explicit {
- f.cpuAffinity = allowed[:2]
- sendCPU = allowed[1]
- }
- conn := &affinityConn{}
- f.writers = []udp.Conn{conn, conn}
- var reads [][]tio.Packet
- var wantRelay, wantDirect []decodedSeg
- for seq := range uint32(6) {
- r := flowTCPSuperpacket(0, seq, 3)
- d := flowTCPSuperpacket(1, seq, 9)
- copy(d.Bytes[16:20], directAddr.AsSlice())
- reads = append(reads, []tio.Packet{r, d})
- for i := range 3 {
- wantRelay = append(wantRelay, decodedSeg{seq: seq, idx: uint16(i)})
- }
- for i := range 9 {
- wantDirect = append(wantDirect, decodedSeg{flow: 1, seq: seq, idx: uint16(i)})
- }
- }
- queue := &affinityQueue{flowBatchQueue: newFlowBatchQueue(reads), conn: conn}
- f.listenIn(queue, 0)
- wire := conn.captured()
- var directWire, relayWire [][]byte
- off := 0
- for i, n := range conn.sizes {
- for _, pkt := range wire[off : off+n] {
- var h header.H
- require.NoError(t, h.Parse(pkt))
- cpu := sendCPU
- if h.Subtype == header.MessageRelay {
- cpu = f.threadCPU(0)
- require.NoError(t, relayPeer.VerifyRelay(nil, h.MessageCounter, pkt, make([]byte, 12)))
- relayWire = append(relayWire, pkt[header.Len:len(pkt)-relayPeer.dKey.Overhead()])
- } else {
- directWire = append(directWire, pkt)
- }
- requireFlowCPU(t, conn.masks[i:i+1], cpu, allowed)
- }
- off += n
- }
- require.Equal(t, wantDirect, decryptWire(t, directReceiver, directWire))
- require.Equal(t, wantRelay, decryptWire(t, receiver, relayWire))
- })
- }
- }
- // A relayed batch shares its outer tunnel with older staged direct data on the SAME routine. Inline completion
- // must not refund that direct sender's credit while its oldest reservation is stalled.
- func TestFlowStagedTXRelaySharedBudget(t *testing.T) {
- sender, _ := newFlowTestPair(t, noise.CipherChaChaPoly, stagedOn(2))
- _, via, _ := addTestRelay(t, sender, ReplayWindow)
- // Give the relay host the same ordinary peer metadata as a direct tunnel.
- direct := newFlowTestPeer(t, flowTestLocal, via.vpnAddrs[0], via.ConnectionState, 300, 400)
- via.buildNetworks(sender.f.myVpnNetworksTable, direct.hostinfo.ConnectionState.peerCert.Certificate)
- via.remoteIndexId = direct.hostinfo.remoteIndexId
- ci := via.ConnectionState
- ci.window = NewBits(32)
- start := ci.messageCounter.Load()
- entered, unblock := make(chan struct{}), make(chan struct{})
- var release sync.Once
- delayCrypto(direct, func(n uint64) {
- if n == start+1 {
- close(entered)
- <-unblock
- }
- })
- p := newStagedTx(sender.f, newFlowBatchQueue(nil), 0)
- defer p.close()
- defer release.Do(func() { close(unblock) })
- d := flowTCPSuperpacket(1, 0, 9)
- copy(d.Bytes[16:20], via.vpnAddrs[0].AsSlice())
- p.send([]tio.Packet{d})
- select {
- case <-entered:
- case <-time.After(5 * time.Second):
- t.Fatal("direct worker did not start")
- }
- for seq := range uint32(7) {
- p.send([]tio.Packet{flowTCPSuperpacket(0, seq, 1)})
- }
- require.Equal(t, uint64(16), (ci.txBudget.state.Load()>>24)&txBudgetMask)
- done := make(chan struct{})
- go func() { defer close(done); p.send([]tio.Packet{flowTCPSuperpacket(0, 7, 1)}) }()
- select {
- case <-done:
- t.Error("relay recycled credit around a stalled direct reservation")
- case <-time.After(50 * time.Millisecond):
- }
- require.Equal(t, start+16, ci.messageCounter.Load())
- release.Do(func() { close(unblock) })
- <-done
- p.drain()
- require.Zero(t, ci.txBudget.state.Load())
- }
|