// Package bird renders BIRD2 configuration for flock-agent. The agent // writes the rendered file to a shared volume; the bird sidecar reads it, // and the agent calls birdc reload (over the shared birdc unix socket) on // changes. package bird import ( "bytes" "fmt" "net" "sort" "text/template" ) // NodeBGP describes the inputs needed to render a node's BIRD config. type NodeBGP struct { NodeName string 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. CIDR6 []string CIDR4 []string // Anycast6/4 are the currently-Ready anycast /128 and /32 addresses. Anycast6 []string Anycast4 []string } type Peer struct { // Family is "v6" or "v4". Family string Address string ASN uint32 } const tpl = `# Generated by flock-agent. DO NOT EDIT. log syslog all; router id {{.RouterID}}; protocol device { scan time 10; } protocol direct { interface "lo"; } protocol kernel kernel6 { learn; ipv6 { import all; export all; }; } protocol kernel kernel4 { learn; ipv4 { import all; export all; }; } protocol static static6 { ipv6; {{range $cidr := .CIDR6}}route {{$cidr}} blackhole; {{end}} } protocol static static4 { ipv4; {{range $cidr := .CIDR4}}route {{$cidr}} blackhole; {{end}} } {{range $i, $p := .Peers}}{{if eq $p.Family "v6"}} protocol bgp upstream6_{{$i}} { 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; {{end}}reject; }; }; } {{else if eq $p.Family "v4"}} protocol bgp upstream4_{{$i}} { 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; {{end}}reject; }; }; } {{end}}{{end}}` // Render produces the bird.conf text. func Render(in NodeBGP) (string, error) { if in.RouterID == "" { return "", fmt.Errorf("RouterID is required") } if in.LocalASN == 0 { return "", fmt.Errorf("LocalASN is required") } // Stable order — important so config changes only when something real // changes (avoids needless birdc reloads). in = normalize(in) t, err := template.New("bird").Parse(tpl) if err != nil { return "", err } var buf bytes.Buffer if err := t.Execute(&buf, in); err != nil { return "", err } return buf.String(), nil } func normalize(in NodeBGP) NodeBGP { cp := in cp.CIDR6 = sortedUnique(in.CIDR6) cp.CIDR4 = sortedUnique(in.CIDR4) cp.Anycast6 = sortedUnique(in.Anycast6) cp.Anycast4 = sortedUnique(in.Anycast4) cp.Peers = append([]Peer(nil), in.Peers...) sort.SliceStable(cp.Peers, func(i, j int) bool { if cp.Peers[i].Family != cp.Peers[j].Family { return cp.Peers[i].Family < cp.Peers[j].Family } return cp.Peers[i].Address < cp.Peers[j].Address }) return cp } func sortedUnique(s []string) []string { if len(s) == 0 { return nil } cp := append([]string(nil), s...) sort.Strings(cp) out := cp[:0] for i, v := range cp { if i == 0 || v != cp[i-1] { out = append(out, v) } } return out } // FamilyOf returns "v6" or "v4" for a peer address string. func FamilyOf(addr string) string { ip := net.ParseIP(addr) if ip == nil { return "" } if ip.To4() != nil { return "v4" } return "v6" }