lanes.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. package nebula
  2. import (
  3. "context"
  4. "hash/fnv"
  5. "log/slog"
  6. "net/netip"
  7. "sync"
  8. "sync/atomic"
  9. "time"
  10. "github.com/flynn/noise"
  11. "github.com/slackhq/nebula/cert"
  12. "github.com/slackhq/nebula/handshake"
  13. "github.com/slackhq/nebula/header"
  14. "github.com/slackhq/nebula/noiseutil"
  15. )
  16. // Multiport lanes give each data-plane routine its own underlay 5-tuple, so one
  17. // tunnel's traffic spreads over ECMP paths, NIC receive queues and per-flow
  18. // policers instead of funnelling through a single flow.
  19. //
  20. // A lane is not a second tunnel: it is an extra session on the same HostInfo.
  21. // Noise leaves us with A.eKey == B.dKey, so both sides expand the same two keys
  22. // with the same per-lane label and land on a matched pair without exchanging
  23. // anything. A lane therefore costs no handshake, has no half-established state,
  24. // and dies exactly when its base tunnel does. Which lane a packet belongs to
  25. // travels in the nebula header, inside the AEAD's associated data.
  26. //
  27. // Lane 0 is the base tunnel itself: HostInfo.ConnectionState, socket 0, and the
  28. // peer's real remote address. Lane s > 0 egresses writers[s] (bound to
  29. // listen.port+s) toward the peer's advertised port range. Receiving on a lane
  30. // needs no permission — the keys are derivable the moment the base handshake
  31. // completes — but sending on one needs proof the new 5-tuple actually works,
  32. // since nothing else would notice a middlebox quietly dropping it. So a lane
  33. // stays down until a probe on it is acked, and falls back to the base tunnel the
  34. // moment it stops being acked.
  35. //
  36. // Both directions are pay-per-use. A lane session is derived on the first packet
  37. // that needs it, because how many lanes exist is partly the peer's call: it
  38. // advertises how many it sends on, and we have to be able to receive all of
  39. // them. Deriving them all up front would let a peer advertising the maximum cost
  40. // us a replay window and two cipher states per lane, per tunnel, for lanes it
  41. // may never send on.
  42. const (
  43. // laneKeyInfo is the HKDF label prefix for lane key expansion. Changing it
  44. // means older builds derive different keys and drop our lane traffic; the
  45. // base tunnel would keep working, so the failure would be a silent loss of
  46. // lanes rather than of connectivity.
  47. laneKeyInfo = "nebula multiport lane v1"
  48. // laneRetryBase and laneRetryMax bound the backoff between probes of a lane
  49. // that will not come up, so a peer whose lane ports are firewalled costs one
  50. // packet a minute rather than one per traffic tick.
  51. laneRetryBase = 5 * time.Second
  52. laneRetryMax = 60 * time.Second
  53. // laneMaxFails caps the failure counter; the backoff saturates well before.
  54. laneMaxFails = 8
  55. // laneProbeTimeout is how long a probe may go unacked before it counts as a
  56. // failure. It is shorter than the connection manager's check interval on
  57. // purpose: an outstanding probe is judged on the next tick either way, and a
  58. // longer timeout would only delay that by a whole tick.
  59. laneProbeTimeout = 2 * time.Second
  60. // laneKeepalive is how often a lane that is up re-proves its path. Traffic
  61. // on a lane is not evidence the lane works — that is the whole reason lanes
  62. // need probing — so a lane that silently breaks is only caught here.
  63. laneKeepalive = 30 * time.Second
  64. )
  65. // laneSet holds a peer's lane sessions and the state deciding which lanes may
  66. // carry traffic. It is built when the base handshake completes and never
  67. // resized, so the slices and their lengths are immutable; mu guards the fields
  68. // under it, and sessions/txAddr/demand are atomics read by the data plane
  69. // without it.
  70. type laneSet struct {
  71. // sessions[s] holds lane s's session once something has needed it, and nil
  72. // until then. sessions[0] is never populated: lane 0 is the base tunnel's own
  73. // ConnectionState. The length is immutable, so the data plane bounds-checks
  74. // and loads with no locking.
  75. sessions []atomic.Pointer[ConnectionState]
  76. // material is what a lane session is derived from, kept because sessions are
  77. // derived lazily and the handshake result is long gone by then.
  78. material laneMaterial
  79. // txAddr[s] holds lane s's remote address while the lane is proven usable
  80. // and nil otherwise. This single atomic is both the TX gate and the
  81. // destination, so a routine that loads non-nil has everything it needs.
  82. // Sized txLanes: lanes above that never send.
  83. txAddr []atomic.Pointer[netip.AddrPort]
  84. // demand[s] is raised by the TX path when a routine riding lane s has
  85. // traffic for this peer and the lane is down. Probing is demand-driven: a
  86. // peer we exchange a trickle with never costs more than its base tunnel, no
  87. // matter how many lanes are configured. Sized txLanes.
  88. demand []atomic.Bool
  89. // txLanes is how many lanes we may send on — our lane count clamped to the
  90. // ports the peer bound. Lanes from txLanes up can only receive, which is how
  91. // a peer with more routines than us still spreads its own traffic.
  92. // Immutable, and the length of every TX-side slice here.
  93. txLanes int
  94. mu sync.Mutex
  95. // peerPortCount and peerBasePort are the peer's advertised port range and
  96. // portOffset is this pair's rotation within it: lane s targets
  97. // peerBasePort + ((s + portOffset) % peerPortCount).
  98. peerPortCount uint16
  99. peerBasePort uint16
  100. portOffset uint16
  101. // peerAddr is the address the current lane targets were built from. The
  102. // peer's lane ports have no derivable relationship to a new NAT mapping, so
  103. // a roam invalidates every lane rather than moving it.
  104. peerAddr netip.Addr
  105. // probe[s] is lane s's probe and backoff state. Sized txLanes.
  106. probe []laneProbeState
  107. }
  108. // laneMaterial is everything a lane session is derived from. The two base keys
  109. // are the same secret the base tunnel's own cipher states already hold — the
  110. // noiseutil.CipherState interface just doesn't hand them back, so a lane set
  111. // keeps its own copy rather than a reference to the session.
  112. type laneMaterial struct {
  113. eKey, dKey [32]byte
  114. cipher noise.CipherFunc
  115. myCert cert.Certificate
  116. peerCert *cert.CachedCertificate
  117. initiator bool
  118. }
  119. type laneProbeState struct {
  120. // gen is the generation of the last probe sent, echoed in the ack so a late
  121. // ack cannot promote a lane on the strength of a superseded probe.
  122. gen uint8
  123. // fails is the consecutive failure count driving retryAt.
  124. fails uint8
  125. // sentAt is when the outstanding probe went out, zero when none is pending.
  126. sentAt time.Time
  127. // target is where the outstanding probe went, promoted to txAddr on ack.
  128. target netip.AddrPort
  129. // lastAck is when the lane was last confirmed usable, driving the keepalive.
  130. lastAck time.Time
  131. // retryAt is the earliest we may probe this lane again.
  132. retryAt time.Time
  133. }
  134. // newLaneSet sets up the lanes for a freshly completed base handshake. It
  135. // returns nil when the pair has no lane beyond the base tunnel, which is the
  136. // normal answer for a peer running without multiport. No session is derived
  137. // here; each is derived on the first packet that needs it.
  138. func newLaneSet(r *handshake.Result, myLanes int, myAddr, peerAddr netip.Addr) *laneSet {
  139. // PeerPortCount and PeerBasePort are already bounded to uint16 by the
  140. // handshake payload parser. A zero port count is a peer that did not
  141. // advertise multiport at all, so there is no lane to be had in either
  142. // direction.
  143. peerPorts := uint16(r.PeerPortCount)
  144. if peerPorts == 0 {
  145. return nil
  146. }
  147. // Sessions have to cover both directions: we send on our lanes and receive
  148. // on the peer's, and one derived session serves both ends of a lane index.
  149. // Only the session table is sized by the peer's advertised count; the TX-side
  150. // state is sized by what we will actually send on.
  151. n := min(max(myLanes, int(r.PeerTxLanes)), header.MaxLane+1)
  152. if n < 2 {
  153. return nil
  154. }
  155. txLanes := min(myLanes, int(peerPorts), n)
  156. return &laneSet{
  157. sessions: make([]atomic.Pointer[ConnectionState], n),
  158. material: laneMaterial{
  159. eKey: r.EKey.UnsafeKey(),
  160. dKey: r.DKey.UnsafeKey(),
  161. cipher: r.Cipher,
  162. myCert: r.MyCert,
  163. peerCert: r.RemoteCert,
  164. initiator: r.Initiator,
  165. },
  166. txAddr: make([]atomic.Pointer[netip.AddrPort], txLanes),
  167. demand: make([]atomic.Bool, txLanes),
  168. probe: make([]laneProbeState, txLanes),
  169. txLanes: txLanes,
  170. peerPortCount: peerPorts,
  171. peerBasePort: uint16(r.PeerBasePort),
  172. portOffset: lanePortOffset(myAddr, peerAddr, peerPorts),
  173. }
  174. }
  175. // lanePortOffset returns the rotation applied to this pair's lane target ports,
  176. // in [0, peerPortCount). Without it every low-routine peer would aim its few
  177. // lanes at a big peer's first few ports, concentrating the big peer's receive
  178. // work on a couple of sockets; the hash spreads pairs across the whole range.
  179. //
  180. // Both sides hash the same sorted vpn-address pair and the higher address
  181. // negates the result, so when port counts match the two sides' rotations
  182. // cancel: our lane s's 4-tuple stays the reverse of the peer's lane s, and each
  183. // side's probe opens the conntrack entry the other's arrives through. (The one
  184. // lane a nonzero rotation lands on the peer's base port has no partner lane;
  185. // behind a port-restricted NAT it may never come up, and its routine rides the
  186. // base tunnel — the standard lane fallback.)
  187. func lanePortOffset(myAddr, peerAddr netip.Addr, peerPortCount uint16) uint16 {
  188. if peerPortCount == 0 {
  189. return 0
  190. }
  191. lo, hi := myAddr, peerAddr
  192. if hi.Less(lo) {
  193. lo, hi = hi, lo
  194. }
  195. h := fnv.New32a()
  196. b := lo.As16()
  197. h.Write(b[:])
  198. b = hi.As16()
  199. h.Write(b[:])
  200. o := uint16(h.Sum32() % uint32(peerPortCount))
  201. if myAddr == hi {
  202. o = (peerPortCount - o) % peerPortCount
  203. }
  204. return o
  205. }
  206. // laneSession returns the session to decrypt a lane s packet with, deriving one
  207. // if this is the first packet to claim that lane. A nil session with no error
  208. // means this tunnel has no lane s at all.
  209. //
  210. // cached reports whether the session was already in the table. A fresh one is
  211. // deliberately left out of it: anyone who can spoof this tunnel's local index
  212. // can name any lane, and installing on sight would let them make us hold a
  213. // replay window and two cipher states per lane without authenticating anything.
  214. // The caller must install with installSession once the packet decrypts, which is
  215. // the first moment the lane is known to be real.
  216. func (i *HostInfo) laneSession(s uint8) (ci *ConnectionState, cached bool, err error) {
  217. ls := i.lanes
  218. if ls == nil || s == 0 || int(s) >= len(ls.sessions) {
  219. return nil, false, nil
  220. }
  221. if cs := ls.sessions[s].Load(); cs != nil {
  222. return cs, true, nil
  223. }
  224. cs, err := newLaneConnectionState(&ls.material, s)
  225. if err != nil {
  226. return nil, false, err
  227. }
  228. return cs, false, nil
  229. }
  230. // installSession publishes a session derived by laneSession, so the next packet
  231. // on the lane doesn't have to derive it again. cs must have already decrypted the
  232. // packet at messageCounter.
  233. //
  234. // Two routines can race on a lane's first packet and derive a session each. The
  235. // loser's is dropped, and with it the replay-window entry for the packet it just
  236. // accepted, so hand that counter to the session that survives — the keys are
  237. // identical, so it is the same window in every respect that matters.
  238. func (ls *laneSet) installSession(l *slog.Logger, s uint8, cs *ConnectionState, messageCounter uint64) {
  239. if ls.sessions[s].CompareAndSwap(nil, cs) {
  240. return
  241. }
  242. ls.sessions[s].Load().noteSeen(l, messageCounter)
  243. }
  244. // session returns lane s's session for our own use, deriving and installing it
  245. // if it doesn't exist yet. Unlike the RX path this needs no proof the lane is
  246. // real: we only ask for lanes we chose to send on. s must be a lane this set
  247. // covers.
  248. func (ls *laneSet) session(s int) (*ConnectionState, error) {
  249. if cs := ls.sessions[s].Load(); cs != nil {
  250. return cs, nil
  251. }
  252. cs, err := newLaneConnectionState(&ls.material, uint8(s))
  253. if err != nil {
  254. return nil, err
  255. }
  256. if !ls.sessions[s].CompareAndSwap(nil, cs) {
  257. return ls.sessions[s].Load(), nil
  258. }
  259. return cs, nil
  260. }
  261. // maxMessageCounter returns the highest counter across the base session and
  262. // every lane session. Data rides the lanes, so the base counter alone would
  263. // never reach the rehandshake or exhaustion thresholds and the lane keys would
  264. // be used past their data-volume margin. Rolling the base tunnel replaces the
  265. // lane keys with it, since lanes are derived from it.
  266. func (i *HostInfo) maxMessageCounter() uint64 {
  267. if i.ConnectionState == nil {
  268. return 0
  269. }
  270. c := i.ConnectionState.messageCounter.Load()
  271. if ls := i.lanes; ls != nil {
  272. for s := range ls.sessions {
  273. cs := ls.sessions[s].Load()
  274. if cs == nil {
  275. // Never derived, so it has never sent anything either.
  276. continue
  277. }
  278. if lc := cs.messageCounter.Load(); lc > c {
  279. c = lc
  280. }
  281. }
  282. }
  283. return c
  284. }
  285. // txLane returns the session and destination for lane s, or a nil session when
  286. // the lane is down and the caller must use the base tunnel. A miss raises
  287. // demand, which is the only thing that gets a lane probed, so we pay for a lane
  288. // exactly where real traffic wanted one.
  289. func (ls *laneSet) txLane(s int) (*ConnectionState, netip.AddrPort) {
  290. if ls == nil || s <= 0 || s >= ls.txLanes {
  291. return nil, netip.AddrPort{}
  292. }
  293. if addr := ls.txAddr[s].Load(); addr != nil {
  294. // The lane is only up because a probe was acked on it, and that probe
  295. // derived the session, so this load cannot miss. Fall back rather than
  296. // derive here anyway: this is the hot path and a nil is not worth an HKDF.
  297. if cs := ls.sessions[s].Load(); cs != nil {
  298. return cs, *addr
  299. }
  300. return nil, netip.AddrPort{}
  301. }
  302. // Load-guarded so the common case of a lane that will not come up is a
  303. // plain read and cannot ping-pong the cache line these flags share.
  304. if !ls.demand[s].Load() {
  305. ls.demand[s].Store(true)
  306. }
  307. return nil, netip.AddrPort{}
  308. }
  309. // laneTargetPortLocked returns the peer port lane s aims at. Only meaningful
  310. // when peerPortCount is nonzero, which txLanes > 0 guarantees.
  311. func (ls *laneSet) laneTargetPortLocked(s int) uint16 {
  312. return ls.peerBasePort + uint16((s+int(ls.portOffset))%int(ls.peerPortCount))
  313. }
  314. // laneRetryDelay is the backoff after fails consecutive probe failures.
  315. func laneRetryDelay(fails uint8) time.Duration {
  316. d := laneRetryBase << min(fails, 4)
  317. if d > laneRetryMax {
  318. d = laneRetryMax
  319. }
  320. return d
  321. }
  322. // noteAck records an acked probe for lane s, promoting the lane if it was down.
  323. // gen must match the outstanding probe. Reports whether the lane was promoted.
  324. func (ls *laneSet) noteAck(s int, gen uint8, now time.Time) bool {
  325. if s <= 0 || s >= len(ls.probe) {
  326. return false
  327. }
  328. ls.mu.Lock()
  329. defer ls.mu.Unlock()
  330. p := &ls.probe[s]
  331. if p.sentAt.IsZero() || p.gen != gen {
  332. // No probe outstanding, or an ack for a probe we have already given up
  333. // on. Either way it says nothing about the lane's current path.
  334. return false
  335. }
  336. p.sentAt = time.Time{}
  337. p.lastAck = now
  338. p.fails = 0
  339. p.retryAt = time.Time{}
  340. if ls.txAddr[s].Load() != nil {
  341. // Keepalive for a lane already up.
  342. return false
  343. }
  344. target := p.target
  345. ls.txAddr[s].Store(&target)
  346. return true
  347. }
  348. // probeLanes runs one lane maintenance pass for a peer: it demotes lanes whose
  349. // probe went unanswered, re-proves lanes that have been up a while without one,
  350. // and probes down lanes the data plane asked for. Driven by the connection
  351. // manager's per-tunnel traffic tick, which only fires for a live tunnel — the
  352. // same condition that produces lane demand in the first place.
  353. func (f *Interface) probeLanes(hostinfo *HostInfo, now time.Time, nb, out []byte) {
  354. ls := hostinfo.lanes
  355. if ls == nil || ls.txLanes < 2 {
  356. return
  357. }
  358. remote := hostinfo.GetRemote()
  359. ls.mu.Lock()
  360. defer ls.mu.Unlock()
  361. if !remote.IsValid() {
  362. // Relayed, or otherwise without a direct path. Lanes are direct-only,
  363. // so drop them all; a later tick rebuilds if a direct path returns.
  364. ls.resetLocked()
  365. return
  366. }
  367. if ls.peerAddr != remote.Addr() {
  368. if ls.peerAddr.IsValid() {
  369. // A roam is a new path, not a failure: forget the lanes built on
  370. // the old one and let demand re-probe from a clean backoff. On the
  371. // first pass there is nothing built yet, so just record the address.
  372. ls.resetLocked()
  373. }
  374. ls.peerAddr = remote.Addr()
  375. }
  376. for s := 1; s < ls.txLanes; s++ {
  377. p := &ls.probe[s]
  378. up := ls.txAddr[s].Load() != nil
  379. if !p.sentAt.IsZero() {
  380. if now.Sub(p.sentAt) < laneProbeTimeout {
  381. continue
  382. }
  383. // An aged-out probe is a failure whether it was bringing the lane
  384. // up or keeping it up.
  385. p.sentAt = time.Time{}
  386. p.fails = min(p.fails+1, laneMaxFails)
  387. p.retryAt = now.Add(laneRetryDelay(p.fails))
  388. if up {
  389. ls.txAddr[s].Store(nil)
  390. hostinfo.logger(f.l).Info("Multiport lane demoted, probe unanswered", "lane", s)
  391. }
  392. continue
  393. }
  394. if up {
  395. if now.Sub(p.lastAck) < laneKeepalive {
  396. continue
  397. }
  398. } else if now.Before(p.retryAt) || !ls.demand[s].Swap(false) {
  399. continue
  400. }
  401. p.gen++
  402. p.target = netip.AddrPortFrom(remote.Addr(), ls.laneTargetPortLocked(s))
  403. if f.sendLaneProbe(hostinfo, s, p.gen, p.target, nb, out) {
  404. p.sentAt = now
  405. } else {
  406. p.fails = min(p.fails+1, laneMaxFails)
  407. p.retryAt = now.Add(laneRetryDelay(p.fails))
  408. }
  409. }
  410. }
  411. // resetLocked takes every lane down and clears its probe state, without
  412. // counting it as a failure.
  413. //
  414. // Demand is deliberately left standing: it records that a routine has real
  415. // traffic for this peer, which a roam or a relay detour does not change. Keeping
  416. // it re-probes the lanes that were actually carrying data as soon as a path
  417. // exists again, while a lane whose routine has gone quiet stays down.
  418. func (ls *laneSet) resetLocked() {
  419. for s := 1; s < len(ls.probe); s++ {
  420. ls.txAddr[s].Store(nil)
  421. ls.probe[s] = laneProbeState{}
  422. }
  423. }
  424. // sendLaneProbe sends a probe on lane s to addr from writers[s]. The probe is an
  425. // ordinary Test packet encrypted with the lane's session, so an ack proves the
  426. // whole lane: our source port reached the peer, its reply reached us, and the
  427. // keys we derived for this lane match the ones it derived. Reports whether the
  428. // probe made it onto the wire.
  429. func (f *Interface) sendLaneProbe(hostinfo *HostInfo, s int, gen uint8, addr netip.AddrPort, nb, out []byte) bool {
  430. // The first probe on a lane is what derives its session.
  431. ci, err := hostinfo.lanes.session(s)
  432. if err != nil {
  433. hostinfo.logger(f.l).Error("Failed to derive multiport lane session", "error", err, "lane", s)
  434. return false
  435. }
  436. if ci == nil || ci.eKey == nil {
  437. return false
  438. }
  439. if noiseutil.EncryptLockNeeded {
  440. ci.writeLock.Lock()
  441. }
  442. c, ok := ci.NextMessageCounter()
  443. if !ok {
  444. if noiseutil.EncryptLockNeeded {
  445. ci.writeLock.Unlock()
  446. }
  447. f.dropExhausted(hostinfo, c, "Dropping multiport lane probe, lane message counter is exhausted")
  448. return false
  449. }
  450. b := header.EncodeLane(out[:0], header.Version, header.Test, header.LaneProbe, hostinfo.remoteIndexId, c, uint8(s))
  451. b, err = ci.eKey.EncryptDanger(b, b, []byte{uint8(s), gen}, c, nb)
  452. if noiseutil.EncryptLockNeeded {
  453. ci.writeLock.Unlock()
  454. }
  455. if err != nil {
  456. hostinfo.logger(f.l).Error("Failed to encrypt multiport lane probe", "error", err, "lane", s)
  457. return false
  458. }
  459. f.messageMetrics.Tx(header.Test, header.LaneProbe, 1)
  460. if err := f.writers[s].WriteTo(b, addr); err != nil {
  461. hostinfo.logger(f.l).Error("Failed to send multiport lane probe", "error", err, "lane", s, "udpAddr", addr)
  462. return false
  463. }
  464. if f.l.Enabled(context.Background(), slog.LevelDebug) {
  465. hostinfo.logger(f.l).Debug("Multiport lane probe sent", "lane", s, "gen", gen, "udpAddr", addr)
  466. }
  467. return true
  468. }
  469. // handleLaneProbe answers a peer's lane probe. The ack rides the base tunnel on
  470. // purpose: a probe proves the peer's lane s works in its send direction, and
  471. // answering on our own lane s would make the result depend on a second path
  472. // that may be broken independently.
  473. func (f *Interface) handleLaneProbe(hostinfo *HostInfo, lane uint8, payload []byte, rxc *rxContext) {
  474. if lane == 0 || len(payload) < 2 {
  475. return
  476. }
  477. // Echo the header's lane rather than the payload's, so a peer cannot get us
  478. // to vouch for a lane it did not actually probe.
  479. f.send(header.Test, header.LaneProbeAck, hostinfo.ConnectionState, hostinfo,
  480. []byte{lane, payload[1]}, rxc.nb, rxc.scratch[:0])
  481. }
  482. // handleLaneProbeAck promotes the lane a peer just acked.
  483. func (f *Interface) handleLaneProbeAck(hostinfo *HostInfo, payload []byte) {
  484. ls := hostinfo.lanes
  485. if ls == nil || len(payload) < 2 {
  486. return
  487. }
  488. if ls.noteAck(int(payload[0]), payload[1], time.Now()) {
  489. hostinfo.logger(f.l).Info("Multiport lane up", "lane", payload[0])
  490. }
  491. }