Repository: metallb/metallb
Pull request #997 (2021-10-18 13:37:31)

Added IPv6 aggregation length and enable IPv6 ci lane.
Those diffs are based off some of the work done in PR metallb#590.
BGP FRR stack will be used for IPv6 unicast BGP peering instead of adding MP support  to existing Metallb stack
Metallb operator changes https://github.com/metallb/metallb-operator/pull/99

Signed-off-by: Mohamed Mahmoud <mmahmoud@redhat.com>

fedepaol: The change makes sense, it's backwards compatible in the sense that there's no current ipv6 support in BGP.
The only doubt I have is letting this in before ipv6 is supported in BGP.
fedepaol: Also, can you change the commit message of the first commit to explain the rationale behind this change? This https://chris.beams.io/posts/git-commit/ is a post that was added to the contributing guidelines.
fedepaol: LGTM

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index c864e550..f126aad2 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -42,7 +42,8 @@ jobs:
           if [ "<< parameters.bgp-type >>" = "native" ]; then SKIP="$SKIP\|FRR"; fi
           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
+          if [ "<< parameters.ip-family >>" = "ipv6" ]; then SKIP="$SKIP\|IPV4"; fi
+          if [ "<< parameters.ip-family >>" = "ipv6" ] && [ "<< parameters.bgp-type >>" = "native" ]; then SKIP="$SKIP\|BGP"; fi # we are skipping BGP for ipv6 for native bgp stack
           echo "Skipping $SKIP"
           inv e2etest --skip $SKIP -e /tmp/kind_logs
       - store_artifacts:
diff --git a/dev-env/README.md b/dev-env/README.md
index 722eb7bf..8c70aa85 100644
--- a/dev-env/README.md
+++ b/dev-env/README.md
@@ -52,6 +52,8 @@ pip install -r requirements.txt
 
 ### BGP
 
+#### IPv4 unicast
+
 This assumes a 3-node `kind` based test cluster as set up by running `inv
 dev-env -p bgp`, which sets up the development environment with some
 configuration and a BGP router for development and testing purposes.
@@ -146,6 +148,50 @@ Commercial support is available at
 </html>
 ```
 
+#### IPv6 unicast
+
+Bring up the cluster using ipv6 address family
+```
+inv dev-env -i ipv6 -p bgp -b frr
+```
+
+Observe that BGP speakers have BGP sessions with our router:
+```
+docker exec frr vtysh -c "show bgp ipv6 unicast sum"
+BGP router identifier 172.18.0.5, local AS number 64512 vrf-id 0
+BGP table version 1
+RIB entries 1, using 192 bytes of memory
+Peers 3, using 43 KiB of memory
+Neighbor              V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt
+fc00:f853:ccd:e793::2 4      64512        25        24        0    0    0 00:21:05            1        0
+fc00:f853:ccd:e793::3 4      64512        25        24        0    0    0 00:21:04            1        0
+fc00:f853:ccd:e793::4 4      64512        25        24        0    0    0 00:21:05            1        0
+Total number of neighbors 3
+```
+
+Create IPv6 `nginx` service of type LoadBalancer
+
+```
+kubectl apply -f dev-env/testsvc_ipv6.yaml
+kubectl get svc
+NAME         TYPE           CLUSTER-IP         EXTERNAL-IP            PORT(S)        AGE
+kubernetes   ClusterIP      fd00:10:96::1      <none>                 443/TCP        15h
+nginx        LoadBalancer   fd00:10:96::d362   fc00:f853:ccd:e799::   80:32587/TCP   15h
+```
+
+Ensure a route to this service has been published by MetalLB BGP speaker
+
+```
+kubectl exec -it speaker-c9fx8 -n metallb-system -c frr -- vtysh -c "show bgp ipv6 unicast sum"
+BGP router identifier 172.18.0.2, local AS number 64512 vrf-id 0
+BGP table version 1
+RIB entries 1, using 192 bytes of memory
+Peers 1, using 14 KiB of memory
+Neighbor              V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt
+fc00:f853:ccd:e793::5 4      64512        27        29        0    0    0 00:21:55            0        1
+Total number of neighbors 1
+```
+
 ## Layer 2
 
 This assumes a 3-node `kind` based test cluster as set up by running `inv
diff --git a/dev-env/bgp/config.yaml.tmpl b/dev-env/bgp/config.yaml.tmpl
index 0313ba38..62feb85e 100644
--- a/dev-env/bgp/config.yaml.tmpl
+++ b/dev-env/bgp/config.yaml.tmpl
@@ -6,9 +6,9 @@ metadata:
 data:
   config: |
     peers:
-      # NOTE: PEER_ADDRESS will be substituted with the IP address
+      # NOTE: IP_PEER_ADDRESS will be substituted with the IP address
       # assigned to the frr container.
-    - peer-address: PEER_ADDRESS
+    - peer-address: IP_PEER_ADDRESS
       peer-asn: 64512
       my-asn: 64512
     address-pools:
diff --git a/dev-env/bgp/frr/bgpd.conf.tmpl b/dev-env/bgp/frr/bgpd.conf.tmpl
index 0a58b60f..b2aa3b85 100644
--- a/dev-env/bgp/frr/bgpd.conf.tmpl
+++ b/dev-env/bgp/frr/bgpd.conf.tmpl
@@ -11,14 +11,23 @@ password zebra
 !
 ! NOTE: These NODE_IPs will be substituted with the IP addresses
 ! assigned to the kind cluster Nodes if they are different.
+! PROTOCOL will be substituted with either ipv4 or ipv6.
 !
 router bgp 64512
+ no bgp default ipv4-unicast
+ no bgp network import-check
  neighbor NODE0_IP remote-as 64512
- neighbor NODE0_IP next-hop-self
  neighbor NODE1_IP remote-as 64512
