bird: per-peer import filter rejects connected subnet
Build flock Image / build (push) Successful in 2m17s
Build flock Image / build (push) Successful in 2m17s
Without a filter, crt001's `network 2602:817:3000:A25::/64` gets
re-advertised to every peer on that subnet. bird installs the BGP /64
with metric 32, beating the kernel-connected route at 256, and all
inter-host VLAN-25 traffic hairpins through the gateway — losing PMTU
9000 and ~30x throughput. Broke Plex 2026-05-04: NFS to nas002 capped
at 7 MB/s, jumbo blackholed.
Add LocalSubnetV6/V4 (CIDR) to NodeBGP. Agent populates by masking the
peer's address to /64 (v6) or /24 (v4) — same fritzlab convention
already in localAddrSameSubnet. Render emits `import where net !=
<subnet>;` per BGP channel when set, falls back to `import all;`
otherwise so existing tests stay green.
Defence in depth: with the matching outbound route-map on crt001
(ROUTE_MAP_CLUSTER_OUT_V{4,6}) the agent now refuses the leak on its
own if the router filter ever drifts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,14 @@ type NodeBGP struct {
|
||||
// hop self that crt001 accepts).
|
||||
LocalV6 string
|
||||
LocalV4 string
|
||||
// LocalSubnetV6 / LocalSubnetV4 are the directly-connected subnets
|
||||
// (CIDR) the BGP peers live on. When set, the per-peer ipv6 / ipv4
|
||||
// channel uses `import where net != <subnet>` so the gateway can't
|
||||
// re-advertise our own connected /64 (or /24) back to us — accepting
|
||||
// it would override the kernel-connected route and hairpin all
|
||||
// inter-host traffic via the gateway.
|
||||
LocalSubnetV6 string
|
||||
LocalSubnetV4 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.
|
||||
@@ -92,7 +100,7 @@ protocol bgp upstream6_{{$i}} {
|
||||
neighbor {{$p.Address}} as {{$p.ASN}};
|
||||
graceful restart;
|
||||
ipv6 {
|
||||
import all;
|
||||
{{if $.LocalSubnetV6}}import where net != {{$.LocalSubnetV6}};{{else}}import all;{{end}}
|
||||
next hop self;
|
||||
export filter {
|
||||
{{range $cidr := $.CIDR6}}if net = {{$cidr}} then accept;
|
||||
@@ -107,7 +115,7 @@ protocol bgp upstream4_{{$i}} {
|
||||
neighbor {{$p.Address}} as {{$p.ASN}};
|
||||
graceful restart;
|
||||
ipv4 {
|
||||
import all;
|
||||
{{if $.LocalSubnetV4}}import where net != {{$.LocalSubnetV4}};{{else}}import all;{{end}}
|
||||
next hop self;
|
||||
export filter {
|
||||
{{range $cidr := $.CIDR4}}if net = {{$cidr}} then accept;
|
||||
@@ -147,6 +155,12 @@ func Render(in NodeBGP) (string, error) {
|
||||
if err := validateLocalSource(in.LocalV4, "v4"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := validateLocalSubnet(in.LocalSubnetV6, "v6"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := validateLocalSubnet(in.LocalSubnetV4, "v4"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, p := range in.Peers {
|
||||
if err := validatePeer(p); err != nil {
|
||||
return "", fmt.Errorf("bird render: peer[%d]: %w", i, err)
|
||||
@@ -263,6 +277,31 @@ func validateLocalSource(s, fam string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateLocalSubnet validates an optional LocalSubnetV6/LocalSubnetV4 CIDR.
|
||||
// Empty is allowed (no import filter); non-empty must be a parseable CIDR of
|
||||
// the matching family in canonical form (host bits zero) so the BIRD `net !=`
|
||||
// comparison matches the route the gateway re-advertises.
|
||||
func validateLocalSubnet(s, fam string) error {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
ip, n, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bird render: LocalSubnet%s %q is not a valid CIDR: %w", strings.ToUpper(fam), s, err)
|
||||
}
|
||||
if !ip.Equal(n.IP) {
|
||||
return fmt.Errorf("bird render: LocalSubnet%s %q has non-zero host bits (want %s)", strings.ToUpper(fam), s, n.String())
|
||||
}
|
||||
isV4 := n.IP.To4() != nil
|
||||
if fam == "v6" && isV4 {
|
||||
return fmt.Errorf("bird render: LocalSubnetV6 %q is IPv4", s)
|
||||
}
|
||||
if fam == "v4" && !isV4 {
|
||||
return fmt.Errorf("bird render: LocalSubnetV4 %q is IPv6", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalize(in NodeBGP) NodeBGP {
|
||||
cp := in
|
||||
cp.CIDR6 = sortedUnique(in.CIDR6)
|
||||
|
||||
Reference in New Issue
Block a user