package overlay import ( "context" "fmt" "io" "log/slog" "net/netip" "strings" "github.com/rcrowley/go-metrics" "github.com/slackhq/nebula/iputil" "github.com/slackhq/nebula/overlay/tio" "github.com/slackhq/nebula/routing" ) type disabledTun struct { read chan []byte vpnNetworks []netip.Prefix // Track these metrics since we don't have the tun device to do it for us tx metrics.Counter rx metrics.Counter l *slog.Logger numReaders int } // disabledQueue is one tio.Queue view onto a shared disabledTun. Each queue // owns a private batchRet so concurrent Read calls from different reader // goroutines do not race on the returned slice. type disabledQueue struct { parent *disabledTun batchRet [1]tio.Packet } func (q *disabledQueue) Capabilities() tio.Capabilities { return tio.Capabilities{} } func (q *disabledQueue) Read() ([]tio.Packet, error) { r, ok := <-q.parent.read if !ok { return nil, io.EOF } q.parent.tx.Inc(1) if q.parent.l.Enabled(context.Background(), slog.LevelDebug) { q.parent.l.Debug("Write payload", "raw", prettyPacket(r)) } q.batchRet[0] = tio.Packet{Bytes: r} return q.batchRet[:], nil } // Write on a queue forwards to the underlying disabledTun. All queues share // one ICMP-handling/log path so this is a thin pass-through. func (q *disabledQueue) Write(b []byte) (int, error) { return q.parent.Write(b) } // Close on a queue is a no-op. The shared channel and metrics are owned by // the disabledTun; Close on the device tears them down once for everybody. func (q *disabledQueue) Close() error { return nil } func newDisabledTun(vpnNetworks []netip.Prefix, queueLen int, metricsEnabled bool, l *slog.Logger) *disabledTun { tun := &disabledTun{ vpnNetworks: vpnNetworks, read: make(chan []byte, queueLen), l: l, numReaders: 1, } if metricsEnabled { tun.tx = metrics.GetOrRegisterCounter("messages.tx.message", nil) tun.rx = metrics.GetOrRegisterCounter("messages.rx.message", nil) } else { tun.tx = &metrics.NilCounter{} tun.rx = &metrics.NilCounter{} } return tun } func (*disabledTun) Activate() error { return nil } func (*disabledTun) RoutesFor(addr netip.Addr) routing.Gateways { return routing.Gateways{} } func (t *disabledTun) Networks() []netip.Prefix { return t.vpnNetworks } func (*disabledTun) Name() string { return "disabled" } func (t *disabledTun) handleICMPEchoRequest(b []byte) bool { out := make([]byte, len(b)) out = iputil.CreateICMPEchoResponse(b, out) if out == nil { return false } // attempt to write it, but don't block select { case t.read <- out: default: t.l.Debug("tun_disabled: dropped ICMP Echo Reply response") } return true } func (t *disabledTun) Write(b []byte) (int, error) { t.rx.Inc(1) // Check for ICMP Echo Request before spending time doing the full parsing if t.handleICMPEchoRequest(b) { if t.l.Enabled(context.Background(), slog.LevelDebug) { t.l.Debug("Disabled tun responded to ICMP Echo Request", "raw", prettyPacket(b)) } } else if t.l.Enabled(context.Background(), slog.LevelDebug) { t.l.Debug("Disabled tun received unexpected payload", "raw", prettyPacket(b)) } return len(b), nil } func (t *disabledTun) SupportsMultiqueue() bool { return true } func (t *disabledTun) NewMultiQueueReader() error { t.numReaders++ return nil } func (t *disabledTun) Readers() []tio.Queue { out := make([]tio.Queue, t.numReaders) for i := range t.numReaders { out[i] = &disabledQueue{parent: t} } return out } func (t *disabledTun) Close() error { if t.read != nil { close(t.read) t.read = nil } return nil } type prettyPacket []byte func (p prettyPacket) String() string { var s strings.Builder for i, b := range p { if i > 0 && i%8 == 0 { s.WriteString(" ") } s.WriteString(fmt.Sprintf("%02x ", b)) } return s.String() }