| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- package nebula
- import (
- "errors"
- "net/netip"
- "github.com/rcrowley/go-metrics"
- "github.com/slackhq/nebula/firewall"
- "github.com/slackhq/nebula/header"
- "github.com/slackhq/nebula/noiseutil"
- "github.com/slackhq/nebula/overlay/batch"
- "github.com/slackhq/nebula/overlay/tio"
- "github.com/slackhq/nebula/udp"
- )
- var errFlowSegmentOverrun = errors.New("superpacket has more segments than counters reserved for it")
- // txItem is one firewall-approved inside packet (or superpacket) with everything needed to encrypt it. A nonzero
- // counter is the first of the segs counters reserved for it on ci, relayCounter the same on the relay tunnel when
- // relayHI is set. Zero counters are allocated per segment while encrypting, as inline sends do.
- type txItem struct {
- pkt tio.Packet
- hostinfo *HostInfo
- ci *ConnectionState
- remote netip.AddrPort
- segs uint64
- counter uint64
- relayHI *HostInfo
- relay *Relay
- relayCounter uint64
- }
- // prepareTxItem does the per-packet work sendInsideMessage does before it encrypts, and resolves where the packet goes.
- func (f *Interface) prepareTxItem(it *txItem, hostinfo *HostInfo, pkt tio.Packet) bool {
- ci := hostinfo.ConnectionState
- if ci.eKey == nil {
- return false
- }
- f.recordOutbound(hostinfo)
- it.pkt, it.hostinfo, it.ci = pkt, hostinfo, ci
- it.remote = hostinfo.GetRemote()
- if it.remote.IsValid() {
- return true
- }
- it.relayHI, it.relay = f.findRelay(hostinfo)
- if it.relayHI == nil || it.relay == nil {
- return false
- }
- it.remote = it.relayHI.GetRemote()
- return true
- }
- // encryptTxItem segments and encrypts it into sb, for inline and staged sends alike.
- func (f *Interface) encryptTxItem(it *txItem, nb []byte, sb *batch.SendBatch) {
- reserved := it.counter != 0
- c, rc, end := it.counter, it.relayCounter, it.counter+it.segs
- err := tio.SegmentSuperpacket(it.pkt, func(seg []byte) error {
- if reserved && c >= end {
- // More segments than counters reserved for them, never encrypt past the reservation
- return errFlowSegmentOverrun
- }
- if it.relayHI == nil {
- // header + plaintext + AEAD tag (16 bytes for both AES-GCM and ChaCha20-Poly1305)
- if out := f.encryptTxSegment(it, c, seg, nb, sb.Reserve(header.Len+len(seg)+16)); out != nil {
- sb.Commit(out, it.remote)
- }
- } else {
- f.encryptTxSegmentVia(it, c, rc, seg, nb, sb)
- }
- if reserved {
- c++
- rc++
- }
- return nil
- })
- if err != nil {
- it.hostinfo.logger(f.l).Error("Failed to segment superpacket for send", "error", err)
- }
- }
- // encryptTxSegment encrypts one segment into out with counter c, or with the next counter of the tunnel when c is
- // zero. It returns nil when encryption failed, which is already logged.
- func (f *Interface) encryptTxSegment(it *txItem, c uint64, seg, nb, out []byte) []byte {
- if c != 0 {
- // Reserved counters are unique, so the encryption needs no lock
- return f.encryptMessage(header.Message, 0, it.hostinfo, it.ci, c, seg, nb, out, it.remote)
- }
- if noiseutil.EncryptLockNeeded {
- it.ci.writeLock.Lock()
- }
- c = it.ci.messageCounter.Add(1)
- out = f.encryptMessage(header.Message, 0, it.hostinfo, it.ci, c, seg, nb, out, it.remote)
- if noiseutil.EncryptLockNeeded {
- it.ci.writeLock.Unlock()
- }
- return out
- }
- // encryptTxSegmentVia encrypts one segment for the relayed tunnel, then wraps it for the relay with counter rc, both
- // allocated on demand when zero like encryptTxSegment does.
- func (f *Interface) encryptTxSegmentVia(it *txItem, c, rc uint64, seg, nb []byte, sb *batch.SendBatch) {
- //relay header + header + plaintext + AEAD tag + relay tag
- scratch := sb.Reserve(len(seg) + header.MaxOverhead)
- out := f.encryptTxSegment(it, c, seg, nb, scratch[header.Len:])
- if out == nil {
- return
- }
- toSend, err := f.prepareSendVia(it.relayHI, it.relay, out, nb, scratch, true, rc)
- if err != nil {
- return
- }
- remote := it.remote
- if rc == 0 {
- // Unreserved sends follow the relay's current remote, like sendNoMetrics
- remote = it.relayHI.GetRemote()
- }
- sb.Commit(toSend, remote)
- }
- // txTunnel accumulates one tunnel's counter needs across a batch so the batch reserves them with one atomic add.
- type txTunnel struct {
- ci *ConnectionState
- hostinfo *HostInfo
- n uint64
- base uint64
- exhausted bool
- }
- // txReserver turns one tun read into txItems with message counters reserved for them. Only the reader goroutine
- // collects and reserves counters.
- type txReserver struct {
- f *Interface
- tunnels []txTunnel
- owner uint64
- // The staged reader separates relay data before reserving direct work. A nil pointer collects both paths.
- inlineRelays *[]txItem
- }
- // collect runs the filter over pkts and appends the sendable ones to items, tallying each tunnel's counter needs.
- func (r *txReserver) collect(items []txItem, pkts []tio.Packet, fwPacket *firewall.ParsedPacket, rejectBuf []byte, q int, localCache firewall.ConntrackCache) []txItem {
- f := r.f
- r.tunnels = r.tunnels[:0]
- for _, pkt := range pkts {
- hostinfo := f.filterInsidePacket(pkt, fwPacket, rejectBuf, q, localCache)
- if hostinfo == nil {
- continue
- }
- var it txItem
- if !f.prepareTxItem(&it, hostinfo, pkt) {
- continue
- }
- it.segs = uint64(pkt.SegmentCount())
- if it.relayHI != nil {
- // The relay bookkeeping inline sends do per segment in prepareSendVia, once per item
- f.connectionManager.OutNoRebind(it.relayHI)
- f.connectionManager.RelayUsed(it.relay.LocalIndex)
- if r.inlineRelays != nil {
- *r.inlineRelays = append(*r.inlineRelays, it)
- continue
- }
- it.relayCounter = r.need(it.relayHI, it.relayHI.ConnectionState, it.segs)
- }
- it.counter = r.need(it.hostinfo, it.ci, it.segs)
- items = append(items, it)
- }
- return items
- }
- // need adds n counters to ci's tally for this batch and returns the offset of the first one within it.
- func (r *txReserver) need(hostinfo *HostInfo, ci *ConnectionState, n uint64) uint64 {
- t := r.tunnel(ci)
- if t == nil {
- r.tunnels = append(r.tunnels, txTunnel{ci: ci, hostinfo: hostinfo})
- t = &r.tunnels[len(r.tunnels)-1]
- }
- off := t.n
- t.n += n
- return off
- }
- // tunnel finds ci's tally, a batch rarely touches more than a couple of tunnels so a scan beats a map.
- func (r *txReserver) tunnel(ci *ConnectionState) *txTunnel {
- for i := range r.tunnels {
- if r.tunnels[i].ci == ci {
- return &r.tunnels[i]
- }
- }
- return nil
- }
- // reserve takes the counters collect tallied and stamps them onto items, dropping the items of any tunnel whose
- // counter is exhausted. It returns the items that are left.
- func (r *txReserver) reserve(items []txItem) []txItem {
- f := r.f
- // One atomic add per tunnel reserves the whole batch's counters before any worker sees it, so counter order on a
- // tunnel is dispatch order, which is the order the batches are flushed in. Reservation and the sender's order are
- // both taken on the reader goroutine, so there is no window for another batch to slip in between.
- for i := range r.tunnels {
- t := &r.tunnels[i]
- end := t.ci.messageCounter.Add(t.n)
- t.base = end - t.n + 1
- if end >= RejectAfterMessages {
- t.ci.messageCounter.Store(RejectAfterMessages)
- t.exhausted = true
- f.messageMetrics.TxExhausted(int64(t.n))
- if t.base <= RejectAfterMessages {
- t.hostinfo.logger(f.l).Error("Dropping outbound packets, tunnel message counter is exhausted")
- }
- }
- }
- n := 0
- for _, it := range items {
- t := r.tunnel(it.ci)
- if t.exhausted {
- continue
- }
- it.counter += t.base
- if it.relayHI != nil {
- rt := r.tunnel(it.relayHI.ConnectionState)
- if rt.exhausted {
- continue
- }
- it.relayCounter += rt.base
- }
- items[n] = it
- n++
- }
- clear(items[n:])
- return items[:n]
- }
- // handoffQueue is a tun queue that can give up the buffer its last Read filled. SwapRxBuf(nil) allocates a
- // correctly sized replacement; later swaps recycle the buffers handed back.
- type handoffQueue interface {
- tio.Queue
- SwapRxBuf([]byte) []byte
- }
- // txSlot is one staged batch. The reader fills items, a worker encrypts them into sb and signals done, and the
- // ordered sender flushes sb.
- type txSlot struct {
- f *Interface
- items []txItem
- // buf owns the queue receive buffer the items point into, swapped out at dispatch
- buf []byte
- sb *batch.SendBatch
- done chan struct{}
- // reservations hold shared tunnel credit until the flush, including for dropped counters
- reservations []txTunnel
- }
- func (s *txSlot) run(nb []byte) {
- for i := range s.items {
- s.f.encryptTxItem(&s.items[i], nb, s.sb)
- }
- s.done <- struct{}{}
- }
- // stagedTx is the staged transmit pipeline of one tun reader. Everything but the workers and the sender runs on the
- // reader goroutine.
- type stagedTx struct {
- txReserver
- // Relay data always leaves on the reader, including small and oversized reads. Its independent reservation
- // scratch keeps mixed reads from changing the direct pipeline's counters, gate or sender placement.
- relayReserver txReserver
- relaySlot txSlot
- q int
- cpu int // reader CPU, inline sends stay here
- senderCPU int // -1 leaves the sender within its inherited affinity
- queue handoffQueue
- pool *flowPool
- // sb and nb are the reader's own, for the sends it does itself
- sb *batch.SendBatch
- nb []byte
- // free holds the reader's idle slots. A dispatched slot goes to the workers and, in the same order, to the
- // sender on ordered; the sender gives it back on returned once it is on the wire.
- free []*txSlot
- ordered chan *txSlot
- returned chan *txSlot
- inFlight int
- senderDone chan struct{}
- // gateInline is the routine's transmit mode, gateStreak how many reads in a row wanted the other one
- gateInline bool
- gateStreak int
- metricGateSwitches metrics.Counter
- metricBatches metrics.Counter
- metricInline metrics.Counter
- }
- // newStagedTx needs a queue that hands over whole read batches, the offload (TSO/USO) queue on linux. On any other
- // queue a read is one packet, and staging it would cost a handoff per packet.
- func newStagedTx(f *Interface, queue handoffQueue, q int) *stagedTx {
- p := &stagedTx{
- txReserver: txReserver{f: f, tunnels: make([]txTunnel, 0, 4), owner: txBudgetOwner(q)},
- q: q,
- senderCPU: -1,
- queue: queue,
- sb: batch.NewSendBatch(f.writers[q], batch.SendBatchCap, batch.SendBatchCap*(udp.MTU+32)),
- nb: make([]byte, 12),
- ordered: make(chan *txSlot, f.staged.txSlots),
- returned: make(chan *txSlot, f.staged.txSlots),
- senderDone: make(chan struct{}),
- metricBatches: metrics.GetOrRegisterCounter("flow.tx.batches", nil),
- metricInline: metrics.GetOrRegisterCounter("flow.tx.inline", nil),
- // Starts inline, the pipeline is empty, unless the threshold never allows it
- gateInline: f.staged.inlineBelow > 0,
- metricGateSwitches: metrics.GetOrRegisterCounter("flow.tx.gate_switches", nil),
- }
- // Inline relay sends can share a tunnel with older direct work on this routine. They are not the ordered
- // sender's sole owner, so they must not recycle credit around a stalled direct reservation.
- p.relayReserver = txReserver{f: f, tunnels: make([]txTunnel, 0, 4)}
- p.inlineRelays = &p.relaySlot.items
- if f.pinThreads {
- p.cpu = f.threadCPU(q)
- // Only an explicit list pins the sender, and never onto its reader's CPU, where the two would serialise.
- // Automatic placement by queue index can put a busy sender on a slow core; unpinned, the scheduler moves it.
- if n := len(f.cpuAffinity); n > 0 && f.cpuAffinity[(q+1)%n] != p.cpu {
- p.senderCPU = f.cpuAffinity[(q+1)%n]
- }
- }
- p.pool = newFlowPool(f.staged.workers, f.staged.txSlots)
- // Ciphertext adds per-segment headers and tags; the arena can grow for small segments.
- buf := queue.SwapRxBuf(nil)
- bufSize := len(buf)
- for i := range f.staged.txSlots {
- if i > 0 {
- buf = make([]byte, bufSize)
- }
- p.free = append(p.free, &txSlot{
- f: f,
- buf: buf,
- sb: batch.NewSendBatch(f.writers[q], 4*batch.SendBatchCap, bufSize+bufSize/4),
- done: make(chan struct{}, 1),
- })
- }
- go p.sender()
- return p
- }
- // run is listenIn's loop with the staged pipeline: read, then hand each batch on, until the queue fails.
- func (p *stagedTx) run(fwPacket *firewall.ParsedPacket, rejectBuf []byte, conntrackCache *firewall.ConntrackCacheTicker) error {
- defer p.close()
- if p.f.pinThreads {
- p.f.pinThreadToCPU(p.q, p.cpu)
- }
- for {
- pkts, err := p.queue.Read()
- if err != nil {
- return err
- }
- p.ready()
- p.handle(pkts, fwPacket, rejectBuf, conntrackCache.Get())
- }
- }
- // handle takes one tun read through the pipeline. It returns once the batch is queued, or sent when the reader sent
- // it itself.
- func (p *stagedTx) handle(pkts []tio.Packet, fwPacket *firewall.ParsedPacket, rejectBuf []byte, localCache firewall.ConntrackCache) {
- p.ready()
- inline := p.gate(p.small(pkts))
- if inline && p.f.routines == 1 {
- // With only one reader and an empty pipeline, no other staged reservation can be overtaken. Keep this common
- // case inexpensive.
- p.inlineSoleReader(pkts, fwPacket, rejectBuf, localCache)
- return
- }
- slot := p.free[len(p.free)-1]
- p.free = p.free[:len(p.free)-1]
- items := p.collect(slot.items[:0], pkts, fwPacket, rejectBuf, p.q, localCache)
- if len(p.relaySlot.items) != 0 {
- p.sendRelays()
- }
- if len(items) == 0 {
- slot.items = items
- p.free = append(p.free, slot)
- return
- }
- if p.oversized() {
- p.metricInline.Inc(1)
- p.sendLarge(&p.txReserver, items)
- slot.items = items[:0]
- p.free = append(p.free, slot)
- return
- }
- if !p.waitBudget(&p.txReserver) {
- clear(items)
- slot.items = items[:0]
- p.free = append(p.free, slot)
- return
- }
- slot.reservations = append(slot.reservations[:0], p.tunnels...)
- items = p.reserve(items)
- slot.items = items
- if len(items) == 0 {
- slot.releaseBudget()
- p.free = append(p.free, slot)
- return
- }
- if inline {
- p.inline(slot)
- p.free = append(p.free, slot)
- return
- }
- // The packets now live in the slot, and the queue reads into the slot's old buffer
- slot.buf = p.queue.SwapRxBuf(slot.buf)
- p.inFlight++
- p.metricBatches.Inc(1)
- // Counters were reserved above on this goroutine and the slot takes its place in the sender's order right here,
- // before any other batch can, so the sender puts them on the wire in counter order.
- p.ordered <- slot
- p.pool.jobs <- slot
- }
- // sendRelays keeps relay data on one send path for the lifetime of this routine. The gate's software drain only
- // waits for socket submission; switching CPUs can still overtake packets on another NIC queue. Direct data retains
- // the existing gate and sender placement. Relay credit is still shared with all other users of both tunnels.
- func (p *stagedTx) sendRelays() {
- r, slot := &p.relayReserver, &p.relaySlot
- r.tunnels = r.tunnels[:0]
- for i := range slot.items {
- it := &slot.items[i]
- it.counter = r.need(it.hostinfo, it.ci, it.segs)
- it.relayCounter = r.need(it.relayHI, it.relayHI.ConnectionState, it.segs)
- }
- if r.oversized() {
- p.metricInline.Inc(1)
- p.sendLarge(r, slot.items)
- slot.items = slot.items[:0]
- return
- }
- if !p.waitBudget(r) {
- clear(slot.items)
- slot.items = slot.items[:0]
- return
- }
- slot.reservations = append(slot.reservations[:0], r.tunnels...)
- slot.items = r.reserve(slot.items)
- p.inline(slot)
- }
- // gate reports whether a read goes inline. Each routine holds one
- // transmit mode, inline on the reader or fanout to the workers and the sender, until gateHold reads in a row want the
- // other one, the last of which goes the new way. Choosing per read let small inline sends leave from the reader's CPU
- // between staged ones leaving from the sender's, and the receiver's TCP saw them reordered. Going inline the reader
- // first waits until everything in flight has been submitted to the socket and the sender is idle. This does not
- // drain independent NIC queues; relay data therefore always uses the reader. Going to fanout nothing is in flight:
- // inline sends finish before the reader moves on.
- func (p *stagedTx) gate(small bool) bool {
- if small == p.gateInline {
- p.gateStreak = 0
- return small
- }
- if p.gateStreak++; p.gateStreak < p.f.staged.gateHold {
- return p.gateInline
- }
- p.gateInline, p.gateStreak = small, 0
- p.metricGateSwitches.Inc(1)
- if !small {
- return false
- }
- for p.flushOldest() {
- }
- return true
- }
- // ready takes back every slot the sender has finished with, and waits for one when all of them are in flight.
- func (p *stagedTx) ready() {
- p.reap()
- if len(p.free) == 0 {
- p.release(<-p.returned)
- }
- }
- // reap takes back every slot the sender has finished with, without waiting.
- func (p *stagedTx) reap() {
- for {
- select {
- case s := <-p.returned:
- p.release(s)
- default:
- return
- }
- }
- }
- // flushOldest waits for the oldest batch in flight to reach the wire and takes its slot back. It reports false when
- // nothing is in flight.
- func (p *stagedTx) flushOldest() bool {
- if p.inFlight == 0 {
- return false
- }
- p.release(<-p.returned)
- return true
- }
- func (p *stagedTx) release(s *txSlot) {
- p.inFlight--
- p.free = append(p.free, s)
- }
- // sender is the ordered send stage: it puts each batch on the wire once its worker is done, in dispatch order.
- func (p *stagedTx) sender() {
- defer close(p.senderDone)
- if p.senderCPU >= 0 {
- p.f.pinThreadToCPU(p.q, p.senderCPU)
- }
- for s := range p.ordered {
- <-s.done
- if p.f.closed.Load() {
- // The sockets are gone, a flush would only log errors
- s.sb.Reset()
- } else {
- p.f.flushSendBatch(s.sb, p.q)
- }
- s.releaseBudget()
- clear(s.items)
- s.items = s.items[:0]
- p.returned <- s
- }
- }
- // close sends everything still in flight, in order, and stops the sender and the workers.
- func (p *stagedTx) close() {
- close(p.ordered)
- <-p.senderDone
- p.pool.close()
- }
- // small counts segments against the inline threshold.
- func (p *stagedTx) small(pkts []tio.Packet) bool {
- below := uint64(p.f.staged.inlineBelow)
- if below == 0 {
- return false
- }
- var n uint64
- for _, pkt := range pkts {
- if n += uint64(pkt.SegmentCount()); n >= below {
- return false
- }
- }
- return true
- }
- // inline encrypts a budgeted batch on the reader, with no worker handoff or buffer swap. The local sender is idle,
- // but other readers may hold reservations on these tunnels, so keep our credit until the flush completes.
- func (p *stagedTx) inline(slot *txSlot) {
- p.metricInline.Inc(1)
- for i := range slot.items {
- p.f.encryptTxItem(&slot.items[i], p.nb, p.sb)
- if p.sb.Len() >= batch.SendBatchCap {
- p.f.flushSendBatch(p.sb, p.q)
- }
- }
- p.f.flushSendBatch(p.sb, p.q)
- slot.releaseBudget()
- clear(slot.items)
- slot.items = slot.items[:0]
- }
- // inlineSoleReader needs no credit: the interface has no other tun reader and this reader has drained every
- // reservation before taking the inline path.
- func (p *stagedTx) inlineSoleReader(pkts []tio.Packet, fwPacket *firewall.ParsedPacket, rejectBuf []byte, localCache firewall.ConntrackCache) {
- p.metricInline.Inc(1)
- for _, pkt := range pkts {
- p.f.consumeInsidePacket(pkt, fwPacket, p.nb, p.sb, rejectBuf, p.q, localCache)
- if p.sb.Len() >= batch.SendBatchCap {
- p.f.flushSendBatch(p.sb, p.q)
- }
- }
- p.f.flushSendBatch(p.sb, p.q)
- }
|