- neighbor NODE1_IP next-hop-self
  neighbor NODE2_IP remote-as 64512
- neighbor NODE2_IP next-hop-self
+
+ address-family PROTOCOL unicast
+  neighbor NODE0_IP activate
+  neighbor NODE0_IP next-hop-self
+  neighbor NODE1_IP activate
+  neighbor NODE1_IP next-hop-self
+  neighbor NODE2_IP activate
+  neighbor NODE2_IP next-hop-self
+ exit-address-family
 
 !
 !router bgp 7675
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index c884123b..daba17c3 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -28,7 +28,7 @@ log timestamp precision 3
 hostname {{.Hostname}}
 
 {{range .Routers -}}
-router bgp {{.MyASN}} view {{.MyASN}}
+router bgp {{.MyASN}}
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/config/config.go b/internal/config/config.go
index b0186c62..f2119b50 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -71,9 +71,10 @@ type addressPool struct {
 }
 
 type bgpAdvertisement struct {
-	AggregationLength *int `yaml:"aggregation-length"`
-	LocalPref         *uint32
-	Communities       []string
+	AggregationLength   *int `yaml:"aggregation-length"`
+	AggregationLengthV6 *int `yaml:"aggregation-length-v6"`
+	LocalPref           *uint32
+	Communities         []string
 }
 
 // Config is a parsed MetalLB configuration.
