Compare commits

..

28 Commits

Author SHA1 Message Date
JackDoan dfe94c6269 window 2026-04-30 12:16:52 -05:00
JackDoan 1c601d776a set PSH 2026-04-30 12:16:35 -05:00
JackDoan 17d8ebff93 window 2026-04-30 12:16:22 -05:00
JackDoan 612d3ef931 dead code 2026-04-29 12:57:04 -05:00
JackDoan 8282a629e5 robot fixes 2026-04-29 12:50:02 -05:00
JackDoan c62f27d4b4 slightly nicer contract? 2026-04-28 17:29:25 -05:00
JackDoan f5db77f214 checkpt 2026-04-28 17:29:25 -05:00
JackDoan b9a7d1edf3 less stateful 2026-04-28 17:29:25 -05:00
JackDoan d1ea33659a correctness 2026-04-28 17:29:25 -05:00
JackDoan 8fdd98f639 attempt to improve readability 2026-04-28 17:29:25 -05:00
JackDoan 45bc0fc055 be safer 2026-04-28 17:29:25 -05:00
JackDoan 24af30bd78 fix? 2026-04-28 17:29:25 -05:00
JackDoan 1d84b81032 fix interfaces 2026-04-28 17:29:25 -05:00
JackDoan b155f4b7e1 fix 2026-04-28 17:29:25 -05:00
JackDoan 194d58cd46 GRO
cruft

fix tests

haha yep faster

checksum speed

haha

save pennies

fix

typo!

checkpt

GSO again
2026-04-28 17:29:24 -05:00
JackDoan a476b1fa07 Remove WriteFromSelf 2026-04-28 10:56:57 -05:00
JackDoan 8b02b8128e better and batched tun interface 2026-04-28 10:39:57 -05:00
Nate Brown 1ab1f71dba Make stats a server we can reconfigure and start/stop (#1670)
gofmt / Run gofmt (push) Failing after 2s
smoke-extra / Run extra smoke tests (push) Failing after 2s
smoke / Run multi node smoke test (push) Failing after 3s
Build and test / Build all and test on ubuntu-linux (push) Failing after 2s
Build and test / Build and test on linux with boringcrypto (push) Failing after 3s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
2026-04-27 12:25:24 -05:00
Nate Brown d0f02ba873 Switch to slog, remove logrus (#1672) 2026-04-27 09:41:47 -05:00
Jack Doan 5f890dbc34 noise: only type-assert once (#1691)
gofmt / Run gofmt (push) Failing after 2s
smoke-extra / Run extra smoke tests (push) Failing after 2s
smoke / Run multi node smoke test (push) Failing after 3s
Build and test / Build all and test on ubuntu-linux (push) Failing after 2s
Build and test / Build and test on linux with boringcrypto (push) Failing after 3s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
2026-04-24 13:12:42 -05:00
brad-defined db85d61c23 SSH handshake in goroutine and defer close (#1640)
gofmt / Run gofmt (push) Failing after 2s
smoke-extra / Run extra smoke tests (push) Failing after 3s
smoke / Run multi node smoke test (push) Failing after 2s
Build and test / Build all and test on ubuntu-linux (push) Failing after 3s
Build and test / Build and test on linux with boringcrypto (push) Failing after 2s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
* SSH handshake in goroutine and defer close
2026-04-23 14:53:52 -04:00
Nate Brown db9218b0be Another shot at the flakey smoke test (#1688) 2026-04-23 13:51:15 -05:00
Nate Brown 5f00ab4b74 Fix e2e tests writing after the tester tun is closed causing a panic (#1681)
gofmt / Run gofmt (push) Failing after 3s
smoke-extra / Run extra smoke tests (push) Failing after 3s
smoke / Run multi node smoke test (push) Failing after 3s
Build and test / Build all and test on ubuntu-linux (push) Failing after 2s
Build and test / Build and test on linux with boringcrypto (push) Failing after 3s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
2026-04-22 17:18:06 -05:00
Guy Nesher 2a1cc62001 fix: guard QueryCert against panic on short/empty QNAME (#1635)
* fix: guard QueryCert against panic on short/empty QNAME

QueryCert slices data[:len(data)-1] to strip a trailing dot, which
panics when data is empty (slice bounds [:-1]). Add a length check
to return early for inputs shorter than a minimal valid "x." form.

While miekg/dns currently rejects wire-format packets that would
produce an empty QNAME, the Nebula code should not rely on library
behavior for crash safety.

Made-with: Cursor

* fix merge conflicts

---------

Co-authored-by: JackDoan <me@jackdoan.com>
2026-04-22 12:42:14 -05:00
John Maguire e753e6e93c Immediate Lighthouse update after reconfig/reconnect (#1645)
gofmt / Run gofmt (push) Failing after 3s
smoke-extra / Run extra smoke tests (push) Failing after 3s
smoke / Run multi node smoke test (push) Failing after 2s
Build and test / Build all and test on ubuntu-linux (push) Failing after 3s
Build and test / Build and test on linux with boringcrypto (push) Failing after 3s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
2026-04-21 16:33:32 -04:00
John Maguire 32a7c04498 Return NODATA instead of NXDOMAIN for missing record types (#1668)
The DNS responder was setting RCODE=NXDOMAIN (Name Error) any time the
answer section was empty, including for names that exist in the
lighthouse but lack a record of the requested type (e.g. an AAAA query
for a v4-only host). Per RFC 2308 §2.1, NXDOMAIN means "the domain
referred to by the QNAME does not exist", and per RFC 2308 §2.2 a name
that exists with no record of the requested type must be answered with
RCODE=NOERROR and an empty answer section (NODATA).

The practical fallout: busybox ping in Alpine issues AAAA first, treats
NXDOMAIN as a hard failure, and never falls through to A. Returning
NODATA lets the resolver continue to the A query as it should.

Track whether any queried A/AAAA name is known in either map and only
set RcodeNameError when no queried name exists at all.
2026-04-21 16:32:48 -04:00
Nate Brown 8c50fc3f60 Plug the conntrack cache ticker leak and nebula-service log.Fatal calls (#1669) 2026-04-21 13:19:54 -05:00
Nate Brown 2f4532f102 No more dns globals, proper cleanup on shutdown (#1667) 2026-04-21 12:41:10 -05:00
104 changed files with 5765 additions and 3333 deletions
+6 -6
View File
@@ -82,7 +82,7 @@ docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host
docker exec host2 ncat -nklv 0.0.0.0 2000 & docker exec host2 ncat -nklv 0.0.0.0 2000 &
docker exec host3 ncat -nklv 0.0.0.0 2000 & docker exec host3 ncat -nklv 0.0.0.0 2000 &
docker exec host4 ncat -nkluv 0.0.0.0 4000 & docker exec host4 ncat -e '/usr/bin/echo helloagainfromhost4' -nkluv 0.0.0.0 4000 &
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 & docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 &
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 & docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 &
@@ -155,11 +155,11 @@ echo " *** Testing conntrack"
echo echo
set -x set -x
# host2 speaking to host4 on UDP 4000 should allow it to reply, when firewall rules would normally not permit this # host4's outbound firewall only allows ICMP to the lighthouse, so host4
docker exec host2 sh -c "/usr/bin/echo host2 | ncat -nuv 192.168.100.4 4000" # cannot initiate UDP to host2. Once host2 initiates a flow to host4:4000,
docker exec host2 ncat -e '/usr/bin/echo helloagainfromhost2' -nkluv 0.0.0.0 4000 & # conntrack must let host4's listener reply on that flow. If it doesn't,
sleep 1 # the echo back from host4 never reaches host2.
docker exec host4 sh -c "/usr/bin/echo host4 | ncat -nuv 192.168.100.2 4000" docker exec host2 sh -c "(/usr/bin/echo host2; sleep 2) | ncat -nuv 192.168.100.4 4000" | grep -q helloagainfromhost4
docker exec host4 sh -c 'kill 1' docker exec host4 sh -c 'kill 1'
docker exec host3 sh -c 'kill 1' docker exec host3 sh -c 'kill 1'
+14
View File
@@ -2,7 +2,21 @@ version: "2"
linters: linters:
default: none default: none
enable: enable:
- sloglint
- testifylint - testifylint
settings:
sloglint:
# Enforce key-value pair form for Info/Debug/Warn/Error/Log/With and
# the package-level slog equivalents. Use l.Log(ctx, level, ...) for
# custom levels instead of LogAttrs when you can.
#
# LogAttrs is also flagged by this rule because it takes ...slog.Attr;
# the few legitimate sites (where attrs is built up as a []slog.Attr)
# carry a //nolint:sloglint with rationale.
kv-only: true
# no-mixed-args is on by default: forbids mixing kv and attrs in one call.
# discard-handler is on by default (since Go 1.24): suggests
# slog.DiscardHandler over slog.NewTextHandler(io.Discard, nil).
exclusions: exclusions:
generated: lax generated: lax
presets: presets:
-70
View File
@@ -1,70 +0,0 @@
package nebula
import "net/netip"
// sendBatchCap is the maximum number of encrypted packets accumulated before a
// flush is forced. TSO superpackets segment to at most ~45 packets on
// reasonable MTUs, so 128 leaves headroom without bloating the backing
// allocation.
const sendBatchCap = 128
// sendBatch accumulates encrypted UDP packets for a single sendmmsg flush.
// One sendBatch is owned by each listenIn goroutine; no locking is needed.
// The backing storage holds up to batchCap packets of slotCap bytes each;
// bufs and dsts are parallel slices of committed slots.
type sendBatch struct {
bufs [][]byte
dsts []netip.AddrPort
backing []byte
slotCap int
batchCap int
nextSlot int
}
func newSendBatch(batchCap, slotCap int) *sendBatch {
return &sendBatch{
bufs: make([][]byte, 0, batchCap),
dsts: make([]netip.AddrPort, 0, batchCap),
backing: make([]byte, batchCap*slotCap),
slotCap: slotCap,
batchCap: batchCap,
}
}
// Next returns a zero-length slice with slotCap capacity over the next unused
// slot's backing bytes. The caller writes into the returned slice and then
// calls Commit with the final length and destination. Next returns nil when
// the batch is full.
func (b *sendBatch) Next() []byte {
if b.nextSlot >= b.batchCap {
return nil
}
start := b.nextSlot * b.slotCap
return b.backing[start : start : start+b.slotCap]
}
// Commit records the slot just returned by Next as a packet of length n
// destined for dst.
func (b *sendBatch) Commit(n int, dst netip.AddrPort) {
start := b.nextSlot * b.slotCap
b.bufs = append(b.bufs, b.backing[start:start+n])
b.dsts = append(b.dsts, dst)
b.nextSlot++
}
// Reset clears committed slots; backing storage is retained for reuse.
func (b *sendBatch) Reset() {
b.bufs = b.bufs[:0]
b.dsts = b.dsts[:0]
b.nextSlot = 0
}
// Len returns the number of committed packets.
func (b *sendBatch) Len() int {
return len(b.bufs)
}
// Cap returns the maximum number of slots in the batch.
func (b *sendBatch) Cap() int {
return b.batchCap
}
-137
View File
@@ -1,137 +0,0 @@
package nebula
import (
"net/netip"
"testing"
)
func TestSendBatchBookkeeping(t *testing.T) {
b := newSendBatch(4, 32)
if b.Len() != 0 || b.Cap() != 4 {
t.Fatalf("fresh batch: len=%d cap=%d", b.Len(), b.Cap())
}
ap := netip.MustParseAddrPort("10.0.0.1:4242")
for i := 0; i < 4; i++ {
slot := b.Next()
if slot == nil {
t.Fatalf("slot %d: Next returned nil before cap", i)
}
if cap(slot) != 32 || len(slot) != 0 {
t.Fatalf("slot %d: got len=%d cap=%d want len=0 cap=32", i, len(slot), cap(slot))
}
// Write a marker byte.
slot = append(slot, byte(i), byte(i+1), byte(i+2))
b.Commit(len(slot), ap)
}
if b.Next() != nil {
t.Fatalf("Next should return nil when full")
}
if b.Len() != 4 {
t.Fatalf("Len=%d want 4", b.Len())
}
for i, buf := range b.bufs {
if len(buf) != 3 || buf[0] != byte(i) {
t.Errorf("buf %d: %x", i, buf)
}
if b.dsts[i] != ap {
t.Errorf("dst %d: got %v want %v", i, b.dsts[i], ap)
}
}
// Reset returns empty and Next works again.
b.Reset()
if b.Len() != 0 {
t.Fatalf("after Reset Len=%d want 0", b.Len())
}
slot := b.Next()
if slot == nil || cap(slot) != 32 {
t.Fatalf("after Reset Next nil or wrong cap: %v cap=%d", slot == nil, cap(slot))
}
}
func TestBatchSegmentable(t *testing.T) {
ap := netip.MustParseAddrPort("10.0.0.1:4242")
other := netip.MustParseAddrPort("10.0.0.2:4242")
mk := func(addrs []netip.AddrPort, sizes []int) *sendBatch {
b := newSendBatch(len(addrs), 64)
for i, a := range addrs {
s := b.Next()
for j := 0; j < sizes[i]; j++ {
s = append(s, byte(j))
}
b.Commit(len(s), a)
}
return b
}
t.Run("uniform same dst", func(t *testing.T) {
b := mk([]netip.AddrPort{ap, ap, ap}, []int{10, 10, 10})
seg, ok := batchSegmentable(b)
if !ok || seg != 10 {
t.Fatalf("got seg=%d ok=%v", seg, ok)
}
})
t.Run("last segment short ok", func(t *testing.T) {
b := mk([]netip.AddrPort{ap, ap, ap}, []int{10, 10, 4})
seg, ok := batchSegmentable(b)
if !ok || seg != 10 {
t.Fatalf("got seg=%d ok=%v", seg, ok)
}
})
t.Run("mixed dst rejected", func(t *testing.T) {
b := mk([]netip.AddrPort{ap, other, ap}, []int{10, 10, 10})
if _, ok := batchSegmentable(b); ok {
t.Fatalf("expected rejection for mixed dst")
}
})
t.Run("mid-batch short rejected", func(t *testing.T) {
b := mk([]netip.AddrPort{ap, ap, ap}, []int{10, 4, 10})
if _, ok := batchSegmentable(b); ok {
t.Fatalf("expected rejection for short mid-batch")
}
})
t.Run("mid-batch longer rejected", func(t *testing.T) {
b := mk([]netip.AddrPort{ap, ap, ap}, []int{10, 11, 10})
if _, ok := batchSegmentable(b); ok {
t.Fatalf("expected rejection for longer mid-batch")
}
})
t.Run("last longer rejected", func(t *testing.T) {
b := mk([]netip.AddrPort{ap, ap, ap}, []int{10, 10, 11})
if _, ok := batchSegmentable(b); ok {
t.Fatalf("expected rejection for longer last segment")
}
})
t.Run("first zero rejected", func(t *testing.T) {
b := mk([]netip.AddrPort{ap, ap}, []int{0, 10})
if _, ok := batchSegmentable(b); ok {
t.Fatalf("expected rejection for zero first")
}
})
}
func TestSendBatchSlotsDoNotOverlap(t *testing.T) {
b := newSendBatch(3, 8)
ap := netip.MustParseAddrPort("10.0.0.1:80")
// Fill three slots, each with its own sentinel byte.
for i := 0; i < 3; i++ {
s := b.Next()
s = append(s, byte(0xA0+i), byte(0xB0+i))
b.Commit(len(s), ap)
}
for i, buf := range b.bufs {
if buf[0] != byte(0xA0+i) || buf[1] != byte(0xB0+i) {
t.Errorf("slot %d corrupted: %x", i, buf)
}
}
}
+24 -14
View File
@@ -1,8 +1,10 @@
package nebula package nebula
import ( import (
"context"
"log/slog"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
) )
type Bits struct { type Bits struct {
@@ -30,7 +32,7 @@ func NewBits(bits uint64) *Bits {
return b return b
} }
func (b *Bits) Check(l *logrus.Logger, i uint64) bool { func (b *Bits) Check(l *slog.Logger, i uint64) bool {
// If i is the next number, return true. // If i is the next number, return true.
if i > b.current { if i > b.current {
return true return true
@@ -42,13 +44,16 @@ func (b *Bits) Check(l *logrus.Logger, i uint64) bool {
} }
// Not within the window // Not within the window
if l.Level >= logrus.DebugLevel { if l.Enabled(context.Background(), slog.LevelDebug) {
l.Debugf("rejected a packet (top) %d %d\n", b.current, i) l.Debug("rejected a packet (top)",
"current", b.current,
"incoming", i,
)
} }
return false return false
} }
func (b *Bits) Update(l *logrus.Logger, i uint64) bool { func (b *Bits) Update(l *slog.Logger, i uint64) bool {
// If i is the next number, return true and update current. // If i is the next number, return true and update current.
if i == b.current+1 { if i == b.current+1 {
// Check if the oldest bit was lost since we are shifting the window by 1 and occupying it with this counter // Check if the oldest bit was lost since we are shifting the window by 1 and occupying it with this counter
@@ -87,9 +92,13 @@ func (b *Bits) Update(l *logrus.Logger, i uint64) bool {
// Check to see if it's a duplicate // Check to see if it's a duplicate
if i > b.current-b.length || i < b.length && b.current < b.length { if i > b.current-b.length || i < b.length && b.current < b.length {
if b.current == i || b.bits[i%b.length] == true { if b.current == i || b.bits[i%b.length] == true {
if l.Level >= logrus.DebugLevel { if l.Enabled(context.Background(), slog.LevelDebug) {
l.WithField("receiveWindow", m{"accepted": false, "currentCounter": b.current, "incomingCounter": i, "reason": "duplicate"}). l.Debug("Receive window",
Debug("Receive window") "accepted", false,
"currentCounter", b.current,
"incomingCounter", i,
"reason", "duplicate",
)
} }
b.dupeCounter.Inc(1) b.dupeCounter.Inc(1)
return false return false
@@ -101,12 +110,13 @@ func (b *Bits) Update(l *logrus.Logger, i uint64) bool {
// In all other cases, fail and don't change current. // In all other cases, fail and don't change current.
b.outOfWindowCounter.Inc(1) b.outOfWindowCounter.Inc(1)
if l.Level >= logrus.DebugLevel { if l.Enabled(context.Background(), slog.LevelDebug) {
l.WithField("accepted", false). l.Debug("Receive window",
WithField("currentCounter", b.current). "accepted", false,
WithField("incomingCounter", i). "currentCounter", b.current,
WithField("reason", "nonsense"). "incomingCounter", i,
Debug("Receive window") "reason", "nonsense",
)
} }
return false return false
} }
+10 -3
View File
@@ -3,8 +3,15 @@
package main package main
import "github.com/sirupsen/logrus" import (
"log/slog"
"os"
func HookLogger(l *logrus.Logger) { "github.com/slackhq/nebula/logging"
// Do nothing, let the logs flow to stdout/stderr )
// newPlatformLogger returns a *slog.Logger that writes to stdout. Non-Windows
// platforms have no special sink to integrate with.
func newPlatformLogger() *slog.Logger {
return logging.NewLogger(os.Stdout)
} }
+71 -39
View File
@@ -1,54 +1,86 @@
package main package main
import ( import (
"fmt" "context"
"io/ioutil" "log/slog"
"os" "strings"
"sync"
"github.com/kardianos/service" "github.com/slackhq/nebula/logging"
"github.com/sirupsen/logrus"
) )
// HookLogger routes the logrus logs through the service logger so that they end up in the Windows Event Viewer // newPlatformLogger returns a *slog.Logger that routes every log record
// logrus output will be discarded // through the Windows service logger so records end up in the Windows
func HookLogger(l *logrus.Logger) { // Event Log. All the heavy lifting (level management, format swap,
l.AddHook(newLogHook(logger)) // timestamp toggle, WithAttrs/WithGroup) comes from logging.NewHandler;
l.SetOutput(ioutil.Discard) // this file only contributes:
//
// - an io.Writer that forwards each formatted line to the service
// logger at the current record's Event Log severity, and
// - a thin severityTag that embeds *logging.Handler and overrides
// only Handle / WithAttrs / WithGroup, so Event Viewer's severity
// column and severity-based filters keep working the way they did
// before the slog migration.
//
// Format (text vs json) is carried by the embedded *logging.Handler, so
// logging.format: json in config still produces JSON lines in Event
// Viewer, same as the pre-slog logrus setup.
func newPlatformLogger() *slog.Logger {
w := &eventLogWriter{}
return slog.New(&severityTag{Handler: logging.NewHandler(w), w: w})
} }
type logHook struct { // eventLogWriter forwards slog-formatted lines to the Windows service
sl service.Logger // logger at the severity most recently stashed by severityTag.Handle.
// The mutex serializes the stash + inner.Handle + Write cycle per record
// across all concurrent goroutines; slog's builtin text/json handlers
// each hold their own mutex around Write, but that only protects the
// Write call itself, not our stash-then-handle sequence.
type eventLogWriter struct {
mu sync.Mutex
level slog.Level
} }
func newLogHook(sl service.Logger) *logHook { func (w *eventLogWriter) Write(p []byte) (int, error) {
return &logHook{sl: sl} line := strings.TrimRight(string(p), "\n")
} switch {
case w.level >= slog.LevelError:
func (h *logHook) Fire(entry *logrus.Entry) error { return len(p), logger.Error(line)
line, err := entry.String() case w.level >= slog.LevelWarn:
if err != nil { return len(p), logger.Warning(line)
fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
return err
}
switch entry.Level {
case logrus.PanicLevel:
return h.sl.Error(line)
case logrus.FatalLevel:
return h.sl.Error(line)
case logrus.ErrorLevel:
return h.sl.Error(line)
case logrus.WarnLevel:
return h.sl.Warning(line)
case logrus.InfoLevel:
return h.sl.Info(line)
case logrus.DebugLevel:
return h.sl.Info(line)
default: default:
return nil return len(p), logger.Info(line)
} }
} }
func (h *logHook) Levels() []logrus.Level { // severityTag embeds *logging.Handler to pick up everything it does for
return logrus.AllLevels // free (Enabled, SetLevel, GetLevel, SetFormat, GetFormat,
// SetDisableTimestamp) and overrides only Handle / WithAttrs / WithGroup
// so each record's slog.Level is stashed on the writer before formatting
// and so derived handlers stay wrapped as severityTag rather than
// downgrading to bare *logging.Handler.
type severityTag struct {
*logging.Handler
w *eventLogWriter
}
func (s *severityTag) Handle(ctx context.Context, r slog.Record) error {
s.w.mu.Lock()
defer s.w.mu.Unlock()
s.w.level = r.Level
return s.Handler.Handle(ctx, r)
}
func (s *severityTag) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return s
}
return &severityTag{Handler: s.Handler.WithAttrs(attrs).(*logging.Handler), w: s.w}
}
func (s *severityTag) WithGroup(name string) slog.Handler {
if name == "" {
return s
}
return &severityTag{Handler: s.Handler.WithGroup(name).(*logging.Handler), w: s.w}
} }
+19 -7
View File
@@ -7,9 +7,9 @@ import (
"runtime/debug" "runtime/debug"
"strings" "strings"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula" "github.com/slackhq/nebula"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/logging"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
) )
@@ -50,9 +50,14 @@ func main() {
os.Exit(0) os.Exit(0)
} }
l := logging.NewLogger(os.Stdout)
if *serviceFlag != "" { if *serviceFlag != "" {
doService(configPath, configTest, Build, serviceFlag) if err := doService(configPath, configTest, Build, serviceFlag); err != nil {
os.Exit(1) l.Error("Service command failed", "error", err)
os.Exit(1)
}
return
} }
if *configPath == "" { if *configPath == "" {
@@ -61,9 +66,6 @@ func main() {
os.Exit(1) os.Exit(1)
} }
l := logrus.New()
l.Out = os.Stdout
c := config.NewC(l) c := config.NewC(l)
err := c.Load(*configPath) err := c.Load(*configPath)
if err != nil { if err != nil {
@@ -71,6 +73,16 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if err := logging.ApplyConfig(l, c); err != nil {
fmt.Printf("failed to apply logging config: %s", err)
os.Exit(1)
}
c.RegisterReloadCallback(func(c *config.C) {
if err := logging.ApplyConfig(l, c); err != nil {
l.Error("Failed to reconfigure logger on reload", "error", err)
}
})
ctrl, err := nebula.Main(c, *configTest, Build, l, nil) ctrl, err := nebula.Main(c, *configTest, Build, l, nil)
if err != nil { if err != nil {
util.LogWithContextIfNeeded("Failed to start", err, l) util.LogWithContextIfNeeded("Failed to start", err, l)
@@ -87,7 +99,7 @@ func main() {
go ctrl.ShutdownBlock() go ctrl.ShutdownBlock()
if err := wait(); err != nil { if err := wait(); err != nil {
l.WithError(err).Error("Nebula stopped due to fatal error") l.Error("Nebula stopped due to fatal error", "error", err)
os.Exit(2) os.Exit(2)
} }
+20 -14
View File
@@ -7,9 +7,9 @@ import (
"path/filepath" "path/filepath"
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula" "github.com/slackhq/nebula"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/logging"
) )
var logger service.Logger var logger service.Logger
@@ -25,8 +25,7 @@ func (p *program) Start(s service.Service) error {
// Start should not block. // Start should not block.
logger.Info("Nebula service starting.") logger.Info("Nebula service starting.")
l := logrus.New() l := newPlatformLogger()
HookLogger(l)
c := config.NewC(l) c := config.NewC(l)
err := c.Load(*p.configPath) err := c.Load(*p.configPath)
@@ -34,6 +33,15 @@ func (p *program) Start(s service.Service) error {
return fmt.Errorf("failed to load config: %s", err) return fmt.Errorf("failed to load config: %s", err)
} }
if err := logging.ApplyConfig(l, c); err != nil {
return fmt.Errorf("failed to apply logging config: %s", err)
}
c.RegisterReloadCallback(func(c *config.C) {
if err := logging.ApplyConfig(l, c); err != nil {
l.Error("Failed to reconfigure logger on reload", "error", err)
}
})
p.control, err = nebula.Main(c, *p.configTest, Build, l, nil) p.control, err = nebula.Main(c, *p.configTest, Build, l, nil)
if err != nil { if err != nil {
return err return err
@@ -57,11 +65,11 @@ func fileExists(filename string) bool {
return true return true
} }
func doService(configPath *string, configTest *bool, build string, serviceFlag *string) { func doService(configPath *string, configTest *bool, build string, serviceFlag *string) error {
if *configPath == "" { if *configPath == "" {
ex, err := os.Executable() ex, err := os.Executable()
if err != nil { if err != nil {
panic(err) return err
} }
*configPath = filepath.Dir(ex) + "/config.yaml" *configPath = filepath.Dir(ex) + "/config.yaml"
if !fileExists(*configPath) { if !fileExists(*configPath) {
@@ -85,16 +93,16 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
// Here are what the different loggers are doing: // Here are what the different loggers are doing:
// - `log` is the standard go log utility, meant to be used while the process is still attached to stdout/stderr // - `log` is the standard go log utility, meant to be used while the process is still attached to stdout/stderr
// - `logger` is the service log utility that may be attached to a special place depending on OS (Windows will have it attached to the event log) // - `logger` is the service log utility that may be attached to a special place depending on OS (Windows will have it attached to the event log)
// - above, in `Run` we create a `logrus.Logger` which is what nebula expects to use // - in program.Start we build a *slog.Logger via newPlatformLogger; on non-Windows that is a stdout-backed slog logger, on Windows it routes records through the service logger
s, err := service.New(prg, svcConfig) s, err := service.New(prg, svcConfig)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
errs := make(chan error, 5) errs := make(chan error, 5)
logger, err = s.Logger(errs) logger, err = s.Logger(errs)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
go func() { go func() {
@@ -109,18 +117,16 @@ func doService(configPath *string, configTest *bool, build string, serviceFlag *
switch *serviceFlag { switch *serviceFlag {
case "run": case "run":
err = s.Run() if err := s.Run(); err != nil {
if err != nil {
// Route any errors to the system logger // Route any errors to the system logger
logger.Error(err) logger.Error(err)
} }
default: default:
err := service.Control(s, *serviceFlag) if err := service.Control(s, *serviceFlag); err != nil {
if err != nil {
log.Printf("Valid actions: %q\n", service.ControlAction) log.Printf("Valid actions: %q\n", service.ControlAction)
log.Fatal(err) return err
} }
return
} }
return nil
} }
+13 -4
View File
@@ -7,9 +7,9 @@ import (
"runtime/debug" "runtime/debug"
"strings" "strings"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula" "github.com/slackhq/nebula"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/logging"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
) )
@@ -55,8 +55,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
l := logrus.New() l := logging.NewLogger(os.Stdout)
l.Out = os.Stdout
c := config.NewC(l) c := config.NewC(l)
err := c.Load(*configPath) err := c.Load(*configPath)
@@ -65,6 +64,16 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if err := logging.ApplyConfig(l, c); err != nil {
fmt.Printf("failed to apply logging config: %s", err)
os.Exit(1)
}
c.RegisterReloadCallback(func(c *config.C) {
if err := logging.ApplyConfig(l, c); err != nil {
l.Error("Failed to reconfigure logger on reload", "error", err)
}
})
ctrl, err := nebula.Main(c, *configTest, Build, l, nil) ctrl, err := nebula.Main(c, *configTest, Build, l, nil)
if err != nil { if err != nil {
util.LogWithContextIfNeeded("Failed to start", err, l) util.LogWithContextIfNeeded("Failed to start", err, l)
@@ -82,7 +91,7 @@ func main() {
notifyReady(l) notifyReady(l)
if err := wait(); err != nil { if err := wait(); err != nil {
l.WithError(err).Error("Nebula stopped due to fatal error") l.Error("Nebula stopped due to fatal error", "error", err)
os.Exit(2) os.Exit(2)
} }
+7 -8
View File
@@ -1,11 +1,10 @@
package main package main
import ( import (
"log/slog"
"net" "net"
"os" "os"
"time" "time"
"github.com/sirupsen/logrus"
) )
// SdNotifyReady tells systemd the service is ready and dependent services can now be started // SdNotifyReady tells systemd the service is ready and dependent services can now be started
@@ -13,30 +12,30 @@ import (
// https://www.freedesktop.org/software/systemd/man/systemd.service.html // https://www.freedesktop.org/software/systemd/man/systemd.service.html
const SdNotifyReady = "READY=1" const SdNotifyReady = "READY=1"
func notifyReady(l *logrus.Logger) { func notifyReady(l *slog.Logger) {
sockName := os.Getenv("NOTIFY_SOCKET") sockName := os.Getenv("NOTIFY_SOCKET")
if sockName == "" { if sockName == "" {
l.Debugln("NOTIFY_SOCKET systemd env var not set, not sending ready signal") l.Debug("NOTIFY_SOCKET systemd env var not set, not sending ready signal")
return return
} }
conn, err := net.DialTimeout("unixgram", sockName, time.Second) conn, err := net.DialTimeout("unixgram", sockName, time.Second)
if err != nil { if err != nil {
l.WithError(err).Error("failed to connect to systemd notification socket") l.Error("failed to connect to systemd notification socket", "error", err)
return return
} }
defer conn.Close() defer conn.Close()
err = conn.SetWriteDeadline(time.Now().Add(time.Second)) err = conn.SetWriteDeadline(time.Now().Add(time.Second))
if err != nil { if err != nil {
l.WithError(err).Error("failed to set the write deadline for the systemd notification socket") l.Error("failed to set the write deadline for the systemd notification socket", "error", err)
return return
} }
if _, err = conn.Write([]byte(SdNotifyReady)); err != nil { if _, err = conn.Write([]byte(SdNotifyReady)); err != nil {
l.WithError(err).Error("failed to signal the systemd notification socket") l.Error("failed to signal the systemd notification socket", "error", err)
return return
} }
l.Debugln("notified systemd the service is ready") l.Debug("notified systemd the service is ready")
} }
+2 -2
View File
@@ -3,8 +3,8 @@
package main package main
import "github.com/sirupsen/logrus" import "log/slog"
func notifyReady(_ *logrus.Logger) { func notifyReady(_ *slog.Logger) {
// No init service to notify // No init service to notify
} }
+15 -6
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"math" "math"
"os" "os"
"os/signal" "os/signal"
@@ -16,7 +17,6 @@ import (
"time" "time"
"dario.cat/mergo" "dario.cat/mergo"
"github.com/sirupsen/logrus"
"go.yaml.in/yaml/v3" "go.yaml.in/yaml/v3"
) )
@@ -26,11 +26,11 @@ type C struct {
Settings map[string]any Settings map[string]any
oldSettings map[string]any oldSettings map[string]any
callbacks []func(*C) callbacks []func(*C)
l *logrus.Logger l *slog.Logger
reloadLock sync.Mutex reloadLock sync.Mutex
} }
func NewC(l *logrus.Logger) *C { func NewC(l *slog.Logger) *C {
return &C{ return &C{
Settings: make(map[string]any), Settings: make(map[string]any),
l: l, l: l,
@@ -107,12 +107,18 @@ func (c *C) HasChanged(k string) bool {
newVals, err := yaml.Marshal(nv) newVals, err := yaml.Marshal(nv)
if err != nil { if err != nil {
c.l.WithField("config_path", k).WithError(err).Error("Error while marshaling new config") c.l.Error("Error while marshaling new config",
"config_path", k,
"error", err,
)
} }
oldVals, err := yaml.Marshal(ov) oldVals, err := yaml.Marshal(ov)
if err != nil { if err != nil {
c.l.WithField("config_path", k).WithError(err).Error("Error while marshaling old config") c.l.Error("Error while marshaling old config",
"config_path", k,
"error", err,
)
} }
return string(newVals) != string(oldVals) return string(newVals) != string(oldVals)
@@ -154,7 +160,10 @@ func (c *C) ReloadConfig() {
err := c.Load(c.path) err := c.Load(c.path)
if err != nil { if err != nil {
c.l.WithField("config_path", c.path).WithError(err).Error("Error occurred while reloading config") c.l.Error("Error occurred while reloading config",
"config_path", c.path,
"error", err,
)
return return
} }
+65 -57
View File
@@ -5,13 +5,13 @@ import (
"context" "context"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"log/slog"
"net/netip" "net/netip"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
@@ -47,10 +47,10 @@ type connectionManager struct {
metricsTxPunchy metrics.Counter metricsTxPunchy metrics.Counter
l *logrus.Logger l *slog.Logger
} }
func newConnectionManagerFromConfig(l *logrus.Logger, c *config.C, hm *HostMap, p *Punchy) *connectionManager { func newConnectionManagerFromConfig(l *slog.Logger, c *config.C, hm *HostMap, p *Punchy) *connectionManager {
cm := &connectionManager{ cm := &connectionManager{
hostMap: hm, hostMap: hm,
l: l, l: l,
@@ -85,9 +85,10 @@ func (cm *connectionManager) reload(c *config.C, initial bool) {
old := cm.getInactivityTimeout() old := cm.getInactivityTimeout()
cm.inactivityTimeout.Store((int64)(c.GetDuration("tunnels.inactivity_timeout", 10*time.Minute))) cm.inactivityTimeout.Store((int64)(c.GetDuration("tunnels.inactivity_timeout", 10*time.Minute)))
if !initial { if !initial {
cm.l.WithField("oldDuration", old). cm.l.Info("Inactivity timeout has changed",
WithField("newDuration", cm.getInactivityTimeout()). "oldDuration", old,
Info("Inactivity timeout has changed") "newDuration", cm.getInactivityTimeout(),
)
} }
} }
@@ -95,9 +96,10 @@ func (cm *connectionManager) reload(c *config.C, initial bool) {
old := cm.dropInactive.Load() old := cm.dropInactive.Load()
cm.dropInactive.Store(c.GetBool("tunnels.drop_inactive", false)) cm.dropInactive.Store(c.GetBool("tunnels.drop_inactive", false))
if !initial { if !initial {
cm.l.WithField("oldBool", old). cm.l.Info("Drop inactive setting has changed",
WithField("newBool", cm.dropInactive.Load()). "oldBool", old,
Info("Drop inactive setting has changed") "newBool", cm.dropInactive.Load(),
)
} }
} }
} }
@@ -256,7 +258,7 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
var err error var err error
index, err = AddRelay(cm.l, newhostinfo, cm.hostMap, r.PeerAddr, nil, r.Type, Requested) index, err = AddRelay(cm.l, newhostinfo, cm.hostMap, r.PeerAddr, nil, r.Type, Requested)
if err != nil { if err != nil {
cm.l.WithError(err).Error("failed to migrate relay to new hostinfo") cm.l.Error("failed to migrate relay to new hostinfo", "error", err)
continue continue
} }
switch r.Type { switch r.Type {
@@ -304,16 +306,16 @@ func (cm *connectionManager) migrateRelayUsed(oldhostinfo, newhostinfo *HostInfo
msg, err := req.Marshal() msg, err := req.Marshal()
if err != nil { if err != nil {
cm.l.WithError(err).Error("failed to marshal Control message to migrate relay") cm.l.Error("failed to marshal Control message to migrate relay", "error", err)
} else { } else {
cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, make([]byte, 12), make([]byte, mtu)) cm.intf.SendMessageToHostInfo(header.Control, 0, newhostinfo, msg, make([]byte, 12), make([]byte, mtu))
cm.l.WithFields(logrus.Fields{ cm.l.Info("send CreateRelayRequest",
"relayFrom": req.RelayFromAddr, "relayFrom", req.RelayFromAddr,
"relayTo": req.RelayToAddr, "relayTo", req.RelayToAddr,
"initiatorRelayIndex": req.InitiatorRelayIndex, "initiatorRelayIndex", req.InitiatorRelayIndex,
"responderRelayIndex": req.ResponderRelayIndex, "responderRelayIndex", req.ResponderRelayIndex,
"vpnAddrs": newhostinfo.vpnAddrs}). "vpnAddrs", newhostinfo.vpnAddrs,
Info("send CreateRelayRequest") )
} }
} }
} }
@@ -325,7 +327,7 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
hostinfo := cm.hostMap.Indexes[localIndex] hostinfo := cm.hostMap.Indexes[localIndex]
if hostinfo == nil { if hostinfo == nil {
cm.l.WithField("localIndex", localIndex).Debugln("Not found in hostmap") cm.l.Debug("Not found in hostmap", "localIndex", localIndex)
return doNothing, nil, nil return doNothing, nil, nil
} }
@@ -345,10 +347,10 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
// A hostinfo is determined alive if there is incoming traffic // A hostinfo is determined alive if there is incoming traffic
if inTraffic { if inTraffic {
decision := doNothing decision := doNothing
if cm.l.Level >= logrus.DebugLevel { if cm.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(cm.l). hostinfo.logger(cm.l).Debug("Tunnel status",
WithField("tunnelCheck", m{"state": "alive", "method": "passive"}). "tunnelCheck", m{"state": "alive", "method": "passive"},
Debug("Tunnel status") )
} }
hostinfo.pendingDeletion.Store(false) hostinfo.pendingDeletion.Store(false)
@@ -375,9 +377,9 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
if hostinfo.pendingDeletion.Load() { if hostinfo.pendingDeletion.Load() {
// We have already sent a test packet and nothing was returned, this hostinfo is dead // We have already sent a test packet and nothing was returned, this hostinfo is dead
hostinfo.logger(cm.l). hostinfo.logger(cm.l).Info("Tunnel status",
WithField("tunnelCheck", m{"state": "dead", "method": "active"}). "tunnelCheck", m{"state": "dead", "method": "active"},
Info("Tunnel status") )
return deleteTunnel, hostinfo, nil return deleteTunnel, hostinfo, nil
} }
@@ -388,10 +390,10 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
inactiveFor, isInactive := cm.isInactive(hostinfo, now) inactiveFor, isInactive := cm.isInactive(hostinfo, now)
if isInactive { if isInactive {
// Tunnel is inactive, tear it down // Tunnel is inactive, tear it down
hostinfo.logger(cm.l). hostinfo.logger(cm.l).Info("Dropping tunnel due to inactivity",
WithField("inactiveDuration", inactiveFor). "inactiveDuration", inactiveFor,
WithField("primary", mainHostInfo). "primary", mainHostInfo,
Info("Dropping tunnel due to inactivity") )
return closeTunnel, hostinfo, primary return closeTunnel, hostinfo, primary
} }
@@ -410,18 +412,18 @@ func (cm *connectionManager) makeTrafficDecision(localIndex uint32, now time.Tim
cm.sendPunch(hostinfo) cm.sendPunch(hostinfo)
} }
if cm.l.Level >= logrus.DebugLevel { if cm.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(cm.l). hostinfo.logger(cm.l).Debug("Tunnel status",
WithField("tunnelCheck", m{"state": "testing", "method": "active"}). "tunnelCheck", m{"state": "testing", "method": "active"},
Debug("Tunnel status") )
} }
// Send a test packet to trigger an authenticated tunnel test, this should suss out any lingering tunnel issues // Send a test packet to trigger an authenticated tunnel test, this should suss out any lingering tunnel issues
decision = sendTestPacket decision = sendTestPacket
} else { } else {
if cm.l.Level >= logrus.DebugLevel { if cm.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(cm.l).Debugf("Hostinfo sadness") hostinfo.logger(cm.l).Debug("Hostinfo sadness")
} }
} }
@@ -493,14 +495,16 @@ func (cm *connectionManager) isInvalidCertificate(now time.Time, hostinfo *HostI
return false //cert is still valid! yay! return false //cert is still valid! yay!
} else if err == cert.ErrBlockListed { //avoiding errors.Is for speed } else if err == cert.ErrBlockListed { //avoiding errors.Is for speed
// Block listed certificates should always be disconnected // Block listed certificates should always be disconnected
hostinfo.logger(cm.l).WithError(err). hostinfo.logger(cm.l).Info("Remote certificate is blocked, tearing down the tunnel",
WithField("fingerprint", remoteCert.Fingerprint). "error", err,
Info("Remote certificate is blocked, tearing down the tunnel") "fingerprint", remoteCert.Fingerprint,
)
return true return true
} else if cm.intf.disconnectInvalid.Load() { } else if cm.intf.disconnectInvalid.Load() {
hostinfo.logger(cm.l).WithError(err). hostinfo.logger(cm.l).Info("Remote certificate is no longer valid, tearing down the tunnel",
WithField("fingerprint", remoteCert.Fingerprint). "error", err,
Info("Remote certificate is no longer valid, tearing down the tunnel") "fingerprint", remoteCert.Fingerprint,
)
return true return true
} else { } else {
//if we reach here, the cert is no longer valid, but we're configured to keep tunnels from now-invalid certs open //if we reach here, the cert is no longer valid, but we're configured to keep tunnels from now-invalid certs open
@@ -539,10 +543,11 @@ func (cm *connectionManager) tryRehandshake(hostinfo *HostInfo) {
curCrtVersion := curCrt.Version() curCrtVersion := curCrt.Version()
myCrt := cs.getCertificate(curCrtVersion) myCrt := cs.getCertificate(curCrtVersion)
if myCrt == nil { if myCrt == nil {
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs). cm.l.Info("Re-handshaking with remote",
WithField("version", curCrtVersion). "vpnAddrs", hostinfo.vpnAddrs,
WithField("reason", "local certificate removed"). "version", curCrtVersion,
Info("Re-handshaking with remote") "reason", "local certificate removed",
)
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil) cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
return return
} }
@@ -550,11 +555,12 @@ func (cm *connectionManager) tryRehandshake(hostinfo *HostInfo) {
if peerCrt != nil && curCrtVersion < peerCrt.Certificate.Version() { if peerCrt != nil && curCrtVersion < peerCrt.Certificate.Version() {
// if our certificate version is less than theirs, and we have a matching version available, rehandshake? // if our certificate version is less than theirs, and we have a matching version available, rehandshake?
if cs.getCertificate(peerCrt.Certificate.Version()) != nil { if cs.getCertificate(peerCrt.Certificate.Version()) != nil {
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs). cm.l.Info("Re-handshaking with remote",
WithField("version", curCrtVersion). "vpnAddrs", hostinfo.vpnAddrs,
WithField("peerVersion", peerCrt.Certificate.Version()). "version", curCrtVersion,
WithField("reason", "local certificate version lower than peer, attempting to correct"). "peerVersion", peerCrt.Certificate.Version(),
Info("Re-handshaking with remote") "reason", "local certificate version lower than peer, attempting to correct",
)
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], func(hh *HandshakeHostInfo) { cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], func(hh *HandshakeHostInfo) {
hh.initiatingVersionOverride = peerCrt.Certificate.Version() hh.initiatingVersionOverride = peerCrt.Certificate.Version()
}) })
@@ -562,17 +568,19 @@ func (cm *connectionManager) tryRehandshake(hostinfo *HostInfo) {
} }
} }
if !bytes.Equal(curCrt.Signature(), myCrt.Signature()) { if !bytes.Equal(curCrt.Signature(), myCrt.Signature()) {
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs). cm.l.Info("Re-handshaking with remote",
WithField("reason", "local certificate is not current"). "vpnAddrs", hostinfo.vpnAddrs,
Info("Re-handshaking with remote") "reason", "local certificate is not current",
)
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil) cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
return return
} }
if curCrtVersion < cs.initiatingVersion { if curCrtVersion < cs.initiatingVersion {
cm.l.WithField("vpnAddrs", hostinfo.vpnAddrs). cm.l.Info("Re-handshaking with remote",
WithField("reason", "current cert version < pki.initiatingVersion"). "vpnAddrs", hostinfo.vpnAddrs,
Info("Re-handshaking with remote") "reason", "current cert version < pki.initiatingVersion",
)
cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil) cm.intf.handshakeManager.StartHandshake(hostinfo.vpnAddrs[0], nil)
return return
+17 -17
View File
@@ -10,7 +10,7 @@ import (
"github.com/flynn/noise" "github.com/flynn/noise"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay" "github.com/slackhq/nebula/overlay/overlaytest"
"github.com/slackhq/nebula/test" "github.com/slackhq/nebula/test"
"github.com/slackhq/nebula/udp" "github.com/slackhq/nebula/udp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -53,7 +53,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
lh := newTestLighthouse() lh := newTestLighthouse()
ifce := &Interface{ ifce := &Interface{
hostMap: hostMap, hostMap: hostMap,
inside: &overlay.NoopTun{}, inside: &overlaytest.NoopTun{},
outside: &udp.NoopConn{}, outside: &udp.NoopConn{},
firewall: &Firewall{}, firewall: &Firewall{},
lightHouse: lh, lightHouse: lh,
@@ -64,9 +64,9 @@ func Test_NewConnectionManagerTest(t *testing.T) {
ifce.pki.cs.Store(cs) ifce.pki.cs.Store(cs)
// Create manager // Create manager
conf := config.NewC(l) conf := config.NewC(test.NewLogger())
punchy := NewPunchyFromConfig(l, conf) punchy := NewPunchyFromConfig(test.NewLogger(), conf)
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy) nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
nc.intf = ifce nc.intf = ifce
p := []byte("") p := []byte("")
nb := make([]byte, 12, 12) nb := make([]byte, 12, 12)
@@ -136,7 +136,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
lh := newTestLighthouse() lh := newTestLighthouse()
ifce := &Interface{ ifce := &Interface{
hostMap: hostMap, hostMap: hostMap,
inside: &overlay.NoopTun{}, inside: &overlaytest.NoopTun{},
outside: &udp.NoopConn{}, outside: &udp.NoopConn{},
firewall: &Firewall{}, firewall: &Firewall{},
lightHouse: lh, lightHouse: lh,
@@ -147,9 +147,9 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
ifce.pki.cs.Store(cs) ifce.pki.cs.Store(cs)
// Create manager // Create manager
conf := config.NewC(l) conf := config.NewC(test.NewLogger())
punchy := NewPunchyFromConfig(l, conf) punchy := NewPunchyFromConfig(test.NewLogger(), conf)
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy) nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
nc.intf = ifce nc.intf = ifce
p := []byte("") p := []byte("")
nb := make([]byte, 12, 12) nb := make([]byte, 12, 12)
@@ -221,7 +221,7 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
lh := newTestLighthouse() lh := newTestLighthouse()
ifce := &Interface{ ifce := &Interface{
hostMap: hostMap, hostMap: hostMap,
inside: &overlay.NoopTun{}, inside: &overlaytest.NoopTun{},
outside: &udp.NoopConn{}, outside: &udp.NoopConn{},
firewall: &Firewall{}, firewall: &Firewall{},
lightHouse: lh, lightHouse: lh,
@@ -232,12 +232,12 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
ifce.pki.cs.Store(cs) ifce.pki.cs.Store(cs)
// Create manager // Create manager
conf := config.NewC(l) conf := config.NewC(test.NewLogger())
conf.Settings["tunnels"] = map[string]any{ conf.Settings["tunnels"] = map[string]any{
"drop_inactive": true, "drop_inactive": true,
} }
punchy := NewPunchyFromConfig(l, conf) punchy := NewPunchyFromConfig(test.NewLogger(), conf)
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy) nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
assert.True(t, nc.dropInactive.Load()) assert.True(t, nc.dropInactive.Load())
nc.intf = ifce nc.intf = ifce
@@ -348,7 +348,7 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
lh := newTestLighthouse() lh := newTestLighthouse()
ifce := &Interface{ ifce := &Interface{
hostMap: hostMap, hostMap: hostMap,
inside: &overlay.NoopTun{}, inside: &overlaytest.NoopTun{},
outside: &udp.NoopConn{}, outside: &udp.NoopConn{},
firewall: &Firewall{}, firewall: &Firewall{},
lightHouse: lh, lightHouse: lh,
@@ -361,9 +361,9 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
ifce.disconnectInvalid.Store(true) ifce.disconnectInvalid.Store(true)
// Create manager // Create manager
conf := config.NewC(l) conf := config.NewC(test.NewLogger())
punchy := NewPunchyFromConfig(l, conf) punchy := NewPunchyFromConfig(test.NewLogger(), conf)
nc := newConnectionManagerFromConfig(l, conf, hostMap, punchy) nc := newConnectionManagerFromConfig(test.NewLogger(), conf, hostMap, punchy)
nc.intf = ifce nc.intf = ifce
ifce.connectionManager = nc ifce.connectionManager = nc
+2 -3
View File
@@ -8,12 +8,11 @@ import (
"sync/atomic" "sync/atomic"
"github.com/flynn/noise" "github.com/flynn/noise"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/noiseutil" "github.com/slackhq/nebula/noiseutil"
) )
const ReplayWindow = 1024 const ReplayWindow = 1024 //todo I've started seeing out-of-window messages in testing?
type ConnectionState struct { type ConnectionState struct {
eKey *NebulaCipherState eKey *NebulaCipherState
@@ -27,7 +26,7 @@ type ConnectionState struct {
writeLock sync.Mutex writeLock sync.Mutex
} }
func NewConnectionState(l *logrus.Logger, cs *CertState, crt cert.Certificate, initiator bool, pattern noise.HandshakePattern) (*ConnectionState, error) { func NewConnectionState(cs *CertState, crt cert.Certificate, initiator bool, pattern noise.HandshakePattern) (*ConnectionState, error) {
var dhFunc noise.DHFunc var dhFunc noise.DHFunc
switch crt.Curve() { switch crt.Curve() {
case cert.Curve_CURVE25519: case cert.Curve_CURVE25519:
+8 -6
View File
@@ -3,13 +3,13 @@ package nebula
import ( import (
"context" "context"
"errors" "errors"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"os/signal" "os/signal"
"sync" "sync"
"syscall" "syscall"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/overlay" "github.com/slackhq/nebula/overlay"
@@ -46,7 +46,7 @@ type Control struct {
state RunState state RunState
f *Interface f *Interface
l *logrus.Logger l *slog.Logger
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
sshStart func() sshStart func()
@@ -151,7 +151,7 @@ func (c *Control) Stop() {
c.CloseAllTunnels(false) c.CloseAllTunnels(false)
if err := c.f.Close(); err != nil { if err := c.f.Close(); err != nil {
c.l.WithError(err).Error("Close interface failed") c.l.Error("Close interface failed", "error", err)
} }
c.stateLock.Lock() c.stateLock.Lock()
c.state = StateStopped c.state = StateStopped
@@ -166,7 +166,7 @@ func (c *Control) ShutdownBlock() {
rawSig := <-sigChan rawSig := <-sigChan
sig := rawSig.String() sig := rawSig.String()
c.l.WithField("signal", sig).Info("Caught signal, shutting down") c.l.Info("Caught signal, shutting down", "signal", sig)
c.Stop() c.Stop()
} }
@@ -303,8 +303,10 @@ func (c *Control) CloseAllTunnels(excludeLighthouses bool) (closed int) {
c.f.send(header.CloseTunnel, 0, h.ConnectionState, h, []byte{}, make([]byte, 12, 12), make([]byte, mtu)) 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.WithField("vpnAddrs", h.vpnAddrs).WithField("udpAddr", h.remote). c.l.Debug("Sending close tunnel message",
Debug("Sending close tunnel message") "vpnAddrs", h.vpnAddrs,
"udpAddr", h.remote,
)
closed++ closed++
} }
+1 -2
View File
@@ -6,7 +6,6 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/test" "github.com/slackhq/nebula/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -83,7 +82,7 @@ func TestControl_GetHostInfoByVpnIp(t *testing.T) {
f: &Interface{ f: &Interface{
hostMap: hm, hostMap: hm,
}, },
l: logrus.New(), l: test.NewLogger(),
} }
thi := c.GetHostInfoByVpnAddr(vpnIp, false) thi := c.GetHostInfoByVpnAddr(vpnIp, false)
+236 -63
View File
@@ -1,63 +1,249 @@
package nebula package nebula
import ( import (
"context"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
) )
// This whole thing should be rewritten to use context type dnsServer struct {
var dnsR *dnsRecords
var dnsServer *dns.Server
var dnsAddr string
type dnsRecords struct {
sync.RWMutex sync.RWMutex
l *logrus.Logger l *slog.Logger
ctx context.Context
dnsMap4 map[string]netip.Addr dnsMap4 map[string]netip.Addr
dnsMap6 map[string]netip.Addr dnsMap6 map[string]netip.Addr
hostMap *HostMap hostMap *HostMap
myVpnAddrsTable *bart.Lite myVpnAddrsTable *bart.Lite
mux *dns.ServeMux
// enabled mirrors `lighthouse.serve_dns && lighthouse.am_lighthouse`.
// Start, Add, and reload consult it so callers don't need to know the
// gating rules. When it toggles off via reload, accumulated records are
// cleared so a later re-enable starts with a fresh map populated from
// new handshakes.
enabled atomic.Bool
serverMu sync.Mutex
server *dns.Server
// started is closed once `server` has finished binding (or after
// ListenAndServe returns on a bind failure). Stop waits on it before
// calling Shutdown to avoid the miekg/dns "server not started" race
// where a Shutdown that arrives before bind completes is silently
// ignored, leaving the listener running forever.
started chan struct{}
addr string
} }
func newDnsRecords(l *logrus.Logger, cs *CertState, hostMap *HostMap) *dnsRecords { // newDnsServerFromConfig builds a dnsServer, applies the initial config, and
return &dnsRecords{ // registers a reload callback. The reload callback is registered before the
// initial config is applied, so a SIGHUP can later enable, fix, or disable
// DNS even if the initial application failed.
//
// The dnsServer internally gates on `lighthouse.serve_dns &&
// lighthouse.am_lighthouse`. Start and Add are safe to call unconditionally,
// they no-op when DNS isn't enabled. Each Start invocation owns a ctx-cancel
// watcher that tears the listener down on nebula shutdown. The returned
// pointer is always non-nil, even on error.
func newDnsServerFromConfig(ctx context.Context, l *slog.Logger, cs *CertState, hostMap *HostMap, c *config.C) (*dnsServer, error) {
ds := &dnsServer{
l: l, l: l,
ctx: ctx,
dnsMap4: make(map[string]netip.Addr), dnsMap4: make(map[string]netip.Addr),
dnsMap6: make(map[string]netip.Addr), dnsMap6: make(map[string]netip.Addr),
hostMap: hostMap, hostMap: hostMap,
myVpnAddrsTable: cs.myVpnAddrsTable, myVpnAddrsTable: cs.myVpnAddrsTable,
} }
ds.mux = dns.NewServeMux()
ds.mux.HandleFunc(".", ds.handleDnsRequest)
c.RegisterReloadCallback(func(c *config.C) {
if err := ds.reload(c, false); err != nil {
ds.l.Error("Failed to reload DNS responder from config", "error", err)
}
})
if err := ds.reload(c, true); err != nil {
return ds, err
}
return ds, nil
} }
func (d *dnsRecords) Query(q uint16, data string) netip.Addr { // reload applies the latest config and reconciles the running state with it:
// - enabled toggled on -> spawn a runner
// - enabled toggled off -> stop the runner
// - listen address changed (while running) -> restart on the new address
// - everything else -> no-op
//
// On the initial call it only records configuration; Control.Start is what
// launches the first runner via dnsStart.
func (d *dnsServer) reload(c *config.C, initial bool) error {
wantsDns := c.GetBool("lighthouse.serve_dns", false)
amLighthouse := c.GetBool("lighthouse.am_lighthouse", false)
enabled := wantsDns && amLighthouse
newAddr := getDnsServerAddr(c)
d.serverMu.Lock()
running := d.server
runningStarted := d.started
sameAddr := d.addr == newAddr
d.addr = newAddr
d.enabled.Store(enabled)
d.serverMu.Unlock()
if initial {
if wantsDns && !amLighthouse {
d.l.Warn("DNS server refusing to run because this host is not a lighthouse.")
}
return nil
}
if !enabled {
if running != nil {
d.Stop()
}
// Drop any records that accumulated while enabled; a later re-enable
// will repopulate from fresh handshakes.
d.clearRecords()
return nil
}
if running == nil {
// Was disabled (or never started); bring it up now.
go d.Start()
return nil
}
if sameAddr {
return nil
}
d.shutdownServer(running, runningStarted, "reload")
// Old Start goroutine has now exited; bring up a fresh listener on the
// new address.
go d.Start()
return nil
}
// shutdownServer waits for the server to finish binding (so Shutdown actually
// stops it rather than no-oping) and then shuts it down.
func (d *dnsServer) shutdownServer(srv *dns.Server, started chan struct{}, reason string) {
if srv == nil {
return
}
if started != nil {
<-started
}
if err := srv.Shutdown(); err != nil {
d.l.Warn("Failed to shut down the DNS responder", "reason", reason, "error", err)
}
}
// Start binds and serves the DNS responder. Blocks until Stop is called or
// the listener errors. Safe to call when DNS is disabled (returns
// immediately). This is what Control.dnsStart points at.
//
// Must be invoked after the tun device is active so that lighthouse.dns.host
// may bind to a nebula IP.
func (d *dnsServer) Start() {
if !d.enabled.Load() {
return
}
started := make(chan struct{})
d.serverMu.Lock()
if d.ctx.Err() != nil {
d.serverMu.Unlock()
return
}
addr := d.addr
server := &dns.Server{
Addr: addr,
Net: "udp",
Handler: d.mux,
NotifyStartedFunc: func() { close(started) },
}
d.server = server
d.started = started
d.serverMu.Unlock()
// Per-invocation ctx watcher. Exits when Start does, so we don't leak a
// watcher per reload-driven restart.
done := make(chan struct{})
go func() {
select {
case <-d.ctx.Done():
d.shutdownServer(server, started, "shutdown")
case <-done:
}
}()
d.l.Info("Starting DNS responder", "dnsListener", addr)
err := server.ListenAndServe()
close(done)
// If the listener never bound (bind error) NotifyStartedFunc never fires,
// so close started here to release any Stop caller waiting on it.
select {
case <-started:
default:
close(started)
}
if err != nil {
d.l.Warn("Failed to run the DNS responder", "error", err)
}
}
// Stop shuts down the active server, if any. Idempotent.
func (d *dnsServer) Stop() {
d.serverMu.Lock()
srv := d.server
started := d.started
d.server = nil
d.started = nil
d.serverMu.Unlock()
d.shutdownServer(srv, started, "stop")
}
// Query returns the address for the given name and query type. The second
// return value reports whether the name is known at all (in either A or AAAA),
// which lets callers distinguish NODATA from NXDOMAIN.
func (d *dnsServer) Query(q uint16, data string) (netip.Addr, bool) {
data = strings.ToLower(data) data = strings.ToLower(data)
d.RLock() d.RLock()
defer d.RUnlock() defer d.RUnlock()
addr4, haveV4 := d.dnsMap4[data]
addr6, haveV6 := d.dnsMap6[data]
nameExists := haveV4 || haveV6
switch q { switch q {
case dns.TypeA: case dns.TypeA:
if r, ok := d.dnsMap4[data]; ok { if haveV4 {
return r return addr4, nameExists
} }
case dns.TypeAAAA: case dns.TypeAAAA:
if r, ok := d.dnsMap6[data]; ok { if haveV6 {
return r return addr6, nameExists
} }
} }
return netip.Addr{} return netip.Addr{}, nameExists
} }
func (d *dnsRecords) QueryCert(data string) string { func (d *dnsServer) QueryCert(data string) string {
if len(data) < 2 {
return ""
}
ip, err := netip.ParseAddr(data[:len(data)-1]) ip, err := netip.ParseAddr(data[:len(data)-1])
if err != nil { if err != nil {
return "" return ""
@@ -80,8 +266,19 @@ func (d *dnsRecords) QueryCert(data string) string {
return string(b) return string(b)
} }
// clearRecords drops all DNS records.
func (d *dnsServer) clearRecords() {
d.Lock()
defer d.Unlock()
clear(d.dnsMap4)
clear(d.dnsMap6)
}
// Add adds the first IPv4 and IPv6 address that appears in `addresses` as the record for `host` // Add adds the first IPv4 and IPv6 address that appears in `addresses` as the record for `host`
func (d *dnsRecords) Add(host string, addresses []netip.Addr) { func (d *dnsServer) Add(host string, addresses []netip.Addr) {
if !d.enabled.Load() {
return
}
host = strings.ToLower(host) host = strings.ToLower(host)
d.Lock() d.Lock()
defer d.Unlock() defer d.Unlock()
@@ -101,7 +298,7 @@ func (d *dnsRecords) Add(host string, addresses []netip.Addr) {
} }
} }
func (d *dnsRecords) isSelfNebulaOrLocalhost(addr string) bool { func (d *dnsServer) isSelfNebulaOrLocalhost(addr string) bool {
a, _, _ := net.SplitHostPort(addr) a, _, _ := net.SplitHostPort(addr)
b, err := netip.ParseAddr(a) b, err := netip.ParseAddr(a)
if err != nil { if err != nil {
@@ -116,13 +313,24 @@ func (d *dnsRecords) isSelfNebulaOrLocalhost(addr string) bool {
return d.myVpnAddrsTable.Contains(b) return d.myVpnAddrsTable.Contains(b)
} }
func (d *dnsRecords) parseQuery(m *dns.Msg, w dns.ResponseWriter) { func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
debugEnabled := d.l.Enabled(context.Background(), slog.LevelDebug)
// Per RFC 2308 §2.2, a name that exists but has no record of the requested
// type must be answered with NOERROR and an empty answer section (NODATA),
// not NXDOMAIN (RFC 2308 §2.1), which is reserved for names that do not
// exist at all.
anyNameExists := false
for _, q := range m.Question { for _, q := range m.Question {
switch q.Qtype { switch q.Qtype {
case dns.TypeA, dns.TypeAAAA: case dns.TypeA, dns.TypeAAAA:
qType := dns.TypeToString[q.Qtype] qType := dns.TypeToString[q.Qtype]
d.l.Debugf("Query for %s %s", qType, q.Name) if debugEnabled {
ip := d.Query(q.Qtype, q.Name) d.l.Debug("DNS query", "type", qType, "name", q.Name)
}
ip, nameExists := d.Query(q.Qtype, q.Name)
if nameExists {
anyNameExists = true
}
if ip.IsValid() { if ip.IsValid() {
rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip)) rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip))
if err == nil { if err == nil {
@@ -134,7 +342,9 @@ func (d *dnsRecords) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
if !d.isSelfNebulaOrLocalhost(w.RemoteAddr().String()) { if !d.isSelfNebulaOrLocalhost(w.RemoteAddr().String()) {
return return
} }
d.l.Debugf("Query for TXT %s", q.Name) if debugEnabled {
d.l.Debug("DNS query", "type", "TXT", "name", q.Name)
}
ip := d.QueryCert(q.Name) ip := d.QueryCert(q.Name)
if ip != "" { if ip != "" {
rr, err := dns.NewRR(fmt.Sprintf("%s TXT %s", q.Name, ip)) rr, err := dns.NewRR(fmt.Sprintf("%s TXT %s", q.Name, ip))
@@ -145,12 +355,12 @@ func (d *dnsRecords) parseQuery(m *dns.Msg, w dns.ResponseWriter) {
} }
} }
if len(m.Answer) == 0 { if len(m.Answer) == 0 && !anyNameExists {
m.Rcode = dns.RcodeNameError m.Rcode = dns.RcodeNameError
} }
} }
func (d *dnsRecords) handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) { func (d *dnsServer) handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg) m := new(dns.Msg)
m.SetReply(r) m.SetReply(r)
m.Compress = false m.Compress = false
@@ -163,21 +373,6 @@ func (d *dnsRecords) handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) {
w.WriteMsg(m) w.WriteMsg(m)
} }
func dnsMain(l *logrus.Logger, cs *CertState, hostMap *HostMap, c *config.C) func() {
dnsR = newDnsRecords(l, cs, hostMap)
// attach request handler func
dns.HandleFunc(".", dnsR.handleDnsRequest)
c.RegisterReloadCallback(func(c *config.C) {
reloadDns(l, c)
})
return func() {
startDns(l, c)
}
}
func getDnsServerAddr(c *config.C) string { func getDnsServerAddr(c *config.C) string {
dnsHost := strings.TrimSpace(c.GetString("lighthouse.dns.host", "")) dnsHost := strings.TrimSpace(c.GetString("lighthouse.dns.host", ""))
// Old guidance was to provide the literal `[::]` in `lighthouse.dns.host` but that won't resolve. // Old guidance was to provide the literal `[::]` in `lighthouse.dns.host` but that won't resolve.
@@ -186,25 +381,3 @@ func getDnsServerAddr(c *config.C) string {
} }
return net.JoinHostPort(dnsHost, strconv.Itoa(c.GetInt("lighthouse.dns.port", 53))) return net.JoinHostPort(dnsHost, strconv.Itoa(c.GetInt("lighthouse.dns.port", 53)))
} }
func startDns(l *logrus.Logger, c *config.C) {
dnsAddr = getDnsServerAddr(c)
dnsServer = &dns.Server{Addr: dnsAddr, Net: "udp"}
l.WithField("dnsListener", dnsAddr).Info("Starting DNS responder")
err := dnsServer.ListenAndServe()
defer dnsServer.Shutdown()
if err != nil {
l.Errorf("Failed to start server: %s\n ", err.Error())
}
}
func reloadDns(l *logrus.Logger, c *config.C) {
if dnsAddr == getDnsServerAddr(c) {
l.Debug("No DNS server config change detected")
return
}
l.Debug("Restarting DNS server")
dnsServer.Shutdown()
go startDns(l, c)
}
+270 -3
View File
@@ -1,19 +1,43 @@
package nebula package nebula
import ( import (
"context"
"log/slog"
"net"
"net/netip" "net/netip"
"strconv"
"testing" "testing"
"time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type stubDNSWriter struct{}
func (stubDNSWriter) LocalAddr() net.Addr { return &net.UDPAddr{} }
func (stubDNSWriter) RemoteAddr() net.Addr {
return &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5353}
}
func (stubDNSWriter) Write([]byte) (int, error) { return 0, nil }
func (stubDNSWriter) WriteMsg(*dns.Msg) error { return nil }
func (stubDNSWriter) Close() error { return nil }
func (stubDNSWriter) TsigStatus() error { return nil }
func (stubDNSWriter) TsigTimersOnly(bool) {}
func (stubDNSWriter) Hijack() {}
func TestParsequery(t *testing.T) { func TestParsequery(t *testing.T) {
l := logrus.New() l := slog.New(slog.DiscardHandler)
hostMap := &HostMap{} hostMap := &HostMap{}
ds := newDnsRecords(l, &CertState{}, hostMap) ds := &dnsServer{
l: l,
dnsMap4: make(map[string]netip.Addr),
dnsMap6: make(map[string]netip.Addr),
hostMap: hostMap,
}
ds.enabled.Store(true)
addrs := []netip.Addr{ addrs := []netip.Addr{
netip.MustParseAddr("1.2.3.4"), netip.MustParseAddr("1.2.3.4"),
netip.MustParseAddr("1.2.3.5"), netip.MustParseAddr("1.2.3.5"),
@@ -21,18 +45,56 @@ func TestParsequery(t *testing.T) {
netip.MustParseAddr("fd01::25"), netip.MustParseAddr("fd01::25"),
} }
ds.Add("test.com.com", addrs) ds.Add("test.com.com", addrs)
ds.Add("v4only.com.com", []netip.Addr{netip.MustParseAddr("1.2.3.6")})
ds.Add("v6only.com.com", []netip.Addr{netip.MustParseAddr("fd01::26")})
m := &dns.Msg{} m := &dns.Msg{}
m.SetQuestion("test.com.com", dns.TypeA) m.SetQuestion("test.com.com", dns.TypeA)
ds.parseQuery(m, nil) ds.parseQuery(m, nil)
assert.NotNil(t, m.Answer) assert.NotNil(t, m.Answer)
assert.Equal(t, "1.2.3.4", m.Answer[0].(*dns.A).A.String()) assert.Equal(t, "1.2.3.4", m.Answer[0].(*dns.A).A.String())
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
m = &dns.Msg{} m = &dns.Msg{}
m.SetQuestion("test.com.com", dns.TypeAAAA) m.SetQuestion("test.com.com", dns.TypeAAAA)
ds.parseQuery(m, nil) ds.parseQuery(m, nil)
assert.NotNil(t, m.Answer) assert.NotNil(t, m.Answer)
assert.Equal(t, "fd01::24", m.Answer[0].(*dns.AAAA).AAAA.String()) assert.Equal(t, "fd01::24", m.Answer[0].(*dns.AAAA).AAAA.String())
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
// A known name with no record of the requested type should return NODATA
// (NOERROR with empty answer), not NXDOMAIN.
m = &dns.Msg{}
m.SetQuestion("v4only.com.com", dns.TypeAAAA)
ds.parseQuery(m, nil)
assert.Empty(t, m.Answer)
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
m = &dns.Msg{}
m.SetQuestion("v6only.com.com", dns.TypeA)
ds.parseQuery(m, nil)
assert.Empty(t, m.Answer)
assert.Equal(t, dns.RcodeSuccess, m.Rcode)
// An unknown name should still return NXDOMAIN.
m = &dns.Msg{}
m.SetQuestion("unknown.com.com", dns.TypeA)
ds.parseQuery(m, nil)
assert.Empty(t, m.Answer)
assert.Equal(t, dns.RcodeNameError, m.Rcode)
// short lookups should not fail
m = &dns.Msg{}
m.Question = []dns.Question{{Name: "", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}}
ds.parseQuery(m, stubDNSWriter{})
assert.Empty(t, m.Answer)
assert.Equal(t, dns.RcodeNameError, m.Rcode)
m = &dns.Msg{}
m.Question = []dns.Question{{Name: ".", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}}
ds.parseQuery(m, stubDNSWriter{})
assert.Empty(t, m.Answer)
assert.Equal(t, dns.RcodeNameError, m.Rcode)
} }
func Test_getDnsServerAddr(t *testing.T) { func Test_getDnsServerAddr(t *testing.T) {
@@ -71,3 +133,208 @@ func Test_getDnsServerAddr(t *testing.T) {
} }
assert.Equal(t, "[::]:1", getDnsServerAddr(c)) assert.Equal(t, "[::]:1", getDnsServerAddr(c))
} }
func newTestDnsServer(t *testing.T) (*dnsServer, *config.C) {
t.Helper()
sl := slog.New(slog.DiscardHandler)
ds := &dnsServer{
l: sl,
ctx: context.Background(),
dnsMap4: make(map[string]netip.Addr),
dnsMap6: make(map[string]netip.Addr),
hostMap: &HostMap{},
}
ds.mux = dns.NewServeMux()
ds.mux.HandleFunc(".", ds.handleDnsRequest)
return ds, config.NewC(nil)
}
func setDnsConfig(c *config.C, host string, port string, amLighthouse, serveDns bool) {
c.Settings["lighthouse"] = map[string]any{
"am_lighthouse": amLighthouse,
"serve_dns": serveDns,
"dns": map[string]any{
"host": host,
"port": port,
},
}
}
func TestDnsServer_reload_initial_disabled(t *testing.T) {
ds, c := newTestDnsServer(t)
setDnsConfig(c, "127.0.0.1", "0", true, false)
require.NoError(t, ds.reload(c, true))
assert.False(t, ds.enabled.Load())
assert.Equal(t, "127.0.0.1:0", ds.addr)
assert.Nil(t, ds.server)
}
func TestDnsServer_reload_initial_enabled(t *testing.T) {
ds, c := newTestDnsServer(t)
setDnsConfig(c, "127.0.0.1", "0", true, true)
require.NoError(t, ds.reload(c, true))
assert.True(t, ds.enabled.Load())
assert.Equal(t, "127.0.0.1:0", ds.addr)
// initial never starts a runner; that's Control.Start's job
assert.Nil(t, ds.server)
}
func TestDnsServer_reload_initial_serveDnsWithoutLighthouse(t *testing.T) {
ds, c := newTestDnsServer(t)
setDnsConfig(c, "127.0.0.1", "0", false, true)
require.NoError(t, ds.reload(c, true))
// Wants DNS but isn't a lighthouse: gated off, no runner.
assert.False(t, ds.enabled.Load())
}
func TestDnsServer_reload_sameAddr_noOp(t *testing.T) {
ds, c := newTestDnsServer(t)
setDnsConfig(c, "127.0.0.1", "0", true, true)
require.NoError(t, ds.reload(c, true))
// No server running yet, no addr change. Reload should not spawn anything.
require.NoError(t, ds.reload(c, false))
assert.True(t, ds.enabled.Load())
assert.Nil(t, ds.server)
}
func TestDnsServer_StartStop_lifecycle(t *testing.T) {
// Bind to a real (random) UDP port so we exercise the actual
// ListenAndServe + Shutdown plumbing including the started-chan race fix.
port := freeUDPPort(t)
ds, c := newTestDnsServer(t)
setDnsConfig(c, "127.0.0.1", port, true, true)
require.NoError(t, ds.reload(c, true))
done := make(chan struct{})
go func() {
ds.Start()
close(done)
}()
waitFor(t, func() bool {
ds.serverMu.Lock()
started := ds.started
ds.serverMu.Unlock()
if started == nil {
return false
}
select {
case <-started:
return true
default:
return false
}
})
ds.Stop()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Start did not return after Stop")
}
}
func TestDnsServer_Stop_beforeBind_doesNotHang(t *testing.T) {
// Stop called immediately after Start should not deadlock even if bind
// hasn't completed yet. This exercises the started-chan close-on-bind-fail
// path: by binding to an obviously bad port (privileged) we get a fast
// bind error before NotifyStartedFunc fires.
ds, c := newTestDnsServer(t)
// Use a port that should fail to bind (negative would be invalid, use a
// host that won't resolve to ensure listenUDP fails quickly).
setDnsConfig(c, "256.256.256.256", "53", true, true)
require.NoError(t, ds.reload(c, true))
done := make(chan struct{})
go func() {
ds.Start()
close(done)
}()
// Give Start a moment to attempt the bind and fail.
select {
case <-done:
// Bind failed and Start returned; Stop should be a no-op.
case <-time.After(time.Second):
t.Fatal("Start did not return after a bad bind")
}
stopped := make(chan struct{})
go func() {
ds.Stop()
close(stopped)
}()
select {
case <-stopped:
case <-time.After(time.Second):
t.Fatal("Stop hung after a failed bind")
}
}
func TestDnsServer_reload_disable_stopsRunningServer(t *testing.T) {
port := freeUDPPort(t)
ds, c := newTestDnsServer(t)
setDnsConfig(c, "127.0.0.1", port, true, true)
require.NoError(t, ds.reload(c, true))
startReturned := make(chan struct{})
go func() {
ds.Start()
close(startReturned)
}()
waitForBind(t, ds)
// Toggle serve_dns off; reload should shut the running server down.
setDnsConfig(c, "127.0.0.1", port, true, false)
require.NoError(t, ds.reload(c, false))
select {
case <-startReturned:
case <-time.After(5 * time.Second):
t.Fatal("Start did not return after reload disabled DNS")
}
assert.False(t, ds.enabled.Load())
}
func freeUDPPort(t *testing.T) string {
t.Helper()
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
require.NoError(t, err)
port := conn.LocalAddr().(*net.UDPAddr).Port
require.NoError(t, conn.Close())
return strconv.Itoa(port)
}
func waitForBind(t *testing.T, ds *dnsServer) {
t.Helper()
waitFor(t, func() bool {
ds.serverMu.Lock()
started := ds.started
ds.serverMu.Unlock()
if started == nil {
return false
}
select {
case <-started:
return true
default:
return false
}
})
}
func waitFor(t *testing.T, cond func() bool) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatal("timed out waiting for condition")
}
+87 -23
View File
@@ -11,7 +11,6 @@ import (
"github.com/google/gopacket" "github.com/google/gopacket"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula" "github.com/slackhq/nebula"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test" "github.com/slackhq/nebula/cert_test"
@@ -749,7 +748,6 @@ func TestStage1RaceRelays2(t *testing.T) {
myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}}) myControl, myVpnIpNet, myUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "me ", "10.128.0.1/24", m{"relay": m{"use_relays": true}})
relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}}) relayControl, relayVpnIpNet, relayUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "relay ", "10.128.0.128/24", m{"relay": m{"am_relay": true}})
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them ", "10.128.0.2/24", m{"relay": m{"use_relays": true}}) theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version1, ca, caKey, "them ", "10.128.0.2/24", m{"relay": m{"use_relays": true}})
l := NewTestLogger()
// Teach my how to get to the relay and that their can be reached via the relay // Teach my how to get to the relay and that their can be reached via the relay
myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr) myControl.InjectLightHouseAddr(relayVpnIpNet[0].Addr(), relayUdpAddr)
@@ -771,49 +769,41 @@ func TestStage1RaceRelays2(t *testing.T) {
theirControl.Start() theirControl.Start()
r.Log("Get a tunnel between me and relay") r.Log("Get a tunnel between me and relay")
l.Info("Get a tunnel between me and relay")
assertTunnel(t, myVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), myControl, relayControl, r) assertTunnel(t, myVpnIpNet[0].Addr(), relayVpnIpNet[0].Addr(), myControl, relayControl, r)
r.Log("Get a tunnel between them and relay") r.Log("Get a tunnel between them and relay")
l.Info("Get a tunnel between them and relay")
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")
l.Info("Trigger a handshake from both them and me via relay to them and me")
myControl.InjectTunUDPPacket(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.InjectTunUDPPacket(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)
r.Log("Wait for a packet from them to me") r.Log("Wait for a packet from them to me; myControl")
l.Info("Wait for a packet from them to me; myControl")
r.RouteForAllUntilTxTun(myControl) r.RouteForAllUntilTxTun(myControl)
l.Info("Wait for a packet from them to me; theirControl") r.Log("Wait for a packet from them to me; theirControl")
r.RouteForAllUntilTxTun(theirControl) r.RouteForAllUntilTxTun(theirControl)
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
l.Info("Assert the tunnel works")
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r) assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
t.Log("Wait until we remove extra tunnels") t.Log("Wait until we remove extra tunnels")
l.Info("Wait until we remove extra tunnels") t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
l.WithFields( len(myControl.GetHostmap().Indexes),
logrus.Fields{ len(theirControl.GetHostmap().Indexes),
"myControl": len(myControl.GetHostmap().Indexes), len(relayControl.GetHostmap().Indexes),
"theirControl": len(theirControl.GetHostmap().Indexes), )
"relayControl": len(relayControl.GetHostmap().Indexes),
}).Info("Waiting for hostinfos to be removed...")
hostInfos := len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes) hostInfos := len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
retries := 60 retries := 60
for hostInfos > 6 && retries > 0 { for hostInfos > 6 && retries > 0 {
hostInfos = len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes) hostInfos = len(myControl.GetHostmap().Indexes) + len(theirControl.GetHostmap().Indexes) + len(relayControl.GetHostmap().Indexes)
l.WithFields( t.Logf("Waiting for hostinfos to be removed... myControl=%d theirControl=%d relayControl=%d",
logrus.Fields{ len(myControl.GetHostmap().Indexes),
"myControl": len(myControl.GetHostmap().Indexes), len(theirControl.GetHostmap().Indexes),
"theirControl": len(theirControl.GetHostmap().Indexes), len(relayControl.GetHostmap().Indexes),
"relayControl": len(relayControl.GetHostmap().Indexes), )
}).Info("Waiting for hostinfos to be removed...")
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r) assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
t.Log("Connection manager hasn't ticked yet") t.Log("Connection manager hasn't ticked yet")
time.Sleep(time.Second) time.Sleep(time.Second)
@@ -821,7 +811,6 @@ func TestStage1RaceRelays2(t *testing.T) {
} }
r.Log("Assert the tunnel works") r.Log("Assert the tunnel works")
l.Info("Assert the tunnel works")
assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r) assertTunnel(t, theirVpnIpNet[0].Addr(), myVpnIpNet[0].Addr(), theirControl, myControl, r)
myControl.Stop() myControl.Stop()
@@ -1369,6 +1358,81 @@ func TestV2NonPrimaryWithOffNetLighthouse(t *testing.T) {
theirControl.Stop() theirControl.Stop()
} }
func TestLighthouseUpdateOnReload(t *testing.T) {
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
// Create the lighthouse
lhControl, lhVpnIpNet, lhUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "lh", "10.128.0.1/24", m{"lighthouse": m{"am_lighthouse": true}})
// Create a client with NO lighthouse configured and a long update interval.
// The initial SendUpdate at startup will be a no-op since no lighthouses are known.
myControl, myVpnIpNet, _, myConfig := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.2/24", m{
"lighthouse": m{
"interval": 600,
"local_allow_list": m{
"10.0.0.0/24": true,
"::/0": false,
},
},
})
r := router.NewR(t, lhControl, myControl)
defer r.RenderFlow()
lhControl.Start()
myControl.Start()
// Drain any startup packets (there should be none meaningful)
r.FlushAll()
// Verify lighthouse has no knowledge of the client
assert.Nil(t, lhControl.QueryLighthouse(myVpnIpNet[0].Addr()))
// Build a new config that adds the lighthouse
newSettings := make(m)
for k, v := range myConfig.Settings {
newSettings[k] = v
}
newSettings["static_host_map"] = m{
lhVpnIpNet[0].Addr().String(): []any{lhUdpAddr.String()},
}
newSettings["lighthouse"] = m{
"hosts": []any{lhVpnIpNet[0].Addr().String()},
"interval": 600,
"local_allow_list": m{
"10.0.0.0/24": true,
"::/0": false,
},
}
newCfg, err := yaml.Marshal(newSettings)
require.NoError(t, err)
// Reload the config. The lighthouse.hosts change triggers TriggerUpdate,
// which wakes the update worker. It calls SendUpdate, initiating a
// handshake to the new lighthouse and caching the HostUpdateNotification.
require.NoError(t, myConfig.ReloadConfigString(string(newCfg)))
// Route until the lighthouse receives the HostUpdateNotification.
// This covers: handshake stage 1, stage 2, then the cached update.
done := make(chan struct{})
go func() {
r.RouteForAllUntilAfterMsgTypeTo(lhControl, header.LightHouse, 0)
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for lighthouse update after config reload")
}
// Verify lighthouse now has the client's addresses
assert.NotNil(t, lhControl.QueryLighthouse(myVpnIpNet[0].Addr()))
r.RenderHostmaps("Final hostmaps", lhControl, myControl)
lhControl.Stop()
myControl.Stop()
}
func TestGoodHandshakeUnsafeDest(t *testing.T) { func TestGoodHandshakeUnsafeDest(t *testing.T) {
unsafePrefix := "192.168.6.0/24" unsafePrefix := "192.168.6.0/24"
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{}) ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
+26 -18
View File
@@ -4,7 +4,6 @@
package e2e package e2e
import ( import (
"fmt"
"io" "io"
"net/netip" "net/netip"
"os" "os"
@@ -12,15 +11,18 @@ import (
"testing" "testing"
"time" "time"
"log/slog"
"dario.cat/mergo" "dario.cat/mergo"
"github.com/google/gopacket" "github.com/google/gopacket"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula" "github.com/slackhq/nebula"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test" "github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/e2e/router" "github.com/slackhq/nebula/e2e/router"
"github.com/slackhq/nebula/logging"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3" "go.yaml.in/yaml/v3"
@@ -132,8 +134,7 @@ func newSimpleServerWithUdpAndUnsafeNetworks(v cert.Version, caCrt cert.Certific
"port": udpAddr.Port(), "port": udpAddr.Port(),
}, },
"logging": m{ "logging": m{
"timestamp_format": fmt.Sprintf("%v 15:04:05.000000", name), "level": testLogLevelName(),
"level": l.Level.String(),
}, },
"timers": m{ "timers": m{
"pending_deletion_interval": 2, "pending_deletion_interval": 2,
@@ -234,8 +235,7 @@ func newServer(caCrt []cert.Certificate, certs []cert.Certificate, key []byte, o
"port": udpAddr.Port(), "port": udpAddr.Port(),
}, },
"logging": m{ "logging": m{
"timestamp_format": fmt.Sprintf("%v 15:04:05.000000", certs[0].Name()), "level": testLogLevelName(),
"level": l.Level.String(),
}, },
"timers": m{ "timers": m{
"pending_deletion_interval": 2, "pending_deletion_interval": 2,
@@ -379,24 +379,32 @@ func getAddrs(ns []netip.Prefix) []netip.Addr {
return a return a
} }
func NewTestLogger() *logrus.Logger { func NewTestLogger() *slog.Logger {
l := logrus.New()
v := os.Getenv("TEST_LOGS") v := os.Getenv("TEST_LOGS")
if v == "" { if v == "" {
l.SetOutput(io.Discard) return slog.New(slog.NewTextHandler(io.Discard, nil))
l.SetLevel(logrus.PanicLevel)
return l
} }
level := slog.LevelInfo
switch v { switch v {
case "2": case "2":
l.SetLevel(logrus.DebugLevel) level = slog.LevelDebug
case "3": case "3":
l.SetLevel(logrus.TraceLevel) level = logging.LevelTrace
default:
l.SetLevel(logrus.InfoLevel)
} }
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level}))
return l }
// testLogLevelName returns the level name string accepted by logging.ApplyConfig
// for the current TEST_LOGS setting. Kept in sync with NewTestLogger.
func testLogLevelName() string {
switch os.Getenv("TEST_LOGS") {
case "2":
return "debug"
case "3":
return "trace"
case "":
return "info"
}
return "info"
} }
+13 -14
View File
@@ -292,24 +292,21 @@ tun:
# Configure logging level # Configure logging level
logging: logging:
# panic, fatal, error, warning, info, or debug. Default is info and is reloadable. # trace, debug, info, warn, or error. Default is info and is reloadable.
#NOTE: Debug mode can log remotely controlled/untrusted data which can quickly fill a disk in some # fatal and panic are accepted for backwards compatibility and map to error.
# scenarios. Debug logging is also CPU intensive and will decrease performance overall. #NOTE: Debug and trace modes can log remotely controlled/untrusted data which can quickly fill a disk in some
# Only enable debug logging while actively investigating an issue. # scenarios. Debug and trace logging are also CPU intensive and will decrease performance overall.
# Only enable debug or trace logging while actively investigating an issue.
level: info level: info
# json or text formats currently available. Default is text # json or text formats currently available. Default is text.
format: text format: text
# Disable timestamp logging. useful when output is redirected to logging system that already adds timestamps. Default is false # Disable timestamp logging. Useful when output is redirected to a logging system that already adds timestamps. Default is false.
#disable_timestamp: true #disable_timestamp: true
# timestamp format is specified in Go time format, see: # Timestamps use RFC3339Nano ("2006-01-02T15:04:05.999999999Z07:00") and are not configurable.
# https://golang.org/pkg/time/#pkg-constants
# default when `format: json`: "2006-01-02T15:04:05Z07:00" (RFC3339)
# default when `format: text`:
# when TTY attached: seconds since beginning of execution
# otherwise: "2006-01-02T15:04:05Z07:00" (RFC3339)
# As an example, to log as RFC3339 with millisecond precision, set to:
#timestamp_format: "2006-01-02T15:04:05.000Z07:00"
# The stats section is reloadable. A HUP may change the backend, toggle stats
# on or off, switch the listen/host address, or pick up new DNS for the
# configured graphite host.
#stats: #stats:
#type: graphite #type: graphite
#prefix: nebula #prefix: nebula
@@ -327,10 +324,12 @@ logging:
# enables counter metrics for meta packets # enables counter metrics for meta packets
# e.g.: `messages.tx.handshake` # e.g.: `messages.tx.handshake`
# NOTE: `message.{tx,rx}.recv_error` is always emitted # NOTE: `message.{tx,rx}.recv_error` is always emitted
# Not reloadable.
#message_metrics: false #message_metrics: false
# enables detailed counter metrics for lighthouse packets # enables detailed counter metrics for lighthouse packets
# e.g.: `lighthouse.rx.HostQuery` # e.g.: `lighthouse.rx.HostQuery`
# Not reloadable.
#lighthouse_metrics: false #lighthouse_metrics: false
# Handshake Manager Settings # Handshake Manager Settings
+2 -3
View File
@@ -7,9 +7,9 @@ import (
"net" "net"
"os" "os"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula" "github.com/slackhq/nebula"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/logging"
"github.com/slackhq/nebula/overlay" "github.com/slackhq/nebula/overlay"
"github.com/slackhq/nebula/service" "github.com/slackhq/nebula/service"
) )
@@ -64,8 +64,7 @@ pki:
return err return err
} }
logger := logrus.New() logger := logging.NewLogger(os.Stdout)
logger.Out = os.Stdout
ctrl, err := nebula.Main(&cfg, false, "custom-app", logger, overlay.NewUserDeviceFromConfig) ctrl, err := nebula.Main(&cfg, false, "custom-app", logger, overlay.NewUserDeviceFromConfig)
if err != nil { if err != nil {
+37 -28
View File
@@ -1,11 +1,13 @@
package nebula package nebula
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"hash/fnv" "hash/fnv"
"log/slog"
"net/netip" "net/netip"
"reflect" "reflect"
"slices" "slices"
@@ -16,7 +18,6 @@ import (
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
@@ -67,7 +68,7 @@ type Firewall struct {
incomingMetrics firewallMetrics incomingMetrics firewallMetrics
outgoingMetrics firewallMetrics outgoingMetrics firewallMetrics
l *logrus.Logger l *slog.Logger
} }
type firewallMetrics struct { type firewallMetrics struct {
@@ -131,7 +132,7 @@ type firewallLocalCIDR struct {
// NewFirewall creates a new Firewall object. A TimerWheel is created for you from the provided timeouts. // NewFirewall creates a new Firewall object. A TimerWheel is created for you from the provided timeouts.
// The certificate provided should be the highest version loaded in memory. // The certificate provided should be the highest version loaded in memory.
func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c cert.Certificate) *Firewall { func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c cert.Certificate) *Firewall {
//TODO: error on 0 duration //TODO: error on 0 duration
var tmin, tmax time.Duration var tmin, tmax time.Duration
@@ -191,7 +192,7 @@ func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.D
} }
} }
func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firewall, error) { func NewFirewallFromConfig(l *slog.Logger, cs *CertState, c *config.C) (*Firewall, error) {
certificate := cs.getCertificate(cert.Version2) certificate := cs.getCertificate(cert.Version2)
if certificate == nil { if certificate == nil {
certificate = cs.getCertificate(cert.Version1) certificate = cs.getCertificate(cert.Version1)
@@ -219,7 +220,7 @@ func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firew
case "drop": case "drop":
fw.InSendReject = false fw.InSendReject = false
default: default:
l.WithField("action", inboundAction).Warn("invalid firewall.inbound_action, defaulting to `drop`") l.Warn("invalid firewall.inbound_action, defaulting to `drop`", "action", inboundAction)
fw.InSendReject = false fw.InSendReject = false
} }
@@ -230,7 +231,7 @@ func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firew
case "drop": case "drop":
fw.OutSendReject = false fw.OutSendReject = false
default: default:
l.WithField("action", outboundAction).Warn("invalid firewall.outbound_action, defaulting to `drop`") l.Warn("invalid firewall.outbound_action, defaulting to `drop`", "action", outboundAction)
fw.OutSendReject = false fw.OutSendReject = false
} }
@@ -268,7 +269,7 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
case firewall.ProtoICMP, firewall.ProtoICMPv6: case firewall.ProtoICMP, firewall.ProtoICMPv6:
//ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided //ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided
if startPort != firewall.PortAny { if startPort != firewall.PortAny {
f.l.WithField("startPort", startPort).Warn("ignoring port specification for ICMP firewall rule") f.l.Warn("ignoring port specification for ICMP firewall rule", "startPort", startPort)
} }
startPort = firewall.PortAny startPort = firewall.PortAny
endPort = firewall.PortAny endPort = firewall.PortAny
@@ -290,8 +291,9 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
if !incoming { if !incoming {
direction = "outgoing" direction = "outgoing"
} }
f.l.WithField("firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha}). f.l.Info("Firewall rule added",
Info("Firewall rule added") "firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha},
)
return fp.addRule(f, startPort, endPort, groups, host, cidr, localCidr, caName, caSha) return fp.addRule(f, startPort, endPort, groups, host, cidr, localCidr, caName, caSha)
} }
@@ -314,7 +316,7 @@ func (f *Firewall) GetRuleHashes() string {
return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10) return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10)
} }
func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw FirewallInterface) error { func AddFirewallRulesFromConfig(l *slog.Logger, inbound bool, c *config.C, fw FirewallInterface) error {
var table string var table string
if inbound { if inbound {
table = "firewall.inbound" table = "firewall.inbound"
@@ -372,7 +374,7 @@ func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw
startPort = firewall.PortAny startPort = firewall.PortAny
endPort = firewall.PortAny endPort = firewall.PortAny
if sPort != "" { if sPort != "" {
l.WithField("port", sPort).Warn("ignoring port specification for ICMP firewall rule") l.Warn("ignoring port specification for ICMP firewall rule", "port", sPort)
} }
default: default:
return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto) return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
@@ -396,7 +398,11 @@ func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw
} }
if warning := r.sanity(); warning != nil { if warning := r.sanity(); warning != nil {
l.Warnf("%s rule #%v; %s", table, i, warning) l.Warn("firewall rule sanity check",
"table", table,
"rule", i,
"warning", warning,
)
} }
err = fw.AddRule(inbound, proto, startPort, endPort, r.Groups, r.Host, r.Cidr, r.LocalCidr, r.CAName, r.CASha) err = fw.AddRule(inbound, proto, startPort, endPort, r.Groups, r.Host, r.Cidr, r.LocalCidr, r.CAName, r.CASha)
@@ -528,26 +534,26 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
// We now know which firewall table to check against // We now know which firewall table to check against
if !table.match(fp, c.incoming, h.ConnectionState.peerCert, caPool) { if !table.match(fp, c.incoming, h.ConnectionState.peerCert, caPool) {
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
h.logger(f.l). h.logger(f.l).Debug("dropping old conntrack entry, does not match new ruleset",
WithField("fwPacket", fp). "fwPacket", fp,
WithField("incoming", c.incoming). "incoming", c.incoming,
WithField("rulesVersion", f.rulesVersion). "rulesVersion", f.rulesVersion,
WithField("oldRulesVersion", c.rulesVersion). "oldRulesVersion", c.rulesVersion,
Debugln("dropping old conntrack entry, does not match new ruleset") )
} }
delete(conntrack.Conns, fp) delete(conntrack.Conns, fp)
conntrack.Unlock() conntrack.Unlock()
return false return false
} }
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
h.logger(f.l). h.logger(f.l).Debug("keeping old conntrack entry, does match new ruleset",
WithField("fwPacket", fp). "fwPacket", fp,
WithField("incoming", c.incoming). "incoming", c.incoming,
WithField("rulesVersion", f.rulesVersion). "rulesVersion", f.rulesVersion,
WithField("oldRulesVersion", c.rulesVersion). "oldRulesVersion", c.rulesVersion,
Debugln("keeping old conntrack entry, does match new ruleset") )
} }
c.rulesVersion = f.rulesVersion c.rulesVersion = f.rulesVersion
@@ -935,7 +941,7 @@ type rule struct {
CASha string CASha string
} }
func convertRule(l *logrus.Logger, p any, table string, i int) (rule, error) { func convertRule(l *slog.Logger, p any, table string, i int) (rule, error) {
r := rule{} r := rule{}
m, ok := p.(map[string]any) m, ok := p.(map[string]any)
@@ -966,7 +972,10 @@ func convertRule(l *logrus.Logger, p any, table string, i int) (rule, error) {
return r, errors.New("group should contain a single value, an array with more than one entry was provided") return r, errors.New("group should contain a single value, an array with more than one entry was provided")
} }
l.Warnf("%s rule #%v; group was an array with a single value, converting to simple value", table, i) l.Warn("group was an array with a single value, converting to simple value",
"table", table,
"rule", i,
)
m["group"] = v[0] m["group"] = v[0]
} }
+18 -10
View File
@@ -1,10 +1,10 @@
package firewall package firewall
import ( import (
"context"
"log/slog"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/sirupsen/logrus"
) )
// ConntrackCache is used as a local routine cache to know if a given flow // ConntrackCache is used as a local routine cache to know if a given flow
@@ -15,41 +15,49 @@ type ConntrackCacheTicker struct {
cacheV uint64 cacheV uint64
cacheTick atomic.Uint64 cacheTick atomic.Uint64
l *slog.Logger
cache ConntrackCache cache ConntrackCache
} }
func NewConntrackCacheTicker(d time.Duration) *ConntrackCacheTicker { func NewConntrackCacheTicker(ctx context.Context, l *slog.Logger, d time.Duration) *ConntrackCacheTicker {
if d == 0 { if d == 0 {
return nil return nil
} }
c := &ConntrackCacheTicker{ c := &ConntrackCacheTicker{
l: l,
cache: ConntrackCache{}, cache: ConntrackCache{},
} }
go c.tick(d) go c.tick(ctx, d)
return c return c
} }
func (c *ConntrackCacheTicker) tick(d time.Duration) { func (c *ConntrackCacheTicker) tick(ctx context.Context, d time.Duration) {
t := time.NewTicker(d)
defer t.Stop()
for { for {
time.Sleep(d) select {
c.cacheTick.Add(1) case <-ctx.Done():
return
case <-t.C:
c.cacheTick.Add(1)
}
} }
} }
// Get checks if the cache ticker has moved to the next version before returning // Get checks if the cache ticker has moved to the next version before returning
// the map. If it has moved, we reset the map. // the map. If it has moved, we reset the map.
func (c *ConntrackCacheTicker) Get(l *logrus.Logger) ConntrackCache { func (c *ConntrackCacheTicker) Get() ConntrackCache {
if c == nil { if c == nil {
return nil return nil
} }
if tick := c.cacheTick.Load(); tick != c.cacheV { if tick := c.cacheTick.Load(); tick != c.cacheV {
c.cacheV = tick c.cacheV = tick
if ll := len(c.cache); ll > 0 { if ll := len(c.cache); ll > 0 {
if l.Level == logrus.DebugLevel { if c.l.Enabled(context.Background(), slog.LevelDebug) {
l.WithField("len", ll).Debug("resetting conntrack cache") c.l.Debug("resetting conntrack cache", "len", ll)
} }
c.cache = make(ConntrackCache, ll) c.cache = make(ConntrackCache, ll)
} }
+69
View File
@@ -0,0 +1,69 @@
package firewall
import (
"bytes"
"log/slog"
"strings"
"testing"
"github.com/slackhq/nebula/test"
"github.com/stretchr/testify/assert"
)
// The tests below pin the log format produced by ConntrackCacheTicker.Get
// so changes cannot silently break what operators are grepping for. The
// ticker's internal state (cache + cacheTick) is poked directly to avoid
// racing a goroutine-driven tick in tests.
func newFixedTicker(t *testing.T, l *slog.Logger, cacheLen int) *ConntrackCacheTicker {
t.Helper()
c := &ConntrackCacheTicker{
l: l,
cache: make(ConntrackCache, cacheLen),
}
for i := 0; i < cacheLen; i++ {
c.cache[Packet{LocalPort: uint16(i) + 1}] = struct{}{}
}
c.cacheTick.Store(1) // cacheV starts at 0, so Get() takes the reset path
return c
}
func TestConntrackCacheTicker_Get_TextFormat(t *testing.T) {
buf := &bytes.Buffer{}
l := test.NewLoggerWithOutputAndLevel(buf, slog.LevelDebug)
c := newFixedTicker(t, l, 3)
c.Get()
assert.Equal(t, "level=DEBUG msg=\"resetting conntrack cache\" len=3\n", buf.String())
}
func TestConntrackCacheTicker_Get_JSONFormat(t *testing.T) {
buf := &bytes.Buffer{}
l := test.NewJSONLoggerWithOutput(buf, slog.LevelDebug)
c := newFixedTicker(t, l, 2)
c.Get()
assert.JSONEq(t, `{"level":"DEBUG","msg":"resetting conntrack cache","len":2}`, strings.TrimSpace(buf.String()))
}
func TestConntrackCacheTicker_Get_QuietBelowDebug(t *testing.T) {
buf := &bytes.Buffer{}
l := test.NewLoggerWithOutputAndLevel(buf, slog.LevelInfo)
c := newFixedTicker(t, l, 5)
c.Get()
assert.Empty(t, buf.String())
}
func TestConntrackCacheTicker_Get_QuietWhenCacheEmpty(t *testing.T) {
buf := &bytes.Buffer{}
l := test.NewLoggerWithOutputAndLevel(buf, slog.LevelDebug)
c := newFixedTicker(t, l, 0)
c.Get()
assert.Empty(t, buf.String())
}
+45 -55
View File
@@ -3,13 +3,13 @@ package nebula
import ( import (
"bytes" "bytes"
"errors" "errors"
"log/slog"
"math" "math"
"net/netip" "net/netip"
"testing" "testing"
"time" "time"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
@@ -58,9 +58,8 @@ func TestNewFirewall(t *testing.T) {
} }
func TestFirewall_AddRule(t *testing.T) { func TestFirewall_AddRule(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
c := &dummyCert{} c := &dummyCert{}
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c) fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c)
@@ -177,9 +176,8 @@ func TestFirewall_AddRule(t *testing.T) {
} }
func TestFirewall_Drop(t *testing.T) { func TestFirewall_Drop(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8")) myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
p := firewall.Packet{ p := firewall.Packet{
@@ -254,9 +252,8 @@ func TestFirewall_Drop(t *testing.T) {
} }
func TestFirewall_DropV6(t *testing.T) { func TestFirewall_DropV6(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::/7")) myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::/7"))
@@ -485,9 +482,8 @@ func BenchmarkFirewallTable_match(b *testing.B) {
} }
func TestFirewall_Drop2(t *testing.T) { func TestFirewall_Drop2(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8")) myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
@@ -544,9 +540,8 @@ func TestFirewall_Drop2(t *testing.T) {
} }
func TestFirewall_Drop3(t *testing.T) { func TestFirewall_Drop3(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8")) myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
@@ -633,9 +628,8 @@ func TestFirewall_Drop3(t *testing.T) {
} }
func TestFirewall_Drop3V6(t *testing.T) { func TestFirewall_Drop3V6(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::/7")) myVpnNetworksTable.Insert(netip.MustParsePrefix("fd00::/7"))
@@ -671,9 +665,8 @@ func TestFirewall_Drop3V6(t *testing.T) {
} }
func TestFirewall_DropConntrackReload(t *testing.T) { func TestFirewall_DropConntrackReload(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8")) myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
@@ -736,9 +729,8 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
} }
func TestFirewall_ICMPPortBehavior(t *testing.T) { func TestFirewall_ICMPPortBehavior(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8")) myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
@@ -880,9 +872,8 @@ func TestFirewall_ICMPPortBehavior(t *testing.T) {
} }
func TestFirewall_DropIPSpoofing(t *testing.T) { func TestFirewall_DropIPSpoofing(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24")) myVpnNetworksTable.Insert(netip.MustParsePrefix("192.0.2.1/24"))
@@ -1045,25 +1036,25 @@ func TestNewFirewallFromConfig(t *testing.T) {
cs, err := newCertState(cert.Version2, nil, c, false, cert.Curve_CURVE25519, nil) cs, err := newCertState(cert.Version2, nil, c, false, cert.Curve_CURVE25519, nil)
require.NoError(t, err) require.NoError(t, err)
conf := config.NewC(l) conf := config.NewC(test.NewLogger())
conf.Settings["firewall"] = map[string]any{"outbound": "asdf"} conf.Settings["firewall"] = map[string]any{"outbound": "asdf"}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound failed to parse, should be an array of rules") require.EqualError(t, err, "firewall.outbound failed to parse, should be an array of rules")
// Test both port and code // Test both port and code
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "code": "2"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "code": "2"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; only one of port or code should be provided") require.EqualError(t, err, "firewall.outbound rule #0; only one of port or code should be provided")
// Test missing host, group, cidr, ca_name and ca_sha // Test missing host, group, cidr, ca_name and ca_sha
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; at least one of host, group, cidr, local_cidr, ca_name, or ca_sha must be provided") require.EqualError(t, err, "firewall.outbound rule #0; at least one of host, group, cidr, local_cidr, ca_name, or ca_sha must be provided")
// Test code/port error // Test code/port error
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "a", "host": "testh", "proto": "any"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "a", "host": "testh", "proto": "any"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; code was not a number; `a`") require.EqualError(t, err, "firewall.outbound rule #0; code was not a number; `a`")
@@ -1073,25 +1064,25 @@ func TestNewFirewallFromConfig(t *testing.T) {
require.EqualError(t, err, "firewall.outbound rule #0; port was not a number; `a`") require.EqualError(t, err, "firewall.outbound rule #0; port was not a number; `a`")
// Test proto error // Test proto error
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "host": "testh"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "host": "testh"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; proto was not understood; ``") require.EqualError(t, err, "firewall.outbound rule #0; proto was not understood; ``")
// Test cidr parse error // Test cidr parse error
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "cidr": "testh", "proto": "any"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "cidr": "testh", "proto": "any"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; cidr did not parse; netip.ParsePrefix(\"testh\"): no '/'") require.EqualError(t, err, "firewall.outbound rule #0; cidr did not parse; netip.ParsePrefix(\"testh\"): no '/'")
// Test local_cidr parse error // Test local_cidr parse error
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "local_cidr": "testh", "proto": "any"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "1", "local_cidr": "testh", "proto": "any"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; local_cidr did not parse; netip.ParsePrefix(\"testh\"): no '/'") require.EqualError(t, err, "firewall.outbound rule #0; local_cidr did not parse; netip.ParsePrefix(\"testh\"): no '/'")
// Test both group and groups // Test both group and groups
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "group": "a", "groups": []string{"b", "c"}}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "group": "a", "groups": []string{"b", "c"}}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.inbound rule #0; only one of group or groups should be defined, both provided") require.EqualError(t, err, "firewall.inbound rule #0; only one of group or groups should be defined, both provided")
@@ -1100,35 +1091,35 @@ func TestNewFirewallFromConfig(t *testing.T) {
func TestAddFirewallRulesFromConfig(t *testing.T) { func TestAddFirewallRulesFromConfig(t *testing.T) {
l := test.NewLogger() l := test.NewLogger()
// Test adding tcp rule // Test adding tcp rule
conf := config.NewC(l) conf := config.NewC(test.NewLogger())
mf := &mockFirewall{} mf := &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "tcp", "host": "a"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "tcp", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoTCP, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoTCP, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
// Test adding udp rule // Test adding udp rule
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "udp", "host": "a"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "udp", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoUDP, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoUDP, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
// Test adding icmp rule // Test adding icmp rule
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "icmp", "host": "a"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "icmp", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
// Test adding icmp rule no port // Test adding icmp rule no port
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"proto": "icmp", "host": "a"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"proto": "icmp", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
// Test adding any rule // Test adding any rule
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "host": "a"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
@@ -1136,14 +1127,14 @@ func TestAddFirewallRulesFromConfig(t *testing.T) {
// Test adding rule with cidr // Test adding rule with cidr
cidr := netip.MustParsePrefix("10.0.0.0/8") cidr := netip.MustParsePrefix("10.0.0.0/8")
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": cidr.String()}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": cidr.String()}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: cidr.String(), localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: cidr.String(), localIp: ""}, mf.lastCall)
// Test adding rule with local_cidr // Test adding rule with local_cidr
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": cidr.String()}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": cidr.String()}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
@@ -1151,82 +1142,82 @@ func TestAddFirewallRulesFromConfig(t *testing.T) {
// Test adding rule with cidr ipv6 // Test adding rule with cidr ipv6
cidr6 := netip.MustParsePrefix("fd00::/8") cidr6 := netip.MustParsePrefix("fd00::/8")
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": cidr6.String()}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": cidr6.String()}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: cidr6.String(), localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: cidr6.String(), localIp: ""}, mf.lastCall)
// Test adding rule with any cidr // Test adding rule with any cidr
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": "any"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": "any"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "any", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "any", localIp: ""}, mf.lastCall)
// Test adding rule with junk cidr // Test adding rule with junk cidr
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": "junk/junk"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "cidr": "junk/junk"}}}
require.EqualError(t, AddFirewallRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP") require.EqualError(t, AddFirewallRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP")
// Test adding rule with local_cidr ipv6 // Test adding rule with local_cidr ipv6
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": cidr6.String()}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": cidr6.String()}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: cidr6.String()}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: cidr6.String()}, mf.lastCall)
// Test adding rule with any local_cidr // Test adding rule with any local_cidr
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": "any"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": "any"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, localIp: "any"}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, localIp: "any"}, mf.lastCall)
// Test adding rule with junk local_cidr // Test adding rule with junk local_cidr
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": "junk/junk"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "local_cidr": "junk/junk"}}}
require.EqualError(t, AddFirewallRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; local_cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP") require.EqualError(t, AddFirewallRulesFromConfig(l, true, conf, mf), "firewall.inbound rule #0; local_cidr did not parse; netip.ParsePrefix(\"junk/junk\"): ParseAddr(\"junk\"): unable to parse IP")
// Test adding rule with ca_sha // Test adding rule with ca_sha
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "ca_sha": "12312313123"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "ca_sha": "12312313123"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: "", caSha: "12312313123"}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: "", caSha: "12312313123"}, mf.lastCall)
// Test adding rule with ca_name // Test adding rule with ca_name
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "ca_name": "root01"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "ca_name": "root01"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: "", caName: "root01"}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: nil, ip: "", localIp: "", caName: "root01"}, mf.lastCall)
// Test single group // Test single group
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "group": "a"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "group": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a"}, ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a"}, ip: "", localIp: ""}, mf.lastCall)
// Test single groups // Test single groups
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "groups": "a"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "groups": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a"}, ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a"}, ip: "", localIp: ""}, mf.lastCall)
// Test multiple AND groups // Test multiple AND groups
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "groups": []string{"a", "b"}}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "groups": []string{"a", "b"}}}}
require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, true, conf, mf))
assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a", "b"}, ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: true, proto: firewall.ProtoAny, startPort: 1, endPort: 1, groups: []string{"a", "b"}, ip: "", localIp: ""}, mf.lastCall)
// Test Add error // Test Add error
conf = config.NewC(l) conf = config.NewC(test.NewLogger())
mf = &mockFirewall{} mf = &mockFirewall{}
mf.nextCallReturn = errors.New("test error") mf.nextCallReturn = errors.New("test error")
conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "host": "a"}}} conf.Settings["firewall"] = map[string]any{"inbound": []any{map[string]any{"port": "1", "proto": "any", "host": "a"}}}
@@ -1234,9 +1225,8 @@ func TestAddFirewallRulesFromConfig(t *testing.T) {
} }
func TestFirewall_convertRule(t *testing.T) { func TestFirewall_convertRule(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
// Ensure group array of 1 is converted and a warning is printed // Ensure group array of 1 is converted and a warning is printed
c := map[string]any{ c := map[string]any{
@@ -1244,7 +1234,9 @@ func TestFirewall_convertRule(t *testing.T) {
} }
r, err := convertRule(l, c, "test", 1) r, err := convertRule(l, c, "test", 1)
assert.Contains(t, ob.String(), "test rule #1; group was an array with a single value, converting to simple value") assert.Contains(t, ob.String(), "group was an array with a single value, converting to simple value")
assert.Contains(t, ob.String(), "table=test")
assert.Contains(t, ob.String(), "rule=1")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []string{"group1"}, r.Groups) assert.Equal(t, []string{"group1"}, r.Groups)
@@ -1270,9 +1262,8 @@ func TestFirewall_convertRule(t *testing.T) {
} }
func TestFirewall_convertRuleSanity(t *testing.T) { func TestFirewall_convertRuleSanity(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
noWarningPlease := []map[string]any{ noWarningPlease := []map[string]any{
{"group": "group1"}, {"group": "group1"},
@@ -1386,7 +1377,7 @@ type testsetup struct {
fw *Firewall fw *Firewall
} }
func newSetup(t *testing.T, l *logrus.Logger, myPrefixes ...netip.Prefix) testsetup { func newSetup(t *testing.T, l *slog.Logger, myPrefixes ...netip.Prefix) testsetup {
c := dummyCert{ c := dummyCert{
name: "me", name: "me",
networks: myPrefixes, networks: myPrefixes,
@@ -1397,7 +1388,7 @@ func newSetup(t *testing.T, l *logrus.Logger, myPrefixes ...netip.Prefix) testse
return newSetupFromCert(t, l, c) return newSetupFromCert(t, l, c)
} }
func newSetupFromCert(t *testing.T, l *logrus.Logger, c dummyCert) testsetup { func newSetupFromCert(t *testing.T, l *slog.Logger, c dummyCert) testsetup {
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
for _, prefix := range c.Networks() { for _, prefix := range c.Networks() {
myVpnNetworksTable.Insert(prefix) myVpnNetworksTable.Insert(prefix)
@@ -1414,9 +1405,8 @@ func newSetupFromCert(t *testing.T, l *logrus.Logger, c dummyCert) testsetup {
func TestFirewall_Drop_EnforceIPMatch(t *testing.T) { func TestFirewall_Drop_EnforceIPMatch(t *testing.T) {
t.Parallel() t.Parallel()
l := test.NewLogger()
ob := &bytes.Buffer{} ob := &bytes.Buffer{}
l.SetOutput(ob) l := test.NewLoggerWithOutput(ob)
myPrefix := netip.MustParsePrefix("1.1.1.1/8") myPrefix := netip.MustParsePrefix("1.1.1.1/8")
// for now, it's okay that these are all "incoming", the logic this test tries to check doesn't care about in/out // for now, it's okay that these are all "incoming", the logic this test tries to check doesn't care about in/out
-1
View File
@@ -18,7 +18,6 @@ require (
github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f github.com/nbrownus/go-metrics-prometheus v0.0.0-20210712211119-974a6260965f
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
github.com/sirupsen/logrus v1.9.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
-2
View File
@@ -133,8 +133,6 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw=
+343 -208
View File
@@ -2,11 +2,12 @@ package nebula
import ( import (
"bytes" "bytes"
"context"
"log/slog"
"net/netip" "net/netip"
"time" "time"
"github.com/flynn/noise" "github.com/flynn/noise"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
) )
@@ -18,8 +19,11 @@ import (
func ixHandshakeStage0(f *Interface, hh *HandshakeHostInfo) bool { func ixHandshakeStage0(f *Interface, hh *HandshakeHostInfo) bool {
err := f.handshakeManager.allocateIndex(hh) err := f.handshakeManager.allocateIndex(hh)
if err != nil { if err != nil {
f.l.WithError(err).WithField("vpnAddrs", hh.hostinfo.vpnAddrs). f.l.Error("Failed to generate index",
WithField("handshake", m{"stage": 0, "style": "ix_psk0"}).Error("Failed to generate index") "error", err,
"vpnAddrs", hh.hostinfo.vpnAddrs,
"handshake", m{"stage": 0, "style": "ix_psk0"},
)
return false return false
} }
@@ -39,28 +43,32 @@ func ixHandshakeStage0(f *Interface, hh *HandshakeHostInfo) bool {
crt := cs.getCertificate(v) crt := cs.getCertificate(v)
if crt == nil { if crt == nil {
f.l.WithField("vpnAddrs", hh.hostinfo.vpnAddrs). f.l.Error("Unable to handshake with host because no certificate is available",
WithField("handshake", m{"stage": 0, "style": "ix_psk0"}). "vpnAddrs", hh.hostinfo.vpnAddrs,
WithField("certVersion", v). "handshake", m{"stage": 0, "style": "ix_psk0"},
Error("Unable to handshake with host because no certificate is available") "certVersion", v,
)
return false return false
} }
crtHs := cs.getHandshakeBytes(v) crtHs := cs.getHandshakeBytes(v)
if crtHs == nil { if crtHs == nil {
f.l.WithField("vpnAddrs", hh.hostinfo.vpnAddrs). f.l.Error("Unable to handshake with host because no certificate handshake bytes is available",
WithField("handshake", m{"stage": 0, "style": "ix_psk0"}). "vpnAddrs", hh.hostinfo.vpnAddrs,
WithField("certVersion", v). "handshake", m{"stage": 0, "style": "ix_psk0"},
Error("Unable to handshake with host because no certificate handshake bytes is available") "certVersion", v,
)
return false return false
} }
ci, err := NewConnectionState(f.l, cs, crt, true, noise.HandshakeIX) ci, err := NewConnectionState(cs, crt, true, noise.HandshakeIX)
if err != nil { if err != nil {
f.l.WithError(err).WithField("vpnAddrs", hh.hostinfo.vpnAddrs). f.l.Error("Failed to create connection state",
WithField("handshake", m{"stage": 0, "style": "ix_psk0"}). "error", err,
WithField("certVersion", v). "vpnAddrs", hh.hostinfo.vpnAddrs,
Error("Failed to create connection state") "handshake", m{"stage": 0, "style": "ix_psk0"},
"certVersion", v,
)
return false return false
} }
hh.hostinfo.ConnectionState = ci hh.hostinfo.ConnectionState = ci
@@ -76,9 +84,12 @@ func ixHandshakeStage0(f *Interface, hh *HandshakeHostInfo) bool {
hsBytes, err := hs.Marshal() hsBytes, err := hs.Marshal()
if err != nil { if err != nil {
f.l.WithError(err).WithField("vpnAddrs", hh.hostinfo.vpnAddrs). f.l.Error("Failed to marshal handshake message",
WithField("certVersion", v). "error", err,
WithField("handshake", m{"stage": 0, "style": "ix_psk0"}).Error("Failed to marshal handshake message") "vpnAddrs", hh.hostinfo.vpnAddrs,
"certVersion", v,
"handshake", m{"stage": 0, "style": "ix_psk0"},
)
return false return false
} }
@@ -86,8 +97,11 @@ func ixHandshakeStage0(f *Interface, hh *HandshakeHostInfo) bool {
msg, _, _, err := ci.H.WriteMessage(h, hsBytes) msg, _, _, err := ci.H.WriteMessage(h, hsBytes)
if err != nil { if err != nil {
f.l.WithError(err).WithField("vpnAddrs", hh.hostinfo.vpnAddrs). f.l.Error("Failed to call noise.WriteMessage",
WithField("handshake", m{"stage": 0, "style": "ix_psk0"}).Error("Failed to call noise.WriteMessage") "error", err,
"vpnAddrs", hh.hostinfo.vpnAddrs,
"handshake", m{"stage": 0, "style": "ix_psk0"},
)
return false return false
} }
@@ -104,18 +118,21 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
cs := f.pki.getCertState() cs := f.pki.getCertState()
crt := cs.GetDefaultCertificate() crt := cs.GetDefaultCertificate()
if crt == nil { if crt == nil {
f.l.WithField("from", via). f.l.Error("Unable to handshake with host because no certificate is available",
WithField("handshake", m{"stage": 0, "style": "ix_psk0"}). "from", via,
WithField("certVersion", cs.initiatingVersion). "handshake", m{"stage": 0, "style": "ix_psk0"},
Error("Unable to handshake with host because no certificate is available") "certVersion", cs.initiatingVersion,
)
return return
} }
ci, err := NewConnectionState(f.l, cs, crt, false, noise.HandshakeIX) ci, err := NewConnectionState(cs, crt, false, noise.HandshakeIX)
if err != nil { if err != nil {
f.l.WithError(err).WithField("from", via). f.l.Error("Failed to create connection state",
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "error", err,
Error("Failed to create connection state") "from", via,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
@@ -124,26 +141,32 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
msg, _, _, err := ci.H.ReadMessage(nil, packet[header.Len:]) msg, _, _, err := ci.H.ReadMessage(nil, packet[header.Len:])
if err != nil { if err != nil {
f.l.WithError(err).WithField("from", via). f.l.Error("Failed to call noise.ReadMessage",
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "error", err,
Error("Failed to call noise.ReadMessage") "from", via,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
hs := &NebulaHandshake{} hs := &NebulaHandshake{}
err = hs.Unmarshal(msg) err = hs.Unmarshal(msg)
if err != nil || hs.Details == nil { if err != nil || hs.Details == nil {
f.l.WithError(err).WithField("from", via). f.l.Error("Failed unmarshal handshake message",
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "error", err,
Error("Failed unmarshal handshake message") "from", via,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
rc, err := cert.Recombine(cert.Version(hs.Details.CertVersion), hs.Details.Cert, ci.H.PeerStatic(), ci.Curve()) rc, err := cert.Recombine(cert.Version(hs.Details.CertVersion), hs.Details.Cert, ci.H.PeerStatic(), ci.Curve())
if err != nil { if err != nil {
f.l.WithError(err).WithField("from", via). f.l.Info("Handshake did not contain a certificate",
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "error", err,
Info("Handshake did not contain a certificate") "from", via,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
@@ -154,23 +177,30 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
fp = "<error generating certificate fingerprint>" fp = "<error generating certificate fingerprint>"
} }
e := f.l.WithError(err).WithField("from", via). attrs := []slog.Attr{
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). slog.Any("error", err),
WithField("certVpnNetworks", rc.Networks()). slog.Any("from", via),
WithField("certFingerprint", fp) slog.Any("handshake", m{"stage": 1, "style": "ix_psk0"}),
slog.Any("certVpnNetworks", rc.Networks()),
if f.l.Level >= logrus.DebugLevel { slog.String("certFingerprint", fp),
e = e.WithField("cert", rc) }
if f.l.Enabled(context.Background(), slog.LevelDebug) {
attrs = append(attrs, slog.Any("cert", rc))
} }
e.Info("Invalid certificate from host") // LogAttrs is intentional: attrs is a pre-built []slog.Attr slice that
// callers grow conditionally, which has no pair-form equivalent.
//nolint:sloglint
f.l.LogAttrs(context.Background(), slog.LevelInfo, "Invalid certificate from host", attrs...)
return return
} }
if !bytes.Equal(remoteCert.Certificate.PublicKey(), ci.H.PeerStatic()) { if !bytes.Equal(remoteCert.Certificate.PublicKey(), ci.H.PeerStatic()) {
f.l.WithField("from", via). f.l.Info("public key mismatch between certificate and handshake",
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "from", via,
WithField("cert", remoteCert).Info("public key mismatch between certificate and handshake") "handshake", m{"stage": 1, "style": "ix_psk0"},
"cert", remoteCert,
)
return return
} }
@@ -178,12 +208,13 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
// We started off using the wrong certificate version, lets see if we can match the version that was sent to us // We started off using the wrong certificate version, lets see if we can match the version that was sent to us
myCertOtherVersion := cs.getCertificate(remoteCert.Certificate.Version()) myCertOtherVersion := cs.getCertificate(remoteCert.Certificate.Version())
if myCertOtherVersion == nil { if myCertOtherVersion == nil {
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithError(err).WithFields(m{ f.l.Debug("Might be unable to handshake with host due to missing certificate version",
"from": via, "error", err,
"handshake": m{"stage": 1, "style": "ix_psk0"}, "from", via,
"cert": remoteCert, "handshake", m{"stage": 1, "style": "ix_psk0"},
}).Debug("Might be unable to handshake with host due to missing certificate version") "cert", remoteCert,
)
} }
} else { } else {
// Record the certificate we are actually using // Record the certificate we are actually using
@@ -192,10 +223,12 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
} }
if len(remoteCert.Certificate.Networks()) == 0 { if len(remoteCert.Certificate.Networks()) == 0 {
f.l.WithError(err).WithField("from", via). f.l.Info("No networks in certificate",
WithField("cert", remoteCert). "error", err,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "from", via,
Info("No networks in certificate") "cert", remoteCert,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
@@ -209,12 +242,15 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
vpnAddrs := make([]netip.Addr, len(vpnNetworks)) vpnAddrs := make([]netip.Addr, len(vpnNetworks))
for i, network := range vpnNetworks { for i, network := range vpnNetworks {
if f.myVpnAddrsTable.Contains(network.Addr()) { if f.myVpnAddrsTable.Contains(network.Addr()) {
f.l.WithField("vpnNetworks", vpnNetworks).WithField("from", via). f.l.Error("Refusing to handshake with myself",
WithField("certName", certName). "vpnNetworks", vpnNetworks,
WithField("certVersion", certVersion). "from", via,
WithField("fingerprint", fingerprint). "certName", certName,
WithField("issuer", issuer). "certVersion", certVersion,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}).Error("Refusing to handshake with myself") "fingerprint", fingerprint,
"issuer", issuer,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
vpnAddrs[i] = network.Addr() vpnAddrs[i] = network.Addr()
@@ -226,20 +262,28 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
if !via.IsRelayed { if !via.IsRelayed {
// We only want to apply the remote allow list for direct tunnels here // We only want to apply the remote allow list for direct tunnels here
if !f.lightHouse.GetRemoteAllowList().AllowAll(vpnAddrs, via.UdpAddr.Addr()) { if !f.lightHouse.GetRemoteAllowList().AllowAll(vpnAddrs, via.UdpAddr.Addr()) {
f.l.WithField("vpnAddrs", vpnAddrs).WithField("from", via). if f.l.Enabled(context.Background(), slog.LevelDebug) {
Debug("lighthouse.remote_allow_list denied incoming handshake") f.l.Debug("lighthouse.remote_allow_list denied incoming handshake",
"vpnAddrs", vpnAddrs,
"from", via,
)
}
return return
} }
} }
myIndex, err := generateIndex(f.l) myIndex, err := generateIndex(f.l)
if err != nil { if err != nil {
f.l.WithError(err).WithField("vpnAddrs", vpnAddrs).WithField("from", via). f.l.Error("Failed to generate index",
WithField("certName", certName). "error", err,
WithField("certVersion", certVersion). "vpnAddrs", vpnAddrs,
WithField("fingerprint", fingerprint). "from", via,
WithField("issuer", issuer). "certName", certName,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}).Error("Failed to generate index") "certVersion", certVersion,
"fingerprint", fingerprint,
"issuer", issuer,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
@@ -257,18 +301,18 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
}, },
} }
msgRxL := f.l.WithFields(m{ msgRxL := f.l.With(
"vpnAddrs": vpnAddrs, "vpnAddrs", vpnAddrs,
"from": via, "from", via,
"certName": certName, "certName", certName,
"certVersion": certVersion, "certVersion", certVersion,
"fingerprint": fingerprint, "fingerprint", fingerprint,
"issuer": issuer, "issuer", issuer,
"initiatorIndex": hs.Details.InitiatorIndex, "initiatorIndex", hs.Details.InitiatorIndex,
"responderIndex": hs.Details.ResponderIndex, "responderIndex", hs.Details.ResponderIndex,
"remoteIndex": h.RemoteIndex, "remoteIndex", h.RemoteIndex,
"handshake": m{"stage": 1, "style": "ix_psk0"}, "handshake", m{"stage": 1, "style": "ix_psk0"},
}) )
if anyVpnAddrsInCommon { if anyVpnAddrsInCommon {
msgRxL.Info("Handshake message received") msgRxL.Info("Handshake message received")
@@ -280,8 +324,9 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
hs.Details.ResponderIndex = myIndex hs.Details.ResponderIndex = myIndex
hs.Details.Cert = cs.getHandshakeBytes(ci.myCert.Version()) hs.Details.Cert = cs.getHandshakeBytes(ci.myCert.Version())
if hs.Details.Cert == nil { if hs.Details.Cert == nil {
msgRxL.WithField("myCertVersion", ci.myCert.Version()). msgRxL.Error("Unable to handshake with host because no certificate handshake bytes is available",
Error("Unable to handshake with host because no certificate handshake bytes is available") "myCertVersion", ci.myCert.Version(),
)
return return
} }
@@ -291,32 +336,43 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
hsBytes, err := hs.Marshal() hsBytes, err := hs.Marshal()
if err != nil { if err != nil {
f.l.WithError(err).WithField("vpnAddrs", hostinfo.vpnAddrs).WithField("from", via). f.l.Error("Failed to marshal handshake message",
WithField("certName", certName). "error", err,
WithField("certVersion", certVersion). "vpnAddrs", hostinfo.vpnAddrs,
WithField("fingerprint", fingerprint). "from", via,
WithField("issuer", issuer). "certName", certName,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}).Error("Failed to marshal handshake message") "certVersion", certVersion,
"fingerprint", fingerprint,
"issuer", issuer,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
nh := header.Encode(make([]byte, header.Len), header.Version, header.Handshake, header.HandshakeIXPSK0, hs.Details.InitiatorIndex, 2) nh := header.Encode(make([]byte, header.Len), header.Version, header.Handshake, header.HandshakeIXPSK0, hs.Details.InitiatorIndex, 2)
msg, dKey, eKey, err := ci.H.WriteMessage(nh, hsBytes) msg, dKey, eKey, err := ci.H.WriteMessage(nh, hsBytes)
if err != nil { if err != nil {
f.l.WithError(err).WithField("vpnAddrs", hostinfo.vpnAddrs).WithField("from", via). f.l.Error("Failed to call noise.WriteMessage",
WithField("certName", certName). "error", err,
WithField("certVersion", certVersion). "vpnAddrs", hostinfo.vpnAddrs,
WithField("fingerprint", fingerprint). "from", via,
WithField("issuer", issuer). "certName", certName,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}).Error("Failed to call noise.WriteMessage") "certVersion", certVersion,
"fingerprint", fingerprint,
"issuer", issuer,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} else if dKey == nil || eKey == nil { } else if dKey == nil || eKey == nil {
f.l.WithField("vpnAddrs", hostinfo.vpnAddrs).WithField("from", via). f.l.Error("Noise did not arrive at a key",
WithField("certName", certName). "vpnAddrs", hostinfo.vpnAddrs,
WithField("certVersion", certVersion). "from", via,
WithField("fingerprint", fingerprint). "certName", certName,
WithField("issuer", issuer). "certVersion", certVersion,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}).Error("Noise did not arrive at a key") "fingerprint", fingerprint,
"issuer", issuer,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
@@ -358,13 +414,20 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
if !via.IsRelayed { if !via.IsRelayed {
err := f.outside.WriteTo(msg, via.UdpAddr) err := f.outside.WriteTo(msg, via.UdpAddr)
if err != nil { if err != nil {
f.l.WithField("vpnAddrs", existing.vpnAddrs).WithField("from", via). f.l.Error("Failed to send handshake message",
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}).WithField("cached", true). "vpnAddrs", existing.vpnAddrs,
WithError(err).Error("Failed to send handshake message") "from", via,
"handshake", m{"stage": 2, "style": "ix_psk0"},
"cached", true,
"error", err,
)
} else { } else {
f.l.WithField("vpnAddrs", existing.vpnAddrs).WithField("from", via). f.l.Info("Handshake message sent",
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}).WithField("cached", true). "vpnAddrs", existing.vpnAddrs,
Info("Handshake message sent") "from", via,
"handshake", m{"stage": 2, "style": "ix_psk0"},
"cached", true,
)
} }
return return
} else { } else {
@@ -374,50 +437,67 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
} }
hostinfo.relayState.InsertRelayTo(via.relayHI.vpnAddrs[0]) hostinfo.relayState.InsertRelayTo(via.relayHI.vpnAddrs[0])
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)
f.l.WithField("vpnAddrs", existing.vpnAddrs).WithField("relay", via.relayHI.vpnAddrs[0]). f.l.Info("Handshake message sent",
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}).WithField("cached", true). "vpnAddrs", existing.vpnAddrs,
Info("Handshake message sent") "relay", via.relayHI.vpnAddrs[0],
"handshake", m{"stage": 2, "style": "ix_psk0"},
"cached", true,
)
return return
} }
case ErrExistingHostInfo: case ErrExistingHostInfo:
// This means there was an existing tunnel and this handshake was older than the one we are currently based on // This means there was an existing tunnel and this handshake was older than the one we are currently based on
f.l.WithField("vpnAddrs", vpnAddrs).WithField("from", via). f.l.Info("Handshake too old",
WithField("certName", certName). "vpnAddrs", vpnAddrs,
WithField("certVersion", certVersion). "from", via,
WithField("oldHandshakeTime", existing.lastHandshakeTime). "certName", certName,
WithField("newHandshakeTime", hostinfo.lastHandshakeTime). "certVersion", certVersion,
WithField("fingerprint", fingerprint). "oldHandshakeTime", existing.lastHandshakeTime,
WithField("issuer", issuer). "newHandshakeTime", hostinfo.lastHandshakeTime,
WithField("initiatorIndex", hs.Details.InitiatorIndex).WithField("responderIndex", hs.Details.ResponderIndex). "fingerprint", fingerprint,
WithField("remoteIndex", h.RemoteIndex).WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "issuer", issuer,
Info("Handshake too old") "initiatorIndex", hs.Details.InitiatorIndex,
"responderIndex", hs.Details.ResponderIndex,
"remoteIndex", h.RemoteIndex,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
// Send a test packet to trigger an authenticated tunnel test, this should suss out any lingering tunnel issues // Send a test packet to trigger an authenticated tunnel test, this should suss out any lingering tunnel issues
f.SendMessageToVpnAddr(header.Test, header.TestRequest, vpnAddrs[0], []byte(""), make([]byte, 12, 12), make([]byte, mtu)) f.SendMessageToVpnAddr(header.Test, header.TestRequest, vpnAddrs[0], []byte(""), make([]byte, 12, 12), make([]byte, mtu))
return return
case ErrLocalIndexCollision: case ErrLocalIndexCollision:
// This means we failed to insert because of collision on localIndexId. Just let the next handshake packet retry // This means we failed to insert because of collision on localIndexId. Just let the next handshake packet retry
f.l.WithField("vpnAddrs", vpnAddrs).WithField("from", via). f.l.Error("Failed to add HostInfo due to localIndex collision",
WithField("certName", certName). "vpnAddrs", vpnAddrs,
WithField("certVersion", certVersion). "from", via,
WithField("fingerprint", fingerprint). "certName", certName,
WithField("issuer", issuer). "certVersion", certVersion,
WithField("initiatorIndex", hs.Details.InitiatorIndex).WithField("responderIndex", hs.Details.ResponderIndex). "fingerprint", fingerprint,
WithField("remoteIndex", h.RemoteIndex).WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "issuer", issuer,
WithField("localIndex", hostinfo.localIndexId).WithField("collision", existing.vpnAddrs). "initiatorIndex", hs.Details.InitiatorIndex,
Error("Failed to add HostInfo due to localIndex collision") "responderIndex", hs.Details.ResponderIndex,
"remoteIndex", h.RemoteIndex,
"handshake", m{"stage": 1, "style": "ix_psk0"},
"localIndex", hostinfo.localIndexId,
"collision", existing.vpnAddrs,
)
return return
default: default:
// Shouldn't happen, but just in case someone adds a new error type to CheckAndComplete // Shouldn't happen, but just in case someone adds a new error type to CheckAndComplete
// And we forget to update it here // And we forget to update it here
f.l.WithError(err).WithField("vpnAddrs", vpnAddrs).WithField("from", via). f.l.Error("Failed to add HostInfo to HostMap",
WithField("certName", certName). "error", err,
WithField("certVersion", certVersion). "vpnAddrs", vpnAddrs,
WithField("fingerprint", fingerprint). "from", via,
WithField("issuer", issuer). "certName", certName,
WithField("initiatorIndex", hs.Details.InitiatorIndex).WithField("responderIndex", hs.Details.ResponderIndex). "certVersion", certVersion,
WithField("remoteIndex", h.RemoteIndex).WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "fingerprint", fingerprint,
Error("Failed to add HostInfo to HostMap") "issuer", issuer,
"initiatorIndex", hs.Details.InitiatorIndex,
"responderIndex", hs.Details.ResponderIndex,
"remoteIndex", h.RemoteIndex,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
return return
} }
} }
@@ -426,15 +506,20 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
f.messageMetrics.Tx(header.Handshake, header.MessageSubType(msg[1]), 1) f.messageMetrics.Tx(header.Handshake, header.MessageSubType(msg[1]), 1)
if !via.IsRelayed { if !via.IsRelayed {
err = f.outside.WriteTo(msg, via.UdpAddr) err = f.outside.WriteTo(msg, via.UdpAddr)
log := f.l.WithField("vpnAddrs", vpnAddrs).WithField("from", via). log := f.l.With(
WithField("certName", certName). "vpnAddrs", vpnAddrs,
WithField("certVersion", certVersion). "from", via,
WithField("fingerprint", fingerprint). "certName", certName,
WithField("issuer", issuer). "certVersion", certVersion,
WithField("initiatorIndex", hs.Details.InitiatorIndex).WithField("responderIndex", hs.Details.ResponderIndex). "fingerprint", fingerprint,
WithField("remoteIndex", h.RemoteIndex).WithField("handshake", m{"stage": 2, "style": "ix_psk0"}) "issuer", issuer,
"initiatorIndex", hs.Details.InitiatorIndex,
"responderIndex", hs.Details.ResponderIndex,
"remoteIndex", h.RemoteIndex,
"handshake", m{"stage": 2, "style": "ix_psk0"},
)
if err != nil { if err != nil {
log.WithError(err).Error("Failed to send handshake") log.Error("Failed to send handshake", "error", err)
} else { } else {
log.Info("Handshake message sent") log.Info("Handshake message sent")
} }
@@ -448,20 +533,29 @@ func ixHandshakeStage1(f *Interface, via ViaSender, packet []byte, h *header.H)
// it's correctly marked as working. // it's correctly marked as working.
via.relayHI.relayState.UpdateRelayForByIdxState(via.remoteIdx, Established) via.relayHI.relayState.UpdateRelayForByIdxState(via.remoteIdx, 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)
f.l.WithField("vpnAddrs", vpnAddrs).WithField("relay", via.relayHI.vpnAddrs[0]). f.l.Info("Handshake message sent",
WithField("certName", certName). "vpnAddrs", vpnAddrs,
WithField("certVersion", certVersion). "relay", via.relayHI.vpnAddrs[0],
WithField("fingerprint", fingerprint). "certName", certName,
WithField("issuer", issuer). "certVersion", certVersion,
WithField("initiatorIndex", hs.Details.InitiatorIndex).WithField("responderIndex", hs.Details.ResponderIndex). "fingerprint", fingerprint,
WithField("remoteIndex", h.RemoteIndex).WithField("handshake", m{"stage": 2, "style": "ix_psk0"}). "issuer", issuer,
Info("Handshake message sent") "initiatorIndex", hs.Details.InitiatorIndex,
"responderIndex", hs.Details.ResponderIndex,
"remoteIndex", h.RemoteIndex,
"handshake", m{"stage": 2, "style": "ix_psk0"},
)
} }
f.connectionManager.AddTrafficWatch(hostinfo) f.connectionManager.AddTrafficWatch(hostinfo)
hostinfo.remotes.RefreshFromHandshake(vpnAddrs) hostinfo.remotes.RefreshFromHandshake(vpnAddrs)
// Don't wait for UpdateWorker
if f.lightHouse.IsAnyLighthouseAddr(vpnAddrs) {
f.lightHouse.TriggerUpdate()
}
return return
} }
@@ -478,7 +572,12 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
if !via.IsRelayed { if !via.IsRelayed {
// The vpnAddr we know about is the one we tried to handshake with, use it to apply the remote allow list. // The vpnAddr we know about is the one we tried to handshake with, use it to apply the remote allow list.
if !f.lightHouse.GetRemoteAllowList().AllowAll(hostinfo.vpnAddrs, via.UdpAddr.Addr()) { if !f.lightHouse.GetRemoteAllowList().AllowAll(hostinfo.vpnAddrs, via.UdpAddr.Addr()) {
f.l.WithField("vpnAddrs", hostinfo.vpnAddrs).WithField("from", via).Debug("lighthouse.remote_allow_list denied incoming handshake") if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.Debug("lighthouse.remote_allow_list denied incoming handshake",
"vpnAddrs", hostinfo.vpnAddrs,
"from", via,
)
}
return false return false
} }
} }
@@ -486,18 +585,24 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
ci := hostinfo.ConnectionState ci := hostinfo.ConnectionState
msg, eKey, dKey, err := ci.H.ReadMessage(nil, packet[header.Len:]) msg, eKey, dKey, err := ci.H.ReadMessage(nil, packet[header.Len:])
if err != nil { if err != nil {
f.l.WithError(err).WithField("vpnAddrs", hostinfo.vpnAddrs).WithField("from", via). f.l.Error("Failed to call noise.ReadMessage",
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}).WithField("header", h). "error", err,
Error("Failed to call noise.ReadMessage") "vpnAddrs", hostinfo.vpnAddrs,
"from", via,
"handshake", m{"stage": 2, "style": "ix_psk0"},
"header", h,
)
// We don't want to tear down the connection on a bad ReadMessage because it could be an attacker trying // We don't want to tear down the connection on a bad ReadMessage because it could be an attacker trying
// to DOS us. Every other error condition after should to allow a possible good handshake to complete in the // to DOS us. Every other error condition after should to allow a possible good handshake to complete in the
// near future // near future
return false return false
} else if dKey == nil || eKey == nil { } else if dKey == nil || eKey == nil {
f.l.WithField("vpnAddrs", hostinfo.vpnAddrs).WithField("from", via). f.l.Error("Noise did not arrive at a key",
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}). "vpnAddrs", hostinfo.vpnAddrs,
Error("Noise did not arrive at a key") "from", via,
"handshake", m{"stage": 2, "style": "ix_psk0"},
)
// This should be impossible in IX but just in case, if we get here then there is no chance to recover // This should be impossible in IX but just in case, if we get here then there is no chance to recover
// the handshake state machine. Tear it down // the handshake state machine. Tear it down
@@ -507,8 +612,12 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
hs := &NebulaHandshake{} hs := &NebulaHandshake{}
err = hs.Unmarshal(msg) err = hs.Unmarshal(msg)
if err != nil || hs.Details == nil { if err != nil || hs.Details == nil {
f.l.WithError(err).WithField("vpnAddrs", hostinfo.vpnAddrs).WithField("from", via). f.l.Error("Failed unmarshal handshake message",
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}).Error("Failed unmarshal handshake message") "error", err,
"vpnAddrs", hostinfo.vpnAddrs,
"from", via,
"handshake", m{"stage": 2, "style": "ix_psk0"},
)
// The handshake state machine is complete, if things break now there is no chance to recover. Tear down and start again // The handshake state machine is complete, if things break now there is no chance to recover. Tear down and start again
return true return true
@@ -516,10 +625,12 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
rc, err := cert.Recombine(cert.Version(hs.Details.CertVersion), hs.Details.Cert, ci.H.PeerStatic(), ci.Curve()) rc, err := cert.Recombine(cert.Version(hs.Details.CertVersion), hs.Details.Cert, ci.H.PeerStatic(), ci.Curve())
if err != nil { if err != nil {
f.l.WithError(err).WithField("from", via). f.l.Info("Handshake did not contain a certificate",
WithField("vpnAddrs", hostinfo.vpnAddrs). "error", err,
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}). "from", via,
Info("Handshake did not contain a certificate") "vpnAddrs", hostinfo.vpnAddrs,
"handshake", m{"stage": 2, "style": "ix_psk0"},
)
return true return true
} }
@@ -530,32 +641,41 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
fp = "<error generating certificate fingerprint>" fp = "<error generating certificate fingerprint>"
} }
e := f.l.WithError(err).WithField("from", via). attrs := []slog.Attr{
WithField("vpnAddrs", hostinfo.vpnAddrs). slog.Any("error", err),
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}). slog.Any("from", via),
WithField("certFingerprint", fp). slog.Any("vpnAddrs", hostinfo.vpnAddrs),
WithField("certVpnNetworks", rc.Networks()) slog.Any("handshake", m{"stage": 2, "style": "ix_psk0"}),
slog.String("certFingerprint", fp),
if f.l.Level >= logrus.DebugLevel { slog.Any("certVpnNetworks", rc.Networks()),
e = e.WithField("cert", rc) }
if f.l.Enabled(context.Background(), slog.LevelDebug) {
attrs = append(attrs, slog.Any("cert", rc))
} }
e.Info("Invalid certificate from host") // LogAttrs is intentional: attrs is a pre-built []slog.Attr slice that
// callers grow conditionally, which has no pair-form equivalent.
//nolint:sloglint
f.l.LogAttrs(context.Background(), slog.LevelInfo, "Invalid certificate from host", attrs...)
return true return true
} }
if !bytes.Equal(remoteCert.Certificate.PublicKey(), ci.H.PeerStatic()) { if !bytes.Equal(remoteCert.Certificate.PublicKey(), ci.H.PeerStatic()) {
f.l.WithField("from", via). f.l.Info("public key mismatch between certificate and handshake",
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}). "from", via,
WithField("cert", remoteCert).Info("public key mismatch between certificate and handshake") "handshake", m{"stage": 2, "style": "ix_psk0"},
"cert", remoteCert,
)
return true return true
} }
if len(remoteCert.Certificate.Networks()) == 0 { if len(remoteCert.Certificate.Networks()) == 0 {
f.l.WithError(err).WithField("from", via). f.l.Info("No networks in certificate",
WithField("vpnAddrs", hostinfo.vpnAddrs). "error", err,
WithField("cert", remoteCert). "from", via,
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}). "vpnAddrs", hostinfo.vpnAddrs,
Info("No networks in certificate") "cert", remoteCert,
"handshake", m{"stage": 2, "style": "ix_psk0"},
)
return true return true
} }
@@ -596,12 +716,14 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
// Ensure the right host responded // Ensure the right host responded
if !correctHostResponded { if !correctHostResponded {
f.l.WithField("intendedVpnAddrs", hostinfo.vpnAddrs).WithField("haveVpnNetworks", vpnNetworks). f.l.Info("Incorrect host responded to handshake",
WithField("from", via). "intendedVpnAddrs", hostinfo.vpnAddrs,
WithField("certName", certName). "haveVpnNetworks", vpnNetworks,
WithField("certVersion", certVersion). "from", via,
WithField("handshake", m{"stage": 2, "style": "ix_psk0"}). "certName", certName,
Info("Incorrect host responded to handshake") "certVersion", certVersion,
"handshake", m{"stage": 2, "style": "ix_psk0"},
)
// Release our old handshake from pending, it should not continue // Release our old handshake from pending, it should not continue
f.handshakeManager.DeleteHostInfo(hostinfo) f.handshakeManager.DeleteHostInfo(hostinfo)
@@ -613,10 +735,11 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
newHH.hostinfo.remotes = hostinfo.remotes newHH.hostinfo.remotes = hostinfo.remotes
newHH.hostinfo.remotes.BlockRemote(via) newHH.hostinfo.remotes.BlockRemote(via)
f.l.WithField("blockedUdpAddrs", newHH.hostinfo.remotes.CopyBlockedRemotes()). f.l.Info("Blocked addresses for handshakes",
WithField("vpnNetworks", vpnNetworks). "blockedUdpAddrs", newHH.hostinfo.remotes.CopyBlockedRemotes(),
WithField("remotes", newHH.hostinfo.remotes.CopyAddrs(f.hostMap.GetPreferredRanges())). "vpnNetworks", vpnNetworks,
Info("Blocked addresses for handshakes") "remotes", newHH.hostinfo.remotes.CopyAddrs(f.hostMap.GetPreferredRanges()),
)
// Swap the packet store to benefit the original intended recipient // Swap the packet store to benefit the original intended recipient
newHH.packetStore = hh.packetStore newHH.packetStore = hh.packetStore
@@ -634,15 +757,20 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
ci.window.Update(f.l, 2) ci.window.Update(f.l, 2)
duration := time.Since(hh.startTime).Nanoseconds() duration := time.Since(hh.startTime).Nanoseconds()
msgRxL := f.l.WithField("vpnAddrs", vpnAddrs).WithField("from", via). msgRxL := f.l.With(
WithField("certName", certName). "vpnAddrs", vpnAddrs,
WithField("certVersion", certVersion). "from", via,
WithField("fingerprint", fingerprint). "certName", certName,
WithField("issuer", issuer). "certVersion", certVersion,
WithField("initiatorIndex", hs.Details.InitiatorIndex).WithField("responderIndex", hs.Details.ResponderIndex). "fingerprint", fingerprint,
WithField("remoteIndex", h.RemoteIndex).WithField("handshake", m{"stage": 2, "style": "ix_psk0"}). "issuer", issuer,
WithField("durationNs", duration). "initiatorIndex", hs.Details.InitiatorIndex,
WithField("sentCachedPackets", len(hh.packetStore)) "responderIndex", hs.Details.ResponderIndex,
"remoteIndex", h.RemoteIndex,
"handshake", m{"stage": 2, "style": "ix_psk0"},
"durationNs", duration,
"sentCachedPackets", len(hh.packetStore),
)
if anyVpnAddrsInCommon { if anyVpnAddrsInCommon {
msgRxL.Info("Handshake message received") msgRxL.Info("Handshake message received")
} else { } else {
@@ -658,8 +786,10 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
f.handshakeManager.Complete(hostinfo, f) f.handshakeManager.Complete(hostinfo, f)
f.connectionManager.AddTrafficWatch(hostinfo) f.connectionManager.AddTrafficWatch(hostinfo)
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debugf("Sending %d stored packets", len(hh.packetStore)) hostinfo.logger(f.l).Debug("Sending stored packets",
"count", len(hh.packetStore),
)
} }
if len(hh.packetStore) > 0 { if len(hh.packetStore) > 0 {
@@ -674,5 +804,10 @@ func ixHandshakeStage2(f *Interface, via ViaSender, hh *HandshakeHostInfo, packe
hostinfo.remotes.RefreshFromHandshake(vpnAddrs) hostinfo.remotes.RefreshFromHandshake(vpnAddrs)
f.metricHandshakes.Update(duration) f.metricHandshakes.Update(duration)
// Don't wait for UpdateWorker
if f.lightHouse.IsAnyLighthouseAddr(vpnAddrs) {
f.lightHouse.TriggerUpdate()
}
return false return false
} }
+80 -77
View File
@@ -6,13 +6,13 @@ import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"log/slog"
"net/netip" "net/netip"
"slices" "slices"
"sync" "sync"
"time" "time"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/udp" "github.com/slackhq/nebula/udp"
@@ -59,7 +59,7 @@ type HandshakeManager struct {
metricInitiated metrics.Counter metricInitiated metrics.Counter
metricTimedOut metrics.Counter metricTimedOut metrics.Counter
f *Interface f *Interface
l *logrus.Logger l *slog.Logger
// can be used to trigger outbound handshake for the given vpnIp // can be used to trigger outbound handshake for the given vpnIp
trigger chan netip.Addr trigger chan netip.Addr
@@ -78,32 +78,32 @@ type HandshakeHostInfo struct {
hostinfo *HostInfo hostinfo *HostInfo
} }
func (hh *HandshakeHostInfo) cachePacket(l *logrus.Logger, t header.MessageType, st header.MessageSubType, packet []byte, f packetCallback, m *cachedPacketMetrics) { func (hh *HandshakeHostInfo) cachePacket(l *slog.Logger, t header.MessageType, st header.MessageSubType, packet []byte, f packetCallback, m *cachedPacketMetrics) {
if len(hh.packetStore) < 100 { if len(hh.packetStore) < 100 {
tempPacket := make([]byte, len(packet)) tempPacket := make([]byte, len(packet))
copy(tempPacket, packet) copy(tempPacket, packet)
hh.packetStore = append(hh.packetStore, &cachedPacket{t, st, f, tempPacket}) hh.packetStore = append(hh.packetStore, &cachedPacket{t, st, f, tempPacket})
if l.Level >= logrus.DebugLevel { if l.Enabled(context.Background(), slog.LevelDebug) {
hh.hostinfo.logger(l). hh.hostinfo.logger(l).Debug("Packet store",
WithField("length", len(hh.packetStore)). "length", len(hh.packetStore),
WithField("stored", true). "stored", true,
Debugf("Packet store") )
} }
} else { } else {
m.dropped.Inc(1) m.dropped.Inc(1)
if l.Level >= logrus.DebugLevel { if l.Enabled(context.Background(), slog.LevelDebug) {
hh.hostinfo.logger(l). hh.hostinfo.logger(l).Debug("Packet store",
WithField("length", len(hh.packetStore)). "length", len(hh.packetStore),
WithField("stored", false). "stored", false,
Debugf("Packet store") )
} }
} }
} }
func NewHandshakeManager(l *logrus.Logger, mainHostMap *HostMap, lightHouse *LightHouse, outside udp.Conn, config HandshakeConfig) *HandshakeManager { func NewHandshakeManager(l *slog.Logger, mainHostMap *HostMap, lightHouse *LightHouse, outside udp.Conn, config HandshakeConfig) *HandshakeManager {
return &HandshakeManager{ return &HandshakeManager{
vpnIps: map[netip.Addr]*HandshakeHostInfo{}, vpnIps: map[netip.Addr]*HandshakeHostInfo{},
indexes: map[uint32]*HandshakeHostInfo{}, indexes: map[uint32]*HandshakeHostInfo{},
@@ -140,7 +140,7 @@ func (hm *HandshakeManager) HandleIncoming(via ViaSender, packet []byte, h *head
// First remote allow list check before we know the vpnIp // First remote allow list check before we know the vpnIp
if !via.IsRelayed { if !via.IsRelayed {
if !hm.lightHouse.GetRemoteAllowList().AllowUnknownVpnAddr(via.UdpAddr.Addr()) { if !hm.lightHouse.GetRemoteAllowList().AllowUnknownVpnAddr(via.UdpAddr.Addr()) {
hm.l.WithField("from", via).Debug("lighthouse.remote_allow_list denied incoming handshake") hm.l.Debug("lighthouse.remote_allow_list denied incoming handshake", "from", via)
return return
} }
} }
@@ -183,12 +183,13 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
hostinfo := hh.hostinfo hostinfo := hh.hostinfo
// If we are out of time, clean up // If we are out of time, clean up
if hh.counter >= hm.config.retries { if hh.counter >= hm.config.retries {
hh.hostinfo.logger(hm.l).WithField("udpAddrs", hh.hostinfo.remotes.CopyAddrs(hm.mainHostMap.GetPreferredRanges())). hh.hostinfo.logger(hm.l).Info("Handshake timed out",
WithField("initiatorIndex", hh.hostinfo.localIndexId). "udpAddrs", hh.hostinfo.remotes.CopyAddrs(hm.mainHostMap.GetPreferredRanges()),
WithField("remoteIndex", hh.hostinfo.remoteIndexId). "initiatorIndex", hh.hostinfo.localIndexId,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "remoteIndex", hh.hostinfo.remoteIndexId,
WithField("durationNs", time.Since(hh.startTime).Nanoseconds()). "handshake", m{"stage": 1, "style": "ix_psk0"},
Info("Handshake timed out") "durationNs", time.Since(hh.startTime).Nanoseconds(),
)
hm.metricTimedOut.Inc(1) hm.metricTimedOut.Inc(1)
hm.DeleteHostInfo(hostinfo) hm.DeleteHostInfo(hostinfo)
return return
@@ -241,10 +242,12 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
hm.messageMetrics.Tx(header.Handshake, header.MessageSubType(hostinfo.HandshakePacket[0][1]), 1) hm.messageMetrics.Tx(header.Handshake, header.MessageSubType(hostinfo.HandshakePacket[0][1]), 1)
err := hm.outside.WriteTo(hostinfo.HandshakePacket[0], addr) err := hm.outside.WriteTo(hostinfo.HandshakePacket[0], addr)
if err != nil { if err != nil {
hostinfo.logger(hm.l).WithField("udpAddr", addr). hostinfo.logger(hm.l).Error("Failed to send handshake message",
WithField("initiatorIndex", hostinfo.localIndexId). "udpAddr", addr,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "initiatorIndex", hostinfo.localIndexId,
WithError(err).Error("Failed to send handshake message") "handshake", m{"stage": 1, "style": "ix_psk0"},
"error", err,
)
} else { } else {
sentTo = append(sentTo, addr) sentTo = append(sentTo, addr)
@@ -254,19 +257,21 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
// Don't be too noisy or confusing if we fail to send a handshake - if we don't get through we'll eventually log a timeout, // Don't be too noisy or confusing if we fail to send a handshake - if we don't get through we'll eventually log a timeout,
// so only log when the list of remotes has changed // so only log when the list of remotes has changed
if remotesHaveChanged { if remotesHaveChanged {
hostinfo.logger(hm.l).WithField("udpAddrs", sentTo). hostinfo.logger(hm.l).Info("Handshake message sent",
WithField("initiatorIndex", hostinfo.localIndexId). "udpAddrs", sentTo,
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "initiatorIndex", hostinfo.localIndexId,
Info("Handshake message sent") "handshake", m{"stage": 1, "style": "ix_psk0"},
} else if hm.l.Level >= logrus.DebugLevel { )
hostinfo.logger(hm.l).WithField("udpAddrs", sentTo). } else if hm.l.Enabled(context.Background(), slog.LevelDebug) {
WithField("initiatorIndex", hostinfo.localIndexId). hostinfo.logger(hm.l).Debug("Handshake message sent",
WithField("handshake", m{"stage": 1, "style": "ix_psk0"}). "udpAddrs", sentTo,
Debug("Handshake message sent") "initiatorIndex", hostinfo.localIndexId,
"handshake", m{"stage": 1, "style": "ix_psk0"},
)
} }
if hm.config.useRelays && len(hostinfo.remotes.relays) > 0 { if hm.config.useRelays && len(hostinfo.remotes.relays) > 0 {
hostinfo.logger(hm.l).WithField("relays", hostinfo.remotes.relays).Info("Attempt to relay through hosts") hostinfo.logger(hm.l).Info("Attempt to relay through hosts", "relays", hostinfo.remotes.relays)
// 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
@@ -281,7 +286,7 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
relayHostInfo := hm.mainHostMap.QueryVpnAddr(relay) relayHostInfo := hm.mainHostMap.QueryVpnAddr(relay)
if relayHostInfo == nil || !relayHostInfo.remote.IsValid() { if relayHostInfo == nil || !relayHostInfo.remote.IsValid() {
hostinfo.logger(hm.l).WithField("relay", relay.String()).Info("Establish tunnel to relay target") hostinfo.logger(hm.l).Info("Establish tunnel to relay target", "relay", relay.String())
hm.f.Handshake(relay) hm.f.Handshake(relay)
continue continue
} }
@@ -292,7 +297,7 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
if relayHostInfo.remote.IsValid() { if relayHostInfo.remote.IsValid() {
idx, err := AddRelay(hm.l, relayHostInfo, hm.mainHostMap, vpnIp, nil, TerminalType, Requested) idx, err := AddRelay(hm.l, relayHostInfo, hm.mainHostMap, vpnIp, nil, TerminalType, Requested)
if err != nil { if err != nil {
hostinfo.logger(hm.l).WithField("relay", relay.String()).WithError(err).Info("Failed to add relay to hostmap") hostinfo.logger(hm.l).Info("Failed to add relay to hostmap", "relay", relay.String(), "error", err)
} }
m := NebulaControl{ m := NebulaControl{
@@ -326,17 +331,15 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
msg, err := m.Marshal() msg, err := m.Marshal()
if err != nil { if err != nil {
hostinfo.logger(hm.l). hostinfo.logger(hm.l).Error("Failed to marshal Control message to create relay", "error", err)
WithError(err).
Error("Failed to marshal Control message to create relay")
} else { } else {
hm.f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, make([]byte, 12), make([]byte, mtu)) hm.f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, make([]byte, 12), make([]byte, mtu))
hm.l.WithFields(logrus.Fields{ hm.l.Info("send CreateRelayRequest",
"relayFrom": hm.f.myVpnAddrs[0], "relayFrom", hm.f.myVpnAddrs[0],
"relayTo": vpnIp, "relayTo", vpnIp,
"initiatorRelayIndex": idx, "initiatorRelayIndex", idx,
"relay": relay}). "relay", relay,
Info("send CreateRelayRequest") )
} }
} }
continue continue
@@ -344,14 +347,14 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
switch existingRelay.State { switch existingRelay.State {
case Established: case Established:
hostinfo.logger(hm.l).WithField("relay", relay.String()).Info("Send handshake via relay") hostinfo.logger(hm.l).Info("Send handshake via relay", "relay", relay.String())
hm.f.SendVia(relayHostInfo, existingRelay, hostinfo.HandshakePacket[0], make([]byte, 12), make([]byte, mtu), false) hm.f.SendVia(relayHostInfo, existingRelay, hostinfo.HandshakePacket[0], 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)
fallthrough fallthrough
case Requested: case Requested:
hostinfo.logger(hm.l).WithField("relay", relay.String()).Info("Re-send CreateRelay request") hostinfo.logger(hm.l).Info("Re-send CreateRelay request", "relay", relay.String())
// Re-send the CreateRelay request, in case the previous one was lost. // Re-send the CreateRelay request, in case the previous one was lost.
m := NebulaControl{ m := NebulaControl{
Type: NebulaControl_CreateRelayRequest, Type: NebulaControl_CreateRelayRequest,
@@ -383,28 +386,26 @@ func (hm *HandshakeManager) handleOutbound(vpnIp netip.Addr, lighthouseTriggered
} }
msg, err := m.Marshal() msg, err := m.Marshal()
if err != nil { if err != nil {
hostinfo.logger(hm.l). hostinfo.logger(hm.l).Error("Failed to marshal Control message to create relay", "error", err)
WithError(err).
Error("Failed to marshal Control message to create relay")
} else { } else {
// This must send over the hostinfo, not over hm.Hosts[ip] // This must send over the hostinfo, not over hm.Hosts[ip]
hm.f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, make([]byte, 12), make([]byte, mtu)) hm.f.SendMessageToHostInfo(header.Control, 0, relayHostInfo, msg, make([]byte, 12), make([]byte, mtu))
hm.l.WithFields(logrus.Fields{ hm.l.Info("send CreateRelayRequest",
"relayFrom": hm.f.myVpnAddrs[0], "relayFrom", hm.f.myVpnAddrs[0],
"relayTo": vpnIp, "relayTo", vpnIp,
"initiatorRelayIndex": existingRelay.LocalIndex, "initiatorRelayIndex", existingRelay.LocalIndex,
"relay": relay}). "relay", relay,
Info("send CreateRelayRequest") )
} }
case PeerRequested: case PeerRequested:
// PeerRequested only occurs in Forwarding relays, not Terminal relays, and this is a Terminal relay case. // PeerRequested only occurs in Forwarding relays, not Terminal relays, and this is a Terminal relay case.
fallthrough fallthrough
default: default:
hostinfo.logger(hm.l). hostinfo.logger(hm.l).Error("Relay unexpected state",
WithField("vpnIp", vpnIp). "vpnIp", vpnIp,
WithField("state", existingRelay.State). "state", existingRelay.State,
WithField("relay", relay). "relay", relay,
Errorf("Relay unexpected state") )
} }
} }
@@ -549,9 +550,10 @@ func (hm *HandshakeManager) CheckAndComplete(hostinfo *HostInfo, handshakePacket
if found && existingRemoteIndex != nil && existingRemoteIndex.vpnAddrs[0] != hostinfo.vpnAddrs[0] { if found && existingRemoteIndex != nil && existingRemoteIndex.vpnAddrs[0] != hostinfo.vpnAddrs[0] {
// We have a collision, but this can happen since we can't control // We have a collision, but this can happen since we can't control
// the remote ID. Just log about the situation as a note. // the remote ID. Just log about the situation as a note.
hostinfo.logger(hm.l). hostinfo.logger(hm.l).Info("New host shadows existing host remoteIndex",
WithField("remoteIndex", hostinfo.remoteIndexId).WithField("collision", existingRemoteIndex.vpnAddrs). "remoteIndex", hostinfo.remoteIndexId,
Info("New host shadows existing host remoteIndex") "collision", existingRemoteIndex.vpnAddrs,
)
} }
hm.mainHostMap.unlockedAddHostInfo(hostinfo, f) hm.mainHostMap.unlockedAddHostInfo(hostinfo, f)
@@ -571,9 +573,10 @@ func (hm *HandshakeManager) Complete(hostinfo *HostInfo, f *Interface) {
if found && existingRemoteIndex != nil { if found && existingRemoteIndex != nil {
// We have a collision, but this can happen since we can't control // We have a collision, but this can happen since we can't control
// the remote ID. Just log about the situation as a note. // the remote ID. Just log about the situation as a note.
hostinfo.logger(hm.l). hostinfo.logger(hm.l).Info("New host shadows existing host remoteIndex",
WithField("remoteIndex", hostinfo.remoteIndexId).WithField("collision", existingRemoteIndex.vpnAddrs). "remoteIndex", hostinfo.remoteIndexId,
Info("New host shadows existing host remoteIndex") "collision", existingRemoteIndex.vpnAddrs,
)
} }
// We need to remove from the pending hostmap first to avoid undoing work when after to the main hostmap. // We need to remove from the pending hostmap first to avoid undoing work when after to the main hostmap.
@@ -629,10 +632,11 @@ func (hm *HandshakeManager) unlockedDeleteHostInfo(hostinfo *HostInfo) {
hm.indexes = map[uint32]*HandshakeHostInfo{} hm.indexes = map[uint32]*HandshakeHostInfo{}
} }
if hm.l.Level >= logrus.DebugLevel { if hm.l.Enabled(context.Background(), slog.LevelDebug) {
hm.l.WithField("hostMap", m{"mapTotalSize": len(hm.vpnIps), hm.l.Debug("Pending hostmap hostInfo deleted",
"vpnAddrs": hostinfo.vpnAddrs, "indexNumber": hostinfo.localIndexId, "remoteIndexNumber": hostinfo.remoteIndexId}). "hostMap", m{"mapTotalSize": len(hm.vpnIps),
Debug("Pending hostmap hostInfo deleted") "vpnAddrs": hostinfo.vpnAddrs, "indexNumber": hostinfo.localIndexId, "remoteIndexNumber": hostinfo.remoteIndexId},
)
} }
} }
@@ -700,7 +704,7 @@ func (hm *HandshakeManager) EmitStats() {
// Utility functions below // Utility functions below
func generateIndex(l *logrus.Logger) (uint32, error) { func generateIndex(l *slog.Logger) (uint32, error) {
b := make([]byte, 4) b := make([]byte, 4)
// Let zero mean we don't know the ID, so don't generate zero // Let zero mean we don't know the ID, so don't generate zero
@@ -708,16 +712,15 @@ func generateIndex(l *logrus.Logger) (uint32, error) {
for index == 0 { for index == 0 {
_, err := rand.Read(b) _, err := rand.Read(b)
if err != nil { if err != nil {
l.Errorln(err) l.Error("Failed to generate index", "error", err)
return 0, err return 0, err
} }
index = binary.BigEndian.Uint32(b) index = binary.BigEndian.Uint32(b)
} }
if l.Level >= logrus.DebugLevel { if l.Enabled(context.Background(), slog.LevelDebug) {
l.WithField("index", index). l.Debug("Generated index", "index", index)
Debug("Generated index")
} }
return index, nil return index, nil
} }
+49 -31
View File
@@ -1,9 +1,11 @@
package nebula package nebula
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
@@ -13,10 +15,10 @@ import (
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/logging"
) )
const defaultPromoteEvery = 1000 // Count of packets sent before we try moving a tunnel to a preferred underlay ip address const defaultPromoteEvery = 1000 // Count of packets sent before we try moving a tunnel to a preferred underlay ip address
@@ -60,7 +62,7 @@ type HostMap struct {
RemoteIndexes map[uint32]*HostInfo RemoteIndexes map[uint32]*HostInfo
Hosts map[netip.Addr]*HostInfo Hosts map[netip.Addr]*HostInfo
preferredRanges atomic.Pointer[[]netip.Prefix] preferredRanges atomic.Pointer[[]netip.Prefix]
l *logrus.Logger l *slog.Logger
} }
// For synchronization, treat the pointed-to Relay struct as immutable. To edit the Relay // For synchronization, treat the pointed-to Relay struct as immutable. To edit the Relay
@@ -313,7 +315,7 @@ type cachedPacketMetrics struct {
dropped metrics.Counter dropped metrics.Counter
} }
func NewHostMapFromConfig(l *logrus.Logger, c *config.C) *HostMap { func NewHostMapFromConfig(l *slog.Logger, c *config.C) *HostMap {
hm := newHostMap(l) hm := newHostMap(l)
hm.reload(c, true) hm.reload(c, true)
@@ -321,13 +323,12 @@ func NewHostMapFromConfig(l *logrus.Logger, c *config.C) *HostMap {
hm.reload(c, false) hm.reload(c, false)
}) })
l.WithField("preferredRanges", hm.GetPreferredRanges()). l.Info("Main HostMap created", "preferredRanges", hm.GetPreferredRanges())
Info("Main HostMap created")
return hm return hm
} }
func newHostMap(l *logrus.Logger) *HostMap { func newHostMap(l *slog.Logger) *HostMap {
return &HostMap{ return &HostMap{
Indexes: map[uint32]*HostInfo{}, Indexes: map[uint32]*HostInfo{},
Relays: map[uint32]*HostInfo{}, Relays: map[uint32]*HostInfo{},
@@ -346,7 +347,10 @@ func (hm *HostMap) reload(c *config.C, initial bool) {
preferredRange, err := netip.ParsePrefix(rawPreferredRange) preferredRange, err := netip.ParsePrefix(rawPreferredRange)
if err != nil { if err != nil {
hm.l.WithError(err).WithField("range", rawPreferredRanges).Warn("Failed to parse preferred ranges, ignoring") hm.l.Warn("Failed to parse preferred ranges, ignoring",
"error", err,
"range", rawPreferredRanges,
)
continue continue
} }
@@ -355,7 +359,10 @@ func (hm *HostMap) reload(c *config.C, initial bool) {
oldRanges := hm.preferredRanges.Swap(&preferredRanges) oldRanges := hm.preferredRanges.Swap(&preferredRanges)
if !initial { if !initial {
hm.l.WithField("oldPreferredRanges", *oldRanges).WithField("newPreferredRanges", preferredRanges).Info("preferred_ranges changed") hm.l.Info("preferred_ranges changed",
"oldPreferredRanges", *oldRanges,
"newPreferredRanges", preferredRanges,
)
} }
} }
} }
@@ -488,10 +495,11 @@ func (hm *HostMap) unlockedInnerDeleteHostInfo(hostinfo *HostInfo, addr netip.Ad
hm.Indexes = map[uint32]*HostInfo{} hm.Indexes = map[uint32]*HostInfo{}
} }
if hm.l.Level >= logrus.DebugLevel { if hm.l.Enabled(context.Background(), slog.LevelDebug) {
hm.l.WithField("hostMap", m{"mapTotalSize": len(hm.Hosts), hm.l.Debug("Hostmap hostInfo deleted",
"vpnAddrs": hostinfo.vpnAddrs, "indexNumber": hostinfo.localIndexId, "remoteIndexNumber": hostinfo.remoteIndexId}). "hostMap", m{"mapTotalSize": len(hm.Hosts),
Debug("Hostmap hostInfo deleted") "vpnAddrs": hostinfo.vpnAddrs, "indexNumber": hostinfo.localIndexId, "remoteIndexNumber": hostinfo.remoteIndexId},
)
} }
if isLastHostinfo { if isLastHostinfo {
@@ -604,9 +612,9 @@ func (hm *HostMap) queryVpnAddr(vpnIp netip.Addr, promoteIfce *Interface) *HostI
// unlockedAddHostInfo assumes you have a write-lock and will add a hostinfo object to the hostmap Indexes and RemoteIndexes maps. // unlockedAddHostInfo assumes you have a write-lock and will add a hostinfo object to the hostmap Indexes and RemoteIndexes maps.
// If an entry exists for the Hosts table (vpnIp -> hostinfo) then the provided hostinfo will be made primary // If an entry exists for the Hosts table (vpnIp -> hostinfo) then the provided hostinfo will be made primary
func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) { func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
if f.serveDns { if f.dnsServer != nil {
remoteCert := hostinfo.ConnectionState.peerCert remoteCert := hostinfo.ConnectionState.peerCert
dnsR.Add(remoteCert.Certificate.Name()+".", hostinfo.vpnAddrs) f.dnsServer.Add(remoteCert.Certificate.Name()+".", hostinfo.vpnAddrs)
} }
for _, addr := range hostinfo.vpnAddrs { for _, addr := range hostinfo.vpnAddrs {
hm.unlockedInnerAddHostInfo(addr, hostinfo, f) hm.unlockedInnerAddHostInfo(addr, hostinfo, f)
@@ -615,10 +623,11 @@ func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
hm.Indexes[hostinfo.localIndexId] = hostinfo hm.Indexes[hostinfo.localIndexId] = hostinfo
hm.RemoteIndexes[hostinfo.remoteIndexId] = hostinfo hm.RemoteIndexes[hostinfo.remoteIndexId] = hostinfo
if hm.l.Level >= logrus.DebugLevel { if hm.l.Enabled(context.Background(), slog.LevelDebug) {
hm.l.WithField("hostMap", m{"vpnAddrs": hostinfo.vpnAddrs, "mapTotalSize": len(hm.Hosts), hm.l.Debug("Hostmap vpnIp added",
"hostinfo": m{"existing": true, "localIndexId": hostinfo.localIndexId, "vpnAddrs": hostinfo.vpnAddrs}}). "hostMap", m{"vpnAddrs": hostinfo.vpnAddrs, "mapTotalSize": len(hm.Hosts),
Debug("Hostmap vpnIp added") "hostinfo": m{"existing": true, "localIndexId": hostinfo.localIndexId, "vpnAddrs": hostinfo.vpnAddrs}},
)
} }
} }
@@ -784,18 +793,21 @@ func (i *HostInfo) buildNetworks(myVpnNetworksTable *bart.Lite, c cert.Certifica
} }
} }
func (i *HostInfo) logger(l *logrus.Logger) *logrus.Entry { // logger returns a derived slog.Logger with per-hostinfo fields pre-bound.
func (i *HostInfo) logger(l *slog.Logger) *slog.Logger {
if i == nil { if i == nil {
return logrus.NewEntry(l) return l
} }
li := l.WithField("vpnAddrs", i.vpnAddrs). li := l.With(
WithField("localIndex", i.localIndexId). "vpnAddrs", i.vpnAddrs,
WithField("remoteIndex", i.remoteIndexId) "localIndex", i.localIndexId,
"remoteIndex", i.remoteIndexId,
)
if connState := i.ConnectionState; connState != nil { if connState := i.ConnectionState; connState != nil {
if peerCert := connState.peerCert; peerCert != nil { if peerCert := connState.peerCert; peerCert != nil {
li = li.WithField("certName", peerCert.Certificate.Name()) li = li.With("certName", peerCert.Certificate.Name())
} }
} }
@@ -804,14 +816,17 @@ func (i *HostInfo) logger(l *logrus.Logger) *logrus.Entry {
// Utility functions // Utility functions
func localAddrs(l *logrus.Logger, allowList *LocalAllowList) []netip.Addr { func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
//FIXME: This function is pretty garbage //FIXME: This function is pretty garbage
var finalAddrs []netip.Addr var finalAddrs []netip.Addr
ifaces, _ := net.Interfaces() ifaces, _ := net.Interfaces()
for _, i := range ifaces { for _, i := range ifaces {
allow := allowList.AllowName(i.Name) allow := allowList.AllowName(i.Name)
if l.Level >= logrus.TraceLevel { if l.Enabled(context.Background(), logging.LevelTrace) {
l.WithField("interfaceName", i.Name).WithField("allow", allow).Trace("localAllowList.AllowName") l.Log(context.Background(), logging.LevelTrace, "localAllowList.AllowName",
"interfaceName", i.Name,
"allow", allow,
)
} }
if !allow { if !allow {
@@ -829,8 +844,8 @@ func localAddrs(l *logrus.Logger, allowList *LocalAllowList) []netip.Addr {
} }
if !addr.IsValid() { if !addr.IsValid() {
if l.Level >= logrus.DebugLevel { if l.Enabled(context.Background(), slog.LevelDebug) {
l.WithField("localAddr", rawAddr).Debug("addr was invalid") l.Debug("addr was invalid", "localAddr", rawAddr)
} }
continue continue
} }
@@ -838,8 +853,11 @@ func localAddrs(l *logrus.Logger, allowList *LocalAllowList) []netip.Addr {
if addr.IsLoopback() == false && addr.IsLinkLocalUnicast() == false { if addr.IsLoopback() == false && addr.IsLinkLocalUnicast() == false {
isAllowed := allowList.Allow(addr) isAllowed := allowList.Allow(addr)
if l.Level >= logrus.TraceLevel { if l.Enabled(context.Background(), logging.LevelTrace) {
l.WithField("localAddr", addr).WithField("allowed", isAllowed).Trace("localAllowList.Allow") l.Log(context.Background(), logging.LevelTrace, "localAllowList.Allow",
"localAddr", addr,
"allowed", isAllowed,
)
} }
if !isAllowed { if !isAllowed {
continue continue
+1 -1
View File
@@ -196,7 +196,7 @@ func TestHostMap_DeleteHostInfo(t *testing.T) {
func TestHostMap_reload(t *testing.T) { func TestHostMap_reload(t *testing.T) {
l := test.NewLogger() l := test.NewLogger()
c := config.NewC(l) c := config.NewC(test.NewLogger())
hm := NewHostMapFromConfig(l, c) hm := NewHostMapFromConfig(l, c)
+86 -63
View File
@@ -1,21 +1,26 @@
package nebula package nebula
import ( import (
"context"
"log/slog"
"net/netip" "net/netip"
"github.com/sirupsen/logrus"
"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/noiseutil"
"github.com/slackhq/nebula/overlay/batch"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet, nb []byte, batch *sendBatch, rejectBuf []byte, q int, localCache firewall.ConntrackCache) { func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet, nb []byte, sendBatch batch.TxBatcher, rejectBuf []byte, q int, localCache firewall.ConntrackCache) {
err := newPacket(packet, false, fwPacket) err := newPacket(packet, false, fwPacket)
if err != nil { if err != nil {
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("packet", packet).Debugf("Error while validating outbound packet: %s", err) f.l.Debug("Error while validating outbound packet",
"packet", packet,
"error", err,
)
} }
return return
} }
@@ -33,9 +38,9 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
// 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
// TUN device. // TUN device.
if immediatelyForwardToSelf { if immediatelyForwardToSelf {
_, err := f.readers[q].WriteReject(packet) _, err := f.readers[q].Write(packet)
if err != nil { if err != nil {
f.l.WithError(err).Error("Failed to forward to tun") f.l.Error("Failed to forward to tun", "error", err)
} }
} }
// Otherwise, drop. On linux, we should never see these packets - Linux // Otherwise, drop. On linux, we should never see these packets - Linux
@@ -54,10 +59,11 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
if hostinfo == nil { if hostinfo == nil {
f.rejectInside(packet, rejectBuf, q) f.rejectInside(packet, rejectBuf, q)
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("vpnAddr", fwPacket.RemoteAddr). f.l.Debug("dropping outbound packet, vpnAddr not in our vpn networks or in unsafe networks",
WithField("fwPacket", fwPacket). "vpnAddr", fwPacket.RemoteAddr,
Debugln("dropping outbound packet, vpnAddr not in our vpn networks or in unsafe networks") "fwPacket", fwPacket,
)
} }
return return
} }
@@ -68,15 +74,14 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
dropReason := f.firewall.Drop(*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.sendInsideMessage(hostinfo, packet, nb, batch, rejectBuf, q) f.sendInsideMessage(hostinfo, packet, nb, sendBatch, rejectBuf, q)
} else { } else {
f.rejectInside(packet, rejectBuf, q) f.rejectInside(packet, rejectBuf, q)
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l). hostinfo.logger(f.l).Debug("dropping outbound packet",
WithField("fwPacket", fwPacket). "fwPacket", fwPacket,
WithField("reason", dropReason). "reason", dropReason,
Debugln("dropping outbound packet") )
} }
} }
} }
@@ -85,7 +90,7 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
// caller's batch slot for later sendmmsg flush. When hostinfo.remote is not // caller's batch slot for later sendmmsg flush. When hostinfo.remote is not
// valid we fall through to the relay slow path via the unbatched sendNoMetrics // valid we fall through to the relay slow path via the unbatched sendNoMetrics
// so relay behavior is unchanged. // so relay behavior is unchanged.
func (f *Interface) sendInsideMessage(hostinfo *HostInfo, p, nb []byte, batch *sendBatch, rejectBuf []byte, q int) { func (f *Interface) sendInsideMessage(hostinfo *HostInfo, p, nb []byte, sendBatch batch.TxBatcher, rejectBuf []byte, q int) {
ci := hostinfo.ConnectionState ci := hostinfo.ConnectionState
if ci.eKey == nil { if ci.eKey == nil {
return return
@@ -98,7 +103,7 @@ func (f *Interface) sendInsideMessage(hostinfo *HostInfo, p, nb []byte, batch *s
return return
} }
scratch := batch.Next() scratch := sendBatch.Next()
if scratch == nil { if scratch == nil {
// Batch full: bypass batching and send this packet directly so we // Batch full: bypass batching and send this packet directly so we
// never drop traffic on over-subscribed iterations. // never drop traffic on over-subscribed iterations.
@@ -119,8 +124,10 @@ func (f *Interface) sendInsideMessage(hostinfo *HostInfo, p, nb []byte, batch *s
// finally used again. This tunnel would eventually be torn down and recreated if this action didn't help. // finally used again. This tunnel would eventually be torn down and recreated if this action didn't help.
f.lightHouse.QueryServer(hostinfo.vpnAddrs[0]) f.lightHouse.QueryServer(hostinfo.vpnAddrs[0])
hostinfo.lastRebindCount = f.rebindCount hostinfo.lastRebindCount = f.rebindCount
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("vpnAddrs", hostinfo.vpnAddrs).Debug("Lighthouse update triggered for punch due to rebind counter") hostinfo.logger(f.l).Debug("Lighthouse update triggered for punch due to rebind counter",
"vpnAddrs", hostinfo.vpnAddrs,
)
} }
} }
@@ -129,13 +136,15 @@ func (f *Interface) sendInsideMessage(hostinfo *HostInfo, p, nb []byte, batch *s
ci.writeLock.Unlock() ci.writeLock.Unlock()
} }
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err). hostinfo.logger(f.l).Error("Failed to encrypt outgoing packet",
WithField("udpAddr", hostinfo.remote).WithField("counter", c). "error", err,
Error("Failed to encrypt outgoing packet") "udpAddr", hostinfo.remote,
"counter", c,
)
return return
} }
batch.Commit(len(out), hostinfo.remote) sendBatch.Commit(len(out), hostinfo.remote)
} }
func (f *Interface) rejectInside(packet []byte, out []byte, q int) { func (f *Interface) rejectInside(packet []byte, out []byte, q int) {
@@ -148,9 +157,9 @@ func (f *Interface) rejectInside(packet []byte, out []byte, q int) {
return return
} }
_, err := f.readers[q].WriteReject(out) _, err := f.readers[q].Write(out)
if err != nil { if err != nil {
f.l.WithError(err).Error("Failed to write to tun") f.l.Error("Failed to write to tun", "error", err)
} }
} }
@@ -165,11 +174,11 @@ func (f *Interface) rejectOutside(packet []byte, ci *ConnectionState, hostinfo *
} }
if len(out) > iputil.MaxRejectPacketSize { if len(out) > iputil.MaxRejectPacketSize {
if f.l.GetLevel() >= logrus.InfoLevel { if f.l.Enabled(context.Background(), slog.LevelInfo) {
f.l. f.l.Info("rejectOutside: packet too big, not sending",
WithField("packet", packet). "packet", packet,
WithField("outPacket", out). "outPacket", out,
Info("rejectOutside: packet too big, not sending") )
} }
return return
} }
@@ -241,10 +250,11 @@ func (f *Interface) getOrHandshakeConsiderRouting(fwPacket *firewall.Packet, cac
// This would also need to interact with unsafe_route updates through reloading the config or // This would also need to interact with unsafe_route updates through reloading the config or
// use of the use_system_route_table option // use of the use_system_route_table option
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("destination", destinationAddr). f.l.Debug("Calculated gateway for ECMP not available, attempting other gateways",
WithField("originalGateway", gatewayAddr). "destination", destinationAddr,
Debugln("Calculated gateway for ECMP not available, attempting other gateways") "originalGateway", gatewayAddr,
)
} }
for i := range gateways { for i := range gateways {
@@ -270,17 +280,18 @@ func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubTyp
fp := &firewall.Packet{} fp := &firewall.Packet{}
err := newPacket(p, false, fp) err := newPacket(p, false, fp)
if err != nil { if err != nil {
f.l.Warnf("error while parsing outgoing packet for firewall check; %v", err) f.l.Warn("error while parsing outgoing packet for firewall check", "error", err)
return return
} }
// check if packet is in outbound fw rules // check if packet is in outbound fw rules
dropReason := f.firewall.Drop(*fp, false, hostinfo, f.pki.GetCAPool(), nil) dropReason := f.firewall.Drop(*fp, false, hostinfo, f.pki.GetCAPool(), nil)
if dropReason != nil { if dropReason != nil {
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("fwPacket", fp). f.l.Debug("dropping cached packet",
WithField("reason", dropReason). "fwPacket", fp,
Debugln("dropping cached packet") "reason", dropReason,
)
} }
return return
} }
@@ -296,9 +307,10 @@ func (f *Interface) SendMessageToVpnAddr(t header.MessageType, st header.Message
}) })
if hostInfo == nil { if hostInfo == nil {
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("vpnAddr", vpnAddr). f.l.Debug("dropping SendMessageToVpnAddr, vpnAddr not in our vpn networks or in unsafe routes",
Debugln("dropping SendMessageToVpnAddr, vpnAddr not in our vpn networks or in unsafe routes") "vpnAddr", vpnAddr,
)
} }
return return
} }
@@ -354,12 +366,12 @@ func (f *Interface) SendVia(via *HostInfo,
if noiseutil.EncryptLockNeeded { if noiseutil.EncryptLockNeeded {
via.ConnectionState.writeLock.Unlock() via.ConnectionState.writeLock.Unlock()
} }
via.logger(f.l). via.logger(f.l).Error("SendVia out buffer not large enough for relay",
WithField("outCap", cap(out)). "outCap", cap(out),
WithField("payloadLen", len(ad)). "payloadLen", len(ad),
WithField("headerLen", len(out)). "headerLen", len(out),
WithField("cipherOverhead", via.ConnectionState.eKey.Overhead()). "cipherOverhead", via.ConnectionState.eKey.Overhead(),
Error("SendVia out buffer not large enough for relay") )
return return
} }
@@ -379,12 +391,12 @@ func (f *Interface) SendVia(via *HostInfo,
via.ConnectionState.writeLock.Unlock() via.ConnectionState.writeLock.Unlock()
} }
if err != nil { if err != nil {
via.logger(f.l).WithError(err).Info("Failed to EncryptDanger in sendVia") via.logger(f.l).Info("Failed to EncryptDanger in sendVia", "error", err)
return return
} }
err = f.writers[0].WriteTo(out, via.remote) err = f.writers[0].WriteTo(out, via.remote)
if err != nil { if err != nil {
via.logger(f.l).WithError(err).Info("Failed to WriteTo in sendVia") via.logger(f.l).Info("Failed to WriteTo in sendVia", "error", err)
} }
f.connectionManager.RelayUsed(relay.LocalIndex) f.connectionManager.RelayUsed(relay.LocalIndex)
} }
@@ -423,8 +435,10 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
// finally used again. This tunnel would eventually be torn down and recreated if this action didn't help. // finally used again. This tunnel would eventually be torn down and recreated if this action didn't help.
f.lightHouse.QueryServer(hostinfo.vpnAddrs[0]) f.lightHouse.QueryServer(hostinfo.vpnAddrs[0])
hostinfo.lastRebindCount = f.rebindCount hostinfo.lastRebindCount = f.rebindCount
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("vpnAddrs", hostinfo.vpnAddrs).Debug("Lighthouse update triggered for punch due to rebind counter") f.l.Debug("Lighthouse update triggered for punch due to rebind counter",
"vpnAddrs", hostinfo.vpnAddrs,
)
} }
} }
@@ -434,24 +448,30 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
ci.writeLock.Unlock() ci.writeLock.Unlock()
} }
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err). hostinfo.logger(f.l).Error("Failed to encrypt outgoing packet",
WithField("udpAddr", remote).WithField("counter", c). "error", err,
WithField("attemptedCounter", c). "udpAddr", remote,
Error("Failed to encrypt outgoing packet") "counter", c,
"attemptedCounter", c,
)
return return
} }
if remote.IsValid() { if remote.IsValid() {
err = f.writers[q].WriteTo(out, remote) err = f.writers[q].WriteTo(out, remote)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err). hostinfo.logger(f.l).Error("Failed to write outgoing packet",
WithField("udpAddr", remote).Error("Failed to write outgoing packet") "error", err,
"udpAddr", remote,
)
} }
} else if hostinfo.remote.IsValid() { } else if hostinfo.remote.IsValid() {
err = f.writers[q].WriteTo(out, hostinfo.remote) err = f.writers[q].WriteTo(out, hostinfo.remote)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err). hostinfo.logger(f.l).Error("Failed to write outgoing packet",
WithField("udpAddr", remote).Error("Failed to write outgoing packet") "error", err,
"udpAddr", remote,
)
} }
} else { } else {
// Try to send via a relay // Try to send via a relay
@@ -459,7 +479,10 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
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).WithField("relay", relayIP).WithError(err).Info("sendNoMetrics failed to find HostInfo") hostinfo.logger(f.l).Info("sendNoMetrics failed to find HostInfo",
"relay", relayIP,
"error", err,
)
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)
+78 -117
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net/netip" "net/netip"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -11,11 +12,13 @@ import (
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/overlay" "github.com/slackhq/nebula/overlay"
"github.com/slackhq/nebula/overlay/batch"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/udp" "github.com/slackhq/nebula/udp"
) )
@@ -28,7 +31,7 @@ type InterfaceConfig struct {
pki *PKI pki *PKI
Cipher string Cipher string
Firewall *Firewall Firewall *Firewall
ServeDns bool DnsServer *dnsServer
HandshakeManager *HandshakeManager HandshakeManager *HandshakeManager
lightHouse *LightHouse lightHouse *LightHouse
connectionManager *connectionManager connectionManager *connectionManager
@@ -45,7 +48,7 @@ type InterfaceConfig struct {
reQueryWait time.Duration reQueryWait time.Duration
ConntrackCacheTimeout time.Duration ConntrackCacheTimeout time.Duration
l *logrus.Logger l *slog.Logger
} }
type Interface struct { type Interface struct {
@@ -56,7 +59,7 @@ type Interface struct {
firewall *Firewall firewall *Firewall
connectionManager *connectionManager connectionManager *connectionManager
handshakeManager *HandshakeManager handshakeManager *HandshakeManager
serveDns bool dnsServer *dnsServer
createTime time.Time createTime time.Time
lightHouse *LightHouse lightHouse *LightHouse
myBroadcastAddrsTable *bart.Lite myBroadcastAddrsTable *bart.Lite
@@ -84,13 +87,14 @@ type Interface struct {
conntrackCacheTimeout time.Duration conntrackCacheTimeout time.Duration
ctx context.Context
writers []udp.Conn writers []udp.Conn
readers []overlay.Queue readers []tio.Queue
// tunCoalescers is one tcpCoalescer per tun queue, wrapping readers[i]. // batchers is one per tun queue, wrapping readers[i].
// decryptToTun sends plaintext into the coalescer; listenOut calls its // decryptToTun sends plaintext into the batch.RxBatcher;
// Flush at the end of each UDP recvmmsg batch. // listenOut calls its Flush at the end of each UDP recvmmsg batch.
tunCoalescers []*tcpCoalescer batchers []batch.RxBatcher
wg sync.WaitGroup wg sync.WaitGroup
// fatalErr holds the first unexpected reader error that caused shutdown. // fatalErr holds the first unexpected reader error that caused shutdown.
// nil means "no fatal error" (yet) // nil means "no fatal error" (yet)
@@ -102,7 +106,7 @@ type Interface struct {
messageMetrics *MessageMetrics messageMetrics *MessageMetrics
cachedPacketMetrics *cachedPacketMetrics cachedPacketMetrics *cachedPacketMetrics
l *logrus.Logger l *slog.Logger
} }
type EncWriter interface { type EncWriter interface {
@@ -173,12 +177,13 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
cs := c.pki.getCertState() cs := c.pki.getCertState()
ifce := &Interface{ ifce := &Interface{
ctx: ctx,
pki: c.pki, pki: c.pki,
hostMap: c.HostMap, hostMap: c.HostMap,
outside: c.Outside, outside: c.Outside,
inside: c.Inside, inside: c.Inside,
firewall: c.Firewall, firewall: c.Firewall,
serveDns: c.ServeDns, dnsServer: c.DnsServer,
handshakeManager: c.HandshakeManager, handshakeManager: c.HandshakeManager,
createTime: time.Now(), createTime: time.Now(),
lightHouse: c.lightHouse, lightHouse: c.lightHouse,
@@ -187,8 +192,8 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
routines: c.routines, routines: c.routines,
version: c.version, version: c.version,
writers: make([]udp.Conn, c.routines), writers: make([]udp.Conn, c.routines),
readers: make([]overlay.Queue, c.routines), readers: make([]tio.Queue, c.routines),
tunCoalescers: make([]*tcpCoalescer, c.routines), batchers: make([]batch.RxBatcher, c.routines),
myVpnNetworks: cs.myVpnNetworks, myVpnNetworks: cs.myVpnNetworks,
myVpnNetworksTable: cs.myVpnNetworksTable, myVpnNetworksTable: cs.myVpnNetworksTable,
myVpnAddrs: cs.myVpnAddrs, myVpnAddrs: cs.myVpnAddrs,
@@ -225,13 +230,16 @@ func (f *Interface) activate() error {
addr, err := f.outside.LocalAddr() addr, err := f.outside.LocalAddr()
if err != nil { if err != nil {
f.l.WithError(err).Error("Failed to get udp listen address") f.l.Error("Failed to get udp listen address", "error", err)
} }
f.l.WithField("interface", f.inside.Name()).WithField("networks", f.myVpnNetworks). f.l.Info("Nebula interface is active",
WithField("build", f.version).WithField("udpAddr", addr). "interface", f.inside.Name(),
WithField("boringcrypto", boringEnabled()). "networks", f.myVpnNetworks,
Info("Nebula interface is active") "build", f.version,
"udpAddr", addr,
"boringcrypto", boringEnabled(),
)
if f.routines > 1 { if f.routines > 1 {
if !f.inside.SupportsMultiqueue() || !f.outside.SupportsMultipleReaders() { if !f.inside.SupportsMultiqueue() || !f.outside.SupportsMultipleReaders() {
@@ -243,16 +251,16 @@ func (f *Interface) activate() error {
metrics.GetOrRegisterGauge("routines", nil).Update(int64(f.routines)) metrics.GetOrRegisterGauge("routines", nil).Update(int64(f.routines))
// Prepare n tun queues // Prepare n tun queues
var reader overlay.Queue = f.inside
for i := 0; i < f.routines; i++ { for i := 0; i < f.routines; i++ {
if i > 0 { if i > 0 {
reader, err = f.inside.NewMultiQueueReader() if err = f.inside.NewMultiQueueReader(); err != nil {
if err != nil {
return err return err
} }
} }
f.readers[i] = reader }
f.tunCoalescers[i] = newTCPCoalescer(reader) f.readers = f.inside.Readers()
for i := range f.readers {
f.batchers[i] = batch.NewTCPCoalescer(f.readers[i])
} }
f.wg.Add(1) // for us to wait on Close() to return f.wg.Add(1) // for us to wait on Close() to return
@@ -308,121 +316,74 @@ func (f *Interface) listenOut(i int) {
li = f.outside li = f.outside
} }
ctCache := firewall.NewConntrackCacheTicker(f.conntrackCacheTimeout) ctCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
lhh := f.lightHouse.NewRequestHandler() lhh := f.lightHouse.NewRequestHandler()
h := &header.H{} h := &header.H{}
fwPacket := &firewall.Packet{} fwPacket := &firewall.Packet{}
nb := make([]byte, 12, 12) nb := make([]byte, 12, 12)
// plaintexts is a ring of decrypt scratches, one per packet in a UDP coalescer := f.batchers[i]
// recvmmsg batch. The coalescer borrows payload slices from here and
// requires they stay valid until Flush — so we rotate each packet and listener := func(fromUdpAddr netip.AddrPort, payload []byte) {
// reset only in the batch-end flush callback. plaintext := f.batchers[i].Reserve(len(payload))
var plaintexts [][]byte f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, plaintext[:0], payload, h, fwPacket, lhh, nb, i, ctCache.Get())
idx := 0 }
coalescer := f.tunCoalescers[i]
err := li.ListenOut(func(fromUdpAddr netip.AddrPort, payload []byte) { flusher := func() {
if idx >= len(plaintexts) {
plaintexts = append(plaintexts, make([]byte, udp.MTU))
}
f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, plaintexts[idx][:0], payload, h, fwPacket, lhh, nb, i, ctCache.Get(f.l))
idx++
}, func() {
if err := coalescer.Flush(); err != nil { if err := coalescer.Flush(); err != nil {
f.l.WithError(err).Error("Failed to flush tun coalescer") f.l.Error("Failed to flush tun coalescer", "error", err)
} }
idx = 0 }
})
err := li.ListenOut(listener, flusher)
if err != nil && !f.closed.Load() { if err != nil && !f.closed.Load() {
f.l.WithError(err).Error("Error while reading inbound packet, closing") f.l.Error("Error while reading inbound packet, closing", "error", err)
f.onFatal(err) f.onFatal(err)
} }
f.l.Debugf("underlay reader %v is done", i) f.l.Debug("underlay reader is done", "reader", i)
} }
func (f *Interface) listenIn(reader overlay.Queue, i int) { func (f *Interface) listenIn(reader tio.Queue, i int) {
rejectBuf := make([]byte, mtu) rejectBuf := make([]byte, mtu)
batch := newSendBatch(sendBatchCap, udp.MTU+32) sb := batch.NewSendBatch(batch.SendBatchCap, udp.MTU+32)
fwPacket := &firewall.Packet{} fwPacket := &firewall.Packet{}
nb := make([]byte, 12, 12) nb := make([]byte, 12, 12)
conntrackCache := firewall.NewConntrackCacheTicker(f.conntrackCacheTimeout) conntrackCache := firewall.NewConntrackCacheTicker(f.ctx, f.l, f.conntrackCacheTimeout)
for { for {
pkts, err := reader.Read() pkts, err := reader.Read()
if err != nil { if err != nil {
if !f.closed.Load() { if !f.closed.Load() {
f.l.WithError(err).WithField("reader", i).Error("Error while reading outbound packet, closing") f.l.Error("Error while reading outbound packet, closing", "error", err, "reader", i)
f.onFatal(err) f.onFatal(err)
} }
break break
} }
batch.Reset() sb.Reset()
for _, pkt := range pkts { for _, pkt := range pkts {
if batch.Len() >= batch.Cap() { if sb.Len() >= sb.Cap() {
f.flushBatch(batch, i) f.flushBatch(sb, i)
batch.Reset() sb.Reset()
} }
f.consumeInsidePacket(pkt, fwPacket, nb, batch, rejectBuf, i, conntrackCache.Get(f.l)) f.consumeInsidePacket(pkt, fwPacket, nb, sb, rejectBuf, i, conntrackCache.Get())
} }
if batch.Len() > 0 { if sb.Len() > 0 {
f.flushBatch(batch, i) f.flushBatch(sb, i)
} }
} }
f.l.Debugf("overlay reader %v is done", i) f.l.Debug("overlay reader is done", "reader", i)
} }
func (f *Interface) flushBatch(batch *sendBatch, q int) { func (f *Interface) flushBatch(sb batch.TxBatcher, q int) {
//if len(batch.bufs) == 1 { bufs, dsts := sb.Get()
// if err := f.writers[q].WriteTo(batch.bufs[0], batch.dsts[0]); err != nil { if err := f.writers[q].WriteBatch(bufs, dsts); err != nil {
// f.l.WithError(err).WithField("writer", q).Error("Failed to write outgoing single-batch") f.l.Error("Failed to write outgoing batch", "error", err, "writer", q)
// }
// return
//}
w := f.writers[q]
if w.SupportsGSO() {
if segSize, ok := batchSegmentable(batch); ok {
if err := w.WriteSegmented(batch.bufs, batch.dsts[0], segSize); err != nil {
f.l.WithError(err).WithField("writer", q).Error("Failed to write outgoing GSO batch")
}
return
}
} }
if err := w.WriteBatch(batch.bufs, batch.dsts); err != nil {
f.l.WithError(err).WithField("writer", q).Error("Failed to write outgoing batch")
}
}
// batchSegmentable reports whether a batch can be emitted as a single UDP GSO
// superpacket: all packets go to the same destination, and every packet
// except possibly the last has the same length. Returns the segment size on
// success. The single-packet case is handled in flushBatch before this runs.
func batchSegmentable(b *sendBatch) (int, bool) {
segSize := len(b.bufs[0])
if segSize == 0 {
return 0, false
}
dst := b.dsts[0]
last := len(b.bufs) - 1
for i := 1; i <= last; i++ {
if b.dsts[i] != dst {
return 0, false
}
if i < last {
if len(b.bufs[i]) != segSize {
return 0, false
}
} else {
if len(b.bufs[i]) == 0 || len(b.bufs[i]) > segSize {
return 0, false
}
}
}
return segSize, true
} }
func (f *Interface) RegisterConfigChangeCallbacks(c *config.C) { func (f *Interface) RegisterConfigChangeCallbacks(c *config.C) {
@@ -442,7 +403,7 @@ func (f *Interface) reloadDisconnectInvalid(c *config.C) {
if initial || c.HasChanged("pki.disconnect_invalid") { if initial || c.HasChanged("pki.disconnect_invalid") {
f.disconnectInvalid.Store(c.GetBool("pki.disconnect_invalid", true)) f.disconnectInvalid.Store(c.GetBool("pki.disconnect_invalid", true))
if !initial { if !initial {
f.l.Infof("pki.disconnect_invalid changed to %v", f.disconnectInvalid.Load()) f.l.Info("pki.disconnect_invalid changed", "value", f.disconnectInvalid.Load())
} }
} }
} }
@@ -456,7 +417,7 @@ func (f *Interface) reloadFirewall(c *config.C) {
fw, err := NewFirewallFromConfig(f.l, f.pki.getCertState(), c) fw, err := NewFirewallFromConfig(f.l, f.pki.getCertState(), c)
if err != nil { if err != nil {
f.l.WithError(err).Error("Error while creating firewall during reload") f.l.Error("Error while creating firewall during reload", "error", err)
return return
} }
@@ -469,10 +430,11 @@ func (f *Interface) reloadFirewall(c *config.C) {
// If rulesVersion is back to zero, we have wrapped all the way around. Be // If rulesVersion is back to zero, we have wrapped all the way around. Be
// safe and just reset conntrack in this case. // safe and just reset conntrack in this case.
if fw.rulesVersion == 0 { if fw.rulesVersion == 0 {
f.l.WithField("firewallHashes", fw.GetRuleHashes()). f.l.Warn("firewall rulesVersion has overflowed, resetting conntrack",
WithField("oldFirewallHashes", oldFw.GetRuleHashes()). "firewallHashes", fw.GetRuleHashes(),
WithField("rulesVersion", fw.rulesVersion). "oldFirewallHashes", oldFw.GetRuleHashes(),
Warn("firewall rulesVersion has overflowed, resetting conntrack") "rulesVersion", fw.rulesVersion,
)
} else { } else {
fw.Conntrack = conntrack fw.Conntrack = conntrack
} }
@@ -480,10 +442,11 @@ func (f *Interface) reloadFirewall(c *config.C) {
f.firewall = fw f.firewall = fw
oldFw.Destroy() oldFw.Destroy()
f.l.WithField("firewallHashes", fw.GetRuleHashes()). f.l.Info("New firewall has been installed",
WithField("oldFirewallHashes", oldFw.GetRuleHashes()). "firewallHashes", fw.GetRuleHashes(),
WithField("rulesVersion", fw.rulesVersion). "oldFirewallHashes", oldFw.GetRuleHashes(),
Info("New firewall has been installed") "rulesVersion", fw.rulesVersion,
)
} }
func (f *Interface) reloadSendRecvError(c *config.C) { func (f *Interface) reloadSendRecvError(c *config.C) {
@@ -505,8 +468,7 @@ func (f *Interface) reloadSendRecvError(c *config.C) {
} }
} }
f.l.WithField("sendRecvError", f.sendRecvErrorConfig.String()). f.l.Info("Loaded send_recv_error config", "sendRecvError", f.sendRecvErrorConfig.String())
Info("Loaded send_recv_error config")
} }
} }
@@ -529,8 +491,7 @@ func (f *Interface) reloadAcceptRecvError(c *config.C) {
} }
} }
f.l.WithField("acceptRecvError", f.acceptRecvErrorConfig.String()). f.l.Info("Loaded accept_recv_error config", "acceptRecvError", f.acceptRecvErrorConfig.String())
Info("Loaded accept_recv_error config")
} }
} }
@@ -604,7 +565,7 @@ func (f *Interface) Close() error {
for i, u := range f.writers { for i, u := range f.writers {
err := u.Close() err := u.Close()
if err != nil { if err != nil {
f.l.WithError(err).WithField("writer", i).Error("Error while closing udp socket") f.l.Error("Error while closing udp socket", "error", err, "writer", i)
errs = append(errs, err) errs = append(errs, err)
} }
} }
+171 -75
View File
@@ -5,6 +5,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
@@ -15,10 +16,10 @@ import (
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/logging"
"github.com/slackhq/nebula/udp" "github.com/slackhq/nebula/udp"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
) )
@@ -69,18 +70,19 @@ type LightHouse struct {
// Addr's of relays that can be used by peers to access me // Addr's of relays that can be used by peers to access me
relaysForMe atomic.Pointer[[]netip.Addr] relaysForMe atomic.Pointer[[]netip.Addr]
queryChan chan netip.Addr updateTrigger chan struct{}
queryChan chan netip.Addr
calculatedRemotes atomic.Pointer[bart.Table[[]*calculatedRemote]] // Maps VpnAddr to []*calculatedRemote calculatedRemotes atomic.Pointer[bart.Table[[]*calculatedRemote]] // Maps VpnAddr to []*calculatedRemote
metrics *MessageMetrics metrics *MessageMetrics
metricHolepunchTx metrics.Counter metricHolepunchTx metrics.Counter
l *logrus.Logger l *slog.Logger
} }
// NewLightHouseFromConfig will build a Lighthouse struct from the values provided in the config object // NewLightHouseFromConfig will build a Lighthouse struct from the values provided in the config object
// addrMap should be nil unless this is during a config reload // addrMap should be nil unless this is during a config reload
func NewLightHouseFromConfig(ctx context.Context, l *logrus.Logger, c *config.C, cs *CertState, pc udp.Conn, p *Punchy) (*LightHouse, error) { func NewLightHouseFromConfig(ctx context.Context, l *slog.Logger, c *config.C, cs *CertState, pc udp.Conn, p *Punchy) (*LightHouse, error) {
amLighthouse := c.GetBool("lighthouse.am_lighthouse", false) amLighthouse := c.GetBool("lighthouse.am_lighthouse", false)
nebulaPort := uint32(c.GetInt("listen.port", 0)) nebulaPort := uint32(c.GetInt("listen.port", 0))
if amLighthouse && nebulaPort == 0 { if amLighthouse && nebulaPort == 0 {
@@ -105,6 +107,7 @@ func NewLightHouseFromConfig(ctx context.Context, l *logrus.Logger, c *config.C,
nebulaPort: nebulaPort, nebulaPort: nebulaPort,
punchConn: pc, punchConn: pc,
punchy: p, punchy: p,
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)),
l: l, l: l,
} }
@@ -131,7 +134,7 @@ func NewLightHouseFromConfig(ctx context.Context, l *logrus.Logger, c *config.C,
case *util.ContextualError: case *util.ContextualError:
v.Log(l) v.Log(l)
case error: case error:
l.WithError(err).Error("failed to reload lighthouse") l.Error("failed to reload lighthouse", "error", err)
} }
}) })
@@ -203,8 +206,10 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
//TODO: we could technically insert all returned addrs instead of just the first one if a dns lookup was used //TODO: we could technically insert all returned addrs instead of just the first one if a dns lookup was used
addr := addrs[0].Unmap() addr := addrs[0].Unmap()
if lh.myVpnNetworksTable.Contains(addr) { if lh.myVpnNetworksTable.Contains(addr) {
lh.l.WithField("addr", rawAddr).WithField("entry", i+1). lh.l.Warn("Ignoring lighthouse.advertise_addrs report because it is within the nebula network range",
Warn("Ignoring lighthouse.advertise_addrs report because it is within the nebula network range") "addr", rawAddr,
"entry", i+1,
)
continue continue
} }
@@ -222,7 +227,9 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
lh.interval.Store(int64(c.GetInt("lighthouse.interval", 10))) lh.interval.Store(int64(c.GetInt("lighthouse.interval", 10)))
if !initial { if !initial {
lh.l.Infof("lighthouse.interval changed to %v", lh.interval.Load()) lh.l.Info("lighthouse.interval changed",
"interval", lh.interval.Load(),
)
if lh.updateCancel != nil { if lh.updateCancel != nil {
// May not always have a running routine // May not always have a running routine
@@ -316,6 +323,7 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
if !initial { if !initial {
//NOTE: we are not tearing down existing lighthouse connections because they might be used for non lighthouse traffic //NOTE: we are not tearing down existing lighthouse connections because they might be used for non lighthouse traffic
lh.l.Info("lighthouse.hosts has changed") lh.l.Info("lighthouse.hosts has changed")
lh.TriggerUpdate()
} }
} }
@@ -333,9 +341,12 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
for _, v := range c.GetStringSlice("relay.relays", nil) { for _, v := range c.GetStringSlice("relay.relays", nil) {
configRIP, err := netip.ParseAddr(v) configRIP, err := netip.ParseAddr(v)
if err != nil { if err != nil {
lh.l.WithField("relay", v).WithError(err).Warn("Parse relay from config failed") lh.l.Warn("Parse relay from config failed",
"relay", v,
"error", err,
)
} else { } else {
lh.l.WithField("relay", v).Info("Read relay from config") lh.l.Info("Read relay from config", "relay", v)
relaysForMe = append(relaysForMe, configRIP) relaysForMe = append(relaysForMe, configRIP)
} }
} }
@@ -360,8 +371,10 @@ func (lh *LightHouse) parseLighthouses(c *config.C) ([]netip.Addr, error) {
} }
if !lh.myVpnNetworksTable.Contains(addr) { if !lh.myVpnNetworksTable.Contains(addr) {
lh.l.WithFields(m{"vpnAddr": addr, "networks": lh.myVpnNetworks}). lh.l.Warn("lighthouse host is not within our networks, lighthouse functionality will work but layer 3 network traffic to the lighthouse will not",
Warn("lighthouse host is not within our networks, lighthouse functionality will work but layer 3 network traffic to the lighthouse will not") "vpnAddr", addr,
"networks", lh.myVpnNetworks,
)
} }
out[i] = addr out[i] = addr
} }
@@ -432,8 +445,11 @@ func (lh *LightHouse) loadStaticMap(c *config.C, staticList map[netip.Addr]struc
} }
if !lh.myVpnNetworksTable.Contains(vpnAddr) { if !lh.myVpnNetworksTable.Contains(vpnAddr) {
lh.l.WithFields(m{"vpnAddr": vpnAddr, "networks": lh.myVpnNetworks, "entry": i + 1}). lh.l.Warn("static_host_map key is not within our networks, layer 3 network traffic to this host will not work",
Warn("static_host_map key is not within our networks, layer 3 network traffic to this host will not work") "vpnAddr", vpnAddr,
"networks", lh.myVpnNetworks,
"entry", i+1,
)
} }
vals, ok := v.([]any) vals, ok := v.([]any)
@@ -534,12 +550,13 @@ func (lh *LightHouse) DeleteVpnAddrs(allVpnAddrs []netip.Addr) {
lh.Lock() lh.Lock()
rm, ok := lh.addrMap[allVpnAddrs[0]] rm, ok := lh.addrMap[allVpnAddrs[0]]
if ok { if ok {
debugEnabled := lh.l.Enabled(context.Background(), slog.LevelDebug)
for _, addr := range allVpnAddrs { for _, addr := range allVpnAddrs {
srm := lh.addrMap[addr] srm := lh.addrMap[addr]
if srm == rm { if srm == rm {
delete(lh.addrMap, addr) delete(lh.addrMap, addr)
if lh.l.Level >= logrus.DebugLevel { if debugEnabled {
lh.l.Debugf("deleting %s from lighthouse.", addr) lh.l.Debug("deleting from lighthouse", "vpnAddr", addr)
} }
} }
} }
@@ -656,9 +673,12 @@ func (lh *LightHouse) unlockedGetRemoteList(allAddrs []netip.Addr) *RemoteList {
func (lh *LightHouse) shouldAdd(vpnAddrs []netip.Addr, to netip.Addr) bool { func (lh *LightHouse) shouldAdd(vpnAddrs []netip.Addr, to netip.Addr) bool {
allow := lh.GetRemoteAllowList().AllowAll(vpnAddrs, to) allow := lh.GetRemoteAllowList().AllowAll(vpnAddrs, to)
if lh.l.Level >= logrus.TraceLevel { if lh.l.Enabled(context.Background(), logging.LevelTrace) {
lh.l.WithField("vpnAddrs", vpnAddrs).WithField("udpAddr", to).WithField("allow", allow). lh.l.Log(context.Background(), logging.LevelTrace, "remoteAllowList.Allow",
Trace("remoteAllowList.Allow") "vpnAddrs", vpnAddrs,
"udpAddr", to,
"allow", allow,
)
} }
if !allow { if !allow {
return false return false
@@ -675,9 +695,12 @@ func (lh *LightHouse) shouldAdd(vpnAddrs []netip.Addr, to netip.Addr) bool {
func (lh *LightHouse) unlockedShouldAddV4(vpnAddr netip.Addr, to *V4AddrPort) bool { func (lh *LightHouse) unlockedShouldAddV4(vpnAddr netip.Addr, to *V4AddrPort) bool {
udpAddr := protoV4AddrPortToNetAddrPort(to) udpAddr := protoV4AddrPortToNetAddrPort(to)
allow := lh.GetRemoteAllowList().Allow(vpnAddr, udpAddr.Addr()) allow := lh.GetRemoteAllowList().Allow(vpnAddr, udpAddr.Addr())
if lh.l.Level >= logrus.TraceLevel { if lh.l.Enabled(context.Background(), logging.LevelTrace) {
lh.l.WithField("vpnAddr", vpnAddr).WithField("udpAddr", udpAddr).WithField("allow", allow). lh.l.Log(context.Background(), logging.LevelTrace, "remoteAllowList.Allow",
Trace("remoteAllowList.Allow") "vpnAddr", vpnAddr,
"udpAddr", udpAddr,
"allow", allow,
)
} }
if !allow { if !allow {
@@ -695,9 +718,12 @@ func (lh *LightHouse) unlockedShouldAddV4(vpnAddr netip.Addr, to *V4AddrPort) bo
func (lh *LightHouse) unlockedShouldAddV6(vpnAddr netip.Addr, to *V6AddrPort) bool { func (lh *LightHouse) unlockedShouldAddV6(vpnAddr netip.Addr, to *V6AddrPort) bool {
udpAddr := protoV6AddrPortToNetAddrPort(to) udpAddr := protoV6AddrPortToNetAddrPort(to)
allow := lh.GetRemoteAllowList().Allow(vpnAddr, udpAddr.Addr()) allow := lh.GetRemoteAllowList().Allow(vpnAddr, udpAddr.Addr())
if lh.l.Level >= logrus.TraceLevel { if lh.l.Enabled(context.Background(), logging.LevelTrace) {
lh.l.WithField("vpnAddr", vpnAddr).WithField("udpAddr", udpAddr).WithField("allow", allow). lh.l.Log(context.Background(), logging.LevelTrace, "remoteAllowList.Allow",
Trace("remoteAllowList.Allow") "vpnAddr", vpnAddr,
"udpAddr", udpAddr,
"allow", allow,
)
} }
if !allow { if !allow {
@@ -772,8 +798,10 @@ func (lh *LightHouse) innerQueryServer(addr netip.Addr, nb, out []byte) {
if v == cert.Version1 { if v == cert.Version1 {
if !addr.Is4() { if !addr.Is4() {
lh.l.WithField("queryVpnAddr", addr).WithField("lighthouseAddr", lhVpnAddr). lh.l.Error("Can't query lighthouse for v6 address using a v1 protocol",
Error("Can't query lighthouse for v6 address using a v1 protocol") "queryVpnAddr", addr,
"lighthouseAddr", lhVpnAddr,
)
continue continue
} }
@@ -784,9 +812,11 @@ func (lh *LightHouse) innerQueryServer(addr netip.Addr, nb, out []byte) {
v1Query, err = msg.Marshal() v1Query, err = msg.Marshal()
if err != nil { if err != nil {
lh.l.WithError(err).WithField("queryVpnAddr", addr). lh.l.Error("Failed to marshal lighthouse v1 query payload",
WithField("lighthouseAddr", lhVpnAddr). "error", err,
Error("Failed to marshal lighthouse v1 query payload") "queryVpnAddr", addr,
"lighthouseAddr", lhVpnAddr,
)
continue continue
} }
} }
@@ -801,9 +831,11 @@ func (lh *LightHouse) innerQueryServer(addr netip.Addr, nb, out []byte) {
v2Query, err = msg.Marshal() v2Query, err = msg.Marshal()
if err != nil { if err != nil {
lh.l.WithError(err).WithField("queryVpnAddr", addr). lh.l.Error("Failed to marshal lighthouse v2 query payload",
WithField("lighthouseAddr", lhVpnAddr). "error", err,
Error("Failed to marshal lighthouse v2 query payload") "queryVpnAddr", addr,
"lighthouseAddr", lhVpnAddr,
)
continue continue
} }
} }
@@ -812,7 +844,11 @@ func (lh *LightHouse) innerQueryServer(addr netip.Addr, nb, out []byte) {
queried++ queried++
} else { } else {
lh.l.Debugf("Can not query lighthouse for %v using unknown protocol version: %v", addr, v) lh.l.Debug("unsupported protocol version",
"op", "query",
"queryVpnAddr", addr,
"version", v,
)
continue continue
} }
} }
@@ -841,11 +877,24 @@ func (lh *LightHouse) StartUpdateWorker() {
return return
case <-clockSource.C: case <-clockSource.C:
continue continue
case <-lh.updateTrigger:
continue
} }
} }
}() }()
} }
// TriggerUpdate requests an immediate lighthouse update. This is a non-blocking
// operation intended to be called after a handshake completes with a lighthouse,
// so the lighthouse has our current addresses without waiting for the next
// periodic update.
func (lh *LightHouse) TriggerUpdate() {
select {
case lh.updateTrigger <- struct{}{}:
default:
}
}
func (lh *LightHouse) SendUpdate() { func (lh *LightHouse) SendUpdate() {
var v4 []*V4AddrPort var v4 []*V4AddrPort
var v6 []*V6AddrPort var v6 []*V6AddrPort
@@ -891,8 +940,9 @@ func (lh *LightHouse) SendUpdate() {
if v == cert.Version1 { if v == cert.Version1 {
if v1Update == nil { if v1Update == nil {
if !lh.myVpnNetworks[0].Addr().Is4() { if !lh.myVpnNetworks[0].Addr().Is4() {
lh.l.WithField("lighthouseAddr", lhVpnAddr). lh.l.Warn("cannot update lighthouse using v1 protocol without an IPv4 address",
Warn("cannot update lighthouse using v1 protocol without an IPv4 address") "lighthouseAddr", lhVpnAddr,
)
continue continue
} }
var relays []uint32 var relays []uint32
@@ -916,8 +966,10 @@ func (lh *LightHouse) SendUpdate() {
v1Update, err = msg.Marshal() v1Update, err = msg.Marshal()
if err != nil { if err != nil {
lh.l.WithError(err).WithField("lighthouseAddr", lhVpnAddr). lh.l.Error("Error while marshaling for lighthouse v1 update",
Error("Error while marshaling for lighthouse v1 update") "error", err,
"lighthouseAddr", lhVpnAddr,
)
continue continue
} }
} }
@@ -943,8 +995,10 @@ func (lh *LightHouse) SendUpdate() {
v2Update, err = msg.Marshal() v2Update, err = msg.Marshal()
if err != nil { if err != nil {
lh.l.WithError(err).WithField("lighthouseAddr", lhVpnAddr). lh.l.Error("Error while marshaling for lighthouse v2 update",
Error("Error while marshaling for lighthouse v2 update") "error", err,
"lighthouseAddr", lhVpnAddr,
)
continue continue
} }
} }
@@ -953,7 +1007,10 @@ func (lh *LightHouse) SendUpdate() {
updated++ updated++
} else { } else {
lh.l.Debugf("Can not update lighthouse using unknown protocol version: %v", v) lh.l.Debug("unsupported protocol version",
"op", "update",
"version", v,
)
continue continue
} }
} }
@@ -967,7 +1024,7 @@ type LightHouseHandler struct {
out []byte out []byte
pb []byte pb []byte
meta *NebulaMeta meta *NebulaMeta
l *logrus.Logger l *slog.Logger
} }
func (lh *LightHouse) NewRequestHandler() *LightHouseHandler { func (lh *LightHouse) NewRequestHandler() *LightHouseHandler {
@@ -1016,14 +1073,19 @@ func (lhh *LightHouseHandler) HandleRequest(rAddr netip.AddrPort, fromVpnAddrs [
n := lhh.resetMeta() n := lhh.resetMeta()
err := n.Unmarshal(p) err := n.Unmarshal(p)
if err != nil { if err != nil {
lhh.l.WithError(err).WithField("vpnAddrs", fromVpnAddrs).WithField("udpAddr", rAddr). lhh.l.Error("Failed to unmarshal lighthouse packet",
Error("Failed to unmarshal lighthouse packet") "error", err,
"vpnAddrs", fromVpnAddrs,
"udpAddr", rAddr,
)
return return
} }
if n.Details == nil { if n.Details == nil {
lhh.l.WithField("vpnAddrs", fromVpnAddrs).WithField("udpAddr", rAddr). lhh.l.Error("Invalid lighthouse update",
Error("Invalid lighthouse update") "vpnAddrs", fromVpnAddrs,
"udpAddr", rAddr,
)
return return
} }
@@ -1051,25 +1113,29 @@ func (lhh *LightHouseHandler) HandleRequest(rAddr netip.AddrPort, fromVpnAddrs [
func (lhh *LightHouseHandler) handleHostQuery(n *NebulaMeta, fromVpnAddrs []netip.Addr, addr netip.AddrPort, w EncWriter) { func (lhh *LightHouseHandler) handleHostQuery(n *NebulaMeta, fromVpnAddrs []netip.Addr, addr netip.AddrPort, w EncWriter) {
// Exit if we don't answer queries // Exit if we don't answer queries
if !lhh.lh.amLighthouse { if !lhh.lh.amLighthouse {
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.Debugln("I don't answer queries, but received from: ", addr) lhh.l.Debug("I don't answer queries, but received one", "from", addr)
} }
return return
} }
queryVpnAddr, useVersion, err := n.Details.GetVpnAddrAndVersion() queryVpnAddr, useVersion, err := n.Details.GetVpnAddrAndVersion()
if err != nil { if err != nil {
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.WithField("from", fromVpnAddrs).WithField("details", n.Details). lhh.l.Debug("Dropping malformed HostQuery",
Debugln("Dropping malformed HostQuery") "from", fromVpnAddrs,
"details", n.Details,
)
} }
return return
} }
if useVersion == cert.Version1 && queryVpnAddr.Is6() { if useVersion == cert.Version1 && queryVpnAddr.Is6() {
// this case really shouldn't be possible to represent, but reject it anyway. // this case really shouldn't be possible to represent, but reject it anyway.
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.WithField("vpnAddrs", fromVpnAddrs).WithField("queryVpnAddr", queryVpnAddr). lhh.l.Debug("invalid vpn addr for v1 handleHostQuery",
Debugln("invalid vpn addr for v1 handleHostQuery") "vpnAddrs", fromVpnAddrs,
"queryVpnAddr", queryVpnAddr,
)
} }
return return
} }
@@ -1094,7 +1160,10 @@ func (lhh *LightHouseHandler) handleHostQuery(n *NebulaMeta, fromVpnAddrs []neti
} }
if err != nil { if err != nil {
lhh.l.WithError(err).WithField("vpnAddrs", fromVpnAddrs).Error("Failed to marshal lighthouse host query reply") lhh.l.Error("Failed to marshal lighthouse host query reply",
"error", err,
"vpnAddrs", fromVpnAddrs,
)
return return
} }
@@ -1122,8 +1191,10 @@ func (lhh *LightHouseHandler) sendHostPunchNotification(n *NebulaMeta, fromVpnAd
if ok { if ok {
whereToPunch = newDest whereToPunch = newDest
} else { } else {
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.WithField("to", crt.Networks()).Debugln("unable to punch to host, no addresses in common") lhh.l.Debug("unable to punch to host, no addresses in common",
"to", crt.Networks(),
)
} }
} }
} }
@@ -1149,7 +1220,10 @@ func (lhh *LightHouseHandler) sendHostPunchNotification(n *NebulaMeta, fromVpnAd
} }
if err != nil { if err != nil {
lhh.l.WithError(err).WithField("vpnAddrs", fromVpnAddrs).Error("Failed to marshal lighthouse host was queried for") lhh.l.Error("Failed to marshal lighthouse host was queried for",
"error", err,
"vpnAddrs", fromVpnAddrs,
)
return return
} }
@@ -1191,8 +1265,11 @@ func (lhh *LightHouseHandler) coalesceAnswers(v cert.Version, c *cache, n *Nebul
n.Details.RelayVpnAddrs = append(n.Details.RelayVpnAddrs, netAddrToProtoAddr(r)) n.Details.RelayVpnAddrs = append(n.Details.RelayVpnAddrs, netAddrToProtoAddr(r))
} }
} else { } else {
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.WithField("version", v).Debug("unsupported protocol version") lhh.l.Debug("unsupported protocol version",
"op", "coalesceAnswers",
"version", v,
)
} }
} }
} }
@@ -1205,8 +1282,11 @@ func (lhh *LightHouseHandler) handleHostQueryReply(n *NebulaMeta, fromVpnAddrs [
certVpnAddr, _, err := n.Details.GetVpnAddrAndVersion() certVpnAddr, _, err := n.Details.GetVpnAddrAndVersion()
if err != nil { if err != nil {
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.WithError(err).WithField("vpnAddrs", fromVpnAddrs).Error("dropping malformed HostQueryReply") lhh.l.Error("dropping malformed HostQueryReply",
"error", err,
"vpnAddrs", fromVpnAddrs,
)
} }
return return
} }
@@ -1231,8 +1311,8 @@ func (lhh *LightHouseHandler) handleHostQueryReply(n *NebulaMeta, fromVpnAddrs [
func (lhh *LightHouseHandler) handleHostUpdateNotification(n *NebulaMeta, fromVpnAddrs []netip.Addr, w EncWriter) { func (lhh *LightHouseHandler) handleHostUpdateNotification(n *NebulaMeta, fromVpnAddrs []netip.Addr, w EncWriter) {
if !lhh.lh.amLighthouse { if !lhh.lh.amLighthouse {
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.Debugln("I am not a lighthouse, do not take host updates: ", fromVpnAddrs) lhh.l.Debug("I am not a lighthouse, do not take host updates", "from", fromVpnAddrs)
} }
return return
} }
@@ -1255,8 +1335,11 @@ func (lhh *LightHouseHandler) handleHostUpdateNotification(n *NebulaMeta, fromVp
//Simple check that the host sent this not someone else, if detailsVpnAddr is filled //Simple check that the host sent this not someone else, if detailsVpnAddr is filled
if detailsVpnAddr.IsValid() && !slices.Contains(fromVpnAddrs, detailsVpnAddr) { if detailsVpnAddr.IsValid() && !slices.Contains(fromVpnAddrs, detailsVpnAddr) {
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.WithField("vpnAddrs", fromVpnAddrs).WithField("answer", detailsVpnAddr).Debugln("Host sent invalid update") lhh.l.Debug("Host sent invalid update",
"vpnAddrs", fromVpnAddrs,
"answer", detailsVpnAddr,
)
} }
return return
} }
@@ -1278,7 +1361,9 @@ func (lhh *LightHouseHandler) handleHostUpdateNotification(n *NebulaMeta, fromVp
switch useVersion { switch useVersion {
case cert.Version1: case cert.Version1:
if !fromVpnAddrs[0].Is4() { if !fromVpnAddrs[0].Is4() {
lhh.l.WithField("vpnAddrs", fromVpnAddrs).Error("Can not send HostUpdateNotificationAck for a ipv6 vpn ip in a v1 message") lhh.l.Error("Can not send HostUpdateNotificationAck for a ipv6 vpn ip in a v1 message",
"vpnAddrs", fromVpnAddrs,
)
return return
} }
vpnAddrB := fromVpnAddrs[0].As4() vpnAddrB := fromVpnAddrs[0].As4()
@@ -1286,13 +1371,16 @@ func (lhh *LightHouseHandler) handleHostUpdateNotification(n *NebulaMeta, fromVp
case cert.Version2: case cert.Version2:
// do nothing, we want to send a blank message // do nothing, we want to send a blank message
default: default:
lhh.l.WithField("useVersion", useVersion).Error("invalid protocol version") lhh.l.Error("invalid protocol version", "useVersion", useVersion)
return return
} }
ln, err := n.MarshalTo(lhh.pb) ln, err := n.MarshalTo(lhh.pb)
if err != nil { if err != nil {
lhh.l.WithError(err).WithField("vpnAddrs", fromVpnAddrs).Error("Failed to marshal lighthouse host update ack") lhh.l.Error("Failed to marshal lighthouse host update ack",
"error", err,
"vpnAddrs", fromVpnAddrs,
)
return return
} }
@@ -1309,8 +1397,11 @@ func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpn
detailsVpnAddr, _, err := n.Details.GetVpnAddrAndVersion() detailsVpnAddr, _, err := n.Details.GetVpnAddrAndVersion()
if err != nil { if err != nil {
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.WithField("details", n.Details).WithError(err).Debugln("dropping invalid HostPunchNotification") lhh.l.Debug("dropping invalid HostPunchNotification",
"details", n.Details,
"error", err,
)
} }
return return
} }
@@ -1327,8 +1418,11 @@ func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpn
lhh.lh.punchConn.WriteTo(empty, vpnPeer) lhh.lh.punchConn.WriteTo(empty, vpnPeer)
}() }()
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.Debugf("Punching on %v for %v", vpnPeer, logVpnAddr) lhh.l.Debug("Punching",
"vpnPeer", vpnPeer,
"logVpnAddr", logVpnAddr,
)
} }
} }
@@ -1353,8 +1447,10 @@ func (lhh *LightHouseHandler) handleHostPunchNotification(n *NebulaMeta, fromVpn
if lhh.lh.punchy.GetRespond() { if lhh.lh.punchy.GetRespond() {
go func() { go func() {
time.Sleep(lhh.lh.punchy.GetRespondDelay()) time.Sleep(lhh.lh.punchy.GetRespondDelay())
if lhh.l.Level >= logrus.DebugLevel { if lhh.l.Enabled(context.Background(), slog.LevelDebug) {
lhh.l.Debugf("Sending a nebula test packet to vpn addr %s", detailsVpnAddr) lhh.l.Debug("Sending a nebula test packet",
"vpnAddr", detailsVpnAddr,
)
} }
//NOTE: we have to allocate a new output buffer here since we are spawning a new goroutine //NOTE: we have to allocate a new output buffer here since we are spawning a new goroutine
// for each punchBack packet. We should move this into a timerwheel or a single goroutine // for each punchBack packet. We should move this into a timerwheel or a single goroutine
-45
View File
@@ -1,45 +0,0 @@
package nebula
import (
"fmt"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config"
)
func configLogger(l *logrus.Logger, c *config.C) error {
// set up our logging level
logLevel, err := logrus.ParseLevel(strings.ToLower(c.GetString("logging.level", "info")))
if err != nil {
return fmt.Errorf("%s; possible levels: %s", err, logrus.AllLevels)
}
l.SetLevel(logLevel)
disableTimestamp := c.GetBool("logging.disable_timestamp", false)
timestampFormat := c.GetString("logging.timestamp_format", "")
fullTimestamp := (timestampFormat != "")
if timestampFormat == "" {
timestampFormat = time.RFC3339
}
logFormat := strings.ToLower(c.GetString("logging.format", "text"))
switch logFormat {
case "text":
l.Formatter = &logrus.TextFormatter{
TimestampFormat: timestampFormat,
FullTimestamp: fullTimestamp,
DisableTimestamp: disableTimestamp,
}
case "json":
l.Formatter = &logrus.JSONFormatter{
TimestampFormat: timestampFormat,
DisableTimestamp: disableTimestamp,
}
default:
return fmt.Errorf("unknown log format `%s`. possible formats: %s", logFormat, []string{"text", "json"})
}
return nil
}
+233
View File
@@ -0,0 +1,233 @@
// Package logging wires the nebula runtime-reconfigurable slog handler used
// by nebula.Main and the nebula CLI binaries. Callers build a logger with
// NewLogger, then call ApplyConfig at startup and from a config reload
// callback to push logging.level, logging.format, and
// logging.disable_timestamp changes onto the logger without rebuilding it.
package logging
import (
"context"
"fmt"
"io"
"log/slog"
"strings"
"sync/atomic"
"time"
)
// Config is the subset of *config.C that ApplyConfig reads. Declaring it
// here keeps the logging package from depending on config directly, which
// would cycle through the shared test helpers (test.NewLogger imports
// logging, and config's tests import test). *config.C satisfies this
// interface structurally with no adapter.
type Config interface {
GetString(key, def string) string
GetBool(key string, def bool) bool
}
// LevelTrace is a custom slog level below Debug, used when logging.level is
// "trace". slog has no builtin trace level; the value is one step below
// slog.LevelDebug in slog's 4-point spacing.
const LevelTrace = slog.Level(-8)
// NewLogger returns a *slog.Logger whose level, format, and timestamp
// emission can be reconfigured at runtime via ApplyConfig and the SSH debug
// commands. The default configuration is info-level text output so log
// calls made before ApplyConfig runs still produce output. Timestamps
// follow slog's default RFC3339Nano format; set logging.disable_timestamp
// in config to suppress them.
//
// ApplyConfig and the SSH commands discover the reconfig surface via
// structural type-assertion on l.Handler(), so replacement implementations
// (tests, platform-specific sinks) need only implement the subset of
// {SetLevel(slog.Level), SetFormat(string) error, SetDisableTimestamp(bool)}
// they care about. Callers that pass a plain *slog.Logger without these
// methods get a silent no-op; reconfiguration is always opt-in.
func NewLogger(w io.Writer) *slog.Logger {
return slog.New(NewHandler(w))
}
// NewHandler builds the *Handler that NewLogger wraps. Exported for
// platform-specific sinks (notably cmd/nebula-service/logs_windows.go)
// that want to wrap the handler with extra behavior, such as tagging each
// record with its Event Log severity, while still benefiting from all the
// level / format / timestamp / WithAttrs machinery implemented here.
func NewHandler(w io.Writer) *Handler {
root := &handlerRoot{}
root.level.Set(slog.LevelInfo)
opts := &slog.HandlerOptions{Level: &root.level}
return &Handler{
root: root,
text: slog.NewTextHandler(w, opts),
json: slog.NewJSONHandler(w, opts),
}
}
// handlerRoot carries the reconfiguration state shared by every logger
// derived from a NewHandler call. All fields are consulted on the log
// path and updated lock-free.
type handlerRoot struct {
level slog.LevelVar
disableTimestamp atomic.Bool
// jsonMode picks which of the pre-derived inner handlers Handler.Handle
// dispatches to. Flipping it propagates instantly to every derived logger
// without rebuilding or chain-replaying anything.
jsonMode atomic.Bool
}
// Handler is the slog.Handler returned by NewHandler. It holds two
// pre-derived slog handlers -- one text, one json -- both built from the
// same accumulated WithAttrs/WithGroup state. Handle picks which one to
// dispatch to based on handlerRoot.jsonMode, so a SetFormat call takes
// effect immediately across the whole process without having to rebuild
// any derived loggers.
type Handler struct {
root *handlerRoot
text slog.Handler
json slog.Handler
}
func (h *Handler) Enabled(_ context.Context, l slog.Level) bool {
return h.root.level.Level() <= l
}
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
if h.root.disableTimestamp.Load() {
r.Time = time.Time{}
}
if h.root.jsonMode.Load() {
return h.json.Handle(ctx, r)
}
return h.text.Handle(ctx, r)
}
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return h
}
return &Handler{
root: h.root,
text: h.text.WithAttrs(attrs),
json: h.json.WithAttrs(attrs),
}
}
func (h *Handler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}
return &Handler{
root: h.root,
text: h.text.WithGroup(name),
json: h.json.WithGroup(name),
}
}
// SetLevel updates the effective log level. Propagates to every derived
// logger via the shared LevelVar.
func (h *Handler) SetLevel(level slog.Level) { h.root.level.Set(level) }
// GetLevel reports the current log level.
func (h *Handler) GetLevel() slog.Level { return h.root.level.Level() }
// SetFormat flips the output format atomically. Valid formats are "text"
// and "json". Every derived logger sees the new format on its next Handle
// call; no rebuild or registration is required.
func (h *Handler) SetFormat(format string) error {
switch format {
case "text":
h.root.jsonMode.Store(false)
case "json":
h.root.jsonMode.Store(true)
default:
return fmt.Errorf("unknown log format `%s`. possible formats: %s", format, []string{"text", "json"})
}
return nil
}
// GetFormat reports the currently selected format name.
func (h *Handler) GetFormat() string {
if h.root.jsonMode.Load() {
return "json"
}
return "text"
}
// SetDisableTimestamp toggles whether Handle zeroes r.Time before
// dispatching (slog's builtin text/json handlers skip emitting the time
// attribute on a zero time).
func (h *Handler) SetDisableTimestamp(v bool) { h.root.disableTimestamp.Store(v) }
// ApplyConfig reads logging.level, logging.format, and (optionally)
// logging.disable_timestamp from c and applies them to l. The reconfig
// surface is discovered via structural type-assertion on l.Handler(), so
// foreign handlers silently opt out of whichever capabilities they do not
// implement.
//
// nebula.Main does NOT call this function on your behalf; callers that want
// config-driven log level / format / timestamp updates invoke it at
// startup and register it as a reload callback themselves. This keeps the
// library from mutating an embedder's logger without their say-so.
func ApplyConfig(l *slog.Logger, c Config) error {
h := l.Handler()
lvl, err := ParseLevel(strings.ToLower(c.GetString("logging.level", "info")))
if err != nil {
return err
}
if ls, ok := h.(interface{ SetLevel(slog.Level) }); ok {
ls.SetLevel(lvl)
}
format := strings.ToLower(c.GetString("logging.format", "text"))
if fs, ok := h.(interface{ SetFormat(string) error }); ok {
if err := fs.SetFormat(format); err != nil {
return err
}
}
if ts, ok := h.(interface{ SetDisableTimestamp(bool) }); ok {
ts.SetDisableTimestamp(c.GetBool("logging.disable_timestamp", false))
}
return nil
}
// ParseLevel converts a config-string level name ("trace", "debug", "info",
// "warn"/"warning", "error", "fatal"/"panic") to a slog.Level. "fatal" and
// "panic" are accepted for backwards compatibility with pre-slog configs
// and both map to slog.LevelError.
func ParseLevel(s string) (slog.Level, error) {
switch s {
case "trace":
return LevelTrace, nil
case "debug":
return slog.LevelDebug, nil
case "info":
return slog.LevelInfo, nil
case "warn", "warning":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
case "fatal", "panic":
return slog.LevelError, nil
default:
return 0, fmt.Errorf("not a valid logging level: %q", s)
}
}
// LevelName returns a human-readable name for a slog.Level matching the
// strings accepted by ParseLevel.
func LevelName(l slog.Level) string {
switch {
case l <= LevelTrace:
return "trace"
case l <= slog.LevelDebug:
return "debug"
case l <= slog.LevelInfo:
return "info"
case l <= slog.LevelWarn:
return "warn"
default:
return "error"
}
}
+90
View File
@@ -0,0 +1,90 @@
package logging
import (
"context"
"io"
"log/slog"
"testing"
)
// BenchmarkLogger_* compare the handler returned by NewLogger against a
// stock slog text handler. The key thing we care about is the per-log
// cost on a logger that has been derived via .With(), because that is the
// shape subsystems store on their structs (HostInfo.logger(),
// lh.l.With("subsystem", ...), etc.) and call from hot paths.
func BenchmarkLogger_Stock_RootInfo(b *testing.B) {
l := slog.New(slog.DiscardHandler)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
l.Info("hello", "i", i)
}
}
func BenchmarkLogger_Nebula_RootInfo(b *testing.B) {
l := NewLogger(io.Discard)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
l.Info("hello", "i", i)
}
}
func BenchmarkLogger_Stock_DerivedInfo(b *testing.B) {
l := slog.New(slog.DiscardHandler).With(
"subsystem", "bench",
"localIndex", 1234,
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
l.Info("hello", "i", i)
}
}
func BenchmarkLogger_Nebula_DerivedInfo(b *testing.B) {
l := NewLogger(io.Discard).With(
"subsystem", "bench",
"localIndex", 1234,
)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
l.Info("hello", "i", i)
}
}
// Gated-off-path benchmarks: mimic the typical hot-path shape
// `if l.Enabled(ctx, slog.LevelDebug) { ... }` where the log is gated below
// the active level. This is the dominant pattern in inside.go/outside.go and
// what we pay on every packet.
func BenchmarkLogger_Stock_DerivedEnabledGateMiss(b *testing.B) {
l := slog.New(slog.DiscardHandler).With(
"subsystem", "bench",
"localIndex", 1234,
)
ctx := context.Background()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if l.Enabled(ctx, slog.LevelDebug) {
l.Debug("hello", "i", i)
}
}
}
func BenchmarkLogger_Nebula_DerivedEnabledGateMiss(b *testing.B) {
l := NewLogger(io.Discard).With(
"subsystem", "bench",
"localIndex", 1234,
)
ctx := context.Background()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if l.Enabled(ctx, slog.LevelDebug) {
l.Debug("hello", "i", i)
}
}
}
+17 -45
View File
@@ -3,13 +3,13 @@ package nebula
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"runtime/debug" "runtime/debug"
"strings" "strings"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay" "github.com/slackhq/nebula/overlay"
"github.com/slackhq/nebula/sshd" "github.com/slackhq/nebula/sshd"
@@ -20,7 +20,7 @@ import (
type m = map[string]any type m = map[string]any
func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logger, deviceFactory overlay.DeviceFactory) (retcon *Control, reterr error) { func Main(c *config.C, configTest bool, buildVersion string, l *slog.Logger, deviceFactory overlay.DeviceFactory) (retcon *Control, reterr error) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
// Automatically cancel the context if Main returns an error, to signal all created goroutines to quit. // Automatically cancel the context if Main returns an error, to signal all created goroutines to quit.
defer func() { defer func() {
@@ -33,11 +33,6 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
buildVersion = moduleVersion() buildVersion = moduleVersion()
} }
l := logger
l.Formatter = &logrus.TextFormatter{
FullTimestamp: true,
}
// Print the config if in test, the exit comes later // Print the config if in test, the exit comes later
if configTest { if configTest {
b, err := yaml.Marshal(c.Settings) b, err := yaml.Marshal(c.Settings)
@@ -46,21 +41,9 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
} }
// Print the final config // Print the final config
l.Println(string(b)) l.Info(string(b))
} }
err := configLogger(l, c)
if err != nil {
return nil, util.ContextualizeIfNeeded("Failed to configure the logger", err)
}
c.RegisterReloadCallback(func(c *config.C) {
err := configLogger(l, c)
if err != nil {
l.WithError(err).Error("Failed to configure the logger")
}
})
pki, err := NewPKIFromConfig(l, c) pki, err := NewPKIFromConfig(l, c)
if err != nil { if err != nil {
return nil, util.ContextualizeIfNeeded("Failed to load PKI from config", err) return nil, util.ContextualizeIfNeeded("Failed to load PKI from config", err)
@@ -70,9 +53,9 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
if err != nil { if err != nil {
return nil, util.ContextualizeIfNeeded("Error while loading firewall rules", err) return nil, util.ContextualizeIfNeeded("Error while loading firewall rules", err)
} }
l.WithField("firewallHashes", fw.GetRuleHashes()).Info("Firewall started") l.Info("Firewall started", "firewallHashes", fw.GetRuleHashes())
ssh, err := sshd.NewSSHServer(l.WithField("subsystem", "sshd")) ssh, err := sshd.NewSSHServer(l.With("subsystem", "sshd"))
if err != nil { if err != nil {
return nil, util.ContextualizeIfNeeded("Error while creating SSH server", err) return nil, util.ContextualizeIfNeeded("Error while creating SSH server", err)
} }
@@ -81,7 +64,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
if c.GetBool("sshd.enabled", false) { if c.GetBool("sshd.enabled", false) {
sshStart, err = configSSH(l, ssh, c) sshStart, err = configSSH(l, ssh, c)
if err != nil { if err != nil {
l.WithError(err).Warn("Failed to configure sshd, ssh debugging will not be available") l.Warn("Failed to configure sshd, ssh debugging will not be available", "error", err)
sshStart = nil sshStart = nil
} }
} }
@@ -99,7 +82,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
routines = 1 routines = 1
} }
if routines > 1 { if routines > 1 {
l.WithField("routines", routines).Info("Using multiple routines") l.Info("Using multiple routines", "routines", routines)
} }
} else { } else {
// deprecated and undocumented // deprecated and undocumented
@@ -107,7 +90,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
udpQueues := c.GetInt("listen.routines", 1) udpQueues := c.GetInt("listen.routines", 1)
routines = max(tunQueues, udpQueues) routines = max(tunQueues, udpQueues)
if routines != 1 { if routines != 1 {
l.WithField("routines", routines).Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead") l.Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead", "routines", routines)
} }
} }
@@ -120,7 +103,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
conntrackCacheTimeout = 1 * time.Second conntrackCacheTimeout = 1 * time.Second
} }
if conntrackCacheTimeout > 0 { if conntrackCacheTimeout > 0 {
l.WithField("duration", conntrackCacheTimeout).Info("Using routine-local conntrack cache") l.Info("Using routine-local conntrack cache", "duration", conntrackCacheTimeout)
} }
var tun overlay.Device var tun overlay.Device
@@ -166,7 +149,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
} }
for i := 0; i < routines; i++ { for i := 0; i < routines; i++ {
l.Infof("listening on %v", netip.AddrPortFrom(listenHost, uint16(port))) l.Info("listening", "addr", netip.AddrPortFrom(listenHost, uint16(port)))
udpServer, err := udp.NewListener(l, listenHost, port, routines > 1, c.GetInt("listen.batch", 64)) udpServer, err := udp.NewListener(l, listenHost, port, routines > 1, c.GetInt("listen.batch", 64))
if err != nil { if err != nil {
return nil, util.NewContextualError("Failed to open udp listener", m{"queue": i}, err) return nil, util.NewContextualError("Failed to open udp listener", m{"queue": i}, err)
@@ -215,13 +198,9 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
handshakeManager := NewHandshakeManager(l, hostMap, lightHouse, udpConns[0], handshakeConfig) handshakeManager := NewHandshakeManager(l, hostMap, lightHouse, udpConns[0], handshakeConfig)
lightHouse.handshakeTrigger = handshakeManager.trigger lightHouse.handshakeTrigger = handshakeManager.trigger
serveDns := false ds, err := newDnsServerFromConfig(ctx, l, pki.getCertState(), hostMap, c)
if c.GetBool("lighthouse.serve_dns", false) { if err != nil {
if c.GetBool("lighthouse.am_lighthouse", false) { l.Warn("Failed to start DNS responder", "error", err)
serveDns = true
} else {
l.Warn("DNS server refusing to run because this host is not a lighthouse.")
}
} }
ifConfig := &InterfaceConfig{ ifConfig := &InterfaceConfig{
@@ -230,7 +209,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
Outside: udpConns[0], Outside: udpConns[0],
pki: pki, pki: pki,
Firewall: fw, Firewall: fw,
ServeDns: serveDns, DnsServer: ds,
HandshakeManager: handshakeManager, HandshakeManager: handshakeManager,
connectionManager: connManager, connectionManager: connManager,
lightHouse: lightHouse, lightHouse: lightHouse,
@@ -267,7 +246,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
go handshakeManager.Run(ctx) go handshakeManager.Run(ctx)
} }
statsStart, err := startStats(l, c, buildVersion, configTest) stats, err := newStatsServerFromConfig(ctx, l, c, buildVersion, configTest)
if err != nil { if err != nil {
return nil, util.ContextualizeIfNeeded("Failed to start stats emitter", err) return nil, util.ContextualizeIfNeeded("Failed to start stats emitter", err)
} }
@@ -280,13 +259,6 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
attachCommands(l, c, ssh, ifce) attachCommands(l, c, ssh, ifce)
// Start DNS server last to allow using the nebula IP as lighthouse.dns.host
var dnsStart func()
if lightHouse.amLighthouse && serveDns {
l.Debugln("Starting dns server")
dnsStart = dnsMain(l, pki.getCertState(), hostMap, c)
}
return &Control{ return &Control{
state: StateReady, state: StateReady,
f: ifce, f: ifce,
@@ -294,8 +266,8 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
sshStart: sshStart, sshStart: sshStart,
statsStart: statsStart, statsStart: stats.Start,
dnsStart: dnsStart, dnsStart: ds.Start,
lighthouseStart: lightHouse.StartUpdateWorker, lighthouseStart: lightHouse.StartUpdateWorker,
connectionManagerStart: connManager.Start, connectionManagerStart: connManager.Start,
}, nil }, nil
+6 -8
View File
@@ -15,14 +15,12 @@ type endianness interface {
var noiseEndianness endianness = binary.BigEndian var noiseEndianness endianness = binary.BigEndian
type NebulaCipherState struct { type NebulaCipherState struct {
c noise.Cipher c cipher.AEAD
//k [32]byte
//n uint64
} }
func NewNebulaCipherState(s *noise.CipherState) *NebulaCipherState { func NewNebulaCipherState(s *noise.CipherState) *NebulaCipherState {
return &NebulaCipherState{c: s.Cipher()} x := s.Cipher()
return &NebulaCipherState{c: x.(cipher.AEAD)}
} }
// EncryptDanger encrypts and authenticates a given payload. // EncryptDanger encrypts and authenticates a given payload.
@@ -46,7 +44,7 @@ func (s *NebulaCipherState) EncryptDanger(out, ad, plaintext []byte, n uint64, n
nb[2] = 0 nb[2] = 0
nb[3] = 0 nb[3] = 0
noiseEndianness.PutUint64(nb[4:], n) noiseEndianness.PutUint64(nb[4:], n)
out = s.c.(cipher.AEAD).Seal(out, nb, plaintext, ad) out = s.c.Seal(out, nb, plaintext, ad)
//l.Debugf("Encryption: outlen: %d, nonce: %d, ad: %s, plainlen %d", len(out), n, ad, len(plaintext)) //l.Debugf("Encryption: outlen: %d, nonce: %d, ad: %s, plainlen %d", len(out), n, ad, len(plaintext))
return out, nil return out, nil
} else { } else {
@@ -61,7 +59,7 @@ func (s *NebulaCipherState) DecryptDanger(out, ad, ciphertext []byte, n uint64,
nb[2] = 0 nb[2] = 0
nb[3] = 0 nb[3] = 0
noiseEndianness.PutUint64(nb[4:], n) noiseEndianness.PutUint64(nb[4:], n)
return s.c.(cipher.AEAD).Open(out, nb, ciphertext, ad) return s.c.Open(out, nb, ciphertext, ad)
} else { } else {
return []byte{}, nil return []byte{}, nil
} }
@@ -69,7 +67,7 @@ func (s *NebulaCipherState) DecryptDanger(out, ad, ciphertext []byte, n uint64,
func (s *NebulaCipherState) Overhead() int { func (s *NebulaCipherState) Overhead() int {
if s != nil { if s != nil {
return s.c.(cipher.AEAD).Overhead() return s.c.Overhead()
} }
return 0 return 0
} }
+101 -58
View File
@@ -1,15 +1,16 @@
package nebula package nebula
import ( import (
"context"
"encoding/binary" "encoding/binary"
"errors" "errors"
"log/slog"
"net/netip" "net/netip"
"time" "time"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
"golang.org/x/net/ipv6" "golang.org/x/net/ipv6"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
@@ -24,7 +25,11 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
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
if len(packet) > 1 { if len(packet) > 1 {
f.l.WithField("packet", packet).Infof("Error while parsing inbound packet from %s: %s", via, err) f.l.Info("Error while parsing inbound packet",
"from", via,
"error", err,
"packet", packet,
)
} }
return return
} }
@@ -32,8 +37,8 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
//l.Error("in packet ", header, packet[HeaderLen:]) //l.Error("in packet ", header, packet[HeaderLen:])
if !via.IsRelayed { if !via.IsRelayed {
if f.myVpnNetworksTable.Contains(via.UdpAddr.Addr()) { if f.myVpnNetworksTable.Contains(via.UdpAddr.Addr()) {
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("from", via).Debug("Refusing to process double encrypted packet") f.l.Debug("Refusing to process double encrypted packet", "from", via)
} }
return return
} }
@@ -87,7 +92,10 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
if !ok { if !ok {
// The only way this happens is if hostmap has an index to the correct HostInfo, but the HostInfo is missing // The only way this happens is if hostmap has an index to the correct HostInfo, but the HostInfo is missing
// its internal mapping. This should never happen. // its internal mapping. This should never happen.
hostinfo.logger(f.l).WithFields(logrus.Fields{"vpnAddrs": hostinfo.vpnAddrs, "remoteIndex": h.RemoteIndex}).Error("HostInfo missing remote relay index") hostinfo.logger(f.l).Error("HostInfo missing remote relay index",
"vpnAddrs", hostinfo.vpnAddrs,
"remoteIndex", h.RemoteIndex,
)
return return
} }
@@ -108,7 +116,11 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
// Find the target HostInfo relay object // Find the target HostInfo relay object
targetHI, targetRelay, err := f.hostMap.QueryVpnAddrsRelayFor(hostinfo.vpnAddrs, relay.PeerAddr) targetHI, targetRelay, err := f.hostMap.QueryVpnAddrsRelayFor(hostinfo.vpnAddrs, relay.PeerAddr)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithField("relayTo", relay.PeerAddr).WithError(err).WithField("hostinfo.vpnAddrs", hostinfo.vpnAddrs).Info("Failed to find target host info by ip") hostinfo.logger(f.l).Info("Failed to find target host info by ip",
"relayTo", relay.PeerAddr,
"error", err,
"hostinfo.vpnAddrs", hostinfo.vpnAddrs,
)
return return
} }
@@ -124,7 +136,11 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
hostinfo.logger(f.l).Error("Unexpected Relay Type of Terminal") hostinfo.logger(f.l).Error("Unexpected Relay Type of Terminal")
} }
} else { } else {
hostinfo.logger(f.l).WithFields(logrus.Fields{"relayTo": relay.PeerAddr, "relayFrom": hostinfo.vpnAddrs[0], "targetRelayState": targetRelay.State}).Info("Unexpected target relay state") hostinfo.logger(f.l).Info("Unexpected target relay state",
"relayTo", relay.PeerAddr,
"relayFrom", hostinfo.vpnAddrs[0],
"targetRelayState", targetRelay.State,
)
return return
} }
} }
@@ -138,9 +154,11 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb) d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err).WithField("from", via). hostinfo.logger(f.l).Error("Failed to decrypt lighthouse packet",
WithField("packet", packet). "error", err,
Error("Failed to decrypt lighthouse packet") "from", via,
"packet", packet,
)
return return
} }
@@ -157,9 +175,11 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb) d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err).WithField("from", via). hostinfo.logger(f.l).Error("Failed to decrypt test packet",
WithField("packet", packet). "error", err,
Error("Failed to decrypt test packet") "from", via,
"packet", packet,
)
return return
} }
@@ -192,14 +212,15 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
} }
_, err = f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb) _, err = f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err).WithField("from", via). hostinfo.logger(f.l).Error("Failed to decrypt CloseTunnel packet",
WithField("packet", packet). "error", err,
Error("Failed to decrypt CloseTunnel packet") "from", via,
"packet", packet,
)
return return
} }
hostinfo.logger(f.l).WithField("from", via). hostinfo.logger(f.l).Info("Close tunnel received, tearing down.", "from", via)
Info("Close tunnel received, tearing down.")
f.closeTunnel(hostinfo) f.closeTunnel(hostinfo)
return return
@@ -211,9 +232,11 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb) d, err := f.decrypt(hostinfo, h.MessageCounter, out, packet, h, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err).WithField("from", via). hostinfo.logger(f.l).Error("Failed to decrypt Control packet",
WithField("packet", packet). "error", err,
Error("Failed to decrypt Control packet") "from", via,
"packet", packet,
)
return return
} }
@@ -221,7 +244,9 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
default: default:
f.messageMetrics.Rx(h.Type, h.Subtype, 1) f.messageMetrics.Rx(h.Type, h.Subtype, 1)
hostinfo.logger(f.l).Debugf("Unexpected packet received from %s", via) if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debug("Unexpected packet received", "from", via)
}
return return
} }
@@ -247,20 +272,27 @@ func (f *Interface) sendCloseTunnel(h *HostInfo) {
func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) { func (f *Interface) handleHostRoaming(hostinfo *HostInfo, via ViaSender) {
if !via.IsRelayed && hostinfo.remote != via.UdpAddr { if !via.IsRelayed && hostinfo.remote != via.UdpAddr {
if !f.lightHouse.GetRemoteAllowList().AllowAll(hostinfo.vpnAddrs, via.UdpAddr.Addr()) { if !f.lightHouse.GetRemoteAllowList().AllowAll(hostinfo.vpnAddrs, via.UdpAddr.Addr()) {
hostinfo.logger(f.l).WithField("newAddr", via.UdpAddr).Debug("lighthouse.remote_allow_list denied roaming") if f.l.Enabled(context.Background(), slog.LevelDebug) {
return hostinfo.logger(f.l).Debug("lighthouse.remote_allow_list denied roaming", "newAddr", via.UdpAddr)
}
if !hostinfo.lastRoam.IsZero() && via.UdpAddr == hostinfo.lastRoamRemote && time.Since(hostinfo.lastRoam) < RoamingSuppressSeconds*time.Second {
if f.l.Level >= logrus.DebugLevel {
hostinfo.logger(f.l).WithField("udpAddr", hostinfo.remote).WithField("newAddr", via.UdpAddr).
Debugf("Suppressing roam back to previous remote for %d seconds", RoamingSuppressSeconds)
} }
return return
} }
hostinfo.logger(f.l).WithField("udpAddr", hostinfo.remote).WithField("newAddr", via.UdpAddr). if !hostinfo.lastRoam.IsZero() && via.UdpAddr == hostinfo.lastRoamRemote && time.Since(hostinfo.lastRoam) < RoamingSuppressSeconds*time.Second {
Info("Host roamed to new udp ip/port.") if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).Debug("Suppressing roam back to previous remote",
"suppressSeconds", RoamingSuppressSeconds,
"udpAddr", hostinfo.remote,
"newAddr", via.UdpAddr,
)
}
return
}
hostinfo.logger(f.l).Info("Host roamed to new udp ip/port.",
"udpAddr", hostinfo.remote,
"newAddr", via.UdpAddr,
)
hostinfo.lastRoam = time.Now() hostinfo.lastRoam = time.Now()
hostinfo.lastRoamRemote = hostinfo.remote hostinfo.lastRoamRemote = hostinfo.remote
hostinfo.SetRemote(via.UdpAddr) hostinfo.SetRemote(via.UdpAddr)
@@ -491,8 +523,9 @@ func (f *Interface) decrypt(hostinfo *HostInfo, mc uint64, out []byte, packet []
} }
if !hostinfo.ConnectionState.window.Update(f.l, mc) { if !hostinfo.ConnectionState.window.Update(f.l, mc) {
hostinfo.logger(f.l).WithField("header", h). if f.l.Enabled(context.Background(), slog.LevelDebug) {
Debugln("dropping out of window packet") hostinfo.logger(f.l).Debug("dropping out of window packet", "header", h)
}
return nil, errors.New("out of window packet") return nil, errors.New("out of window packet")
} }
@@ -504,20 +537,23 @@ func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, out
out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], messageCounter, nb) out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, packet[:header.Len], packet[header.Len:], messageCounter, nb)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err).Error("Failed to decrypt packet") hostinfo.logger(f.l).Error("Failed to decrypt packet", "error", err)
return false return false
} }
err = newPacket(out, true, fwPacket) err = newPacket(out, true, fwPacket)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err).WithField("packet", out). hostinfo.logger(f.l).Warn("Error while validating inbound packet",
Warnf("Error while validating inbound packet") "error", err,
"packet", out,
)
return false return false
} }
if !hostinfo.ConnectionState.window.Update(f.l, messageCounter) { if !hostinfo.ConnectionState.window.Update(f.l, messageCounter) {
hostinfo.logger(f.l).WithField("fwPacket", fwPacket). if f.l.Enabled(context.Background(), slog.LevelDebug) {
Debugln("dropping out of window packet") hostinfo.logger(f.l).Debug("dropping out of window packet", "fwPacket", fwPacket)
}
return false return false
} }
@@ -526,18 +562,19 @@ func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, out
// NOTE: We give `packet` as the `out` here since we already decrypted from it and we don't need it anymore // NOTE: We give `packet` as the `out` here since we already decrypted from it and we don't need it anymore
// This gives us a buffer to build the reject packet in // This gives us a buffer to build the reject packet in
f.rejectOutside(out, hostinfo.ConnectionState, hostinfo, nb, packet, q) f.rejectOutside(out, hostinfo.ConnectionState, hostinfo, nb, packet, q)
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
hostinfo.logger(f.l).WithField("fwPacket", fwPacket). hostinfo.logger(f.l).Debug("dropping inbound packet",
WithField("reason", dropReason). "fwPacket", fwPacket,
Debugln("dropping inbound packet") "reason", dropReason,
)
} }
return false return false
} }
f.connectionManager.In(hostinfo) f.connectionManager.In(hostinfo)
err = f.tunCoalescers[q].Add(out) err = f.batchers[q].Commit(out)
if err != nil { if err != nil {
f.l.WithError(err).Error("Failed to write to tun") f.l.Error("Failed to write to tun", "error", err)
} }
return true return true
} }
@@ -553,35 +590,41 @@ func (f *Interface) sendRecvError(endpoint netip.AddrPort, index uint32) {
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)
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("index", index). f.l.Debug("Recv error sent",
WithField("udpAddr", endpoint). "index", index,
Debug("Recv error sent") "udpAddr", endpoint,
)
} }
} }
func (f *Interface) handleRecvError(addr netip.AddrPort, h *header.H) { func (f *Interface) handleRecvError(addr netip.AddrPort, h *header.H) {
if !f.acceptRecvErrorConfig.ShouldRecvError(addr) { if !f.acceptRecvErrorConfig.ShouldRecvError(addr) {
f.l.WithField("index", h.RemoteIndex). f.l.Debug("Recv error received, ignoring",
WithField("udpAddr", addr). "index", h.RemoteIndex,
Debug("Recv error received, ignoring") "udpAddr", addr,
)
return return
} }
if f.l.Level >= logrus.DebugLevel { if f.l.Enabled(context.Background(), slog.LevelDebug) {
f.l.WithField("index", h.RemoteIndex). f.l.Debug("Recv error received",
WithField("udpAddr", addr). "index", h.RemoteIndex,
Debug("Recv error received") "udpAddr", addr,
)
} }
hostinfo := f.hostMap.QueryReverseIndex(h.RemoteIndex) hostinfo := f.hostMap.QueryReverseIndex(h.RemoteIndex)
if hostinfo == nil { if hostinfo == nil {
f.l.WithField("remoteIndex", h.RemoteIndex).Debugln("Did not find remote index in main hostmap") f.l.Debug("Did not find remote index in main hostmap", "remoteIndex", h.RemoteIndex)
return return
} }
if hostinfo.remote.IsValid() && hostinfo.remote != addr { if hostinfo.remote.IsValid() && hostinfo.remote != addr {
f.l.Infoln("Someone spoofing recv_errors? ", addr, hostinfo.remote) f.l.Info("Someone spoofing recv_errors?",
"addr", addr,
"hostinfoRemote", hostinfo.remote,
)
return return
} }
+33
View File
@@ -0,0 +1,33 @@
package batch
import "net/netip"
type RxBatcher interface {
// Reserve creates a pkt to borrow
Reserve(sz int) []byte
// Commit borrows pkt. The caller must keep pkt valid until the next Flush
Commit(pkt []byte) error
// Flush emits every queued packet in arrival order. Returns the
// first error observed; keeps draining so one bad packet doesn't hold up
// the rest. After Flush returns, borrowed payload slices may be recycled.
Flush() error
}
type TxBatcher interface {
// Next returns a zero-length slice with slotCap capacity over the next unused
// slot's backing bytes. The caller writes into the returned slice and then
// calls Commit with the final length and destination. Next returns nil when
// the batch is full.
Next() []byte
// Commit records the slot just returned by Next as a packet of length n
// destined for dst.
Commit(n int, dst netip.AddrPort)
// Reset clears committed slots; backing storage is retained for reuse.
Reset()
// Len returns the number of committed packets.
Len() int
// Cap returns the maximum number of slots in the batch.
Cap() int
// Get returns the buffers needed to send the batch
Get() ([][]byte, []netip.AddrPort)
}
+57
View File
@@ -0,0 +1,57 @@
package batch
import (
"io"
"github.com/slackhq/nebula/udp"
)
// Passthrough is a RxBatcher that doesn't batch anything, it just accumulates and then sends packets.
type Passthrough struct {
out io.Writer
slots [][]byte
backing []byte
cursor int
}
func NewPassthrough(w io.Writer) *Passthrough {
const baseNumSlots = 128
return &Passthrough{
out: w,
slots: make([][]byte, 0, baseNumSlots),
backing: make([]byte, 0, baseNumSlots*udp.MTU),
}
}
func (p *Passthrough) Reserve(sz int) []byte {
if len(p.backing)+sz > cap(p.backing) {
// Grow: allocate a fresh backing. Already-committed slices still
// reference the old array and remain valid until Flush drops them.
newCap := max(cap(p.backing)*2, sz)
p.backing = make([]byte, 0, newCap)
}
start := len(p.backing)
p.backing = p.backing[:start+sz]
return p.backing[start : start+sz : start+sz] //return zero length, sz-cap slice
}
func (p *Passthrough) Commit(pkt []byte) error {
p.slots = append(p.slots, pkt)
return nil
}
func (p *Passthrough) Flush() error {
var firstErr error
for _, s := range p.slots {
_, err := p.out.Write(s)
if err != nil && firstErr == nil {
firstErr = err
}
}
for i := range p.slots {
p.slots[i] = nil
}
p.slots = p.slots[:0]
p.backing = p.backing[:0]
return firstErr
}
@@ -1,11 +1,11 @@
package nebula package batch
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"io" "io"
"github.com/slackhq/nebula/overlay" "github.com/slackhq/nebula/overlay/tio"
) )
// ipProtoTCP is the IANA protocol number for TCP. Hardcoded instead of // ipProtoTCP is the IANA protocol number for TCP. Hardcoded instead of
@@ -66,14 +66,14 @@ type coalesceSlot struct {
payIovs [][]byte payIovs [][]byte
} }
// tcpCoalescer accumulates adjacent in-flow TCP data segments across // TCPCoalescer accumulates adjacent in-flow TCP data segments across
// multiple concurrent flows and emits each flow's run as a single TSO // multiple concurrent flows and emits each flow's run as a single TSO
// superpacket via overlay.GSOWriter. All output — coalesced or not — is // superpacket via tio.GSOWriter. All output — coalesced or not — is
// deferred until Flush so arrival order is preserved on the wire. Owns // deferred until Flush so arrival order is preserved on the wire. Owns
// no locks; one coalescer per TUN write queue. // no locks; one coalescer per TUN write queue.
type tcpCoalescer struct { type TCPCoalescer struct {
plainW io.Writer plainW io.Writer
gsoW overlay.GSOWriter // nil when the queue doesn't support TSO gsoW tio.GSOWriter // nil when the queue doesn't support TSO
// slots is the ordered event queue. Flush walks it once and emits each // slots is the ordered event queue. Flush walks it once and emits each
// entry as either a WriteGSO (coalesced) or a plainW.Write (passthrough). // entry as either a WriteGSO (coalesced) or a plainW.Write (passthrough).
@@ -84,16 +84,19 @@ type tcpCoalescer struct {
// when a non-admissible packet for that flow arrives, or in Flush. // when a non-admissible packet for that flow arrives, or in Flush.
openSlots map[flowKey]*coalesceSlot openSlots map[flowKey]*coalesceSlot
pool []*coalesceSlot // free list for reuse pool []*coalesceSlot // free list for reuse
backing []byte
} }
func newTCPCoalescer(w io.Writer) *tcpCoalescer { func NewTCPCoalescer(w io.Writer) *TCPCoalescer {
c := &tcpCoalescer{ c := &TCPCoalescer{
plainW: w, plainW: w,
slots: make([]*coalesceSlot, 0, initialSlots), slots: make([]*coalesceSlot, 0, initialSlots),
openSlots: make(map[flowKey]*coalesceSlot, initialSlots), openSlots: make(map[flowKey]*coalesceSlot, initialSlots),
pool: make([]*coalesceSlot, 0, initialSlots), pool: make([]*coalesceSlot, 0, initialSlots),
backing: make([]byte, 0, initialSlots*65535),
} }
if gw, ok := w.(overlay.GSOWriter); ok && gw.GSOSupported() { if gw, ok := w.(tio.GSOWriter); ok && gw.GSOSupported() {
c.gsoW = gw c.gsoW = gw
} }
return c return c
@@ -194,10 +197,22 @@ func (p parsedTCP) coalesceable() bool {
return p.payLen > 0 return p.payLen > 0
} }
// Add borrows pkt. The caller must keep pkt valid until the next Flush, func (c *TCPCoalescer) Reserve(sz int) []byte {
if len(c.backing)+sz > cap(c.backing) {
// Grow: allocate a fresh backing. Already-committed slices still
// reference the old array and remain valid until Flush drops them.
newCap := max(cap(c.backing)*2, sz)
c.backing = make([]byte, 0, newCap)
}
start := len(c.backing)
c.backing = c.backing[:start+sz]
return c.backing[start : start+sz : start+sz] //return zero length, sz-cap slice
}
// Commit borrows pkt. The caller must keep pkt valid until the next Flush,
// whether or not the packet was coalesced — passthrough (non-admissible) // whether or not the packet was coalesced — passthrough (non-admissible)
// packets are queued and written at Flush time, not synchronously. // packets are queued and written at Flush time, not synchronously.
func (c *tcpCoalescer) Add(pkt []byte) error { func (c *TCPCoalescer) Commit(pkt []byte) error {
if c.gsoW == nil { if c.gsoW == nil {
c.addPassthrough(pkt) c.addPassthrough(pkt)
return nil return nil
@@ -237,7 +252,7 @@ func (c *tcpCoalescer) Add(pkt []byte) error {
// via WriteGSO; passthrough slots go out via plainW.Write. Returns the // via WriteGSO; passthrough slots go out via plainW.Write. Returns the
// first error observed; keeps draining so one bad packet doesn't hold up // first error observed; keeps draining so one bad packet doesn't hold up
// the rest. After Flush returns, borrowed payload slices may be recycled. // the rest. After Flush returns, borrowed payload slices may be recycled.
func (c *tcpCoalescer) Flush() error { func (c *TCPCoalescer) Flush() error {
var first error var first error
for _, s := range c.slots { for _, s := range c.slots {
var err error var err error
@@ -258,17 +273,19 @@ func (c *tcpCoalescer) Flush() error {
for k := range c.openSlots { for k := range c.openSlots {
delete(c.openSlots, k) delete(c.openSlots, k)
} }
c.backing = c.backing[:0]
return first return first
} }
func (c *tcpCoalescer) addPassthrough(pkt []byte) { func (c *TCPCoalescer) addPassthrough(pkt []byte) {
s := c.take() s := c.take()
s.passthrough = true s.passthrough = true
s.rawPkt = pkt s.rawPkt = pkt
c.slots = append(c.slots, s) c.slots = append(c.slots, s)
} }
func (c *tcpCoalescer) seed(pkt []byte, info parsedTCP) { func (c *TCPCoalescer) seed(pkt []byte, info parsedTCP) {
if info.hdrLen > tcpCoalesceHdrCap || info.hdrLen+info.payLen > tcpCoalesceBufSize { if info.hdrLen > tcpCoalesceHdrCap || info.hdrLen+info.payLen > tcpCoalesceBufSize {
// Pathological shape — can't fit our scratch, emit as-is. // Pathological shape — can't fit our scratch, emit as-is.
c.addPassthrough(pkt) c.addPassthrough(pkt)
@@ -297,7 +314,7 @@ func (c *tcpCoalescer) seed(pkt []byte, info parsedTCP) {
// canAppend reports whether info's packet extends the slot's seed: same // canAppend reports whether info's packet extends the slot's seed: same
// header shape and stable contents, adjacent seq, not oversized, chain not // header shape and stable contents, adjacent seq, not oversized, chain not
// closed. // closed.
func (c *tcpCoalescer) canAppend(s *coalesceSlot, pkt []byte, info parsedTCP) bool { func (c *TCPCoalescer) canAppend(s *coalesceSlot, pkt []byte, info parsedTCP) bool {
if s.psh { if s.psh {
return false return false
} }
@@ -322,17 +339,22 @@ func (c *tcpCoalescer) canAppend(s *coalesceSlot, pkt []byte, info parsedTCP) bo
return true return true
} }
func (c *tcpCoalescer) appendPayload(s *coalesceSlot, pkt []byte, info parsedTCP) { func (c *TCPCoalescer) appendPayload(s *coalesceSlot, pkt []byte, info parsedTCP) {
s.payIovs = append(s.payIovs, pkt[info.hdrLen:info.hdrLen+info.payLen]) s.payIovs = append(s.payIovs, pkt[info.hdrLen:info.hdrLen+info.payLen])
s.numSeg++ s.numSeg++
s.totalPay += info.payLen s.totalPay += info.payLen
s.nextSeq = info.seq + uint32(info.payLen) s.nextSeq = info.seq + uint32(info.payLen)
if info.flags&0x08 != 0 {
// Propagate PSH into the seed header so kernel TSO sets it on the
// last segment. Without this the sender's push signal is dropped.
s.hdrBuf[s.ipHdrLen+13] |= 0x08
}
if info.payLen < s.gsoSize || info.flags&0x08 != 0 { if info.payLen < s.gsoSize || info.flags&0x08 != 0 {
s.psh = true s.psh = true
} }
} }
func (c *tcpCoalescer) take() *coalesceSlot { func (c *TCPCoalescer) take() *coalesceSlot {
if n := len(c.pool); n > 0 { if n := len(c.pool); n > 0 {
s := c.pool[n-1] s := c.pool[n-1]
c.pool[n-1] = nil c.pool[n-1] = nil
@@ -342,7 +364,7 @@ func (c *tcpCoalescer) take() *coalesceSlot {
return &coalesceSlot{} return &coalesceSlot{}
} }
func (c *tcpCoalescer) release(s *coalesceSlot) { func (c *TCPCoalescer) release(s *coalesceSlot) {
s.passthrough = false s.passthrough = false
s.rawPkt = nil s.rawPkt = nil
for i := range s.payIovs { for i := range s.payIovs {
@@ -357,7 +379,7 @@ func (c *tcpCoalescer) release(s *coalesceSlot) {
// flushSlot patches the header and calls WriteGSO. Does not remove the // flushSlot patches the header and calls WriteGSO. Does not remove the
// slot from c.slots. // slot from c.slots.
func (c *tcpCoalescer) flushSlot(s *coalesceSlot) error { func (c *TCPCoalescer) flushSlot(s *coalesceSlot) error {
total := s.hdrLen + s.totalPay total := s.hdrLen + s.totalPay
l4Len := total - s.ipHdrLen l4Len := total - s.ipHdrLen
hdr := s.hdrBuf[:s.hdrLen] hdr := s.hdrBuf[:s.hdrLen]
@@ -380,7 +402,7 @@ func (c *tcpCoalescer) flushSlot(s *coalesceSlot) error {
tcsum := s.ipHdrLen + 16 tcsum := s.ipHdrLen + 16
binary.BigEndian.PutUint16(hdr[tcsum:tcsum+2], foldOnceNoInvert(psum)) binary.BigEndian.PutUint16(hdr[tcsum:tcsum+2], foldOnceNoInvert(psum))
return c.gsoW.WriteGSO(hdr, s.payIovs, uint16(s.gsoSize), s.isV6, uint16(s.ipHdrLen)) return c.gsoW.WriteGSO(hdr[:s.ipHdrLen], hdr[s.ipHdrLen:], s.payIovs)
} }
// headersMatch compares two IP+TCP header prefixes for byte-for-byte // headersMatch compares two IP+TCP header prefixes for byte-for-byte
@@ -1,4 +1,4 @@
package nebula package batch
import ( import (
"encoding/binary" "encoding/binary"
@@ -6,14 +6,17 @@ import (
) )
// fakeTunWriter records plain Writes and WriteGSO calls without touching a // fakeTunWriter records plain Writes and WriteGSO calls without touching a
// real TUN fd. WriteGSO preserves the split between hdr and borrowed pays // real TUN fd. WriteGSO records the IP header, transport header, and
// so tests can inspect each independently. // borrowed payload fragments separately so tests can inspect each.
type fakeTunWriter struct { type fakeTunWriter struct {
gsoEnabled bool gsoEnabled bool
writes [][]byte writes [][]byte
gsoWrites []fakeGSOWrite gsoWrites []fakeGSOWrite
} }
// fakeGSOWrite captures one WriteGSO call. hdr is the concatenation of the
// IP and transport headers (in that order), gsoSize / isV6 / csumStart are
// derived from the call so existing assertions keep working unchanged.
type fakeGSOWrite struct { type fakeGSOWrite struct {
hdr []byte hdr []byte
pays [][]byte pays [][]byte
@@ -47,21 +50,27 @@ func (w *fakeTunWriter) Write(p []byte) (int, error) {
return len(p), nil return len(p), nil
} }
func (w *fakeTunWriter) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error { func (w *fakeTunWriter) WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte) error {
hcopy := make([]byte, len(hdr)) hcopy := make([]byte, len(hdr)+len(transportHdr))
copy(hcopy, hdr) copy(hcopy, hdr)
copy(hcopy[len(hdr):], transportHdr)
paysCopy := make([][]byte, len(pays)) paysCopy := make([][]byte, len(pays))
for i, p := range pays { for i, p := range pays {
pc := make([]byte, len(p)) pc := make([]byte, len(p))
copy(pc, p) copy(pc, p)
paysCopy[i] = pc paysCopy[i] = pc
} }
var gsoSize uint16
if len(pays) > 1 {
gsoSize = uint16(len(pays[0]))
}
isV6 := len(hdr) > 0 && hdr[0]>>4 == 6
w.gsoWrites = append(w.gsoWrites, fakeGSOWrite{ w.gsoWrites = append(w.gsoWrites, fakeGSOWrite{
hdr: hcopy, hdr: hcopy,
pays: paysCopy, pays: paysCopy,
gsoSize: gsoSize, gsoSize: gsoSize,
isV6: isV6, isV6: isV6,
csumStart: csumStart, csumStart: uint16(len(hdr)),
}) })
return nil return nil
} }
@@ -114,9 +123,9 @@ const (
func TestCoalescerPassthroughWhenGSOUnavailable(t *testing.T) { func TestCoalescerPassthroughWhenGSOUnavailable(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: false} w := &fakeTunWriter{gsoEnabled: false}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pkt := buildTCPv4(1000, tcpAck, []byte("hello")) pkt := buildTCPv4(1000, tcpAck, []byte("hello"))
if err := c.Add(pkt); err != nil { if err := c.Commit(pkt); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// No sync write — passthrough is deferred to Flush. // No sync write — passthrough is deferred to Flush.
@@ -133,14 +142,14 @@ func TestCoalescerPassthroughWhenGSOUnavailable(t *testing.T) {
func TestCoalescerNonTCPPassthrough(t *testing.T) { func TestCoalescerNonTCPPassthrough(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pkt := make([]byte, 28) pkt := make([]byte, 28)
pkt[0] = 0x45 pkt[0] = 0x45
binary.BigEndian.PutUint16(pkt[2:4], 28) binary.BigEndian.PutUint16(pkt[2:4], 28)
pkt[9] = 1 pkt[9] = 1
copy(pkt[12:16], []byte{10, 0, 0, 1}) copy(pkt[12:16], []byte{10, 0, 0, 1})
copy(pkt[16:20], []byte{10, 0, 0, 2}) copy(pkt[16:20], []byte{10, 0, 0, 2})
if err := c.Add(pkt); err != nil { if err := c.Commit(pkt); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -153,9 +162,9 @@ func TestCoalescerNonTCPPassthrough(t *testing.T) {
func TestCoalescerSeedThenFlushAlone(t *testing.T) { func TestCoalescerSeedThenFlushAlone(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pkt := buildTCPv4(1000, tcpAck, make([]byte, 1000)) pkt := buildTCPv4(1000, tcpAck, make([]byte, 1000))
if err := c.Add(pkt); err != nil { if err := c.Commit(pkt); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(w.writes) != 0 || len(w.gsoWrites) != 0 { if len(w.writes) != 0 || len(w.gsoWrites) != 0 {
@@ -180,15 +189,15 @@ func TestCoalescerSeedThenFlushAlone(t *testing.T) {
func TestCoalescerCoalescesAdjacentACKs(t *testing.T) { func TestCoalescerCoalescesAdjacentACKs(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 1200) pay := make([]byte, 1200)
if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(1000, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4(2200, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(2200, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4(3400, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(3400, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -220,12 +229,12 @@ func TestCoalescerCoalescesAdjacentACKs(t *testing.T) {
func TestCoalescerRejectsSeqGap(t *testing.T) { func TestCoalescerRejectsSeqGap(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 1200) pay := make([]byte, 1200)
if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(1000, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4(3000, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(3000, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -239,15 +248,15 @@ func TestCoalescerRejectsSeqGap(t *testing.T) {
func TestCoalescerRejectsFlagMismatch(t *testing.T) { func TestCoalescerRejectsFlagMismatch(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 1200) pay := make([]byte, 1200)
if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(1000, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// SYN|ACK is non-admissible. Must flush matching flow's slot (gso) // SYN|ACK is non-admissible. Must flush matching flow's slot (gso)
// and then plain-write the SYN packet itself. // and then plain-write the SYN packet itself.
syn := buildTCPv4(2200, tcpSyn|tcpAck, pay) syn := buildTCPv4(2200, tcpSyn|tcpAck, pay)
if err := c.Add(syn); err != nil { if err := c.Commit(syn); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -260,9 +269,9 @@ func TestCoalescerRejectsFlagMismatch(t *testing.T) {
func TestCoalescerRejectsFIN(t *testing.T) { func TestCoalescerRejectsFIN(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
fin := buildTCPv4(1000, tcpAck|tcpFin, []byte("x")) fin := buildTCPv4(1000, tcpAck|tcpFin, []byte("x"))
if err := c.Add(fin); err != nil { if err := c.Commit(fin); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -276,18 +285,18 @@ func TestCoalescerRejectsFIN(t *testing.T) {
func TestCoalescerShortLastSegmentClosesChain(t *testing.T) { func TestCoalescerShortLastSegmentClosesChain(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
full := make([]byte, 1200) full := make([]byte, 1200)
half := make([]byte, 500) half := make([]byte, 500)
if err := c.Add(buildTCPv4(1000, tcpAck, full)); err != nil { if err := c.Commit(buildTCPv4(1000, tcpAck, full)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4(2200, tcpAck, half)); err != nil { if err := c.Commit(buildTCPv4(2200, tcpAck, half)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Chain now closed; next packet seeds a new slot on the same flow // Chain now closed; next packet seeds a new slot on the same flow
// after flushing the old one. // after flushing the old one.
if err := c.Add(buildTCPv4(2700, tcpAck, full)); err != nil { if err := c.Commit(buildTCPv4(2700, tcpAck, full)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -311,15 +320,15 @@ func TestCoalescerShortLastSegmentClosesChain(t *testing.T) {
func TestCoalescerPSHFinalizesChain(t *testing.T) { func TestCoalescerPSHFinalizesChain(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 1200) pay := make([]byte, 1200)
if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(1000, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4(2200, tcpAckPsh, pay)); err != nil { if err := c.Commit(buildTCPv4(2200, tcpAckPsh, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4(3400, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(3400, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -334,17 +343,50 @@ func TestCoalescerPSHFinalizesChain(t *testing.T) {
} }
} }
// TestCoalescerPropagatesPSHFromAppended ensures that when an appended
// segment carries PSH (or is short, sealing the chain), the PSH bit ends
// up in the emitted superpacket's TCP flags. The kernel TSO path keeps
// PSH only on the last segment iff the input header has it set; if the
// coalescer drops it the sender's push signal never reaches the receiver.
func TestCoalescerPropagatesPSHFromAppended(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true}
c := NewTCPCoalescer(w)
pay := make([]byte, 1200)
// Seed has no PSH; second segment carries PSH and seals the chain.
if err := c.Commit(buildTCPv4(1000, tcpAck, pay)); err != nil {
t.Fatal(err)
}
if err := c.Commit(buildTCPv4(2200, tcpAckPsh, pay)); err != nil {
t.Fatal(err)
}
if err := c.Flush(0); err != nil {
t.Fatal(err)
}
if len(w.gsoWrites) != 1 {
t.Fatalf("want 1 gso write got %d", len(w.gsoWrites))
}
g := w.gsoWrites[0]
const ipHdrLen = 20
flags := g.hdr[ipHdrLen+13]
if flags&tcpPsh == 0 {
t.Fatalf("PSH lost from coalesced superpacket: flags=0x%02x", flags)
}
if flags&tcpAck == 0 {
t.Fatalf("ACK missing from coalesced superpacket: flags=0x%02x", flags)
}
}
func TestCoalescerRejectsDifferentFlow(t *testing.T) { func TestCoalescerRejectsDifferentFlow(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 1200) pay := make([]byte, 1200)
p1 := buildTCPv4(1000, tcpAck, pay) p1 := buildTCPv4(1000, tcpAck, pay)
p2 := buildTCPv4(2200, tcpAck, pay) p2 := buildTCPv4(2200, tcpAck, pay)
binary.BigEndian.PutUint16(p2[20:22], 9999) binary.BigEndian.PutUint16(p2[20:22], 9999)
if err := c.Add(p1); err != nil { if err := c.Commit(p1); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(p2); err != nil { if err := c.Commit(p2); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -358,13 +400,13 @@ func TestCoalescerRejectsDifferentFlow(t *testing.T) {
func TestCoalescerRejectsIPOptions(t *testing.T) { func TestCoalescerRejectsIPOptions(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 500) pay := make([]byte, 500)
pkt := buildTCPv4(1000, tcpAck, pay) pkt := buildTCPv4(1000, tcpAck, pay)
// Bump IHL to 6 to simulate 4 bytes of IP options. Don't actually add // Bump IHL to 6 to simulate 4 bytes of IP options. Don't actually add
// bytes — parser should bail before it matters. // bytes — parser should bail before it matters.
pkt[0] = 0x46 pkt[0] = 0x46
if err := c.Add(pkt); err != nil { if err := c.Commit(pkt); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -378,11 +420,11 @@ func TestCoalescerRejectsIPOptions(t *testing.T) {
func TestCoalescerCapBySegments(t *testing.T) { func TestCoalescerCapBySegments(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 512) pay := make([]byte, 512)
seq := uint32(1000) seq := uint32(1000)
for i := 0; i < tcpCoalesceMaxSegs+5; i++ { for i := 0; i < tcpCoalesceMaxSegs+5; i++ {
if err := c.Add(buildTCPv4(seq, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4(seq, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
seq += uint32(len(pay)) seq += uint32(len(pay))
@@ -402,26 +444,26 @@ func TestCoalescerCapBySegments(t *testing.T) {
// flows coalesce independently in a single Flush. // flows coalesce independently in a single Flush.
func TestCoalescerMultipleFlowsInSameBatch(t *testing.T) { func TestCoalescerMultipleFlowsInSameBatch(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 1200) pay := make([]byte, 1200)
// Flow A: sport 1000. Flow B: sport 3000. // Flow A: sport 1000. Flow B: sport 3000.
if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4Ports(1000, 2000, 2500, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(1000, 2000, 2500, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4Ports(3000, 2000, 2900, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(3000, 2000, 2900, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
@@ -459,11 +501,11 @@ func TestCoalescerMultipleFlowsInSameBatch(t *testing.T) {
// writing passthrough packets synchronously. // writing passthrough packets synchronously.
func TestCoalescerPreservesArrivalOrder(t *testing.T) { func TestCoalescerPreservesArrivalOrder(t *testing.T) {
w := &orderedFakeWriter{gsoEnabled: true} w := &orderedFakeWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
// Sequence: coalesceable TCP, ICMP (passthrough), coalesceable TCP on // Sequence: coalesceable TCP, ICMP (passthrough), coalesceable TCP on
// a different flow. Expected emit order: gso(X), plain(ICMP), gso(Y). // a different flow. Expected emit order: gso(X), plain(ICMP), gso(Y).
pay := make([]byte, 1200) pay := make([]byte, 1200)
if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
icmp := make([]byte, 28) icmp := make([]byte, 28)
@@ -472,10 +514,10 @@ func TestCoalescerPreservesArrivalOrder(t *testing.T) {
icmp[9] = 1 icmp[9] = 1
copy(icmp[12:16], []byte{10, 0, 0, 1}) copy(icmp[12:16], []byte{10, 0, 0, 1})
copy(icmp[16:20], []byte{10, 0, 0, 3}) copy(icmp[16:20], []byte{10, 0, 0, 3})
if err := c.Add(icmp); err != nil { if err := c.Commit(icmp); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Nothing should have hit the writer synchronously. // Nothing should have hit the writer synchronously.
@@ -502,7 +544,7 @@ func (w *orderedFakeWriter) Write(p []byte) (int, error) {
return len(p), nil return len(p), nil
} }
func (w *orderedFakeWriter) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error { func (w *orderedFakeWriter) WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte) error {
w.events = append(w.events, "gso") w.events = append(w.events, "gso")
return nil return nil
} }
@@ -525,30 +567,30 @@ func stringSliceEq(a, b []string) bool {
// packet (SYN) mid-flow only flushes its own flow, not others. // packet (SYN) mid-flow only flushes its own flow, not others.
func TestCoalescerInterleavedFlowsPreserveOrdering(t *testing.T) { func TestCoalescerInterleavedFlowsPreserveOrdering(t *testing.T) {
w := &fakeTunWriter{gsoEnabled: true} w := &fakeTunWriter{gsoEnabled: true}
c := newTCPCoalescer(w) c := NewTCPCoalescer(w)
pay := make([]byte, 1200) pay := make([]byte, 1200)
// Flow A two segments. // Flow A two segments.
if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Flow B two segments. // Flow B two segments.
if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Add(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Flow A SYN (non-admissible) — must flush only flow A's slot. // Flow A SYN (non-admissible) — must flush only flow A's slot.
syn := buildTCPv4Ports(1000, 2000, 9999, tcpSyn|tcpAck, pay) syn := buildTCPv4Ports(1000, 2000, 9999, tcpSyn|tcpAck, pay)
if err := c.Add(syn); err != nil { if err := c.Commit(syn); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Flow B continues — should still be coalesced with its seed. // Flow B continues — should still be coalesced with its seed.
if err := c.Add(buildTCPv4Ports(3000, 2000, 2900, tcpAck, pay)); err != nil { if err := c.Commit(buildTCPv4Ports(3000, 2000, 2900, tcpAck, pay)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := c.Flush(); err != nil { if err := c.Flush(); err != nil {
+61
View File
@@ -0,0 +1,61 @@
package batch
import "net/netip"
const SendBatchCap = 128
// SendBatch accumulates encrypted UDP packets for potential TX offloading.
// One SendBatch is owned by each listenIn goroutine; no locking is needed.
// The backing storage holds up to batchCap packets of slotCap bytes each;
// bufs and dsts are parallel slices of committed slots.
type SendBatch struct {
bufs [][]byte
dsts []netip.AddrPort
backing []byte
slotCap int
batchCap int
nextSlot int
}
func NewSendBatch(batchCap, slotCap int) *SendBatch {
return &SendBatch{
bufs: make([][]byte, 0, batchCap),
dsts: make([]netip.AddrPort, 0, batchCap),
backing: make([]byte, batchCap*slotCap),
slotCap: slotCap,
batchCap: batchCap,
}
}
func (b *SendBatch) Next() []byte {
if b.nextSlot >= b.batchCap {
return nil
}
start := b.nextSlot * b.slotCap
return b.backing[start : start : start+b.slotCap] //set len to 0 but cap to slotCap
}
func (b *SendBatch) Commit(n int, dst netip.AddrPort) {
start := b.nextSlot * b.slotCap
b.bufs = append(b.bufs, b.backing[start:start+n])
b.dsts = append(b.dsts, dst)
b.nextSlot++
}
func (b *SendBatch) Reset() {
b.bufs = b.bufs[:0]
b.dsts = b.dsts[:0]
b.nextSlot = 0
}
func (b *SendBatch) Len() int {
return len(b.bufs)
}
func (b *SendBatch) Cap() int {
return b.batchCap
}
func (b *SendBatch) Get() ([][]byte, []netip.AddrPort) {
return b.bufs, b.dsts
}
+69
View File
@@ -0,0 +1,69 @@
package batch
import (
"net/netip"
"testing"
)
func TestSendBatchBookkeeping(t *testing.T) {
b := NewSendBatch(4, 32)
if b.Len() != 0 || b.Cap() != 4 {
t.Fatalf("fresh batch: len=%d cap=%d", b.Len(), b.Cap())
}
ap := netip.MustParseAddrPort("10.0.0.1:4242")
for i := 0; i < 4; i++ {
slot := b.Next()
if slot == nil {
t.Fatalf("slot %d: Next returned nil before cap", i)
}
if cap(slot) != 32 || len(slot) != 0 {
t.Fatalf("slot %d: got len=%d cap=%d want len=0 cap=32", i, len(slot), cap(slot))
}
// Write a marker byte.
slot = append(slot, byte(i), byte(i+1), byte(i+2))
b.Commit(len(slot), ap)
}
if b.Next() != nil {
t.Fatalf("Next should return nil when full")
}
if b.Len() != 4 {
t.Fatalf("Len=%d want 4", b.Len())
}
for i, buf := range b.bufs {
if len(buf) != 3 || buf[0] != byte(i) {
t.Errorf("buf %d: %x", i, buf)
}
if b.dsts[i] != ap {
t.Errorf("dst %d: got %v want %v", i, b.dsts[i], ap)
}
}
// Reset returns empty and Next works again.
b.Reset()
if b.Len() != 0 {
t.Fatalf("after Reset Len=%d want 0", b.Len())
}
slot := b.Next()
if slot == nil || cap(slot) != 32 {
t.Fatalf("after Reset Next nil or wrong cap: %v cap=%d", slot == nil, cap(slot))
}
}
func TestSendBatchSlotsDoNotOverlap(t *testing.T) {
b := NewSendBatch(3, 8)
ap := netip.MustParseAddrPort("10.0.0.1:80")
// Fill three slots, each with its own sentinel byte.
for i := 0; i < 3; i++ {
s := b.Next()
s = append(s, byte(0xA0+i), byte(0xB0+i))
b.Commit(len(s), ap)
}
for i, buf := range b.bufs {
if buf[0] != byte(0xA0+i) || buf[1] != byte(0xB0+i) {
t.Errorf("slot %d corrupted: %x", i, buf)
}
}
}
+5 -50
View File
@@ -4,6 +4,7 @@ import (
"io" "io"
"net/netip" "net/netip"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
@@ -11,59 +12,13 @@ import (
// that don't do TSO segmentation. 65535 covers any single IP packet. // that don't do TSO segmentation. 65535 covers any single IP packet.
const defaultBatchBufSize = 65535 const defaultBatchBufSize = 65535
// Queue is a readable/writable tun queue. One Queue is driven by a single
// read goroutine plus concurrent writers (see Write / WriteReject below).
type Queue interface {
io.Closer
// Read returns one or more packets. The returned slices are borrowed
// from the Queue's internal buffer and are only valid until the next
// Read or Close on this Queue — callers must encrypt or copy each
// slice before the next call. Not safe for concurrent Reads; exactly
// one goroutine per Queue reads.
Read() ([][]byte, error)
// Write emits a single packet on the plaintext (outside→inside)
// delivery path. May run concurrently with WriteReject on the same
// Queue, but not with itself.
Write(p []byte) (int, error)
// WriteReject writes a single packet that originated from the inside
// path (reject replies or self-forward) using scratch state distinct
// from Write, so it can run concurrently with Write on the same Queue
// without a data race. On backends without a shared-scratch Write, a
// trivial delegation to Write is acceptable.
WriteReject(p []byte) (int, error)
}
type Device interface { type Device interface {
Queue io.Closer
Activate() error Activate() error
Networks() []netip.Prefix Networks() []netip.Prefix
Name() string Name() string
RoutesFor(netip.Addr) routing.Gateways RoutesFor(netip.Addr) routing.Gateways
SupportsMultiqueue() bool SupportsMultiqueue() bool //todo remove?
NewMultiQueueReader() (Queue, error) NewMultiQueueReader() error
} Readers() []tio.Queue
// GSOWriter is implemented by Queues that can emit a TCP TSO superpacket
// assembled from a header prefix plus one or more borrowed payload
// fragments, in a single vectored write (writev with a leading
// virtio_net_hdr). This lets the coalescer avoid copying payload bytes
// between the caller's decrypt buffer and the TUN. Backends without GSO
// support return false from GSOSupported and coalescing is skipped.
//
// hdr contains the IPv4/IPv6 + TCP header prefix (mutable — callers will
// have filled in total length and pseudo-header partial). pays are
// non-overlapping payload fragments whose concatenation is the full
// superpacket payload; they are read-only from the writer's perspective
// and must remain valid until the call returns. gsoSize is the MSS:
// every segment except possibly the last is exactly that many bytes.
// csumStart is the byte offset where the TCP header begins within hdr.
//
// hdr's TCP checksum field must already hold the pseudo-header partial
// sum (single-fold, not inverted), per virtio NEEDS_CSUM semantics.
type GSOWriter interface {
WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error
GSOSupported() bool
} }
@@ -1,12 +1,18 @@
package overlay // Package overlaytest provides fakes of overlay.Device for tests that do
// not want to touch a real tun device or route table.
package overlaytest
import ( import (
"errors" "errors"
"net/netip" "net/netip"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
// NoopTun is an overlay.Device that silently discards every read and write.
// Useful in tests that need to construct a nebula Interface but do not
// exercise the datapath.
type NoopTun struct{} type NoopTun struct{}
func (NoopTun) RoutesFor(addr netip.Addr) routing.Gateways { func (NoopTun) RoutesFor(addr netip.Addr) routing.Gateways {
@@ -33,16 +39,16 @@ func (NoopTun) Write([]byte) (int, error) {
return 0, nil return 0, nil
} }
func (NoopTun) WriteReject(p []byte) (int, error) {
return 0, nil
}
func (NoopTun) SupportsMultiqueue() bool { func (NoopTun) SupportsMultiqueue() bool {
return false return false
} }
func (NoopTun) NewMultiQueueReader() (Queue, error) { func (NoopTun) NewMultiQueueReader() error {
return nil, errors.New("unsupported") return errors.New("unsupported")
}
func (NoopTun) Readers() []tio.Queue {
return []tio.Queue{NoopTun{}}
} }
func (NoopTun) Close() error { func (NoopTun) Close() error {
+6 -3
View File
@@ -2,6 +2,7 @@ package overlay
import ( import (
"fmt" "fmt"
"log/slog"
"math" "math"
"net" "net"
"net/netip" "net/netip"
@@ -9,7 +10,6 @@ import (
"strconv" "strconv"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
@@ -48,11 +48,14 @@ func (r Route) String() string {
return s return s
} }
func makeRouteTree(l *logrus.Logger, routes []Route, allowMTU bool) (*bart.Table[routing.Gateways], error) { func makeRouteTree(l *slog.Logger, routes []Route, allowMTU bool) (*bart.Table[routing.Gateways], error) {
routeTree := new(bart.Table[routing.Gateways]) routeTree := new(bart.Table[routing.Gateways])
for _, r := range routes { for _, r := range routes {
if !allowMTU && r.MTU > 0 { if !allowMTU && r.MTU > 0 {
l.WithField("route", r).Warnf("route MTU is not supported in %s", runtime.GOOS) l.Warn("route MTU is not supported on this platform",
"goos", runtime.GOOS,
"route", r,
)
} }
gateways := r.Via gateways := r.Via
+2 -2
View File
@@ -295,7 +295,7 @@ func Test_makeRouteTree(t *testing.T) {
routes, err := parseUnsafeRoutes(c, []netip.Prefix{n}) routes, err := parseUnsafeRoutes(c, []netip.Prefix{n})
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, routes, 2) assert.Len(t, routes, 2)
routeTree, err := makeRouteTree(l, routes, true) routeTree, err := makeRouteTree(test.NewLogger(), routes, true)
require.NoError(t, err) require.NoError(t, err)
ip, err := netip.ParseAddr("1.0.0.2") ip, err := netip.ParseAddr("1.0.0.2")
@@ -367,7 +367,7 @@ func Test_makeMultipathUnsafeRouteTree(t *testing.T) {
routes, err := parseUnsafeRoutes(c, []netip.Prefix{n}) routes, err := parseUnsafeRoutes(c, []netip.Prefix{n})
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, routes, 3) assert.Len(t, routes, 3)
routeTree, err := makeRouteTree(l, routes, true) routeTree, err := makeRouteTree(test.NewLogger(), routes, true)
require.NoError(t, err) require.NoError(t, err)
ip, err := netip.ParseAddr("192.168.86.1") ip, err := netip.ParseAddr("192.168.86.1")
+70
View File
@@ -0,0 +1,70 @@
package tio
import (
"encoding/binary"
"errors"
"fmt"
"golang.org/x/sys/unix"
)
type offloadContainer struct {
pq []*Offload
// pqi is exactly the same as pq, but stored as the interface type
pqi []Queue
shutdownFd int
}
func NewOffloadContainer() (Container, error) {
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
if err != nil {
return nil, fmt.Errorf("failed to create eventfd: %w", err)
}
out := &offloadContainer{
pq: []*Offload{},
pqi: []Queue{},
shutdownFd: shutdownFd,
}
return out, nil
}
func (c *offloadContainer) Queues() []Queue {
return c.pqi
}
func (c *offloadContainer) Add(fd int) error {
x, err := newOffload(fd, c.shutdownFd)
if err != nil {
return err
}
c.pq = append(c.pq, x)
c.pqi = append(c.pqi, x)
return nil
}
func (c *offloadContainer) wakeForShutdown() error {
var buf [8]byte
binary.NativeEndian.PutUint64(buf[:], 1)
_, err := unix.Write(c.shutdownFd, buf[:])
return err
}
func (c *offloadContainer) Close() error {
errs := []error{}
// Signal all readers blocked in poll to wake up and exit
if err := c.wakeForShutdown(); err != nil {
errs = append(errs, err)
}
for _, x := range c.pq {
if err := x.Close(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
+69
View File
@@ -0,0 +1,69 @@
package tio
import (
"encoding/binary"
"errors"
"fmt"
"golang.org/x/sys/unix"
)
type pollContainer struct {
pq []*Poll
// pqi is exactly the same as pq, but stored as the interface type
pqi []Queue
shutdownFd int
}
func NewPollContainer() (Container, error) {
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
if err != nil {
return nil, fmt.Errorf("failed to create eventfd: %w", err)
}
out := &pollContainer{
pq: []*Poll{},
pqi: []Queue{},
shutdownFd: shutdownFd,
}
return out, nil
}
func (c *pollContainer) Queues() []Queue {
return c.pqi
}
func (c *pollContainer) Add(fd int) error {
x, err := newPoll(fd, c.shutdownFd)
if err != nil {
return err
}
c.pq = append(c.pq, x)
c.pqi = append(c.pqi, x)
return nil
}
func (c *pollContainer) wakeForShutdown() error {
var buf [8]byte
binary.NativeEndian.PutUint64(buf[:], 1)
_, err := unix.Write(int(c.shutdownFd), buf[:])
return err
}
func (c *pollContainer) Close() error {
errs := []error{}
if err := c.wakeForShutdown(); err != nil {
errs = append(errs, err)
}
for _, x := range c.pq {
if err := x.Close(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
+67
View File
@@ -0,0 +1,67 @@
package tio
import (
"io"
)
// defaultBatchBufSize is the per-Queue scratch size for Read on backends
// that don't do TSO segmentation. 65535 covers any single IP packet.
const defaultBatchBufSize = 65535
// Container holds one or many Queue objects and helps close them in an orderly way
type Container interface {
io.Closer
Queues() []Queue
// Add takes a tun fd, adds it to the container, and prepares it for use as a Queue
Add(fd int) error
io.Closer
}
// Queue is a readable/writable Poll queue. One Queue is driven by a single
// read goroutine plus concurrent writers (see Write / WriteReject below).
type Queue interface {
io.Closer
// Read returns one or more packets. The returned slices are borrowed
// from the Queue's internal buffer and are only valid until the next
// Read or Close on this Queue - callers must encrypt or copy each
// slice before the next call. Not safe for concurrent Reads.
Read() ([][]byte, error)
// Write emits a single packet on the plaintext (outside→inside)
// delivery path. Not safe for concurrent Writes.
Write(p []byte) (int, error)
}
// GSOWriter is implemented by Queues that can emit a TCP TSO superpacket
// assembled from a header prefix plus one or more borrowed payload
// fragments, in a single vectored write (writev with a leading
// virtio_net_hdr). This lets the coalescer avoid copying payload bytes
// between the caller's decrypt buffer and the TUN. Backends without GSO
// support return false from GSOSupported and coalescing is skipped.
//
// hdr contains the IPv4/IPv6 + TCP header prefix (mutable - callers will
// have filled in total length and pseudo-header partial). pays are
// non-overlapping payload fragments whose concatenation is the full
// superpacket payload; they are read-only from the writer's perspective
// and must remain valid until the call returns. gsoSize is the MSS:
// every segment except possibly the last is exactly that many bytes.
// csumStart is the byte offset where the TCP header begins within hdr.
//
// # TODO fold into Queue
//
// hdr's TCP checksum field must already hold the pseudo-header partial
// sum (single-fold, not inverted), per virtio NEEDS_CSUM semantics.
type GSOWriter interface {
// WriteGSO emits a TCP TSO superpacket in a single writev. hdr is the
// IPv4/IPv6 + TCP header prefix (already finalized — total length, IP csum,
// and TCP pseudo-header partial set by the caller). pays are payload
// fragments whose concatenation forms the full coalesced payload; each
// slice is read-only and must stay valid until return.
// every segment in pays except possibly the last is exactly the same size.
// csumStart is the byte offset where the TCP header begins within hdr.
WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte) error
GSOSupported() bool
}
+353
View File
@@ -0,0 +1,353 @@
package tio
import (
"fmt"
"io"
"os"
"sync"
"sync/atomic"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
// Space for segmented output. Worst case is many small segments, each paying
// an IP+TCP header. Should be a multiple of 64KiB.
// const tunSegBufSize = 0xffff * 8 TODO larger? config?
const tunSegBufSize = 131072
// tunSegBufCap is the total size we allocate for the per-reader segment
// buffer. It is sized as one worst-case TSO superpacket (tunSegBufSize) plus
// the same again as drain headroom so a Read wake can accumulate
// additional packets after an initial big read without overflowing.
const tunSegBufCap = tunSegBufSize * 2
// tunDrainCap caps how many packets a single Read will accumulate via
// the post-wake drain loop. Sized to soak up a burst of small ACKs while
// bounding how much work a single caller holds before handing off.
const tunDrainCap = 64 //256
// gsoInitialPayIovs is the starting capacity (in payload fragments) of
// Offload.gsoIovs. Sized to cover the default coalesce segment cap without
// any reallocations.
const gsoInitialPayIovs = 66
// validVnetHdr is the 10-byte virtio_net_hdr we prepend to every non-GSO TUN
// write. Only flag set is VIRTIO_NET_HDR_F_DATA_VALID, which marks the skb
// CHECKSUM_UNNECESSARY so the receiving network stack skips L4 checks
// verification. All packets that reach the plain Write paths
// already carry a valid L4 checksum (either supplied by a remote peer whose
// ciphertext we AEAD-authenticated, or produced by finishChecksum during TSO
// segmentation, or built locally by CreateRejectPacket), so trusting them is
// safe.
var validVnetHdr = [virtioNetHdrLen]byte{unix.VIRTIO_NET_HDR_F_DATA_VALID}
// Offload wraps a TUN file descriptor with poll-based reads. The FD provided will be changed to non-blocking.
// A shared eventfd allows Close to wake all readers blocked in poll.
type Offload struct {
fd int
shutdownFd int
readPoll [2]unix.PollFd
writePoll [2]unix.PollFd
writeLock sync.Mutex //there's more than one potential write source per-routine, so we need this to protect writePoll
closed atomic.Bool
readBuf []byte // scratch for a single raw read (virtio hdr + superpacket)
segBuf []byte // backing store for segmented output
segOff int // cursor into segBuf for the current Read drain
pending [][]byte // segments returned from the most recent Read
// gsoHdrBuf is a per-queue 10-byte scratch for the virtio_net_hdr emitted
// by WriteGSO. Separate from validVnetHdr so a concurrent non-GSO Write on
// another queue never observes a half-written header.
gsoHdrBuf [virtioNetHdrLen]byte
// gsoIovs is the writev iovec scratch for WriteGSO. Sized to hold the
// virtio header + IP/TCP header + up to gsoInitialPayIovs payload
// fragments; grown on demand if a coalescer pushes more.
gsoIovs []unix.Iovec
}
func newOffload(fd int, shutdownFd int) (*Offload, error) {
if err := unix.SetNonblock(fd, true); err != nil {
return nil, fmt.Errorf("failed to set tun fd non-blocking: %w", err)
}
out := &Offload{
fd: fd,
shutdownFd: shutdownFd,
closed: atomic.Bool{},
readBuf: make([]byte, virtioNetHdrLen+tunReadBufSize),
readPoll: [2]unix.PollFd{
{Fd: int32(fd), Events: unix.POLLIN},
{Fd: int32(shutdownFd), Events: unix.POLLIN},
},
writePoll: [2]unix.PollFd{
{Fd: int32(fd), Events: unix.POLLOUT},
{Fd: int32(shutdownFd), Events: unix.POLLIN},
},
writeLock: sync.Mutex{},
segBuf: make([]byte, tunSegBufCap),
gsoIovs: make([]unix.Iovec, 2, 2+gsoInitialPayIovs),
}
out.gsoIovs[0].Base = &out.gsoHdrBuf[0]
out.gsoIovs[0].SetLen(virtioNetHdrLen)
return out, nil
}
func (r *Offload) blockOnRead() error {
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
var err error
for {
_, err = unix.Poll(r.readPoll[:], -1)
if err != unix.EINTR {
break
}
}
//always reset these!
tunEvents := r.readPoll[0].Revents
shutdownEvents := r.readPoll[1].Revents
r.readPoll[0].Revents = 0
r.readPoll[1].Revents = 0
//do the err check before trusting the potentially bogus bits we just got
if err != nil {
return err
}
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
return os.ErrClosed
} else if tunEvents&problemFlags != 0 {
return os.ErrClosed
}
return nil
}
func (r *Offload) blockOnWrite() error {
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
var err error
for {
_, err = unix.Poll(r.writePoll[:], -1)
if err != unix.EINTR {
break
}
}
//always reset these!
r.writeLock.Lock()
tunEvents := r.writePoll[0].Revents
shutdownEvents := r.writePoll[1].Revents
r.writePoll[0].Revents = 0
r.writePoll[1].Revents = 0
r.writeLock.Unlock()
//do the err check before trusting the potentially bogus bits we just got
if err != nil {
return err
}
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
return os.ErrClosed
} else if tunEvents&problemFlags != 0 {
return os.ErrClosed
}
return nil
}
func (r *Offload) readRaw(buf []byte) (int, error) {
for {
if n, err := unix.Read(r.fd, buf); err == nil {
return n, nil
} else if err == unix.EAGAIN {
if err = r.blockOnRead(); err != nil {
return 0, err
}
continue
} else if err == unix.EINTR {
continue
} else if err == unix.EBADF {
return 0, os.ErrClosed
} else {
return 0, err
}
}
}
// Read reads one or more superpackets from the tun and returns the
// resulting packets. The first read blocks via poll; once the fd is known
// readable we drain additional packets non-blocking until the kernel queue
// is empty (EAGAIN), we've collected tunDrainCap packets, or we're out of
// segBuf headroom. This amortizes the poll wake over bursts of small
// packets (e.g. TCP ACKs). Slices point into the Offload's internal buffers
// and are only valid until the next Read or Close on this Queue.
func (r *Offload) Read() ([][]byte, error) {
r.pending = r.pending[:0]
r.segOff = 0
// Initial (blocking) read. Retry on decode errors so a single bad
// packet does not stall the reader.
for {
n, err := r.readRaw(r.readBuf)
if err != nil {
return nil, err
}
if err := r.decodeRead(n); err != nil {
// Drop and read again — a bad packet should not kill the reader.
continue
}
break
}
// Drain: non-blocking reads until the kernel queue is empty, the drain
// cap is reached, or segBuf no longer has room for another worst-case
// superpacket.
for len(r.pending) < tunDrainCap && tunSegBufCap-r.segOff >= tunSegBufSize {
n, err := unix.Read(r.fd, r.readBuf)
if err != nil {
// EAGAIN / EINTR / anything else: stop draining. We already
// have a valid batch from the first read.
break
}
if n <= 0 {
break
}
if err := r.decodeRead(n); err != nil {
// Drop this packet and stop the drain; we'd rather hand off
// what we have than keep spinning here.
break
}
}
return r.pending, nil
}
// decodeRead decodes the virtio header plus payload in r.readBuf[:n], appends
// the segments to r.pending, and advances r.segOff by the total scratch used.
func (r *Offload) decodeRead(n int) error {
if n < virtioNetHdrLen {
return fmt.Errorf("short tun read: %d < %d", n, virtioNetHdrLen)
}
var hdr VirtioNetHdr
hdr.decode(r.readBuf[:virtioNetHdrLen])
before := len(r.pending)
if err := segmentInto(r.readBuf[virtioNetHdrLen:n], hdr, &r.pending, r.segBuf[r.segOff:]); err != nil {
return err
}
for k := before; k < len(r.pending); k++ {
r.segOff += len(r.pending[k])
}
return nil
}
func (r *Offload) Write(buf []byte) (int, error) {
iovs := [2]unix.Iovec{
{Base: &validVnetHdr[0]},
{Base: &buf[0]},
}
iovs[0].SetLen(virtioNetHdrLen)
iovs[1].SetLen(len(buf))
return r.writeWithScratch(buf, &iovs)
}
func (r *Offload) writeWithScratch(buf []byte, iovs *[2]unix.Iovec) (int, error) {
if len(buf) == 0 {
return 0, nil
}
// Point the payload iovec at the caller's buffer. iovs[0] is pre-wired
// to validVnetHdr during Offload construction so we don't rebuild it here.
iovs[1].Base = &buf[0]
iovs[1].SetLen(len(buf))
return r.rawWrite(unsafe.Slice(&iovs[0], len(iovs)))
}
func (r *Offload) rawWrite(iovs []unix.Iovec) (int, error) {
for {
n, _, errno := syscall.Syscall(unix.SYS_WRITEV, uintptr(r.fd), uintptr(unsafe.Pointer(&iovs[0])), uintptr(len(iovs)))
if errno == 0 {
if int(n) < virtioNetHdrLen {
return 0, io.ErrShortWrite
}
return int(n) - virtioNetHdrLen, nil
}
if errno == unix.EAGAIN {
if err := r.blockOnWrite(); err != nil {
return 0, err
}
continue
}
if errno == unix.EINTR {
continue
}
if errno == unix.EBADF {
return 0, os.ErrClosed
}
return 0, errno
}
}
// GSOSupported reports whether this queue was opened with IFF_VNET_HDR and
// can accept WriteGSO. When false, callers should fall back to per-segment
// Write calls.
func (r *Offload) GSOSupported() bool { return true }
func (r *Offload) WriteGSO(hdr []byte, transportHdr []byte, pays [][]byte) error {
if len(hdr) == 0 || len(pays) == 0 || len(transportHdr) == 0 {
return nil
}
vhdr := VirtioNetHdr{
Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
HdrLen: uint16(len(hdr) + len(transportHdr)),
GSOSize: uint16(len(pays[0])),
CsumStart: uint16(len(hdr)),
CsumOffset: 16, // TCP checksum field lives 16 bytes into the TCP header
}
if len(pays) > 1 {
ipVer := hdr[0] >> 4
if ipVer == 6 {
vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_TCPV6
} else if ipVer == 4 {
vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_TCPV4
} else {
vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_NONE
vhdr.GSOSize = 0
}
} else {
vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_NONE
vhdr.GSOSize = 0
}
vhdr.encode(r.gsoHdrBuf[:])
// Build the iovec array: [virtio_hdr, hdr, transportHdr, pays...]. r.gsoIovs[0] is
// wired to gsoHdrBuf at construction and never changes.
need := 3 + len(pays)
if cap(r.gsoIovs) < need {
grown := make([]unix.Iovec, need)
grown[0] = r.gsoIovs[0]
r.gsoIovs = grown
} else {
r.gsoIovs = r.gsoIovs[:need]
}
r.gsoIovs[1].Base = &hdr[0]
r.gsoIovs[1].SetLen(len(hdr))
r.gsoIovs[2].Base = &transportHdr[0]
r.gsoIovs[2].SetLen(len(transportHdr))
for i, p := range pays {
r.gsoIovs[3+i].Base = &p[0]
r.gsoIovs[3+i].SetLen(len(p))
}
_, err := r.rawWrite(r.gsoIovs)
return err
}
func (r *Offload) Close() error {
if r.closed.Swap(true) {
return nil
}
//shutdownFd is owned by the container, so we should not close it
var err error
if r.fd >= 0 {
err = unix.Close(r.fd)
r.fd = -1
}
return err
}
+164
View File
@@ -0,0 +1,164 @@
package tio
import (
"fmt"
"os"
"sync/atomic"
"golang.org/x/sys/unix"
)
// Maximum size we accept for a single read from a TUN with IFF_VNET_HDR. A
// TSO superpacket can be up to 64KiB of payload plus a single L2/L3/L4 header
// prefix plus the virtio header.
const tunReadBufSize = 65535
type Poll struct {
fd int
readPoll [2]unix.PollFd
writePoll [2]unix.PollFd
closed atomic.Bool
readBuf []byte
batchRet [1][]byte
}
func newPoll(fd int, shutdownFd int) (*Poll, error) {
if err := unix.SetNonblock(fd, true); err != nil {
_ = unix.Close(fd)
return nil, fmt.Errorf("failed to set Poll device as nonblocking: %w", err)
}
out := &Poll{
fd: fd,
readBuf: make([]byte, tunReadBufSize),
readPoll: [2]unix.PollFd{
{Fd: int32(fd), Events: unix.POLLIN},
{Fd: int32(shutdownFd), Events: unix.POLLIN},
},
writePoll: [2]unix.PollFd{
{Fd: int32(fd), Events: unix.POLLOUT},
{Fd: int32(shutdownFd), Events: unix.POLLIN},
},
}
return out, nil
}
// blockOnRead waits until the Poll fd is readable or shutdown has been signaled.
// Returns os.ErrClosed if Close was called.
func (t *Poll) blockOnRead() error {
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
var err error
for {
_, err = unix.Poll(t.readPoll[:], -1)
if err != unix.EINTR {
break
}
}
tunEvents := t.readPoll[0].Revents
shutdownEvents := t.readPoll[1].Revents
t.readPoll[0].Revents = 0
t.readPoll[1].Revents = 0
if err != nil {
return err
}
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
return os.ErrClosed
}
if tunEvents&problemFlags != 0 {
return os.ErrClosed
}
return nil
}
func (t *Poll) blockOnWrite() error {
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
var err error
for {
_, err = unix.Poll(t.writePoll[:], -1)
if err != unix.EINTR {
break
}
}
tunEvents := t.writePoll[0].Revents
shutdownEvents := t.writePoll[1].Revents
t.writePoll[0].Revents = 0
t.writePoll[1].Revents = 0
if err != nil {
return err
}
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
return os.ErrClosed
}
if tunEvents&problemFlags != 0 {
return os.ErrClosed
}
return nil
}
func (t *Poll) Read() ([][]byte, error) {
n, err := t.readOne(t.readBuf)
if err != nil {
return nil, err
}
t.batchRet[0] = t.readBuf[:n]
return t.batchRet[:], nil
}
func (t *Poll) readOne(to []byte) (int, error) {
for {
n, errno := unix.Read(t.fd, to)
if errno == nil {
return n, nil
}
switch errno {
case unix.EAGAIN:
if err := t.blockOnRead(); err != nil {
return 0, err
}
case unix.EINTR:
// retry
case unix.EBADF:
return 0, os.ErrClosed
default:
return 0, errno
}
}
}
// Write is only valid for single threaded use
func (t *Poll) Write(from []byte) (int, error) {
for {
n, errno := unix.Write(t.fd, from)
if errno == nil {
return n, nil
}
switch errno {
case unix.EAGAIN:
if err := t.blockOnWrite(); err != nil {
return 0, err
}
case unix.EINTR:
// retry
case unix.EBADF:
return 0, os.ErrClosed
default:
return 0, errno
}
}
}
func (t *Poll) Close() error {
if t.closed.Swap(true) {
return nil
}
//shutdownFd is owned by the container, so we should not close it
var err error
if t.fd >= 0 {
err = unix.Close(t.fd)
t.fd = -1
}
return err
}
+82
View File
@@ -0,0 +1,82 @@
//go:build linux && !android && !e2e_testing
// +build linux,!android,!e2e_testing
package tio
import (
"errors"
"os"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
// newReadPipe returns a read fd. The matching write fd is registered for cleanup.
// The caller takes ownership of the read fd (pass it to newOffload / newFriend).
func newReadPipe(t *testing.T) int {
t.Helper()
var fds [2]int
if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
t.Fatalf("pipe2: %v", err)
}
t.Cleanup(func() { _ = unix.Close(fds[1]) })
return fds[0]
}
func TestPoll_WakeForShutdown_WakesFriends(t *testing.T) {
pipe1 := newReadPipe(t)
pipe2 := newReadPipe(t)
parent, err := NewPollContainer()
require.NoError(t, err)
require.NoError(t, parent.Add(pipe1))
require.NoError(t, parent.Add(pipe2))
t.Cleanup(func() {
_ = unix.Close(pipe1)
_ = unix.Close(pipe2)
})
readers := parent.Queues()
errs := make([]error, len(readers))
var wg sync.WaitGroup
for i, r := range readers {
wg.Add(1)
go func(i int, r Queue) {
defer wg.Done()
_, errs[i] = r.Read()
}(i, r)
}
time.Sleep(50 * time.Millisecond)
if err := parent.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
done := make(chan struct{})
go func() { wg.Wait(); close(done) }()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("readers did not wake")
}
for i, err := range errs {
if !errors.Is(err, os.ErrClosed) {
t.Errorf("reader %d: expected os.ErrClosed, got %v", i, err)
}
}
}
func TestPoll_Close_Idempotent(t *testing.T) {
tf, err := newPoll(newReadPipe(t), 1)
require.NoError(t, err)
if err := tf.Close(); err != nil {
t.Fatalf("first Close: %v", err)
}
if err := tf.Close(); err != nil {
t.Fatalf("second Close should be a no-op, got %v", err)
}
}
+331
View File
@@ -0,0 +1,331 @@
//go:build linux && !android && !e2e_testing
// +build linux,!android,!e2e_testing
package tio
import (
"encoding/binary"
"errors"
"fmt"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
)
// Protocol header size bounds used to validate / cap kernel-supplied offsets.
const (
ipv4HeaderMinLen = 20 // IHL=5, no options
ipv4HeaderMaxLen = 60 // IHL=15, max options
ipv6FixedLen = 40 // IPv6 base header; extensions would extend this
tcpHeaderMinLen = 20 // data-offset=5, no options
tcpHeaderMaxLen = 60 // data-offset=15, max options
)
// Byte offsets inside an IPv4 header.
const (
ipv4TotalLenOff = 2
ipv4IDOff = 4
ipv4ChecksumOff = 10
ipv4SrcOff = 12
ipv4AddrsEnd = 20 // end of dst address (ipv4SrcOff + 2*4)
)
// Byte offsets inside an IPv6 header.
const (
ipv6PayloadLenOff = 4
ipv6SrcOff = 8
ipv6AddrsEnd = 40 // end of dst address (ipv6SrcOff + 2*16)
)
// Byte offsets inside a TCP header (relative to its start, i.e. csumStart).
const (
tcpSeqOff = 4
tcpDataOffOff = 12 // upper nibble is header len in 32-bit words
tcpFlagsOff = 13
tcpChecksumOff = 16
)
// tcpFinPshMask is cleared on every segment except the last of a TSO burst.
const tcpFinPshMask = 0x09 // FIN(0x01) | PSH(0x08)
func checkVirtioValid(pkt []byte, hdr VirtioNetHdr) error {
// When RSC_INFO is set the csum_start/csum_offset fields are repurposed to
// carry coalescing info rather than checksum offsets. A TUN writing via
// IFF_VNET_HDR should never emit this, but if it did we would silently
// miscompute the segment checksums — refuse the packet instead.
if hdr.Flags&unix.VIRTIO_NET_HDR_F_RSC_INFO != 0 {
return fmt.Errorf("virtio RSC_INFO flag not supported on TUN reads")
}
if len(pkt) < ipv4HeaderMinLen {
return fmt.Errorf("packet too short")
}
ipVersion := pkt[0] >> 4
switch hdr.GSOType {
case unix.VIRTIO_NET_HDR_GSO_TCPV4:
if ipVersion != 4 {
return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
}
case unix.VIRTIO_NET_HDR_GSO_TCPV6:
if ipVersion != 6 {
return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
}
default:
if !(ipVersion == 6 || ipVersion == 4) {
return fmt.Errorf("invalid IP version %d for GSO type %d", ipVersion, hdr.GSOType)
}
}
return nil
}
func handleGSONone(pkt []byte, hdr VirtioNetHdr, out *[][]byte, scratch []byte) error {
if len(pkt) > len(scratch) {
return fmt.Errorf("packet larger than segment buffer: %d > %d", len(pkt), len(scratch))
}
copy(scratch, pkt)
seg := scratch[:len(pkt)]
if hdr.Flags&unix.VIRTIO_NET_HDR_F_NEEDS_CSUM != 0 {
if err := finishChecksum(seg, hdr); err != nil {
return err
}
}
*out = append(*out, seg)
return nil
}
func correctHdrLen(pkt []byte, hdr *VirtioNetHdr) error {
// Thank you wireguard-go for documenting these edge-cases
// Don't trust hdr.hdrLen from the kernel as it can be equal to the length
// of the entire first packet when the kernel is handling it as part of a
// FORWARD path. Instead, parse the transport header length and add it onto
// csumStart, which is synonymous for IP header length.
const tcpDataOffset = 12
if hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_UDP_L4 {
hdr.HdrLen = hdr.CsumStart + 8
} else {
if len(pkt) <= int(hdr.CsumStart+tcpDataOffset) {
return errors.New("packet is too short")
}
tcpHLen := uint16(pkt[hdr.CsumStart+tcpDataOffset] >> 4 * 4)
if tcpHLen < 20 || tcpHLen > 60 {
// A TCP header must be between 20 and 60 bytes in length.
return fmt.Errorf("tcp header len is invalid: %d", tcpHLen)
}
hdr.HdrLen = hdr.CsumStart + tcpHLen
}
if len(pkt) < int(hdr.HdrLen) {
return fmt.Errorf("length of packet (%d) < virtioNetHdr.HdrLen (%d)", len(pkt), hdr.HdrLen)
}
if hdr.HdrLen < hdr.CsumStart {
return fmt.Errorf("virtioNetHdr.HdrLen (%d) < virtioNetHdr.CsumStart (%d)", hdr.HdrLen, hdr.CsumStart)
}
cSumAt := int(hdr.CsumStart + hdr.CsumStart)
if cSumAt+1 >= len(pkt) {
return fmt.Errorf("end of checksum offset (%d) exceeds packet length (%d)", cSumAt+1, len(pkt))
}
return nil
}
// segmentInto splits a TUN-side packet described by hdr into one or more
// IP packets, each appended to *out as a slice of scratch. scratch must be
// sized to hold every segment (including replicated headers).
func segmentInto(pkt []byte, hdr VirtioNetHdr, out *[][]byte, scratch []byte) error {
if err := checkVirtioValid(pkt, hdr); err != nil {
return err
}
if hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_NONE {
return handleGSONone(pkt, hdr, out, scratch)
}
if err := correctHdrLen(pkt, &hdr); err != nil {
return err
}
switch hdr.GSOType {
case unix.VIRTIO_NET_HDR_GSO_TCPV4, unix.VIRTIO_NET_HDR_GSO_TCPV6:
return segmentTCP(pkt, hdr, out, scratch)
default:
return fmt.Errorf("unsupported virtio gso type: %d", hdr.GSOType)
}
}
// finishChecksum computes the L4 checksum for a non-GSO packet that the kernel
// handed us with NEEDS_CSUM set. csum_start / csum_offset point at the 16-bit
// checksum field; we zero it, fold a full sum (the field was pre-loaded with
// the pseudo-header partial sum by the kernel), and store the result.
func finishChecksum(seg []byte, hdr VirtioNetHdr) error {
cs := int(hdr.CsumStart)
co := int(hdr.CsumOffset)
if cs+co+2 > len(seg) {
return fmt.Errorf("csum offsets out of range: start=%d offset=%d len=%d", cs, co, len(seg))
}
// The kernel stores a partial pseudo-header sum at [cs+co:]; sum over the
// L4 region starting at cs, folding the prior partial in as the seed.
partial := binary.BigEndian.Uint16(seg[cs+co : cs+co+2])
seg[cs+co] = 0
seg[cs+co+1] = 0
binary.BigEndian.PutUint16(seg[cs+co:cs+co+2], ^checksum.Checksum(seg[cs:], partial))
return nil
}
// segmentTCP software-segments a TSO superpacket into one IP packet per MSS
// chunk. The caller guarantees hdr.GSOType is TCPV4 or TCPV6.
//
// Hot-path shape: the per-segment loop only sums the payload chunk. The TCP
// header, the IPv4 header, and the pseudo-header src/dst/proto contributions
// are each summed once up front — every segment reuses those three pre-folded
// uint32 values and combines them with small per-segment deltas (seq, flags,
// tcpLen, ip_id, total_len) that are cheap to fold in.
func segmentTCP(pkt []byte, hdr VirtioNetHdr, out *[][]byte, scratch []byte) error {
if hdr.GSOSize == 0 {
return fmt.Errorf("gso_size is zero")
}
if hdr.CsumStart == 0 {
return fmt.Errorf("csum_start is zero")
}
isV4 := hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_TCPV4
headerLen := int(hdr.HdrLen) // already corrected by the caller
csumStart := int(hdr.CsumStart)
tcpHdrLen := int(pkt[csumStart+tcpDataOffOff]>>4) * 4
payload := pkt[headerLen:]
payLen := len(payload)
gsoSize := int(hdr.GSOSize)
numSeg := (payLen + gsoSize - 1) / gsoSize
if numSeg == 0 {
numSeg = 1
}
need := numSeg*headerLen + payLen
if need > len(scratch) {
return fmt.Errorf("scratch too small for %d segments: need %d have %d", numSeg, need, len(scratch))
}
origSeq := binary.BigEndian.Uint32(pkt[csumStart+tcpSeqOff : csumStart+tcpSeqOff+4])
origFlags := pkt[csumStart+tcpFlagsOff]
// Precompute the TCP header sum with seq/flags/csum zeroed. Copy onto
// the stack, zero the per-segment-varying fields, sum once.
var tmp [tcpHeaderMaxLen]byte
copy(tmp[:tcpHdrLen], pkt[csumStart:headerLen])
tmp[tcpSeqOff], tmp[tcpSeqOff+1], tmp[tcpSeqOff+2], tmp[tcpSeqOff+3] = 0, 0, 0, 0
tmp[tcpFlagsOff] = 0
tmp[tcpChecksumOff], tmp[tcpChecksumOff+1] = 0, 0
baseTcpHdrSum := uint32(checksum.Checksum(tmp[:tcpHdrLen], 0))
// Pseudo-header src+dst+proto contribution (tcpLen varies per segment).
var baseProtoSum uint32
if isV4 {
baseProtoSum = uint32(checksum.Checksum(pkt[ipv4SrcOff:ipv4AddrsEnd], 0))
} else {
baseProtoSum = uint32(checksum.Checksum(pkt[ipv6SrcOff:ipv6AddrsEnd], 0))
}
baseProtoSum += uint32(unix.IPPROTO_TCP)
// Precompute IPv4 header sum with total_len/id/csum zeroed.
var origIPID uint16
var ihl int
var baseIPHdrSum uint32
if isV4 {
origIPID = binary.BigEndian.Uint16(pkt[ipv4IDOff : ipv4IDOff+2])
ihl = int(pkt[0]&0x0f) * 4
if ihl < ipv4HeaderMinLen || ihl > csumStart {
return fmt.Errorf("bad IPv4 IHL: %d", ihl)
}
var ipTmp [ipv4HeaderMaxLen]byte
copy(ipTmp[:ihl], pkt[:ihl])
ipTmp[ipv4TotalLenOff], ipTmp[ipv4TotalLenOff+1] = 0, 0
ipTmp[ipv4IDOff], ipTmp[ipv4IDOff+1] = 0, 0
ipTmp[ipv4ChecksumOff], ipTmp[ipv4ChecksumOff+1] = 0, 0
baseIPHdrSum = uint32(checksum.Checksum(ipTmp[:ihl], 0))
}
off := 0
for i := 0; i < numSeg; i++ {
segStart := i * gsoSize
segEnd := segStart + gsoSize
if segEnd > payLen {
segEnd = payLen
}
segPayLen := segEnd - segStart
copy(scratch[off:], pkt[:headerLen])
copy(scratch[off+headerLen:], payload[segStart:segEnd])
seg := scratch[off : off+headerLen+segPayLen]
off += headerLen + segPayLen
segSeq := origSeq + uint32(segStart)
segFlags := origFlags
if i != numSeg-1 {
segFlags = origFlags &^ tcpFinPshMask
}
totalLen := headerLen + segPayLen
// Patch IP header and write the v4 header checksum from the precomputed base.
if isV4 {
segID := origIPID + uint16(i)
binary.BigEndian.PutUint16(seg[ipv4TotalLenOff:ipv4TotalLenOff+2], uint16(totalLen))
binary.BigEndian.PutUint16(seg[ipv4IDOff:ipv4IDOff+2], segID)
ipSum := baseIPHdrSum + uint32(totalLen) + uint32(segID)
binary.BigEndian.PutUint16(seg[ipv4ChecksumOff:ipv4ChecksumOff+2], foldComplement(ipSum))
} else {
// IPv6 payload length excludes the fixed header but includes any
// extension headers between [ipv6FixedLen:csumStart].
binary.BigEndian.PutUint16(seg[ipv6PayloadLenOff:ipv6PayloadLenOff+2], uint16(headerLen-ipv6FixedLen+segPayLen))
}
// Patch TCP header.
binary.BigEndian.PutUint32(seg[csumStart+tcpSeqOff:csumStart+tcpSeqOff+4], segSeq)
seg[csumStart+tcpFlagsOff] = segFlags
// (csum is written below; its prior contents in `seg` don't affect the
// computation since we never sum over the segment's own header.)
tcpLen := tcpHdrLen + segPayLen
paySum := uint32(checksum.Checksum(payload[segStart:segEnd], 0))
// Combine pre-folded uint32s into a wider accumulator, then fold. Using
// uint64 guards against overflow when segSeq's high bits set.
wide := uint64(baseTcpHdrSum) + uint64(paySum) + uint64(baseProtoSum)
wide += uint64(segSeq) + uint64(segFlags) + uint64(tcpLen)
wide = (wide & 0xffffffff) + (wide >> 32)
wide = (wide & 0xffffffff) + (wide >> 32)
binary.BigEndian.PutUint16(seg[csumStart+tcpChecksumOff:csumStart+tcpChecksumOff+2], foldComplement(uint32(wide)))
*out = append(*out, seg)
}
return nil
}
// foldComplement folds a 32-bit one's-complement partial sum to 16 bits and
// complements it, yielding the on-wire Internet checksum value.
func foldComplement(sum uint32) uint16 {
sum = (sum & 0xffff) + (sum >> 16)
sum = (sum & 0xffff) + (sum >> 16)
return ^uint16(sum)
}
// pseudoHeaderIPv4 returns the folded pseudo-header sum used to verify a TCP
// segment's checksum in tests. src/dst are 4 bytes each.
func pseudoHeaderIPv4(src, dst []byte, proto byte, tcpLen int) uint16 {
s := uint32(checksum.Checksum(src, 0)) + uint32(checksum.Checksum(dst, 0))
s += uint32(proto) + uint32(tcpLen)
s = (s & 0xffff) + (s >> 16)
s = (s & 0xffff) + (s >> 16)
return uint16(s)
}
// pseudoHeaderIPv6 returns the folded pseudo-header sum used to verify a TCP
// segment's checksum in tests. src/dst are 16 bytes each.
func pseudoHeaderIPv6(src, dst []byte, proto byte, tcpLen int) uint16 {
s := uint32(checksum.Checksum(src, 0)) + uint32(checksum.Checksum(dst, 0))
s += uint32(tcpLen>>16) + uint32(tcpLen&0xffff) + uint32(proto)
s = (s & 0xffff) + (s >> 16)
s = (s & 0xffff) + (s >> 16)
return uint16(s)
}
@@ -1,7 +1,7 @@
//go:build linux && !android && !e2e_testing //go:build linux && !android && !e2e_testing
// +build linux,!android,!e2e_testing // +build linux,!android,!e2e_testing
package overlay package tio
import ( import (
"encoding/binary" "encoding/binary"
@@ -9,21 +9,18 @@ import (
"testing" "testing"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
) )
// verifyChecksum confirms that the one's-complement sum across `b`, optionally // verifyChecksum confirms that the one's-complement sum across `b`, seeded
// seeded with a pseudo-header sum, folds to all-ones (valid). // with a folded pseudo-header sum, equals all-ones (valid).
func verifyChecksum(b []byte, pseudo uint32) bool { func verifyChecksum(b []byte, pseudo uint16) bool {
sum := checksumBytes(b, pseudo) return checksum.Checksum(b, pseudo) == 0xffff
for sum>>16 != 0 {
sum = (sum & 0xffff) + (sum >> 16)
}
return uint16(sum) == 0xffff
} }
// buildTSOv4 builds a synthetic IPv4/TCP TSO superpacket with a payload of // buildTSOv4 builds a synthetic IPv4/TCP TSO superpacket with a payload of
// `payLen` bytes split at `mss`. // `payLen` bytes split at `mss`.
func buildTSOv4(t *testing.T, payLen, mss int) ([]byte, virtioNetHdr) { func buildTSOv4(t *testing.T, payLen, mss int) ([]byte, VirtioNetHdr) {
t.Helper() t.Helper()
const ipLen = 20 const ipLen = 20
const tcpLen = 20 const tcpLen = 20
@@ -53,7 +50,7 @@ func buildTSOv4(t *testing.T, payLen, mss int) ([]byte, virtioNetHdr) {
pkt[ipLen+tcpLen+i] = byte(i & 0xff) pkt[ipLen+tcpLen+i] = byte(i & 0xff)
} }
return pkt, virtioNetHdr{ return pkt, VirtioNetHdr{
Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV4, GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV4,
HdrLen: uint16(ipLen + tcpLen), HdrLen: uint16(ipLen + tcpLen),
@@ -174,7 +171,7 @@ func TestSegmentTCPv6(t *testing.T) {
pkt[ipLen+tcpLen+i] = byte(i) pkt[ipLen+tcpLen+i] = byte(i)
} }
hdr := virtioNetHdr{ hdr := VirtioNetHdr{
Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV6, GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV6,
HdrLen: uint16(ipLen + tcpLen), HdrLen: uint16(ipLen + tcpLen),
@@ -240,7 +237,7 @@ func TestSegmentGSONonePassesThrough(t *testing.T) {
} }
func TestSegmentRejectsUDP(t *testing.T) { func TestSegmentRejectsUDP(t *testing.T) {
hdr := virtioNetHdr{GSOType: unix.VIRTIO_NET_HDR_GSO_UDP} hdr := VirtioNetHdr{GSOType: unix.VIRTIO_NET_HDR_GSO_UDP}
var out [][]byte var out [][]byte
if err := segmentInto(nil, hdr, &out, nil); err == nil { if err := segmentInto(nil, hdr, &out, nil); err == nil {
t.Fatalf("expected rejection for UDP GSO") t.Fatalf("expected rejection for UDP GSO")
@@ -279,7 +276,7 @@ func BenchmarkSegmentTCPv4(b *testing.B) {
for i := 0; i < sz.payLen; i++ { for i := 0; i < sz.payLen; i++ {
pkt[ipLen+tcpLen+i] = byte(i) pkt[ipLen+tcpLen+i] = byte(i)
} }
hdr := virtioNetHdr{ hdr := VirtioNetHdr{
Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM, Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV4, GSOType: unix.VIRTIO_NET_HDR_GSO_TCPV4,
HdrLen: uint16(ipLen + tcpLen), HdrLen: uint16(ipLen + tcpLen),
@@ -312,9 +309,7 @@ func TestTunFileWriteVnetHdrNoAlloc(t *testing.T) {
} }
t.Cleanup(func() { _ = unix.Close(fd) }) t.Cleanup(func() { _ = unix.Close(fd) })
tf := &tunFile{fd: fd, vnetHdr: true} tf := &Offload{fd: fd}
tf.writeIovs[0].Base = &validVnetHdr[0]
tf.writeIovs[0].SetLen(virtioNetHdrLen)
payload := make([]byte, 1400) payload := make([]byte, 1400)
// Warm up (first call may trigger one-time internal allocations elsewhere). // Warm up (first call may trigger one-time internal allocations elsewhere).
+39
View File
@@ -0,0 +1,39 @@
package tio
import "encoding/binary"
// Size of the legacy struct virtio_net_hdr that the kernel prepends/expects on
// a TUN opened with IFF_VNET_HDR (TUNSETVNETHDRSZ not set).
const virtioNetHdrLen = 10
type VirtioNetHdr struct {
Flags uint8
GSOType uint8
HdrLen uint16
GSOSize uint16
CsumStart uint16
CsumOffset uint16
}
// decode reads a virtio_net_hdr in host byte order (TUN default; we never
// call TUNSETVNETLE so the kernel matches our endianness).
func (h *VirtioNetHdr) decode(b []byte) {
h.Flags = b[0]
h.GSOType = b[1]
h.HdrLen = binary.NativeEndian.Uint16(b[2:4])
h.GSOSize = binary.NativeEndian.Uint16(b[4:6])
h.CsumStart = binary.NativeEndian.Uint16(b[6:8])
h.CsumOffset = binary.NativeEndian.Uint16(b[8:10])
}
// encode is the inverse of decode: writes the virtio_net_hdr fields into b
// (must be at least virtioNetHdrLen bytes). Used to emit a TSO superpacket
// on egress.
func (h *VirtioNetHdr) encode(b []byte) {
b[0] = h.Flags
b[1] = h.GSOType
binary.NativeEndian.PutUint16(b[2:4], h.HdrLen)
binary.NativeEndian.PutUint16(b[4:6], h.GSOSize)
binary.NativeEndian.PutUint16(b[6:8], h.CsumStart)
binary.NativeEndian.PutUint16(b[8:10], h.CsumOffset)
}
+4 -4
View File
@@ -2,10 +2,10 @@ package overlay
import ( import (
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
) )
@@ -22,9 +22,9 @@ func (e *NameError) Error() string {
} }
// TODO: We may be able to remove routines // TODO: We may be able to remove routines
type DeviceFactory func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) type DeviceFactory func(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error)
func NewDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) { func NewDeviceFromConfig(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) {
switch { switch {
case c.GetBool("tun.disabled", false): case c.GetBool("tun.disabled", false):
tun := newDisabledTun(vpnNetworks, c.GetInt("tun.tx_queue", 500), c.GetBool("stats.message_metrics", false), l) tun := newDisabledTun(vpnNetworks, c.GetInt("tun.tx_queue", 500), c.GetBool("stats.message_metrics", false), l)
@@ -36,7 +36,7 @@ func NewDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Pref
} }
func NewFdDeviceFromConfig(fd *int) DeviceFactory { func NewFdDeviceFromConfig(fd *int) DeviceFactory {
return func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) { return func(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) {
return newTunFromFd(c, l, *fd, vpnNetworks) return newTunFromFd(c, l, *fd, vpnNetworks)
} }
} }
+13 -14
View File
@@ -6,13 +6,14 @@ package overlay
import ( import (
"fmt" "fmt"
"io" "io"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"sync/atomic" "sync/atomic"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
) )
@@ -23,16 +24,13 @@ type tun struct {
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *logrus.Logger l *slog.Logger
readBuf []byte readBuf []byte
batchRet [1][]byte batchRet [1][]byte
} }
func (t *tun) Read() ([][]byte, error) { func (t *tun) Read() ([][]byte, error) {
if t.readBuf == nil {
t.readBuf = make([]byte, defaultBatchBufSize)
}
n, err := t.rwc.Read(t.readBuf) n, err := t.rwc.Read(t.readBuf)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -45,15 +43,11 @@ func (t *tun) Write(p []byte) (int, error) {
return t.rwc.Write(p) return t.rwc.Write(p)
} }
func (t *tun) WriteReject(p []byte) (int, error) {
return t.rwc.Write(p)
}
func (t *tun) Close() error { func (t *tun) Close() error {
return t.rwc.Close() return t.rwc.Close()
} }
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) { func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
// XXX Android returns an fd in non-blocking mode which is necessary for shutdown to work properly. // XXX Android returns an fd in non-blocking mode which is necessary for shutdown to work properly.
// Be sure not to call file.Fd() as it will set the fd to blocking mode. // Be sure not to call file.Fd() as it will set the fd to blocking mode.
file := os.NewFile(uintptr(deviceFd), "/dev/net/tun") file := os.NewFile(uintptr(deviceFd), "/dev/net/tun")
@@ -63,6 +57,7 @@ func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []net
fd: deviceFd, fd: deviceFd,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
l: l, l: l,
readBuf: make([]byte, defaultBatchBufSize),
} }
err := t.reload(c, true) err := t.reload(c, true)
@@ -80,7 +75,7 @@ func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []net
return t, nil return t, nil
} }
func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ bool) (*tun, error) { func newTun(_ *config.C, _ *slog.Logger, _ []netip.Prefix, _ bool) (*tun, error) {
return nil, fmt.Errorf("newTun not supported in Android") return nil, fmt.Errorf("newTun not supported in Android")
} }
@@ -89,7 +84,7 @@ func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
return r return r
} }
func (t tun) Activate() error { func (t *tun) Activate() error {
return nil return nil
} }
@@ -126,6 +121,10 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (Queue, error) { func (t *tun) NewMultiQueueReader() error {
return nil, fmt.Errorf("TODO: multiqueue not implemented for android") return fmt.Errorf("TODO: multiqueue not implemented for android")
}
func (t *tun) Readers() []tio.Queue {
return []tio.Queue{t}
} }
+16 -18
View File
@@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"sync/atomic" "sync/atomic"
@@ -14,8 +15,8 @@ import (
"unsafe" "unsafe"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
netroute "golang.org/x/net/route" netroute "golang.org/x/net/route"
@@ -30,7 +31,7 @@ type tun struct {
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
linkAddr *netroute.LinkAddr linkAddr *netroute.LinkAddr
l *logrus.Logger l *slog.Logger
// cache out buffer since we need to prepend 4 bytes for tun metadata // cache out buffer since we need to prepend 4 bytes for tun metadata
out []byte out []byte
@@ -82,7 +83,7 @@ type ifreqAlias6 struct {
Lifetime addrLifetime Lifetime addrLifetime
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
name := c.GetString("tun.dev", "") name := c.GetString("tun.dev", "")
ifIndex := -1 ifIndex := -1
if name != "" && name != "utun" { if name != "" && name != "utun" {
@@ -132,6 +133,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU), DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
l: l, l: l,
readBuf: make([]byte, defaultBatchBufSize),
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -156,7 +158,7 @@ func (t *tun) deviceBytes() (o [16]byte) {
return return
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in Darwin") return nil, fmt.Errorf("newTunFromFd not supported in Darwin")
} }
@@ -392,8 +394,7 @@ func (t *tun) addRoutes(logErrors bool) error {
err := addRoute(r.Cidr, t.linkAddr) err := addRoute(r.Cidr, t.linkAddr)
if err != nil { if err != nil {
if errors.Is(err, unix.EEXIST) { if errors.Is(err, unix.EEXIST) {
t.l.WithField("route", r.Cidr). t.l.Warn("unable to add unsafe_route, identical route already exists", "route", r.Cidr)
Warnf("unable to add unsafe_route, identical route already exists")
} else { } else {
retErr := util.NewContextualError("Failed to add route", map[string]any{"route": r}, err) retErr := util.NewContextualError("Failed to add route", map[string]any{"route": r}, err)
if logErrors { if logErrors {
@@ -403,7 +404,7 @@ func (t *tun) addRoutes(logErrors bool) error {
} }
} }
} else { } else {
t.l.WithField("route", r).Info("Added route") t.l.Info("Added route", "route", r)
} }
} }
@@ -418,9 +419,9 @@ func (t *tun) removeRoutes(routes []Route) error {
err := delRoute(r.Cidr, t.linkAddr) err := delRoute(r.Cidr, t.linkAddr)
if err != nil { if err != nil {
t.l.WithError(err).WithField("route", r).Error("Failed to remove route") t.l.Error("Failed to remove route", "error", err, "route", r)
} else { } else {
t.l.WithField("route", r).Info("Removed route") t.l.Info("Removed route", "route", r)
} }
} }
return nil return nil
@@ -516,9 +517,6 @@ func (t *tun) readOne(to []byte) (int, error) {
} }
func (t *tun) Read() ([][]byte, error) { func (t *tun) Read() ([][]byte, error) {
if t.readBuf == nil {
t.readBuf = make([]byte, defaultBatchBufSize)
}
n, err := t.readOne(t.readBuf) n, err := t.readOne(t.readBuf)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -527,10 +525,6 @@ func (t *tun) Read() ([][]byte, error) {
return t.batchRet[:], nil return t.batchRet[:], nil
} }
func (t *tun) WriteReject(p []byte) (int, error) {
return t.Write(p)
}
// Write is only valid for single threaded use // Write is only valid for single threaded use
func (t *tun) Write(from []byte) (int, error) { func (t *tun) Write(from []byte) (int, error) {
buf := t.out buf := t.out
@@ -572,6 +566,10 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (Queue, error) { func (t *tun) NewMultiQueueReader() error {
return nil, fmt.Errorf("TODO: multiqueue not implemented for darwin") return fmt.Errorf("TODO: multiqueue not implemented for darwin")
}
func (t *tun) Readers() []tio.Queue {
return []tio.Queue{t}
} }
+27 -18
View File
@@ -1,14 +1,16 @@
package overlay package overlay
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/netip" "net/netip"
"strings" "strings"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/iputil" "github.com/slackhq/nebula/iputil"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
@@ -17,9 +19,10 @@ type disabledTun struct {
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
// Track these metrics since we don't have the tun device to do it for us // Track these metrics since we don't have the tun device to do it for us
tx metrics.Counter tx metrics.Counter
rx metrics.Counter rx metrics.Counter
l *logrus.Logger l *slog.Logger
numReaders int
batchRet [1][]byte batchRet [1][]byte
} }
@@ -31,19 +34,20 @@ func (t *disabledTun) Read() ([][]byte, error) {
} }
t.tx.Inc(1) t.tx.Inc(1)
if t.l.Level >= logrus.DebugLevel { if t.l.Enabled(context.Background(), slog.LevelDebug) {
t.l.WithField("raw", prettyPacket(r)).Debugf("Write payload") t.l.Debug("Write payload", "raw", prettyPacket(r))
} }
t.batchRet[0] = r t.batchRet[0] = r
return t.batchRet[:], nil return t.batchRet[:], nil
} }
func newDisabledTun(vpnNetworks []netip.Prefix, queueLen int, metricsEnabled bool, l *logrus.Logger) *disabledTun { func newDisabledTun(vpnNetworks []netip.Prefix, queueLen int, metricsEnabled bool, l *slog.Logger) *disabledTun {
tun := &disabledTun{ tun := &disabledTun{
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
read: make(chan []byte, queueLen), read: make(chan []byte, queueLen),
l: l, l: l,
numReaders: 1,
} }
if metricsEnabled { if metricsEnabled {
@@ -84,7 +88,7 @@ func (t *disabledTun) handleICMPEchoRequest(b []byte) bool {
select { select {
case t.read <- out: case t.read <- out:
default: default:
t.l.Debugf("tun_disabled: dropped ICMP Echo Reply response") t.l.Debug("tun_disabled: dropped ICMP Echo Reply response")
} }
return true return true
@@ -95,25 +99,30 @@ func (t *disabledTun) Write(b []byte) (int, error) {
// Check for ICMP Echo Request before spending time doing the full parsing // Check for ICMP Echo Request before spending time doing the full parsing
if t.handleICMPEchoRequest(b) { if t.handleICMPEchoRequest(b) {
if t.l.Level >= logrus.DebugLevel { if t.l.Enabled(context.Background(), slog.LevelDebug) {
t.l.WithField("raw", prettyPacket(b)).Debugf("Disabled tun responded to ICMP Echo Request") t.l.Debug("Disabled tun responded to ICMP Echo Request", "raw", prettyPacket(b))
} }
} else if t.l.Level >= logrus.DebugLevel { } else if t.l.Enabled(context.Background(), slog.LevelDebug) {
t.l.WithField("raw", prettyPacket(b)).Debugf("Disabled tun received unexpected payload") t.l.Debug("Disabled tun received unexpected payload", "raw", prettyPacket(b))
} }
return len(b), nil return len(b), nil
} }
func (t *disabledTun) WriteReject(b []byte) (int, error) {
return t.Write(b)
}
func (t *disabledTun) SupportsMultiqueue() bool { func (t *disabledTun) SupportsMultiqueue() bool {
return true return true
} }
func (t *disabledTun) NewMultiQueueReader() (Queue, error) { func (t *disabledTun) NewMultiQueueReader() error {
return t, nil t.numReaders++
return nil
}
func (t *disabledTun) Readers() []tio.Queue {
out := make([]tio.Queue, t.numReaders)
for i := range t.numReaders {
out[i] = t
}
return out
} }
func (t *disabledTun) Close() error { func (t *disabledTun) Close() error {
-120
View File
@@ -1,120 +0,0 @@
//go:build linux && !android && !e2e_testing
// +build linux,!android,!e2e_testing
package overlay
import (
"errors"
"os"
"sync"
"testing"
"time"
"golang.org/x/sys/unix"
)
// newReadPipe returns a read fd. The matching write fd is registered for cleanup.
// The caller takes ownership of the read fd (pass it to newTunFd / newFriend).
func newReadPipe(t *testing.T) int {
t.Helper()
var fds [2]int
if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
t.Fatalf("pipe2: %v", err)
}
t.Cleanup(func() { _ = unix.Close(fds[1]) })
return fds[0]
}
func TestTunFile_WakeForShutdown_UnblocksRead(t *testing.T) {
tf, err := newTunFd(newReadPipe(t))
if err != nil {
t.Fatalf("newTunFd: %v", err)
}
t.Cleanup(func() { _ = tf.Close() })
done := make(chan error, 1)
go func() {
_, err := tf.Read(make([]byte, 64))
done <- err
}()
// Verify Read is actually blocked in poll.
select {
case err := <-done:
t.Fatalf("Read returned before shutdown signal: %v", err)
case <-time.After(50 * time.Millisecond):
}
if err := tf.wakeForShutdown(); err != nil {
t.Fatalf("wakeForShutdown: %v", err)
}
select {
case err := <-done:
if !errors.Is(err, os.ErrClosed) {
t.Fatalf("expected os.ErrClosed, got %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Read did not wake on shutdown")
}
}
func TestTunFile_WakeForShutdown_WakesFriends(t *testing.T) {
parent, err := newTunFd(newReadPipe(t))
if err != nil {
t.Fatalf("newTunFd: %v", err)
}
friend, err := parent.newFriend(newReadPipe(t))
if err != nil {
_ = parent.Close()
t.Fatalf("newFriend: %v", err)
}
t.Cleanup(func() {
_ = friend.Close()
_ = parent.Close()
})
readers := []*tunFile{parent, friend}
errs := make([]error, len(readers))
var wg sync.WaitGroup
for i, r := range readers {
wg.Add(1)
go func(i int, r *tunFile) {
defer wg.Done()
_, errs[i] = r.Read(make([]byte, 64))
}(i, r)
}
time.Sleep(50 * time.Millisecond)
if err := parent.wakeForShutdown(); err != nil {
t.Fatalf("wakeForShutdown: %v", err)
}
done := make(chan struct{})
go func() { wg.Wait(); close(done) }()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("readers did not wake")
}
for i, err := range errs {
if !errors.Is(err, os.ErrClosed) {
t.Errorf("reader %d: expected os.ErrClosed, got %v", i, err)
}
}
}
func TestTunFile_Close_Idempotent(t *testing.T) {
tf, err := newTunFd(newReadPipe(t))
if err != nil {
t.Fatalf("newTunFd: %v", err)
}
if err := tf.Close(); err != nil {
t.Fatalf("first Close: %v", err)
}
if err := tf.Close(); err != nil {
t.Fatalf("second Close should be a no-op, got %v", err)
}
}
+18 -18
View File
@@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"sync/atomic" "sync/atomic"
@@ -16,8 +17,9 @@ import (
"unsafe" "unsafe"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
netroute "golang.org/x/net/route" netroute "golang.org/x/net/route"
@@ -92,7 +94,7 @@ type tun struct {
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
linkAddr *netroute.LinkAddr linkAddr *netroute.LinkAddr
l *logrus.Logger l *slog.Logger
fd int fd int
shutdownR int // read end of the shutdown pipe; closing the write end wakes blocked polls shutdownR int // read end of the shutdown pipe; closing the write end wakes blocked polls
@@ -158,9 +160,6 @@ func (t *tun) blockOnWrite() error {
} }
func (t *tun) Read() ([][]byte, error) { func (t *tun) Read() ([][]byte, error) {
if t.readBuf == nil {
t.readBuf = make([]byte, defaultBatchBufSize)
}
n, err := t.readOne(t.readBuf) n, err := t.readOne(t.readBuf)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -169,10 +168,6 @@ func (t *tun) Read() ([][]byte, error) {
return t.batchRet[:], nil return t.batchRet[:], nil
} }
func (t *tun) WriteReject(p []byte) (int, error) {
return t.Write(p)
}
func (t *tun) readOne(to []byte) (int, error) { func (t *tun) readOne(to []byte) (int, error) {
// first 4 bytes is protocol family, in network byte order // first 4 bytes is protocol family, in network byte order
var head [4]byte var head [4]byte
@@ -261,7 +256,7 @@ func (t *tun) Close() error {
if t.fd >= 0 { if t.fd >= 0 {
if err := unix.Close(t.fd); err != nil { if err := unix.Close(t.fd); err != nil {
t.l.WithError(err).Error("Error closing device") t.l.Error("Error closing device", "error", err)
} }
t.fd = -1 t.fd = -1
} }
@@ -282,7 +277,7 @@ func (t *tun) Close() error {
err = ioctl(uintptr(s), syscall.SIOCIFDESTROY, uintptr(unsafe.Pointer(&ifreq))) err = ioctl(uintptr(s), syscall.SIOCIFDESTROY, uintptr(unsafe.Pointer(&ifreq)))
} }
if err != nil { if err != nil {
t.l.WithError(err).Error("Error destroying tunnel") t.l.Error("Error destroying tunnel", "error", err)
} }
}() }()
@@ -295,11 +290,11 @@ func (t *tun) Close() error {
return nil return nil
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in FreeBSD") return nil, fmt.Errorf("newTunFromFd not supported in FreeBSD")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open existing tun device // Try to open existing tun device
var fd int var fd int
var err error var err error
@@ -391,6 +386,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
MTU: c.GetInt("tun.mtu", DefaultMTU), MTU: c.GetInt("tun.mtu", DefaultMTU),
l: l, l: l,
fd: fd, fd: fd,
readBuf: make([]byte, defaultBatchBufSize),
shutdownR: shutdownR, shutdownR: shutdownR,
shutdownW: shutdownW, shutdownW: shutdownW,
readPoll: [2]unix.PollFd{ readPoll: [2]unix.PollFd{
@@ -581,8 +577,8 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (Queue, error) { func (t *tun) NewMultiQueueReader() error {
return nil, fmt.Errorf("TODO: multiqueue not implemented for freebsd") return fmt.Errorf("TODO: multiqueue not implemented for freebsd")
} }
func (t *tun) addRoutes(logErrors bool) error { func (t *tun) addRoutes(logErrors bool) error {
@@ -602,13 +598,17 @@ func (t *tun) addRoutes(logErrors bool) error {
return retErr return retErr
} }
} else { } else {
t.l.WithField("route", r).Info("Added route") t.l.Info("Added route", "route", r)
} }
} }
return nil return nil
} }
func (t *tun) Readers() []tio.Queue {
return []tio.Queue{t}
}
func (t *tun) removeRoutes(routes []Route) error { func (t *tun) removeRoutes(routes []Route) error {
for _, r := range routes { for _, r := range routes {
if !r.Install { if !r.Install {
@@ -617,9 +617,9 @@ func (t *tun) removeRoutes(routes []Route) error {
err := delRoute(r.Cidr, t.linkAddr) err := delRoute(r.Cidr, t.linkAddr)
if err != nil { if err != nil {
t.l.WithError(err).WithField("route", r).Error("Failed to remove route") t.l.Error("Failed to remove route", "error", err, "route", r)
} else { } else {
t.l.WithField("route", r).Info("Removed route") t.l.Info("Removed route", "route", r)
} }
} }
return nil return nil
+12 -13
View File
@@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"sync" "sync"
@@ -14,8 +15,8 @@ import (
"syscall" "syscall"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
) )
@@ -25,16 +26,13 @@ type tun struct {
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *logrus.Logger l *slog.Logger
readBuf []byte readBuf []byte
batchRet [1][]byte batchRet [1][]byte
} }
func (t *tun) Read() ([][]byte, error) { func (t *tun) Read() ([][]byte, error) {
if t.readBuf == nil {
t.readBuf = make([]byte, defaultBatchBufSize)
}
n, err := t.rwc.Read(t.readBuf) n, err := t.rwc.Read(t.readBuf)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -47,24 +45,21 @@ func (t *tun) Write(p []byte) (int, error) {
return t.rwc.Write(p) return t.rwc.Write(p)
} }
func (t *tun) WriteReject(p []byte) (int, error) {
return t.rwc.Write(p)
}
func (t *tun) Close() error { func (t *tun) Close() error {
return t.rwc.Close() return t.rwc.Close()
} }
func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ bool) (*tun, error) { func newTun(_ *config.C, _ *slog.Logger, _ []netip.Prefix, _ bool) (*tun, error) {
return nil, fmt.Errorf("newTun not supported in iOS") return nil, fmt.Errorf("newTun not supported in iOS")
} }
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) { func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
file := os.NewFile(uintptr(deviceFd), "/dev/tun") file := os.NewFile(uintptr(deviceFd), "/dev/tun")
t := &tun{ t := &tun{
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
rwc: &tunReadCloser{f: file}, rwc: &tunReadCloser{f: file},
l: l, l: l,
readBuf: make([]byte, defaultBatchBufSize),
} }
err := t.reload(c, true) err := t.reload(c, true)
@@ -182,6 +177,10 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (Queue, error) { func (t *tun) NewMultiQueueReader() error {
return nil, fmt.Errorf("TODO: multiqueue not implemented for ios") return fmt.Errorf("TODO: multiqueue not implemented for ios")
}
func (t *tun) Readers() []tio.Queue {
return []tio.Queue{t}
} }
+68 -520
View File
@@ -4,478 +4,28 @@
package overlay package overlay
import ( import (
"encoding/binary"
"fmt" "fmt"
"io" "log/slog"
"net" "net"
"net/netip" "net/netip"
"os" "os"
"runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall"
"time" "time"
"unsafe" "unsafe"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// tunFile wraps a TUN file descriptor with poll-based reads. The FD provided will be changed to non-blocking.
// A shared eventfd allows Close to wake all readers blocked in poll.
type tunFile struct {
fd int
shutdownFd int
lastOne bool
readPoll [2]unix.PollFd
writePoll [2]unix.PollFd
closed bool
// vnetHdr is true when this fd was opened with IFF_VNET_HDR and the
// kernel successfully accepted TUNSETOFFLOAD. Reads include a leading
// virtio_net_hdr and may carry a TSO superpacket we must segment;
// writes must prepend a zeroed virtio_net_hdr.
vnetHdr bool
readBuf []byte // scratch for a single raw read (virtio hdr + superpacket)
segBuf []byte // backing store for segmented output
segOff int // cursor into segBuf for the current Read drain
pending [][]byte // segments returned from the most recent Read
writeIovs [2]unix.Iovec // preallocated iovecs for Write (coalescer passthrough); iovs[0] is fixed to validVnetHdr
// rejectIovs is a second preallocated iovec scratch used exclusively by
// WriteReject (reject + self-forward from the inside path). It mirrors
// writeIovs but lets listenIn goroutines emit reject packets without
// racing with the listenOut coalescer that owns writeIovs.
rejectIovs [2]unix.Iovec
// gsoHdrBuf is a per-queue 10-byte scratch for the virtio_net_hdr emitted
// by WriteGSO. Separate from validVnetHdr so a concurrent non-GSO Write on
// another queue never observes a half-written header.
gsoHdrBuf [virtioNetHdrLen]byte
// gsoIovs is the writev iovec scratch for WriteGSO. Sized to hold the
// virtio header + IP/TCP header + up to gsoInitialPayIovs payload
// fragments; grown on demand if a coalescer pushes more.
gsoIovs []unix.Iovec
}
// gsoInitialPayIovs is the starting capacity (in payload fragments) of
// tunFile.gsoIovs. Sized to cover the default coalesce segment cap without
// any reallocations.
const gsoInitialPayIovs = 66
// validVnetHdr is the 10-byte virtio_net_hdr we prepend to every non-GSO TUN
// write. Only flag set is VIRTIO_NET_HDR_F_DATA_VALID, which marks the skb
// CHECKSUM_UNNECESSARY so the receiving network stack skips L4 checksum
// verification. All packets that reach the plain Write / WriteReject paths
// already carry a valid L4 checksum (either supplied by a remote peer whose
// ciphertext we AEAD-authenticated, or produced by finishChecksum during TSO
// segmentation, or built locally by CreateRejectPacket), so trusting them is
// safe.
var validVnetHdr = [virtioNetHdrLen]byte{unix.VIRTIO_NET_HDR_F_DATA_VALID}
// newFriend makes a tunFile for a MultiQueueReader that copies the shutdown eventfd from the parent tun
func (r *tunFile) newFriend(fd int) (*tunFile, error) {
if err := unix.SetNonblock(fd, true); err != nil {
return nil, fmt.Errorf("failed to set tun fd non-blocking: %w", err)
}
out := &tunFile{
fd: fd,
shutdownFd: r.shutdownFd,
vnetHdr: r.vnetHdr,
readBuf: make([]byte, tunReadBufSize),
readPoll: [2]unix.PollFd{
{Fd: int32(fd), Events: unix.POLLIN},
{Fd: int32(r.shutdownFd), Events: unix.POLLIN},
},
writePoll: [2]unix.PollFd{
{Fd: int32(fd), Events: unix.POLLOUT},
{Fd: int32(r.shutdownFd), Events: unix.POLLIN},
},
}
if r.vnetHdr {
out.segBuf = make([]byte, tunSegBufCap)
out.writeIovs[0].Base = &validVnetHdr[0]
out.writeIovs[0].SetLen(virtioNetHdrLen)
out.rejectIovs[0].Base = &validVnetHdr[0]
out.rejectIovs[0].SetLen(virtioNetHdrLen)
out.gsoIovs = make([]unix.Iovec, 2, 2+gsoInitialPayIovs)
out.gsoIovs[0].Base = &out.gsoHdrBuf[0]
out.gsoIovs[0].SetLen(virtioNetHdrLen)
}
return out, nil
}
func newTunFd(fd int, vnetHdr bool) (*tunFile, error) {
if err := unix.SetNonblock(fd, true); err != nil {
return nil, fmt.Errorf("failed to set tun fd non-blocking: %w", err)
}
shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
if err != nil {
return nil, fmt.Errorf("failed to create eventfd: %w", err)
}
out := &tunFile{
fd: fd,
shutdownFd: shutdownFd,
lastOne: true,
vnetHdr: vnetHdr,
readBuf: make([]byte, tunReadBufSize),
readPoll: [2]unix.PollFd{
{Fd: int32(fd), Events: unix.POLLIN},
{Fd: int32(shutdownFd), Events: unix.POLLIN},
},
writePoll: [2]unix.PollFd{
{Fd: int32(fd), Events: unix.POLLOUT},
{Fd: int32(shutdownFd), Events: unix.POLLIN},
},
}
if vnetHdr {
out.segBuf = make([]byte, tunSegBufCap)
out.writeIovs[0].Base = &validVnetHdr[0]
out.writeIovs[0].SetLen(virtioNetHdrLen)
out.rejectIovs[0].Base = &validVnetHdr[0]
out.rejectIovs[0].SetLen(virtioNetHdrLen)
out.gsoIovs = make([]unix.Iovec, 2, 2+gsoInitialPayIovs)
out.gsoIovs[0].Base = &out.gsoHdrBuf[0]
out.gsoIovs[0].SetLen(virtioNetHdrLen)
}
return out, nil
}
func (r *tunFile) blockOnRead() error {
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
var err error
for {
_, err = unix.Poll(r.readPoll[:], -1)
if err != unix.EINTR {
break
}
}
//always reset these!
tunEvents := r.readPoll[0].Revents
shutdownEvents := r.readPoll[1].Revents
r.readPoll[0].Revents = 0
r.readPoll[1].Revents = 0
//do the err check before trusting the potentially bogus bits we just got
if err != nil {
return err
}
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
return os.ErrClosed
} else if tunEvents&problemFlags != 0 {
return os.ErrClosed
}
return nil
}
func (r *tunFile) blockOnWrite() error {
const problemFlags = unix.POLLHUP | unix.POLLNVAL | unix.POLLERR
var err error
for {
_, err = unix.Poll(r.writePoll[:], -1)
if err != unix.EINTR {
break
}
}
//always reset these!
tunEvents := r.writePoll[0].Revents
shutdownEvents := r.writePoll[1].Revents
r.writePoll[0].Revents = 0
r.writePoll[1].Revents = 0
//do the err check before trusting the potentially bogus bits we just got
if err != nil {
return err
}
if shutdownEvents&(unix.POLLIN|problemFlags) != 0 {
return os.ErrClosed
} else if tunEvents&problemFlags != 0 {
return os.ErrClosed
}
return nil
}
func (r *tunFile) readRaw(buf []byte) (int, error) {
for {
if n, err := unix.Read(r.fd, buf); err == nil {
return n, nil
} else if err == unix.EAGAIN {
if err = r.blockOnRead(); err != nil {
return 0, err
}
continue
} else if err == unix.EINTR {
continue
} else if err == unix.EBADF {
return 0, os.ErrClosed
} else {
return 0, err
}
}
}
// Read reads one or more superpackets from the tun and returns the
// resulting packets. The first read blocks via poll; once the fd is known
// readable we drain additional packets non-blocking until the kernel queue
// is empty (EAGAIN), we've collected tunDrainCap packets, or we're out of
// segBuf headroom. This amortizes the poll wake over bursts of small
// packets (e.g. TCP ACKs). Slices point into the tunFile's internal buffers
// and are only valid until the next Read or Close on this Queue.
func (r *tunFile) Read() ([][]byte, error) {
r.pending = r.pending[:0]
r.segOff = 0
// Initial (blocking) read. Retry on decode errors so a single bad
// packet does not stall the reader.
for {
n, err := r.readRaw(r.readBuf)
if err != nil {
return nil, err
}
if !r.vnetHdr {
r.pending = append(r.pending, r.readBuf[:n])
// Non-vnetHdr mode shares one readBuf so we can't drain safely
// without copying; return the single packet as before.
return r.pending, nil
}
if err := r.decodeRead(n); err != nil {
// Drop and read again — a bad packet should not kill the reader.
continue
}
break
}
// Drain: non-blocking reads until the kernel queue is empty, the drain
// cap is reached, or segBuf no longer has room for another worst-case
// superpacket.
for len(r.pending) < tunDrainCap && tunSegBufCap-r.segOff >= tunSegBufSize {
n, err := unix.Read(r.fd, r.readBuf)
if err != nil {
// EAGAIN / EINTR / anything else: stop draining. We already
// have a valid batch from the first read.
break
}
if n <= 0 {
break
}
if err := r.decodeRead(n); err != nil {
// Drop this packet and stop the drain; we'd rather hand off
// what we have than keep spinning here.
break
}
}
return r.pending, nil
}
// decodeRead decodes the virtio header plus payload in r.readBuf[:n], appends
// the segments to r.pending, and advances r.segOff by the total scratch used.
// Caller must have already ensured r.vnetHdr is true.
func (r *tunFile) decodeRead(n int) error {
if n < virtioNetHdrLen {
return fmt.Errorf("short tun read: %d < %d", n, virtioNetHdrLen)
}
var hdr virtioNetHdr
hdr.decode(r.readBuf[:virtioNetHdrLen])
before := len(r.pending)
if err := segmentInto(r.readBuf[virtioNetHdrLen:n], hdr, &r.pending, r.segBuf[r.segOff:]); err != nil {
return err
}
for k := before; k < len(r.pending); k++ {
r.segOff += len(r.pending[k])
}
return nil
}
func (r *tunFile) Write(buf []byte) (int, error) {
return r.writeWithScratch(buf, &r.writeIovs)
}
// WriteReject emits a packet using a dedicated iovec scratch (rejectIovs)
// distinct from the one used by the coalescer's Write path. This avoids a
// data race between the inside (listenIn) goroutine emitting reject or
// self-forward packets and the outside (listenOut) goroutine flushing TCP
// coalescer passthroughs on the same tunFile.
func (r *tunFile) WriteReject(buf []byte) (int, error) {
return r.writeWithScratch(buf, &r.rejectIovs)
}
func (r *tunFile) writeWithScratch(buf []byte, iovs *[2]unix.Iovec) (int, error) {
if !r.vnetHdr {
for {
if n, err := unix.Write(r.fd, buf); err == nil {
return n, nil
} else if err == unix.EAGAIN {
if err = r.blockOnWrite(); err != nil {
return 0, err
}
continue
} else if err == unix.EINTR {
continue
} else if err == unix.EBADF {
return 0, os.ErrClosed
} else {
return 0, err
}
}
}
if len(buf) == 0 {
return 0, nil
}
// Point the payload iovec at the caller's buffer. iovs[0] is pre-wired
// to validVnetHdr during tunFile construction so we don't rebuild it here.
iovs[1].Base = &buf[0]
iovs[1].SetLen(len(buf))
iovPtr := uintptr(unsafe.Pointer(&iovs[0]))
// The TUN fd is non-blocking (set in newTunFd / newFriend), so writev
// either completes promptly or returns EAGAIN — it cannot park the
// goroutine inside the kernel. That lets us use syscall.RawSyscall and
// skip the runtime.entersyscall / exitsyscall bookkeeping on every
// packet; we only pay that cost when we fall through to blockOnWrite.
for {
n, _, errno := syscall.RawSyscall(unix.SYS_WRITEV, uintptr(r.fd), iovPtr, 2)
if errno == 0 {
runtime.KeepAlive(buf)
if int(n) < virtioNetHdrLen {
return 0, io.ErrShortWrite
}
return int(n) - virtioNetHdrLen, nil
}
if errno == unix.EAGAIN {
runtime.KeepAlive(buf)
if err := r.blockOnWrite(); err != nil {
return 0, err
}
continue
}
if errno == unix.EINTR {
continue
}
runtime.KeepAlive(buf)
return 0, errno
}
}
// GSOSupported reports whether this queue was opened with IFF_VNET_HDR and
// can accept WriteGSO. When false, callers should fall back to per-segment
// Write calls.
func (r *tunFile) GSOSupported() bool { return r.vnetHdr }
// WriteGSO emits a TCP TSO superpacket in a single writev. hdr is the
// IPv4/IPv6 + TCP header prefix (already finalized — total length, IP csum,
// and TCP pseudo-header partial set by the caller). pays are payload
// fragments whose concatenation forms the full coalesced payload; each
// slice is read-only and must stay valid until return. gsoSize is the MSS;
// every segment except possibly the last is exactly gsoSize bytes.
// csumStart is the byte offset where the TCP header begins within hdr.
func (r *tunFile) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error {
if !r.vnetHdr {
return fmt.Errorf("WriteGSO called on tun without IFF_VNET_HDR")
}
if len(hdr) == 0 || len(pays) == 0 {
return nil
}
// Build the virtio_net_hdr. When pays total to <= gsoSize the kernel
// would produce a single segment; keep NEEDS_CSUM semantics but skip
// the GSO type so the kernel doesn't spuriously mark this as TSO.
vhdr := virtioNetHdr{
Flags: unix.VIRTIO_NET_HDR_F_NEEDS_CSUM,
HdrLen: uint16(len(hdr)),
GSOSize: gsoSize,
CsumStart: csumStart,
CsumOffset: 16, // TCP checksum field lives 16 bytes into the TCP header
}
var totalPay int
for _, p := range pays {
totalPay += len(p)
}
if totalPay > int(gsoSize) {
if isV6 {
vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_TCPV6
} else {
vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_TCPV4
}
} else {
vhdr.GSOType = unix.VIRTIO_NET_HDR_GSO_NONE
vhdr.GSOSize = 0
}
vhdr.encode(r.gsoHdrBuf[:])
// Build the iovec array: [virtio_hdr, hdr, pays...]. r.gsoIovs[0] is
// wired to gsoHdrBuf at construction and never changes.
need := 2 + len(pays)
if cap(r.gsoIovs) < need {
grown := make([]unix.Iovec, need)
grown[0] = r.gsoIovs[0]
r.gsoIovs = grown
} else {
r.gsoIovs = r.gsoIovs[:need]
}
r.gsoIovs[1].Base = &hdr[0]
r.gsoIovs[1].SetLen(len(hdr))
for i, p := range pays {
r.gsoIovs[2+i].Base = &p[0]
r.gsoIovs[2+i].SetLen(len(p))
}
iovPtr := uintptr(unsafe.Pointer(&r.gsoIovs[0]))
iovCnt := uintptr(len(r.gsoIovs))
for {
n, _, errno := syscall.RawSyscall(unix.SYS_WRITEV, uintptr(r.fd), iovPtr, iovCnt)
if errno == 0 {
runtime.KeepAlive(hdr)
runtime.KeepAlive(pays)
if int(n) < virtioNetHdrLen {
return io.ErrShortWrite
}
return nil
}
if errno == unix.EAGAIN {
runtime.KeepAlive(hdr)
runtime.KeepAlive(pays)
if err := r.blockOnWrite(); err != nil {
return err
}
continue
}
if errno == unix.EINTR {
continue
}
runtime.KeepAlive(hdr)
runtime.KeepAlive(pays)
return errno
}
}
func (r *tunFile) wakeForShutdown() error {
var buf [8]byte
binary.NativeEndian.PutUint64(buf[:], 1)
_, err := unix.Write(int(r.readPoll[1].Fd), buf[:])
return err
}
func (r *tunFile) Close() error {
if r.closed { // avoid closing more than once. Technically a fd could get re-used, which would be a problem
return nil
}
r.closed = true
if r.lastOne {
_ = unix.Close(r.shutdownFd)
}
return unix.Close(r.fd)
}
type tun struct { type tun struct {
*tunFile readers tio.Container
readers []*tunFile
closeLock sync.Mutex closeLock sync.Mutex
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
@@ -484,6 +34,7 @@ type tun struct {
TXQueueLen int TXQueueLen int
deviceIndex int deviceIndex int
ioctlFd uintptr ioctlFd uintptr
vnetHdr bool
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
@@ -496,7 +47,7 @@ type tun struct {
routesFromSystem map[netip.Prefix]routing.Gateways routesFromSystem map[netip.Prefix]routing.Gateways
routesFromSystemLock sync.Mutex routesFromSystemLock sync.Mutex
l *logrus.Logger l *slog.Logger
} }
func (t *tun) Networks() []netip.Prefix { func (t *tun) Networks() []netip.Prefix {
@@ -521,7 +72,7 @@ type ifreqQLEN struct {
pad [8]byte pad [8]byte
} }
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) { func newTunFromFd(c *config.C, l *slog.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
// We don't know what flags the caller opened this fd with and can't turn // We don't know what flags the caller opened this fd with and can't turn
// on IFF_VNET_HDR after TUNSETIFF, so skip offload on inherited fds. // on IFF_VNET_HDR after TUNSETIFF, so skip offload on inherited fds.
t, err := newTunGeneric(c, l, deviceFd, false, vpnNetworks) t, err := newTunGeneric(c, l, deviceFd, false, vpnNetworks)
@@ -573,7 +124,7 @@ func tunSetIff(fd int, name string, flags uint16) (string, error) {
// TSO-capable TUN is available. CSUM is required as a prerequisite for TSO. // TSO-capable TUN is available. CSUM is required as a prerequisite for TSO.
const tsoOffloadFlags = unix.TUN_F_CSUM | unix.TUN_F_TSO4 | unix.TUN_F_TSO6 const tsoOffloadFlags = unix.TUN_F_CSUM | unix.TUN_F_TSO4 | unix.TUN_F_TSO6
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) { func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) {
baseFlags := uint16(unix.IFF_TUN | unix.IFF_NO_PI) baseFlags := uint16(unix.IFF_TUN | unix.IFF_NO_PI)
if multiqueue { if multiqueue {
baseFlags |= unix.IFF_MULTI_QUEUE baseFlags |= unix.IFF_MULTI_QUEUE
@@ -588,12 +139,12 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueu
return nil, err return nil, err
} }
vnetHdr := true vnetHdr := true
name, err := tunSetIff(fd, nameStr, baseFlags|unix.IFF_VNET_HDR|unix.IFF_NAPI) name, err := tunSetIff(fd, nameStr, baseFlags|unix.IFF_VNET_HDR)
if err != nil { if err != nil {
_ = unix.Close(fd) _ = unix.Close(fd)
vnetHdr = false vnetHdr = false
} else if err = ioctl(uintptr(fd), unix.TUNSETOFFLOAD, uintptr(tsoOffloadFlags)); err != nil { } else if err = ioctl(uintptr(fd), unix.TUNSETOFFLOAD, uintptr(tsoOffloadFlags)); err != nil {
l.WithError(err).Warn("Failed to enable TUN offload (TSO); proceeding without virtio headers") l.Warn("Failed to enable TUN offload (TSO); proceeding without virtio headers", "error", err)
_ = unix.Close(fd) _ = unix.Close(fd)
vnetHdr = false vnetHdr = false
} }
@@ -621,16 +172,29 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueu
} }
// newTunGeneric does all the stuff common to different tun initialization paths. It will close your files on error. // newTunGeneric does all the stuff common to different tun initialization paths. It will close your files on error.
func newTunGeneric(c *config.C, l *logrus.Logger, fd int, vnetHdr bool, vpnNetworks []netip.Prefix) (*tun, error) { func newTunGeneric(c *config.C, l *slog.Logger, fd int, vnetHdr bool, vpnNetworks []netip.Prefix) (*tun, error) {
tfd, err := newTunFd(fd, vnetHdr) var container tio.Container
var err error
if vnetHdr {
container, err = tio.NewOffloadContainer()
} else {
container, err = tio.NewPollContainer()
}
if err != nil { if err != nil {
_ = unix.Close(fd) _ = unix.Close(fd)
return nil, err return nil, err
} }
err = container.Add(fd)
if err != nil {
_ = unix.Close(fd)
return nil, err
}
t := &tun{ t := &tun{
tunFile: tfd, readers: container,
readers: []*tunFile{tfd},
closeLock: sync.Mutex{}, closeLock: sync.Mutex{},
vnetHdr: vnetHdr,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
TXQueueLen: c.GetInt("tun.tx_queue", 500), TXQueueLen: c.GetInt("tun.tx_queue", 500),
useSystemRoutes: c.GetBool("tun.use_system_route_table", false), useSystemRoutes: c.GetBool("tun.use_system_route_table", false),
@@ -700,16 +264,16 @@ func (t *tun) reload(c *config.C, initial bool) error {
if !initial { if !initial {
if oldMaxMTU != newMaxMTU { if oldMaxMTU != newMaxMTU {
t.setMTU() t.setMTU()
t.l.Infof("Set max MTU to %v was %v", t.MaxMTU, oldMaxMTU) t.l.Info("Set max MTU", "mtu", t.MaxMTU, "oldMTU", oldMaxMTU)
} }
if oldDefaultMTU != newDefaultMTU { if oldDefaultMTU != newDefaultMTU {
for i := range t.vpnNetworks { for i := range t.vpnNetworks {
err := t.setDefaultRoute(t.vpnNetworks[i]) err := t.setDefaultRoute(t.vpnNetworks[i])
if err != nil { if err != nil {
t.l.Warn(err) t.l.Warn(err.Error())
} else { } else {
t.l.Infof("Set default MTU to %v was %v", t.DefaultMTU, oldDefaultMTU) t.l.Info("Set default MTU", "mtu", t.DefaultMTU, "oldMTU", oldDefaultMTU)
} }
} }
} }
@@ -732,40 +296,38 @@ func (t *tun) SupportsMultiqueue() bool {
return true return true
} }
func (t *tun) NewMultiQueueReader() (Queue, error) { func (t *tun) NewMultiQueueReader() error {
t.closeLock.Lock() t.closeLock.Lock()
defer t.closeLock.Unlock() defer t.closeLock.Unlock()
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0) fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
if err != nil { if err != nil {
return nil, err return err
} }
flags := uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE) flags := uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE)
if t.vnetHdr { if t.vnetHdr {
flags |= unix.IFF_VNET_HDR | unix.IFF_NAPI flags |= unix.IFF_VNET_HDR
} }
if _, err = tunSetIff(fd, t.Device, flags); err != nil { if _, err = tunSetIff(fd, t.Device, flags); err != nil {
_ = unix.Close(fd) _ = unix.Close(fd)
return nil, err return err
} }
if t.vnetHdr { if t.vnetHdr {
if err = ioctl(uintptr(fd), unix.TUNSETOFFLOAD, uintptr(tsoOffloadFlags)); err != nil { if err = ioctl(uintptr(fd), unix.TUNSETOFFLOAD, uintptr(tsoOffloadFlags)); err != nil {
_ = unix.Close(fd) _ = unix.Close(fd)
return nil, fmt.Errorf("failed to enable offload on multiqueue tun fd: %w", err) return fmt.Errorf("failed to enable offload on multiqueue tun fd: %w", err)
} }
} }
out, err := t.tunFile.newFriend(fd) err = t.readers.Add(fd)
if err != nil { if err != nil {
_ = unix.Close(fd) _ = unix.Close(fd)
return nil, err return err
} }
t.readers = append(t.readers, out) return nil
return out, nil
} }
func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways { func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
@@ -822,9 +384,9 @@ func (t *tun) addIPs(link netlink.Link) error {
} }
err = netlink.AddrDel(link, &al[i]) err = netlink.AddrDel(link, &al[i])
if err != nil { if err != nil {
t.l.WithError(err).Error("failed to remove address from tun address list") t.l.Error("failed to remove address from tun address list", "error", err)
} else { } else {
t.l.WithField("removed", al[i].String()).Info("removed address not listed in cert(s)") t.l.Info("removed address not listed in cert(s)", "removed", al[i].String())
} }
} }
@@ -868,12 +430,12 @@ func (t *tun) Activate() error {
ifrq := ifreqQLEN{Name: devName, Value: int32(t.TXQueueLen)} ifrq := ifreqQLEN{Name: devName, Value: int32(t.TXQueueLen)}
if err = ioctl(t.ioctlFd, unix.SIOCSIFTXQLEN, uintptr(unsafe.Pointer(&ifrq))); err != nil { if err = ioctl(t.ioctlFd, unix.SIOCSIFTXQLEN, uintptr(unsafe.Pointer(&ifrq))); err != nil {
// If we can't set the queue length nebula will still work but it may lead to packet loss // If we can't set the queue length nebula will still work but it may lead to packet loss
t.l.WithError(err).Error("Failed to set tun tx queue length") t.l.Error("Failed to set tun tx queue length", "error", err)
} }
const modeNone = 1 const modeNone = 1
if err = netlink.LinkSetIP6AddrGenMode(link, modeNone); err != nil { if err = netlink.LinkSetIP6AddrGenMode(link, modeNone); err != nil {
t.l.WithError(err).Warn("Failed to disable link local address generation") t.l.Warn("Failed to disable link local address generation", "error", err)
} }
if err = t.addIPs(link); err != nil { if err = t.addIPs(link); err != nil {
@@ -912,7 +474,7 @@ func (t *tun) setMTU() {
ifm := ifreqMTU{Name: t.deviceBytes(), MTU: int32(t.MaxMTU)} ifm := ifreqMTU{Name: t.deviceBytes(), MTU: int32(t.MaxMTU)}
if err := ioctl(t.ioctlFd, unix.SIOCSIFMTU, uintptr(unsafe.Pointer(&ifm))); err != nil { if err := ioctl(t.ioctlFd, unix.SIOCSIFMTU, uintptr(unsafe.Pointer(&ifm))); err != nil {
// This is currently a non fatal condition because the route table must have the MTU set appropriately as well // This is currently a non fatal condition because the route table must have the MTU set appropriately as well
t.l.WithError(err).Error("Failed to set tun mtu") t.l.Error("Failed to set tun mtu", "error", err)
} }
} }
@@ -935,7 +497,7 @@ func (t *tun) setDefaultRoute(cidr netip.Prefix) error {
} }
err := netlink.RouteReplace(&nr) err := netlink.RouteReplace(&nr)
if err != nil { if err != nil {
t.l.WithError(err).WithField("cidr", cidr).Warn("Failed to set default route MTU, retrying") t.l.Warn("Failed to set default route MTU, retrying", "error", err, "cidr", cidr)
//retry twice more -- on some systems there appears to be a race condition where if we set routes too soon, netlink says `invalid argument` //retry twice more -- on some systems there appears to be a race condition where if we set routes too soon, netlink says `invalid argument`
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@@ -943,7 +505,11 @@ func (t *tun) setDefaultRoute(cidr netip.Prefix) error {
if err == nil { if err == nil {
break break
} else { } else {
t.l.WithError(err).WithField("cidr", cidr).WithField("mtu", t.DefaultMTU).Warn("Failed to set default route MTU, retrying") t.l.Warn("Failed to set default route MTU, retrying",
"error", err,
"cidr", cidr,
"mtu", t.DefaultMTU,
)
} }
} }
if err != nil { if err != nil {
@@ -988,7 +554,7 @@ func (t *tun) addRoutes(logErrors bool) error {
return retErr return retErr
} }
} else { } else {
t.l.WithField("route", r).Info("Added route") t.l.Info("Added route", "route", r)
} }
} }
@@ -1020,9 +586,9 @@ func (t *tun) removeRoutes(routes []Route) {
err := netlink.RouteDel(&nr) err := netlink.RouteDel(&nr)
if err != nil { if err != nil {
t.l.WithError(err).WithField("route", r).Error("Failed to remove route") t.l.Error("Failed to remove route", "error", err, "route", r)
} else { } else {
t.l.WithField("route", r).Info("Removed route") t.l.Info("Removed route", "route", r)
} }
} }
} }
@@ -1051,11 +617,11 @@ func (t *tun) watchRoutes() {
netlinkOptions := netlink.RouteSubscribeOptions{ netlinkOptions := netlink.RouteSubscribeOptions{
ReceiveBufferSize: t.useSystemRoutesBufferSize, ReceiveBufferSize: t.useSystemRoutesBufferSize,
ReceiveBufferForceSize: t.useSystemRoutesBufferSize != 0, ReceiveBufferForceSize: t.useSystemRoutesBufferSize != 0,
ErrorCallback: func(e error) { t.l.WithError(e).Errorf("netlink error") }, ErrorCallback: func(e error) { t.l.Error("netlink error", "error", e) },
} }
if err := netlink.RouteSubscribeWithOptions(rch, doneChan, netlinkOptions); err != nil { if err := netlink.RouteSubscribeWithOptions(rch, doneChan, netlinkOptions); err != nil {
t.l.WithError(err).Errorf("failed to subscribe to system route changes") t.l.Error("failed to subscribe to system route changes", "error", err)
return return
} }
@@ -1097,7 +663,7 @@ func (t *tun) getGatewaysFromRoute(r *netlink.Route) routing.Gateways {
link, err := netlink.LinkByName(t.Device) link, err := netlink.LinkByName(t.Device)
if err != nil { if err != nil {
t.l.WithField("deviceName", t.Device).Error("Ignoring route update: failed to get link by name") t.l.Error("Ignoring route update: failed to get link by name", "deviceName", t.Device)
return gateways return gateways
} }
@@ -1109,10 +675,10 @@ func (t *tun) getGatewaysFromRoute(r *netlink.Route) routing.Gateways {
gateways = append(gateways, routing.NewGateway(gwAddr, 1)) gateways = append(gateways, routing.NewGateway(gwAddr, 1))
} else { } else {
// Gateway isn't in our overlay network, ignore // Gateway isn't in our overlay network, ignore
t.l.WithField("route", r).Debug("Ignoring route update, gateway is not in our network") t.l.Debug("Ignoring route update, gateway is not in our network", "route", r)
} }
} else { } else {
t.l.WithField("route", r).Debug("Ignoring route update, invalid gateway or via address") t.l.Debug("Ignoring route update, invalid gateway or via address", "route", r)
} }
} }
@@ -1125,10 +691,10 @@ func (t *tun) getGatewaysFromRoute(r *netlink.Route) routing.Gateways {
gateways = append(gateways, routing.NewGateway(gwAddr, p.Hops+1)) gateways = append(gateways, routing.NewGateway(gwAddr, p.Hops+1))
} else { } else {
// Gateway isn't in our overlay network, ignore // Gateway isn't in our overlay network, ignore
t.l.WithField("route", r).Debug("Ignoring route update, gateway is not in our network") t.l.Debug("Ignoring route update, gateway is not in our network", "route", r)
} }
} else { } else {
t.l.WithField("route", r).Debug("Ignoring route update, invalid gateway or via address") t.l.Debug("Ignoring route update, invalid gateway or via address", "route", r)
} }
} }
} }
@@ -1160,18 +726,18 @@ func (t *tun) updateRoutes(r netlink.RouteUpdate) {
gateways := t.getGatewaysFromRoute(&r.Route) gateways := t.getGatewaysFromRoute(&r.Route)
if len(gateways) == 0 { if len(gateways) == 0 {
// No gateways relevant to our network, no routing changes required. // No gateways relevant to our network, no routing changes required.
t.l.WithField("route", r).Debug("Ignoring route update, no gateways") t.l.Debug("Ignoring route update, no gateways", "route", r)
return return
} }
if r.Dst == nil { if r.Dst == nil {
t.l.WithField("route", r).Debug("Ignoring route update, no destination address") t.l.Debug("Ignoring route update, no destination address", "route", r)
return return
} }
dstAddr, ok := netip.AddrFromSlice(r.Dst.IP) dstAddr, ok := netip.AddrFromSlice(r.Dst.IP)
if !ok { if !ok {
t.l.WithField("route", r).Debug("Ignoring route update, invalid destination address") t.l.Debug("Ignoring route update, invalid destination address", "route", r)
return return
} }
@@ -1182,12 +748,12 @@ func (t *tun) updateRoutes(r netlink.RouteUpdate) {
t.routesFromSystemLock.Lock() t.routesFromSystemLock.Lock()
if r.Type == unix.RTM_NEWROUTE { if r.Type == unix.RTM_NEWROUTE {
t.l.WithField("destination", dst).WithField("via", gateways).Info("Adding route") t.l.Info("Adding route", "destination", dst, "via", gateways)
t.routesFromSystem[dst] = gateways t.routesFromSystem[dst] = gateways
newTree.Insert(dst, gateways) newTree.Insert(dst, gateways)
} else { } else {
t.l.WithField("destination", dst).WithField("via", gateways).Info("Removing route") t.l.Info("Removing route", "destination", dst, "via", gateways)
delete(t.routesFromSystem, dst) delete(t.routesFromSystem, dst)
newTree.Delete(dst) newTree.Delete(dst)
} }
@@ -1195,6 +761,10 @@ func (t *tun) updateRoutes(r netlink.RouteUpdate) {
t.routeTree.Store(newTree) t.routeTree.Store(newTree)
} }
func (t *tun) Readers() []tio.Queue {
return t.readers.Queues()
}
func (t *tun) Close() error { func (t *tun) Close() error {
t.closeLock.Lock() t.closeLock.Lock()
defer t.closeLock.Unlock() defer t.closeLock.Unlock()
@@ -1204,32 +774,10 @@ func (t *tun) Close() error {
t.routeChan = nil t.routeChan = nil
} }
// Signal all readers blocked in poll to wake up and exit
_ = t.tunFile.wakeForShutdown()
if t.ioctlFd > 0 { if t.ioctlFd > 0 {
_ = unix.Close(int(t.ioctlFd)) _ = unix.Close(int(t.ioctlFd))
t.ioctlFd = 0 t.ioctlFd = 0
} }
for i := range t.readers { return t.readers.Close()
if i == 0 {
continue //we want to close the zeroth reader last
}
err := t.readers[i].Close()
if err != nil {
t.l.WithField("reader", i).WithError(err).Error("error closing tun reader")
} else {
t.l.WithField("reader", i).Info("closed tun reader")
}
}
//this is t.readers[0] too
err := t.tunFile.Close()
if err != nil {
t.l.WithField("reader", 0).WithError(err).Error("error closing tun reader")
} else {
t.l.WithField("reader", 0).Info("closed tun reader")
}
return err
} }
-331
View File
@@ -1,331 +0,0 @@
//go:build linux && !android && !e2e_testing
// +build linux,!android,!e2e_testing
package overlay
import (
"encoding/binary"
"fmt"
"golang.org/x/sys/unix"
)
// Size of the legacy struct virtio_net_hdr that the kernel prepends/expects on
// a TUN opened with IFF_VNET_HDR (TUNSETVNETHDRSZ not set).
const virtioNetHdrLen = 10
// Maximum size we accept for a single read from a TUN with IFF_VNET_HDR. A
// TSO superpacket can be up to 64KiB of payload plus a single L2/L3/L4 header
// prefix plus the virtio header.
const tunReadBufSize = 65535
// Space for segmented output. Worst case is many small segments, each paying
// an IP+TCP header. 128KiB comfortably covers the 64KiB payload ceiling.
const tunSegBufSize = 131072
// tunSegBufCap is the total size we allocate for the per-reader segment
// buffer. It is sized as one worst-case TSO superpacket (tunSegBufSize) plus
// the same again as drain headroom so a Read wake can accumulate
// additional packets after an initial big read without overflowing.
const tunSegBufCap = tunSegBufSize * 2
// tunDrainCap caps how many packets a single Read will accumulate via
// the post-wake drain loop. Sized to soak up a burst of small ACKs while
// bounding how much work a single caller holds before handing off.
const tunDrainCap = 64
type virtioNetHdr struct {
Flags uint8
GSOType uint8
HdrLen uint16
GSOSize uint16
CsumStart uint16
CsumOffset uint16
}
// decode reads a virtio_net_hdr in host byte order (TUN default; we never
// call TUNSETVNETLE so the kernel matches our endianness).
func (h *virtioNetHdr) decode(b []byte) {
h.Flags = b[0]
h.GSOType = b[1]
h.HdrLen = binary.NativeEndian.Uint16(b[2:4])
h.GSOSize = binary.NativeEndian.Uint16(b[4:6])
h.CsumStart = binary.NativeEndian.Uint16(b[6:8])
h.CsumOffset = binary.NativeEndian.Uint16(b[8:10])
}
// encode is the inverse of decode: writes the virtio_net_hdr fields into b
// (must be at least virtioNetHdrLen bytes). Used to emit a TSO superpacket
// on egress.
func (h *virtioNetHdr) encode(b []byte) {
b[0] = h.Flags
b[1] = h.GSOType
binary.NativeEndian.PutUint16(b[2:4], h.HdrLen)
binary.NativeEndian.PutUint16(b[4:6], h.GSOSize)
binary.NativeEndian.PutUint16(b[6:8], h.CsumStart)
binary.NativeEndian.PutUint16(b[8:10], h.CsumOffset)
}
// segmentInto splits a TUN-side packet described by hdr into one or more
// IP packets, each appended to *out as a slice of scratch. scratch must be
// sized to hold every segment (including replicated headers).
func segmentInto(pkt []byte, hdr virtioNetHdr, out *[][]byte, scratch []byte) error {
// When RSC_INFO is set the csum_start/csum_offset fields are repurposed to
// carry coalescing info rather than checksum offsets. A TUN writing via
// IFF_VNET_HDR should never emit this, but if it did we would silently
// miscompute the segment checksums — refuse the packet instead.
if hdr.Flags&unix.VIRTIO_NET_HDR_F_RSC_INFO != 0 {
return fmt.Errorf("virtio RSC_INFO flag not supported on TUN reads")
}
switch hdr.GSOType {
case unix.VIRTIO_NET_HDR_GSO_NONE:
if len(pkt) > len(scratch) {
return fmt.Errorf("packet larger than segment buffer: %d > %d", len(pkt), len(scratch))
}
copy(scratch, pkt)
seg := scratch[:len(pkt)]
if hdr.Flags&unix.VIRTIO_NET_HDR_F_NEEDS_CSUM != 0 {
if err := finishChecksum(seg, hdr); err != nil {
return err
}
}
*out = append(*out, seg)
return nil
case unix.VIRTIO_NET_HDR_GSO_TCPV4, unix.VIRTIO_NET_HDR_GSO_TCPV6:
return segmentTCP(pkt, hdr, out, scratch)
default:
return fmt.Errorf("unsupported virtio gso type: %d", hdr.GSOType)
}
}
// finishChecksum computes the L4 checksum for a non-GSO packet that the kernel
// handed us with NEEDS_CSUM set. csum_start / csum_offset point at the 16-bit
// checksum field; we zero it, fold a full sum (the field was pre-loaded with
// the pseudo-header partial sum by the kernel), and store the result.
func finishChecksum(seg []byte, hdr virtioNetHdr) error {
cs := int(hdr.CsumStart)
co := int(hdr.CsumOffset)
if cs+co+2 > len(seg) {
return fmt.Errorf("csum offsets out of range: start=%d offset=%d len=%d", cs, co, len(seg))
}
// The kernel stores a partial pseudo-header sum at [cs+co:]; sum over the
// L4 region starting at cs, folding the prior partial in as the seed.
partial := uint32(binary.BigEndian.Uint16(seg[cs+co : cs+co+2]))
seg[cs+co] = 0
seg[cs+co+1] = 0
sum := checksumBytes(seg[cs:], partial)
binary.BigEndian.PutUint16(seg[cs+co:cs+co+2], checksumFold(sum))
return nil
}
// segmentTCP software-segments a TSO superpacket into one IP packet per MSS
// chunk. The caller guarantees hdr.GSOType is TCPV4 or TCPV6.
//
// Hot-path shape: the per-segment loop only sums the payload chunk. The TCP
// header, the IPv4 header, and the pseudo-header src/dst/proto contributions
// are each summed once up front — every segment reuses those three pre-folded
// uint32 values and combines them with small per-segment deltas (seq, flags,
// tcpLen, ip_id, total_len) that are cheap to fold in.
func segmentTCP(pkt []byte, hdr virtioNetHdr, out *[][]byte, scratch []byte) error {
if hdr.GSOSize == 0 {
return fmt.Errorf("gso_size is zero")
}
if int(hdr.HdrLen) > len(pkt) || hdr.HdrLen == 0 {
return fmt.Errorf("hdr_len %d out of range (pkt %d)", hdr.HdrLen, len(pkt))
}
if hdr.CsumStart == 0 || hdr.CsumStart >= hdr.HdrLen {
return fmt.Errorf("csum_start %d out of range (hdr_len %d)", hdr.CsumStart, hdr.HdrLen)
}
isV4 := hdr.GSOType == unix.VIRTIO_NET_HDR_GSO_TCPV4
headerLen := int(hdr.HdrLen)
csumStart := int(hdr.CsumStart)
if isV4 && csumStart < 20 {
return fmt.Errorf("csum_start %d too small for IPv4", csumStart)
}
if !isV4 && csumStart < 40 {
return fmt.Errorf("csum_start %d too small for IPv6", csumStart)
}
tcpHdrLen := headerLen - csumStart
if tcpHdrLen < 20 {
return fmt.Errorf("tcp header region too small: %d", tcpHdrLen)
}
payload := pkt[headerLen:]
payLen := len(payload)
gso := int(hdr.GSOSize)
numSeg := (payLen + gso - 1) / gso
if numSeg == 0 {
numSeg = 1
}
need := numSeg*headerLen + payLen
if need > len(scratch) {
return fmt.Errorf("scratch too small for %d segments: need %d have %d", numSeg, need, len(scratch))
}
origSeq := binary.BigEndian.Uint32(pkt[csumStart+4 : csumStart+8])
origFlags := pkt[csumStart+13]
const tcpFinPsh = 0x09 // FIN(0x01) | PSH(0x08)
// Precompute the TCP header sum with seq/flags/csum zeroed. The max TCP
// header is 60 bytes; copy onto the stack, zero the per-segment-varying
// fields, sum once.
var tmp [60]byte
copy(tmp[:tcpHdrLen], pkt[csumStart:headerLen])
tmp[4], tmp[5], tmp[6], tmp[7] = 0, 0, 0, 0 // seq
tmp[13] = 0 // flags
tmp[16], tmp[17] = 0, 0 // csum
baseTcpHdrSum := checksumBytes(tmp[:tcpHdrLen], 0)
// Pseudo-header src+dst+proto contribution (tcpLen varies per segment).
var baseProtoSum uint32
if isV4 {
baseProtoSum = checksumBytes(pkt[12:16], 0)
baseProtoSum = checksumBytes(pkt[16:20], baseProtoSum)
} else {
baseProtoSum = checksumBytes(pkt[8:24], 0)
baseProtoSum = checksumBytes(pkt[24:40], baseProtoSum)
}
baseProtoSum += uint32(unix.IPPROTO_TCP)
// Precompute IPv4 header sum with total_len/id/csum zeroed.
var origIPID uint16
var ihl int
var baseIPHdrSum uint32
if isV4 {
origIPID = binary.BigEndian.Uint16(pkt[4:6])
ihl = int(pkt[0]&0x0f) * 4
if ihl < 20 || ihl > csumStart {
return fmt.Errorf("bad IPv4 IHL: %d", ihl)
}
var ipTmp [60]byte
copy(ipTmp[:ihl], pkt[:ihl])
ipTmp[2], ipTmp[3] = 0, 0 // total_len
ipTmp[4], ipTmp[5] = 0, 0 // id
ipTmp[10], ipTmp[11] = 0, 0 // checksum
baseIPHdrSum = checksumBytes(ipTmp[:ihl], 0)
}
off := 0
for i := 0; i < numSeg; i++ {
segStart := i * gso
segEnd := segStart + gso
if segEnd > payLen {
segEnd = payLen
}
segPayLen := segEnd - segStart
copy(scratch[off:], pkt[:headerLen])
copy(scratch[off+headerLen:], payload[segStart:segEnd])
seg := scratch[off : off+headerLen+segPayLen]
off += headerLen + segPayLen
segSeq := origSeq + uint32(segStart)
segFlags := origFlags
if i != numSeg-1 {
segFlags = origFlags &^ tcpFinPsh
}
totalLen := headerLen + segPayLen
// Patch IP header and write the v4 header checksum from the precomputed base.
if isV4 {
segID := origIPID + uint16(i)
binary.BigEndian.PutUint16(seg[2:4], uint16(totalLen))
binary.BigEndian.PutUint16(seg[4:6], segID)
ipSum := baseIPHdrSum + uint32(totalLen) + uint32(segID)
binary.BigEndian.PutUint16(seg[10:12], checksumFold(ipSum))
} else {
// IPv6 payload length excludes the 40-byte fixed header but
// includes any extension headers between [40:csumStart].
binary.BigEndian.PutUint16(seg[4:6], uint16(headerLen-40+segPayLen))
}
// Patch TCP header.
binary.BigEndian.PutUint32(seg[csumStart+4:csumStart+8], segSeq)
seg[csumStart+13] = segFlags
// (csum is written below; its prior contents in `seg` don't affect the
// computation since we never sum over the segment's own header.)
tcpLen := tcpHdrLen + segPayLen
paySum := checksumBytes(payload[segStart:segEnd], 0)
// Combine pre-folded uint32s into a wider accumulator, then fold. Using
// uint64 guards against overflow when segSeq's high bits set.
wide := uint64(baseTcpHdrSum) + uint64(paySum) + uint64(baseProtoSum)
wide += uint64(segSeq) + uint64(segFlags) + uint64(tcpLen)
wide = (wide & 0xffffffff) + (wide >> 32)
wide = (wide & 0xffffffff) + (wide >> 32)
binary.BigEndian.PutUint16(seg[csumStart+16:csumStart+18], checksumFold(uint32(wide)))
*out = append(*out, seg)
}
return nil
}
// checksumBytes returns the Internet-checksum partial sum of b, seeded with
// initial. Result is a 32-bit accumulator; the caller folds to 16.
//
// Each 4-byte load is added directly into a 64-bit accumulator. Two parallel
// accumulators break the serial dependency through `sum` and let the CPU
// overlap independent adds. The final fold from 64 → 32 → 16 handles the
// carries that accumulated across the 32-bit lane boundary.
func checksumBytes(b []byte, initial uint32) uint32 {
s0 := uint64(initial)
var s1 uint64
for len(b) >= 32 {
s0 += uint64(binary.BigEndian.Uint32(b[0:4]))
s1 += uint64(binary.BigEndian.Uint32(b[4:8]))
s0 += uint64(binary.BigEndian.Uint32(b[8:12]))
s1 += uint64(binary.BigEndian.Uint32(b[12:16]))
s0 += uint64(binary.BigEndian.Uint32(b[16:20]))
s1 += uint64(binary.BigEndian.Uint32(b[20:24]))
s0 += uint64(binary.BigEndian.Uint32(b[24:28]))
s1 += uint64(binary.BigEndian.Uint32(b[28:32]))
b = b[32:]
}
sum := s0 + s1
for len(b) >= 4 {
sum += uint64(binary.BigEndian.Uint32(b[:4]))
b = b[4:]
}
if len(b) >= 2 {
sum += uint64(binary.BigEndian.Uint16(b[:2]))
b = b[2:]
}
if len(b) == 1 {
sum += uint64(b[0]) << 8
}
sum = (sum & 0xffffffff) + (sum >> 32)
sum = (sum & 0xffffffff) + (sum >> 32)
return uint32(sum)
}
func checksumFold(sum uint32) uint16 {
for sum>>16 != 0 {
sum = (sum & 0xffff) + (sum >> 16)
}
return ^uint16(sum)
}
func pseudoHeaderIPv4(src, dst []byte, proto byte, tcpLen int) uint32 {
sum := checksumBytes(src, 0)
sum = checksumBytes(dst, sum)
sum += uint32(proto)
sum += uint32(tcpLen)
return sum
}
func pseudoHeaderIPv6(src, dst []byte, proto byte, tcpLen int) uint32 {
sum := checksumBytes(src, 0)
sum = checksumBytes(dst, sum)
sum += uint32(tcpLen >> 16)
sum += uint32(tcpLen & 0xffff)
sum += uint32(proto)
return sum
}
+3 -1
View File
@@ -3,7 +3,9 @@
package overlay package overlay
import "testing" import (
"testing"
)
var runAdvMSSTests = []struct { var runAdvMSSTests = []struct {
name string name string
+14 -15
View File
@@ -6,6 +6,7 @@ package overlay
import ( import (
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"regexp" "regexp"
@@ -14,8 +15,8 @@ import (
"unsafe" "unsafe"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
netroute "golang.org/x/net/route" netroute "golang.org/x/net/route"
@@ -62,7 +63,7 @@ type tun struct {
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 *logrus.Logger l *slog.Logger
f *os.File f *os.File
fd int fd int
@@ -71,9 +72,6 @@ type tun struct {
} }
func (t *tun) Read() ([][]byte, error) { func (t *tun) Read() ([][]byte, error) {
if t.readBuf == nil {
t.readBuf = make([]byte, defaultBatchBufSize)
}
n, err := t.readOne(t.readBuf) n, err := t.readOne(t.readBuf)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -82,17 +80,17 @@ func (t *tun) Read() ([][]byte, error) {
return t.batchRet[:], nil return t.batchRet[:], nil
} }
func (t *tun) WriteReject(p []byte) (int, error) { func (t *tun) Readers() []tio.Queue {
return t.Write(p) return []tio.Queue{t}
} }
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`) var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in NetBSD") return nil, fmt.Errorf("newTunFromFd not supported in NetBSD")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open tun device // Try to open tun device
var err error var err error
deviceName := c.GetString("tun.dev", "") deviceName := c.GetString("tun.dev", "")
@@ -110,7 +108,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
err = unix.SetNonblock(fd, true) err = unix.SetNonblock(fd, true)
if err != nil { if err != nil {
l.WithError(err).Warn("Failed to set the tun device as nonblocking") l.Warn("Failed to set the tun device as nonblocking", "error", err)
} }
t := &tun{ t := &tun{
@@ -120,6 +118,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
MTU: c.GetInt("tun.mtu", DefaultMTU), MTU: c.GetInt("tun.mtu", DefaultMTU),
l: l, l: l,
readBuf: make([]byte, defaultBatchBufSize),
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -412,8 +411,8 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (Queue, error) { func (t *tun) NewMultiQueueReader() error {
return nil, fmt.Errorf("TODO: multiqueue not implemented for netbsd") return fmt.Errorf("TODO: multiqueue not implemented for netbsd")
} }
func (t *tun) addRoutes(logErrors bool) error { func (t *tun) addRoutes(logErrors bool) error {
@@ -434,7 +433,7 @@ func (t *tun) addRoutes(logErrors bool) error {
return retErr return retErr
} }
} else { } else {
t.l.WithField("route", r).Info("Added route") t.l.Info("Added route", "route", r)
} }
} }
@@ -449,9 +448,9 @@ func (t *tun) removeRoutes(routes []Route) error {
err := delRoute(r.Cidr, t.vpnNetworks) err := delRoute(r.Cidr, t.vpnNetworks)
if err != nil { if err != nil {
t.l.WithError(err).WithField("route", r).Error("Failed to remove route") t.l.Error("Failed to remove route", "error", err, "route", r)
} else { } else {
t.l.WithField("route", r).Info("Removed route") t.l.Info("Removed route", "route", r)
} }
} }
return nil return nil
+16 -17
View File
@@ -6,6 +6,7 @@ package overlay
import ( import (
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"regexp" "regexp"
@@ -14,8 +15,8 @@ import (
"unsafe" "unsafe"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
netroute "golang.org/x/net/route" netroute "golang.org/x/net/route"
@@ -53,7 +54,7 @@ type tun struct {
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 *logrus.Logger l *slog.Logger
f *os.File f *os.File
fd int fd int
// cache out buffer since we need to prepend 4 bytes for tun metadata // cache out buffer since we need to prepend 4 bytes for tun metadata
@@ -64,9 +65,6 @@ type tun struct {
} }
func (t *tun) Read() ([][]byte, error) { func (t *tun) Read() ([][]byte, error) {
if t.readBuf == nil {
t.readBuf = make([]byte, defaultBatchBufSize)
}
n, err := t.readOne(t.readBuf) n, err := t.readOne(t.readBuf)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -75,17 +73,13 @@ func (t *tun) Read() ([][]byte, error) {
return t.batchRet[:], nil return t.batchRet[:], nil
} }
func (t *tun) WriteReject(p []byte) (int, error) {
return t.Write(p)
}
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`) var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in openbsd") return nil, fmt.Errorf("newTunFromFd not supported in openbsd")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open tun device // Try to open tun device
var err error var err error
deviceName := c.GetString("tun.dev", "") deviceName := c.GetString("tun.dev", "")
@@ -103,7 +97,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
err = unix.SetNonblock(fd, true) err = unix.SetNonblock(fd, true)
if err != nil { if err != nil {
l.WithError(err).Warn("Failed to set the tun device as nonblocking") l.Warn("Failed to set the tun device as nonblocking", "error", err)
} }
t := &tun{ t := &tun{
@@ -113,6 +107,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
MTU: c.GetInt("tun.mtu", DefaultMTU), MTU: c.GetInt("tun.mtu", DefaultMTU),
l: l, l: l,
readBuf: make([]byte, defaultBatchBufSize),
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -332,8 +327,8 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (Queue, error) { func (t *tun) NewMultiQueueReader() error {
return nil, fmt.Errorf("TODO: multiqueue not implemented for openbsd") return fmt.Errorf("TODO: multiqueue not implemented for openbsd")
} }
func (t *tun) addRoutes(logErrors bool) error { func (t *tun) addRoutes(logErrors bool) error {
@@ -354,7 +349,7 @@ func (t *tun) addRoutes(logErrors bool) error {
return retErr return retErr
} }
} else { } else {
t.l.WithField("route", r).Info("Added route") t.l.Info("Added route", "route", r)
} }
} }
@@ -369,9 +364,9 @@ func (t *tun) removeRoutes(routes []Route) error {
err := delRoute(r.Cidr, t.vpnNetworks) err := delRoute(r.Cidr, t.vpnNetworks)
if err != nil { if err != nil {
t.l.WithError(err).WithField("route", r).Error("Failed to remove route") t.l.Error("Failed to remove route", "error", err, "route", r)
} else { } else {
t.l.WithField("route", r).Info("Removed route") t.l.Info("Removed route", "route", r)
} }
} }
return nil return nil
@@ -384,6 +379,10 @@ func (t *tun) deviceBytes() (o [16]byte) {
return return
} }
func (t *tun) Readers() []tio.Queue {
return []tio.Queue{t}
}
func addRoute(prefix netip.Prefix, gateways []netip.Prefix) error { func addRoute(prefix netip.Prefix, gateways []netip.Prefix) error {
sock, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC) sock, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
if err != nil { if err != nil {
+14 -12
View File
@@ -4,15 +4,17 @@
package overlay package overlay
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"sync/atomic" "sync/atomic"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
@@ -21,7 +23,7 @@ type TestTun struct {
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
Routes []Route Routes []Route
routeTree *bart.Table[routing.Gateways] routeTree *bart.Table[routing.Gateways]
l *logrus.Logger l *slog.Logger
closed atomic.Bool closed atomic.Bool
rxPackets chan []byte // Packets to receive into nebula rxPackets chan []byte // Packets to receive into nebula
@@ -39,7 +41,7 @@ func (t *TestTun) Read() ([][]byte, error) {
return t.batchRet[:], nil return t.batchRet[:], nil
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*TestTun, error) { func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*TestTun, error) {
_, routes, err := getAllRoutesFromConfig(c, vpnNetworks, true) _, routes, err := getAllRoutesFromConfig(c, vpnNetworks, true)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -60,7 +62,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
}, nil }, nil
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*TestTun, error) { func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (*TestTun, error) {
return nil, fmt.Errorf("newTunFromFd not supported") return nil, fmt.Errorf("newTunFromFd not supported")
} }
@@ -72,8 +74,8 @@ func (t *TestTun) Send(packet []byte) {
return return
} }
if t.l.Level >= logrus.DebugLevel { if t.l.Enabled(context.Background(), slog.LevelDebug) {
t.l.WithField("dataLen", len(packet)).Debug("Tun receiving injected packet") t.l.Debug("Tun receiving injected packet", "dataLen", len(packet))
} }
t.rxPackets <- packet t.rxPackets <- packet
} }
@@ -126,10 +128,6 @@ func (t *TestTun) Write(b []byte) (n int, err error) {
return len(b), nil return len(b), nil
} }
func (t *TestTun) WriteReject(b []byte) (int, error) {
return t.Write(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)
@@ -138,10 +136,14 @@ func (t *TestTun) Close() error {
return nil return nil
} }
func (t *TestTun) Readers() []tio.Queue {
return []tio.Queue{t}
}
func (t *TestTun) SupportsMultiqueue() bool { func (t *TestTun) SupportsMultiqueue() bool {
return false return false
} }
func (t *TestTun) NewMultiQueueReader() (Queue, error) { func (t *TestTun) NewMultiQueueReader() error {
return nil, fmt.Errorf("TODO: multiqueue not implemented") return fmt.Errorf("TODO: multiqueue not implemented")
} }
+16 -17
View File
@@ -6,6 +6,7 @@ package overlay
import ( import (
"crypto" "crypto"
"fmt" "fmt"
"log/slog"
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
@@ -15,8 +16,8 @@ import (
"unsafe" "unsafe"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
"github.com/slackhq/nebula/wintun" "github.com/slackhq/nebula/wintun"
@@ -32,7 +33,7 @@ type winTun struct {
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 *logrus.Logger l *slog.Logger
tun *wintun.NativeTun tun *wintun.NativeTun
@@ -41,9 +42,6 @@ type winTun struct {
} }
func (t *winTun) Read() ([][]byte, error) { func (t *winTun) Read() ([][]byte, error) {
if t.readBuf == nil {
t.readBuf = make([]byte, defaultBatchBufSize)
}
n, err := t.tun.Read(t.readBuf, 0) n, err := t.tun.Read(t.readBuf, 0)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -52,15 +50,11 @@ func (t *winTun) Read() ([][]byte, error) {
return t.batchRet[:], nil return t.batchRet[:], nil
} }
func (t *winTun) WriteReject(p []byte) (int, error) { func newTunFromFd(_ *config.C, _ *slog.Logger, _ int, _ []netip.Prefix) (Device, error) {
return t.Write(p)
}
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (Device, error) {
return nil, fmt.Errorf("newTunFromFd not supported in Windows") return nil, fmt.Errorf("newTunFromFd not supported in Windows")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*winTun, error) { func newTun(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, _ bool) (*winTun, error) {
err := checkWinTunExists() err := checkWinTunExists()
if err != nil { if err != nil {
return nil, fmt.Errorf("can not load the wintun driver: %w", err) return nil, fmt.Errorf("can not load the wintun driver: %w", err)
@@ -73,6 +67,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
} }
t := &winTun{ t := &winTun{
readBuf: make([]byte, defaultBatchBufSize),
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
MTU: c.GetInt("tun.mtu", DefaultMTU), MTU: c.GetInt("tun.mtu", DefaultMTU),
@@ -89,7 +84,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
if err != nil { if err != nil {
// Windows 10 has an issue with unclean shutdowns not fully cleaning up the wintun device. // Windows 10 has an issue with unclean shutdowns not fully cleaning up the wintun device.
// Trying a second time resolves the issue. // Trying a second time resolves the issue.
l.WithError(err).Debug("Failed to create wintun device, retrying") l.Debug("Failed to create wintun device, retrying", "error", err)
tunDevice, err = wintun.CreateTUNWithRequestedGUID(deviceName, guid, t.MTU) tunDevice, err = wintun.CreateTUNWithRequestedGUID(deviceName, guid, t.MTU)
if err != nil { if err != nil {
return nil, &NameError{ return nil, &NameError{
@@ -188,7 +183,7 @@ func (t *winTun) addRoutes(logErrors bool) error {
return retErr return retErr
} }
} else { } else {
t.l.WithField("route", r).Info("Added route") t.l.Info("Added route", "route", r)
} }
if !foundDefault4 { if !foundDefault4 {
@@ -226,9 +221,9 @@ func (t *winTun) removeRoutes(routes []Route) error {
// See comment on luid.AddRoute // See comment on luid.AddRoute
err := luid.DeleteRoute(r.Cidr, r.Via[0].Addr()) err := luid.DeleteRoute(r.Cidr, r.Via[0].Addr())
if err != nil { if err != nil {
t.l.WithError(err).WithField("route", r).Error("Failed to remove route") t.l.Error("Failed to remove route", "error", err, "route", r)
} else { } else {
t.l.WithField("route", r).Info("Removed route") t.l.Info("Removed route", "route", r)
} }
} }
return nil return nil
@@ -255,8 +250,12 @@ func (t *winTun) SupportsMultiqueue() bool {
return false return false
} }
func (t *winTun) NewMultiQueueReader() (Queue, error) { func (t *winTun) NewMultiQueueReader() error {
return nil, fmt.Errorf("TODO: multiqueue not implemented for windows") return fmt.Errorf("TODO: multiqueue not implemented for windows")
}
func (t *winTun) Readers() []tio.Queue {
return []tio.Queue{t}
} }
func (t *winTun) Close() error { func (t *winTun) Close() error {
+17 -7
View File
@@ -2,14 +2,15 @@ package overlay
import ( import (
"io" "io"
"log/slog"
"net/netip" "net/netip"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/overlay/tio"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
func NewUserDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) { func NewUserDeviceFromConfig(c *config.C, l *slog.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) {
return NewUserDevice(vpnNetworks) return NewUserDevice(vpnNetworks)
} }
@@ -23,11 +24,13 @@ func NewUserDevice(vpnNetworks []netip.Prefix) (Device, error) {
outboundWriter: ow, outboundWriter: ow,
inboundReader: ir, inboundReader: ir,
inboundWriter: iw, inboundWriter: iw,
numReaders: 1,
}, nil }, nil
} }
type UserDevice struct { type UserDevice struct {
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
numReaders int
outboundReader *io.PipeReader outboundReader *io.PipeReader
outboundWriter *io.PipeWriter outboundWriter *io.PipeWriter
@@ -65,8 +68,17 @@ func (d *UserDevice) SupportsMultiqueue() bool {
return true return true
} }
func (d *UserDevice) NewMultiQueueReader() (Queue, error) { func (d *UserDevice) NewMultiQueueReader() error {
return d, nil d.numReaders++
return nil
}
func (d *UserDevice) Readers() []tio.Queue {
out := make([]tio.Queue, d.numReaders)
for i := range d.numReaders {
out[i] = d
}
return out
} }
func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) { func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) {
@@ -76,9 +88,7 @@ func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) {
func (d *UserDevice) Write(p []byte) (n int, err error) { func (d *UserDevice) Write(p []byte) (n int, err error) {
return d.inboundWriter.Write(p) return d.inboundWriter.Write(p)
} }
func (d *UserDevice) WriteReject(p []byte) (n int, err error) {
return d.Write(p)
}
func (d *UserDevice) Close() error { func (d *UserDevice) Close() error {
d.inboundWriter.Close() d.inboundWriter.Close()
d.outboundWriter.Close() d.outboundWriter.Close()
+9 -9
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@@ -15,7 +16,6 @@ import (
"time" "time"
"github.com/gaissmai/bart" "github.com/gaissmai/bart"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
@@ -24,7 +24,7 @@ import (
type PKI struct { type PKI struct {
cs atomic.Pointer[CertState] cs atomic.Pointer[CertState]
caPool atomic.Pointer[cert.CAPool] caPool atomic.Pointer[cert.CAPool]
l *logrus.Logger l *slog.Logger
} }
type CertState struct { type CertState struct {
@@ -46,7 +46,7 @@ type CertState struct {
myVpnBroadcastAddrsTable *bart.Lite myVpnBroadcastAddrsTable *bart.Lite
} }
func NewPKIFromConfig(l *logrus.Logger, c *config.C) (*PKI, error) { func NewPKIFromConfig(l *slog.Logger, c *config.C) (*PKI, error) {
pki := &PKI{l: l} pki := &PKI{l: l}
err := pki.reload(c, true) err := pki.reload(c, true)
if err != nil { if err != nil {
@@ -182,9 +182,9 @@ func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError {
p.cs.Store(newState) p.cs.Store(newState)
if initial { if initial {
p.l.WithField("cert", newState).Debug("Client nebula certificate(s)") p.l.Debug("Client nebula certificate(s)", "cert", newState)
} else { } else {
p.l.WithField("cert", newState).Info("Client certificate(s) refreshed from disk") p.l.Info("Client certificate(s) refreshed from disk", "cert", newState)
} }
return nil return nil
} }
@@ -196,7 +196,7 @@ func (p *PKI) reloadCAPool(c *config.C) *util.ContextualError {
} }
p.caPool.Store(caPool) p.caPool.Store(caPool)
p.l.WithField("fingerprints", caPool.GetFingerprints()).Debug("Trusted CA fingerprints") p.l.Debug("Trusted CA fingerprints", "fingerprints", caPool.GetFingerprints())
return nil return nil
} }
@@ -487,7 +487,7 @@ func loadCertificate(b []byte) (cert.Certificate, []byte, error) {
return c, b, nil return c, b, nil
} }
func loadCAPoolFromConfig(l *logrus.Logger, c *config.C) (*cert.CAPool, error) { func loadCAPoolFromConfig(l *slog.Logger, c *config.C) (*cert.CAPool, error) {
caPathOrPEM := c.GetString("pki.ca", "") caPathOrPEM := c.GetString("pki.ca", "")
if caPathOrPEM == "" { if caPathOrPEM == "" {
return nil, errors.New("no pki.ca path or PEM data provided") return nil, errors.New("no pki.ca path or PEM data provided")
@@ -512,7 +512,7 @@ func loadCAPoolFromConfig(l *logrus.Logger, c *config.C) (*cert.CAPool, error) {
for _, crt := range caPool.CAs { for _, crt := range caPool.CAs {
if crt.Certificate.Expired(time.Now()) { if crt.Certificate.Expired(time.Now()) {
expired++ expired++
l.WithField("cert", crt).Warn("expired certificate present in CA pool") l.Warn("expired certificate present in CA pool", "cert", crt)
} }
} }
@@ -530,7 +530,7 @@ func loadCAPoolFromConfig(l *logrus.Logger, c *config.C) (*cert.CAPool, error) {
caPool.BlocklistFingerprint(fp) caPool.BlocklistFingerprint(fp)
} }
l.WithField("fingerprintCount", len(bl)).Info("Blocklisted certificates") l.Info("Blocklisted certificates", "fingerprintCount", len(bl))
} }
return caPool, nil return caPool, nil
+1 -1
View File
@@ -41,7 +41,7 @@ func BenchmarkReloadConfigWithCAs(b *testing.B) {
c := config.NewC(l) c := config.NewC(l)
require.NoError(b, c.Load(dir)) require.NoError(b, c.Load(dir))
_, err := NewPKIFromConfig(l, c) _, err := NewPKIFromConfig(test.NewLogger(), c)
require.NoError(b, err) require.NoError(b, err)
b.ReportAllocs() b.ReportAllocs()
+7 -7
View File
@@ -1,10 +1,10 @@
package nebula package nebula
import ( import (
"log/slog"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
) )
@@ -14,10 +14,10 @@ type Punchy struct {
delay atomic.Int64 delay atomic.Int64
respondDelay atomic.Int64 respondDelay atomic.Int64
punchEverything atomic.Bool punchEverything atomic.Bool
l *logrus.Logger l *slog.Logger
} }
func NewPunchyFromConfig(l *logrus.Logger, c *config.C) *Punchy { func NewPunchyFromConfig(l *slog.Logger, c *config.C) *Punchy {
p := &Punchy{l: l} p := &Punchy{l: l}
p.reload(c, true) p.reload(c, true)
@@ -62,7 +62,7 @@ func (p *Punchy) reload(c *config.C, initial bool) {
p.respond.Store(yes) p.respond.Store(yes)
if !initial { if !initial {
p.l.Infof("punchy.respond changed to %v", p.GetRespond()) p.l.Info("punchy.respond changed", "respond", p.GetRespond())
} }
} }
@@ -70,21 +70,21 @@ func (p *Punchy) reload(c *config.C, initial bool) {
if initial || c.HasChanged("punchy.delay") { if initial || c.HasChanged("punchy.delay") {
p.delay.Store((int64)(c.GetDuration("punchy.delay", time.Second))) p.delay.Store((int64)(c.GetDuration("punchy.delay", time.Second)))
if !initial { if !initial {
p.l.Infof("punchy.delay changed to %s", p.GetDelay()) p.l.Info("punchy.delay changed", "delay", p.GetDelay())
} }
} }
if initial || c.HasChanged("punchy.target_all_remotes") { if initial || c.HasChanged("punchy.target_all_remotes") {
p.punchEverything.Store(c.GetBool("punchy.target_all_remotes", false)) p.punchEverything.Store(c.GetBool("punchy.target_all_remotes", false))
if !initial { if !initial {
p.l.WithField("target_all_remotes", p.GetTargetEverything()).Info("punchy.target_all_remotes changed") p.l.Info("punchy.target_all_remotes changed", "target_all_remotes", p.GetTargetEverything())
} }
} }
if initial || c.HasChanged("punchy.respond_delay") { if initial || c.HasChanged("punchy.respond_delay") {
p.respondDelay.Store((int64)(c.GetDuration("punchy.respond_delay", 5*time.Second))) p.respondDelay.Store((int64)(c.GetDuration("punchy.respond_delay", 5*time.Second)))
if !initial { if !initial {
p.l.Infof("punchy.respond_delay changed to %s", p.GetRespondDelay()) p.l.Info("punchy.respond_delay changed", "respond_delay", p.GetRespondDelay())
} }
} }
} }
+165 -8
View File
@@ -1,6 +1,8 @@
package nebula package nebula
import ( import (
"context"
"log/slog"
"testing" "testing"
"time" "time"
@@ -15,7 +17,7 @@ func TestNewPunchyFromConfig(t *testing.T) {
c := config.NewC(l) c := config.NewC(l)
// Test defaults // Test defaults
p := NewPunchyFromConfig(l, c) p := NewPunchyFromConfig(test.NewLogger(), c)
assert.False(t, p.GetPunch()) assert.False(t, p.GetPunch())
assert.False(t, p.GetRespond()) assert.False(t, p.GetRespond())
assert.Equal(t, time.Second, p.GetDelay()) assert.Equal(t, time.Second, p.GetDelay())
@@ -23,33 +25,33 @@ func TestNewPunchyFromConfig(t *testing.T) {
// punchy deprecation // punchy deprecation
c.Settings["punchy"] = true c.Settings["punchy"] = true
p = NewPunchyFromConfig(l, c) p = NewPunchyFromConfig(test.NewLogger(), c)
assert.True(t, p.GetPunch()) assert.True(t, p.GetPunch())
// punchy.punch // punchy.punch
c.Settings["punchy"] = map[string]any{"punch": true} c.Settings["punchy"] = map[string]any{"punch": true}
p = NewPunchyFromConfig(l, c) p = NewPunchyFromConfig(test.NewLogger(), c)
assert.True(t, p.GetPunch()) assert.True(t, p.GetPunch())
// punch_back deprecation // punch_back deprecation
c.Settings["punch_back"] = true c.Settings["punch_back"] = true
p = NewPunchyFromConfig(l, c) p = NewPunchyFromConfig(test.NewLogger(), c)
assert.True(t, p.GetRespond()) assert.True(t, p.GetRespond())
// punchy.respond // punchy.respond
c.Settings["punchy"] = map[string]any{"respond": true} c.Settings["punchy"] = map[string]any{"respond": true}
c.Settings["punch_back"] = false c.Settings["punch_back"] = false
p = NewPunchyFromConfig(l, c) p = NewPunchyFromConfig(test.NewLogger(), c)
assert.True(t, p.GetRespond()) assert.True(t, p.GetRespond())
// punchy.delay // punchy.delay
c.Settings["punchy"] = map[string]any{"delay": "1m"} c.Settings["punchy"] = map[string]any{"delay": "1m"}
p = NewPunchyFromConfig(l, c) p = NewPunchyFromConfig(test.NewLogger(), c)
assert.Equal(t, time.Minute, p.GetDelay()) assert.Equal(t, time.Minute, p.GetDelay())
// punchy.respond_delay // punchy.respond_delay
c.Settings["punchy"] = map[string]any{"respond_delay": "1m"} c.Settings["punchy"] = map[string]any{"respond_delay": "1m"}
p = NewPunchyFromConfig(l, c) p = NewPunchyFromConfig(test.NewLogger(), c)
assert.Equal(t, time.Minute, p.GetRespondDelay()) assert.Equal(t, time.Minute, p.GetRespondDelay())
} }
@@ -62,7 +64,7 @@ punchy:
delay: 1m delay: 1m
respond: false respond: false
`)) `))
p := NewPunchyFromConfig(l, c) p := NewPunchyFromConfig(test.NewLogger(), c)
assert.Equal(t, delay, p.GetDelay()) assert.Equal(t, delay, p.GetDelay())
assert.False(t, p.GetRespond()) assert.False(t, p.GetRespond())
@@ -76,3 +78,158 @@ punchy:
assert.Equal(t, newDelay, p.GetDelay()) assert.Equal(t, newDelay, p.GetDelay())
assert.True(t, p.GetRespond()) assert.True(t, p.GetRespond())
} }
// The tests below pin the shape of each log line Punchy produces so changes
// cannot silently break whatever operators are grepping for. The assertions
// are on the structured message + attrs (e.g. "punchy.respond changed" with
// a respond=true field) rather than a formatted string.
//
// Punchy.reload also emits a spurious "Changing punchy.punch with reload is
// not supported" warning whenever any key under punchy changes, because of
// the c.HasChanged("punchy") fallback kept for the deprecated top-level
// punchy form. The tests filter by message rather than asserting total
// entry counts so that warning is tolerated without being locked into
// the format.
type capturedEntry struct {
Level slog.Level
Msg string
Attrs map[string]any
}
// capturingHandler is a slog.Handler that records each Record it receives so
// tests can assert on the level, message, and attribute map of individual log
// lines without coupling to any specific text format.
type capturingHandler struct {
entries []capturedEntry
}
func (h *capturingHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
func (h *capturingHandler) Handle(_ context.Context, r slog.Record) error {
e := capturedEntry{
Level: r.Level,
Msg: r.Message,
Attrs: make(map[string]any),
}
r.Attrs(func(a slog.Attr) bool {
e.Attrs[a.Key] = a.Value.Resolve().Any()
return true
})
h.entries = append(h.entries, e)
return nil
}
func (h *capturingHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
func (h *capturingHandler) WithGroup(_ string) slog.Handler { return h }
func newCapturingPunchyLogger(t *testing.T) (*slog.Logger, *capturingHandler) {
t.Helper()
hook := &capturingHandler{}
return slog.New(hook), hook
}
func findEntry(t *testing.T, entries []capturedEntry, msg string) capturedEntry {
t.Helper()
for _, e := range entries {
if e.Msg == msg {
return e
}
}
t.Fatalf("no entry with message %q among %d entries", msg, len(entries))
return capturedEntry{}
}
func TestPunchy_LogFormat_InitialEnabled(t *testing.T) {
l, hook := newCapturingPunchyLogger(t)
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(`punchy: {punch: true}`))
NewPunchyFromConfig(l, c)
entry := findEntry(t, hook.entries, "punchy enabled")
assert.Equal(t, slog.LevelInfo, entry.Level)
assert.Empty(t, entry.Attrs)
}
func TestPunchy_LogFormat_InitialDisabled(t *testing.T) {
l, hook := newCapturingPunchyLogger(t)
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(`punchy: {punch: false}`))
NewPunchyFromConfig(l, c)
entry := findEntry(t, hook.entries, "punchy disabled")
assert.Equal(t, slog.LevelInfo, entry.Level)
assert.Empty(t, entry.Attrs)
}
func TestPunchy_LogFormat_ReloadPunchUnsupported(t *testing.T) {
l, hook := newCapturingPunchyLogger(t)
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(`punchy: {punch: false}`))
NewPunchyFromConfig(l, c)
hook.entries = nil
require.NoError(t, c.ReloadConfigString(`punchy: {punch: true}`))
entry := findEntry(t, hook.entries, "Changing punchy.punch with reload is not supported, ignoring.")
assert.Equal(t, slog.LevelWarn, entry.Level)
assert.Empty(t, entry.Attrs)
}
func TestPunchy_LogFormat_ReloadRespond(t *testing.T) {
l, hook := newCapturingPunchyLogger(t)
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(`punchy: {respond: false}`))
NewPunchyFromConfig(l, c)
hook.entries = nil
require.NoError(t, c.ReloadConfigString(`punchy: {respond: true}`))
entry := findEntry(t, hook.entries, "punchy.respond changed")
assert.Equal(t, slog.LevelInfo, entry.Level)
assert.Equal(t, map[string]any{"respond": true}, entry.Attrs)
}
func TestPunchy_LogFormat_ReloadDelay(t *testing.T) {
l, hook := newCapturingPunchyLogger(t)
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(`punchy: {delay: 1s}`))
NewPunchyFromConfig(l, c)
hook.entries = nil
require.NoError(t, c.ReloadConfigString(`punchy: {delay: 10s}`))
entry := findEntry(t, hook.entries, "punchy.delay changed")
assert.Equal(t, slog.LevelInfo, entry.Level)
assert.Equal(t, map[string]any{"delay": 10 * time.Second}, entry.Attrs)
}
func TestPunchy_LogFormat_ReloadTargetAllRemotes(t *testing.T) {
l, hook := newCapturingPunchyLogger(t)
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(`punchy: {target_all_remotes: false}`))
NewPunchyFromConfig(l, c)
hook.entries = nil
require.NoError(t, c.ReloadConfigString(`punchy: {target_all_remotes: true}`))
entry := findEntry(t, hook.entries, "punchy.target_all_remotes changed")
assert.Equal(t, slog.LevelInfo, entry.Level)
assert.Equal(t, map[string]any{"target_all_remotes": true}, entry.Attrs)
}
func TestPunchy_LogFormat_ReloadRespondDelay(t *testing.T) {
l, hook := newCapturingPunchyLogger(t)
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(`punchy: {respond_delay: 5s}`))
NewPunchyFromConfig(l, c)
hook.entries = nil
require.NoError(t, c.ReloadConfigString(`punchy: {respond_delay: 15s}`))
entry := findEntry(t, hook.entries, "punchy.respond_delay changed")
assert.Equal(t, slog.LevelInfo, entry.Level)
assert.Equal(t, map[string]any{"respond_delay": 15 * time.Second}, entry.Attrs)
}
+82 -83
View File
@@ -5,22 +5,22 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net/netip" "net/netip"
"sync/atomic" "sync/atomic"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
) )
type relayManager struct { type relayManager struct {
l *logrus.Logger l *slog.Logger
hostmap *HostMap hostmap *HostMap
amRelay atomic.Bool amRelay atomic.Bool
} }
func NewRelayManager(ctx context.Context, l *logrus.Logger, hostmap *HostMap, c *config.C) *relayManager { func NewRelayManager(ctx context.Context, l *slog.Logger, hostmap *HostMap, c *config.C) *relayManager {
rm := &relayManager{ rm := &relayManager{
l: l, l: l,
hostmap: hostmap, hostmap: hostmap,
@@ -29,7 +29,7 @@ func NewRelayManager(ctx context.Context, l *logrus.Logger, hostmap *HostMap, c
c.RegisterReloadCallback(func(c *config.C) { c.RegisterReloadCallback(func(c *config.C) {
err := rm.reload(c, false) err := rm.reload(c, false)
if err != nil { if err != nil {
l.WithError(err).Error("Failed to reload relay_manager") rm.l.Error("Failed to reload relay_manager", "error", err)
} }
}) })
return rm return rm
@@ -52,7 +52,7 @@ func (rm *relayManager) setAmRelay(v bool) {
// AddRelay finds an available relay index on the hostmap, and associates the relay info with it. // AddRelay finds an available relay index on the hostmap, and associates the relay info with it.
// relayHostInfo is the Nebula peer which can be used as a relay to access the target vpnIp. // relayHostInfo is the Nebula peer which can be used as a relay to access the target vpnIp.
func AddRelay(l *logrus.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp netip.Addr, remoteIdx *uint32, relayType int, state int) (uint32, error) { func AddRelay(l *slog.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp netip.Addr, remoteIdx *uint32, relayType int, state int) (uint32, error) {
hm.Lock() hm.Lock()
defer hm.Unlock() defer hm.Unlock()
for range 32 { for range 32 {
@@ -92,24 +92,24 @@ func AddRelay(l *logrus.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp neti
func (rm *relayManager) EstablishRelay(relayHostInfo *HostInfo, m *NebulaControl) (*Relay, error) { func (rm *relayManager) EstablishRelay(relayHostInfo *HostInfo, m *NebulaControl) (*Relay, error) {
relay, ok := relayHostInfo.relayState.CompleteRelayByIdx(m.InitiatorRelayIndex, m.ResponderRelayIndex) relay, ok := relayHostInfo.relayState.CompleteRelayByIdx(m.InitiatorRelayIndex, m.ResponderRelayIndex)
if !ok { if !ok {
fields := logrus.Fields{ var relayFrom, relayTo any
"relay": relayHostInfo.vpnAddrs[0],
"initiatorRelayIndex": m.InitiatorRelayIndex,
}
if m.RelayFromAddr == nil { if m.RelayFromAddr == nil {
fields["relayFrom"] = m.OldRelayFromAddr relayFrom = m.OldRelayFromAddr
} else { } else {
fields["relayFrom"] = m.RelayFromAddr relayFrom = m.RelayFromAddr
} }
if m.RelayToAddr == nil { if m.RelayToAddr == nil {
fields["relayTo"] = m.OldRelayToAddr relayTo = m.OldRelayToAddr
} else { } else {
fields["relayTo"] = m.RelayToAddr relayTo = m.RelayToAddr
} }
rm.l.WithFields(fields).Info("relayManager failed to update relay") rm.l.Info("relayManager failed to update relay",
"relay", relayHostInfo.vpnAddrs[0],
"initiatorRelayIndex", m.InitiatorRelayIndex,
"relayFrom", relayFrom,
"relayTo", relayTo,
)
return nil, fmt.Errorf("unknown relay") return nil, fmt.Errorf("unknown relay")
} }
@@ -120,7 +120,7 @@ func (rm *relayManager) HandleControlMsg(h *HostInfo, d []byte, f *Interface) {
msg := &NebulaControl{} msg := &NebulaControl{}
err := msg.Unmarshal(d) err := msg.Unmarshal(d)
if err != nil { if err != nil {
h.logger(f.l).WithError(err).Error("Failed to unmarshal control message") h.logger(f.l).Error("Failed to unmarshal control message", "error", err)
return return
} }
@@ -147,20 +147,20 @@ func (rm *relayManager) HandleControlMsg(h *HostInfo, d []byte, f *Interface) {
} }
func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f *Interface, m *NebulaControl) { func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f *Interface, m *NebulaControl) {
rm.l.WithFields(logrus.Fields{ rm.l.Info("handleCreateRelayResponse",
"relayFrom": protoAddrToNetAddr(m.RelayFromAddr), "relayFrom", protoAddrToNetAddr(m.RelayFromAddr),
"relayTo": protoAddrToNetAddr(m.RelayToAddr), "relayTo", protoAddrToNetAddr(m.RelayToAddr),
"initiatorRelayIndex": m.InitiatorRelayIndex, "initiatorRelayIndex", m.InitiatorRelayIndex,
"responderRelayIndex": m.ResponderRelayIndex, "responderRelayIndex", m.ResponderRelayIndex,
"vpnAddrs": h.vpnAddrs}). "vpnAddrs", h.vpnAddrs,
Info("handleCreateRelayResponse") )
target := m.RelayToAddr target := m.RelayToAddr
targetAddr := protoAddrToNetAddr(target) targetAddr := protoAddrToNetAddr(target)
relay, err := rm.EstablishRelay(h, m) relay, err := rm.EstablishRelay(h, m)
if err != nil { if err != nil {
rm.l.WithError(err).Error("Failed to update relay for relayTo") rm.l.Error("Failed to update relay for relayTo", "error", err)
return return
} }
// Do I need to complete the relays now? // Do I need to complete the relays now?
@@ -170,12 +170,12 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
// I'm the middle man. Let the initiator know that the I've established the relay they requested. // I'm the middle man. Let the initiator know that the I've established the relay they requested.
peerHostInfo := rm.hostmap.QueryVpnAddr(relay.PeerAddr) peerHostInfo := rm.hostmap.QueryVpnAddr(relay.PeerAddr)
if peerHostInfo == nil { if peerHostInfo == nil {
rm.l.WithField("relayTo", relay.PeerAddr).Error("Can't find a HostInfo for peer") rm.l.Error("Can't find a HostInfo for peer", "relayTo", relay.PeerAddr)
return return
} }
peerRelay, ok := peerHostInfo.relayState.QueryRelayForByIp(targetAddr) peerRelay, ok := peerHostInfo.relayState.QueryRelayForByIp(targetAddr)
if !ok { if !ok {
rm.l.WithField("relayTo", peerHostInfo.vpnAddrs[0]).Error("peerRelay does not have Relay state for relayTo") rm.l.Error("peerRelay does not have Relay state for relayTo", "relayTo", peerHostInfo.vpnAddrs[0])
return return
} }
switch peerRelay.State { switch peerRelay.State {
@@ -193,12 +193,13 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
if v == cert.Version1 { if v == cert.Version1 {
peer := peerHostInfo.vpnAddrs[0] peer := peerHostInfo.vpnAddrs[0]
if !peer.Is4() { if !peer.Is4() {
rm.l.WithField("relayFrom", peer). rm.l.Error("Refusing to CreateRelayResponse for a v1 relay with an ipv6 address",
WithField("relayTo", target). "relayFrom", peer,
WithField("initiatorRelayIndex", resp.InitiatorRelayIndex). "relayTo", target,
WithField("responderRelayIndex", resp.ResponderRelayIndex). "initiatorRelayIndex", resp.InitiatorRelayIndex,
WithField("vpnAddrs", peerHostInfo.vpnAddrs). "responderRelayIndex", resp.ResponderRelayIndex,
Error("Refusing to CreateRelayResponse for a v1 relay with an ipv6 address") "vpnAddrs", peerHostInfo.vpnAddrs,
)
return return
} }
@@ -213,17 +214,16 @@ func (rm *relayManager) handleCreateRelayResponse(v cert.Version, h *HostInfo, f
msg, err := resp.Marshal() msg, err := resp.Marshal()
if err != nil { if err != nil {
rm.l.WithError(err). rm.l.Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay", "error", err)
Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay")
} else { } else {
f.SendMessageToHostInfo(header.Control, 0, peerHostInfo, msg, make([]byte, 12), make([]byte, mtu)) f.SendMessageToHostInfo(header.Control, 0, peerHostInfo, msg, make([]byte, 12), make([]byte, mtu))
rm.l.WithFields(logrus.Fields{ rm.l.Info("send CreateRelayResponse",
"relayFrom": resp.RelayFromAddr, "relayFrom", resp.RelayFromAddr,
"relayTo": resp.RelayToAddr, "relayTo", resp.RelayToAddr,
"initiatorRelayIndex": resp.InitiatorRelayIndex, "initiatorRelayIndex", resp.InitiatorRelayIndex,
"responderRelayIndex": resp.ResponderRelayIndex, "responderRelayIndex", resp.ResponderRelayIndex,
"vpnAddrs": peerHostInfo.vpnAddrs}). "vpnAddrs", peerHostInfo.vpnAddrs,
Info("send CreateRelayResponse") )
} }
} }
} }
@@ -232,17 +232,18 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
from := protoAddrToNetAddr(m.RelayFromAddr) from := protoAddrToNetAddr(m.RelayFromAddr)
target := protoAddrToNetAddr(m.RelayToAddr) target := protoAddrToNetAddr(m.RelayToAddr)
logMsg := rm.l.WithFields(logrus.Fields{ logMsg := rm.l.With(
"relayFrom": from, "relayFrom", from,
"relayTo": target, "relayTo", target,
"initiatorRelayIndex": m.InitiatorRelayIndex, "initiatorRelayIndex", m.InitiatorRelayIndex,
"vpnAddrs": h.vpnAddrs}) "vpnAddrs", h.vpnAddrs,
)
logMsg.Info("handleCreateRelayRequest") logMsg.Info("handleCreateRelayRequest")
// Is the source of the relay me? This should never happen, but did happen due to // Is the source of the relay me? This should never happen, but did happen due to
// an issue migrating relays over to newly re-handshaked host info objects. // an issue migrating relays over to newly re-handshaked host info objects.
if f.myVpnAddrsTable.Contains(from) { if f.myVpnAddrsTable.Contains(from) {
logMsg.WithField("myIP", from).Error("Discarding relay request from myself") logMsg.Error("Discarding relay request from myself", "myIP", from)
return return
} }
@@ -261,37 +262,37 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
if existingRelay.RemoteIndex != m.InitiatorRelayIndex { if existingRelay.RemoteIndex != m.InitiatorRelayIndex {
// We got a brand new Relay request, because its index is different than what we saw before. // We got a brand new Relay request, because its index is different than what we saw before.
// This should never happen. The peer should never change an index, once created. // This should never happen. The peer should never change an index, once created.
logMsg.WithFields(logrus.Fields{ logMsg.Error("Existing relay mismatch with CreateRelayRequest",
"existingRemoteIndex": existingRelay.RemoteIndex}).Error("Existing relay mismatch with CreateRelayRequest") "existingRemoteIndex", existingRelay.RemoteIndex)
return return
} }
case Disestablished: case Disestablished:
if existingRelay.RemoteIndex != m.InitiatorRelayIndex { if existingRelay.RemoteIndex != m.InitiatorRelayIndex {
// We got a brand new Relay request, because its index is different than what we saw before. // We got a brand new Relay request, because its index is different than what we saw before.
// This should never happen. The peer should never change an index, once created. // This should never happen. The peer should never change an index, once created.
logMsg.WithFields(logrus.Fields{ logMsg.Error("Existing relay mismatch with CreateRelayRequest",
"existingRemoteIndex": existingRelay.RemoteIndex}).Error("Existing relay mismatch with CreateRelayRequest") "existingRemoteIndex", existingRelay.RemoteIndex)
return return
} }
// Mark the relay as 'Established' because it's safe to use again // Mark the relay as 'Established' because it's safe to use again
h.relayState.UpdateRelayForByIpState(from, Established) h.relayState.UpdateRelayForByIpState(from, Established)
case PeerRequested: case PeerRequested:
// I should never be in this state, because I am terminal, not forwarding. // I should never be in this state, because I am terminal, not forwarding.
logMsg.WithFields(logrus.Fields{ logMsg.Error("Unexpected Relay State found",
"existingRemoteIndex": existingRelay.RemoteIndex, "existingRemoteIndex", existingRelay.RemoteIndex,
"state": existingRelay.State}).Error("Unexpected Relay State found") "state", existingRelay.State)
} }
} else { } else {
_, err := AddRelay(rm.l, h, f.hostMap, from, &m.InitiatorRelayIndex, TerminalType, Established) _, err := AddRelay(rm.l, h, f.hostMap, from, &m.InitiatorRelayIndex, TerminalType, Established)
if err != nil { if err != nil {
logMsg.WithError(err).Error("Failed to add relay") logMsg.Error("Failed to add relay", "error", err)
return return
} }
} }
relay, ok := h.relayState.QueryRelayForByIp(from) relay, ok := h.relayState.QueryRelayForByIp(from)
if !ok { if !ok {
logMsg.WithField("from", from).Error("Relay State not found") logMsg.Error("Relay State not found", "from", from)
return return
} }
@@ -313,17 +314,16 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
msg, err := resp.Marshal() msg, err := resp.Marshal()
if err != nil { if err != nil {
logMsg. logMsg.Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay", "error", err)
WithError(err).Error("relayManager Failed to marshal Control CreateRelayResponse message to create relay")
} else { } else {
f.SendMessageToHostInfo(header.Control, 0, h, msg, make([]byte, 12), make([]byte, mtu)) f.SendMessageToHostInfo(header.Control, 0, h, msg, make([]byte, 12), make([]byte, mtu))
rm.l.WithFields(logrus.Fields{ rm.l.Info("send CreateRelayResponse",
"relayFrom": from, "relayFrom", from,
"relayTo": target, "relayTo", target,
"initiatorRelayIndex": resp.InitiatorRelayIndex, "initiatorRelayIndex", resp.InitiatorRelayIndex,
"responderRelayIndex": resp.ResponderRelayIndex, "responderRelayIndex", resp.ResponderRelayIndex,
"vpnAddrs": h.vpnAddrs}). "vpnAddrs", h.vpnAddrs,
Info("send CreateRelayResponse") )
} }
return return
} else { } else {
@@ -363,12 +363,13 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
if v == cert.Version1 { if v == cert.Version1 {
if !h.vpnAddrs[0].Is4() { if !h.vpnAddrs[0].Is4() {
rm.l.WithField("relayFrom", h.vpnAddrs[0]). rm.l.Error("Refusing to CreateRelayRequest for a v1 relay with an ipv6 address",
WithField("relayTo", target). "relayFrom", h.vpnAddrs[0],
WithField("initiatorRelayIndex", req.InitiatorRelayIndex). "relayTo", target,
WithField("responderRelayIndex", req.ResponderRelayIndex). "initiatorRelayIndex", req.InitiatorRelayIndex,
WithField("vpnAddr", target). "responderRelayIndex", req.ResponderRelayIndex,
Error("Refusing to CreateRelayRequest for a v1 relay with an ipv6 address") "vpnAddr", target,
)
return return
} }
@@ -383,17 +384,16 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
msg, err := req.Marshal() msg, err := req.Marshal()
if err != nil { if err != nil {
logMsg. logMsg.Error("relayManager Failed to marshal Control message to create relay", "error", err)
WithError(err).Error("relayManager Failed to marshal Control message to create relay")
} else { } else {
f.SendMessageToHostInfo(header.Control, 0, peer, msg, make([]byte, 12), make([]byte, mtu)) f.SendMessageToHostInfo(header.Control, 0, peer, msg, make([]byte, 12), make([]byte, mtu))
rm.l.WithFields(logrus.Fields{ rm.l.Info("send CreateRelayRequest",
"relayFrom": h.vpnAddrs[0], "relayFrom", h.vpnAddrs[0],
"relayTo": target, "relayTo", target,
"initiatorRelayIndex": req.InitiatorRelayIndex, "initiatorRelayIndex", req.InitiatorRelayIndex,
"responderRelayIndex": req.ResponderRelayIndex, "responderRelayIndex", req.ResponderRelayIndex,
"vpnAddr": target}). "vpnAddr", target,
Info("send CreateRelayRequest") )
} }
// Also track the half-created Relay state just received // Also track the half-created Relay state just received
@@ -401,8 +401,7 @@ func (rm *relayManager) handleCreateRelayRequest(v cert.Version, h *HostInfo, f
if !ok { if !ok {
_, err := AddRelay(rm.l, h, f.hostMap, target, &m.InitiatorRelayIndex, ForwardingType, PeerRequested) _, err := AddRelay(rm.l, h, f.hostMap, target, &m.InitiatorRelayIndex, ForwardingType, PeerRequested)
if err != nil { if err != nil {
logMsg. logMsg.Error("relayManager Failed to allocate a local index for relay", "error", err)
WithError(err).Error("relayManager Failed to allocate a local index for relay")
return return
} }
} }
+12 -6
View File
@@ -2,6 +2,7 @@ package nebula
import ( import (
"context" "context"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
@@ -10,8 +11,6 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/sirupsen/logrus"
) )
// forEachFunc is used to benefit folks that want to do work inside the lock // forEachFunc is used to benefit folks that want to do work inside the lock
@@ -66,11 +65,11 @@ type hostnamesResults struct {
network string network string
lookupTimeout time.Duration lookupTimeout time.Duration
cancelFn func() cancelFn func()
l *logrus.Logger l *slog.Logger
ips atomic.Pointer[map[netip.AddrPort]struct{}] ips atomic.Pointer[map[netip.AddrPort]struct{}]
} }
func NewHostnameResults(ctx context.Context, l *logrus.Logger, d time.Duration, network string, timeout time.Duration, hostPorts []string, onUpdate func()) (*hostnamesResults, error) { func NewHostnameResults(ctx context.Context, l *slog.Logger, d time.Duration, network string, timeout time.Duration, hostPorts []string, onUpdate func()) (*hostnamesResults, error) {
r := &hostnamesResults{ r := &hostnamesResults{
hostnames: make([]hostnamePort, len(hostPorts)), hostnames: make([]hostnamePort, len(hostPorts)),
network: network, network: network,
@@ -121,7 +120,11 @@ func NewHostnameResults(ctx context.Context, l *logrus.Logger, d time.Duration,
addrs, err := net.DefaultResolver.LookupNetIP(timeoutCtx, r.network, hostPort.name) addrs, err := net.DefaultResolver.LookupNetIP(timeoutCtx, r.network, hostPort.name)
timeoutCancel() timeoutCancel()
if err != nil { if err != nil {
l.WithFields(logrus.Fields{"hostname": hostPort.name, "network": r.network}).WithError(err).Error("DNS resolution failed for static_map host") l.Error("DNS resolution failed for static_map host",
"hostname", hostPort.name,
"network", r.network,
"error", err,
)
continue continue
} }
for _, a := range addrs { for _, a := range addrs {
@@ -145,7 +148,10 @@ func NewHostnameResults(ctx context.Context, l *logrus.Logger, d time.Duration,
} }
} }
if different { if different {
l.WithFields(logrus.Fields{"origSet": origSet, "newSet": netipAddrs}).Info("DNS results changed for host list") l.Info("DNS results changed for host list",
"origSet", origSet,
"newSet", netipAddrs,
)
r.ips.Store(&netipAddrs) r.ips.Store(&netipAddrs)
onUpdate() onUpdate()
} }
+2 -3
View File
@@ -10,11 +10,11 @@ import (
"time" "time"
"dario.cat/mergo" "dario.cat/mergo"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula" "github.com/slackhq/nebula"
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test" "github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/logging"
"github.com/slackhq/nebula/overlay" "github.com/slackhq/nebula/overlay"
"go.yaml.in/yaml/v3" "go.yaml.in/yaml/v3"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -75,8 +75,7 @@ func newSimpleService(caCrt cert.Certificate, caKey []byte, name string, udpIp n
panic(err) panic(err)
} }
logger := logrus.New() logger := logging.NewLogger(os.Stdout)
logger.Out = os.Stdout
control, err := nebula.Main(&c, false, "custom-app", logger, overlay.NewUserDeviceFromConfig) control, err := nebula.Main(&c, false, "custom-app", logger, overlay.NewUserDeviceFromConfig)
if err != nil { if err != nil {
+51 -32
View File
@@ -6,21 +6,21 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"log/slog"
"maps" "maps"
"net" "net"
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/header" "github.com/slackhq/nebula/header"
"github.com/slackhq/nebula/logging"
"github.com/slackhq/nebula/sshd" "github.com/slackhq/nebula/sshd"
) )
@@ -57,12 +57,12 @@ type sshDeviceInfoFlags struct {
Pretty bool Pretty bool
} }
func wireSSHReload(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) { func wireSSHReload(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) {
c.RegisterReloadCallback(func(c *config.C) { c.RegisterReloadCallback(func(c *config.C) {
if c.GetBool("sshd.enabled", false) { if c.GetBool("sshd.enabled", false) {
sshRun, err := configSSH(l, ssh, c) sshRun, err := configSSH(l, ssh, c)
if err != nil { if err != nil {
l.WithError(err).Error("Failed to reconfigure the sshd") l.Error("Failed to reconfigure the sshd", "error", err)
ssh.Stop() ssh.Stop()
} }
if sshRun != nil { if sshRun != nil {
@@ -78,7 +78,7 @@ func wireSSHReload(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) {
// updates the passed-in SSHServer. On success, it returns a function // updates the passed-in SSHServer. On success, it returns a function
// that callers may invoke to run the configured ssh server. On // that callers may invoke to run the configured ssh server. On
// failure, it returns nil, error. // failure, it returns nil, error.
func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error) { func configSSH(l *slog.Logger, ssh *sshd.SSHServer, c *config.C) (func(), error) {
listen := c.GetString("sshd.listen", "") listen := c.GetString("sshd.listen", "")
if listen == "" { if listen == "" {
return nil, fmt.Errorf("sshd.listen must be provided") return nil, fmt.Errorf("sshd.listen must be provided")
@@ -120,7 +120,7 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro
for _, caAuthorizedKey := range rawCAs { for _, caAuthorizedKey := range rawCAs {
err := ssh.AddTrustedCA(caAuthorizedKey) err := ssh.AddTrustedCA(caAuthorizedKey)
if err != nil { if err != nil {
l.WithError(err).WithField("sshCA", caAuthorizedKey).Warn("SSH CA had an error, ignoring") l.Warn("SSH CA had an error, ignoring", "error", err, "sshCA", caAuthorizedKey)
continue continue
} }
} }
@@ -131,13 +131,13 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro
for _, rk := range keys { for _, rk := range keys {
kDef, ok := rk.(map[string]any) kDef, ok := rk.(map[string]any)
if !ok { if !ok {
l.WithField("sshKeyConfig", rk).Warn("Authorized user had an error, ignoring") l.Warn("Authorized user had an error, ignoring", "sshKeyConfig", rk)
continue continue
} }
user, ok := kDef["user"].(string) user, ok := kDef["user"].(string)
if !ok { if !ok {
l.WithField("sshKeyConfig", rk).Warn("Authorized user is missing the user field") l.Warn("Authorized user is missing the user field", "sshKeyConfig", rk)
continue continue
} }
@@ -146,7 +146,11 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro
case string: case string:
err := ssh.AddAuthorizedKey(user, v) err := ssh.AddAuthorizedKey(user, v)
if err != nil { if err != nil {
l.WithError(err).WithField("sshKeyConfig", rk).WithField("sshKey", v).Warn("Failed to authorize key") l.Warn("Failed to authorize key",
"error", err,
"sshKeyConfig", rk,
"sshKey", v,
)
continue continue
} }
@@ -154,19 +158,25 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro
for _, subK := range v { for _, subK := range v {
sk, ok := subK.(string) sk, ok := subK.(string)
if !ok { if !ok {
l.WithField("sshKeyConfig", rk).WithField("sshKey", subK).Warn("Did not understand ssh key") l.Warn("Did not understand ssh key",
"sshKeyConfig", rk,
"sshKey", subK,
)
continue continue
} }
err := ssh.AddAuthorizedKey(user, sk) err := ssh.AddAuthorizedKey(user, sk)
if err != nil { if err != nil {
l.WithError(err).WithField("sshKeyConfig", sk).Warn("Failed to authorize key") l.Warn("Failed to authorize key",
"error", err,
"sshKeyConfig", sk,
)
continue continue
} }
} }
default: default:
l.WithField("sshKeyConfig", rk).Warn("Authorized user is missing the keys field or was not understood") l.Warn("Authorized user is missing the keys field or was not understood", "sshKeyConfig", rk)
} }
} }
} else { } else {
@@ -178,7 +188,7 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro
ssh.Stop() ssh.Stop()
runner = func() { runner = func() {
if err := ssh.Run(listen); err != nil { if err := ssh.Run(listen); err != nil {
l.WithField("err", err).Warn("Failed to run the SSH server") l.Warn("Failed to run the SSH server", "error", err)
} }
} }
} else { } else {
@@ -188,7 +198,7 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro
return runner, nil return runner, nil
} }
func attachCommands(l *logrus.Logger, c *config.C, ssh *sshd.SSHServer, f *Interface) { func attachCommands(l *slog.Logger, c *config.C, ssh *sshd.SSHServer, f *Interface) {
// sandboxDir defaults to a dir in temp. The intention is that end user will // sandboxDir defaults to a dir in temp. The intention is that end user will
// create this dir as needed. Overriding this config value to "" allows // create this dir as needed. Overriding this config value to "" allows
// writing to anywhere in the system. // writing to anywhere in the system.
@@ -789,36 +799,45 @@ func sshGetMutexProfile(sandboxDir string, fs any, a []string, w sshd.StringWrit
return w.WriteLine(fmt.Sprintf("Mutex profile created at %s", a)) return w.WriteLine(fmt.Sprintf("Mutex profile created at %s", a))
} }
func sshLogLevel(l *logrus.Logger, fs any, a []string, w sshd.StringWriter) error { func sshLogLevel(l *slog.Logger, fs any, a []string, w sshd.StringWriter) error {
ctrl, ok := l.Handler().(interface {
GetLevel() slog.Level
SetLevel(slog.Level)
})
if !ok {
return w.WriteLine("Log level is not reconfigurable on this logger")
}
if len(a) == 0 { if len(a) == 0 {
return w.WriteLine(fmt.Sprintf("Log level is: %s", l.Level)) return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
} }
level, err := logrus.ParseLevel(a[0]) level, err := logging.ParseLevel(strings.ToLower(a[0]))
if err != nil { if err != nil {
return w.WriteLine(fmt.Sprintf("Unknown log level %s. Possible log levels: %s", a, logrus.AllLevels)) return w.WriteLine(fmt.Sprintf("Unknown log level %s. Possible log levels: trace, debug, info, warn, error", a))
} }
l.SetLevel(level) ctrl.SetLevel(level)
return w.WriteLine(fmt.Sprintf("Log level is: %s", l.Level)) return w.WriteLine(fmt.Sprintf("Log level is: %s", logging.LevelName(ctrl.GetLevel())))
} }
func sshLogFormat(l *logrus.Logger, fs any, a []string, w sshd.StringWriter) error { func sshLogFormat(l *slog.Logger, fs any, a []string, w sshd.StringWriter) error {
ctrl, ok := l.Handler().(interface {
GetFormat() string
SetFormat(string) error
})
if !ok {
return w.WriteLine("Log format is not reconfigurable on this logger")
}
if len(a) == 0 { if len(a) == 0 {
return w.WriteLine(fmt.Sprintf("Log format is: %s", reflect.TypeOf(l.Formatter))) return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
} }
logFormat := strings.ToLower(a[0]) if err := ctrl.SetFormat(strings.ToLower(a[0])); err != nil {
switch logFormat { return err
case "text":
l.Formatter = &logrus.TextFormatter{}
case "json":
l.Formatter = &logrus.JSONFormatter{}
default:
return fmt.Errorf("unknown log format `%s`. possible formats: %s", logFormat, []string{"text", "json"})
} }
return w.WriteLine(fmt.Sprintf("Log format is: %s", ctrl.GetFormat()))
return w.WriteLine(fmt.Sprintf("Log format is: %s", reflect.TypeOf(l.Formatter)))
} }
func sshPrintCert(ifce *Interface, fs any, a []string, w sshd.StringWriter) error { func sshPrintCert(ifce *Interface, fs any, a []string, w sshd.StringWriter) error {
+57 -51
View File
@@ -2,19 +2,19 @@ package sshd
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net" "net"
"sync"
"github.com/armon/go-radix" "github.com/armon/go-radix"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type SSHServer struct { type SSHServer struct {
config *ssh.ServerConfig config *ssh.ServerConfig
l *logrus.Entry l *slog.Logger
certChecker *ssh.CertChecker certChecker *ssh.CertChecker
@@ -27,20 +27,21 @@ type SSHServer struct {
commands *radix.Tree commands *radix.Tree
listener net.Listener listener net.Listener
// Locks the conns/counter to avoid concurrent map access // Call the cancel() function to stop all active sessions
connsLock sync.Mutex ctx context.Context
conns map[int]*session cancel func()
counter int
} }
// NewSSHServer creates a new ssh server rigged with default commands and prepares to listen // NewSSHServer creates a new ssh server rigged with default commands and prepares to listen
func NewSSHServer(l *logrus.Entry) (*SSHServer, error) { func NewSSHServer(l *slog.Logger) (*SSHServer, error) {
ctx, cancel := context.WithCancel(context.Background())
s := &SSHServer{ s := &SSHServer{
trustedKeys: make(map[string]map[string]bool), trustedKeys: make(map[string]map[string]bool),
l: l, l: l,
commands: radix.New(), commands: radix.New(),
conns: make(map[int]*session), ctx: ctx,
cancel: cancel,
} }
cc := ssh.CertChecker{ cc := ssh.CertChecker{
@@ -120,7 +121,7 @@ func (s *SSHServer) AddTrustedCA(pubKey string) error {
} }
s.trustedCAs = append(s.trustedCAs, pk) s.trustedCAs = append(s.trustedCAs, pk)
s.l.WithField("sshKey", pubKey).Info("Trusted CA key") s.l.Info("Trusted CA key", "sshKey", pubKey)
return nil return nil
} }
@@ -138,7 +139,10 @@ func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
} }
tk[string(pk.Marshal())] = true tk[string(pk.Marshal())] = true
s.l.WithField("sshKey", pubKey).WithField("sshUser", user).Info("Authorized ssh key") s.l.Info("Authorized ssh key",
"sshKey", pubKey,
"sshUser", user,
)
return nil return nil
} }
@@ -155,7 +159,7 @@ func (s *SSHServer) Run(addr string) error {
return err return err
} }
s.l.WithField("sshListener", addr).Info("SSH server is listening") s.l.Info("SSH server is listening", "sshListener", addr)
// Run loops until there is an error // Run loops until there is an error
s.run() s.run()
@@ -171,48 +175,54 @@ func (s *SSHServer) run() {
c, err := s.listener.Accept() c, err := s.listener.Accept()
if err != nil { if err != nil {
if !errors.Is(err, net.ErrClosed) { if !errors.Is(err, net.ErrClosed) {
s.l.WithError(err).Warn("Error in listener, shutting down") s.l.Warn("Error in listener, shutting down", "error", err)
} }
return return
} }
go func(c net.Conn) {
conn, chans, reqs, err := ssh.NewServerConn(c, s.config) // NewServerConn may block while waiting for the client to complete the handshake.
fp := "" // Ensure that a bad client doesn't hurt us by checking for the parent context
if conn != nil { // cancellation before calling NewServerConn, and forcing the socket to close when
fp = conn.Permissions.Extensions["fp"] // the context is cancelled.
} sessionContext, sessionCancel := context.WithCancel(s.ctx)
go func() {
if err != nil { <-sessionContext.Done()
l := s.l.WithError(err).WithField("remoteAddress", c.RemoteAddr()) c.Close()
}()
conn, chans, reqs, err := ssh.NewServerConn(c, s.config)
fp := ""
if conn != nil { if conn != nil {
l = l.WithField("sshUser", conn.User()) fp = conn.Permissions.Extensions["fp"]
conn.Close()
} }
if fp != "" {
l = l.WithField("sshFingerprint", fp) if err != nil {
l := s.l.With(
"error", err,
"remoteAddress", c.RemoteAddr(),
)
if conn != nil {
l = l.With("sshUser", conn.User())
conn.Close()
}
if fp != "" {
l = l.With("sshFingerprint", fp)
}
l.Warn("failed to handshake")
sessionCancel()
return
} }
l.Warn("failed to handshake")
continue
}
l := s.l.WithField("sshUser", conn.User()) l := s.l.With("sshUser", conn.User())
l.WithField("remoteAddress", c.RemoteAddr()).WithField("sshFingerprint", fp).Info("ssh user logged in") l.Info("ssh user logged in",
"remoteAddress", c.RemoteAddr(),
"sshFingerprint", fp,
)
session := NewSession(s.commands, conn, chans, l.WithField("subsystem", "sshd.session")) NewSession(s.commands, conn, chans, sessionCancel, l.With("subsystem", "sshd.session"))
s.connsLock.Lock()
s.counter++
counter := s.counter
s.conns[counter] = session
s.connsLock.Unlock()
go ssh.DiscardRequests(reqs) go ssh.DiscardRequests(reqs)
go func() {
<-session.exitChan }(c)
s.l.WithField("id", counter).Debug("closing conn")
s.connsLock.Lock()
delete(s.conns, counter)
s.connsLock.Unlock()
}()
} }
} }
@@ -220,15 +230,11 @@ func (s *SSHServer) Stop() {
// Close the listener, this will cause all session to terminate as well, see SSHServer.Run // Close the listener, this will cause all session to terminate as well, see SSHServer.Run
if s.listener != nil { if s.listener != nil {
if err := s.listener.Close(); err != nil { if err := s.listener.Close(); err != nil {
s.l.WithError(err).Warn("Failed to close the sshd listener") s.l.Warn("Failed to close the sshd listener", "error", err)
} }
} }
} }
func (s *SSHServer) closeSessions() { func (s *SSHServer) closeSessions() {
s.connsLock.Lock() s.cancel()
for _, c := range s.conns {
c.Close()
}
s.connsLock.Unlock()
} }
+13 -15
View File
@@ -2,30 +2,30 @@ package sshd
import ( import (
"fmt" "fmt"
"log/slog"
"sort" "sort"
"strings" "strings"
"github.com/anmitsu/go-shlex" "github.com/anmitsu/go-shlex"
"github.com/armon/go-radix" "github.com/armon/go-radix"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/term" "golang.org/x/term"
) )
type session struct { type session struct {
l *logrus.Entry l *slog.Logger
c *ssh.ServerConn c *ssh.ServerConn
term *term.Terminal term *term.Terminal
commands *radix.Tree commands *radix.Tree
exitChan chan bool cancel func()
} }
func NewSession(commands *radix.Tree, conn *ssh.ServerConn, chans <-chan ssh.NewChannel, l *logrus.Entry) *session { func NewSession(commands *radix.Tree, conn *ssh.ServerConn, chans <-chan ssh.NewChannel, cancel func(), l *slog.Logger) *session {
s := &session{ s := &session{
commands: radix.NewFromMap(commands.ToMap()), commands: radix.NewFromMap(commands.ToMap()),
l: l, l: l,
c: conn, c: conn,
exitChan: make(chan bool), cancel: cancel,
} }
s.commands.Insert("logout", &Command{ s.commands.Insert("logout", &Command{
@@ -42,16 +42,17 @@ func NewSession(commands *radix.Tree, conn *ssh.ServerConn, chans <-chan ssh.New
} }
func (s *session) handleChannels(chans <-chan ssh.NewChannel) { func (s *session) handleChannels(chans <-chan ssh.NewChannel) {
defer s.Close()
for newChannel := range chans { for newChannel := range chans {
if newChannel.ChannelType() != "session" { if newChannel.ChannelType() != "session" {
s.l.WithField("sshChannelType", newChannel.ChannelType()).Error("unknown channel type") s.l.Error("unknown channel type", "sshChannelType", newChannel.ChannelType())
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue continue
} }
channel, requests, err := newChannel.Accept() channel, requests, err := newChannel.Accept()
if err != nil { if err != nil {
s.l.WithError(err).Warn("could not accept channel") s.l.Warn("could not accept channel", "error", err)
continue continue
} }
@@ -94,13 +95,12 @@ func (s *session) handleRequests(in <-chan *ssh.Request, channel ssh.Channel) {
return return
default: default:
s.l.WithField("sshRequest", req.Type).Debug("Rejected unknown request") s.l.Debug("Rejected unknown request", "sshRequest", req.Type)
err = req.Reply(false, nil) err = req.Reply(false, nil)
} }
if err != nil { if err != nil {
s.l.WithError(err).Info("Error handling ssh session requests") s.l.Info("Error handling ssh session requests", "error", err)
s.Close()
return return
} }
} }
@@ -123,12 +123,11 @@ func (s *session) createTerm(channel ssh.Channel) *term.Terminal {
return "", 0, false return "", 0, false
} }
go s.handleInput(channel) go s.handleInput()
return term return term
} }
func (s *session) handleInput(channel ssh.Channel) { func (s *session) handleInput() {
defer s.Close()
w := &stringWriter{w: s.term} w := &stringWriter{w: s.term}
for { for {
line, err := s.term.ReadLine() line, err := s.term.ReadLine()
@@ -170,10 +169,9 @@ func (s *session) dispatchCommand(line string, w StringWriter) {
} }
_ = execCommand(c, args[1:], w) _ = execCommand(c, args[1:], w)
return
} }
func (s *session) Close() { func (s *session) Close() {
s.c.Close() s.c.Close()
s.exitChan <- true s.cancel()
} }
+328 -88
View File
@@ -1,13 +1,16 @@
package nebula package nebula
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"log" "log/slog"
"net" "net"
"net/http" "net/http"
"runtime" "runtime"
"strconv" "strconv"
"sync"
"sync/atomic"
"time" "time"
graphite "github.com/cyberdelia/go-metrics-graphite" graphite "github.com/cyberdelia/go-metrics-graphite"
@@ -15,113 +18,350 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
) )
// startStats initializes stats from config. On success, if any further work // statsServer owns nebula's stats subsystem: the periodic metric capture
// is needed to serve stats, it returns a func to handle that work. If no // goroutine and (for prometheus) an HTTP listener. It mirrors the lifecycle
// work is needed, it'll return nil. On failure, it returns nil, error. // shape of dnsServer: constructor wires the reload callback, reload records
func startStats(l *logrus.Logger, c *config.C, buildVersion string, configTest bool) (func(), error) { // config, Start builds and runs the runtime, Stop tears it down.
mType := c.GetString("stats.type", "") type statsServer struct {
if mType == "" || mType == "none" { l *slog.Logger
return nil, nil ctx context.Context
} buildVersion string
configTest bool
interval := c.GetDuration("stats.interval", 0) // enabled mirrors "stats configured to a real backend". Start consults
if interval == 0 { // it so callers don't need to know the gating rules.
return nil, fmt.Errorf("stats.interval was an invalid duration: %s", c.GetString("stats.interval", "")) enabled atomic.Bool
}
var startFn func() runMu sync.Mutex
switch mType { runCfg *statsConfig
case "graphite": run *statsRuntime // non-nil while a runtime is live
err := startGraphiteStats(l, interval, c, configTest)
if err != nil {
return nil, err
}
case "prometheus":
var err error
startFn, err = startPrometheusStats(l, interval, c, buildVersion, configTest)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("stats.type was not understood: %s", mType)
}
metrics.RegisterDebugGCStats(metrics.DefaultRegistry)
metrics.RegisterRuntimeMemStats(metrics.DefaultRegistry)
go metrics.CaptureDebugGCStats(metrics.DefaultRegistry, interval)
go metrics.CaptureRuntimeMemStats(metrics.DefaultRegistry, interval)
return startFn, nil
} }
func startGraphiteStats(l *logrus.Logger, i time.Duration, c *config.C, configTest bool) error { // statsRuntime is the live state owned by a single Start invocation. Start
proto := c.GetString("stats.protocol", "tcp") // stashes a pointer under runMu; Stop and Start's own exit path use pointer
host := c.GetString("stats.host", "") // equality to tell "my runtime" apart from one that replaced it after a
if host == "" { // reload.
return errors.New("stats.host can not be empty") type statsRuntime struct {
cancel context.CancelFunc
listener *http.Server // nil for graphite
}
// statsConfig is the snapshot of stats-related config that drives the runtime.
// It is comparable with == so reload can detect "no change" cheaply.
type statsConfig struct {
typ string
interval time.Duration
graphite graphiteConfig
prom promConfig
}
type graphiteConfig struct {
protocol string
host string
// resolvedAddr is the string form of host resolved at config-load time.
// Including it in the struct means a SIGHUP picks up DNS changes even
// when stats.host hasn't been edited.
resolvedAddr string
prefix string
}
type promConfig struct {
listen string
path string
namespace string
subsystem string
}
// newStatsServerFromConfig builds a statsServer, applies the initial config,
// and registers a reload callback. The reload callback is registered before
// the initial config is applied so a SIGHUP can later enable, fix, or disable
// stats even if the initial application failed.
//
// Start is safe to call unconditionally: it no-ops when stats are disabled.
// The returned pointer is always non-nil, even on error.
func newStatsServerFromConfig(ctx context.Context, l *slog.Logger, c *config.C, buildVersion string, configTest bool) (*statsServer, error) {
s := &statsServer{
l: l,
ctx: ctx,
buildVersion: buildVersion,
configTest: configTest,
} }
prefix := c.GetString("stats.prefix", "nebula") c.RegisterReloadCallback(func(c *config.C) {
addr, err := net.ResolveTCPAddr(proto, host) if err := s.reload(c, false); err != nil {
s.l.Error("Failed to reload stats from config", "error", err)
}
})
if err := s.reload(c, true); err != nil {
return s, err
}
return s, nil
}
// reload records the latest config. On the initial call it only records it;
// Control.Start is what launches the first runtime via statsStart. On later
// calls it reconciles the running runtime with the new config:
//
// - newly enabled -> spawn Start
// - newly disabled -> Stop the runtime
// - config changed (still enabled) -> Stop the old, Start the new
// - no change -> no-op
func (s *statsServer) reload(c *config.C, initial bool) error {
newCfg, err := loadStatsConfig(c)
if err != nil { if err != nil {
return fmt.Errorf("error while setting up graphite sink: %s", err) return err
}
enabled := newCfg.typ != "" && newCfg.typ != "none"
s.runMu.Lock()
sameCfg := s.runCfg != nil && *s.runCfg == newCfg
s.runCfg = &newCfg
running := s.run != nil
s.runMu.Unlock()
s.enabled.Store(enabled)
if initial || sameCfg {
return nil
} }
if !configTest { if running {
l.Infof("Starting graphite. Interval: %s, prefix: %s, addr: %s", i, prefix, addr) s.Stop()
go graphite.Graphite(metrics.DefaultRegistry, i, prefix, addr) }
if enabled && !s.configTest {
go s.Start()
} }
return nil return nil
} }
func startPrometheusStats(l *logrus.Logger, i time.Duration, c *config.C, buildVersion string, configTest bool) (func(), error) { // Start builds the runtime from the latest config, spawns the capture loop,
namespace := c.GetString("stats.namespace", "") // and blocks until Stop is called or ctx fires. For prometheus it also serves
subsystem := c.GetString("stats.subsystem", "") // the HTTP listener. For graphite it blocks on the capture loop's context.
// Safe to call when stats are disabled or already running (both no-op).
listen := c.GetString("stats.listen", "") func (s *statsServer) Start() {
if listen == "" { if !s.enabled.Load() || s.configTest {
return nil, fmt.Errorf("stats.listen should not be empty") return
} }
path := c.GetString("stats.path", "") s.runMu.Lock()
if path == "" { if s.ctx.Err() != nil || s.run != nil || s.runCfg == nil {
return nil, fmt.Errorf("stats.path should not be empty") s.runMu.Unlock()
return
}
cfg := *s.runCfg
captureFns, listener := s.buildRuntime(cfg)
runCtx, cancel := context.WithCancel(s.ctx)
rt := &statsRuntime{cancel: cancel, listener: listener}
s.run = rt
s.runMu.Unlock()
go captureStatsLoop(runCtx, cfg.interval, captureFns)
cleanExit := true
if listener == nil {
// Graphite: no HTTP listener to serve; block until teardown.
<-runCtx.Done()
} else {
cleanExit = s.serveListener(listener)
} }
pr := prometheus.NewRegistry() // Clear our runtime only if nothing has replaced it. Stop races through
pClient := mp.NewPrometheusProvider(metrics.DefaultRegistry, namespace, subsystem, pr, i) // here too but leaves s.run == nil, so the pointer check skips.
if !configTest { s.runMu.Lock()
go pClient.UpdatePrometheusMetrics() if s.run == rt {
} rt.cancel()
s.run = nil
// Export our version information as labels on a static gauge // A listener that exited with an error (e.g., bind conflict) leaves
g := prometheus.NewGauge(prometheus.GaugeOpts{ // runCfg cached as if it were applied. Drop it so a SIGHUP with the
Namespace: namespace, // same config re-triggers Start once the user fixes the underlying
Subsystem: subsystem, // problem.
Name: "info", if !cleanExit {
Help: "Version information for the Nebula binary", s.runCfg = nil
ConstLabels: prometheus.Labels{
"version": buildVersion,
"goversion": runtime.Version(),
"boringcrypto": strconv.FormatBool(boringEnabled()),
},
})
pr.MustRegister(g)
g.Set(1)
var startFn func()
if !configTest {
startFn = func() {
l.Infof("Prometheus stats listening on %s at %s", listen, path)
http.Handle(path, promhttp.HandlerFor(pr, promhttp.HandlerOpts{ErrorLog: l}))
log.Fatal(http.ListenAndServe(listen, nil))
} }
} }
s.runMu.Unlock()
return startFn, nil }
// serveListener runs ListenAndServe and ensures ctx cancellation unblocks it.
// Returns true if the listener exited cleanly (Stop, ctx cancellation, or any
// other http.ErrServerClosed path), false on an unexpected error.
func (s *statsServer) serveListener(listener *http.Server) bool {
// Per-invocation watcher: ctx cancellation triggers a listener shutdown
// which in turn unblocks ListenAndServe. Closing `done` on exit keeps
// the watcher from outliving this call.
done := make(chan struct{})
go func() {
select {
case <-s.ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := listener.Shutdown(shutdownCtx); err != nil {
s.l.Warn("Failed to shut down prometheus stats listener", "error", err)
}
case <-done:
}
}()
defer close(done)
s.l.Info("Starting prometheus stats listener", "addr", listener.Addr)
err := listener.ListenAndServe()
if err == nil || errors.Is(err, http.ErrServerClosed) {
return true
}
s.l.Error("Prometheus stats listener exited", "error", err)
return false
}
// Stop tears down the active runtime, if any. Idempotent.
func (s *statsServer) Stop() {
s.runMu.Lock()
rt := s.run
s.run = nil
s.runMu.Unlock()
if rt == nil {
return
}
rt.cancel()
if rt.listener != nil {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := rt.listener.Shutdown(shutdownCtx); err != nil {
s.l.Warn("Failed to shut down prometheus stats listener", "error", err)
}
cancel()
}
}
// buildRuntime produces the capture functions and, for prometheus, an un-served
// http.Server from cfg. cfg has already been validated by loadStatsConfig.
func (s *statsServer) buildRuntime(cfg statsConfig) ([]func(), *http.Server) {
// rcrowley/go-metrics guards these registrations with a private sync.Once,
// so subsequent reloads are no-ops.
metrics.RegisterDebugGCStats(metrics.DefaultRegistry)
metrics.RegisterRuntimeMemStats(metrics.DefaultRegistry)
captureFns := []func(){
func() { metrics.CaptureDebugGCStatsOnce(metrics.DefaultRegistry) },
func() { metrics.CaptureRuntimeMemStatsOnce(metrics.DefaultRegistry) },
}
switch cfg.typ {
case "graphite":
// loadStatsConfig already resolved and validated the address; re-parse
// the resolved form (no DNS lookup) to get a *net.TCPAddr.
addr, _ := net.ResolveTCPAddr(cfg.graphite.protocol, cfg.graphite.resolvedAddr)
gcfg := graphite.Config{
Addr: addr,
Registry: metrics.DefaultRegistry,
FlushInterval: cfg.interval,
DurationUnit: time.Nanosecond,
Prefix: cfg.graphite.prefix,
Percentiles: []float64{0.5, 0.75, 0.95, 0.99, 0.999},
}
captureFns = append(captureFns, func() {
if err := graphite.Once(gcfg); err != nil {
s.l.Error("Graphite export failed", "error", err)
}
})
s.l.Info("Starting graphite stats",
"interval", cfg.interval,
"prefix", cfg.graphite.prefix,
"addr", addr,
)
return captureFns, nil
case "prometheus":
pr := prometheus.NewRegistry()
pClient := mp.NewPrometheusProvider(metrics.DefaultRegistry, cfg.prom.namespace, cfg.prom.subsystem, pr, cfg.interval)
captureFns = append(captureFns, func() {
if err := pClient.UpdatePrometheusMetricsOnce(); err != nil {
s.l.Error("Prometheus metrics update failed", "error", err)
}
})
g := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: cfg.prom.namespace,
Subsystem: cfg.prom.subsystem,
Name: "info",
Help: "Version information for the Nebula binary",
ConstLabels: prometheus.Labels{
"version": s.buildVersion,
"goversion": runtime.Version(),
"boringcrypto": strconv.FormatBool(boringEnabled()),
},
})
pr.MustRegister(g)
g.Set(1)
// promhttp.HandlerOpts.ErrorLog needs a stdlib-shaped Println logger,
// so bridge our slog.Logger back to a *log.Logger that emits at Error.
errLog := slog.NewLogLogger(s.l.Handler(), slog.LevelError)
mux := http.NewServeMux()
mux.Handle(cfg.prom.path, promhttp.HandlerFor(pr, promhttp.HandlerOpts{ErrorLog: errLog}))
return captureFns, &http.Server{Addr: cfg.prom.listen, Handler: mux}
}
return captureFns, nil
}
// captureStatsLoop runs each fn on every tick of d until ctx is cancelled.
func captureStatsLoop(ctx context.Context, d time.Duration, fns []func()) {
t := time.NewTicker(d)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
for _, fn := range fns {
fn()
}
}
}
}
func loadStatsConfig(c *config.C) (statsConfig, error) {
cfg := statsConfig{
typ: c.GetString("stats.type", ""),
}
if cfg.typ == "" || cfg.typ == "none" {
return cfg, nil
}
cfg.interval = c.GetDuration("stats.interval", 0)
if cfg.interval == 0 {
return cfg, fmt.Errorf("stats.interval was an invalid duration: %s", c.GetString("stats.interval", ""))
}
switch cfg.typ {
case "graphite":
cfg.graphite.protocol = c.GetString("stats.protocol", "tcp")
cfg.graphite.host = c.GetString("stats.host", "")
if cfg.graphite.host == "" {
return cfg, errors.New("stats.host can not be empty")
}
addr, err := net.ResolveTCPAddr(cfg.graphite.protocol, cfg.graphite.host)
if err != nil {
return cfg, fmt.Errorf("error while setting up graphite sink: %s", err)
}
cfg.graphite.resolvedAddr = addr.String()
cfg.graphite.prefix = c.GetString("stats.prefix", "nebula")
case "prometheus":
cfg.prom.listen = c.GetString("stats.listen", "")
if cfg.prom.listen == "" {
return cfg, errors.New("stats.listen should not be empty")
}
cfg.prom.path = c.GetString("stats.path", "")
if cfg.prom.path == "" {
return cfg, errors.New("stats.path should not be empty")
}
cfg.prom.namespace = c.GetString("stats.namespace", "")
cfg.prom.subsystem = c.GetString("stats.subsystem", "")
default:
return cfg, fmt.Errorf("stats.type was not understood: %s", cfg.typ)
}
return cfg, nil
} }
+410
View File
@@ -0,0 +1,410 @@
package nebula
import (
"context"
"io"
"log/slog"
"net"
"strconv"
"testing"
"time"
"github.com/slackhq/nebula/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestStatsServer(t *testing.T) (*statsServer, *config.C) {
t.Helper()
l := slog.New(slog.DiscardHandler)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
return &statsServer{
l: l,
ctx: ctx,
}, config.NewC(l)
}
func setStatsConfig(c *config.C, m map[string]any) {
c.Settings["stats"] = m
}
func currentRuntime(s *statsServer) *statsRuntime {
s.runMu.Lock()
defer s.runMu.Unlock()
return s.run
}
func TestStatsServer_reload_initial_disabled(t *testing.T) {
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{"type": "none"})
require.NoError(t, s.reload(c, true))
assert.False(t, s.enabled.Load())
assert.Nil(t, currentRuntime(s))
}
func TestStatsServer_reload_initial_invalidInterval(t *testing.T) {
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "graphite",
"host": "127.0.0.1:0",
"prefix": "test",
})
err := s.reload(c, true)
require.Error(t, err)
assert.False(t, s.enabled.Load())
}
func TestStatsServer_reload_initial_unknownType(t *testing.T) {
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "carbon",
"interval": "1s",
})
err := s.reload(c, true)
require.Error(t, err)
assert.False(t, s.enabled.Load())
}
func TestStatsServer_reload_unchanged_noOp(t *testing.T) {
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{"type": "none"})
require.NoError(t, s.reload(c, true))
require.NoError(t, s.reload(c, false))
assert.False(t, s.enabled.Load())
}
func TestStatsServer_reload_initial_graphite(t *testing.T) {
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "graphite",
"interval": "1s",
"protocol": "tcp",
"host": "127.0.0.1:2003",
"prefix": "test",
})
require.NoError(t, s.reload(c, true))
assert.True(t, s.enabled.Load())
// reload only records config; Start builds the runtime.
assert.Nil(t, currentRuntime(s))
}
func TestStatsServer_reload_initial_prometheus(t *testing.T) {
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:0",
"path": "/metrics",
})
require.NoError(t, s.reload(c, true))
assert.True(t, s.enabled.Load())
// reload only records config; Start builds the runtime.
assert.Nil(t, currentRuntime(s))
}
func TestStatsServer_Start_graphite_blocksUntilStop(t *testing.T) {
sink := newGraphiteSink(t)
defer sink.Close()
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "graphite",
"interval": "1s",
"protocol": "tcp",
"host": sink.Addr(),
"prefix": "test",
})
require.NoError(t, s.reload(c, true))
done := make(chan struct{})
go func() {
s.Start()
close(done)
}()
// Wait for Start to publish runtime state.
waitFor(t, func() bool { return currentRuntime(s) != nil })
rt := currentRuntime(s)
require.NotNil(t, rt)
assert.Nil(t, rt.listener, "graphite has no listener")
s.Stop()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("graphite Start did not return after Stop")
}
assert.Nil(t, currentRuntime(s))
}
func TestStatsServer_StartStop_lifecycle(t *testing.T) {
port := freeTCPPort(t)
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:" + port,
"path": "/metrics",
})
require.NoError(t, s.reload(c, true))
done := make(chan struct{})
go func() {
s.Start()
close(done)
}()
waitForListening(t, "127.0.0.1:"+port)
rt := currentRuntime(s)
require.NotNil(t, rt)
require.NotNil(t, rt.listener)
s.Stop()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Start did not return after Stop")
}
assert.Nil(t, currentRuntime(s))
}
func TestStatsServer_reload_disable_stopsRunningRuntime(t *testing.T) {
port := freeTCPPort(t)
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:" + port,
"path": "/metrics",
})
require.NoError(t, s.reload(c, true))
done := make(chan struct{})
go func() {
s.Start()
close(done)
}()
waitForListening(t, "127.0.0.1:"+port)
setStatsConfig(c, map[string]any{"type": "none"})
require.NoError(t, s.reload(c, false))
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Start did not return after reload disabled stats")
}
assert.False(t, s.enabled.Load())
assert.Nil(t, currentRuntime(s))
}
func TestStatsServer_reload_changeListener_restartsListener(t *testing.T) {
port1 := freeTCPPort(t)
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:" + port1,
"path": "/metrics",
})
require.NoError(t, s.reload(c, true))
firstDone := make(chan struct{})
go func() {
s.Start()
close(firstDone)
}()
waitForListening(t, "127.0.0.1:"+port1)
first := currentRuntime(s)
require.NotNil(t, first)
port2 := freeTCPPort(t)
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:" + port2,
"path": "/metrics",
})
require.NoError(t, s.reload(c, false))
select {
case <-firstDone:
case <-time.After(5 * time.Second):
t.Fatal("old Start did not return after reload")
}
waitForListening(t, "127.0.0.1:"+port2)
second := currentRuntime(s)
require.NotNil(t, second)
assert.NotSame(t, first, second, "expected a new runtime after listen address change")
s.Stop()
}
func TestStatsServer_Stop_beforeStart_doesNotBlock(t *testing.T) {
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:0",
"path": "/metrics",
})
require.NoError(t, s.reload(c, true))
stopped := make(chan struct{})
go func() {
s.Stop()
close(stopped)
}()
select {
case <-stopped:
case <-time.After(time.Second):
t.Fatal("Stop hung with no runtime started")
}
}
func TestStatsServer_configTest_validatesWithoutSpawning(t *testing.T) {
s, c := newTestStatsServer(t)
s.configTest = true
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:0",
"path": "/metrics",
})
require.NoError(t, s.reload(c, true))
s.Start()
assert.Nil(t, currentRuntime(s))
}
func TestStatsServer_ctxCancel_unblocksStart(t *testing.T) {
// Ensures ctx cancellation alone (no explicit Stop) tears down both
// graphite and prom Start invocations.
port := freeTCPPort(t)
l := slog.New(slog.DiscardHandler)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s := &statsServer{l: l, ctx: ctx}
c := config.NewC(l)
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:" + port,
"path": "/metrics",
})
require.NoError(t, s.reload(c, true))
done := make(chan struct{})
go func() {
s.Start()
close(done)
}()
waitForListening(t, "127.0.0.1:"+port)
cancel()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Start did not return after ctx cancel")
}
}
func TestStatsServer_listenerBindFailure_sameCfgReloadRetries(t *testing.T) {
// Hold the port so ListenAndServe will fail on first Start.
blocker, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
port := strconv.Itoa(blocker.Addr().(*net.TCPAddr).Port)
s, c := newTestStatsServer(t)
setStatsConfig(c, map[string]any{
"type": "prometheus",
"interval": "1s",
"listen": "127.0.0.1:" + port,
"path": "/metrics",
})
require.NoError(t, s.reload(c, true))
done := make(chan struct{})
go func() {
s.Start()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Start did not return after bind failure")
}
// Bind failure should have dropped the cached config so a same-cfg
// SIGHUP can retry.
s.runMu.Lock()
cfgAfterFailure := s.runCfg
s.runMu.Unlock()
assert.Nil(t, cfgAfterFailure)
// Free the port and reload with the same config; Start should fire again.
require.NoError(t, blocker.Close())
require.NoError(t, s.reload(c, false))
waitForListening(t, "127.0.0.1:"+port)
require.NotNil(t, currentRuntime(s))
s.Stop()
}
func waitForListening(t *testing.T, addr string) {
t.Helper()
waitFor(t, func() bool {
conn, err := net.DialTimeout("tcp", addr, 200*time.Millisecond)
if err != nil {
return false
}
_ = conn.Close()
return true
})
}
// graphiteSink is a minimal TCP accept-and-discard server so graphite.Once
// calls in tests don't spam error logs or wedge on connection refused.
type graphiteSink struct {
ln net.Listener
}
func newGraphiteSink(t *testing.T) *graphiteSink {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
g := &graphiteSink{ln: ln}
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func(c net.Conn) {
_, _ = io.Copy(io.Discard, c)
_ = c.Close()
}(conn)
}
}()
return g
}
func (g *graphiteSink) Addr() string { return g.ln.Addr().String() }
func (g *graphiteSink) Close() { _ = g.ln.Close() }
func freeTCPPort(t *testing.T) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
port := ln.Addr().(*net.TCPAddr).Port
require.NoError(t, ln.Close())
return strconv.Itoa(port)
}
+56 -12
View File
@@ -1,29 +1,73 @@
package test package test
import ( import (
"context"
"io" "io"
"log/slog"
"os" "os"
"time"
"github.com/sirupsen/logrus" "github.com/slackhq/nebula/logging"
) )
func NewLogger() *logrus.Logger { // NewLogger returns a *slog.Logger suitable for use in tests. Output goes to
l := logrus.New() // io.Discard by default; set TEST_LOGS=1 (info), 2 (debug), or 3 (trace) to
// stream output to stderr for local debugging.
func NewLogger() *slog.Logger {
v := os.Getenv("TEST_LOGS") v := os.Getenv("TEST_LOGS")
if v == "" { if v == "" {
l.SetOutput(io.Discard) return slog.New(slog.DiscardHandler)
return l
} }
level := slog.LevelInfo
switch v { switch v {
case "2": case "2":
l.SetLevel(logrus.DebugLevel) level = slog.LevelDebug
case "3": case "3":
l.SetLevel(logrus.TraceLevel) level = logging.LevelTrace
default:
l.SetLevel(logrus.InfoLevel)
} }
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level}))
return l }
// NewLoggerWithOutput returns a *slog.Logger whose text output is captured by
// w. Timestamps are suppressed so tests can assert on exact output without
// baking the current time into expected strings.
func NewLoggerWithOutput(w io.Writer) *slog.Logger {
return slog.New(&stripTimeHandler{inner: slog.NewTextHandler(w, nil)})
}
// NewLoggerWithOutputAndLevel is NewLoggerWithOutput with an explicit level
// so tests can exercise Enabled-gated paths.
func NewLoggerWithOutputAndLevel(w io.Writer, level slog.Level) *slog.Logger {
return slog.New(&stripTimeHandler{inner: slog.NewTextHandler(w, &slog.HandlerOptions{Level: level})})
}
// NewJSONLoggerWithOutput returns a *slog.Logger emitting JSON to w with
// timestamps suppressed, for tests that pin the JSON shape.
func NewJSONLoggerWithOutput(w io.Writer, level slog.Level) *slog.Logger {
return slog.New(&stripTimeHandler{inner: slog.NewJSONHandler(w, &slog.HandlerOptions{Level: level})})
}
// stripTimeHandler zeros each record's time before delegating so slog's
// built-in handlers skip emitting the time attribute. Used to avoid
// timestamp-dependent assertions in tests without resorting to ReplaceAttr.
type stripTimeHandler struct {
inner slog.Handler
}
func (h *stripTimeHandler) Enabled(ctx context.Context, l slog.Level) bool {
return h.inner.Enabled(ctx, l)
}
func (h *stripTimeHandler) Handle(ctx context.Context, r slog.Record) error {
r.Time = time.Time{}
return h.inner.Handle(ctx, r)
}
func (h *stripTimeHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &stripTimeHandler{inner: h.inner.WithAttrs(attrs)}
}
func (h *stripTimeHandler) WithGroup(name string) slog.Handler {
return &stripTimeHandler{inner: h.inner.WithGroup(name)}
} }
-16
View File
@@ -35,16 +35,6 @@ type Conn interface {
// WriteTo loop. Returns on the first error; callers may observe a // WriteTo loop. Returns on the first error; callers may observe a
// partial send if some packets went out before the error. // partial send if some packets went out before the error.
WriteBatch(bufs [][]byte, addrs []netip.AddrPort) error WriteBatch(bufs [][]byte, addrs []netip.AddrPort) error
// WriteSegmented sends bufs as a single UDP GSO sendmsg when the kernel
// supports it: all bufs go to the same addr, each must be exactly segSize
// bytes except the last which may be shorter. The kernel emits one
// datagram per buf on the wire. Backends / kernels without GSO support
// fall back to a per-packet WriteTo loop. Returns on the first error.
WriteSegmented(bufs [][]byte, addr netip.AddrPort, segSize int) error
// SupportsGSO reports whether WriteSegmented takes the single-syscall
// GSO path. Callers use this to decide at batch-assembly time whether
// the uniform-size / same-dst check is worth running.
SupportsGSO() bool
ReloadConfig(c *config.C) ReloadConfig(c *config.C)
SupportsMultipleReaders() bool SupportsMultipleReaders() bool
Close() error Close() error
@@ -70,12 +60,6 @@ func (NoopConn) WriteTo(_ []byte, _ netip.AddrPort) error {
func (NoopConn) WriteBatch(_ [][]byte, _ []netip.AddrPort) error { func (NoopConn) WriteBatch(_ [][]byte, _ []netip.AddrPort) error {
return nil return nil
} }
func (NoopConn) WriteSegmented(_ [][]byte, _ netip.AddrPort, _ int) error {
return nil
}
func (NoopConn) SupportsGSO() bool {
return false
}
func (NoopConn) ReloadConfig(_ *config.C) { func (NoopConn) ReloadConfig(_ *config.C) {
return return
} }
+3 -2
View File
@@ -9,11 +9,12 @@ import (
"net/netip" "net/netip"
"syscall" "syscall"
"github.com/sirupsen/logrus" "log/slog"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func NewListener(l *logrus.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) {
return NewGenericListener(l, ip, port, multi, batch) return NewGenericListener(l, ip, port, multi, batch)
} }
+3 -2
View File
@@ -12,11 +12,12 @@ import (
"net/netip" "net/netip"
"syscall" "syscall"
"github.com/sirupsen/logrus" "log/slog"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func NewListener(l *logrus.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) {
return NewGenericListener(l, ip, port, multi, batch) return NewGenericListener(l, ip, port, multi, batch)
} }
+5 -16
View File
@@ -8,12 +8,12 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -22,12 +22,12 @@ type StdConn struct {
*net.UDPConn *net.UDPConn
isV4 bool isV4 bool
sysFd uintptr sysFd uintptr
l *logrus.Logger l *slog.Logger
} }
var _ Conn = &StdConn{} var _ Conn = &StdConn{}
func NewListener(l *logrus.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) {
lc := NewListenConfig(multi) lc := NewListenConfig(multi)
pc, err := lc.ListenPacket(context.TODO(), "udp", net.JoinHostPort(ip.String(), fmt.Sprintf("%v", port))) pc, err := lc.ListenPacket(context.TODO(), "udp", net.JoinHostPort(ip.String(), fmt.Sprintf("%v", port)))
if err != nil { if err != nil {
@@ -149,17 +149,6 @@ func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) error {
return nil return nil
} }
func (u *StdConn) WriteSegmented(bufs [][]byte, addr netip.AddrPort, _ int) error {
for _, b := range bufs {
if err := u.WriteTo(b, addr); err != nil {
return err
}
}
return nil
}
func (u *StdConn) SupportsGSO() bool { return false }
func (u *StdConn) LocalAddr() (netip.AddrPort, error) { func (u *StdConn) LocalAddr() (netip.AddrPort, error) {
a := u.UDPConn.LocalAddr() a := u.UDPConn.LocalAddr()
@@ -196,7 +185,7 @@ func (u *StdConn) ListenOut(r EncReader, flush func()) error {
return err return err
} }
u.l.WithError(err).Error("unexpected udp socket receive error") u.l.Error("unexpected udp socket receive error", "error", err)
} }
r(netip.AddrPortFrom(rua.Addr().Unmap(), rua.Port()), buffer[:n]) r(netip.AddrPortFrom(rua.Addr().Unmap(), rua.Port()), buffer[:n])
@@ -217,7 +206,7 @@ func (u *StdConn) Rebind() error {
} }
if err != nil { if err != nil {
u.l.WithError(err).Error("Failed to rebind udp socket") u.l.Error("Failed to rebind udp socket", "error", err)
} }
return nil return nil
+4 -15
View File
@@ -12,22 +12,22 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
) )
type GenericConn struct { type GenericConn struct {
*net.UDPConn *net.UDPConn
l *logrus.Logger l *slog.Logger
} }
var _ Conn = &GenericConn{} var _ Conn = &GenericConn{}
func NewGenericListener(l *logrus.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) { func NewGenericListener(l *slog.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) {
lc := NewListenConfig(multi) lc := NewListenConfig(multi)
pc, err := lc.ListenPacket(context.TODO(), "udp", net.JoinHostPort(ip.String(), fmt.Sprintf("%v", port))) pc, err := lc.ListenPacket(context.TODO(), "udp", net.JoinHostPort(ip.String(), fmt.Sprintf("%v", port)))
if err != nil { if err != nil {
@@ -53,17 +53,6 @@ func (u *GenericConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) error {
return nil return nil
} }
func (u *GenericConn) WriteSegmented(bufs [][]byte, addr netip.AddrPort, _ int) error {
for _, b := range bufs {
if _, err := u.UDPConn.WriteToUDPAddrPort(b, addr); err != nil {
return err
}
}
return nil
}
func (u *GenericConn) SupportsGSO() bool { return false }
func (u *GenericConn) LocalAddr() (netip.AddrPort, error) { func (u *GenericConn) LocalAddr() (netip.AddrPort, error) {
a := u.UDPConn.LocalAddr() a := u.UDPConn.LocalAddr()
@@ -108,7 +97,7 @@ func (u *GenericConn) ListenOut(r EncReader, flush func()) error {
// Dampen unexpected message warns to once per minute // Dampen unexpected message warns to once per minute
if lastRecvErr.IsZero() || time.Since(lastRecvErr) > time.Minute { if lastRecvErr.IsZero() || time.Since(lastRecvErr) > time.Minute {
lastRecvErr = time.Now() lastRecvErr = time.Now()
u.l.WithError(err).Warn("unexpected udp socket receive error") u.l.Warn("unexpected udp socket receive error", "error", err)
} }
continue continue
} }
+270 -182
View File
@@ -7,13 +7,13 @@ import (
"context" "context"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/rcrowley/go-metrics" "github.com/rcrowley/go-metrics"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@@ -22,7 +22,7 @@ type StdConn struct {
udpConn *net.UDPConn udpConn *net.UDPConn
rawConn syscall.RawConn rawConn syscall.RawConn
isV4 bool isV4 bool
l *logrus.Logger l *slog.Logger
batch int batch int
// sendmmsg scratch. Each queue has its own StdConn, so no locking is // sendmmsg scratch. Each queue has its own StdConn, so no locking is
@@ -32,6 +32,17 @@ type StdConn struct {
writeIovs []iovec writeIovs []iovec
writeNames [][]byte writeNames [][]byte
// Per-entry UDP_SEGMENT cmsg scratch. writeCmsg is one contiguous slab
// of MaxWriteBatch * writeCmsgSpace bytes; each entry's cmsg header is
// pre-filled once in prepareWriteMessages. WriteBatch only rewrites the
// 2-byte gso_size payload (and toggles Hdr.Control on/off) per call.
writeCmsg []byte
writeCmsgSpace int
// writeEntryEnd[e] is the bufs index *after* the last packet packed
// into mmsghdr entry e. Used to rewind `i` on partial sendmmsg success.
writeEntryEnd []int
// Preallocated closure + in/out slots for sendmmsg, so the hot path // Preallocated closure + in/out slots for sendmmsg, so the hot path
// does not heap-allocate a fresh closure per call. // does not heap-allocate a fresh closure per call.
writeChunk int writeChunk int
@@ -43,13 +54,13 @@ type StdConn struct {
// probed once at socket creation. When true, WriteSegmented takes a // probed once at socket creation. When true, WriteSegmented takes a
// single-syscall GSO path; otherwise it falls back to a WriteTo loop. // single-syscall GSO path; otherwise it falls back to a WriteTo loop.
gsoSupported bool gsoSupported bool
gsoMsg msghdr
gsoIovs []iovec // UDP GRO (recvmsg with UDP_GRO cmsg) support. groSupported is probed
gsoName []byte // SizeofSockaddrInet6 // once at socket creation. When true, listenOutBatch allocates larger
gsoCmsg []byte // CmsgSpace(2) // RX buffers and a per-entry cmsg slot so the kernel can coalesce
gsoSent int // consecutive same-flow datagrams into a single recvmmsg entry; the
gsoErrno syscall.Errno // delivered cmsg carries the gso_size used to split them back apart.
gsoFunc func(fd uintptr) bool groSupported bool
} }
func setReusePort(network, address string, c syscall.RawConn) error { func setReusePort(network, address string, c syscall.RawConn) error {
@@ -64,7 +75,7 @@ func setReusePort(network, address string, c syscall.RawConn) error {
return opErr return opErr
} }
func NewListener(l *logrus.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) {
listen := netip.AddrPortFrom(ip, uint16(port)) listen := netip.AddrPortFrom(ip, uint16(port))
lc := net.ListenConfig{} lc := net.ListenConfig{}
if multi { if multi {
@@ -100,10 +111,44 @@ func NewListener(l *logrus.Logger, ip netip.Addr, port int, multi bool, batch in
out.writeFunc = out.sendmmsgRawWrite out.writeFunc = out.sendmmsgRawWrite
out.prepareGSO() out.prepareGSO()
// GRO delivers coalesced superpackets that need a cmsg to split back
// into segments. The single-packet RX path uses ReadFromUDPAddrPort
// and cannot see that cmsg, so only enable GRO for the batch path.
if batch > 1 {
out.prepareGRO()
}
return out, nil return out, nil
} }
// prepareWriteMessages allocates one mmsghdr/iovec/sockaddr/cmsg scratch
// slot per sendmmsg entry. The iovec slab is sized to the same n so a
// single entry can fan out to up to n iovecs (needed for UDP_SEGMENT runs
// that coalesce consecutive bufs into one entry). Hdr.Iov / Hdr.Iovlen /
// Hdr.Control / Hdr.Controllen are wired per call since each entry can
// span a variable number of iovecs and may or may not carry a cmsg.
func (u *StdConn) prepareWriteMessages(n int) {
u.writeMsgs = make([]rawMessage, n)
u.writeIovs = make([]iovec, n)
u.writeNames = make([][]byte, n)
u.writeEntryEnd = make([]int, n)
u.writeCmsgSpace = unix.CmsgSpace(2)
u.writeCmsg = make([]byte, n*u.writeCmsgSpace)
for k := 0; k < n; k++ {
off := k * u.writeCmsgSpace
h := (*unix.Cmsghdr)(unsafe.Pointer(&u.writeCmsg[off]))
h.Level = unix.SOL_UDP
h.Type = unix.UDP_SEGMENT
setCmsgLen(h, unix.CmsgLen(2))
}
for i := range u.writeMsgs {
u.writeNames[i] = make([]byte, unix.SizeofSockaddrInet6)
u.writeMsgs[i].Hdr.Name = &u.writeNames[i][0]
}
}
// maxGSOSegments caps the per-sendmsg GSO fan-out. Linux kernels have // maxGSOSegments caps the per-sendmsg GSO fan-out. Linux kernels have
// historically capped UDP_MAX_SEGMENTS at 64; newer kernels raise it to 128 // historically capped UDP_MAX_SEGMENTS at 64; newer kernels raise it to 128
// but we stay conservative so the same code works everywhere. // but we stay conservative so the same code works everywhere.
@@ -116,9 +161,7 @@ const maxGSOSegments = 64
// fits, avoiding EMSGSIZE on large TSO superpackets. // fits, avoiding EMSGSIZE on large TSO superpackets.
const maxGSOBytes = 65535 const maxGSOBytes = 65535
// prepareGSO probes UDP_SEGMENT support and, on success, sets up the // prepareGSO probes UDP_SEGMENT support
// reusable sendmsg scratch (iovecs, sockaddr, cmsg) plus the preallocated
// raw-write closure used to avoid heap allocations on the hot path.
func (u *StdConn) prepareGSO() { func (u *StdConn) prepareGSO() {
var probeErr error var probeErr error
if err := u.rawConn.Control(func(fd uintptr) { if err := u.rawConn.Control(func(fd uintptr) {
@@ -130,25 +173,34 @@ func (u *StdConn) prepareGSO() {
return return
} }
u.gsoSupported = true u.gsoSupported = true
u.gsoIovs = make([]iovec, maxGSOSegments) }
u.gsoName = make([]byte, unix.SizeofSockaddrInet6)
u.gsoCmsg = make([]byte, unix.CmsgSpace(2))
// Wire up the static pieces of gsoMsg. Iovlen / Controllen / Namelen / // udpGROBufferSize sizes the per-entry recvmmsg buffer when UDP_GRO is on.
// cmsg contents get refreshed per call; Iov, Name, Control pointers are // The kernel stitches a run of same-flow datagrams into a single skb whose
// fixed because the scratch slices never move. // length is bounded by sk_gso_max_size (typically 65535); anything larger
u.gsoMsg.Iov = &u.gsoIovs[0] // would be MSG_TRUNCed. We use the maximum representable UDP length so a
u.gsoMsg.Name = &u.gsoName[0] // full superpacket always lands intact.
u.gsoMsg.Control = &u.gsoCmsg[0] const udpGROBufferSize = 65535
// Prepopulate the cmsg header. Len/Level/Type are constant for our use; // udpGROCmsgPayload is the size of the UDP_GRO cmsg data delivered by the
// only the 2-byte gso_size payload changes per call. // kernel: a single int (gso_size in bytes). See udp_cmsg_recv() in
cmsghdr := (*unix.Cmsghdr)(unsafe.Pointer(&u.gsoCmsg[0])) // net/ipv4/udp.c.
cmsghdr.Level = unix.SOL_UDP const udpGROCmsgPayload = 4
cmsghdr.Type = unix.UDP_SEGMENT
setCmsgLen(cmsghdr, unix.CmsgLen(2))
u.gsoFunc = u.sendmsgRawWriteGSO // prepareGRO turns on UDP_GRO so the kernel coalesces consecutive same-flow
// datagrams into one recvmmsg entry, with a cmsg carrying the gso_size used
// to split them back apart on the application side.
func (u *StdConn) prepareGRO() {
var probeErr error
if err := u.rawConn.Control(func(fd uintptr) {
probeErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
}); err != nil {
return
}
if probeErr != nil {
return
}
u.groSupported = true
} }
func (u *StdConn) SupportsMultipleReaders() bool { func (u *StdConn) SupportsMultipleReaders() bool {
@@ -271,7 +323,13 @@ func (u *StdConn) listenOutBatch(r EncReader, flush func()) error {
var n int var n int
var operr error var operr error
msgs, buffers, names := u.PrepareRawMessages(u.batch) bufSize := MTU
cmsgSpace := 0
if u.groSupported {
bufSize = udpGROBufferSize
cmsgSpace = unix.CmsgSpace(udpGROCmsgPayload)
}
msgs, buffers, names, _ := u.PrepareRawMessages(u.batch, bufSize, cmsgSpace)
//reader needs to capture variables from this function, since it's used as a lambda with rawConn.Read //reader needs to capture variables from this function, since it's used as a lambda with rawConn.Read
//defining it outside the loop so it gets re-used //defining it outside the loop so it gets re-used
@@ -281,6 +339,11 @@ func (u *StdConn) listenOutBatch(r EncReader, flush func()) error {
} }
for { for {
if cmsgSpace > 0 {
for i := range msgs {
setMsgControllen(&msgs[i].Hdr, cmsgSpace)
}
}
err := u.rawConn.Read(reader) err := u.rawConn.Read(reader)
if err != nil { if err != nil {
return err return err
@@ -296,7 +359,28 @@ func (u *StdConn) listenOutBatch(r EncReader, flush func()) error {
} else { } else {
ip, _ = netip.AddrFromSlice(names[i][8:24]) ip, _ = netip.AddrFromSlice(names[i][8:24])
} }
r(netip.AddrPortFrom(ip.Unmap(), binary.BigEndian.Uint16(names[i][2:4])), buffers[i][:msgs[i].Len]) from := netip.AddrPortFrom(ip.Unmap(), binary.BigEndian.Uint16(names[i][2:4]))
payload := buffers[i][:msgs[i].Len]
segSize := 0
if u.groSupported {
segSize = parseUDPGRO(&msgs[i].Hdr)
}
if segSize <= 0 || segSize >= len(payload) {
// No coalescing happened (or a lone datagram).
r(from, payload)
continue
}
// GRO superpacket: the kernel guarantees every segment is
// exactly segSize bytes except for the final one, which may be
// short.
for off := 0; off < len(payload); off += segSize {
end := off + segSize
if end > len(payload) {
end = len(payload)
}
r(from, payload[off:end])
}
} }
// End-of-batch: let callers (e.g. TUN write coalescer) flush any // End-of-batch: let callers (e.g. TUN write coalescer) flush any
// state they accumulated across this batch. // state they accumulated across this batch.
@@ -304,6 +388,38 @@ func (u *StdConn) listenOutBatch(r EncReader, flush func()) error {
} }
} }
// parseUDPGRO walks the control buffer on hdr looking for a SOL_UDP/UDP_GRO
// cmsg and returns the gso_size (bytes per coalesced segment) it carries.
// Returns 0 when no UDP_GRO cmsg is present, which is the normal case for
// lone datagrams that the kernel did not coalesce.
func parseUDPGRO(hdr *msghdr) int {
controllen := int(hdr.Controllen)
if controllen < unix.SizeofCmsghdr || hdr.Control == nil {
return 0
}
ctrl := unsafe.Slice(hdr.Control, controllen)
off := 0
for off+unix.SizeofCmsghdr <= len(ctrl) {
ch := (*unix.Cmsghdr)(unsafe.Pointer(&ctrl[off]))
clen := int(ch.Len)
if clen < unix.SizeofCmsghdr || off+clen > len(ctrl) {
return 0
}
if ch.Level == unix.SOL_UDP && ch.Type == unix.UDP_GRO {
dataOff := off + unix.CmsgLen(0)
if dataOff+udpGROCmsgPayload <= len(ctrl) {
return int(int32(binary.NativeEndian.Uint32(ctrl[dataOff : dataOff+udpGROCmsgPayload])))
}
return 0
}
// Advance by the aligned cmsg space. CmsgSpace(n) is the stride
// from one header to the next (len aligned up to the platform's
// cmsg alignment).
off += unix.CmsgSpace(clen - unix.CmsgLen(0))
}
return 0
}
func (u *StdConn) ListenOut(r EncReader, flush func()) error { func (u *StdConn) ListenOut(r EncReader, flush func()) error {
if u.batch == 1 { if u.batch == 1 {
return u.listenOutSingle(r, flush) return u.listenOutSingle(r, flush)
@@ -318,62 +434,143 @@ func (u *StdConn) WriteTo(b []byte, ip netip.AddrPort) error {
} }
// WriteBatch sends bufs via sendmmsg(2) using the preallocated scratch on // WriteBatch sends bufs via sendmmsg(2) using the preallocated scratch on
// StdConn. Chunks larger than the scratch are processed in multiple syscalls. // StdConn. Consecutive packets to the same destination with matching segment
// If sendmmsg returns a fatal error mid-chunk we fall back to single WriteTo // sizes (all but possibly the last) are coalesced into a single mmsghdr entry
// calls for the remainder so the caller still gets best-effort delivery. // carrying a UDP_SEGMENT cmsg, so one syscall can mix runs of GSO superpackets
// with plain one-off datagrams. Without GSO support every packet is its own
// entry, matching the prior behaviour.
//
// Chunks larger than the scratch are processed across multiple syscalls. If
// sendmmsg returns a fatal error before any entry is sent we fall back to
// per-packet WriteTo for that chunk so the caller still gets best-effort
// delivery.
func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) error { func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) error {
if len(bufs) != len(addrs) { if len(bufs) != len(addrs) {
return fmt.Errorf("WriteBatch: len(bufs)=%d != len(addrs)=%d", len(bufs), len(addrs)) return fmt.Errorf("WriteBatch: len(bufs)=%d != len(addrs)=%d", len(bufs), len(addrs))
} }
//u.l.WithField("bufs", len(bufs)).Info("WriteBatch")
i := 0 i := 0
for i < len(bufs) { for i < len(bufs) {
chunk := len(bufs) - i baseI := i
if chunk > len(u.writeMsgs) { entry := 0
chunk = len(u.writeMsgs) iovIdx := 0
}
for k := 0; k < chunk; k++ { for entry < len(u.writeMsgs) && i < len(bufs) {
b := bufs[i+k] iovBudget := len(u.writeIovs) - iovIdx
if len(b) == 0 { if iovBudget < 1 {
// sendmmsg with an empty iovec is legal but pointless; fall break
// through after filling the slot so Base is still valid.
u.writeIovs[k].Base = nil
setIovLen(&u.writeIovs[k], 0)
} else {
u.writeIovs[k].Base = &b[0]
setIovLen(&u.writeIovs[k], len(b))
} }
nlen, err := writeSockaddr(u.writeNames[k], addrs[i+k], u.isV4) runLen, segSize := u.planRun(bufs, addrs, i, iovBudget)
if runLen == 0 {
break
}
for k := 0; k < runLen; k++ {
b := bufs[i+k]
if len(b) == 0 {
u.writeIovs[iovIdx+k].Base = nil
setIovLen(&u.writeIovs[iovIdx+k], 0)
} else {
u.writeIovs[iovIdx+k].Base = &b[0]
setIovLen(&u.writeIovs[iovIdx+k], len(b))
}
}
nlen, err := writeSockaddr(u.writeNames[entry], addrs[i], u.isV4)
if err != nil { if err != nil {
return err return err
} }
u.writeMsgs[k].Hdr.Namelen = uint32(nlen)
hdr := &u.writeMsgs[entry].Hdr
hdr.Iov = &u.writeIovs[iovIdx]
setMsgIovlen(hdr, runLen)
hdr.Namelen = uint32(nlen)
if runLen >= 2 {
off := entry * u.writeCmsgSpace
dataOff := off + unix.CmsgLen(0)
binary.NativeEndian.PutUint16(u.writeCmsg[dataOff:dataOff+2], uint16(segSize))
hdr.Control = &u.writeCmsg[off]
setMsgControllen(hdr, u.writeCmsgSpace)
} else {
hdr.Control = nil
setMsgControllen(hdr, 0)
}
i += runLen
iovIdx += runLen
u.writeEntryEnd[entry] = i
entry++
} }
sent, serr := u.sendmmsg(chunk) if entry == 0 {
if serr != nil { return fmt.Errorf("sendmmsg: no progress")
if sent <= 0 { }
// nothing went out; fall back to WriteTo for this chunk.
for k := 0; k < chunk; k++ { sent, serr := u.sendmmsg(entry)
if err := u.WriteTo(bufs[i+k], addrs[i+k]); err != nil { if serr != nil && sent <= 0 {
return err // Nothing went out for this chunk; fall back to WriteTo for each
} // packet that was queued this iteration.
for k := baseI; k < i; k++ {
if werr := u.WriteTo(bufs[k], addrs[k]); werr != nil {
return werr
} }
i += chunk
continue
} }
// partial: treat as success for the sent packets and retry the continue
// remainder on the next outer-loop iteration.
} }
if sent == 0 { if sent == 0 {
return fmt.Errorf("sendmmsg made no progress") return fmt.Errorf("sendmmsg made no progress")
} }
i += sent // Rewind i to the end of the last successfully sent entry. For a
// full-success send this leaves i unchanged; for a partial send it
// replays the remainder on the next outer-loop iteration.
i = u.writeEntryEnd[sent-1]
} }
return nil return nil
} }
// planRun groups consecutive packets starting at `start` that can be sent as
// a single UDP GSO superpacket (one sendmmsg entry with UDP_SEGMENT cmsg).
// A run of length 1 means the entry carries no cmsg and the kernel treats
// it as a plain datagram. Returns the run length and the per-segment size
// (which equals len(bufs[start])). Without GSO support every call returns
// runLen=1.
func (u *StdConn) planRun(bufs [][]byte, addrs []netip.AddrPort, start, iovBudget int) (int, int) {
if start >= len(bufs) || iovBudget < 1 {
return 0, 0
}
segSize := len(bufs[start])
if !u.gsoSupported || segSize == 0 || segSize > maxGSOBytes {
return 1, segSize
}
dst := addrs[start]
maxLen := maxGSOSegments
if iovBudget < maxLen {
maxLen = iovBudget
}
runLen := 1
total := segSize
for runLen < maxLen && start+runLen < len(bufs) {
nextLen := len(bufs[start+runLen])
if nextLen == 0 || nextLen > segSize {
break
}
if addrs[start+runLen] != dst {
break
}
if total+nextLen > maxGSOBytes {
break
}
total += nextLen
runLen++
if nextLen < segSize {
// A short packet must be the last in the run.
break
}
}
return runLen, segSize
}
// sendmmsgRawWrite is the preallocated callback passed to rawConn.Write. It // sendmmsgRawWrite is the preallocated callback passed to rawConn.Write. It
// reads its input (u.writeChunk) and writes its outputs (u.writeSent, // reads its input (u.writeChunk) and writes its outputs (u.writeSent,
// u.writeErrno) through StdConn fields so the closure itself does not // u.writeErrno) through StdConn fields so the closure itself does not
@@ -396,115 +593,6 @@ func (u *StdConn) sendmmsgRawWrite(fd uintptr) bool {
return true return true
} }
func (u *StdConn) SupportsGSO() bool {
return u.gsoSupported
}
// WriteSegmented sends bufs to addr as a UDP GSO superpacket. The kernel
// emits one datagram per iovec on the wire; all iovecs except the last must
// be exactly segSize bytes. Non-GSO kernels hit the WriteTo fallback.
// Called with len(bufs) >= 1. len(bufs) > maxGSOSegments is chunked.
func (u *StdConn) WriteSegmented(bufs [][]byte, addr netip.AddrPort, segSize int) error {
if len(bufs) == 0 {
return nil
}
if !u.gsoSupported {
for _, b := range bufs {
if err := u.WriteTo(b, addr); err != nil {
return err
}
}
return nil
}
nlen, err := writeSockaddr(u.gsoName, addr, u.isV4)
if err != nil {
return err
}
u.gsoMsg.Namelen = uint32(nlen)
setMsgControllen(&u.gsoMsg, unix.CmsgSpace(2))
// Cap the per-syscall fan-out by both segment count and total bytes.
// Kernel rejects sendmsg with EMSGSIZE when segCount*segSize would
// exceed sk_gso_max_size (typically 65536). For segSize > maxGSOBytes
// we can't use GSO at all and must fall back per-packet.
segsByBytes := maxGSOBytes / segSize
if segsByBytes == 0 {
for _, b := range bufs {
if werr := u.WriteTo(b, addr); werr != nil {
return werr
}
}
return nil
}
maxChunk := maxGSOSegments
if segsByBytes < maxChunk {
maxChunk = segsByBytes
}
i := 0
for i < len(bufs) {
chunk := len(bufs) - i
if chunk > maxChunk {
chunk = maxChunk
}
for k := 0; k < chunk; k++ {
b := bufs[i+k]
if len(b) == 0 {
u.gsoIovs[k].Base = nil
setIovLen(&u.gsoIovs[k], 0)
} else {
u.gsoIovs[k].Base = &b[0]
setIovLen(&u.gsoIovs[k], len(b))
}
}
setMsgIovlen(&u.gsoMsg, chunk)
binary.NativeEndian.PutUint16(u.gsoCmsg[unix.CmsgLen(0):unix.CmsgLen(0)+2], uint16(segSize))
if serr := u.sendmsgGSO(); serr != nil {
// Fall back to a per-packet loop for the remainder of the
// batch. Dropping the GSO call entirely is safer than
// returning mid-superpacket and losing bytes.
for k := 0; k < chunk; k++ {
if werr := u.WriteTo(bufs[i+k], addr); werr != nil {
return werr
}
}
}
i += chunk
}
return nil
}
// sendmsgRawWriteGSO is the preallocated rawConn.Write callback for the GSO
// path. Reads the prebuilt u.gsoMsg and writes u.gsoSent / u.gsoErrno.
func (u *StdConn) sendmsgRawWriteGSO(fd uintptr) bool {
r1, _, errno := unix.Syscall(
unix.SYS_SENDMSG,
fd,
uintptr(unsafe.Pointer(&u.gsoMsg)),
0,
)
if errno == syscall.EAGAIN || errno == syscall.EWOULDBLOCK {
return false
}
u.gsoSent = int(r1)
u.gsoErrno = errno
return true
}
func (u *StdConn) sendmsgGSO() error {
u.gsoSent = 0
u.gsoErrno = 0
if err := u.rawConn.Write(u.gsoFunc); err != nil {
return err
}
if u.gsoErrno != 0 {
return &net.OpError{Op: "sendmsg", Err: u.gsoErrno}
}
return nil
}
func (u *StdConn) sendmmsg(n int) (int, error) { func (u *StdConn) sendmmsg(n int) (int, error) {
u.writeChunk = n u.writeChunk = n
u.writeSent = 0 u.writeSent = 0
@@ -555,12 +643,12 @@ func (u *StdConn) ReloadConfig(c *config.C) {
if err == nil { if err == nil {
s, err := u.GetRecvBuffer() s, err := u.GetRecvBuffer()
if err == nil { if err == nil {
u.l.WithField("size", s).Info("listen.read_buffer was set") u.l.Info("listen.read_buffer was set", "size", s)
} else { } else {
u.l.WithError(err).Warn("Failed to get listen.read_buffer") u.l.Warn("Failed to get listen.read_buffer", "error", err)
} }
} else { } else {
u.l.WithError(err).Error("Failed to set listen.read_buffer") u.l.Error("Failed to set listen.read_buffer", "error", err)
} }
} }
@@ -570,12 +658,12 @@ func (u *StdConn) ReloadConfig(c *config.C) {
if err == nil { if err == nil {
s, err := u.GetSendBuffer() s, err := u.GetSendBuffer()
if err == nil { if err == nil {
u.l.WithField("size", s).Info("listen.write_buffer was set") u.l.Info("listen.write_buffer was set", "size", s)
} else { } else {
u.l.WithError(err).Warn("Failed to get listen.write_buffer") u.l.Warn("Failed to get listen.write_buffer", "error", err)
} }
} else { } else {
u.l.WithError(err).Error("Failed to set listen.write_buffer") u.l.Error("Failed to set listen.write_buffer", "error", err)
} }
} }
@@ -586,12 +674,12 @@ func (u *StdConn) ReloadConfig(c *config.C) {
if err == nil { if err == nil {
s, err := u.GetSoMark() s, err := u.GetSoMark()
if err == nil { if err == nil {
u.l.WithField("mark", s).Info("listen.so_mark was set") u.l.Info("listen.so_mark was set", "mark", s)
} else { } else {
u.l.WithError(err).Warn("Failed to get listen.so_mark") u.l.Warn("Failed to get listen.so_mark", "error", err)
} }
} else { } else {
u.l.WithError(err).Error("Failed to set listen.so_mark") u.l.Error("Failed to set listen.so_mark", "error", err)
} }
} }
} }
+13 -19
View File
@@ -30,13 +30,18 @@ type rawMessage struct {
Len uint32 Len uint32
} }
func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte) { func (u *StdConn) PrepareRawMessages(n, bufSize, cmsgSpace int) ([]rawMessage, [][]byte, [][]byte, []byte) {
msgs := make([]rawMessage, n) msgs := make([]rawMessage, n)
buffers := make([][]byte, n) buffers := make([][]byte, n)
names := make([][]byte, n) names := make([][]byte, n)
var cmsgs []byte
if cmsgSpace > 0 {
cmsgs = make([]byte, n*cmsgSpace)
}
for i := range msgs { for i := range msgs {
buffers[i] = make([]byte, MTU) buffers[i] = make([]byte, bufSize)
names[i] = make([]byte, unix.SizeofSockaddrInet6) names[i] = make([]byte, unix.SizeofSockaddrInet6)
vs := []iovec{ vs := []iovec{
@@ -48,25 +53,14 @@ func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte) {
msgs[i].Hdr.Name = &names[i][0] msgs[i].Hdr.Name = &names[i][0]
msgs[i].Hdr.Namelen = uint32(len(names[i])) msgs[i].Hdr.Namelen = uint32(len(names[i]))
if cmsgSpace > 0 {
msgs[i].Hdr.Control = &cmsgs[i*cmsgSpace]
msgs[i].Hdr.Controllen = uint32(cmsgSpace)
}
} }
return msgs, buffers, names return msgs, buffers, names, cmsgs
}
// prepareWriteMessages allocates one Mmsghdr/iovec/sockaddr scratch per slot,
// wired up so each writeMsgs[i] already points at writeIovs[i] and
// writeNames[i]. Callers fill in the iovec Base/Len, the sockaddr bytes, and
// Namelen before each sendmmsg.
func (u *StdConn) prepareWriteMessages(n int) {
u.writeMsgs = make([]rawMessage, n)
u.writeIovs = make([]iovec, n)
u.writeNames = make([][]byte, n)
for i := range u.writeMsgs {
u.writeNames[i] = make([]byte, unix.SizeofSockaddrInet6)
u.writeMsgs[i].Hdr.Iov = &u.writeIovs[i]
u.writeMsgs[i].Hdr.Iovlen = 1
u.writeMsgs[i].Hdr.Name = &u.writeNames[i][0]
}
} }
func setIovLen(v *iovec, n int) { func setIovLen(v *iovec, n int) {
+13 -19
View File
@@ -33,13 +33,18 @@ type rawMessage struct {
Pad0 [4]byte Pad0 [4]byte
} }
func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte) { func (u *StdConn) PrepareRawMessages(n, bufSize, cmsgSpace int) ([]rawMessage, [][]byte, [][]byte, []byte) {
msgs := make([]rawMessage, n) msgs := make([]rawMessage, n)
buffers := make([][]byte, n) buffers := make([][]byte, n)
names := make([][]byte, n) names := make([][]byte, n)
var cmsgs []byte
if cmsgSpace > 0 {
cmsgs = make([]byte, n*cmsgSpace)
}
for i := range msgs { for i := range msgs {
buffers[i] = make([]byte, MTU) buffers[i] = make([]byte, bufSize)
names[i] = make([]byte, unix.SizeofSockaddrInet6) names[i] = make([]byte, unix.SizeofSockaddrInet6)
vs := []iovec{ vs := []iovec{
@@ -51,25 +56,14 @@ func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte) {
msgs[i].Hdr.Name = &names[i][0] msgs[i].Hdr.Name = &names[i][0]
msgs[i].Hdr.Namelen = uint32(len(names[i])) msgs[i].Hdr.Namelen = uint32(len(names[i]))
if cmsgSpace > 0 {
msgs[i].Hdr.Control = &cmsgs[i*cmsgSpace]
msgs[i].Hdr.Controllen = uint64(cmsgSpace)
}
} }
return msgs, buffers, names return msgs, buffers, names, cmsgs
}
// prepareWriteMessages allocates one Mmsghdr/iovec/sockaddr scratch per slot,
// wired up so each writeMsgs[i] already points at writeIovs[i] and
// writeNames[i]. Callers fill in the iovec Base/Len, the sockaddr bytes, and
// Namelen before each sendmmsg.
func (u *StdConn) prepareWriteMessages(n int) {
u.writeMsgs = make([]rawMessage, n)
u.writeIovs = make([]iovec, n)
u.writeNames = make([][]byte, n)
for i := range u.writeMsgs {
u.writeNames[i] = make([]byte, unix.SizeofSockaddrInet6)
u.writeMsgs[i].Hdr.Iov = &u.writeIovs[i]
u.writeMsgs[i].Hdr.Iovlen = 1
u.writeMsgs[i].Hdr.Name = &u.writeNames[i][0]
}
} }
func setIovLen(v *iovec, n int) { func setIovLen(v *iovec, n int) {
+3 -2
View File
@@ -11,11 +11,12 @@ import (
"net/netip" "net/netip"
"syscall" "syscall"
"github.com/sirupsen/logrus" "log/slog"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func NewListener(l *logrus.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) {
return NewGenericListener(l, ip, port, multi, batch) return NewGenericListener(l, ip, port, multi, batch)
} }
+7 -18
View File
@@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"net" "net"
"net/netip" "net/netip"
"sync" "sync"
@@ -17,7 +18,6 @@ import (
"time" "time"
"unsafe" "unsafe"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/conn/winrio" "golang.zx2c4.com/wireguard/conn/winrio"
@@ -53,14 +53,14 @@ type ringBuffer struct {
type RIOConn struct { type RIOConn struct {
isOpen atomic.Bool isOpen atomic.Bool
l *logrus.Logger l *slog.Logger
sock windows.Handle sock windows.Handle
rx, tx ringBuffer rx, tx ringBuffer
rq winrio.Rq rq winrio.Rq
results [packetsPerRing]winrio.Result results [packetsPerRing]winrio.Result
} }
func NewRIOListener(l *logrus.Logger, addr netip.Addr, port int) (*RIOConn, error) { func NewRIOListener(l *slog.Logger, addr netip.Addr, port int) (*RIOConn, error) {
if !winrio.Initialize() { if !winrio.Initialize() {
return nil, errors.New("could not initialize winrio") return nil, errors.New("could not initialize winrio")
} }
@@ -83,7 +83,7 @@ func NewRIOListener(l *logrus.Logger, addr netip.Addr, port int) (*RIOConn, erro
return u, nil return u, nil
} }
func (u *RIOConn) bind(l *logrus.Logger, sa windows.Sockaddr) error { func (u *RIOConn) bind(l *slog.Logger, sa windows.Sockaddr) error {
var err error var err error
u.sock, err = winrio.Socket(windows.AF_INET6, windows.SOCK_DGRAM, windows.IPPROTO_UDP) u.sock, err = winrio.Socket(windows.AF_INET6, windows.SOCK_DGRAM, windows.IPPROTO_UDP)
if err != nil { if err != nil {
@@ -103,7 +103,7 @@ func (u *RIOConn) bind(l *logrus.Logger, sa windows.Sockaddr) error {
if err != nil { if err != nil {
// This is a best-effort to prevent errors from being returned by the udp recv operation. // This is a best-effort to prevent errors from being returned by the udp recv operation.
// Quietly log a failure and continue. // Quietly log a failure and continue.
l.WithError(err).Debug("failed to set UDP_CONNRESET ioctl") l.Debug("failed to set UDP_CONNRESET ioctl", "error", err)
} }
ret = 0 ret = 0
@@ -114,7 +114,7 @@ func (u *RIOConn) bind(l *logrus.Logger, sa windows.Sockaddr) error {
if err != nil { if err != nil {
// This is a best-effort to prevent errors from being returned by the udp recv operation. // This is a best-effort to prevent errors from being returned by the udp recv operation.
// Quietly log a failure and continue. // Quietly log a failure and continue.
l.WithError(err).Debug("failed to set UDP_NETRESET ioctl") l.Debug("failed to set UDP_NETRESET ioctl", "error", err)
} }
err = u.rx.Open() err = u.rx.Open()
@@ -156,7 +156,7 @@ func (u *RIOConn) ListenOut(r EncReader, flush func()) error {
// Dampen unexpected message warns to once per minute // Dampen unexpected message warns to once per minute
if lastRecvErr.IsZero() || time.Since(lastRecvErr) > time.Minute { if lastRecvErr.IsZero() || time.Since(lastRecvErr) > time.Minute {
lastRecvErr = time.Now() lastRecvErr = time.Now()
u.l.WithError(err).Warn("unexpected udp socket receive error") u.l.Warn("unexpected udp socket receive error", "error", err)
} }
continue continue
} }
@@ -326,17 +326,6 @@ func (u *RIOConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) error {
return nil return nil
} }
func (u *RIOConn) WriteSegmented(bufs [][]byte, addr netip.AddrPort, _ int) error {
for _, b := range bufs {
if err := u.WriteTo(b, addr); err != nil {
return err
}
}
return nil
}
func (u *RIOConn) SupportsGSO() bool { return false }
func (u *RIOConn) LocalAddr() (netip.AddrPort, error) { func (u *RIOConn) LocalAddr() (netip.AddrPort, error) {
sa, err := windows.Getsockname(u.sock) sa, err := windows.Getsockname(u.sock)
if err != nil { if err != nil {

Some files were not shown because too many files have changed in this diff Show More