Repository: metallb/metallb
Pull request #1028 (2021-11-03 18:04:20)

Speaker: reprocess the configuration when the ip assigned to services
When applying changes in bulk, a change in a service may be received / processed asynchronously from the configuration. We may want to be able to reprocess the configmap later, when the service is deleted.


fedepaol: Testing this PR on top of https://github.com/metallb/metallb/pull/1012
russellb: Can you add a longer explanation of the scenario you're fixing? I don't really understand it.  I see that you've changed a couple of cases to not retry ... but what exactly was the test scenario that triggered a problem?  What was the problem?  Why does this fix it?
fedepaol: I reworded the commit message, plus putting the references to the current code here

In a scenario where we erase the config and we remove the service at the same time, what could happen is:

- the config is handled first, here https://github.com/metallb/metallb/blob/9cb1653a87673ec91971a60c873204e0752ff97b/speaker/main.go#L358 , returning `k8s.SyncStateError` which is intended to be "this error is transitient" but in the specific case of configmap is intended as "this will never work", so it's transformed here https://github.com/metallb/metallb/blob/7fb3231966a0c2a6741122f4b81020abd3380011/internal/k8s/k8s.go#L537 in success so we'll never retry.
- Then we receive the service, we delete it but that does not trigger a reload of the configmap, because the only thing we reload is services as consequence of SyncStateReprocessAll: https://github.com/metallb/metallb/blob/7fb3231966a0c2a6741122f4b81020abd3380011/internal/k8s/k8s.go#L434 (forceSync only force services)

So the valid configuration change is lost forever. What we are doing here, is to add another SyncState result type that tells "never retry" that is returned for those scenarios where we thing we should really never retry, and we trust the result returned by the `configChanged` callback.


