Files
nebula/flow_tx.go
T
Ryan Huber 6146f3e4a6 tun: Prepare batch counter reservation for staged encryption
Build on the inside-packet filtering split with shared preparation and encryption helpers for inline and reserved sends. Reserve each batch’s per-tunnel counter range before encryption, including relay counters, without enabling staged processing yet. Test reserved encryption against a real receiver and check segmentation counts.
2026-10-16 21:21:23 +00:00

234 lines
7.5 KiB
Go

package nebula
import (
"errors"
"net/netip"
"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"
)
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)
}
// segmentCount reports how many segments tio.SegmentSuperpacket will cut pkt into, without touching its bytes.
func segmentCount(pkt tio.Packet) uint64 {
if !pkt.GSO.IsSuperpacket() {
return 1
}
n := (len(pkt.Bytes) - int(pkt.GSO.HdrLen) + int(pkt.GSO.Size) - 1) / int(pkt.GSO.Size)
return uint64(max(n, 1))
}
// 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
}
// 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 = segmentCount(pkt)
it.counter = r.need(it.hostinfo, it.ci, it.segs)
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)
it.relayCounter = r.need(it.relayHI, it.relayHI.ConnectionState, 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]
}