Repository: metallb/metallb
Pull request #1014 (2021-10-30 14:43:16)

Metallb FRR integration
This PR connects the dots in FRR integration by collecting the remaining bits from https://github.com/metallb/metallb/pull/969 and:

- adding the reloader to the speaker image
- implementing the reload function
- adding a metallb_frr.yaml manifest to deploy metallb in frr experimental mode
- adding a CI lane that tests bgp in frr mode

fedepaol: Double checked the artifacts, at least the frr-ipv4 lane is really running in frr mode


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index b3c71499..04a0909f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -27,18 +27,23 @@ jobs:
     parameters:
       ip-family:
         type: string
+      bgp-type:
+        type: string
     machine:
       image: ubuntu-2004:202104-01
     steps:
       - setup_ci_image
       - setup_kind
       - run: inv build
-      - run: inv dev-env -i  << parameters.ip-family >>
+      - run: inv dev-env -i  << parameters.ip-family >> -b  << parameters.bgp-type >>
       - run: |
-          if [ "<< parameters.ip-family >>" = "ipv4" ]; then SKIP="--skip IPV6"; fi
-          if [ "<< parameters.ip-family >>" = "dual" ]; then SKIP="--skip IPV6"; fi # TODO: until dual stack is supported, we can't run the ipv6 tests against the cluster
-          if [ "<< parameters.ip-family >>" = "ipv6" ]; then SKIP="--skip IPV4\|BGP"; fi # TODO: we are currently skipping BGP for ipv6, re enable when we enable the support
-          inv e2etest $SKIP -e /tmp/kind_logs
+          SKIP="none"
+          if [ "<< parameters.bgp-type >>" = "frr" ]; then SKIP="$SKIP\|metrics"; fi # TODO: enable metrics tests when implementing in FRR
+          if [ "<< parameters.ip-family >>" = "ipv4" ]; then SKIP="$SKIP\|IPV6"; fi
+          if [ "<< parameters.ip-family >>" = "dual" ]; then SKIP="$SKIP\|IPV6"; fi # TODO: until dual stack is supported, we can't run the ipv6 tests against the cluster
+          if [ "<< parameters.ip-family >>" = "ipv6" ]; then SKIP="$SKIP\|IPV4\|BGP"; fi # TODO: we are currently skipping BGP for ipv6, re enable when we enable the support
+          echo "Skipping $SKIP"
+          inv e2etest --skip $SKIP -e /tmp/kind_logs
       - store_artifacts:
           path: /tmp/kind_logs
   lint-1-16:
@@ -126,6 +131,7 @@ workflows:
           matrix:
             parameters:
               ip-family: [ipv4, ipv6, dual]
+              bgp-type: [native, frr]
       - publish-images:
           filters:
             branches:
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index 1ea8839f..23f6c767 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -7,10 +7,13 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"strconv"
+	"syscall"
 	"text/template"
 )
 
-var configFileName = "/etc/frr/frr.conf"
+var configFileName = "/etc/frr_reloader/frr.conf"
+var reloaderPidFileName = "/etc/frr_reloader/reloader.pid"
 
 // TODO: We will probably need to update this template when we start to
 // integrate with FRR. The current template is a reasonable first pass
@@ -35,7 +38,16 @@ router bgp {{.MyASN}}
   neighbor {{.Addr}} port {{.Port}}
 {{- end }}
 {{range $n := .Neighbors -}}
-{{range .Advertisements }}
+{{/* no bgp default ipv4-unicast prevents peering if no address families are defined. We declare an ipv4 one for the peer to make the pairing happen */}}
+{{- if eq (len .Advertisements) 0}}
+  address-family ipv4 unicast
+    neighbor {{$n.Addr}} activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor {{$n.Addr}} activate
+  exit-address-family
+{{- end}}
+{{- range .Advertisements }}
   address-family {{.Version}} unicast
     neighbor {{$n.Addr}} activate
     network {{.Prefix}}
