diff --git a/README.md b/README.md index 785ad2c0..b156bbb7 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ [![License](https://img.shields.io/github/license/natesales/bcg?style=for-the-badge)](https://choosealicense.com/licenses/gpl-3.0/) [![Release](https://img.shields.io/github/v/release/natesales/bcg?style=for-the-badge)](https://github.com/natesales/bcg/releases) -The automatic BIRD configuration generator with bogon, IRR, RPKI, and max prefix filtering support. +The automatic router configuration generator for BGP with bogon, IRR, RPKI, and max prefix filtering support. ### Installation -bcg depends on [bird2](https://gitlab.nic.cz/labs/bird/), [GoRTR](https://github.com/cloudflare/gortr), and [bgpq4](https://github.com/bgp/bgpq4). Make sure the `bird` and `gortr` daemons are running and `bgpq4` is in path before running bcg. Releases can be downloaded from Github and from my public code repositories - see https://github.com/natesales/repo for more info. You can also build from source by cloning the repo and running `go build`. It's recommended to run bcg every 12 hours to update IRR prefix lists and PeeringDB prefix limits. Adding `0 */12 * * * /usr/bin/bcg` to your crontab will update the filters at 12 AM and PM. If you're using ZSH you might also be interested in my [birdc completion](https://github.com/natesales/zsh-bird-completions). +bcg depends on [bird2](https://gitlab.nic.cz/labs/bird/), [GoRTR](https://github.com/cloudflare/gortr), [bgpq4](https://github.com/bgp/bgpq4), and optionally [keepalived](https://github.com/acassen/keepalived). Make sure the `bird` and `gortr` daemons are running and `bgpq4` is in path before running bcg. Releases can be downloaded from Github and from my public code repositories - see https://github.com/natesales/repo for more info. You can also build from source by cloning the repo and running `go build`. It's recommended to run bcg every 12 hours to update IRR prefix lists and PeeringDB prefix limits. Adding `0 */12 * * * /usr/bin/bcg` to your crontab will update the filters at 12 AM and PM. If you're using ZSH you might also be interested in my [birdc completion](https://github.com/natesales/zsh-bird-completions). #### Configuration @@ -101,6 +101,10 @@ bcg uses RFC 8092 BGP Large Communities Peers with type `peer` or `downstream` reject any route with a Tier 1 ASN in path ([Peerlock Lite](https://github.com/job/peerlock)). +#### VRRP + +bcg can build [keepalived](https://github.com/acassen/keepalived) configs for VRRP. To enable VRRP, add a `vrrp` config key containing a list of VRRP instances to your bcg config file. + #### Communities | Large | Meaning | @@ -132,7 +136,7 @@ Peers with type `peer` or `downstream` reject any route with a Tier 1 ASN in pat | filter-default | Should default routes be denied? | | enable-default | Add static default routes | -#### Peer Configuration Options +#### BGP Peer Configuration Options | Option | Usage | | -------------- | --------------------------------------------------------------------------------------------------------- | @@ -157,10 +161,19 @@ Peers with type `peer` or `downstream` reject any route with a Tier 1 ASN in pat | bfd | Enable BFD | | session-global | String to add to session global config | | enforce-first-as | Reject routes that don't have the peer ASN as the first ASN in path | -| enforce-peer-nexthop | Reject routes where the next hop doesn't match the neighbor address | -| export-default | Should a default route be sent over the session? (default false) | +| enforce-peer-nexthop | Reject routes where the next hop doesn't match the neighbor address | +| export-default | Should a default route be sent over the session? (default false) | | no-specifics | Don't send specific routes (default false, make sure to enable export-default or else no routes will be exported) | | allow-blackholes | Accept community (ASN,1,666) to blackhole /32 and /128 prefixes | | communities | List of BGP communities to add on export (two comma-separated values per list element; example `0,0`) | | large-communities | List of BGP large communities to add on export (three comma-separated values per list element; example `0,0,0`) | | description | Description string (just for human reference) | + +#### VRRP instance config options +| Option | Usage | +| ----------- | ------------------------------------------------------------------------------ | +| state | VRRP state (`primary` or `backup`) | +| interface | Interface to run VRRP on | +| vrrid | VRRP Router ID (must be the same for multiple routers in the same VRRP domain | +| priority | VRRP router selection priority | +| vips | List of Virtual IPs | diff --git a/internal/config/main.go b/internal/config/main.go index 936cf430..a0986bf2 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -60,12 +60,25 @@ type Peer struct { PrefixSet6 []string `yaml:"-" toml:"-" json:"-"` } +// VRRPInstance stores a VRRP instance +type VRRPInstance struct { + State string `yaml:"state" json:"state" toml:"State"` + Interface string `yaml:"interface" json:"interface" toml:"Interface"` + VRRID uint `yaml:"vrrid" json:"vrrid" toml:"VRRID"` + Priority uint8 `yaml:"priority" json:"priority" toml:"Priority"` + VIPs []string `yaml:"vips" json:"vips" toml:"VIPs"` + + VIPs4 []string `yaml:"-" json:"-" toml:"-"` + VIPs6 []string `yaml:"-" json:"-" toml:"-"` +} + // Config contains global configuration about this router and BCG instance type Config struct { Asn uint `yaml:"asn" toml:"ASN" json:"asn"` RouterId string `yaml:"router-id" toml:"Router-ID" json:"router-id"` Prefixes []string `yaml:"prefixes" toml:"Prefixes" json:"prefixes"` Peers map[string]*Peer `yaml:"peers" toml:"Peers" json:"peers"` + VRRPInstances []*VRRPInstance `yaml:"vrrp" toml:"VRRP" json:"vrrp"` IrrDb string `yaml:"irrdb" toml:"IRRDB" json:"irrdb"` RtrServer string `yaml:"rtr-server" toml:"RTR-Server" json:"rtr-server"` RtrPort int `yaml:"rtr-port" toml:"RTR-Port" json:"rtr-port"` @@ -182,5 +195,39 @@ func Load(filename string) (*Config, error) { setPeerDefaults(name, peer) } + // Parse VRRP configs + for _, vrrpInstance := range config.VRRPInstances { + // Sort VIPs by address family + for _, vip := range vrrpInstance.VIPs { + ip, _, err := net.ParseCIDR(vip) + if err != nil { + return nil, errorx.Decorate(err, "Invalid VIP") + } + + if ip.To4() == nil { // If IPv6 + vrrpInstance.VIPs6 = append(vrrpInstance.VIPs6, vip) + } else { // If IPv4 + vrrpInstance.VIPs4 = append(vrrpInstance.VIPs4, vip) + } + } + + // Validate vrrpInstance + if vrrpInstance.State == "primary" { + vrrpInstance.State = "MASTER" + } else if vrrpInstance.State == "backup" { + vrrpInstance.State = "BACKUP" + } else { + return nil, errors.New("VRRP state must be 'primary' or 'backup', unexpected " + vrrpInstance.State) + } + + if vrrpInstance.Interface == "" { + return nil, errors.New("VRRP interface is not defined") + } + + if len(vrrpInstance.VIPs) < 1 { + return nil, errors.New("VRRP instance must have at least one VIP defined") + } + } + return &config, nil // nil error } diff --git a/internal/templating/main.go b/internal/templating/main.go index 7b976d5d..e4cb7f7b 100644 --- a/internal/templating/main.go +++ b/internal/templating/main.go @@ -72,6 +72,7 @@ var funcMap = template.FuncMap{ var PeerTemplate *template.Template var GlobalTemplate *template.Template var UiTemplate *template.Template +var VRRPTemplate *template.Template // Load loads the templates from the embedded filesystem func Load(fs embed.FS) error { @@ -95,5 +96,11 @@ func Load(fs embed.FS) error { return errorx.Decorate(err, "Reading ui template") } + // Generate VRRP template + VRRPTemplate, err = template.New("").Funcs(funcMap).ParseFS(fs, "templates/vrrp.tmpl") + if err != nil { + return errorx.Decorate(err, "Reading VRRP template") + } + return nil // nil error } diff --git a/main.go b/main.go index 0e6678de..2c01d530 100644 --- a/main.go +++ b/main.go @@ -46,15 +46,16 @@ const ( // Flags var opts struct { - ConfigFile string `short:"c" long:"config" description:"Configuration file in YAML, TOML, or JSON format" default:"/etc/bcg/config.yml"` - Output string `short:"o" long:"output" description:"Directory to write output files to" default:"/etc/bird/"` - Socket string `short:"s" long:"socket" description:"BIRD control socket" default:"/run/bird/bird.ctl"` - UiFile string `short:"u" long:"ui-file" description:"File to store web UI" default:"/tmp/bcg-ui.html"` - NoUi bool `short:"n" long:"no-ui" description:"Don't generate web UI"` - Verbose bool `short:"v" long:"verbose" description:"Show verbose log messages"` - DryRun bool `short:"d" long:"dry-run" description:"Don't modify BIRD config"` - NoConfigure bool `long:"no-configure" description:"Don't configure BIRD"` - ShowVersion bool `long:"version" description:"Show version and exit"` + ConfigFile string `short:"c" long:"config" description:"Configuration file in YAML, TOML, or JSON format" default:"/etc/bcg/config.yml"` + Output string `short:"o" long:"output" description:"Directory to write output files to" default:"/etc/bird/"` + Socket string `short:"s" long:"socket" description:"BIRD control socket" default:"/run/bird/bird.ctl"` + KeepalivedConfig string `short:"k" long:"keepalived-config" description:"Configuration file for keepalived" default:"/etc/keepalived/keepalived.conf"` + UiFile string `short:"u" long:"ui-file" description:"File to store web UI" default:"/tmp/bcg-ui.html"` + NoUi bool `short:"n" long:"no-ui" description:"Don't generate web UI"` + Verbose bool `short:"v" long:"verbose" description:"Show verbose log messages"` + DryRun bool `short:"d" long:"dry-run" description:"Don't modify BIRD config"` + NoConfigure bool `long:"no-configure" description:"Don't configure BIRD"` + ShowVersion bool `long:"version" description:"Show version and exit"` } // Embedded filesystem @@ -392,6 +393,23 @@ func main() { } } + // Write VRRP config + if !opts.DryRun && len(globalConfig.VRRPInstances) > 0 { + // Create the peer specific file + peerSpecificFile, err := os.Create(path.Join(opts.KeepalivedConfig)) + if err != nil { + log.Fatalf("Create peer specific output file: %v", err) + } + + // Render the template and write to disk + err = templating.VRRPTemplate.ExecuteTemplate(peerSpecificFile, "vrrp.tmpl", globalConfig.VRRPInstances) + if err != nil { + log.Fatalf("Execute template: %v", err) + } + } else { + log.Infof("Dry run is enabled, not writing VRRP config") + } + if !opts.DryRun { if !opts.NoUi { // Create the ui output file diff --git a/templates/vrrp.tmpl b/templates/vrrp.tmpl index a2255e9f..53063956 100644 --- a/templates/vrrp.tmpl +++ b/templates/vrrp.tmpl @@ -1,12 +1,23 @@ -vrrp_instance {{ .Name }} { - state {{ .State }} - interface {{ .Interface }} - virtual_router_id {{ .VRRID }} - priority {{ .Priority }} - advert_int 1 - virtual_ipaddress { - {{- range $i, $vip := .VIPs4 }} - {{ $vip }} - {{- end }} - } +{{- range $instanceId, $instance := . -}} +vrrp_instance VRRP{{ $instanceId }} { + state {{ .State }} + interface {{ .Interface }} + virtual_router_id {{ .VRRID }} + priority {{ .Priority }} + advert_int 1 + {{- if .VIPs4 }} + virtual_ipaddress { + {{- range $i, $vip := .VIPs4 }} + {{ $vip }} + {{- end }} + } + {{- end }} + {{- if .VIPs6 }} + virtual_ipaddress_excluded { + {{- range $i, $vip := .VIPs6 }} + {{ $vip }} + {{- end }} + } + {{- end }} } +{{- end }}