Skip to content

Commit 459cfc6

Browse files
committed
warn on uselessly low MTU
1 parent 8673386 commit 459cfc6

4 files changed

Lines changed: 246 additions & 7 deletions

File tree

examples/config.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ lighthouse:
110110
#- "1.1.1.1:4242"
111111
#- "1.2.3.4:0" # port will be replaced with the real listening port
112112

113+
# Locally discovered addresses are checked against the MTU of the link they were found on. If the link cannot fit
114+
# a full-size packet from the nebula tun device (`tun.mtu` plus encapsulation overhead, which is larger for relayed
115+
# traffic) without fragmenting, a warning is logged.
116+
# When omit_low_mtu_addrs is true, addresses whose links cannot fit normal nebula traffic are dropped from
117+
# lighthouse reports entirely.
118+
# Addresses that can fit normal nebula traffic but not relayed traffic are always still advertised.
119+
# This does not apply to addresses listed in advertise_addrs.
120+
#omit_low_mtu_addrs: false
121+
113122
# EXPERIMENTAL: This option may change or disappear in the future.
114123
# This setting allows us to "guess" what the remote might be for a host
115124
# while we wait for the lighthouse response.

hostmap.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -869,9 +869,17 @@ func (i *HostInfo) logger(l *slog.Logger) *slog.Logger {
869869

870870
// Utility functions
871871

872-
func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
872+
// localAddr is a locally discovered address candidate for lighthouse
873+
// advertisement, along with details about the link it was found on.
874+
type localAddr struct {
875+
addr netip.Addr
876+
ifName string
877+
linkMTU int // MTU reported for the link, or <= 0 if unknown
878+
}
879+
880+
func localAddrs(l *slog.Logger, allowList *LocalAllowList) []localAddr {
873881
//FIXME: This function is pretty garbage
874-
var finalAddrs []netip.Addr
882+
var finalAddrs []localAddr
875883
ifaces, _ := net.Interfaces()
876884
for _, i := range ifaces {
877885
allow := allowList.AllowName(i.Name)
@@ -916,7 +924,7 @@ func localAddrs(l *slog.Logger, allowList *LocalAllowList) []netip.Addr {
916924
continue
917925
}
918926

919-
finalAddrs = append(finalAddrs, addr)
927+
finalAddrs = append(finalAddrs, localAddr{addr: addr, ifName: i.Name, linkMTU: i.MTU})
920928
}
921929
}
922930
}

lighthouse.go

Lines changed: 143 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import (
1919
"github.com/slackhq/nebula/config"
2020
"github.com/slackhq/nebula/header"
2121
"github.com/slackhq/nebula/logging"
22+
"github.com/slackhq/nebula/overlay"
2223
"github.com/slackhq/nebula/udp"
2324
"github.com/slackhq/nebula/util"
25+
"golang.org/x/net/ipv4"
26+
"golang.org/x/net/ipv6"
2427
)
2528

2629
var ErrHostNotKnown = errors.New("host not known")
@@ -65,6 +68,18 @@ type LightHouse struct {
6568

6669
advertiseAddrs atomic.Pointer[[]netip.AddrPort]
6770

71+
// tunMTU mirrors tun.mtu so locally discovered addrs can be checked for
72+
// links too small to carry a full-size nebula packet without fragmenting.
73+
tunMTU atomic.Int64
74+
// omitLowMTUAddrs drops such addrs from lighthouse updates (and demotes
75+
// the associated warnings to debug logs) instead of advertising them.
76+
omitLowMTUAddrs atomic.Bool
77+
// mtuWarned tracks the last classification logged per local addr so a
78+
// warning is only emitted when the classification changes, not on every
79+
// periodic update.
80+
mtuWarnLock sync.Mutex
81+
mtuWarned map[mtuWarnKey]linkMTUTier
82+
6883
// Addr's of relays that can be used by peers to access me
6984
relaysForMe atomic.Pointer[[]netip.Addr]
7085

@@ -105,6 +120,7 @@ func NewLightHouseFromConfig(ctx context.Context, l *slog.Logger, c *config.C, c
105120
punchy: p,
106121
updateTrigger: make(chan struct{}, 1),
107122
queryChan: make(chan netip.Addr, c.GetUint32("handshakes.query_buffer", 64)),
123+
mtuWarned: make(map[mtuWarnKey]linkMTUTier),
108124
l: l,
109125
}
110126
lighthouses := make([]netip.Addr, 0)
@@ -216,6 +232,23 @@ func (lh *LightHouse) reload(c *config.C, initial bool) error {
216232
}
217233
}
218234