@@ -103,8 +115,28 @@ func writeConfig(config string, filename string) error {
 
 // reloadConfig requests that FRR reloads the configuration file. This is
 // called after updating the configuration.
-func reloadConfig() error {
-	// TODO
+var reloadConfig = func() error {
+	pidFile, found := os.LookupEnv("FRR_RELOADER_PID_FILE")
+	if found {
+		reloaderPidFileName = pidFile
+	}
+
+	pid, err := os.ReadFile(reloaderPidFileName)
+	if err != nil {
+		return err
+	}
+
+	pidInt, err := strconv.Atoi(string(pid))
+	if err != nil {
+		return err
+	}
+
+	// send HUP signal to FRR reloader
+	err = syscall.Kill(pidInt, syscall.SIGHUP)
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 
diff --git a/manifests/metallb-frr.yaml b/manifests/metallb-frr.yaml
new file mode 100644
index 00000000..b39cc836
--- /dev/null
+++ b/manifests/metallb-frr.yaml
@@ -0,0 +1,619 @@
+apiVersion: policy/v1beta1
+kind: PodSecurityPolicy
+metadata:
+  labels:
+    app: metallb
+  name: controller
+  namespace: metallb-system
+spec:
+  allowPrivilegeEscalation: false
+  allowedCapabilities: []
+  allowedHostPaths: []
+  defaultAddCapabilities: []
+  defaultAllowPrivilegeEscalation: false
+  fsGroup:
+    ranges:
+    - max: 65535
+      min: 1
+    rule: MustRunAs
+  hostIPC: false
+  hostNetwork: false
+  hostPID: false
+  privileged: false
+  readOnlyRootFilesystem: true
+  requiredDropCapabilities:
+  - ALL
+  runAsUser:
+    ranges:
+    - max: 65535
+      min: 1
+    rule: MustRunAs
+  seLinux:
+    rule: RunAsAny
+  supplementalGroups:
+    ranges:
+    - max: 65535
+      min: 1
+    rule: MustRunAs
+  volumes:
+  - configMap
+  - secret
+  - emptyDir
+---
+apiVersion: policy/v1beta1
+kind: PodSecurityPolicy
+metadata:
+  labels:
+    app: metallb
+  name: speaker
+  namespace: metallb-system
+spec:
+  allowPrivilegeEscalation: false
+  allowedCapabilities:
+  - NET_RAW
+  allowedHostPaths: []
+  defaultAddCapabilities: []
+  defaultAllowPrivilegeEscalation: false
+  fsGroup:
+    rule: RunAsAny
+  hostIPC: false
+  hostNetwork: true
+  hostPID: false
+  hostPorts:
+  - max: 7472
+    min: 7472
+  - max: 7946
+    min: 7946
+  privileged: true
+  readOnlyRootFilesystem: true
+  requiredDropCapabilities:
+  - ALL
+  runAsUser:
+    rule: RunAsAny
+  seLinux:
+    rule: RunAsAny
+  supplementalGroups:
+    rule: RunAsAny
+  volumes:
+  - configMap
+  - secret
+  - emptyDir
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  labels:
+    app: metallb
+  name: controller
+  namespace: metallb-system
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  labels:
+    app: metallb
+  name: speaker
+  namespace: metallb-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app: metallb
+  name: metallb-system:controller
+rules:
+- apiGroups:
+  - ''
+  resources:
+  - services
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ''
+  resources:
+  - services/status
+  verbs:
+  - update
+- apiGroups:
+  - ''
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
+- apiGroups:
+  - policy
+  resourceNames:
+  - controller
+  resources:
+  - podsecuritypolicies
+  verbs:
+  - use
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app: metallb
+  name: metallb-system:speaker
+rules:
+- apiGroups:
+  - ''
+  resources:
+  - services
+  - endpoints
+  - nodes
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups: ["discovery.k8s.io"]
+  resources:
+  - endpointslices
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ''
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
+- apiGroups:
+  - policy
+  resourceNames:
+  - speaker
+  resources:
+  - podsecuritypolicies
+  verbs:
+  - use
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app: metallb
+  name: config-watcher
+  namespace: metallb-system
+rules:
+- apiGroups:
+  - ''
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - list
+  - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app: metallb
+  name: pod-lister
+  namespace: metallb-system
+rules:
+- apiGroups:
+  - ''
+  resources:
+  - pods
+  verbs:
+  - list
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app: metallb
+  name: controller
+  namespace: metallb-system
+rules:
+- apiGroups:
+  - ''
+  resources:
+  - secrets
+  verbs:
+  - create
+- apiGroups:
+  - ''
+  resources:
+  - secrets
+  resourceNames:
+  - memberlist
+  verbs:
+  - list
+- apiGroups:
+  - apps
+  resources:
+  - deployments
+  resourceNames:
+  - controller
+  verbs:
+  - get
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: metallb-system:controller
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: metallb-system:controller
+subjects:
+- kind: ServiceAccount
+  name: controller
+  namespace: metallb-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: metallb-system:speaker
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: metallb-system:speaker
+subjects:
+- kind: ServiceAccount
+  name: speaker
+  namespace: metallb-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: config-watcher
+  namespace: metallb-system
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: config-watcher
+subjects:
+- kind: ServiceAccount
+  name: controller
+- kind: ServiceAccount
+  name: speaker
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: pod-lister
+  namespace: metallb-system
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: pod-lister
+subjects:
+- kind: ServiceAccount
+  name: speaker
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app: metallb
+  name: controller
+  namespace: metallb-system
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: controller
+subjects:
+- kind: ServiceAccount
+  name: controller
+---
+# FRR expects to have these files owned by frr:frr on startup.
+# Having them in a ConfigMap allows us to modify behaviors: for example enabling more daemons on startup.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: frr-startup
+  namespace: metallb-system
+data:
+  daemons: |
+    # This file tells the frr package which daemons to start.
+    #
+    # Sample configurations for these daemons can be found in
+    # /usr/share/doc/frr/examples/.
+    #
+    # ATTENTION:
+    #
+    # When activating a daemon for the first time, a config file, even if it is
+    # empty, has to be present *and* be owned by the user and group "frr", else
+    # the daemon will not be started by /etc/init.d/frr. The permissions should
+    # be u=rw,g=r,o=.
+    # When using "vtysh" such a config file is also needed. It should be owned by
+    # group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too.
+    #
+    # The watchfrr and zebra daemons are always started.
+    #
+    bgpd=yes
+    ospfd=no
+    ospf6d=no
+    ripd=no
+    ripngd=no
+    isisd=no
+    pimd=no
+    ldpd=no
+    nhrpd=no
+    eigrpd=no
+    babeld=no
+    sharpd=no
+    pbrd=no
+    bfdd=no
+    fabricd=no
+    vrrpd=no
+
+    #
+    # If this option is set the /etc/init.d/frr script automatically loads
+    # the config via "vtysh -b" when the servers are started.
+    # Check /etc/pam.d/frr if you intend to use "vtysh"!
+    #
+    vtysh_enable=yes
+    zebra_options="  -A 127.0.0.1 -s 90000000"
+    bgpd_options="   -A 127.0.0.1"
+    ospfd_options="  -A 127.0.0.1"
+    ospf6d_options=" -A ::1"
+    ripd_options="   -A 127.0.0.1"
+    ripngd_options=" -A ::1"
+    isisd_options="  -A 127.0.0.1"
+    pimd_options="   -A 127.0.0.1"
+    ldpd_options="   -A 127.0.0.1"
+    nhrpd_options="  -A 127.0.0.1"
+    eigrpd_options=" -A 127.0.0.1"
+    babeld_options=" -A 127.0.0.1"
+    sharpd_options=" -A 127.0.0.1"
+    pbrd_options="   -A 127.0.0.1"
+    staticd_options="-A 127.0.0.1"
+    bfdd_options="   -A 127.0.0.1"
+    fabricd_options="-A 127.0.0.1"
+    vrrpd_options="  -A 127.0.0.1"
+
+    # configuration profile
+    #
+    #frr_profile="traditional"
+    #frr_profile="datacenter"
+
+    #
+    # This is the maximum number of FD's that will be available.
+    # Upon startup this is read by the control files and ulimit
+    # is called. Uncomment and use a reasonable value for your
+    # setup if you are expecting a large number of peers in
+    # say BGP.
+    #MAX_FDS=1024
+
+    # The list of daemons to watch is automatically generated by the init script.
+    #watchfrr_options=""
+
+    # for debugging purposes, you can specify a "wrap" command to start instead
+    # of starting the daemon directly, e.g. to use valgrind on ospfd:
+    #   ospfd_wrap="/usr/bin/valgrind"
+    # or you can use "all_wrap" for all daemons, e.g. to use perf record:
+    #   all_wrap="/usr/bin/perf record --call-graph -"
+    # the normal daemon command is added to this at the end.
+  vtysh.conf: ""
+  zebra.conf: |+
+    ! -*- zebra -*-
+    !
+    ! zebra sample configuration file
+    !
+    hostname Router
+    password zebra
+    enable password zebra
+    !
+    ! Interface's description.
+    !
+    !interface lo
+    ! description test of desc.
+    !
+    !interface sit0
+    ! multicast
+
+    !
+    ! Static default route sample.
+    !
+    !ip route 0.0.0.0/0 203.181.89.241
+    !
+
+    !log file zebra.log
+
+---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  labels:
+    app: metallb
+    component: speaker
+  name: speaker
+  namespace: metallb-system
+spec:
+  selector:
+    matchLabels:
+      app: metallb
+      component: speaker
+  template:
+    metadata:
+      annotations:
+        prometheus.io/port: '7472'
+        prometheus.io/scrape: 'true'
+      labels:
+        app: metallb
+        component: speaker
+    spec:
+      volumes:
+        - name: frr-sockets
+          emptyDir: {}
+        - name: frr-startup
+          configMap:
+            name: frr-startup
+        - name: frr-conf
+          emptyDir: {}
+        - name: reloader
+          emptyDir: {}
+      initContainers:
+        # Copies the initial config files with the right permissions to the shared volume.
+        - name: cp-frr-files
+          securityContext:
+            runAsUser: 100
+            runAsGroup: 101
+          image: frrouting/frr:v7.5.1
+          command: ["/bin/sh", "-c", "cp -rLf /tmp/frr/* /etc/frr/"]
+          volumeMounts:
+            - name: frr-startup
+              mountPath: /tmp/frr
+            - name: frr-conf
+              mountPath: /etc/frr
+        # Copies the reloader to the shared volume between the speaker and reloader.
+        - name: cp-reloader
+          image: quay.io/metallb/speaker:main
+          command: ["/bin/sh", "-c", "cp -f /frr-reloader.sh /etc/frr_reloader/"]
+          volumeMounts:
+            - name: reloader
+              mountPath: /etc/frr_reloader
+      containers:
+        - name: frr
+          securityContext:
+            capabilities:
+              add: ["NET_ADMIN", "NET_RAW", "SYS_ADMIN", "NET_BIND_SERVICE"]
+          image: frrouting/frr:v7.5.1
+          env:
+            - name: TINI_SUBREAPER
+              value: "true"
+          volumeMounts:
+            - name: frr-sockets
+              mountPath: /var/run/frr
+            - name: frr-conf
+              mountPath: /etc/frr
+        - name: reloader
+          image: frrouting/frr:v7.5.1
+          command: ["/etc/frr_reloader/frr-reloader.sh"]
+          volumeMounts:
+            - name: frr-sockets
+              mountPath: /var/run/frr
+            - name: frr-conf
+              mountPath: /etc/frr
+            - name: reloader
+              mountPath: /etc/frr_reloader
+        - args:
+            - --port=7472
+            - --config=config
+            - --log-level=info
+          env:
+            - name: METALLB_NODE_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: spec.nodeName
+            - name: METALLB_HOST
+              valueFrom:
+                fieldRef:
+                  fieldPath: status.hostIP
+            - name: METALLB_ML_BIND_ADDR
+              valueFrom:
+                fieldRef:
+                  fieldPath: status.podIP
+            # needed when another software is also using memberlist / port 7946
+            # when changing this default you also need to update the container ports definition
+            # and the PodSecurityPolicy hostPorts definition
+            #- name: METALLB_ML_BIND_PORT
+            #  value: "7946"
+            - name: METALLB_ML_LABELS
+              value: "app=metallb,component=speaker"
+            - name: METALLB_ML_SECRET_KEY
+              valueFrom:
+                secretKeyRef:
+                  name: memberlist
+                  key: secretkey
+          image: quay.io/metallb/speaker:main
+          name: speaker
+          volumeMounts:
+            - name: reloader
+              mountPath: /etc/frr_reloader
+          ports:
+            - containerPort: 7472
+              name: monitoring
+            - containerPort: 7946
+              name: memberlist-tcp
+            - containerPort: 7946
+              name: memberlist-udp
+              protocol: UDP
+          securityContext:
+            allowPrivilegeEscalation: false
+            capabilities:
+              add:
+                - NET_RAW
+              drop:
+                - ALL
+            readOnlyRootFilesystem: true
+      hostNetwork: true
+      shareProcessNamespace: true
+      nodeSelector:
+        kubernetes.io/os: linux
+      serviceAccountName: speaker
+      terminationGracePeriodSeconds: 2
+      tolerations:
+        - effect: NoSchedule
+          key: node-role.kubernetes.io/master
+          operator: Exists
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: metallb
+    component: controller
+  name: controller
+  namespace: metallb-system
+spec:
+  revisionHistoryLimit: 3
+  selector:
+    matchLabels:
+      app: metallb
+      component: controller
+  template:
+    metadata:
+      annotations:
+        prometheus.io/port: '7472'
+        prometheus.io/scrape: 'true'
+      labels:
+        app: metallb
+        component: controller
+    spec:
+      containers:
+      - args:
+        - --port=7472
+        - --config=config
+        - --log-level=info
+        env:
+        - name: METALLB_ML_SECRET_NAME
+          value: memberlist
+        - name: METALLB_DEPLOYMENT
+          value: controller
+        image: quay.io/metallb/controller:main
+        name: controller
+        ports:
+        - containerPort: 7472
+          name: monitoring
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            drop:
+            - all
+          readOnlyRootFilesystem: true
+      nodeSelector:
+        kubernetes.io/os: linux
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 65534
+        fsGroup: 65534
+      serviceAccountName: controller
+      terminationGracePeriodSeconds: 0
diff --git a/speaker/Dockerfile b/speaker/Dockerfile
index caa64aa0..d62d0fa5 100644
--- a/speaker/Dockerfile
+++ b/speaker/Dockerfile
@@ -1,10 +1,11 @@
 FROM alpine:latest
 
+ADD frr-reloader.sh /frr-reloader.sh
 ADD speaker /speaker
 
 # When running as non root and building in an environment that `umask` masks out
 # '+x' for others, it won't be possible to execute. Make sure all can execute,
 # just in case
-RUN chmod a+x /speaker
+RUN chmod a+x /speaker /frr-reloader.sh
 
 ENTRYPOINT ["/speaker"]
diff --git a/tasks.py b/tasks.py
index 3518e3df..71da7ccf 100644
--- a/tasks.py
+++ b/tasks.py
@@ -145,6 +145,9 @@ def build(ctx, binaries, architectures, registry="quay.io", repo="metallb", tag=
             "GOARM": "6",
             "GO111MODULE": "on",
         }
+        if "speaker" in binaries:
+            shutil.copy("frr-reloader/frr-reloader.sh","build/{arch}/speaker/".format(arch=arch))
+
         for bin in binaries:
             run("go build -v -o build/{arch}/{bin}/{bin} -ldflags "
                 "'-X go.universe.tf/metallb/internal/version.gitCommit={commit} "
@@ -248,9 +251,12 @@ def validate_kind_version():
     "node_img": "Optional node image to use for the kind cluster (e.g. kindest/node:v1.18.19)."
                 "The node image drives the kubernetes version used in kind.",
     "ip_family": "Optional ipfamily of the cluster."
-                 "Default: ipv4, supported families are 'ipv6' and 'dual'."
+                 "Default: ipv4, supported families are 'ipv6' and 'dual'.",
+    "bgp_type": "Type of BGP implementation to use."
+                "Supported: 'native' (default), 'frr'"
 })
-def dev_env(ctx, architecture="amd64", name="kind", cni=None, protocol=None, node_img=None, ip_family="ipv4"):
+def dev_env(ctx, architecture="amd64", name="kind", cni=None, protocol=None,
+        node_img=None, ip_family="ipv4", bgp_type="native"):
     """Build and run MetalLB in a local Kind cluster.
 
     If the cluster specified by --name (default "kind") doesn't exist,
@@ -307,7 +313,10 @@ def dev_env(ctx, architecture="amd64", name="kind", cni=None, protocol=None, nod
         # Copy namespace manifest.
         shutil.copy(manifests_dir + "/namespace.yaml", tmpdir)
 
-        with open(manifests_dir + "/metallb.yaml") as f:
+        # FIXME: This is a hack to get the correct manifest file.
+        manifest_filename = "metallb-frr.yaml" if bgp_type == "frr" else "metallb.yaml"
+        # open file and replace the protocol with the one specified by the user
+        with open(manifests_dir + "/" + manifest_filename) as f:
             manifest = f.read()
         for image in binaries:
             manifest = re.sub("image: quay.io/metallb/{}:.*".format(image),


Test patch:
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index 6b4ab556..8d2ab001 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -57,6 +57,8 @@ func testSetup(t *testing.T) {
 	osHostname = testOsHostname
 
 	os.Setenv("FRR_CONFIG_FILE", configFile)
+	// override reloadConfig so it doesn't try to reload it.
+	reloadConfig = func() error { return nil }
 }
 
 func testCheckConfigFile(t *testing.T) {
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
index 2bdb8fd4..e33a333a 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
@@ -11,5 +11,11 @@ router bgp 100
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
 
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
 
 
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
index 2bdb8fd4..e33a333a 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
@@ -11,5 +11,11 @@ router bgp 100
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
 
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
 
 
diff --git a/internal/bgp/frr/testdata/TestSingleSession.golden b/internal/bgp/frr/testdata/TestSingleSession.golden
index 2bdb8fd4..e33a333a 100644
--- a/internal/bgp/frr/testdata/TestSingleSession.golden
+++ b/internal/bgp/frr/testdata/TestSingleSession.golden
@@ -11,5 +11,11 @@ router bgp 100
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
 
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
 
 
diff --git a/internal/bgp/frr/testdata/TestTwoSessions.golden b/internal/bgp/frr/testdata/TestTwoSessions.golden
index 336df76a..c7ddf93c 100644
--- a/internal/bgp/frr/testdata/TestTwoSessions.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessions.golden
@@ -11,6 +11,12 @@ router bgp 100
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
 
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
 
 router bgp 300
   no bgp network import-check
@@ -21,5 +27,11 @@ router bgp 300
   neighbor 10.4.4.254 remote-as 400
   neighbor 10.4.4.254 port 179
 
+  address-family ipv4 unicast
+    neighbor 10.4.4.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.4.4.254 activate
+  exit-address-family
 
 
diff --git a/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden b/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
index 2bdb8fd4..e33a333a 100644
--- a/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
@@ -11,5 +11,11 @@ router bgp 100
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
 
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
 
 
diff --git a/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden b/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
index acdfc2b3..9ef70701 100644
--- a/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
@@ -13,6 +13,18 @@ router bgp 100
   neighbor 10.4.4.254 remote-as 400
   neighbor 10.4.4.254 port 179
 
-
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+
+  address-family ipv4 unicast
+    neighbor 10.4.4.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.4.4.254 activate
+  exit-address-family