package birdfmt import ( "fmt" "net/netip" "strings" ) // Parent protocol names for discovery listeners (not spawned session names). const ( DiscoveryProtocolV4 = "evobgp_discover_v4" DiscoveryProtocolV6 = "evobgp_discover_v6" ) // DynamicBGPDiscoveryOptions configures quarantine dynamic BGP listeners. type DynamicBGPDiscoveryOptions struct { RangesV4 []string // CIDR prefixes RangesV6 []string RequireExternal bool // neighbor range … external (default true) DynamicNameDigits int // default 4 } // RenderDynamicBGPDiscovery renders passive dynamic BGP quarantine listeners. // Spawned sessions use DynamicPeerProtocolPrefix ("evobgp_dyn_"). // Channel policy is always import none / export none (no announcements until approve). func RenderDynamicBGPDiscovery(opts DynamicBGPDiscoveryOptions) (string, error) { digits := opts.DynamicNameDigits if digits <= 0 { digits = 4 } var parts []string if len(opts.RangesV4) > 0 { s, err := renderDynamicDiscoveryAF(DiscoveryProtocolV4, BGPTemplateNameV4, "ipv4", opts.RangesV4, opts.RequireExternal, digits) if err != nil { return "", err } parts = append(parts, s) } if len(opts.RangesV6) > 0 { s, err := renderDynamicDiscoveryAF(DiscoveryProtocolV6, BGPTemplateNameV6, "ipv6", opts.RangesV6, opts.RequireExternal, digits) if err != nil { return "", err } parts = append(parts, s) } if len(parts) == 0 { return "", nil } return strings.Join(parts, "\n"), nil } func renderDynamicDiscoveryAF(protoName, templateName, af string, ranges []string, external bool, digits int) (string, error) { var cleaned []string for _, r := range ranges { r = strings.TrimSpace(r) if r == "" { continue } pfx, err := netip.ParsePrefix(r) if err != nil { return "", fmt.Errorf("birdfmt: invalid discovery range %q: %w", r, err) } if af == "ipv4" && !pfx.Addr().Is4() { return "", fmt.Errorf("birdfmt: discovery range %q is not IPv4", r) } if af == "ipv6" && !pfx.Addr().Is6() { return "", fmt.Errorf("birdfmt: discovery range %q is not IPv6", r) } cleaned = append(cleaned, pfx.Masked().String()) } if len(cleaned) == 0 { return "", nil } var b strings.Builder fmt.Fprintf(&b, "protocol bgp %s from %s {\n", protoName, templateName) for _, cidr := range cleaned { b.WriteString(" neighbor range ") b.WriteString(cidr) if external { b.WriteString(" external") } b.WriteString(";\n") } fmt.Fprintf(&b, " dynamic name \"%s\";\n", DynamicPeerProtocolPrefix) fmt.Fprintf(&b, " dynamic name digits %d;\n", digits) b.WriteString(" multihop;\n") b.WriteString(" passive;\n") fmt.Fprintf(&b, " %s {\n", af) b.WriteString(" import none;\n") b.WriteString(" export none;\n") b.WriteString(" };\n") b.WriteString("}\n") return b.String(), nil } // ParseDiscoveryRanges splits a settings string (comma / newline / space separated) into CIDRs. func ParseDiscoveryRanges(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" { return nil } raw = strings.ReplaceAll(raw, ",", " ") raw = strings.ReplaceAll(raw, "\n", " ") raw = strings.ReplaceAll(raw, ";", " ") fields := strings.Fields(raw) out := make([]string, 0, len(fields)) seen := map[string]struct{}{} for _, f := range fields { f = strings.TrimSpace(f) if f == "" { continue } if _, ok := seen[f]; ok { continue } seen[f] = struct{}{} out = append(out, f) } return out }