diff --git a/internal/pkg/netutils/sockopt.go b/internal/pkg/netutils/sockopt.go index baa441cca..e054186ec 100644 --- a/internal/pkg/netutils/sockopt.go +++ b/internal/pkg/netutils/sockopt.go @@ -56,7 +56,7 @@ func SetReuseAddrSockopt(sc syscall.RawConn) error { return SetReuseAddrSockoptImpl(sc) } -func DialerControl(logger *slog.Logger, network, address string, c syscall.RawConn, ttl, minTtl uint8, mss uint16, password string, bindInterface string, tos uint8) error { +func DialerControl(logger *slog.Logger, network, address string, c syscall.RawConn, ttl, minTtl uint8, mss uint16, password string, bindInterface string, tos uint8, tcpAO *TCPAOConfig) error { if password != "" { logger.Warn("setting md5 for active connection is not supported", slog.String("Topic", "Peer"), @@ -87,5 +87,8 @@ func DialerControl(logger *slog.Logger, network, address string, c syscall.RawCo slog.String("Key", address), ) } + if tcpAO != nil { + return ErrTCPAONotSupported + } return nil } diff --git a/internal/pkg/netutils/sockopt_linux.go b/internal/pkg/netutils/sockopt_linux.go index f9aba28e2..6c917abf7 100644 --- a/internal/pkg/netutils/sockopt_linux.go +++ b/internal/pkg/netutils/sockopt_linux.go @@ -180,7 +180,7 @@ func SetReuseAddrSockopt(sc syscall.RawConn) error { return setSockOptInt(sc, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) } -func DialerControl(logger *slog.Logger, network, address string, c syscall.RawConn, ttl, minTtl uint8, mss uint16, password string, bindInterface string, tos uint8) error { +func DialerControl(logger *slog.Logger, network, address string, c syscall.RawConn, ttl, minTtl uint8, mss uint16, password string, bindInterface string, tos uint8, tcpAO *TCPAOConfig) error { family := syscall.AF_INET raddr, _ := net.ResolveTCPAddr("tcp", address) if raddr.IP.To4() == nil { @@ -280,5 +280,16 @@ func DialerControl(logger *slog.Logger, network, address string, c syscall.RawCo return sockerr } } + + if tcpAO != nil { + addrPort, err := netip.ParseAddrPort(address) + if err != nil { + return fmt.Errorf("invalid TCP-AO peer address %q: %w", address, err) + } + addr := addrPort.Addr().Unmap() + if err := AddTCPAOKeysSockopt(c, netip.PrefixFrom(addr, addr.BitLen()), bindInterface, *tcpAO); err != nil { + return fmt.Errorf("failed to configure TCP-AO for peer %s: %w", addr, err) + } + } return nil } diff --git a/internal/pkg/netutils/sockopt_openbsd.go b/internal/pkg/netutils/sockopt_openbsd.go index 5f3f875dd..151e5e89f 100644 --- a/internal/pkg/netutils/sockopt_openbsd.go +++ b/internal/pkg/netutils/sockopt_openbsd.go @@ -422,7 +422,7 @@ func SetReuseAddrSockopt(sc syscall.RawConn) error { return setSockOptInt(sc, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) } -func DialerControl(logger *slog.Logger, network, address string, c syscall.RawConn, ttl, minTtl uint8, mss uint16, password string, bindInterface string, tos uint8) error { +func DialerControl(logger *slog.Logger, network, address string, c syscall.RawConn, ttl, minTtl uint8, mss uint16, password string, bindInterface string, tos uint8, tcpAO *TCPAOConfig) error { family := syscall.AF_INET raddr, _ := net.ResolveTCPAddr("tcp", address) if raddr.IP.To4() == nil { @@ -472,5 +472,8 @@ func DialerControl(logger *slog.Logger, network, address string, c syscall.RawCo return sockerr } } + if tcpAO != nil { + return ErrTCPAONotSupported + } return nil } diff --git a/internal/pkg/netutils/sockopt_windows.go b/internal/pkg/netutils/sockopt_windows.go index 598882e21..386eb37a8 100644 --- a/internal/pkg/netutils/sockopt_windows.go +++ b/internal/pkg/netutils/sockopt_windows.go @@ -94,7 +94,7 @@ func SetReuseAddrSockopt(sc syscall.RawConn) error { return setSockOptInt(sc, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) } -func DialerControl(logger *slog.Logger, network, address string, c syscall.RawConn, ttl, ttlMin uint8, mss uint16, password string, bindInterface string, tos uint8) error { +func DialerControl(logger *slog.Logger, network, address string, c syscall.RawConn, ttl, ttlMin uint8, mss uint16, password string, bindInterface string, tos uint8, tcpAO *TCPAOConfig) error { if password != "" { logger.Warn("setting md5 for active connection is not supported", slog.String("Topic", "Peer"), @@ -120,5 +120,8 @@ func DialerControl(logger *slog.Logger, network, address string, c syscall.RawCo slog.String("Topic", "Peer"), slog.String("Key", address)) } + if tcpAO != nil { + return ErrTCPAONotSupported + } return nil } diff --git a/internal/pkg/netutils/tcp_ao_linux_test.go b/internal/pkg/netutils/tcp_ao_linux_test.go index ead0a247e..26a15d0d6 100644 --- a/internal/pkg/netutils/tcp_ao_linux_test.go +++ b/internal/pkg/netutils/tcp_ao_linux_test.go @@ -19,6 +19,7 @@ package netutils import ( "context" "errors" + "log/slog" "net" "net/netip" "syscall" @@ -173,9 +174,8 @@ func TestTCPAOKeySelection(t *testing.T) { PreferredSendID: ¤t, } dialer := net.Dialer{Timeout: time.Second} - dialer.SetMultipathTCP(false) - dialer.Control = func(_, _ string, raw syscall.RawConn) error { - return AddTCPAOKeysSockopt(raw, peer, "", clientConfig) + dialer.Control = func(network, address string, raw syscall.RawConn) error { + return DialerControl(slog.Default(), network, address, raw, 0, 0, 0, "", "", 0, &clientConfig) } clientConn, err := dialer.DialContext(context.Background(), "tcp4", listener.Addr().String()) require.NoError(t, err) diff --git a/pkg/config/oc/util.go b/pkg/config/oc/util.go index 351df17a5..7d8925932 100644 --- a/pkg/config/oc/util.go +++ b/pkg/config/oc/util.go @@ -226,7 +226,8 @@ func (n *Neighbor) NeedsResendOpenMessage(new *Neighbor) bool { !n.GracefulRestart.Config.Equal(&new.GracefulRestart.Config) || isAfiSafiChanged(n.AfiSafis, new.AfiSafis) || !n.EbgpMultihop.Config.Equal(&new.EbgpMultihop.Config) || - !n.TtlSecurity.Config.Equal(&new.TtlSecurity.Config) + !n.TtlSecurity.Config.Equal(&new.TtlSecurity.Config) || + n.TcpAo.Config.Keychain != new.TcpAo.Config.Keychain } // TODO: these regexp are duplicated in api diff --git a/pkg/server/fsm.go b/pkg/server/fsm.go index 36f324127..1e869a58b 100644 --- a/pkg/server/fsm.go +++ b/pkg/server/fsm.go @@ -417,6 +417,20 @@ func (p *pConfAccess) Update(conf *oc.Neighbor) { p.conf.Store(conf) } +// tcpAoKeyBinding is a peer's resolved TCP-AO configuration: +// a reference to a shared keychain plus the peer's preferred send ID. +// +// Keychain's key additions and deletions mutate the shared keychain under its lock, +// socketKeys() can be used to take live key snapshots for socket operations. +type tcpAoKeyBinding struct { + keychain *tcpAoKeychain + preferredSendID uint8 +} + +func (b *tcpAoKeyBinding) socketKeys() (*tcpAoSocketKeys, error) { + return b.keychain.socketKeys(b.preferredSendID) +} + type fsm struct { counterStats oc.Messages timerStats oc.Timers @@ -450,6 +464,16 @@ type fsm struct { adminStateCh chan adminStateOperation outgoingConnCh chan outgoingConn + // tcpAoKeyBinding holds resolved TCP-AO configuration for the peer: + // - Passive peers have all configured keys installed on matching listening sockets; + // preferred key is not set on the listener until an inbound connection is accepted and matched to the peer. + // - Active peers install all keys in net.Dialer.Control in connectLoop() and mark the preferred key during installation. + // - Async keychain update causes update of keys directly on the listening sockets and fsm conn sockets of the affected peers. + // This is handled on best effort; keychain update is not rolled back upon individual socket operation errors. + // - Attaching, removing, or changing a keychain of a peer always uses peer delete-and-add path. + // A preferred key change within the same keychain is handled live by updating RNext on the current connection. + tcpAoKeyBinding atomic.Pointer[tcpAoKeyBinding] + // only loop goroutine accesses; no lock required outgoingConnMgr *outgoingConnManager idleHoldTime float64 @@ -911,7 +935,7 @@ func (h *fsmHandler) idle(ctx context.Context) (bgp.FSMState, *fsmStateReason) { func (h *fsmHandler) connectLoop(ctx context.Context) net.Conn { fsm := h.fsm - retryInterval, addr, port, password, ttl, ttlMin, mss, localAddress, localPort, bindInterface, tos := func() (int, string, int, string, uint8, uint8, uint16, string, int, string, uint8) { + retryInterval, addr, port, password, ttl, ttlMin, mss, localAddress, localPort, bindInterface, tos := func() (int, netip.Addr, int, string, uint8, uint8, uint16, string, int, string, uint8) { conf := fsm.pConf.ReadOnly() tick := max(int(conf.Timers.Config.ConnectRetry), minConnectRetryInterval) @@ -937,7 +961,7 @@ func (h *fsmHandler) connectLoop(ctx context.Context) net.Conn { ttl = conf.EbgpMultihop.Config.MultihopTtl } } - return tick, addr.String(), port, password, ttl, ttlMin, conf.Transport.Config.TcpMss, conf.Transport.Config.LocalAddress.String(), int(conf.Transport.Config.LocalPort), conf.Transport.Config.BindInterface, tos + return tick, addr, port, password, ttl, ttlMin, conf.Transport.Config.TcpMss, conf.Transport.Config.LocalAddress.String(), int(conf.Transport.Config.LocalPort), conf.Transport.Config.BindInterface, tos }() tick := minConnectRetryInterval @@ -963,11 +987,22 @@ func (h *fsmHandler) connectLoop(ctx context.Context) net.Conn { Timeout: time.Duration(max(retryInterval-1, minConnectRetryInterval)) * time.Second, KeepAlive: -1, Control: func(network, address string, c syscall.RawConn) error { - return netutils.DialerControl(fsm.logger, network, address, c, ttl, ttlMin, mss, password, bindInterface, tos) + var tcpAO *netutils.TCPAOConfig + if keyBinding := fsm.tcpAoKeyBinding.Load(); keyBinding != nil { + tcpAoKeys, err := keyBinding.socketKeys() + if err != nil { + return fmt.Errorf("failed to load TCP-AO keychain for peer %s: %w", addr, err) + } + config, err := tcpAoKeys.netutilsConfig(true) + if err != nil { + return fmt.Errorf("failed to configure TCP-AO for peer %s: %w", addr, err) + } + tcpAO = &config + } + return netutils.DialerControl(fsm.logger, network, address, c, ttl, ttlMin, mss, password, bindInterface, tos, tcpAO) }, } - - conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(addr, strconv.Itoa(port))) + conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(addr.String(), strconv.Itoa(port))) select { case <-ctx.Done(): fsm.logger.Debug("stop connect loop") diff --git a/pkg/server/fsm_test.go b/pkg/server/fsm_test.go index 18f98c621..7ad28b418 100644 --- a/pkg/server/fsm_test.go +++ b/pkg/server/fsm_test.go @@ -554,7 +554,7 @@ func TestFsmPeerConfigAccess(t *testing.T) { }, } - peer := newPeer(nil, &a, bgp.BGP_FSM_ESTABLISHED, nil, nil, slog.Default()) + peer := newPeer(nil, &a, bgp.BGP_FSM_ESTABLISHED, nil, nil, nil, slog.Default()) b := peer.fsm.pConf.ReadCopy() assert.True(t, a.Equal(&b)) diff --git a/pkg/server/peer.go b/pkg/server/peer.go index 2414323e2..b3a69f859 100644 --- a/pkg/server/peer.go +++ b/pkg/server/peer.go @@ -65,6 +65,12 @@ func (pg *peerGroup) DeleteDynamicNeighbor(prefix string) { } func newDynamicPeer(g *oc.Global, neighborAddress string, pg *oc.PeerGroup, loc *table.TableManager, policy *table.RoutingPolicy, logger *slog.Logger) *peer { + if pg.TcpAo.Config.Keychain != "" { + logger.Debug("TCP-AO dynamic neighbors are not supported", + slog.String("Topic", "Peer"), + slog.String("Key", neighborAddress)) + return nil + } conf := oc.Neighbor{ Config: oc.NeighborConfig{ PeerGroup: pg.Config.PeerGroupName, @@ -93,7 +99,7 @@ func newDynamicPeer(g *oc.Global, neighborAddress string, pg *oc.PeerGroup, loc return nil } - return newPeer(g, &conf, bgp.BGP_FSM_ACTIVE, loc, policy, logger) + return newPeer(g, &conf, bgp.BGP_FSM_ACTIVE, loc, policy, nil, logger) } // pathIDSet is the set of add-path local identifiers advertised for a destination. @@ -125,7 +131,7 @@ type peer struct { routeRefreshInProgress sync.RWMutex } -func newPeer(g *oc.Global, conf *oc.Neighbor, state bgp.FSMState, loc *table.TableManager, policy *table.RoutingPolicy, logger *slog.Logger) *peer { +func newPeer(g *oc.Global, conf *oc.Neighbor, state bgp.FSMState, loc *table.TableManager, policy *table.RoutingPolicy, tcpAo *tcpAoKeyBinding, logger *slog.Logger) *peer { peer := &peer{ localRib: loc, policy: policy, @@ -140,6 +146,7 @@ func newPeer(g *oc.Global, conf *oc.Neighbor, state bgp.FSMState, loc *table.Tab rfs, _ := oc.AfiSafis(conf.AfiSafis).ToRfList() peer.adjRibIn = table.NewAdjRib(logger, rfs) peer.rtmHandler = table.NewRouteTargetMembershipHandler() + peer.fsm.tcpAoKeyBinding.Store(tcpAo) return peer } diff --git a/pkg/server/server.go b/pkg/server/server.go index 20d4e88d0..6f0305617 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -341,6 +341,19 @@ func (s *BgpServer) passConnToPeer(conn net.Conn) { conn.Close() return } + if keyBinding := peer.fsm.tcpAoKeyBinding.Load(); keyBinding != nil { + tcpAoKeys, err := keyBinding.socketKeys() + if err != nil { + peer.fsm.logger.Warn("could not load TCP-AO keychain", slog.String("Error", err.Error())) + conn.Close() + return + } + if err := setTcpAoConnectionRNext(conn, tcpAoKeys); err != nil { + peer.fsm.logger.Warn("could not configure TCP-AO for the connection", slog.String("Error", err.Error())) + conn.Close() + return + } + } peer.fsm.logger.Debug("Accepted a new passive connection") peer.PassConn(conn) @@ -3435,6 +3448,18 @@ func (s *BgpServer) ListPeer(ctx context.Context, r *api.ListPeerRequest, fn fun } // FIXME: should remove toConfig() conversion p := oc.NewPeerFromConfigStruct(s.toConfig(peer, getAdvertised)) + if peer.fsm.tcpAoKeyBinding.Load() != nil { + peer.fsm.lock.Lock() + if peer.fsm.conn != nil { + state, err := getTcpAoConnectionState(peer.fsm.conn) + if err != nil { + peer.fsm.logger.Debug("failed to get TCP-AO socket state", slog.String("Err", err.Error())) + } else { + p.State.TcpAoState = state + } + } + peer.fsm.lock.Unlock() + } for _, family := range peer.configuredRFlist() { for i, afisafi := range p.AfiSafis { if !afisafi.Config.Enabled { @@ -3536,8 +3561,11 @@ func (s *BgpServer) addNeighbor(c *oc.Neighbor) error { if err != nil { return err } - - if _, y := s.neighborMap[netip.MustParseAddr(addr)]; y { + ipAddr, err := netip.ParseAddr(addr) + if err != nil { + return fmt.Errorf("failed to parse IP address: %v", err) + } + if _, y := s.neighborMap[ipAddr]; y { return fmt.Errorf("can't overwrite the existing peer: %s", addr) } @@ -3560,17 +3588,17 @@ func (s *BgpServer) addNeighbor(c *oc.Neighbor) error { if c.RouteServer.Config.RouteServerClient && c.RouteReflector.Config.RouteReflectorClient { return fmt.Errorf("can't be both route-server-client and route-reflector-client") } + if c.Config.AuthPassword != "" && c.TcpAo.Config.Keychain != "" { + return fmt.Errorf("TCP-AO and TCP-MD5 authentication are mutually exclusive") + } + tcpAoKeyBinding, err := s.getTcpAoKeyBinding(&c.TcpAo.Config) + if err != nil { + return err + } if s.bgpConfig.Global.Config.Port > 0 { - for _, l := range s.listListeners(addr) { - if c.Config.AuthPassword != "" { - if err := netutils.SetTCPMD5SigSockopt(l, c.Transport.Config.BindInterface, addr, c.Config.AuthPassword); err != nil { - s.logger.Warn("failed to set md5", - slog.String("Topic", "Peer"), - slog.String("Key", addr), - slog.String("Err", err.Error())) - } - } + if err := s.addAuthKeysToListeners(s.listListeners(addr), ipAddr, c, tcpAoKeyBinding); err != nil { + return err } } s.logger.Info("Add a peer configuration", @@ -3581,19 +3609,15 @@ func (s *BgpServer) addNeighbor(c *oc.Neighbor) error { if c.RouteServer.Config.RouteServerClient { rib = s.rsRib } - peer := newPeer(&s.bgpConfig.Global, c, bgp.BGP_FSM_IDLE, rib, s.policy, s.logger) + peer := newPeer(&s.bgpConfig.Global, c, bgp.BGP_FSM_IDLE, rib, s.policy, tcpAoKeyBinding, s.logger) if err := s.setPeerPolicy(peer, c.ApplyPolicy); err != nil { return fmt.Errorf("failed to set peer policy for %s: %v", addr, err) } - s.neighborMap[netip.MustParseAddr(addr)] = peer + s.neighborMap[ipAddr] = peer if name := c.Config.PeerGroup; name != "" { s.peerGroupMap[name].AddMember(*c) } if s.bfdServer != nil { - ipAddr, err := netip.ParseAddr(addr) - if err != nil { - return fmt.Errorf("failed to parse IP address: %v", err) - } if err := s.bfdServer.AddPeer(context.Background(), ipAddr, c.Bfd.Config, c.Transport.Config.BindInterface); err != nil { s.logger.Warn("failed to add BFD peer", slog.String("Topic", "Peer"), @@ -3605,6 +3629,45 @@ func (s *BgpServer) addNeighbor(c *oc.Neighbor) error { return nil } +func (s *BgpServer) getTcpAoKeyBinding(config *oc.TcpAoConfig) (*tcpAoKeyBinding, error) { + if config == nil || config.Keychain == "" { + return nil, nil + } + name := string(config.Keychain) + keychain, ok := s.keychainStore.getKeychain(name) + if !ok { + return nil, fmt.Errorf("TCP-AO keychain %q does not exist", name) + } + preferred := config.SendId + if !keychain.hasSendID(preferred) { + return nil, fmt.Errorf("TCP-AO keychain %q has no key with send ID %d", name, preferred) + } + return &tcpAoKeyBinding{keychain: keychain, preferredSendID: preferred}, nil +} + +func (s *BgpServer) addAuthKeysToListeners(listeners []*net.TCPListener, peerAddr netip.Addr, config *oc.Neighbor, tcpAoKeyBinding *tcpAoKeyBinding) error { + if tcpAoKeyBinding != nil { + tcpAoKeys, err := tcpAoKeyBinding.socketKeys() + if err != nil { + return err + } + if err := addTcpAoKeysToListeners(listeners, peerAddr, s.tcpAoBindInterface(config.Transport.Config), tcpAoKeys); err != nil { + return fmt.Errorf("failed to configure TCP-AO listener for peer %s: %w", peerAddr, err) + } + } + if config.Config.AuthPassword != "" { + for _, listener := range listeners { + if err := netutils.SetTCPMD5SigSockopt(listener, config.Transport.Config.BindInterface, peerAddr.String(), config.Config.AuthPassword); err != nil { + s.logger.Warn("failed to set md5", + slog.String("Topic", "Peer"), + slog.String("Key", peerAddr.String()), + slog.String("Err", err.Error())) + } + } + } + return nil +} + func apiBfdSessionStateToOC(state api.BfdSessionState) oc.BfdSessionState { switch state { case api.BfdSessionState_BFD_SESSION_STATE_UP: @@ -3698,6 +3761,9 @@ func (s *BgpServer) AddDynamicNeighbor(ctx context.Context, r *api.AddDynamicNei if !ok { return fmt.Errorf("no such peer-group: %s", c.Config.PeerGroup) } + if pg.Conf.TcpAo.Config.Keychain != "" { + return status.Error(codes.Unimplemented, "TCP-AO dynamic neighbors are not supported") + } pg.AddDynamicNeighbor(c) pConf := pg.Conf @@ -3736,13 +3802,6 @@ func (s *BgpServer) deletePeerGroup(name string) error { } func (s *BgpServer) deleteNeighbor(c *oc.Neighbor, code, subcode uint8, sendNotification bool) error { - if c.Config.PeerGroup != "" { - _, y := s.peerGroupMap[c.Config.PeerGroup] - if y { - s.peerGroupMap[c.Config.PeerGroup].DeleteMember(*c) - } - } - addr, err := c.ExtractNeighborAddress() if err != nil { return err @@ -3755,17 +3814,38 @@ func (s *BgpServer) deleteNeighbor(c *oc.Neighbor, code, subcode uint8, sendNoti return err } } - n, y := s.neighborMap[netip.MustParseAddr(addr)] + ipAddr, err := netip.ParseAddr(addr) + if err != nil { + return fmt.Errorf("failed to parse IP address: %v", err) + } + n, y := s.neighborMap[ipAddr] if !y { return fmt.Errorf("can't delete a peer configuration for %s", addr) } - for _, l := range s.listListeners(addr) { - if c.Config.AuthPassword != "" { - if err := netutils.SetTCPMD5SigSockopt(l, c.Transport.Config.BindInterface, addr, ""); err != nil { + + configured := n.fsm.pConf.ReadOnly() + listeners := s.listListeners(addr) + if keyBinding := n.fsm.tcpAoKeyBinding.Load(); keyBinding != nil { + tcpAoKeys, err := keyBinding.socketKeys() + if err != nil { + return err + } + for _, err := range deleteTcpAoKeysFromListeners(listeners, ipAddr, s.tcpAoBindInterface(configured.Transport.Config), tcpAoKeys) { + n.fsm.logger.Warn("failed to unset TCP-AO", slog.String("Err", err.Error())) + } + } + for _, l := range listeners { + if configured.Config.AuthPassword != "" { + if err := netutils.SetTCPMD5SigSockopt(l, configured.Transport.Config.BindInterface, addr, ""); err != nil { n.fsm.logger.Warn("failed to unset md5", slog.String("Err", err.Error())) } } } + if configured.Config.PeerGroup != "" { + if group, ok := s.peerGroupMap[configured.Config.PeerGroup]; ok { + group.DeleteMember(*configured) + } + } n.fsm.logger.Info("Delete a peer configuration") if sendNotification { @@ -3848,13 +3928,16 @@ func (s *BgpServer) DeleteDynamicNeighbor(ctx context.Context, r *api.DeleteDyna func (s *BgpServer) updatePeerGroup(pg *oc.PeerGroup) (needsSoftResetIn bool, err error) { name := pg.Config.PeerGroupName - _, ok := s.peerGroupMap[name] + group, ok := s.peerGroupMap[name] if !ok { return false, fmt.Errorf("peer-group %s doesn't exist", name) } - s.peerGroupMap[name].Conf = pg + if pg.TcpAo.Config.Keychain != "" && len(group.dynamicNeighbors) != 0 { + return false, status.Error(codes.Unimplemented, "TCP-AO dynamic neighbors are not supported") + } + group.Conf = pg - for _, n := range s.peerGroupMap[name].members { + for _, n := range group.members { u, err := s.updateNeighbor(&n) if err != nil { return needsSoftResetIn, err @@ -3974,6 +4057,26 @@ func (s *BgpServer) updateNeighbor(c *oc.Neighbor) (needsSoftResetIn bool, err e conf.Timers.Config = c.Timers.Config } + if !original.TcpAo.Config.Equal(&c.TcpAo.Config) { + keyBinding, err := s.getTcpAoKeyBinding(&c.TcpAo.Config) + if err != nil { + peer.fsm.lock.Unlock() + return needsSoftResetIn, err + } + if peer.fsm.conn != nil && keyBinding != nil { + tcpAoKeys, err := keyBinding.socketKeys() + if err == nil { + err = setTcpAoConnectionRNext(peer.fsm.conn, tcpAoKeys) + } + if err != nil { + peer.fsm.lock.Unlock() + return needsSoftResetIn, fmt.Errorf("failed changing the TCP-AO key for peer %s: %w", addr, err) + } + } + peer.fsm.tcpAoKeyBinding.Store(keyBinding) + conf.TcpAo = c.TcpAo + } + isLimit, err := peer.updatePrefixLimitConfig(&conf, c.AfiSafis) if err == nil { peer.fsm.pConf.Update(&conf) @@ -5437,11 +5540,21 @@ func (s *BgpServer) UpdateTcpAoKeychain(_ context.Context, r *api.UpdateTcpAoKey if !ok { return status.Errorf(codes.NotFound, "TCP-AO keychain %q does not exist", r.Name) } + for i, delKey := range r.DeleteKeys { + sendID, receiveID, err := tcpAoKeyIDs(r.Name, i, delKey) + if err != nil { + return err + } + if key, exists := keychain.getKey(sendID, receiveID); exists && s.tcpAoKeyConfigured(r.Name, sendID) { + return status.Errorf(codes.FailedPrecondition, "TCP-AO keychain %q key with send ID %d is configured as a preferred send key", r.Name, key.SendID) + } + } added, deleted, err := validateTcpAoKeychainUpdate(keychain, r) if err != nil { return err } keychain.updateKeys(added, deleted) + s.updateTcpAoKeychainSockets(r.Name, added, deleted) response = &api.UpdateTcpAoKeychainResponse{Keychain: keychain.toAPIKeychain()} return nil }, false) @@ -5460,6 +5573,9 @@ func (s *BgpServer) DeleteTcpAoKeychain(_ context.Context, r *api.DeleteTcpAoKey } return s.mgmtOperation(func() error { + if s.tcpAoKeychainUsed(r.Name) { + return status.Errorf(codes.FailedPrecondition, "TCP-AO keychain %q is in use", r.Name) + } if !s.keychainStore.deleteKeychain(r.Name) { return status.Errorf(codes.NotFound, "TCP-AO keychain %q does not exist", r.Name) } @@ -5501,3 +5617,125 @@ func (s *BgpServer) ListTcpAoKeychain(ctx context.Context, r *api.ListTcpAoKeych } return nil } + +func (s *BgpServer) tcpAoKeychainUsed(name string) bool { + for _, group := range s.peerGroupMap { + config := &group.Conf.TcpAo.Config + if config.Keychain != "" && string(config.Keychain) == name { + return true + } + } + for _, peer := range s.neighborMap { + config := &peer.fsm.pConf.ReadOnly().TcpAo.Config + if config.Keychain != "" && string(config.Keychain) == name { + return true + } + keyBinding := peer.fsm.tcpAoKeyBinding.Load() + if keyBinding != nil && keyBinding.keychain.name == name { + return true + } + } + return false +} + +func (s *BgpServer) tcpAoKeyConfigured(keychainName string, sendID uint8) bool { + for _, group := range s.peerGroupMap { + config := &group.Conf.TcpAo.Config + if config.Keychain != "" && string(config.Keychain) == keychainName && config.SendId == sendID { + return true + } + } + for _, peer := range s.neighborMap { + keyBinding := peer.fsm.tcpAoKeyBinding.Load() + if keyBinding != nil && keyBinding.keychain.name == keychainName && keyBinding.preferredSendID == sendID { + return true + } + } + return false +} + +func (s *BgpServer) updateTcpAoKeychainSockets(name string, added, deleted []netutils.TCPAOKey) { + if len(added) == 0 && len(deleted) == 0 { + return + } + addedKeys := newTcpAoSocketKeys(added, nil) + deletedKeys := newTcpAoSocketKeys(deleted, nil) + + logError := func(peer *peer, target string, err error) { + peer.fsm.logger.Warn("failed to update TCP-AO keys", + slog.String("Target", target), + slog.String("Error", err.Error())) + } + for _, peer := range s.neighborMap { + keyBinding := peer.fsm.tcpAoKeyBinding.Load() + if keyBinding == nil || keyBinding.keychain.name != name { + continue + } + conf := peer.fsm.pConf.ReadOnly() + addr, err := conf.ExtractNeighborAddress() + if err != nil { + logError(peer, "peer", err) + continue + } + peerAddr, err := netip.ParseAddr(addr) + if err != nil { + logError(peer, "peer", err) + continue + } + interfaceName := s.tcpAoBindInterface(conf.Transport.Config) + for _, listener := range s.listListeners(addr) { + raw, err := listener.SyscallConn() + if err != nil { + if len(deleted) != 0 { + logError(peer, "listener", fmt.Errorf("delete TCP-AO keys: %w", err)) + } + if len(added) != 0 { + logError(peer, "listener", fmt.Errorf("add TCP-AO keys: %w", err)) + } + continue + } + if len(deleted) != 0 { + if err := deleteTcpAoKeys(raw, peerAddr, interfaceName, deletedKeys); err != nil { + logError(peer, "listener", fmt.Errorf("delete TCP-AO keys: %w", err)) + } + } + if len(added) != 0 { + if err := addTcpAoKeys(raw, peerAddr, interfaceName, addedKeys, false); err != nil { + logError(peer, "listener", fmt.Errorf("add TCP-AO keys: %w", err)) + } + } + } + peer.fsm.lock.Lock() + if peer.fsm.conn != nil { + raw, err := tcpAoRawConn(peer.fsm.conn) + if err != nil { + if len(deleted) != 0 { + logError(peer, "connection", fmt.Errorf("delete TCP-AO keys: %w", err)) + } + if len(added) != 0 { + logError(peer, "connection", fmt.Errorf("add TCP-AO keys: %w", err)) + } + peer.fsm.lock.Unlock() + continue + } + if len(deleted) != 0 { + if err := deleteTcpAoKeys(raw, peerAddr, interfaceName, deletedKeys); err != nil { + logError(peer, "connection", fmt.Errorf("delete TCP-AO keys: %w", err)) + } + } + if len(added) != 0 { + if err := addTcpAoKeys(raw, peerAddr, interfaceName, addedKeys, false); err != nil { + logError(peer, "connection", fmt.Errorf("add TCP-AO keys: %w", err)) + } + } + } + peer.fsm.lock.Unlock() + } +} + +func (s *BgpServer) tcpAoBindInterface(peerConfig oc.TransportConfig) string { + if peerConfig.BindInterface != "" { + return peerConfig.BindInterface + } + return s.bgpConfig.Global.Config.BindToDevice +} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index d917ceaa7..0a362a06d 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -1649,6 +1649,7 @@ func newPeerandInfo(t *testing.T, myAs, as uint32, address string, rib *table.Ta bgp.BGP_FSM_IDLE, rib, policy, + nil, logger) rfmap := make(map[bgp.Family]bgp.BGPAddPathMode) for _, f := range rib.GetRFlist() { diff --git a/pkg/server/tcp_ao.go b/pkg/server/tcp_ao.go index 338ff9e9d..4891fe0d3 100644 --- a/pkg/server/tcp_ao.go +++ b/pkg/server/tcp_ao.go @@ -16,10 +16,15 @@ package server import ( "bytes" + "errors" + "fmt" "maps" "math" + "net" + "net/netip" "slices" "sync" + "syscall" "github.com/osrg/gobgp/v4/api" "github.com/osrg/gobgp/v4/internal/pkg/netutils" @@ -240,6 +245,14 @@ func (c *tcpAoKeychain) toAPIKeychain() *api.TcpAoKeychain { return result } +func (c *tcpAoKeychain) hasSendID(sendID uint8) bool { + c.mu.RLock() + defer c.mu.RUnlock() + + _, ok := c.keys[sendID] + return ok +} + func (c *tcpAoKeychain) getKey(sendID, receiveID uint8) (netutils.TCPAOKey, bool) { c.mu.RLock() defer c.mu.RUnlock() @@ -292,3 +305,166 @@ func (c *tcpAoKeychain) clearKeys() { } clear(c.keys) } + +func (c *tcpAoKeychain) socketKeys(preferredSendID uint8) (*tcpAoSocketKeys, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + if len(c.keys) == 0 { + return nil, status.Errorf(codes.NotFound, "TCP-AO keychain %q does not contain any key", c.name) + } + if _, ok := c.keys[preferredSendID]; !ok { + return nil, status.Errorf(codes.NotFound, "TCP-AO keychain %q has no key with send ID %d", c.name, preferredSendID) + } + keys := slices.Collect(maps.Values(c.keys)) + return newTcpAoSocketKeys(keys, &preferredSendID), nil +} + +// tcpAoSocketKeys is a short-lived TCP-AO key snapshot used for socket operations. +// Its master keys are deep-copied while the keychain is locked, allowing the +// potentially blocking socket calls to run without holding keychain lock. +// The preferred send ID is optional: listeners and key deletion only need the keys; +// active and accepted connections also select a send ID. +type tcpAoSocketKeys struct { + keys []netutils.TCPAOKey + preferredSendID *uint8 +} + +func newTcpAoSocketKeys(keys []netutils.TCPAOKey, preferredSendID *uint8) *tcpAoSocketKeys { + socketKeys := &tcpAoSocketKeys{keys: make([]netutils.TCPAOKey, 0, len(keys))} + for _, key := range keys { + key.MasterKey = append([]byte{}, key.MasterKey...) + socketKeys.keys = append(socketKeys.keys, key) + } + if preferredSendID != nil { + preferred := *preferredSendID + socketKeys.preferredSendID = &preferred + } + return socketKeys +} + +func (k *tcpAoSocketKeys) netutilsConfig(selectPreferred bool) (netutils.TCPAOConfig, error) { + if k == nil { + return netutils.TCPAOConfig{}, fmt.Errorf("missing TCP-AO socket keys") + } + result := netutils.TCPAOConfig{Keys: k.keys} + if selectPreferred { + if k.preferredSendID == nil { + return netutils.TCPAOConfig{}, fmt.Errorf("missing TCP-AO preferred send ID") + } + preferred := *k.preferredSendID + result.PreferredSendID = &preferred + } + return result, nil +} + +func addTcpAoKeys(raw syscall.RawConn, peerAddr netip.Addr, interfaceName string, socketKeys *tcpAoSocketKeys, selectPreferred bool) error { + config, err := socketKeys.netutilsConfig(selectPreferred) + if err != nil { + return err + } + return netutils.AddTCPAOKeysSockopt(raw, tcpAoPeerPrefix(peerAddr), interfaceName, config) +} + +func deleteTcpAoKeys(raw syscall.RawConn, peerAddr netip.Addr, interfaceName string, socketKeys *tcpAoSocketKeys) error { + config, err := socketKeys.netutilsConfig(false) + if err != nil { + return err + } + return netutils.DeleteTCPAOKeysSockopt(raw, tcpAoPeerPrefix(peerAddr), interfaceName, config) +} + +func addTcpAoKeysToListeners(listeners []*net.TCPListener, peerAddr netip.Addr, interfaceName string, socketKeys *tcpAoSocketKeys) error { + configured := make([]*net.TCPListener, 0, len(listeners)) + rollback := func(cause error) error { + errs := []error{cause} + for _, err := range deleteTcpAoKeysFromListeners(configured, peerAddr, interfaceName, socketKeys) { + errs = append(errs, fmt.Errorf("failed to roll back TCP-AO listener configuration: %w", err)) + } + return errors.Join(errs...) + } + for _, listener := range listeners { + raw, err := listener.SyscallConn() + if err != nil { + return rollback(err) + } + // AddTCPAOKeysSockopt installs keys one at a time and can fail + // after partially configuring the listener. + configured = append(configured, listener) + if err := addTcpAoKeys(raw, peerAddr, interfaceName, socketKeys, false); err != nil { + return rollback(err) + } + } + return nil +} + +func deleteTcpAoKeysFromListeners(listeners []*net.TCPListener, peerAddr netip.Addr, interfaceName string, socketKeys *tcpAoSocketKeys) []error { + var result []error + for _, listener := range listeners { + raw, err := listener.SyscallConn() + if err == nil { + err = deleteTcpAoKeys(raw, peerAddr, interfaceName, socketKeys) + } + if err != nil { + result = append(result, err) + } + } + return result +} + +func setTcpAoConnectionRNext(conn net.Conn, socketKeys *tcpAoSocketKeys) error { + raw, err := tcpAoRawConn(conn) + if err != nil { + return err + } + config, err := socketKeys.netutilsConfig(true) + if err != nil { + return err + } + return netutils.SetTCPAOKeySockopt(raw, config, true, false) +} + +func getTcpAoConnectionState(conn net.Conn) (*api.TcpAoPeerState, error) { + raw, err := tcpAoRawConn(conn) + if err != nil { + return nil, err + } + keyStates, err := netutils.GetTCPAOKeyStateSockopt(raw) + if err != nil { + return nil, err + } + counters, err := netutils.GetTCPAOSocketCountersSockopt(raw) + if err != nil { + return nil, err + } + state := &api.TcpAoPeerState{ + Keys: make([]*api.TcpAoKeyState, 0, len(keyStates)), + PacketsKeyNotFound: counters.PacketsKeyNotFound, + PacketsAoRequired: counters.PacketsAORequired, + PacketsDroppedIcmp: counters.PacketsDroppedICMP, + } + for _, key := range keyStates { + state.Keys = append(state.Keys, &api.TcpAoKeyState{ + SendId: uint32(key.SendID), + ReceiveId: uint32(key.ReceiveID), + Current: key.Current, + ReceiveNext: key.ReceiveNext, + PacketsGood: key.PacketsGood, + PacketsBad: key.PacketsBad, + }) + } + return state, nil +} + +func tcpAoPeerPrefix(addr netip.Addr) netip.Prefix { + addr = addr.Unmap() + return netip.PrefixFrom(addr, addr.BitLen()) +} + +func tcpAoRawConn(conn net.Conn) (syscall.RawConn, error) { + syscallConn, ok := conn.(syscall.Conn) + if !ok { + return nil, fmt.Errorf("TCP connection does not expose a syscall connection") + } + return syscallConn.SyscallConn() +} diff --git a/pkg/server/tcp_ao_test.go b/pkg/server/tcp_ao_test.go index 6c7ea4668..60370150b 100644 --- a/pkg/server/tcp_ao_test.go +++ b/pkg/server/tcp_ao_test.go @@ -17,6 +17,8 @@ package server import ( "context" "io" + "net" + "net/netip" "os" "path/filepath" "testing" @@ -320,3 +322,246 @@ func TestTcpAoKeychainOperations(t *testing.T) { _, err = stream.Recv() assert.ErrorIs(t, err, io.EOF) } + +func tcpAoTestPeer(address, chain string, preferredSendID uint32) *api.Peer { + peer := &api.Peer{ + Conf: &api.PeerConf{ + NeighborAddress: address, + PeerAsn: 65001, + }, + Transport: &api.Transport{PassiveMode: true}, + } + if chain != "" { + peer.TcpAo = &api.TcpAoPeerConfig{ + Keychain: chain, + SendId: preferredSendID, + } + } + return peer +} + +func startTcpAoTestServer(t *testing.T, opts ...ServerOption) *BgpServer { + t.Helper() + s := NewBgpServer(opts...) + go s.Serve() + require.NoError(t, s.StartBgp(context.Background(), &api.StartBgpRequest{Global: &api.Global{ + Asn: 65000, + RouterId: "192.0.2.254", + ListenPort: -1, + }})) + t.Cleanup(func() { + if s.isServing.Load() { + require.NoError(t, s.StopBgp(context.Background(), &api.StopBgpRequest{})) + } + }) + return s +} + +func addTcpAoTestKeychain(t *testing.T, s *BgpServer, name string, sendID, receiveID uint32) { + t.Helper() + chain := testTcpAoKeychain(name) + chain.Keys[0].SendId = sendID + chain.Keys[0].ReceiveId = receiveID + err := s.AddTcpAoKeychain(context.Background(), &api.AddTcpAoKeychainRequest{Keychain: chain}) + require.NoError(t, err) +} + +func TestTcpAoPeerOperations(t *testing.T) { + s := startTcpAoTestServer(t) + addTcpAoTestKeychain(t, s, "primary", 1, 2) + addTcpAoTestKeychain(t, s, "replacement", 3, 4) + + // TCP-AO and TCP-MD5 authentication are mutually exclusive. + err := s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: &api.Peer{ + Conf: &api.PeerConf{ + NeighborAddress: "192.0.2.10", + PeerAsn: 65001, + AuthPassword: "md5", + }, + Transport: &api.Transport{ + PassiveMode: true, + }, + TcpAo: &api.TcpAoPeerConfig{Keychain: "primary", SendId: 1}, + }}) + assert.Error(t, err) + + // A zoned link-local peer is accepted. + linkLocalPeer := tcpAoTestPeer("fe80::1%lo", "primary", 1) + linkLocalPeer.Transport.LocalAddress = "::" + require.NoError(t, s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: linkLocalPeer})) + require.NoError(t, s.DeletePeer(context.Background(), &api.DeletePeerRequest{Address: "fe80::1%lo"})) + + // TCP-AO can be attached to an existing peer through UpdatePeer. + plainPeerRequest := tcpAoTestPeer("192.0.2.2", "", 0) + require.NoError(t, s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: plainPeerRequest})) + _, err = s.UpdatePeer(context.Background(), &api.UpdatePeerRequest{Peer: tcpAoTestPeer("192.0.2.2", "primary", 1)}) + require.NoError(t, err) + plainPeer := s.neighborMap[netip.MustParseAddr("192.0.2.2")] + require.NotNil(t, plainPeer.fsm.tcpAoKeyBinding.Load()) + assert.Equal(t, "primary", plainPeer.fsm.tcpAoKeyBinding.Load().keychain.name) + require.NoError(t, s.DeletePeer(context.Background(), &api.DeletePeerRequest{Address: "192.0.2.2"})) + + // A newly added peer resolves its configured keychain and preferred send ID. + peerRequest := tcpAoTestPeer("192.0.2.1", "primary", 1) + require.NoError(t, s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: peerRequest})) + peer := s.neighborMap[netip.MustParseAddr("192.0.2.1")] + require.NotNil(t, peer) + keyBinding := peer.fsm.tcpAoKeyBinding.Load() + require.NotNil(t, keyBinding) + assert.Equal(t, "primary", keyBinding.keychain.name) + assert.Equal(t, uint8(1), keyBinding.preferredSendID) + + // ListPeer exposes the effective TCP-AO configuration. + var listed *api.Peer + require.NoError(t, s.ListPeer(context.Background(), &api.ListPeerRequest{Address: "192.0.2.1"}, func(peer *api.Peer) { + listed = peer + })) + require.NotNil(t, listed.GetTcpAo()) + assert.Equal(t, "primary", listed.GetTcpAo().GetKeychain()) + assert.Equal(t, uint32(1), listed.GetTcpAo().GetSendId()) + + // Omitting tcp_ao removes the configured attachment. + withoutTcpAo := tcpAoTestPeer("192.0.2.1", "", 0) + withoutTcpAo.Conf.Description = "updated" + _, err = s.UpdatePeer(context.Background(), &api.UpdatePeerRequest{Peer: withoutTcpAo}) + require.NoError(t, err) + peer = s.neighborMap[netip.MustParseAddr("192.0.2.1")] + require.NotNil(t, peer) + assert.Nil(t, peer.fsm.tcpAoKeyBinding.Load()) + assert.Empty(t, peer.fsm.pConf.ReadOnly().TcpAo.Config.Keychain) + + _, err = s.UpdatePeer(context.Background(), &api.UpdatePeerRequest{Peer: tcpAoTestPeer("192.0.2.1", "primary", 1)}) + require.NoError(t, err) + peer = s.neighborMap[netip.MustParseAddr("192.0.2.1")] + require.NotNil(t, peer.fsm.tcpAoKeyBinding.Load()) + assert.Equal(t, "primary", peer.fsm.tcpAoKeyBinding.Load().keychain.name) + + // Updating with the same attachment preserves the selected key. + sameTcpAo := tcpAoTestPeer("192.0.2.1", "primary", 1) + _, err = s.UpdatePeer(context.Background(), &api.UpdatePeerRequest{Peer: sameTcpAo}) + require.NoError(t, err) + peer = s.neighborMap[netip.MustParseAddr("192.0.2.1")] + require.NotNil(t, peer) + assert.Equal(t, uint8(1), peer.fsm.pConf.ReadOnly().TcpAo.Config.SendId) + + // Replacing the keychain recreates the peer so new sockets use the new keys. + previousPeer := peer + _, err = s.UpdatePeer(context.Background(), &api.UpdatePeerRequest{Peer: tcpAoTestPeer("192.0.2.1", "replacement", 3)}) + require.NoError(t, err) + peer = s.neighborMap[netip.MustParseAddr("192.0.2.1")] + assert.NotSame(t, previousPeer, peer) + assert.Equal(t, "replacement", peer.fsm.tcpAoKeyBinding.Load().keychain.name) + + _, err = s.UpdatePeer(context.Background(), &api.UpdatePeerRequest{Peer: tcpAoTestPeer("192.0.2.1", "primary", 1)}) + require.NoError(t, err) + peer = s.neighborMap[netip.MustParseAddr("192.0.2.1")] + assert.Equal(t, "primary", peer.fsm.tcpAoKeyBinding.Load().keychain.name) + + // A referenced keychain cannot be deleted. + err = s.DeleteTcpAoKeychain(context.Background(), &api.DeleteTcpAoKeychainRequest{Name: "primary"}) + assert.Equal(t, codes.FailedPrecondition, status.Code(err)) + + // Adding a key updates the shared binding and makes older socket snapshots stale. + _, err = s.UpdateTcpAoKeychain(context.Background(), &api.UpdateTcpAoKeychainRequest{ + Name: "primary", + AddKeys: []*api.TcpAoKey{{SendId: 5, ReceiveId: 6, Algorithm: api.TcpAoAlgorithm_TCP_AO_ALGORITHM_HMAC_SHA1_96, MasterKey: []byte("new")}}, + }) + require.NoError(t, err) + peerKeys, err := peer.fsm.tcpAoKeyBinding.Load().socketKeys() + require.NoError(t, err) + require.Len(t, peerKeys.keys, 2) + + // Selecting the newly added key rotates the peer in place. + rotated := tcpAoTestPeer("192.0.2.1", "primary", 5) + previousPeer = peer + _, err = s.UpdatePeer(context.Background(), &api.UpdatePeerRequest{Peer: rotated}) + require.NoError(t, err) + peer = s.neighborMap[netip.MustParseAddr("192.0.2.1")] + assert.Same(t, previousPeer, peer) + assert.Equal(t, uint8(5), peer.fsm.tcpAoKeyBinding.Load().preferredSendID) + assert.Equal(t, uint8(5), peer.fsm.pConf.ReadOnly().TcpAo.Config.SendId) + + // The old key can be removed after the peer has switched away from it. + _, err = s.UpdateTcpAoKeychain(context.Background(), &api.UpdateTcpAoKeychainRequest{ + Name: "primary", + DeleteKeys: []*api.TcpAoKey{{SendId: 1, ReceiveId: 2}}, + }) + require.NoError(t, err) + peerKeys, err = peer.fsm.tcpAoKeyBinding.Load().socketKeys() + require.NoError(t, err) + require.Len(t, peerKeys.keys, 1) + assert.Equal(t, uint8(5), peerKeys.keys[0].SendID) + + // The preferred and last remaining key cannot be removed. + _, err = s.UpdateTcpAoKeychain(context.Background(), &api.UpdateTcpAoKeychainRequest{ + Name: "primary", + DeleteKeys: []*api.TcpAoKey{{SendId: 5, ReceiveId: 6}}, + }) + assert.Equal(t, codes.FailedPrecondition, status.Code(err)) + + // Changing the bind interface used as the Linux VRF socket scope recreates the peer while retaining TCP-AO. + changedBindInterface := tcpAoTestPeer("192.0.2.1", "primary", 5) + changedBindInterface.Transport.BindInterface = "blue" + previousPeer = peer + _, err = s.UpdatePeer(context.Background(), &api.UpdatePeerRequest{Peer: changedBindInterface}) + require.NoError(t, err) + peer = s.neighborMap[netip.MustParseAddr("192.0.2.1")] + assert.NotSame(t, previousPeer, peer) + assert.Equal(t, "blue", peer.fsm.pConf.ReadOnly().Transport.Config.BindInterface) + assert.Equal(t, "blue", s.tcpAoBindInterface(peer.fsm.pConf.ReadOnly().Transport.Config)) + assert.Equal(t, "primary", peer.fsm.tcpAoKeyBinding.Load().keychain.name) + + // Socket synchronization is best effort: a socket failure does not reject a + // valid keychain update or roll back the stored key. + failedSocket, failedPeer := net.Pipe() + peer.fsm.lock.Lock() + peer.fsm.conn = failedSocket + peer.fsm.lock.Unlock() + _, err = s.UpdateTcpAoKeychain(context.Background(), &api.UpdateTcpAoKeychainRequest{ + Name: "primary", + AddKeys: []*api.TcpAoKey{{SendId: 7, ReceiveId: 8, Algorithm: api.TcpAoAlgorithm_TCP_AO_ALGORITHM_HMAC_SHA1_96, MasterKey: []byte("newest")}}, + }) + require.NoError(t, err) + assert.True(t, peer.fsm.tcpAoKeyBinding.Load().keychain.hasSendID(7)) + peer.fsm.lock.Lock() + peer.fsm.conn = nil + peer.fsm.lock.Unlock() + _ = failedSocket.Close() + _ = failedPeer.Close() + + require.NoError(t, s.DeletePeer(context.Background(), &api.DeletePeerRequest{Address: "192.0.2.1"})) + require.NoError(t, s.DeleteTcpAoKeychain(context.Background(), &api.DeleteTcpAoKeychainRequest{Name: "primary"})) + + // A peer in a logical VRF resolves and retains its TCP-AO keychain binding. + addTcpAoTestKeychain(t, s, "vrf-chain", 1, 2) + addVrf(t, s, "blue", "65000:100", []string{"65000:100"}, []string{"65000:100"}, 1) + + peerRequest = tcpAoTestPeer("192.0.2.20", "vrf-chain", 1) + peerRequest.Conf.Vrf = "blue" + require.NoError(t, s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: peerRequest})) + + peer = s.neighborMap[netip.MustParseAddr("192.0.2.20")] + require.NotNil(t, peer) + assert.Equal(t, "blue", peer.fsm.pConf.ReadOnly().Config.Vrf) + keyBinding = peer.fsm.tcpAoKeyBinding.Load() + require.NotNil(t, keyBinding) + assert.Equal(t, "vrf-chain", keyBinding.keychain.name) + + // A peer inherits the TCP-AO attachment from its peer group. + addTcpAoTestKeychain(t, s, "group-chain", 1, 2) + require.NoError(t, s.AddPeerGroup(context.Background(), &api.AddPeerGroupRequest{PeerGroup: &api.PeerGroup{ + Conf: &api.PeerGroupConf{ + PeerGroupName: "ao-group", + PeerAsn: 65001, + }, + TcpAo: &api.TcpAoPeerConfig{Keychain: "group-chain", SendId: 1}, + }})) + groupPeer := tcpAoTestPeer("192.0.2.11", "", 0) + groupPeer.Conf.PeerGroup = "ao-group" + require.NoError(t, s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: groupPeer})) + peer = s.neighborMap[netip.MustParseAddr("192.0.2.11")] + keyBinding = peer.fsm.tcpAoKeyBinding.Load() + require.NotNil(t, keyBinding) + assert.Equal(t, "group-chain", keyBinding.keychain.name) + assert.Equal(t, "group-chain", string(peer.fsm.pConf.ReadOnly().TcpAo.Config.Keychain)) +}