@@ -145,6 +146,9 @@ type BGPAdvertisement struct {
 	// length. Optional, defaults to 32 (i.e. no aggregation) if not
 	// specified.
 	AggregationLength int
+	// Optional, defaults to 128 (i.e. no aggregation) if not
+	// specified.
+	AggregationLengthV6 int
 	// Value of the LOCAL_PREF BGP path attribute. Used only when
 	// advertising to IBGP peers (i.e. Peer.MyASN == Peer.ASN).
 	LocalPref uint32
@@ -370,9 +374,10 @@ func parseBGPAdvertisements(ads []bgpAdvertisement, cidrs []*net.IPNet, communit
 	if len(ads) == 0 {
 		return []*BGPAdvertisement{
 			{
-				AggregationLength: 32,
-				LocalPref:         0,
-				Communities:       map[uint32]bool{},
+				AggregationLength:   32,
+				AggregationLengthV6: 128,
+				LocalPref:           0,
+				Communities:         map[uint32]bool{},
 			},
 		}, nil
 	}
@@ -380,21 +385,34 @@ func parseBGPAdvertisements(ads []bgpAdvertisement, cidrs []*net.IPNet, communit
 	var ret []*BGPAdvertisement
 	for _, rawAd := range ads {
 		ad := &BGPAdvertisement{
-			AggregationLength: 32,
-			LocalPref:         0,
-			Communities:       map[uint32]bool{},
+			AggregationLength:   32,
+			AggregationLengthV6: 128,
+			LocalPref:           0,
+			Communities:         map[uint32]bool{},
 		}
 
 		if rawAd.AggregationLength != nil {
 			ad.AggregationLength = *rawAd.AggregationLength
 		}
 		if ad.AggregationLength > 32 {
-			return nil, fmt.Errorf("invalid aggregation length %q", ad.AggregationLength)
+			return nil, fmt.Errorf("invalid aggregation length %q for IPv4", ad.AggregationLength)
+		}
+		if rawAd.AggregationLengthV6 != nil {
+			ad.AggregationLengthV6 = *rawAd.AggregationLengthV6
+			if ad.AggregationLengthV6 > 128 {
+				return nil, fmt.Errorf("invalid aggregation length %q for IPv6", ad.AggregationLengthV6)
+			}
 		}
+
 		for _, cidr := range cidrs {
 			o, _ := cidr.Mask.Size()
-			if ad.AggregationLength < o {
-				return nil, fmt.Errorf("invalid aggregation length %d: prefix %q in this pool is more specific than the aggregation length", ad.AggregationLength, cidr)
+			maxLength := ad.AggregationLength
+			if cidr.IP.To4() == nil {
+				maxLength = ad.AggregationLengthV6
+			}
+			if maxLength < o {
+				return nil, fmt.Errorf("invalid aggregation length %d: prefix %q in "+
+					"this pool is more specific than the aggregation length", ad.AggregationLength, cidr)
 			}
 		}
 
diff --git a/manifests/metallb-frr.yaml b/manifests/metallb-frr.yaml
index 6e7c360e..c74780cf 100644
--- a/manifests/metallb-frr.yaml
+++ b/manifests/metallb-frr.yaml
@@ -404,7 +404,7 @@ data:
   vtysh.conf: |+
     service integrated-vtysh-config
   frr.conf: |+
-    frr version 8.0.1
+    frr version 7.5.1
     frr defaults traditional
     hostname Router
     line vty
@@ -448,7 +448,7 @@ spec:
           securityContext:
             runAsUser: 100
             runAsGroup: 101
-          image: frrouting/frr:v8.0.1
+          image: frrouting/frr:v7.5.1
           command: ["/bin/sh", "-c", "cp -rLf /tmp/frr/* /etc/frr/"]
           volumeMounts:
             - name: frr-startup
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index c0527ace..febbb8ed 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -228,6 +228,9 @@ func (c *bgpController) SetBalancer(l log.Logger, name string, lbIP net.IP, pool
 	c.svcAds[name] = nil
 	for _, adCfg := range pool.BGPAdvertisements {
 		m := net.CIDRMask(adCfg.AggregationLength, 32)
+		if lbIP.To4() == nil {
+			m = net.CIDRMask(adCfg.AggregationLengthV6, 128)
+		}
 		ad := &bgp.Advertisement{
 			Prefix: &net.IPNet{
 				IP:   lbIP.Mask(m),
diff --git a/speaker/main.go b/speaker/main.go
index 84fe5d13..9a1c11ef 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -165,7 +165,8 @@ func main() {
 }
 
 type controller struct {
-	myNode string
+	myNode  string
+	bgpType bgpImplementation
 
 	config *config.Config
 	client service
@@ -212,6 +213,7 @@ func newController(cfg controllerConfig) (*controller, error) {
 
 	ret := &controller{
 		myNode:    cfg.MyNode,
+		bgpType:   cfg.bgpType,
 		protocols: protocols,
 		announced: map[string]config.Proto{},
 		svcIP:     map[string]net.IP{},
diff --git a/tasks.py b/tasks.py
index 02c35355..b8d3bf6a 100644
--- a/tasks.py
+++ b/tasks.py
@@ -338,7 +338,7 @@ def dev_env(ctx, architecture="amd64", name="kind", cni=None, protocol=None,
 
     if protocol == "bgp":
         print("Configuring MetalLB with a BGP test environment")
-        bgp_dev_env()
+        bgp_dev_env(ip_family)
     elif protocol == "layer2":
         print("Configuring MetalLB with a layer 2 test environment")
         layer2_dev_env()
@@ -364,7 +364,7 @@ def layer2_dev_env():
 # Configure MetalLB in the dev-env for BGP testing. Start an frr based BGP
 # router in a container and configure MetalLB to peer with it.
 # See dev-env/bgp/README.md for some more information.
-def bgp_dev_env():
+def bgp_dev_env(ip_family):
     dev_env_dir = os.getcwd() + "/dev-env/bgp"
     frr_volume_dir = dev_env_dir + "/frr-volume"
 
@@ -400,6 +400,7 @@ def bgp_dev_env():
     # Node IPs.
     with open("%s/frr/bgpd.conf.tmpl" % dev_env_dir, 'r') as f:
         bgpd_config = "! THIS FILE IS AUTOGENERATED\n" + f.read()
+        bgpd_config = bgpd_config.replace("PROTOCOL", ip_family)
     for n in range(0, len(node_ips)):
         bgpd_config = bgpd_config.replace("NODE%d_IP" % n, node_ips[n])
     with open("%s/bgpd.conf" % frr_volume_dir, 'w') as f:
@@ -412,11 +413,18 @@ def bgp_dev_env():
     run("docker run -d --privileged --network kind --rm --name frr --volume %s:/etc/frr "
             "frrouting/frr:v7.5.1" % frr_volume_dir, echo=True)
 
-    peer_address = run('docker inspect -f "{{ '
+    if ip_family == "ipv4":
+        peer_address = run('docker inspect -f "{{ '
             'range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" frr', echo=True)
+    elif ip_family == "ipv6":
+        peer_address = run('docker inspect -f "{{ '
+            'range .NetworkSettings.Networks}}{{.GlobalIPv6Address}}{{end}}" frr', echo=True)
+    else:
+        raise Exit(message='Unsupported ip address family %s' % ip_family)
+
     with open("%s/config.yaml.tmpl" % dev_env_dir, 'r') as f:
         mlb_config = "# THIS FILE IS AUTOGENERATED\n" + f.read()
-    mlb_config = mlb_config.replace("PEER_ADDRESS", peer_address.stdout.strip())
+    mlb_config = mlb_config.replace("IP_PEER_ADDRESS", peer_address.stdout.strip())
     with open("%s/config.yaml" % dev_env_dir, 'w') as f:
         f.write(mlb_config)
     # Apply the MetalLB ConfigMap


Test patch:
diff --git a/dev-env/testsvc_ipv6.yaml b/dev-env/testsvc_ipv6.yaml
new file mode 100644
index 00000000..cdab619f
--- /dev/null
+++ b/dev-env/testsvc_ipv6.yaml
@@ -0,0 +1,37 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx
+spec:
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+      - name: nginx
+        image: nginx:1
+        ports:
+        - name: http
+          containerPort: 80
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nginx
+spec:
+  ipFamilyPolicy: SingleStack
+  ipFamilies:
+  - IPv6
+  ports:
+  - name: http
+    port: 80
+    protocol: TCP
+    targetPort: 80
+  selector:
+    app: nginx
+  type: LoadBalancer
diff --git a/e2etest/bgp_test.go b/e2etest/bgp_test.go
index 5c86206d..e6d708cc 100644
--- a/e2etest/bgp_test.go
+++ b/e2etest/bgp_test.go
@@ -82,11 +82,11 @@ var _ = ginkgo.Describe("BGP", func() {
 		{
 			name: frrEBGP,
 			nc: frrconfig.NeighborConfig{
-				ASN:      EBGPAsn,
+				ASN:      IBGPAsn,
 				Password: "ebgp-test",
 			},
 			rc: frrconfig.RouterConfig{
-				ASN:      IBGPAsn,
+				ASN:      EBGPAsn,
 				BGPPort:  180,
 				RouterID: "11.11.11.11",
 				Password: "ebgp-test",
@@ -95,11 +95,13 @@ var _ = ginkgo.Describe("BGP", func() {
 	}
 	ginkgo.BeforeEach(func() {
 		var err error
-		// 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")
 			}
+			if net.ParseIP(hostIPv6) == nil {
+				framework.Fail("Invalid hostIPv6")
+			}
 		}
 		cs = f.ClientSet
 		frrContainers, err = createFRRContainers(containersConf)
@@ -119,50 +121,54 @@ var _ = ginkgo.Describe("BGP", func() {
 		}
 	})
 
-	ginkgo.Context("type=Loadbalancer", func() {
+	table.DescribeTable("A service of protocol load balancer should work with", func(ipFamily string, setProtocoltest string) {
 		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,
-				})
+		var peers []peer
+		for _, c := range frrContainers {
+			c.RouterConfig.IPFamily = ipFamily
+			c.NeighborConfig.IPFamily = ipFamily
+			address := c.Ipv4
+			if ipFamily == "ipv6" {
+				address = c.Ipv6
 			}
-			configData := configFile{
-				Pools: []addressPool{
-					{
-						Name:     "bgp-test",
-						Protocol: BGP,
-						Addresses: []string{
-							"192.168.10.0/24",
-							"fc00:f853:0ccd:e799::/124",
-						},
+			peers = append(peers, peer{
+				Addr:     address,
+				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{
+				{
+					Name:     "bgp-test",
+					Protocol: BGP,
+					Addresses: []string{
+						"192.168.10.0/24",
+						"fc00:f853:0ccd:e799::/124",
 					},
 				},
-				Peers: peers,
-			}
-			for _, c := range frrContainers {
-				pairExternalFRRWithNodes(cs, c)
-			}
+			},
+			Peers: peers,
+		}
+		for _, c := range frrContainers {
+			pairExternalFRRWithNodes(cs, c)
+		}
 
-			err := updateConfigMap(cs, configData)
-			framework.ExpectNoError(err)
+		err := updateConfigMap(cs, configData)
+		framework.ExpectNoError(err)
 
-			for _, c := range frrContainers {
-				validateFRRPeeredWithNodes(cs, c)
-			}
+		for _, c := range frrContainers {
+			validateFRRPeeredWithNodes(cs, c)
+		}
 
-			allNodes, err = cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
-			framework.ExpectNoError(err)
-		})
+		allNodes, err = cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+		framework.ExpectNoError(err)
 
-		ginkgo.It("should work for ExternalTrafficPolicy=Cluster", func() {
+		if setProtocoltest == "ExternalTrafficPolicyCluster" {
 			svc, _ := createServiceWithBackend(cs, f.Namespace.Name, corev1.ServiceExternalTrafficPolicyTypeCluster)
 
 			defer func() {
@@ -171,13 +177,13 @@ var _ = ginkgo.Describe("BGP", func() {
 			}()
 
 			for _, c := range frrContainers {
-				validateService(cs, svc, allNodes.Items, c)
+				validateService(cs, svc, allNodes.Items, c, ipFamily)
 			}
-		})
+		}
 
-		ginkgo.It("should work for ExternalTrafficPolicy=Local", func() {
+		if setProtocoltest == "ExternalTrafficPolicyLocal" {
 			svc, jig := createServiceWithBackend(cs, f.Namespace.Name, corev1.ServiceExternalTrafficPolicyTypeLocal)
-			err := jig.Scale(2)
+			err = jig.Scale(2)
 			framework.ExpectNoError(err)
 
 			epNodes, err := jig.ListNodesWithEndpoint() // Only nodes with an endpoint should be advertising the IP
@@ -189,17 +195,22 @@ var _ = ginkgo.Describe("BGP", func() {
 			}()
 
 			for _, c := range frrContainers {
-				validateService(cs, svc, epNodes, c)
+				validateService(cs, svc, epNodes, c, ipFamily)
 			}
-		})
+		}
 
-		ginkgo.It("would check that FRR is running in the speaker", func() {
+		if setProtocoltest == "CheckSpeakerFRRPodRunning" {
 			for _, c := range frrContainers {
-				paired := frrIsPairedOnPods(cs, c)
-				framework.ExpectEqual(paired, true)
+				frrIsPairedOnPods(cs, c, ipFamily)
 			}
-		})
-	})
+		}
+	},
+		table.Entry("IPV4 - ExternalTrafficPolicyCluster", "ipv4", "ExternalTrafficPolicyCluster"),
+		table.Entry("IPV4 - ExternalTrafficPolicyLocal", "ipv4", "ExternalTrafficPolicyLocal"),
+		table.Entry("IPV4 - FRR running in the speaker POD", "ipv4", "CheckSpeakerFRRPodRunning"),
+		table.Entry("IPV6 - ExternalTrafficPolicyCluster", "ipv6", "ExternalTrafficPolicyCluster"),
+		table.Entry("IPV6 - ExternalTrafficPolicyLocal", "ipv6", "ExternalTrafficPolicyLocal"),
+		table.Entry("IPV6 - FRR running in the speaker POD", "ipv6", "CheckSpeakerFRRPodRunning"))
 
 	ginkgo.Context("metrics", func() {
 		var controllerPod *corev1.Pod
@@ -223,15 +234,21 @@ 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
+		table.DescribeTable("should be exposed by the controller", func(ipFamily string) {
 			poolName := "bgp-test"
 
 			var peers []peer
 			var peerAddrs []string
+
 			for _, c := range frrContainers {
+				c.RouterConfig.IPFamily = ipFamily
+				c.NeighborConfig.IPFamily = ipFamily
+				address := c.Ipv4
+				if ipFamily == "ipv6" {
+					address = c.Ipv6
+				}
 				peers = append(peers, peer{
-					Addr:     c.Ipv4,
+					Addr:     address,
 					ASN:      c.RouterConfig.ASN,
 					MyASN:    c.NeighborConfig.ASN,
 					Port:     c.RouterConfig.BGPPort,
@@ -239,7 +256,7 @@ var _ = ginkgo.Describe("BGP", func() {
 					Password: c.RouterConfig.Password,
 					HoldTime: c.RouterConfig.HoldTime,
 				})
-				peerAddrs = append(peerAddrs, c.Ipv4+fmt.Sprintf(":%d", c.RouterConfig.BGPPort))
+				peerAddrs = append(peerAddrs, address+fmt.Sprintf(":%d", c.RouterConfig.BGPPort))
 			}
 
 			configData := configFile{
@@ -357,7 +374,9 @@ var _ = ginkgo.Describe("BGP", func() {
 					return nil
 				}, 2*time.Minute, 1*time.Second).Should(BeNil())
 			}
-		})
+		},
+			table.Entry("IPV4 - Checking service", "ipv4"),
+			table.Entry("IPV6 - Checking service", "ipv6"))
 	})
 
 	ginkgo.Context("validate different AddressPools for type=Loadbalancer", func() {
@@ -367,11 +386,18 @@ var _ = ginkgo.Describe("BGP", func() {
 			framework.ExpectNoError(err)
 		})
 
-		table.DescribeTable("set different AddressPools ranges modes", func(addressPools []addressPool) {
+		table.DescribeTable("set different AddressPools ranges modes", func(addressPools []addressPool, ipFamily string) {
 			var peers []peer
+
 			for _, c := range frrContainers {
+				c.RouterConfig.IPFamily = ipFamily
+				c.NeighborConfig.IPFamily = ipFamily
+				address := c.Ipv4
+				if ipFamily == "ipv6" {
+					address = c.Ipv6
+				}
 				peers = append(peers, peer{
-					Addr:     c.Ipv4,
+					Addr:     address,
 					ASN:      c.RouterConfig.ASN,
 					MyASN:    c.NeighborConfig.ASN,
 					Port:     c.RouterConfig.BGPPort,
@@ -413,10 +439,10 @@ var _ = ginkgo.Describe("BGP", func() {
 			framework.ExpectNoError(err)
 
 			for _, c := range frrContainers {
-				validateService(cs, svc, allNodes.Items, c)
+				validateService(cs, svc, allNodes.Items, c, ipFamily)
 			}
 		},
-			table.Entry("AddressPool defined by network prefix", []addressPool{
+			table.Entry("IPV4 - test AddressPool defined by network prefix", []addressPool{
 				{
 					Name:     "bgp-test",
 					Protocol: BGP,
@@ -424,9 +450,9 @@ var _ = ginkgo.Describe("BGP", func() {
 						"192.168.10.0/24",
 						"fc00:f853:0ccd:e799::/124",
 					},
-				},
-			}),
-			table.Entry("AddressPool defined by address range", []addressPool{
+				}}, "ipv4",
+			),
+			table.Entry("IPV6 - test AddressPool defined by address range", []addressPool{
 				{
 					Name:     "bgp-test",
 					Protocol: BGP,
@@ -434,8 +460,8 @@ var _ = ginkgo.Describe("BGP", func() {
 						"192.168.10.0-192.168.10.18",
 						"fc00:f853:0ccd:e799::0-fc00:f853:0ccd:e799::18",
 					},
-				},
-			}),
+				}}, "ipv6",
+			),
 		)
 	})
 })
@@ -519,10 +545,10 @@ func validateFRRPeeredWithNodes(cs clientset.Interface, c *frrcontainer.FRR) {
 		framework.ExpectNoError(err)
 		err = frr.NeighborsMatchNodes(allNodes.Items, neighbors)
 		return err
-	}, 2*time.Minute, 1*time.Second).Should(BeNil())
+	}, 4*time.Minute, 1*time.Second).Should(BeNil())
 }
 
-func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1.Node, c *frrcontainer.FRR) {
+func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1.Node, c *frrcontainer.FRR, ipFamily string) {
 	port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
 	ingressIP := e2eservice.GetIngressPoint(
 		&svc.Status.LoadBalancer.Ingress[0])
@@ -536,20 +562,22 @@ func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1
 			return err
 		}
 
-		advertised := routes.ForIP(ingressIP, c)
+		advertised := routes.ForIP(ingressIP, c, ipFamily)
 		err = routes.MatchNodes(nodes, advertised)
 		if err != nil {
 			return err
 		}
 
-		frrRoutes, _, err := frr.Routes(c)
+		frrRoutes, frrRoutesV6, err := frr.Routes(c)
 		if err != nil {
 			return err
 		}
-
 		routes, ok := frrRoutes[ingressIP]
+		if ipFamily == "ipv6" {
+			routes, ok = frrRoutesV6[ingressIP]
+		}
 		if !ok {
-			return fmt.Errorf("%s not found in frr routes %v", ingressIP, frrRoutes)
+			return fmt.Errorf("%s not found in frr routes %v", ingressIP, routes)
 		}
 
 		err = frr.RoutesMatchNodes(nodes, routes)
@@ -557,19 +585,32 @@ func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1
 			return err
 		}
 		return nil
-	}, 2*time.Minute, 1*time.Second).Should(BeNil())
+	}, 4*time.Minute, 1*time.Second).Should(BeNil())
 }
 
-func frrIsPairedOnPods(cs clientset.Interface, n *frrcontainer.FRR) bool {
+func frrIsPairedOnPods(cs clientset.Interface, n *frrcontainer.FRR, ipFamily string) {
 	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 view %d neighbor %s json", n.NeighborConfig.ASN, n.Ipv4))
-	framework.ExpectNoError(err)
-	res, err := frr.NeighborConnected(toParse)
-	framework.ExpectNoError(err)
-	return res
+	Eventually(func() error {
+		address := n.Ipv4
+		if ipFamily == "ipv6" {
+			address = n.Ipv6
+		}
+		toParse, err := framework.RunKubectl("metallb-system", "exec", pods.Items[0].Name, "-c", "frr", "--", "vtysh", "-c", fmt.Sprintf("show bgp neighbor %s json", address))
+		if err != nil {
+			return err
+		}
+		res, err := frr.NeighborConnected(toParse)
+		if err != nil {
+			return err
+		}
+		if res != true {
+			return fmt.Errorf("expecting neighbor %s to be connected", n.Ipv4)
+		}
+		return nil
+	}, 4*time.Minute, 1*time.Second).Should(BeNil())
 }
 
 func createFRRContainers(c []containerConfig) ([]*frrcontainer.FRR, error) {
diff --git a/e2etest/e2etest_suite_test.go b/e2etest/e2etest_suite_test.go
index ad6f1388..8bc9ada7 100644
--- a/e2etest/e2etest_suite_test.go
+++ b/e2etest/e2etest_suite_test.go
@@ -62,7 +62,7 @@ func handleFlags() {
 	framework.RegisterCommonFlags(flag.CommandLine)
 	framework.RegisterClusterFlags(flag.CommandLine)
 	flag.UintVar(&servicePodPort, "service-pod-port", 80, "port number that pod opens, default: 80")
-	flag.BoolVar(&skipDockerCmd, "skip-docker", false, "et this to true if the BGP daemon is running on the host instead of in a container")
+	flag.BoolVar(&skipDockerCmd, "skip-docker", false, "set this to true if the BGP daemon is running on the host instead of in a container")
 	flag.StringVar(&ipv4ServiceRange, "ipv4-service-range", "0", "a range of IPv4 addresses for MetalLB to use when running in layer2 mode")
 	flag.StringVar(&ipv6ServiceRange, "ipv6-service-range", "0", "a range of IPv6 addresses for MetalLB to use when running in layer2 mode")
 	flag.Parse()
diff --git a/e2etest/pkg/frr/config/config.go b/e2etest/pkg/frr/config/config.go
index 841c865d..887eb7f7 100644
--- a/e2etest/pkg/frr/config/config.go
+++ b/e2etest/pkg/frr/config/config.go
@@ -17,15 +17,26 @@ import (
 
 // BGP router config.
 const bgpConfigTemplate = `
+route-map RMAP permit 10
+set ipv6 next-hop prefer-global
 router bgp {{.ASN}}
   no bgp ebgp-requires-policy
+  no bgp default ipv4-unicast
 {{range .Neighbors }}
   neighbor {{.Addr}} remote-as {{.ASN}}
-  neighbor {{.Addr}} next-hop-self
   {{ if .Password -}}
   neighbor {{.Addr}} password {{.Password}}
   {{- end }}
 {{- end }}
+  address-family {{.IPFamily}} unicast
+{{range .Neighbors }}
+    neighbor {{.Addr}} next-hop-self
+    neighbor {{.Addr}} activate
+    {{ if eq .IPFamily "ipv6" -}}
+    neighbor {{.Addr}} route-map RMAP in
+    {{- end }}
+{{- end }}
+  exit-address-family
 `
 
 type RouterConfig struct {
@@ -35,12 +46,14 @@ type RouterConfig struct {
 	RouterID  string
 	Password  string
 	HoldTime  string
+	IPFamily  string
 }
 
 type NeighborConfig struct {
 	ASN      uint32
 	Addr     string
 	Password string
+	IPFamily string
 }
 
 // Set the IP of each node in the cluster in the BGP router configuration.
@@ -55,7 +68,7 @@ func BGPPeersForAllNodes(cs clientset.Interface, nc NeighborConfig, rc RouterCon
 		return "", errors.Wrapf(err, "Failed to get cluster nodes")
 	}
 
-	// TODO: The IP address handling will need updates to add support for IPv6.
+	// TODO: The IP address handling will need updates to add support for dual stack
 	for _, node := range nodes.Items {
 		for i := range node.Status.Addresses {
 			if node.Status.Addresses[i].Type == "InternalIP" {
diff --git a/e2etest/pkg/frr/parse.go b/e2etest/pkg/frr/parse.go
index 63302e86..1a596ee5 100644
--- a/e2etest/pkg/frr/parse.go
+++ b/e2etest/pkg/frr/parse.go
@@ -40,7 +40,8 @@ type FRRRoute struct {
 	Valid    bool   `json:"valid"`
 	PeerID   string `json:"peerId"`
 	Nexthops []struct {
-		IP string `json:"ip"`
+		IP    string `json:"ip"`
+		Scope string `json:"scope"`
 	} `json:"nexthops"`
 }
 
@@ -132,6 +133,9 @@ func parseRoutes(vtyshRes string) (map[string]Route, error) {
 				if ip == nil {
 					return nil, fmt.Errorf("failed to parse ip %s", h.IP)
 				}
+				if ip.To4() == nil && h.Scope == "link-local" {
+					continue
+				}
 				r.NextHops = append(r.NextHops, ip)
 			}
 		}
diff --git a/e2etest/pkg/frr/parse_test.go b/e2etest/pkg/frr/parse_test.go
index 5fa01966..4eacddda 100644
--- a/e2etest/pkg/frr/parse_test.go
+++ b/e2etest/pkg/frr/parse_test.go
@@ -322,6 +322,124 @@ const threeNeighbours = `
     "nextConnectTimerDueInMsecs":107000,
     "readThread":"off",
     "writeThread":"off"
+  },
+  "fc00:f853:ccd:e793::4":{
+    "remoteAs":64512,
+    "localAs":64513,
+    "nbrExternalLink":true,
+    "hostname":"kind-control-plane",
+    "bgpVersion":4,
+    "remoteRouterId":"11.11.11.11",
+    "localRouterId":"172.18.0.5",
+    "bgpState":"Established",
+    "bgpTimerUpMsec":0,
+    "bgpTimerUpString":"00:00:00",
+    "bgpTimerUpEstablishedEpoch":1636386709,
+    "bgpTimerLastRead":4000,
+    "bgpTimerLastWrite":0,
+    "bgpInUpdateElapsedTimeMsecs":78272000,
+    "bgpTimerHoldTimeMsecs":90000,
+    "bgpTimerKeepAliveIntervalMsecs":30000,
+    "neighborCapabilities":{
+      "4byteAs":"advertisedAndReceived",
+      "extendedMessage":"advertisedAndReceived",
+      "addPath":{
+        "ipv6Unicast":{
+          "rxAdvertisedAndReceived":true
+        }
+      },
+      "routeRefresh":"advertisedAndReceivedOldNew",
+      "enhancedRouteRefresh":"advertisedAndReceived",
+      "multiprotocolExtensions":{
+        "ipv4Unicast":{
+          "received":true
+        },
+        "ipv6Unicast":{
+          "advertisedAndReceived":true
+        }
+      },
+      "hostName":{
+        "advHostName":"85e811e29230",
+        "advDomainName":"n\/a",
+        "rcvHostName":"kind-control-plane",
+        "rcvDomainName":"n\/a"
+      },
+      "gracefulRestart":"advertisedAndReceived",
+      "gracefulRestartRemoteTimerMsecs":120000,
+      "addressFamiliesByPeer":"none"
+    },
+    "gracefulRestartInfo":{
+      "endOfRibSend":{
+        "ipv6Unicast":true
+      },
+      "endOfRibRecv":{
+      },
+      "localGrMode":"Helper*",
+      "remoteGrMode":"Helper",
+      "rBit":true,
+      "timers":{
+        "configuredRestartTimer":120,
+        "receivedRestartTimer":120
+      },
+      "ipv6Unicast":{
+        "fBit":false,
+        "endOfRibStatus":{
+          "endOfRibSend":true,
+          "endOfRibSentAfterUpdate":true,
+          "endOfRibRecv":false
+        },
+        "timers":{
+          "stalePathTimer":360
+        }
+      }
+    },
+    "messageStats":{
+      "depthInq":0,
+      "depthOutq":0,
+      "opensSent":1,
+      "opensRecv":1,
+      "notificationsSent":0,
+      "notificationsRecv":0,
+      "updatesSent":1,
+      "updatesRecv":0,
+      "keepalivesSent":1,
+      "keepalivesRecv":1,
+      "routeRefreshSent":0,
+      "routeRefreshRecv":0,
+      "capabilitySent":0,
+      "capabilityRecv":0,
+      "totalSent":3,
+      "totalRecv":2
+    },
+    "minBtwnAdvertisementRunsTimerMsecs":0,
+    "addressFamilyInfo":{
+      "ipv6Unicast":{
+        "updateGroupId":1,
+        "subGroupId":1,
+        "packetQueueLength":0,
+        "routerAlwaysNextHop":true,
+        "commAttriSentToNbr":"extendedAndStandard",
+        "acceptedPrefixCounter":0,
+        "sentPrefixCounter":0
+      }
+    },
+    "connectionsEstablished":1,
+    "connectionsDropped":0,
+    "lastResetTimerMsecs":4000,
+    "lastResetDueTo":"No AFI\/SAFI activated for peer",
+    "lastResetCode":30,
+    "hostLocal":"fc00:f853:ccd:e793::5",
+    "portLocal":180,
+    "hostForeign":"fc00:f853:ccd:e793::4",
+    "portForeign":53568,
+    "nexthop":"172.18.0.5",
+    "nexthopGlobal":"fc00:f853:ccd:e793::5",
+    "nexthopLocal":"fe80::42:acff:fe12:5",
+    "bgpConnection":"sharedNetwork",
+    "connectRetryTimer":120,
+    "authenticationEnabled":1,
+    "readThread":"on",
+    "writeThread":"on"
   }
 }`
 
@@ -330,8 +448,8 @@ func TestNeighbours(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Failed to parse %s", err)
 	}
-	if len(nn) != 3 {
-		t.Fatalf("Expected 3 neighbours, got %d", len(nn))
+	if len(nn) != 4 {
+		t.Fatalf("Expected 4 neighbours, got %d", len(nn))
 	}
 	sort.Slice(nn, func(i, j int) bool {
 		return (bytes.Compare(nn[i].ip, nn[j].ip) < 0)
diff --git a/e2etest/pkg/routes/routes.go b/e2etest/pkg/routes/routes.go
index 3d67ede2..9d709c7c 100644
--- a/e2etest/pkg/routes/routes.go
+++ b/e2etest/pkg/routes/routes.go
@@ -25,8 +25,12 @@ func init() {
 
 // For IP returns the list of routes in the given container
 // (or in the current host) to reach the service ip.
-func ForIP(target string, exec executor.Executor) []net.IP {
+func ForIP(target string, exec executor.Executor, ipFamily string) []net.IP {
+
 	res, err := exec.Exec("ip", []string{"route", "show", target}...)
+	if ipFamily == "ipv6" {
+		res, err = exec.Exec("ip", []string{"-6", "route", "show", target}...)
+	}
 	framework.ExpectNoError(err)
 
 	routes := make([]net.IP, 0)
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisement.golden b/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
index ced74571..a341486e 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
index 33831853..b6488164 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalid.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalid.golden
index 75f2dbe5..244ed543 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalid.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalid.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp default ipv4-unicast
   bgp router-id 10.1.1.254
 
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
index 7862d7d5..21768dd8 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
index 063c013e..6099f2c8 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
index 7862d7d5..21768dd8 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestSingleSession.golden b/internal/bgp/frr/testdata/TestSingleSession.golden
index 7862d7d5..21768dd8 100644
--- a/internal/bgp/frr/testdata/TestSingleSession.golden
+++ b/internal/bgp/frr/testdata/TestSingleSession.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestTwoAdvertisements.golden b/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
index 45ce8a4e..ae9fc482 100644
--- a/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
+++ b/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestTwoSessions.golden b/internal/bgp/frr/testdata/TestTwoSessions.golden
index be027eaf..5177eed2 100644
--- a/internal/bgp/frr/testdata/TestTwoSessions.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessions.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
@@ -23,7 +23,7 @@ router bgp 100 view 100
     neighbor 10.2.2.254 activate
   exit-address-family
 
-router bgp 300 view 300
+router bgp 300
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden b/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
index 7862d7d5..21768dd8 100644
--- a/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden b/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
index c7db97c5..006c180c 100644
--- a/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
@@ -3,7 +3,7 @@ log file /etc/frr/frr.log informational
 log timestamp precision 3
 hostname dummyhostname
 
-router bgp 100 view 100
+router bgp 100
   no bgp ebgp-requires-policy
   no bgp network import-check
   no bgp default ipv4-unicast
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index b5e53096..c57f69c9 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -80,6 +80,7 @@ address-pools:
     localpref: 100
     communities: ["bar", "1234:2345"]
   - aggregation-length: 24
+    aggregation-length-v6: 64
 - name: pool2
   protocol: bgp
   addresses:
@@ -125,16 +126,18 @@ address-pools:
 						AutoAssign:    false,
 						BGPAdvertisements: []*BGPAdvertisement{
 							{
-								AggregationLength: 32,
-								LocalPref:         100,
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								LocalPref:           100,
 								Communities: map[uint32]bool{
 									0xfc0004d2: true,
 									0x04D20929: true,
 								},
 							},
 							{
-								AggregationLength: 24,
-								Communities:       map[uint32]bool{},
+								AggregationLength:   24,
+								AggregationLengthV6: 64,
+								Communities:         map[uint32]bool{},
 							},
 						},
 					},
@@ -144,8 +147,9 @@ address-pools:
 						AutoAssign: true,
 						BGPAdvertisements: []*BGPAdvertisement{
 							{
-								AggregationLength: 32,
-								Communities:       map[uint32]bool{},
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
 							},
 						},
 					},
@@ -449,8 +453,9 @@ address-pools:
 						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
 							{
-								AggregationLength: 32,
-								Communities:       map[uint32]bool{},
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
 							},
 						},
 					},
@@ -474,8 +479,9 @@ address-pools:
 						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
 							{
-								AggregationLength: 32,
-								Communities:       map[uint32]bool{},
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
 							},
 						},
 					},
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index d79da061..c818adfa 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -210,6 +210,7 @@ func TestBGPSpeaker(t *testing.T) {
 	c, err := newController(controllerConfig{
 		MyNode:        "pandora",
 		DisableLayer2: true,
+		bgpType:       bgpNative,
 	})
 	if err != nil {
 		t.Fatalf("creating controller: %s", err)
@@ -224,7 +225,9 @@ func TestBGPSpeaker(t *testing.T) {
 		svc      *v1.Service
 		eps      k8s.EpsOrSlices
 
-		wantAds map[string][]*bgp.Advertisement
+		wantAds        map[string][]*bgp.Advertisement
+		expectedCfgRet k8s.SyncState
+		expectedLBRet  k8s.SyncState
 	}{
 		{
 			desc:     "Service ignored, no config",
@@ -251,7 +254,9 @@ func TestBGPSpeaker(t *testing.T) {
 				},
 				Type: k8s.Eps,
 			},
-			wantAds: map[string][]*bgp.Advertisement{},
+			wantAds:        map[string][]*bgp.Advertisement{},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -278,6 +283,7 @@ func TestBGPSpeaker(t *testing.T) {
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
 		},
 
 		{
@@ -307,6 +313,8 @@ func TestBGPSpeaker(t *testing.T) {
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -341,6 +349,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -371,6 +381,8 @@ func TestBGPSpeaker(t *testing.T) {
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -409,6 +421,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -457,6 +471,8 @@ func TestBGPSpeaker(t *testing.T) {
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -473,6 +489,8 @@ func TestBGPSpeaker(t *testing.T) {
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -503,6 +521,8 @@ func TestBGPSpeaker(t *testing.T) {
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -543,6 +563,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -608,6 +630,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -670,6 +694,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -708,6 +734,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -753,6 +781,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -800,6 +830,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -817,6 +849,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -870,6 +904,8 @@ func TestBGPSpeaker(t *testing.T) {
 					},
 				},
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
+			expectedLBRet:  k8s.SyncStateSuccess,
 		},
 
 		{
@@ -878,18 +914,19 @@ func TestBGPSpeaker(t *testing.T) {
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.5:0": nil,
 			},
+			expectedCfgRet: k8s.SyncStateReprocessAll,
 		},
 	}
 
 	l := log.NewNopLogger()
 	for _, test := range tests {
 		if test.config != nil {
-			if c.SetConfig(l, test.config) == k8s.SyncStateError {
+			if c.SetConfig(l, test.config) != test.expectedCfgRet {
 				t.Errorf("%q: SetConfig failed", test.desc)
 			}
 		}
 		if test.balancer != "" {
-			if c.SetBalancer(l, test.balancer, test.svc, test.eps) == k8s.SyncStateError {
+			if c.SetBalancer(l, test.balancer, test.svc, test.eps) != test.expectedLBRet {
 				t.Errorf("%q: SetBalancer failed", test.desc)
 			}
 		}
@@ -911,6 +948,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 	c, err := newController(controllerConfig{
 		MyNode:        "pandora",
 		DisableLayer2: true,
+		bgpType:       bgpNative,
 	})
 	if err != nil {
 		t.Fatalf("creating controller: %s", err)
@@ -1723,6 +1761,7 @@ func TestNodeSelectors(t *testing.T) {
 	c, err := newController(controllerConfig{
 		MyNode:        "pandora",
 		DisableLayer2: true,
+		bgpType:       bgpNative,
 	})
 	if err != nil {
 		t.Fatalf("creating controller: %s", err)