Patch:
diff --git a/controller/main.go b/controller/main.go
index 6f53d988..95759c7f 100644
--- a/controller/main.go
+++ b/controller/main.go
@@ -104,12 +104,12 @@ func (c *controller) SetConfig(l log.Logger, cfg *config.Config) k8s.SyncState {
 
 	if cfg == nil {
 		level.Error(l).Log("op", "setConfig", "error", "no MetalLB configuration in cluster", "msg", "configuration is missing, MetalLB will not function")
-		return k8s.SyncStateError
+		return k8s.SyncStateErrorNoRetry
 	}
 
 	if err := c.ips.SetPools(cfg.Pools); err != nil {
 		level.Error(l).Log("op", "setConfig", "error", err, "msg", "applying new configuration failed")
-		return k8s.SyncStateError
+		return k8s.SyncStateErrorNoRetry
 	}
 	c.config = cfg
 	return k8s.SyncStateReprocessAll
diff --git a/go.mod b/go.mod
index 01775153..4480e4fd 100644
--- a/go.mod
+++ b/go.mod
@@ -25,6 +25,7 @@ require (
 	github.com/prometheus/client_model v0.2.0
 	github.com/prometheus/common v0.10.0
 	github.com/spf13/viper v1.8.1 // indirect
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/sys v0.0.0-20210510120138-977fb7262007
 	gopkg.in/yaml.v2 v2.4.0
 	k8s.io/api v0.20.2
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index 23f6c767..c884123b 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -23,11 +23,13 @@ var reloaderPidFileName = "/etc/frr_reloader/reloader.pid"
 // (https://pkg.go.dev/text/template#hdr-Nested_template_definitions), this
 // should also be considered.
 const configTemplate = `
-log stdout {{.Loglevel}}
+log file /etc/frr/frr.log {{.Loglevel}}
+log timestamp precision 3
 hostname {{.Hostname}}
 
 {{range .Routers -}}
-router bgp {{.MyASN}}
+router bgp {{.MyASN}} view {{.MyASN}}
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 {{ if .RouterId }}
@@ -35,7 +37,16 @@ router bgp {{.MyASN}}
 {{- end }}
 {{range .Neighbors }}
   neighbor {{.Addr}} remote-as {{.ASN}}
+  {{ if .Port -}}
   neighbor {{.Addr}} port {{.Port}}
+  {{- end }}
+  neighbor {{.Addr}} timers {{.KeepaliveTime}} {{.HoldTime}}
+  {{ if .Password -}}
+  neighbor {{.Addr}} password {{.Password}}
+  {{- end }}
+  {{ if .SrcAddr -}}
+  neighbor {{.Addr}} update-source {{.SrcAddr}}
+  {{- end }}
 {{- end }}
 {{range $n := .Neighbors -}}
 {{/* 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 */}}
@@ -72,7 +83,11 @@ type routerConfig struct {
 type neighborConfig struct {
 	ASN            uint32
 	Addr           string
+	SrcAddr        string
 	Port           uint16
+	HoldTime       uint64
+	KeepaliveTime  uint64
+	Password       string
 	Advertisements []*advertisementConfig
 }
 
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index 80a58840..9ccf83a0 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -168,7 +168,7 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 
 	config := &frrConfig{
 		Hostname: hostname,
-		Loglevel: "", // TODO.
+		Loglevel: "informational", // TODO - make loglevel configurable via envvar.
 		Routers:  make(map[string]*routerConfig),
 	}
 
@@ -205,8 +205,14 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 				ASN:            s.asn,
 				Addr:           host,
 				Port:           uint16(portUint),
+				HoldTime:       uint64(s.holdTime / time.Second),
+				KeepaliveTime:  uint64(s.holdTime / (3 * time.Second)), // TODO use 1/3 of holdtime till we can configure it.
+				Password:       s.password,
 				Advertisements: make([]*advertisementConfig, 0),
 			}
+			if s.srcAddr != nil {
+				neighbor.SrcAddr = s.srcAddr.String()
+			}
 			router.Neighbors[neighborName] = neighbor
 		}
 
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
index 7f0f8aed..1b3b1fa4 100644
--- a/internal/k8s/k8s.go
+++ b/internal/k8s/k8s.go
@@ -69,6 +69,9 @@ const (
 	// The update was accepted, but requires reprocessing all watched
 	// services.
 	SyncStateReprocessAll
+	// The update caused a non transient error, the k8s client should
+	// just report and giveup.
+	SyncStateErrorNoRetry
 )
 
 // Config specifies the configuration of the Kubernetes
@@ -425,6 +428,7 @@ func (c *Client) Run(stopCh <-chan struct{}) error {
 		st := c.sync(key)
 		switch st {
 		case SyncStateSuccess:
+		case SyncStateErrorNoRetry:
 			c.queue.Forget(key)
 		case SyncStateError:
 			updateErrors.Inc()
@@ -538,10 +542,10 @@ func (c *Client) sync(key interface{}) SyncState {
 		}
 
 		st := c.configChanged(l, cfg)
-		if st == SyncStateError {
+		if st == SyncStateErrorNoRetry || st == SyncStateError {
 			level.Error(l).Log("event", "configStale", "error", err, "msg", "config (re)load failed, config marked stale")
 			configStale.Set(1)
-			return SyncStateSuccess
+			return st
 		}
 
 		configLoaded.Set(1)
diff --git a/manifests/metallb-frr.yaml b/manifests/metallb-frr.yaml
index 48d8d508..6e7c360e 100644
--- a/manifests/metallb-frr.yaml
+++ b/manifests/metallb-frr.yaml
@@ -361,7 +361,7 @@ data:
     #
     vtysh_enable=yes
     zebra_options="  -A 127.0.0.1 -s 90000000"
-    bgpd_options="   -A 127.0.0.1"
+    bgpd_options="   -A 127.0.0.1 -p 0"
     ospfd_options="  -A 127.0.0.1"
     ospf6d_options=" -A ::1"
     ripd_options="   -A 127.0.0.1"
@@ -467,7 +467,7 @@ spec:
           securityContext:
             capabilities:
               add: ["NET_ADMIN", "NET_RAW", "SYS_ADMIN", "NET_BIND_SERVICE"]
-          image: frrouting/frr:v8.0.1
+          image: frrouting/frr:v7.5.1
           env:
             - name: TINI_SUBREAPER
               value: "true"
@@ -477,7 +477,7 @@ spec:
             - name: frr-conf
               mountPath: /etc/frr
         - name: reloader
-          image: frrouting/frr:v8.0.1
+          image: frrouting/frr:v7.5.1
           command: ["/etc/frr_reloader/frr-reloader.sh"]
           volumeMounts:
             - name: frr-sockets
diff --git a/speaker/main.go b/speaker/main.go
index 4247432d..84fe5d13 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -349,7 +349,7 @@ func (c *controller) SetConfig(l log.Logger, cfg *config.Config) k8s.SyncState {
 
 	if cfg == nil {
 		level.Error(l).Log("op", "setConfig", "error", "no MetalLB configuration in cluster", "msg", "configuration is missing, MetalLB will not function")
-		return k8s.SyncStateError
+		return k8s.SyncStateErrorNoRetry
 	}
 
 	for svc, ip := range c.svcIP {
@@ -362,7 +362,7 @@ func (c *controller) SetConfig(l log.Logger, cfg *config.Config) k8s.SyncState {
 	for proto, handler := range c.protocols {
 		if err := handler.SetConfig(l, cfg); err != nil {
 			level.Error(l).Log("op", "setConfig", "protocol", proto, "error", err, "msg", "applying new configuration to protocol handler failed")
-			return k8s.SyncStateError
+			return k8s.SyncStateErrorNoRetry
 		}
 	}
 
diff --git a/website/content/community/release-process.md b/website/content/community/release-process.md
index c25e21cc..05f434b3 100644
--- a/website/content/community/release-process.md
+++ b/website/content/community/release-process.md
@@ -96,6 +96,13 @@ tags that don't exist until CircleCI makes them exist.
 Check on Quay for a `vX.Y.Z` tag on each image, or check on
 CircleCI that the deploy has completed.
 
+### Publish a helm chart update
+
+This is a manual step until some future time when we build an appropriate CI
+job that can do it for us. See
+[issue #873](https://github.com/metallb/metallb/issues/873) for more details on
+how to manually publish the helm chart update.
+
 ### Repoint the live website
 
 Move the `live-website` branch to the newly created tag with `git
@@ -104,6 +111,10 @@ push -f origin live-website`. This will trigger Netlify to
 redeploy [metallb.universe.tf](https://metallb.universe.tf) with
 updated documentation for the new version.
 
+### Update Slack
+
+Update the topic in `#metallb` on Kubernetes slack.
+
 ### Brag about new release
 
 Tweet, post to G+, slack, IRC, whatever. Make some noise, if it's


Test patch:
diff --git a/controller/controller_test.go b/controller/controller_test.go
index b242ea30..2ff51d40 100644
--- a/controller/controller_test.go
+++ b/controller/controller_test.go
@@ -621,12 +621,12 @@ func TestControllerConfig(t *testing.T) {
 	}
 
 	// Now that an IP is allocated, removing the IP pool is not allowed.
-	if c.SetConfig(l, &config.Config{}) != k8s.SyncStateError {
+	if c.SetConfig(l, &config.Config{}) != k8s.SyncStateErrorNoRetry {
 		t.Fatalf("SetConfig that deletes allocated IPs was accepted")
 	}
 
 	// Deleting the config also makes MetalLB sad.
-	if c.SetConfig(l, nil) != k8s.SyncStateError {
+	if c.SetConfig(l, nil) != k8s.SyncStateErrorNoRetry {
 		t.Fatalf("SetConfig that deletes the config was accepted")
 	}
 }
diff --git a/e2etest/bgp_test.go b/e2etest/bgp_test.go
index 5481b97a..6bdf1c22 100644
--- a/e2etest/bgp_test.go
+++ b/e2etest/bgp_test.go
@@ -21,7 +21,6 @@ package e2e
 import (
 	"context"
 	"fmt"
-	"io/ioutil"
 	"net"
 	"strconv"
 	"time"
@@ -29,13 +28,15 @@ import (
 	"go.universe.tf/metallb/e2etest/pkg/metrics"
 	"go.universe.tf/metallb/e2etest/pkg/routes"
 
+	"golang.org/x/sync/errgroup"
+
 	"github.com/onsi/ginkgo"
 	"github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 	dto "github.com/prometheus/client_model/go"
-	"go.universe.tf/metallb/e2etest/pkg/executor"
 	"go.universe.tf/metallb/e2etest/pkg/frr"
 	frrconfig "go.universe.tf/metallb/e2etest/pkg/frr/config"
+	frrcontainer "go.universe.tf/metallb/e2etest/pkg/frr/container"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clientset "k8s.io/client-go/kubernetes"
@@ -43,37 +44,65 @@ import (
 	e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
 )
 
-const frrContainer = "frr"
+const (
+	frrIBGP = "frr-iBGP"
+	frrEBGP = "frr-eBGP"
+	IBGPAsn = 64512
+	EBGPAsn = 64513
+)
 
 var (
-	frrContainerIPv4 string
-	frrContainerIPv6 string
+	frrContainers []*frrcontainer.FRR
 )
 
+type containerConfig struct {
+	name string
+	nc   frrconfig.NeighborConfig
+	rc   frrconfig.RouterConfig
+}
+
 var _ = ginkgo.Describe("BGP", func() {
 	f := framework.NewDefaultFramework("bgp")
 	var cs clientset.Interface
-
+	containersConf := []containerConfig{
+		{
+			name: frrIBGP,
+			nc: frrconfig.NeighborConfig{
+				ASN:      IBGPAsn,
+				Password: "ibgp-test",
+			},
+			rc: frrconfig.RouterConfig{
+				ASN:      IBGPAsn,
+				BGPPort:  179,
+				RouterID: "10.10.10.10",
+				Password: "ibgp-test",
+				HoldTime: "180s",
+			},
+		},
+		{
+			name: frrEBGP,
+			nc: frrconfig.NeighborConfig{
+				ASN:      EBGPAsn,
+				Password: "ebgp-test",
+			},
+			rc: frrconfig.RouterConfig{
+				ASN:      IBGPAsn,
+				BGPPort:  180,
+				RouterID: "11.11.11.11",
+				Password: "ebgp-test",
+			},
+		},
+	}
 	ginkgo.BeforeEach(func() {
-		cs = f.ClientSet
-
 		var err error
-		// If the FRR container is not running, start it on the local host.
-		if skipDockerCmd {
-			if len(frrTestConfigDir) == 0 {
-				framework.Fail("Missing FRR config directory.")
+		// TODO: When adding support for bgp + IPv6 need to validate hostIPv6 as well
+		if containersNetwork == "host" {
+			if net.ParseIP(hostIPv4) == nil {
+				framework.Fail("Invalid hostIPv4")
 			}
-		} else {
-			frrTestConfigDir, err = ioutil.TempDir("", "frr-conf")
-			framework.ExpectNoError(err)
-			err = frr.StartContainer(frrContainer, frrTestConfigDir)
-			framework.ExpectNoError(err)
 		}
-
-		frrContainerIPv4, frrContainerIPv6, err = frr.GetContainerIPs(frrContainer)
-		framework.ExpectNoError(err)
-
-		err = frr.UpdateContainerVolumePermissions(frrContainer)
+		cs = f.ClientSet
+		frrContainers, err = createFRRContainers(containersConf)
 		framework.ExpectNoError(err)
 	})
 
@@ -82,10 +111,8 @@ var _ = ginkgo.Describe("BGP", func() {
 		err := updateConfigMap(cs, configFile{})
 		framework.ExpectNoError(err)
 
-		if !skipDockerCmd {
-			err = frr.StopContainer(frrContainer, frrTestConfigDir)
-			framework.ExpectNoError(err)
-		}
+		err = stopFRRContainers(frrContainers)
+		framework.ExpectNoError(err)
 
 		if ginkgo.CurrentGinkgoTestDescription().Failed {
 			DescribeSvc(f.Namespace.Name)
@@ -95,6 +122,18 @@ var _ = ginkgo.Describe("BGP", func() {
 	ginkgo.Context("type=Loadbalancer", func() {
 		var allNodes *corev1.NodeList
 		ginkgo.BeforeEach(func() {
+			var peers []peer
+			for _, c := range frrContainers {
+				peers = append(peers, peer{
+					Addr:     c.Ipv4,
+					ASN:      c.RouterConfig.ASN,
+					MyASN:    c.NeighborConfig.ASN,
+					Port:     c.RouterConfig.BGPPort,
+					RouterID: c.RouterConfig.RouterID,
+					Password: c.RouterConfig.Password,
+					HoldTime: c.RouterConfig.HoldTime,
+				})
+			}
 			configData := configFile{
 				Pools: []addressPool{
 					{
@@ -106,18 +145,18 @@ var _ = ginkgo.Describe("BGP", func() {
 						},
 					},
 				},
-				Peers: []peer{
-					{
-						Addr:  frrContainerIPv4,
-						ASN:   64512,
-						MyASN: 64512,
-					},
-				},
+				Peers: peers,
+			}
+			for _, c := range frrContainers {
+				pairExternalFRRWithNodes(cs, c)
 			}
+
 			err := updateConfigMap(cs, configData)
 			framework.ExpectNoError(err)
 
-			pairExternalFRRWithNodes(cs)
+			for _, c := range frrContainers {
+				validateFRRPeeredWithNodes(cs, c)
+			}
 
 			allNodes, err = cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
 			framework.ExpectNoError(err)
@@ -131,7 +170,9 @@ var _ = ginkgo.Describe("BGP", func() {
 				framework.ExpectNoError(err)
 			}()
 
-			validateService(cs, svc, allNodes.Items)
+			for _, c := range frrContainers {
+				validateService(cs, svc, allNodes.Items, c)
+			}
 		})
 
 		ginkgo.It("should work for ExternalTrafficPolicy=Local", func() {
@@ -147,12 +188,16 @@ var _ = ginkgo.Describe("BGP", func() {
 				framework.ExpectNoError(err)
 			}()
 
-			validateService(cs, svc, epNodes)
+			for _, c := range frrContainers {
+				validateService(cs, svc, epNodes, c)
+			}
 		})
 
 		ginkgo.It("would check that FRR is running in the speaker", func() {
-			paired := frrIsPairedOnPods(cs, frrContainerIPv4)
-			framework.ExpectEqual(paired, true)
+			for _, c := range frrContainers {
+				paired := frrIsPairedOnPods(cs, c)
+				framework.ExpectEqual(paired, true)
+			}
 		})
 	})
 
@@ -180,9 +225,23 @@ var _ = ginkgo.Describe("BGP", func() {
 
 		ginkgo.It("should be exposed by the controller", func() {
 			// TODO: The peer address will require update to add support for bgp + IPv6
-			peerAddr := frrContainerIPv4 + ":179"
 			poolName := "bgp-test"
 
+			var peers []peer
+			var peerAddrs []string
+			for _, c := range frrContainers {
+				peers = append(peers, peer{
+					Addr:     c.Ipv4,
+					ASN:      c.RouterConfig.ASN,
+					MyASN:    c.NeighborConfig.ASN,
+					Port:     c.RouterConfig.BGPPort,
+					RouterID: c.RouterConfig.RouterID,
+					Password: c.RouterConfig.Password,
+					HoldTime: c.RouterConfig.HoldTime,
+				})
+				peerAddrs = append(peerAddrs, c.Ipv4+fmt.Sprintf(":%d", c.RouterConfig.BGPPort))
+			}
+
 			configData := configFile{
 				Pools: []addressPool{
 					{
@@ -194,18 +253,18 @@ var _ = ginkgo.Describe("BGP", func() {
 						},
 					},
 				},
-				Peers: []peer{
-					{
-						Addr:  frrContainerIPv4,
-						ASN:   64512,
-						MyASN: 64512,
-					},
-				},
+				Peers: peers,
 			}
+			for _, c := range frrContainers {
+				pairExternalFRRWithNodes(cs, c)
+			}
+
 			err := updateConfigMap(cs, configData)
 			framework.ExpectNoError(err)
 
-			pairExternalFRRWithNodes(cs)
+			for _, c := range frrContainers {
+				validateFRRPeeredWithNodes(cs, c)
+			}
 
 			ginkgo.By("checking the metrics when no service is added")
 			controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod)
@@ -218,8 +277,10 @@ var _ = ginkgo.Describe("BGP", func() {
 
 				speakerMetrics, err := metrics.ForPod(controllerPod, speaker)
 				framework.ExpectNoError(err)
-				validateGaugeValue(1, "metallb_bgp_session_up", map[string]string{"peer": peerAddr}, speakerMetrics)
-				validateGaugeValue(0, "metallb_bgp_announced_prefixes_total", map[string]string{"peer": peerAddr}, speakerMetrics)
+				for _, addr := range peerAddrs {
+					validateGaugeValue(1, "metallb_bgp_session_up", map[string]string{"peer": addr}, speakerMetrics)
+					validateGaugeValue(0, "metallb_bgp_announced_prefixes_total", map[string]string{"peer": addr}, speakerMetrics)
+				}
 			}
 
 			ginkgo.By("creating a service")
@@ -239,10 +300,12 @@ var _ = ginkgo.Describe("BGP", func() {
 
 				speakerMetrics, err := metrics.ForPod(controllerPod, speaker)
 				framework.ExpectNoError(err)
-				validateGaugeValue(1, "metallb_bgp_session_up", map[string]string{"peer": peerAddr}, speakerMetrics)
-				validateGaugeValue(1, "metallb_bgp_announced_prefixes_total", map[string]string{"peer": peerAddr}, speakerMetrics)
+				for _, addr := range peerAddrs {
+					validateGaugeValue(1, "metallb_bgp_session_up", map[string]string{"peer": addr}, speakerMetrics)
+					validateGaugeValue(1, "metallb_bgp_announced_prefixes_total", map[string]string{"peer": addr}, speakerMetrics)
 
-				validateCounterValue(1, "metallb_bgp_updates_total", map[string]string{"peer": peerAddr}, speakerMetrics)
+					validateCounterValue(1, "metallb_bgp_updates_total", map[string]string{"peer": addr}, speakerMetrics)
+				}
 
 				validateGaugeValue(1, "metallb_speaker_announced", map[string]string{"node": speaker.Spec.NodeName, "protocol": "bgp", "service": fmt.Sprintf("%s/%s", f.Namespace.Name, svc.Name)}, speakerMetrics)
 			}
@@ -257,20 +320,32 @@ var _ = ginkgo.Describe("BGP", func() {
 		})
 
 		table.DescribeTable("set different AddressPools ranges modes", func(addressPools []addressPool) {
+			var peers []peer
+			for _, c := range frrContainers {
+				peers = append(peers, peer{
+					Addr:     c.Ipv4,
+					ASN:      c.RouterConfig.ASN,
+					MyASN:    c.NeighborConfig.ASN,
+					Port:     c.RouterConfig.BGPPort,
+					RouterID: c.RouterConfig.RouterID,
+					Password: c.RouterConfig.Password,
+					HoldTime: c.RouterConfig.HoldTime,
+				})
+			}
 			configData := configFile{
-				Peers: []peer{
-					{
-						Addr:  frrContainerIPv4,
-						ASN:   64512,
-						MyASN: 64512,
-					},
-				},
+				Peers: peers,
 				Pools: addressPools,
 			}
+			for _, c := range frrContainers {
+				pairExternalFRRWithNodes(cs, c)
+			}
+
 			err := updateConfigMap(cs, configData)
 			framework.ExpectNoError(err)
 
-			pairExternalFRRWithNodes(cs)
+			for _, c := range frrContainers {
+				validateFRRPeeredWithNodes(cs, c)
+			}
 
 			svc, _ := createServiceWithBackend(cs, f.Namespace.Name, corev1.ServiceExternalTrafficPolicyTypeCluster)
 
@@ -289,7 +364,9 @@ var _ = ginkgo.Describe("BGP", func() {
 			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
 			framework.ExpectNoError(err)
 
-			validateService(cs, svc, allNodes.Items)
+			for _, c := range frrContainers {
+				validateService(cs, svc, allNodes.Items, c)
+			}
 		},
 			table.Entry("AddressPool defined by network prefix", []addressPool{
 				{
@@ -371,40 +448,28 @@ func validateCounterValue(expectedValue int, metricName string, labels map[strin
 	}
 }
 
-func pairExternalFRRWithNodes(cs clientset.Interface) {
-	bgpConfig, err := frrconfig.BGPPeersForAllNodes(cs)
+func pairExternalFRRWithNodes(cs clientset.Interface, c *frrcontainer.FRR) {
+	bgpConfig, err := frrconfig.BGPPeersForAllNodes(cs, c.NeighborConfig, c.RouterConfig)
 	framework.ExpectNoError(err)
 
-	exc := executor.ForContainer(frrContainer)
-	if skipDockerCmd {
-		exc = executor.Host
-	}
-
-	err = frr.UpdateBGPConfigFile(frrTestConfigDir, bgpConfig, exc)
+	err = c.UpdateBGPConfigFile(bgpConfig)
 	framework.ExpectNoError(err)
-
-	validateFRRPeeredWithNodes(cs)
 }
 
-func validateFRRPeeredWithNodes(cs clientset.Interface) {
-	exc := executor.ForContainer(frrContainer)
-	if skipDockerCmd {
-		exc = executor.Host
-	}
-
+func validateFRRPeeredWithNodes(cs clientset.Interface, c *frrcontainer.FRR) {
 	allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
 	framework.ExpectNoError(err)
 
-	ginkgo.By("checking all nodes are peered with the frr instance")
+	ginkgo.By(fmt.Sprintf("checking all nodes are peered with the frr instance %s", c.Name))
 	Eventually(func() error {
-		neighbors, err := frr.NeighborsInfo(exc)
+		neighbors, err := frr.NeighborsInfo(c)
 		framework.ExpectNoError(err)
 		err = frr.NeighborsMatchNodes(allNodes.Items, neighbors)
 		return err
 	}, 2*time.Minute, 1*time.Second).Should(BeNil())
 }
 
-func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1.Node) {
+func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1.Node, c *frrcontainer.FRR) {
 	port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
 	ingressIP := e2eservice.GetIngressPoint(
 		&svc.Status.LoadBalancer.Ingress[0])
@@ -412,30 +477,19 @@ func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1
 	hostport := net.JoinHostPort(ingressIP, port)
 	address := fmt.Sprintf("http://%s/", hostport)
 
-	if skipDockerCmd {
-		ginkgo.By(fmt.Sprintf("checking connectivity to %s", address))
-	} else {
-		ginkgo.By(fmt.Sprintf("checking connectivity to %s with docker", address))
-	}
-
-	exc := executor.ForContainer(frrContainer)
-	if skipDockerCmd {
-		exc = executor.Host
-	}
-
 	Eventually(func() error {
-		err := wgetRetry(address, exc)
+		err := wgetRetry(address, c)
 		if err != nil {
 			return err
 		}
 
-		advertised := routes.ForIP(ingressIP, exc)
+		advertised := routes.ForIP(ingressIP, c)
 		err = routes.MatchNodes(nodes, advertised)
 		if err != nil {
 			return err
 		}
 
-		frrRoutes, _, err := frr.Routes(exc)
+		frrRoutes, _, err := frr.Routes(c)
 		if err != nil {
 			return err
 		}
@@ -453,14 +507,45 @@ func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1
 	}, 2*time.Minute, 1*time.Second).Should(BeNil())
 }
 
-func frrIsPairedOnPods(cs clientset.Interface, neighbor string) bool {
+func frrIsPairedOnPods(cs clientset.Interface, n *frrcontainer.FRR) bool {
 	pods, err := cs.CoreV1().Pods("metallb-system").List(context.Background(), metav1.ListOptions{
 		LabelSelector: "component=speaker",
 	})
 	framework.ExpectNoError(err)
-	toParse, err := framework.RunKubectl("metallb-system", "exec", pods.Items[0].Name, "-c", "frr", "--", "vtysh", "-c", fmt.Sprintf("show bgp neighbor %s json", neighbor))
+	toParse, err := framework.RunKubectl("metallb-system", "exec", pods.Items[0].Name, "-c", "frr", "--", "vtysh", "-c", fmt.Sprintf("show bgp view %d neighbor %s json", n.NeighborConfig.ASN, n.Ipv4))
 	framework.ExpectNoError(err)
 	res, err := frr.NeighborConnected(toParse)
 	framework.ExpectNoError(err)
 	return res
 }
+
+func createFRRContainers(c []containerConfig) ([]*frrcontainer.FRR, error) {
+	frrContainers = make([]*frrcontainer.FRR, 0)
+	g := new(errgroup.Group)
+	for _, conf := range c {
+		conf := conf
+		g.Go(func() error {
+			c, err := frrcontainer.Start(conf.name, conf.nc, conf.rc, containersNetwork, hostIPv4, hostIPv6)
+			if c != nil {
+				frrContainers = append(frrContainers, c)
+			}
+			return err
+		})
+	}
+	err := g.Wait()
+
+	return frrContainers, err
+}
+
+func stopFRRContainers(containers []*frrcontainer.FRR) error {
+	g := new(errgroup.Group)
+	for _, c := range containers {
+		c := c
+		g.Go(func() error {
+			err := c.Stop()
+			return err
+		})
+	}
+
+	return g.Wait()
+}
diff --git a/e2etest/config/frr/daemons b/e2etest/config/frr/daemons
index 5e1f5bdc..587add00 100644
--- a/e2etest/config/frr/daemons
+++ b/e2etest/config/frr/daemons
@@ -38,7 +38,7 @@ vrrpd=no
 #
 vtysh_enable=yes
 zebra_options="  -A 127.0.0.1 -s 90000000"
-bgpd_options="   -A 127.0.0.1"
+bgpd_options="   -A 127.0.0.1 -p {{.BGPPort}}"
 ospfd_options="  -A 127.0.0.1"
 ospf6d_options=" -A ::1"
 ripd_options="   -A 127.0.0.1"
diff --git a/e2etest/e2etest_suite_test.go b/e2etest/e2etest_suite_test.go
index 7caa7d47..ad6f1388 100644
--- a/e2etest/e2etest_suite_test.go
+++ b/e2etest/e2etest_suite_test.go
@@ -39,16 +39,21 @@ import (
 	e2econfig "k8s.io/kubernetes/test/e2e/framework/config"
 )
 
-const defaultTestNameSpace = "metallb-system"
+const (
+	defaultTestNameSpace     = "metallb-system"
+	defaultContainersNetwork = "kind"
+)
 
 var (
 	// Use ephemeral port for pod, instead of well-known port (tcp/80).
-	servicePodPort   uint
-	skipDockerCmd    bool
-	ipv4ServiceRange string
-	ipv6ServiceRange string
-	testNameSpace    = defaultTestNameSpace
-	frrTestConfigDir string
+	servicePodPort    uint
+	skipDockerCmd     bool
+	ipv4ServiceRange  string
+	ipv6ServiceRange  string
+	testNameSpace     = defaultTestNameSpace
+	containersNetwork = defaultContainersNetwork
+	hostIPv4          string
+	hostIPv6          string
 )
 
 // handleFlags sets up all flags and parses the command line.
@@ -98,8 +103,15 @@ var _ = ginkgo.BeforeSuite(func() {
 		testNameSpace = ns
 	}
 
-	if dir := os.Getenv("FRR_CONFIG_DIR"); len(dir) != 0 {
-		frrTestConfigDir = dir
+	if _, res := os.LookupEnv("RUN_FRR_CONTAINER_ON_HOST_NETWORK"); res == true {
+		containersNetwork = "host"
+	}
+
+	if ip := os.Getenv("PROVISIONING_HOST_EXTERNAL_IPV4"); len(ip) != 0 {
+		hostIPv4 = ip
+	}
+	if ip := os.Getenv("PROVISIONING_HOST_EXTERNAL_IPV6"); len(ip) != 0 {
+		hostIPv6 = ip
 	}
 
 	// Validate the IPv4 service range.
diff --git a/e2etest/pkg/executor/executor.go b/e2etest/pkg/executor/executor.go
index 726e7d1d..443b207c 100644
--- a/e2etest/pkg/executor/executor.go
+++ b/e2etest/pkg/executor/executor.go
@@ -2,7 +2,10 @@
 
 package executor
 
-import "os/exec"
+import (
+	"os"
+	"os/exec"
+)
 
 type Executor interface {
 	Exec(cmd string, args ...string) (string, error)
@@ -10,7 +13,16 @@ type Executor interface {
 
 type hostExecutor struct{}
 
-var Host hostExecutor
+var (
+	Host             hostExecutor
+	ContainerRuntime = "docker"
+)
+
+func init() {
+	if cr := os.Getenv("CONTAINER_RUNTIME"); len(cr) != 0 {
+		ContainerRuntime = cr
+	}
+}
 
 func (hostExecutor) Exec(cmd string, args ...string) (string, error) {
 	out, err := exec.Command(cmd, args...).CombinedOutput()
@@ -27,6 +39,6 @@ type containerExecutor struct {
 
 func (e *containerExecutor) Exec(cmd string, args ...string) (string, error) {
 	newArgs := append([]string{"exec", e.container, cmd}, args...)
-	out, err := exec.Command("docker", newArgs...).CombinedOutput()
+	out, err := exec.Command(ContainerRuntime, newArgs...).CombinedOutput()
 	return string(out), err
 }
diff --git a/e2etest/pkg/frr/config/config.go b/e2etest/pkg/frr/config/config.go
index b4a510bf..841c865d 100644
--- a/e2etest/pkg/frr/config/config.go
+++ b/e2etest/pkg/frr/config/config.go
@@ -18,30 +18,37 @@ import (
 // BGP router config.
 const bgpConfigTemplate = `
 router bgp {{.ASN}}
+  no bgp ebgp-requires-policy
 {{range .Neighbors }}
   neighbor {{.Addr}} remote-as {{.ASN}}
   neighbor {{.Addr}} next-hop-self
+  {{ if .Password -}}
+  neighbor {{.Addr}} password {{.Password}}
+  {{- end }}
 {{- end }}
 `
 
-type routerConfig struct {
+type RouterConfig struct {
 	ASN       uint32
-	Neighbors []*neighborConfig
+	Neighbors []*NeighborConfig
+	BGPPort   uint16
+	RouterID  string
+	Password  string
+	HoldTime  string
 }
 
-type neighborConfig struct {
-	ASN  uint32
-	Addr string
+type NeighborConfig struct {
+	ASN      uint32
+	Addr     string
+	Password string
 }
 
 // Set the IP of each node in the cluster in the BGP router configuration.
 // Each node will peer with the BGP router.
-func BGPPeersForAllNodes(cs clientset.Interface) (string, error) {
-	router := &routerConfig{
-		ASN: 64512,
-	}
+func BGPPeersForAllNodes(cs clientset.Interface, nc NeighborConfig, rc RouterConfig) (string, error) {
+	router := rc
 
-	router.Neighbors = make([]*neighborConfig, 0)
+	router.Neighbors = make([]*NeighborConfig, 0)
 
 	nodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
 	if err != nil {
@@ -52,11 +59,9 @@ func BGPPeersForAllNodes(cs clientset.Interface) (string, error) {
 	for _, node := range nodes.Items {
 		for i := range node.Status.Addresses {
 			if node.Status.Addresses[i].Type == "InternalIP" {
-				neighbor := &neighborConfig{
-					ASN:  64512,
-					Addr: node.Status.Addresses[i].Address,
-				}
-				router.Neighbors = append(router.Neighbors, neighbor)
+				neighbor := nc
+				neighbor.Addr = node.Status.Addresses[i].Address
+				router.Neighbors = append(router.Neighbors, &neighbor)
 			}
 		}
 	}
@@ -105,3 +110,34 @@ func SetBGPConfig(testDirName string, config string) error {
 
 	return nil
 }
+
+// Set daemons config file.
+func SetDaemonsConfig(testDirName string, rc RouterConfig) error {
+	path := fmt.Sprintf("%s/%s", testDirName, consts.DaemonsConfigFile)
+	tpl, err := template.ParseFiles(path)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to parse %s", path)
+	}
+
+	f, err := os.Create(path)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to open file %s", path)
+	}
+
+	defer f.Close()
+
+	type Template struct {
+		BGPPort uint16
+	}
+
+	info := Template{
+		BGPPort: rc.BGPPort,
+	}
+
+	err = tpl.Execute(f, info)
+	if err != nil {
+		return errors.Wrapf(err, "Failed to update %s", path)
+	}
+
+	return nil
+}
diff --git a/e2etest/pkg/frr/consts/consts.go b/e2etest/pkg/frr/consts/consts.go
index 6db5e70a..24647155 100644
--- a/e2etest/pkg/frr/consts/consts.go
+++ b/e2etest/pkg/frr/consts/consts.go
@@ -5,4 +5,6 @@ package consts
 const (
 	// BGPConfigFile contains the BGP router configuration file.
 	BGPConfigFile = "bgpd.conf"
+	// DaemonsConfigFile contains the frr daemons configuration file.
+	DaemonsConfigFile = "daemons"
 )
diff --git a/e2etest/pkg/frr/container.go b/e2etest/pkg/frr/container/container.go
similarity index 50%
rename from e2etest/pkg/frr/container.go
rename to e2etest/pkg/frr/container/container.go
index 0cafc663..208c4742 100644
--- a/e2etest/pkg/frr/container.go
+++ b/e2etest/pkg/frr/container/container.go
@@ -1,16 +1,17 @@
 // SPDX-License-Identifier:Apache-2.0
 
-package frr
+package container
 
 import (
 	"fmt"
+	"io/ioutil"
 	"os"
 	"os/exec"
 	"strings"
 
 	"github.com/pkg/errors"
 	"go.universe.tf/metallb/e2etest/pkg/executor"
-	config "go.universe.tf/metallb/e2etest/pkg/frr/config"
+	"go.universe.tf/metallb/e2etest/pkg/frr/config"
 	"go.universe.tf/metallb/e2etest/pkg/frr/consts"
 )
 
@@ -19,20 +20,78 @@ const (
 	frrConfigDir = "config/frr"
 	// FRR routing image.
 	frrImage = "frrouting/frr:v7.5.1"
+	// Host network name.
+	hostNetwork = "host"
 	// FRR container mount destination path.
 	frrMountPath = "/etc/frr"
 )
 
+type FRR struct {
+	executor.Executor
+	Name           string
+	configDir      string
+	NeighborConfig config.NeighborConfig
+	RouterConfig   config.RouterConfig
+	Ipv4           string
+	Ipv6           string
+}
+
+// Start creates a new FRR container on the host and returns the corresponding *FRR.
+// A situation where a non-nil container and an error are returned is possible.
+func Start(name string, nc config.NeighborConfig, rc config.RouterConfig, network string, hostipv4 string, hostipv6 string) (*FRR, error) {
+	configDir, err := ioutil.TempDir("", "frr-conf")
+	if err != nil {
+		return nil, err
+	}
+
+	err = startContainer(name, configDir, rc, network)
+	if err != nil {
+		return nil, err
+	}
+
+	exc := executor.ForContainer(name)
+
+	frr := &FRR{
+		Executor:       exc,
+		Name:           name,
+		configDir:      configDir,
+		NeighborConfig: nc,
+		RouterConfig:   rc,
+	}
+
+	if network == hostNetwork {
+		frr.Ipv4 = hostipv4
+		frr.Ipv6 = hostipv6
+	} else {
+		err = frr.updateIPS()
+		if err != nil {
+			return frr, err
+		}
+	}
+
+	err = frr.updateVolumePermissions()
+	if err != nil {
+		return frr, err
+	}
+
+	return frr, nil
+}
+
 // Run a BGP router in a container.
-func StartContainer(containerName string, testDirName string) error {
+func startContainer(containerName string, testDirName string, rc config.RouterConfig, network string) error {
 	srcFiles := fmt.Sprintf("%s/.", frrConfigDir)
 	res, err := exec.Command("cp", "-r", srcFiles, testDirName).CombinedOutput()
 	if err != nil {
 		return errors.Wrapf(err, "Failed to copy FRR config directory. %s", string(res))
 	}
 
+	err = config.SetDaemonsConfig(testDirName, rc)
+	if err != nil {
+		return err
+	}
+
 	volume := fmt.Sprintf("%s:%s", testDirName, frrMountPath)
-	out, err := exec.Command("docker", "run", "-d", "--privileged", "--network", "kind", "--rm", "--name", containerName,
+	out, err := exec.Command(executor.ContainerRuntime, "run", "-d", "--privileged", "--network", network, "--rm", "--name", containerName,
 		"--volume", volume, frrImage).CombinedOutput()
 	if err != nil {
 		return errors.Wrapf(err, "Failed to start %s container. %s", containerName, out)
@@ -41,35 +100,42 @@ func StartContainer(containerName string, testDirName string) error {
 	return nil
 }
 
-// Returns the IPv4 and IPv6 addresses of the container.
-func GetContainerIPs(containerName string) (ipv4 string, ipv6 string, err error) {
-	containerIP, err := exec.Command("docker", "inspect", "-f", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}",
-		containerName).CombinedOutput()
+// Sets the IPv4 and IPv6 addresses of the *FRR.
+// todo: improve error handling, especially check that containerIPv4 and containerIPv6 are not empty
+func (c *FRR) updateIPS() (err error) {
+	containerIP, err := exec.Command(executor.ContainerRuntime, "inspect", "-f", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}",
+		c.Name).CombinedOutput()
 	if err != nil {
-		return "", "", errors.Wrapf(err, "Failed to get FRR IP address")
+		return errors.Wrapf(err, "Failed to get FRR IPv4 address")
 	}
 
 	containerIPv4 := strings.TrimSuffix(string(containerIP), "\n")
 
-	containerIP, err = exec.Command("docker", "inspect", "-f", "{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}",
-		containerName).CombinedOutput()
+	containerIP, err = exec.Command(executor.ContainerRuntime, "inspect", "-f", "{{range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}",
+		c.Name).CombinedOutput()
 	if err != nil {
-		return "", "", errors.Wrapf(err, "Failed to get FRR IP address")
+		return errors.Wrapf(err, "Failed to get FRR IPv6 address")
 	}
 
 	containerIPv6 := strings.TrimSuffix(string(containerIP), "\n")
 
-	return containerIPv4, containerIPv6, nil
+	if containerIPv4 == "" && containerIPv6 == "" {
+		return errors.Errorf("Failed to get FRR IP addresses")
+	}
+	c.Ipv4 = containerIPv4
+	c.Ipv6 = containerIPv6
+
+	return nil
 }
 
 // Updating the BGP config file.
-func UpdateBGPConfigFile(testDirName string, bgpConfig string, exec executor.Executor) error {
-	err := config.SetBGPConfig(testDirName, bgpConfig)
+func (c *FRR) UpdateBGPConfigFile(bgpConfig string) error {
+	err := config.SetBGPConfig(c.configDir, bgpConfig)
 	if err != nil {
 		return errors.Wrapf(err, "Failed to update BGP config file")
 	}
 
-	err = reloadFRRConfig(consts.BGPConfigFile, exec)
+	err = reloadFRRConfig(consts.BGPConfigFile, c)
 	if err != nil {
 		return errors.Wrapf(err, "Failed to reload BGP config file")
 	}
@@ -78,14 +144,14 @@ func UpdateBGPConfigFile(testDirName string, bgpConfig string, exec executor.Exe
 }
 
 // Delete the BGP router container configuration.
-func StopContainer(containerName string, testDirName string) error {
+func (c *FRR) Stop() error {
 	// Kill the BGP router container.
-	out, err := exec.Command("docker", "kill", containerName).CombinedOutput()
+	out, err := exec.Command(executor.ContainerRuntime, "kill", c.Name).CombinedOutput()
 	if err != nil {
-		return errors.Wrapf(err, "Failed to kill %s container. %s", containerName, out)
+		return errors.Wrapf(err, "Failed to kill %s container. %s", c.Name, out)
 	}
 
-	err = os.RemoveAll(testDirName)
+	err = os.RemoveAll(c.configDir)
 	if err != nil {
 		return errors.Wrapf(err, "Failed to delete FRR config directory.")
 	}
@@ -95,7 +161,7 @@ func StopContainer(containerName string, testDirName string) error {
 
 // Change volume permissions.
 // Allows deleting the test directory or updating the files in the volume.
-func UpdateContainerVolumePermissions(containerName string) error {
+func (c *FRR) updateVolumePermissions() error {
 	var uid, gid int
 
 	if isPodman() {
@@ -111,9 +177,9 @@ func UpdateContainerVolumePermissions(containerName string) error {
 	}
 
 	cmd := fmt.Sprintf("chown -R %d:%d %s", uid, gid, frrMountPath)
-	out, err := exec.Command("docker", "exec", containerName, "sh", "-c", cmd).CombinedOutput()
+	out, err := exec.Command(executor.ContainerRuntime, "exec", c.Name, "sh", "-c", cmd).CombinedOutput()
 	if err != nil {
-		return errors.Wrapf(err, "Failed to change %s container volume permissions. %s", containerName, string(out))
+		return errors.Wrapf(err, "Failed to change %s container volume permissions. %s", c.Name, string(out))
 	}
 
 	return nil
diff --git a/internal/bgp/frr/docker_test.go b/internal/bgp/frr/docker_test.go
index 898929ee..2fe193c3 100644
--- a/internal/bgp/frr/docker_test.go
+++ b/internal/bgp/frr/docker_test.go
@@ -22,7 +22,7 @@ var (
 )
 
 const (
-	frrImageTag = "v8.0.1"
+	frrImageTag = "v7.5.1"
 )
 
 func TestMain(m *testing.M) {
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisement.golden b/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
index e298a9cf..ced74571 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,6 +12,9 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
index 65fd204a..33831853 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,6 +12,9 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalid.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalid.golden
index 0975efc9..75f2dbe5 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalid.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalid.golden
@@ -1,12 +1,15 @@
 
-log stdout 
-hostname 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
+hostname
 
-router bgp 100
+router bgp 100 view 100
   no bgp default ipv4-unicast
   bgp router-id 10.1.1.254
 
   neighbor 10.2.2.254 remote-as 200
-
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
 
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
index e33a333a..7862d7d5 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,6 +12,9 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
index e16d59e0..063c013e 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
@@ -1,14 +1,19 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
index e33a333a..7862d7d5 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,6 +12,9 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
diff --git a/internal/bgp/frr/testdata/TestSingleSession.golden b/internal/bgp/frr/testdata/TestSingleSession.golden
index e33a333a..7862d7d5 100644
--- a/internal/bgp/frr/testdata/TestSingleSession.golden
+++ b/internal/bgp/frr/testdata/TestSingleSession.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,6 +12,9 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
diff --git a/internal/bgp/frr/testdata/TestSingleSessionClose.golden b/internal/bgp/frr/testdata/TestSingleSessionClose.golden
index a7fcabc6..111e444f 100644
--- a/internal/bgp/frr/testdata/TestSingleSessionClose.golden
+++ b/internal/bgp/frr/testdata/TestSingleSessionClose.golden
@@ -1,5 +1,6 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
 
diff --git a/internal/bgp/frr/testdata/TestTwoAdvertisements.golden b/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
index 6180fbad..45ce8a4e 100644
--- a/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
+++ b/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,6 +12,9 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
diff --git a/internal/bgp/frr/testdata/TestTwoSessions.golden b/internal/bgp/frr/testdata/TestTwoSessions.golden
index c7ddf93c..be027eaf 100644
--- a/internal/bgp/frr/testdata/TestTwoSessions.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessions.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,6 +12,9 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
@@ -18,7 +23,8 @@ router bgp 100
     neighbor 10.2.2.254 activate
   exit-address-family
 
-router bgp 300
+router bgp 300 view 300
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -26,6 +32,9 @@ router bgp 300
 
   neighbor 10.4.4.254 remote-as 400
   neighbor 10.4.4.254 port 179
+  neighbor 10.4.4.254 timers 0 1
+  neighbor 10.4.4.254 password password
+  neighbor 10.4.4.254 update-source 10.3.3.254
 
   address-family ipv4 unicast
     neighbor 10.4.4.254 activate
diff --git a/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden b/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
index e33a333a..7862d7d5 100644
--- a/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,6 +12,9 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate
diff --git a/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden b/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
index 9ef70701..c7db97c5 100644
--- a/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
@@ -1,8 +1,10 @@
 
-log stdout 
+log file /etc/frr/frr.log informational
+log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100
+router bgp 100 view 100
+  no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
 
@@ -10,8 +12,14 @@ router bgp 100
 
   neighbor 10.2.2.254 remote-as 200
   neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254
   neighbor 10.4.4.254 remote-as 400
   neighbor 10.4.4.254 port 179
+  neighbor 10.4.4.254 timers 0 1
+  neighbor 10.4.4.254 password password
+  neighbor 10.4.4.254 update-source 10.1.1.254
 
   address-family ipv4 unicast
     neighbor 10.2.2.254 activate