diff --git a/pkg/agent/bird.go b/pkg/agent/bird.go index 03c0d25..3a38d31 100644 --- a/pkg/agent/bird.go +++ b/pkg/agent/bird.go @@ -44,12 +44,24 @@ func (b *BirdManager) Render(nc *flockv1alpha1.NodeConfig, anycast6, anycast4 [] Anycast6: anycast6, Anycast4: anycast4, } + // Pick a local source address per family that's on the same subnet as + // the BGP peer. crt001 rejects IPv6 advertisements whose next-hop is + // link-local-only; an explicit `source address` makes BIRD use a + // global next-hop self, which Cisco accepts. for _, p := range nc.Spec.BGP.Peers { fam := bird.FamilyOf(p.Address) if fam == "" { continue } in.Peers = append(in.Peers, bird.Peer{Family: fam, Address: p.Address, ASN: p.ASN}) + if local := localAddrSameSubnet(p.Address); local != "" { + if fam == "v6" && in.LocalV6 == "" { + in.LocalV6 = local + } + if fam == "v4" && in.LocalV4 == "" { + in.LocalV4 = local + } + } } cfg, err := bird.Render(in) @@ -126,6 +138,49 @@ func (b *BirdManager) SummaryRoutes(nc *flockv1alpha1.NodeConfig) error { return nil } +// localAddrSameSubnet finds an IP on a local interface that's in the same +// /64 (v6) or /24 (v4) as `peer`. Returns "" if none. Used to derive the +// `source address` for a BGP session. +func localAddrSameSubnet(peer string) string { + pip := net.ParseIP(peer) + if pip == nil { + return "" + } + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + v4 := pip.To4() != nil + for _, a := range addrs { + ipn, ok := a.(*net.IPNet) + if !ok { + continue + } + ip := ipn.IP + if ip.IsLoopback() || ip.IsLinkLocalUnicast() { + continue + } + if (ip.To4() != nil) != v4 { + continue + } + // Use the peer's mask (assume same subnet) for membership test. + var mask net.IPMask + if v4 { + mask = net.CIDRMask(24, 32) + } else { + mask = net.CIDRMask(64, 128) + } + peerSubnet := &net.IPNet{IP: pip, Mask: mask} + if peerSubnet.Contains(ip) { + if v4 { + return ip.To4().String() + } + return ip.To16().String() + } + } + return "" +} + func installBlackhole(cidr string) error { // Use `ip` rather than netlink so this file stays portable for non-Linux // builds (the agent on macOS just no-ops). The agent only runs in diff --git a/pkg/routing/bird/config.go b/pkg/routing/bird/config.go index a97f35d..e7a6845 100644 --- a/pkg/routing/bird/config.go +++ b/pkg/routing/bird/config.go @@ -18,6 +18,13 @@ type NodeBGP struct { RouterID string // IPv4 (any usable v4 on the node, typically the host's) LocalASN uint32 Peers []Peer + // LocalV6 / LocalV4 are this node's local source addresses on the + // same subnet as the v6 / v4 BGP peers. Used as `source address` in + // each BGP protocol stanza (Cisco rejects v6 advertisements whose + // next-hop is link-local-only — explicit source forces a global next- + // hop self that crt001 accepts). + LocalV6 string + LocalV4 string // CIDR6 / CIDR4 are the per-node summary aggregates the agent wants // advertised. The agent installs blackhole kernel routes for each so // BIRD's protocol kernel imports them. @@ -69,11 +76,12 @@ protocol static static4 { } {{range $i, $p := .Peers}}{{if eq $p.Family "v6"}} protocol bgp upstream6_{{$i}} { - local as {{$.LocalASN}}; + local{{if $.LocalV6}} {{$.LocalV6}}{{end}} as {{$.LocalASN}}; neighbor {{$p.Address}} as {{$p.ASN}}; graceful restart; ipv6 { import all; + next hop self; export filter { {{range $cidr := $.CIDR6}}if net = {{$cidr}} then accept; {{end}}{{range $a := $.Anycast6}}if net = {{$a}}/128 then accept; @@ -83,11 +91,12 @@ protocol bgp upstream6_{{$i}} { } {{else if eq $p.Family "v4"}} protocol bgp upstream4_{{$i}} { - local as {{$.LocalASN}}; + local{{if $.LocalV4}} {{$.LocalV4}}{{end}} as {{$.LocalASN}}; neighbor {{$p.Address}} as {{$p.ASN}}; graceful restart; ipv4 { import all; + next hop self; export filter { {{range $cidr := $.CIDR4}}if net = {{$cidr}} then accept; {{end}}{{range $a := $.Anycast4}}if net = {{$a}}/32 then accept;