235+
if initial || c.HasChanged("tun.mtu") || c.HasChanged("lighthouse.omit_low_mtu_addrs") {
236+
lh.tunMTU.Store(int64(c.GetInt("tun.mtu", overlay.DefaultMTU)))
237+
lh.omitLowMTUAddrs.Store(c.GetBool("lighthouse.omit_low_mtu_addrs", false))
238+
239+
// Re-log any addrs whose links are still too small under the new values
240+
lh.mtuWarnLock.Lock()
241+
clear(lh.mtuWarned)
242+
lh.mtuWarnLock.Unlock()
243+
244+
if !initial {
245+
lh.l.Info("tun.mtu and/or lighthouse.omit_low_mtu_addrs has changed",
246+
"tunMTU", lh.tunMTU.Load(),
247+
"omitLowMTUAddrs", lh.omitLowMTUAddrs.Load(),
248+
)
249+
}
250+
}
251+
219252
if initial || c.HasChanged("lighthouse.interval") {
220253
lh.interval.Store(int64(c.GetInt("lighthouse.interval", 10)))
221254

@@ -905,6 +938,108 @@ func (lh *LightHouse) TriggerUpdate() {
905938
}
906939
}
907940

941+
// linkMTUTier classifies how well a local addr's link MTU can carry
942+
// full-size nebula packets built from a tun packet of tun.mtu bytes.
943+
type linkMTUTier uint8
944+
945+
// mtuWarnKey identifies a local addr for MTU warning dedup purposes. The
946+
// interface name is included because the same addr can exist on multiple
947+
// links with different MTUs.
948+
type mtuWarnKey struct {
949+
ifName string
950+
addr netip.Addr
951+
}
952+
953+
const (
954+
// The link can carry both normal and relayed nebula traffic
955+
linkMTUOk linkMTUTier = iota
956+
// The link can carry normal nebula traffic, but relayed traffic (which
957+
// adds a second layer of encapsulation) will not fit
958+
linkMTUTooSmallForRelay
959+
// Even normal nebula traffic will not fit
960+
linkMTUTooSmall
961+
)
962+
963+
const (
964+
// Both AES-256-GCM and ChaCha20-Poly1305 append a 16 byte AEAD tag
965+
cipherTagLen = 16
966+
udpHeaderLen = 8
967+
)
968+
969+
// requiredLinkMTU returns the minimum underlay link MTU that can carry a
970+
// full-size tun packet to an addr of the given family without fragmentation,
971+
// both directly and via a relay (which wraps the packet in a second nebula
972+
// header and AEAD tag).
973+
func requiredLinkMTU(tunMTU int, is4 bool) (direct, relayed int) {
974+
ipHeaderLen := ipv6.HeaderLen
975+
if is4 {
976+
ipHeaderLen = ipv4.HeaderLen
977+
}
978+
979+
direct = tunMTU + header.Len + cipherTagLen + udpHeaderLen + ipHeaderLen
980+
relayed = direct + header.Len + cipherTagLen
981+
return direct, relayed
982+
}
983+
984+
// checkLocalLinkMTU classifies e's link MTU, logs when the classification
985+
// changes, and reports whether e should be advertised to lighthouses.
986+
func (lh *LightHouse) checkLocalLinkMTU(e localAddr) bool {
987+
tunMTU := int(lh.tunMTU.Load())
988+
omit := lh.omitLowMTUAddrs.Load()
989+
990+
tier := linkMTUOk
991+
direct, relayed := requiredLinkMTU(tunMTU, e.addr.Is4())
992+
if e.linkMTU > 0 { // links with an unknown MTU are advertised as-is
993+
if e.linkMTU < direct {
994+
tier = linkMTUTooSmall
995+
} else if e.linkMTU < relayed {
996+
tier = linkMTUTooSmallForRelay
997+
}
998+
}
999+
advertise := tier != linkMTUTooSmall || !omit
1000+
1001+
key := mtuWarnKey{ifName: e.ifName, addr: e.addr}
1002+
lh.mtuWarnLock.Lock()
1003+
changed := lh.mtuWarned[key] != tier
1004+
if changed {
1005+
if tier == linkMTUOk {
1006+
delete(lh.mtuWarned, key)
1007+
} else {
1008+
lh.mtuWarned[key] = tier
1009+
}
1010+
}
1011+
lh.mtuWarnLock.Unlock()
1012+
1013+
if !changed || tier == linkMTUOk {
1014+
return advertise
1015+
}
1016+
1017+
level := slog.LevelWarn
1018+
if omit {
1019+
level = slog.LevelDebug
1020+
}
1021+
1022+
if lh.l.Enabled(context.Background(), level) {
1023+
msg := "Link MTU too small for nebula traffic, expect fragmentation or drops"
1024+
if !advertise {
1025+
msg = "Omitting addr with too-small link MTU from lighthouse report"
1026+
} else if tier == linkMTUTooSmallForRelay {
1027+
msg = "Link MTU too small for relayed nebula traffic"
1028+
}
1029+
1030+
lh.l.Log(context.Background(), level, msg,
1031+
"localAddr", e.addr,
1032+
"interface", e.ifName,
1033+
"linkMTU", e.linkMTU,
1034+
"requiredMTU", direct,
1035+
"requiredRelayMTU", relayed,
1036+
"tunMTU", tunMTU,
1037+
)
1038+
}
1039+
1040+
return advertise
1041+
}
1042+
9081043
func (lh *LightHouse) SendUpdate() {
9091044
var v4 []*V4AddrPort
9101045
var v6 []*V6AddrPort
@@ -919,15 +1054,19 @@ func (lh *LightHouse) SendUpdate() {
9191054

9201055
lal := lh.GetLocalAllowList()
9211056
for _, e := range localAddrs(lh.l, lal) {
922-
if lh.myVpnNetworksTable.Contains(e) {
1057+
if lh.myVpnNetworksTable.Contains(e.addr) {
1058+
continue
1059+
}
1060+
1061+
if !lh.checkLocalLinkMTU(e) {
9231062
continue
9241063
}
9251064

9261065
// Only add addrs that aren't my VPN/tun networks
927-
if e.Is4() {
928-
v4 = append(v4, netAddrToProtoV4AddrPort(e, uint16(lh.nebulaPort)))
1066+
if e.addr.Is4() {
1067+
v4 = append(v4, netAddrToProtoV4AddrPort(e.addr, uint16(lh.nebulaPort)))
9291068
} else {
930-
v6 = append(v6, netAddrToProtoV6AddrPort(e, uint16(lh.nebulaPort)))
1069+
v6 = append(v6, netAddrToProtoV6AddrPort(e.addr, uint16(lh.nebulaPort)))
9311070
}
9321071
}
9331072

lighthouse_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,3 +738,86 @@ func TestLighthouse_DeletesWork(t *testing.T) {
738738
out = lh.Query(testHost)
739739
assert.Nil(t, out)
740740
}
741+
742+
func Test_requiredLinkMTU(t *testing.T) {
743+
// tun packet + nebula header (16) + AEAD tag (16) + udp (8) + ip header
744+
direct, relayed := requiredLinkMTU(1300, true)
745+
assert.Equal(t, 1360, direct)
746+
assert.Equal(t, 1392, relayed)
747+
748+
direct, relayed = requiredLinkMTU(1300, false)
749+
assert.Equal(t, 1380, direct)
750+
assert.Equal(t, 1412, relayed)
751+
}
752+
753+
func Test_checkLocalLinkMTU(t *testing.T) {
754+
lh := &LightHouse{l: test.NewLogger(), mtuWarned: make(map[mtuWarnKey]linkMTUTier)}
755+
lh.tunMTU.Store(1300)
756+
757+
v4 := netip.MustParseAddr("192.168.1.2")
758+
v6 := netip.MustParseAddr("fd00::2")
759+
mkAddr := func(a netip.Addr, mtu int) localAddr {
760+
return localAddr{addr: a, ifName: "test0", linkMTU: mtu}
761+
}
762+
763+
// Plenty of room, no state recorded
764+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1500)))
765+
assert.Empty(t, lh.mtuWarned)
766+
767+
// Unknown link MTU is not classified
768+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 0)))
769+
assert.Empty(t, lh.mtuWarned)
770+
771+
// Too small for even normal traffic, still advertised by default
772+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1359)))
773+
assert.Equal(t, linkMTUTooSmall, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v4}])
774+
775+
// Fits normal traffic but not relayed traffic
776+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1360)))
777+
assert.Equal(t, linkMTUTooSmallForRelay, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v4}])
778+
779+
// Exactly enough for relayed traffic clears the state
780+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1392)))
781+
assert.Empty(t, lh.mtuWarned)
782+
783+
// v6 addrs need 20 more bytes of headroom
784+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v6, 1380)))
785+
assert.Equal(t, linkMTUTooSmallForRelay, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v6}])
786+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v6, 1379)))
787+
assert.Equal(t, linkMTUTooSmall, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v6}])
788+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v6, 1412)))
789+
assert.Empty(t, lh.mtuWarned)
790+
791+
// With omit enabled, only addrs that can't fit normal traffic are dropped
792+
lh.omitLowMTUAddrs.Store(true)
793+
assert.False(t, lh.checkLocalLinkMTU(mkAddr(v4, 1359)))
794+
assert.Equal(t, linkMTUTooSmall, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v4}])
795+
assert.True(t, lh.checkLocalLinkMTU(mkAddr(v4, 1360)))
796+
assert.Equal(t, linkMTUTooSmallForRelay, lh.mtuWarned[mtuWarnKey{ifName: "test0", addr: v4}])
797+
assert.False(t, lh.checkLocalLinkMTU(mkAddr(v4, 1359)))
798+
}
799+
800+
func Test_lighthouseMTUConfig(t *testing.T) {
801+
l := test.NewLogger()
802+
myVpnNet := netip.MustParsePrefix("10.128.0.1/16")
803+
nt := new(bart.Lite)
804+
nt.Insert(myVpnNet)
805+
cs := &CertState{
806+
myVpnNetworks: []netip.Prefix{myVpnNet},
807+
myVpnNetworksTable: nt,
808+
}
809+
810+
c := config.NewC(l)
811+
lh, err := NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil)
812+
require.NoError(t, err)
813+
assert.Equal(t, int64(1300), lh.tunMTU.Load())
814+
assert.False(t, lh.omitLowMTUAddrs.Load())
815+
816+
c = config.NewC(l)
817+
c.Settings["tun"] = map[string]any{"mtu": 8000}
818+
c.Settings["lighthouse"] = map[string]any{"omit_low_mtu_addrs": true}
819+
lh, err = NewLightHouseFromConfig(t.Context(), l, c, cs, nil, nil)
820+
require.NoError(t, err)
821+
assert.Equal(t, int64(8000), lh.tunMTU.Load())
822+
assert.True(t, lh.omitLowMTUAddrs.Load())
823+
}

0 commit comments

Comments
 (0)