Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions internal/frr/frr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ package frr

import (
"context"
"encoding/binary"
"fmt"
"hash/crc32"
"net"
"os"
"strings"
"sync"
Expand Down Expand Up @@ -32,10 +35,11 @@ type Status struct {
}

type FRR struct {
reloadConfig chan reloadEvent
logLevel string
Status Status
onStatusChanged StatusChanged
reloadConfig chan reloadEvent
logLevel string
Status Status
onStatusChanged StatusChanged
fallbackRouterID string
sync.Mutex
}

Expand All @@ -54,10 +58,42 @@ func (f *FRR) ApplyConfig(config *Config) error {
// TODO add internal wrapper
config.Loglevel = f.logLevel
config.Hostname = hostname
if f.fallbackRouterID != "" {
for _, r := range config.Routers {
if r.RouterID == "" {
r.RouterID = f.fallbackRouterID
}
}
}
f.reloadConfig <- reloadEvent{config: config}
return nil
}

var netInterfaceAddrs = net.InterfaceAddrs

func hasIPv4Address() bool {
addrs, err := netInterfaceAddrs()
if err != nil {
return false
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok && ipnet.IP.To4() != nil {
return true
}
}
return false
}

func hashRouterID() (string, error) {
hostname, err := osHostname()
if err != nil {
return "", err
}
b := make([]byte, 4)
binary.LittleEndian.PutUint32(b, crc32.ChecksumIEEE([]byte(hostname)))
return net.IP(b).String(), nil
}

var debounceTimeout = 3 * time.Second
var failureTimeout = time.Second * 5

Expand All @@ -71,6 +107,17 @@ func NewFRR(ctx context.Context, onStatusChanged StatusChanged, logger log.Logge
return generateAndReloadConfigFile(config, logger)
}

// On IPv6-only nodes, FRR defaults router-id to 0.0.0.0 (RFC 6286 violation).
if !hasIPv4Address() {
routerID, err := hashRouterID()
if err != nil {
level.Error(logger).Log("op", "startup", "error", fmt.Errorf("failed to generate fallback router-id: %w", err))
} else {
res.fallbackRouterID = routerID
level.Info(logger).Log("op", "startup", "msg", "no IPv4 address found, using fallback router-id", "routerID", res.fallbackRouterID)
}
}

debouncer(ctx, reload, res.reloadConfig, debounceTimeout, failureTimeout, logger)
res.pollStatus(ctx, logger)
return res
Expand Down
82 changes: 82 additions & 0 deletions internal/frr/frr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"flag"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -80,6 +81,11 @@ func testSetup(t *testing.T) {
osHostname = testOsHostname
}

func testNewFRR(t *testing.T, ctx context.Context) *FRR {
t.Helper()
return NewFRR(ctx, emptyCB, log.NewNopLogger(), logging.LevelInfo)
}

func testCheckConfigFile(t *testing.T) {
configFile, goldenFile := testGenerateFileNames(t)

Expand Down Expand Up @@ -939,6 +945,82 @@ func TestSingleUnnumberedSession(t *testing.T) {
testCheckConfigFile(t)
}

func TestSingleSessionExplicitRouterID(t *testing.T) {
testSetup(t)
ctx, cancel := context.WithCancel(context.Background())
frr := testNewFRR(t, ctx)
defer cancel()

config := Config{
Routers: []*RouterConfig{
{
MyASN: 65000,
RouterID: "10.10.10.1",
Neighbors: []*NeighborConfig{
{
IPFamily: ipfamily.IPv4,
ASN: "65001",
Addr: "192.168.1.2",
Port: ptr.To[uint16](4567),
Outgoing: AllowedOut{
PrefixesV4: []string{
"192.169.1.0/24",
},
},
},
},
IPV4Prefixes: []string{"192.169.1.0/24"},
},
},
}
err := frr.ApplyConfig(&config)
if err != nil {
t.Fatalf("Failed to apply config: %s", err)
}

testCheckConfigFile(t)
}

func TestSingleSessionIPv6OnlyNode(t *testing.T) {
testSetup(t)
netInterfaceAddrs = func() ([]net.Addr, error) {
return nil, nil
}
t.Cleanup(func() { netInterfaceAddrs = net.InterfaceAddrs })

ctx, cancel := context.WithCancel(context.Background())
frr := testNewFRR(t, ctx)
defer cancel()

config := Config{
Routers: []*RouterConfig{
{
MyASN: 65000,
Neighbors: []*NeighborConfig{
{
IPFamily: ipfamily.IPv6,
ASN: "65001",
Addr: "2001:db8::1",
Port: ptr.To[uint16](179),
Outgoing: AllowedOut{
PrefixesV6: []string{
"2001:db8:1::/64",
},
},
},
},
IPV6Prefixes: []string{"2001:db8:1::/64"},
},
},
}
err := frr.ApplyConfig(&config)
if err != nil {
t.Fatalf("Failed to apply config: %s", err)
}

testCheckConfigFile(t)
}

func communityPrefixListFor(neigID, comm string, ipFamily string, prefixes ...string) CommunityPrefixList {
community, err := community.New(comm)
if err != nil {
Expand Down
54 changes: 54 additions & 0 deletions internal/frr/testdata/TestSingleSessionExplicitRouterID.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
log stdout informational
log timestamp precision 3
hostname dummyhostname
ip nht resolve-via-default
ipv6 nht resolve-via-default



ip prefix-list 192.168.1.2-allowed-ipv4 seq 1 permit 192.169.1.0/24


ipv6 prefix-list 192.168.1.2-allowed-ipv6 seq 1 deny any

route-map 192.168.1.2-out permit 1
match ip address prefix-list 192.168.1.2-allowed-ipv4

route-map 192.168.1.2-out permit 2
match ipv6 address prefix-list 192.168.1.2-allowed-ipv6





ip prefix-list 192.168.1.2-inpl-ipv4 seq 1 deny any

ipv6 prefix-list 192.168.1.2-inpl-ipv4 seq 2 deny any
route-map 192.168.1.2-in permit 3
match ip address prefix-list 192.168.1.2-inpl-ipv4
route-map 192.168.1.2-in permit 4
match ipv6 address prefix-list 192.168.1.2-inpl-ipv4

router bgp 65000
no bgp ebgp-requires-policy
no bgp network import-check
no bgp default ipv4-unicast
bgp graceful-restart preserve-fw-state

bgp router-id 10.10.10.1
neighbor 192.168.1.2 remote-as 65001
neighbor 192.168.1.2 port 4567




address-family ipv4 unicast
neighbor 192.168.1.2 activate
neighbor 192.168.1.2 route-map 192.168.1.2-in in
neighbor 192.168.1.2 route-map 192.168.1.2-out out
exit-address-family
address-family ipv4 unicast
network 192.169.1.0/24
exit-address-family


55 changes: 55 additions & 0 deletions internal/frr/testdata/TestSingleSessionIPv6OnlyNode.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
log stdout informational
log timestamp precision 3
hostname dummyhostname
ip nht resolve-via-default
ipv6 nht resolve-via-default



ip prefix-list 2001:db8::1-allowed-ipv4 seq 1 deny any


ipv6 prefix-list 2001:db8::1-allowed-ipv6 seq 1 permit 2001:db8:1::/64

route-map 2001:db8::1-out permit 1
match ip address prefix-list 2001:db8::1-allowed-ipv4

route-map 2001:db8::1-out permit 2
match ipv6 address prefix-list 2001:db8::1-allowed-ipv6





ip prefix-list 2001:db8::1-inpl-ipv6 seq 1 deny any

ipv6 prefix-list 2001:db8::1-inpl-ipv6 seq 2 deny any
route-map 2001:db8::1-in permit 3
match ip address prefix-list 2001:db8::1-inpl-ipv6
route-map 2001:db8::1-in permit 4
match ipv6 address prefix-list 2001:db8::1-inpl-ipv6

router bgp 65000
no bgp ebgp-requires-policy
no bgp network import-check
no bgp default ipv4-unicast
bgp graceful-restart preserve-fw-state

bgp router-id 167.62.253.30
neighbor 2001:db8::1 remote-as 65001
neighbor 2001:db8::1 port 179



neighbor 2001:db8::1 disable-connected-check

address-family ipv6 unicast
neighbor 2001:db8::1 activate
neighbor 2001:db8::1 route-map 2001:db8::1-in in
neighbor 2001:db8::1 route-map 2001:db8::1-out out
exit-address-family
address-family ipv6 unicast
network 2001:db8:1::/64
exit-address-family