Repository: metallb/metallb
Pull request #1054 (2021-11-16 12:29:50)

Fix e2etests to use the namespace variable
We should use the testNameSpace variable all over the tests code to run the tests in the right namesapce.



Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 53343da1..6ee388b3 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -35,10 +35,10 @@ jobs:
       - setup_ci_image
       - setup_kind
       - run: inv build
-      - run: inv dev-env -i  << parameters.ip-family >> -b  << parameters.bgp-type >>
+      - run: inv dev-env -i  << parameters.ip-family >> -b  << parameters.bgp-type >> -l all
       - run: |
           SKIP="none"
-          if [ "<< parameters.bgp-type >>" = "native" ]; then SKIP="$SKIP\|FRR"; fi
+          if [ "<< parameters.bgp-type >>" = "native" ]; then SKIP="$SKIP\|FRR\|BFD"; 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"; fi
diff --git a/dev-env/bgp/frr/daemons b/dev-env/bgp/frr/daemons
index 5e1f5bdc..db81175f 100644
--- a/dev-env/bgp/frr/daemons
+++ b/dev-env/bgp/frr/daemons
@@ -27,7 +27,7 @@ eigrpd=no
 babeld=no
 sharpd=no
 pbrd=no
-bfdd=no
+bfdd=yes
 fabricd=no
 vrrpd=no
 
diff --git a/internal/bgp/bgp.go b/internal/bgp/bgp.go
index a4f245cb..96e8abcb 100644
--- a/internal/bgp/bgp.go
+++ b/internal/bgp/bgp.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/go-kit/kit/log"
+	"go.universe.tf/metallb/internal/config"
 )
 
 // Advertisement represents one network path and its BGP attributes.
@@ -44,5 +45,6 @@ type Session interface {
 }
 
 type SessionManager interface {
-	NewSession(logger log.Logger, addr string, srcAddr net.IP, myASN uint32, routerID net.IP, asn uint32, hold time.Duration, keepalive time.Duration, password string, myNode string) (Session, error)
+	NewSession(logger log.Logger, addr string, srcAddr net.IP, myASN uint32, routerID net.IP, asn uint32, hold, keepalive time.Duration, password, myNode, bfdProfile string) (Session, error)
+	SyncBFDProfiles(profiles map[string]*config.BFDProfile) error
 }
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index daba17c3..a8bc8ffe 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -47,6 +47,9 @@ router bgp {{.MyASN}}
   {{ if .SrcAddr -}}
   neighbor {{.Addr}} update-source {{.SrcAddr}}
   {{- end }}
+{{- if ne .BFDProfile ""}} 
+  neighbor {{.Addr}} bfd profile {{.BFDProfile}}
+{{- 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 */}}
@@ -64,14 +67,41 @@ router bgp {{.MyASN}}
     network {{.Prefix}}
   exit-address-family
 {{- end}}
-{{end}}
-{{end}}
-`
+{{end -}}
+{{end }}
+{{- if gt (len .BFDProfiles) 0}}
+bfd
+{{- range .BFDProfiles }}
+  profile {{.Name}}
+    {{ if .ReceiveInterval -}}
+    receive-interval {{.ReceiveInterval}}
+    {{end -}}
+    {{ if .TransmitInterval -}}
+    transmit-interval {{.TransmitInterval}}
+    {{end -}}
+    {{ if .DetectMultiplier -}}
+    detect-multiplier {{.DetectMultiplier}}
+    {{end -}}
+    {{ if .EchoMode -}}
+    echo-mode
+    {{end -}}
+    {{ if .EchoInterval -}}
+    echo-interval {{.EchoInterval}}
+    {{end -}}
+    {{ if .PassiveMode -}}
+    passive-mode
+    {{end -}}
+    {{ if .MinimumTTL -}}
+    minimum-ttl {{ .MinimumTTL }}
+    {{end -}}
+{{ end }}
+{{ end }}`
 
 type frrConfig struct {
-	Loglevel string
-	Hostname string
-	Routers  map[string]*routerConfig
+	Loglevel    string
+	Hostname    string
+	Routers     map[string]*routerConfig
+	BFDProfiles []BFDProfile
 }
 
 type routerConfig struct {
@@ -80,6 +110,17 @@ type routerConfig struct {
 	Neighbors map[string]*neighborConfig
 }
 
+type BFDProfile struct {
+	Name             string
+	ReceiveInterval  *uint32
+	TransmitInterval *uint32
+	DetectMultiplier *uint32
+	EchoInterval     *uint32
+	EchoMode         bool
+	PassiveMode      bool
+	MinimumTTL       *uint32
+}
+
 type neighborConfig struct {
 	ASN            uint32
 	Addr           string
@@ -89,6 +130,7 @@ type neighborConfig struct {
 	KeepaliveTime  uint64
 	Password       string
 	Advertisements []*advertisementConfig
+	BFDProfile     string
 }
 
 type advertisementConfig struct {
@@ -168,7 +210,6 @@ func generateAndReloadConfigFile(config *frrConfig) error {
 	if err != nil {
 		return err
 	}
-
 	err = writeConfig(configString, configFileName)
 	if err != nil {
 		return err
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index e114ea7d..0efd1ba6 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -6,17 +6,21 @@ import (
 	"fmt"
 	"net"
 	"os"
+	"sort"
 	"strconv"
 	"time"
 
 	"github.com/go-kit/kit/log"
 	"go.universe.tf/metallb/internal/bgp"
+	"go.universe.tf/metallb/internal/config"
 )
 
 // As the MetalLB controller should handle messages synchronously, there should
 // no need to lock this data structure. TODO: confirm this.
+
 type sessionManager struct {
-	sessions map[string]*session
+	sessions    map[string]*session
+	bfdProfiles []BFDProfile
 }
 
 type session struct {
@@ -31,6 +35,7 @@ type session struct {
 	logger         log.Logger
 	password       string
 	advertised     []*bgp.Advertisement
+	bfdProfile     string
 	sessionManager *sessionManager
 }
 
@@ -109,7 +114,7 @@ func (s *session) Close() error {
 //
 // The session will immediately try to connect and synchronize its
 // local state with the peer.
-func (sm *sessionManager) NewSession(l log.Logger, addr string, srcAddr net.IP, myASN uint32, routerID net.IP, asn uint32, holdTime time.Duration, keepaliveTime time.Duration, password string, myNode string) (bgp.Session, error) {
+func (sm *sessionManager) NewSession(l log.Logger, addr string, srcAddr net.IP, myASN uint32, routerID net.IP, asn uint32, holdTime, keepaliveTime time.Duration, password, myNode, bfdProfile string) (bgp.Session, error) {
 	s := &session{
 		myASN:          myASN,
 		routerID:       routerID,
@@ -123,6 +128,7 @@ func (sm *sessionManager) NewSession(l log.Logger, addr string, srcAddr net.IP,
 		password:       password,
 		advertised:     []*bgp.Advertisement{},
 		sessionManager: sm,
+		bfdProfile:     bfdProfile,
 	}
 
 	_ = sm.addSession(s)
@@ -162,6 +168,24 @@ func (sm *sessionManager) deleteSession(s *session) error {
 	return nil
 }
 
+func (sm *sessionManager) SyncBFDProfiles(profiles map[string]*config.BFDProfile) error {
+	sm.bfdProfiles = make([]BFDProfile, 0)
+	for _, p := range profiles {
+		frrProfile := configBFDProfileToFRR(p)
+		sm.bfdProfiles = append(sm.bfdProfiles, *frrProfile)
+	}
+	sort.Slice(sm.bfdProfiles, func(i, j int) bool {
+		return sm.bfdProfiles[i].Name < sm.bfdProfiles[j].Name
+	})
+
+	frrConfig, err := sm.createConfig()
+	if err != nil {
+		return err
+	}
+
+	return generateAndReloadConfigFile(frrConfig)
+}
+
 func (sm *sessionManager) createConfig() (*frrConfig, error) {
 	hostname, err := osHostname()
 	if err != nil {
@@ -169,9 +193,10 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 	}
 
 	config := &frrConfig{
-		Hostname: hostname,
-		Loglevel: "informational",
-		Routers:  make(map[string]*routerConfig),
+		Hostname:    hostname,
+		Loglevel:    "informational",
+		Routers:     make(map[string]*routerConfig),
+		BFDProfiles: sm.bfdProfiles,
 	}
 	frrLogLevel, found := os.LookupEnv("FRR_LOGGING_LEVEL")
 	if found {
@@ -215,6 +240,7 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 				KeepaliveTime:  uint64(s.keepaliveTime / time.Second),
 				Password:       s.password,
 				Advertisements: make([]*advertisementConfig, 0),
+				BFDProfile:     s.bfdProfile,
 			}
 			if s.srcAddr != nil {
 				neighbor.SrcAddr = s.srcAddr.String()
@@ -247,6 +273,20 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 
 func NewSessionManager() *sessionManager {
 	return &sessionManager{
-		sessions: map[string]*session{},
+		sessions:    map[string]*session{},
+		bfdProfiles: []BFDProfile{},
 	}
 }
+
+func configBFDProfileToFRR(p *config.BFDProfile) *BFDProfile {
+	res := &BFDProfile{}
+	res.Name = p.Name
+	res.ReceiveInterval = p.ReceiveInterval
+	res.TransmitInterval = p.TransmitInterval
+	res.DetectMultiplier = p.DetectMultiplier
+	res.EchoInterval = p.EchoInterval
+	res.EchoMode = p.EchoMode
+	res.PassiveMode = p.PassiveMode
+	res.MinimumTTL = p.MinimumTTL
+	return res
+}
diff --git a/internal/bgp/frr/parse.go b/internal/bgp/frr/parse.go
index d1c1505b..f69ecbc2 100644
--- a/internal/bgp/frr/parse.go
+++ b/internal/bgp/frr/parse.go
@@ -55,6 +55,30 @@ type FRRRoute struct {
 	} `json:"nexthops"`
 }
 
+type BFDPeer struct {
+	Multihop               bool   `json:"multihop"`
+	Peer                   string `json:"peer"`
+	Local                  string `json:"local"`
+	Vrf                    string `json:"vrf"`
+	Interface              string `json:"interface"`
+	ID                     int    `json:"id"`
+	RemoteID               int64  `json:"remote-id"`
+	PassiveMode            bool   `json:"passive-mode"`
+	Status                 string `json:"status"`
+	Uptime                 int    `json:"uptime"`
+	Diagnostic             string `json:"diagnostic"`
+	RemoteDiagnostic       string `json:"remote-diagnostic"`
+	ReceiveInterval        int    `json:"receive-interval"`
+	TransmitInterval       int    `json:"transmit-interval"`
+	EchoReceiveInterval    int    `json:"echo-receive-interval"`
+	EchoTransmitInterval   int    `json:"echo-transmit-interval"`
+	DetectMultiplier       int    `json:"detect-multiplier"`
+	RemoteReceiveInterval  int    `json:"remote-receive-interval"`
+	RemoteTransmitInterval int    `json:"remote-transmit-interval"`
+	RemoteEchoInterval     int    `json:"remote-echo-interval"`
+	RemoteDetectMultiplier int    `json:"remote-detect-multiplier"`
+}
+
 // parseNeighbour takes the result of a show bgp neighbor x.y.w.z
 // and parses the informations related to the neighbour.
 func ParseNeighbour(vtyshRes string) (*Neighbor, error) {
@@ -167,3 +191,17 @@ func ParseRoutes(vtyshRes string) (map[string]Route, error) {
 	}
 	return res, nil
 }
+
+func ParseBFDPeers(vtyshRes string) (map[string]BFDPeer, error) {
+	parseRes := []BFDPeer{}
+	err := json.Unmarshal([]byte(vtyshRes), &parseRes)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to parse vtysh response")
+	}
+	res := make(map[string]BFDPeer)
+	for _, p := range parseRes {
+		res[p.Peer] = p
+
+	}
+	return res, nil
+}
diff --git a/internal/bgp/native/native.go b/internal/bgp/native/native.go
index 072569c7..e89c6af3 100644
--- a/internal/bgp/native/native.go
+++ b/internal/bgp/native/native.go
@@ -21,6 +21,7 @@ import (
 	"github.com/go-kit/kit/log"
 	"github.com/go-kit/kit/log/level"
 	"go.universe.tf/metallb/internal/bgp"
+	"go.universe.tf/metallb/internal/config"
 	"golang.org/x/sys/unix"
 )
 
@@ -65,7 +66,7 @@ func NewSessionManager() *sessionManager {
 //
 // The session will immediately try to connect and synchronize its
 // local state with the peer.
-func (sm *sessionManager) NewSession(l log.Logger, addr string, srcAddr net.IP, myASN uint32, routerID net.IP, asn uint32, holdTime time.Duration, keepaliveTime time.Duration, password string, myNode string) (bgp.Session, error) {
+func (sm *sessionManager) NewSession(l log.Logger, addr string, srcAddr net.IP, myASN uint32, routerID net.IP, asn uint32, holdTime, keepaliveTime time.Duration, password, myNode, bfdProfile string) (bgp.Session, error) {
 	ret := &session{
 		addr:          addr,
 		srcAddr:       srcAddr,
@@ -90,6 +91,10 @@ func (sm *sessionManager) NewSession(l log.Logger, addr string, srcAddr net.IP,
 	return ret, nil
 }
 
+func (sm *sessionManager) SyncBFDProfiles(profiles map[string]*config.BFDProfile) error {
+	return errors.New("bfd profiles not supported in native mode")
+}
+
 // run tries to stay connected to the peer, and pumps route updates to it.
 func (s *session) run() {
 	defer stats.DeleteSession(s.addr)
diff --git a/internal/config/config.go b/internal/config/config.go
index 6d3cdfa9..84beb77a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -16,7 +16,6 @@ package config // import "go.universe.tf/metallb/internal/config"
 
 import (
 	"bytes"
-	"errors"
 	"fmt"
 	"net"
 	"reflect"
@@ -25,6 +24,7 @@ import (
 	"time"
 
 	"github.com/mikioh/ipaddr"
+	"github.com/pkg/errors"
 	yaml "gopkg.in/yaml.v2"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -36,6 +36,7 @@ type configFile struct {
 	Peers          []peer
 	BGPCommunities map[string]string `yaml:"bgp-communities"`
 	Pools          []addressPool     `yaml:"address-pools"`
+	BFDProfiles    []bfdProfile      `yaml:"bfd-profiles"`
 }
 
 type peer struct {
@@ -49,6 +50,7 @@ type peer struct {
 	RouterID      string         `yaml:"router-id"`
 	NodeSelectors []nodeSelector `yaml:"node-selectors"`
 	Password      string         `yaml:"password"`
+	BFDProfile    string         `yaml:"bfd-profile"`
 }
 
 type nodeSelector struct {
@@ -78,12 +80,25 @@ type bgpAdvertisement struct {
 	Communities         []string
 }
 
+type bfdProfile struct {
+	Name             string  `yaml:"name"`
+	ReceiveInterval  *uint32 `yaml:"receive-interval"`
+	TransmitInterval *uint32 `yaml:"transmit-interval"`
+	DetectMultiplier *uint32 `yaml:"detect-multiplier"`
+	EchoInterval     *uint32 `yaml:"echo-interval"`
+	EchoMode         bool    `yaml:"echo-mode"`
+	PassiveMode      bool    `yaml:"passive-mode"`
+	MinimumTTL       *uint32 `yaml:"minimum-ttl"`
+}
+
 // Config is a parsed MetalLB configuration.
 type Config struct {
 	// Routers that MetalLB should peer with.
 	Peers []*Peer
 	// Address pools from which to allocate load balancer IPs.
 	Pools map[string]*Pool
+	// BFD profiles that can be used by peers.
+	BFDProfiles map[string]*BFDProfile
 }
 
 // Proto holds the protocol we are speaking.
@@ -118,6 +133,8 @@ type Peer struct {
 	NodeSelectors []labels.Selector
 	// Authentication password for routers enforcing TCP MD5 authenticated sessions
 	Password string
+	// The optional BFD profile to be used for this BGP session
+	BFDProfile string
 	// TODO: more BGP session settings
 }
 
@@ -159,6 +176,18 @@ type BGPAdvertisement struct {
 	Communities map[uint32]bool
 }
 
+// BFDProfile describes a BFD profile to be applied to a set of peers.
+type BFDProfile struct {
+	Name             string
+	ReceiveInterval  *uint32
+	TransmitInterval *uint32
+	DetectMultiplier *uint32
+	EchoInterval     *uint32
+	EchoMode         bool
+	PassiveMode      bool
+	MinimumTTL       *uint32
+}
+
 func parseNodeSelector(ns *nodeSelector) (labels.Selector, error) {
 	if len(ns.MatchLabels)+len(ns.MatchExpressions) == 0 {
 		return labels.Everything(), nil
@@ -219,12 +248,32 @@ func Parse(bs []byte) (*Config, error) {
 		return nil, fmt.Errorf("could not parse config: %s", err)
 	}
 
-	cfg := &Config{Pools: map[string]*Pool{}}
+	cfg := &Config{
+		Pools:       map[string]*Pool{},
+		BFDProfiles: map[string]*BFDProfile{},
+	}
+
+	for i, bfd := range raw.BFDProfiles {
+		parsed, err := parseBFDProfile(bfd)
+		if err != nil {
+			return nil, fmt.Errorf("parsing bfd profile #%d: %s", i+1, err)
+		}
+		if _, ok := cfg.BFDProfiles[parsed.Name]; ok {
+			return nil, fmt.Errorf("found duplicate bfd profile name %s", parsed.Name)
+		}
+		cfg.BFDProfiles[bfd.Name] = parsed
+	}
+
 	for i, p := range raw.Peers {
 		peer, err := parsePeer(p)
 		if err != nil {
 			return nil, fmt.Errorf("parsing peer #%d: %s", i+1, err)
 		}
+		if peer.BFDProfile != "" {
+			if _, ok := cfg.BFDProfiles[peer.BFDProfile]; !ok {
+				return nil, fmt.Errorf("peer #%d referencing non existing bfd profile %s", i+1, peer.BFDProfile)
+			}
+		}
 		for _, ep := range cfg.Peers {
 			// TODO: Be smarter regarding conflicting peers. For example, two
 			// peers could have a different hold time but they'd still result
@@ -232,6 +281,7 @@ func Parse(bs []byte) (*Config, error) {
 			if reflect.DeepEqual(peer, ep) {
 				return nil, fmt.Errorf("peer #%d already exists", i+1)
 			}
+
 		}
 		cfg.Peers = append(cfg.Peers, peer)
 	}
@@ -335,6 +385,7 @@ func parsePeer(p peer) (*Peer, error) {
 	if p.Password != "" {
 		password = p.Password
 	}
+
 	return &Peer{
 		MyASN:         p.MyASN,
 		ASN:           p.ASN,
@@ -346,6 +397,7 @@ func parsePeer(p peer) (*Peer, error) {
 		RouterID:      routerID,
 		NodeSelectors: nodeSels,
 		Password:      password,
+		BFDProfile:    p.BFDProfile,
 	}, nil
 }
 
@@ -395,6 +447,39 @@ func parseAddressPool(p addressPool, bgpCommunities map[string]uint32) (*Pool, e
 	return ret, nil
 }
 
+func parseBFDProfile(p bfdProfile) (*BFDProfile, error) {
+	if p.Name == "" {
+		return nil, fmt.Errorf("missing bfd profile name")
+	}
+	res := &BFDProfile{}
+	res.Name = p.Name
+	var err error
+	res.DetectMultiplier, err = bfdIntFromConfig(p.DetectMultiplier, 2, 255)
+	if err != nil {
+		return nil, errors.Wrap(err, "invalid detect multiplier value")
+	}
+	res.ReceiveInterval, err = bfdIntFromConfig(p.ReceiveInterval, 10, 60000)
+	if err != nil {
+		return nil, errors.Wrap(err, "invalid receive interval value")
+	}
+	res.TransmitInterval, err = bfdIntFromConfig(p.TransmitInterval, 10, 60000)
+	if err != nil {
+		return nil, errors.Wrap(err, "invalid transmit interval value")
+	}
+	res.MinimumTTL, err = bfdIntFromConfig(p.MinimumTTL, 1, 254)
+	if err != nil {
+		return nil, errors.Wrap(err, "invalid minimum ttl value")
+	}
+	res.EchoInterval, err = bfdIntFromConfig(p.EchoInterval, 10, 60000)
+	if err != nil {
+		return nil, errors.Wrap(err, "invalid minimum ttl value")
+	}
+	res.EchoMode = p.EchoMode
+	res.PassiveMode = p.PassiveMode
+
+	return res, nil
+}
+
 func parseBGPAdvertisements(ads []bgpAdvertisement, cidrs []*net.IPNet, communities map[string]uint32) ([]*BGPAdvertisement, error) {
 	if len(ads) == 0 {
 		return []*BGPAdvertisement{
@@ -532,3 +617,13 @@ func cidrContainsCIDR(outer, inner *net.IPNet) bool {
 	}
 	return false
 }
+
+func bfdIntFromConfig(value *uint32, min, max uint32) (*uint32, error) {
+	if value == nil {
+		return nil, nil
+	}
+	if *value < min || *value > max {
+		return nil, fmt.Errorf("invalid value %d, must be in %d-%d range", *value, min, max)
+	}
+	return value, nil
+}
diff --git a/manifests/metallb-frr.yaml b/manifests/metallb-frr.yaml
index 965744a9..5fa357a2 100644
--- a/manifests/metallb-frr.yaml
+++ b/manifests/metallb-frr.yaml
@@ -350,7 +350,7 @@ data:
     babeld=no
     sharpd=no
     pbrd=no
-    bfdd=no
+    bfdd=yes
     fabricd=no
     vrrpd=no
 
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 4fcdc292..8b4fa23e 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -31,6 +31,7 @@ import (
 
 	"github.com/go-kit/kit/log"
 	"github.com/go-kit/kit/log/level"
+	"github.com/pkg/errors"
 )
 
 type bgpImplementation string
@@ -94,6 +95,10 @@ newPeers:
 		}
 	}
 
+	err := c.syncBFDProfiles(cfg.BFDProfiles)
+	if err != nil {
+		return errors.Wrap(err, "failed to sync bfd profiles")
+	}
 	return c.syncPeers(l)
 }
 
@@ -205,7 +210,7 @@ func (c *bgpController) syncPeers(l log.Logger) error {
 			if p.cfg.RouterID != nil {
 				routerID = p.cfg.RouterID
 			}
-			s, err := c.sessionManager.NewSession(c.logger, net.JoinHostPort(p.cfg.Addr.String(), strconv.Itoa(int(p.cfg.Port))), p.cfg.SrcAddr, p.cfg.MyASN, routerID, p.cfg.ASN, p.cfg.HoldTime, p.cfg.KeepaliveTime, p.cfg.Password, c.myNode)
+			s, err := c.sessionManager.NewSession(c.logger, net.JoinHostPort(p.cfg.Addr.String(), strconv.Itoa(int(p.cfg.Port))), p.cfg.SrcAddr, p.cfg.MyASN, routerID, p.cfg.ASN, p.cfg.HoldTime, p.cfg.KeepaliveTime, p.cfg.Password, c.myNode, p.cfg.BFDProfile)
 			if err != nil {
 				level.Error(l).Log("op", "syncPeers", "error", err, "peer", p.cfg.Addr, "msg", "failed to create BGP session")
 				errs++
@@ -228,6 +233,14 @@ func (c *bgpController) syncPeers(l log.Logger) error {
 	return nil
 }
 
+func (c *bgpController) syncBFDProfiles(profiles map[string]*config.BFDProfile) error {
+	if len(profiles) == 0 {
+		return nil
+	}
+
+	return c.sessionManager.SyncBFDProfiles(profiles)
+}
+
 func (c *bgpController) SetBalancer(l log.Logger, name string, lbIP net.IP, pool *config.Pool) error {
 	c.svcAds[name] = nil
 	for _, adCfg := range pool.BGPAdvertisements {
diff --git a/speaker/main.go b/speaker/main.go
index 9a1c11ef..0610cd6c 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -346,6 +346,7 @@ func poolFor(pools map[string]*config.Pool, ip net.IP) string {
 }
 
 func (c *controller) SetConfig(l log.Logger, cfg *config.Config) k8s.SyncState {
+
 	level.Debug(l).Log("event", "startUpdate", "msg", "start of config update")
 	defer level.Debug(l).Log("event", "endUpdate", "msg", "end of config update")
 
@@ -354,6 +355,14 @@ func (c *controller) SetConfig(l log.Logger, cfg *config.Config) k8s.SyncState {
 		return k8s.SyncStateErrorNoRetry
 	}
 
+	if c.bgpType == bgpNative {
+		err := validateFRROnlyConfiguration(cfg)
+		if err != nil {
+			level.Error(l).Log("op", "setConfig", "error", err)
+			return k8s.SyncStateError
+		}
+	}
+
 	for svc, ip := range c.svcIP {
 		if pool := poolFor(cfg.Pools, ip); pool == "" {
 			level.Error(l).Log("op", "setConfig", "service", svc, "ip", ip, "error", "service has no configuration under new config", "msg", "new configuration rejected")
diff --git a/speaker/validate.go b/speaker/validate.go
new file mode 100644
index 00000000..4fe52514
--- /dev/null
+++ b/speaker/validate.go
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package main
+
+import (
+	"fmt"
+
+	"github.com/pkg/errors"
+	"go.universe.tf/metallb/internal/config"
+)
+
+func validateFRROnlyConfiguration(cfg *config.Config) error {
+	for _, p := range cfg.Peers {
+		if p.BFDProfile != "" {
+			return fmt.Errorf("peer %s has bfd profile set", p.Addr)
+		}
+	}
+	if len(cfg.BFDProfiles) > 0 {
+		return errors.New("bfd profiles section set")
+	}
+	return nil
+}
diff --git a/tasks.py b/tasks.py
index 5ef8fb85..de62cfb9 100644
--- a/tasks.py
+++ b/tasks.py
@@ -263,10 +263,12 @@ def validate_kind_version():
     "ip_family": "Optional ipfamily of the cluster."
                  "Default: ipv4, supported families are 'ipv6' and 'dual'.",
     "bgp_type": "Type of BGP implementation to use."
-                "Supported: 'native' (default), 'frr'"
+                "Supported: 'native' (default), 'frr'",
+    "log_level": "Log level for the controller and the speaker."
+                "Default: info, Supported: 'all', 'debug', 'info', 'warn', 'error' or 'none'"
 })
 def dev_env(ctx, architecture="amd64", name="kind", cni=None, protocol=None,
-        node_img=None, ip_family="ipv4", bgp_type="native"):
+        node_img=None, ip_family="ipv4", bgp_type="native", log_level="info"):
     """Build and run MetalLB in a local Kind cluster.
 
     If the cluster specified by --name (default "kind") doesn't exist,
@@ -331,6 +333,7 @@ def dev_env(ctx, architecture="amd64", name="kind", cni=None, protocol=None,
         for image in binaries:
             manifest = re.sub("image: quay.io/metallb/{}:.*".format(image),
                           "image: quay.io/metallb/{}:dev-{}".format(image, architecture), manifest)
+            manifest = re.sub("--log-level=info", "--log-level={}".format(log_level), manifest)
         with open(tmpdir + "/metallb.yaml", "w") as f:
             f.write(manifest)
             f.flush()
diff --git a/website/content/configuration/_index.md b/website/content/configuration/_index.md
index da04d969..b6af7310 100644
--- a/website/content/configuration/_index.md
+++ b/website/content/configuration/_index.md
@@ -17,7 +17,7 @@ If you've named the config map `config.yaml`, you can deploy the manifest with `
 If you installed MetalLB with Helm, you will need to change the
 namespace of the config map to match the namespace in which MetalLB was
 deployed, and change the name of the config map from `config` to
-`metallb-config`.
+`metallb`.
 {{% /notice %}}
 
 The specific configuration depends on the protocol(s) you want to use


Test patch:
diff --git a/e2etest/bgp_test.go b/e2etest/bgp_test.go
index 87c7d85a..dd704fd2 100644
--- a/e2etest/bgp_test.go
+++ b/e2etest/bgp_test.go
@@ -33,10 +33,12 @@ import (
 	"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/frr"
 	frrconfig "go.universe.tf/metallb/e2etest/pkg/frr/config"
 	frrcontainer "go.universe.tf/metallb/e2etest/pkg/frr/container"
+	bgpfrr "go.universe.tf/metallb/internal/bgp/frr"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clientset "k8s.io/client-go/kubernetes"
@@ -45,10 +47,11 @@ import (
 )
 
 const (
-	frrIBGP = "frr-iBGP"
-	frrEBGP = "frr-eBGP"
-	IBGPAsn = 64512
-	EBGPAsn = 64513
+	frrIBGP      = "frr-iBGP"
+	frrEBGP      = "frr-eBGP"
+	IBGPAsn      = 64512
+	EBGPAsn      = 64513
+	baseRouterID = "10.10.10.%d"
 )
 
 var (
@@ -74,9 +77,7 @@ var _ = ginkgo.Describe("BGP", func() {
 			rc: frrconfig.RouterConfig{
 				ASN:      IBGPAsn,
 				BGPPort:  179,
-				RouterID: "10.10.10.10",
 				Password: "ibgp-test",
-				HoldTime: "180s",
 			},
 		},
 		{
@@ -88,7 +89,6 @@ var _ = ginkgo.Describe("BGP", func() {
 			rc: frrconfig.RouterConfig{
 				ASN:      EBGPAsn,
 				BGPPort:  180,
-				RouterID: "11.11.11.11",
 				Password: "ebgp-test",
 			},
 		},
@@ -106,6 +106,7 @@ var _ = ginkgo.Describe("BGP", func() {
 		cs = f.ClientSet
 		frrContainers, err = createFRRContainers(containersConf)
 		framework.ExpectNoError(err)
+
 	})
 
 	ginkgo.AfterEach(func() {
@@ -123,24 +124,6 @@ var _ = ginkgo.Describe("BGP", func() {
 
 	table.DescribeTable("A service of protocol load balancer should work with", func(ipFamily string, setProtocoltest string) {
 		var allNodes *corev1.NodeList
-		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:     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{
 				{
@@ -152,9 +135,11 @@ var _ = ginkgo.Describe("BGP", func() {
 					},
 				},
 			},
-			Peers: peers,
+			Peers: peersForContainers(frrContainers, ipFamily),
 		}
 		for _, c := range frrContainers {
+			c.RouterConfig.IPFamily = ipFamily
+			c.NeighborConfig.IPFamily = ipFamily
 			pairExternalFRRWithNodes(cs, c)
 		}
 
@@ -217,14 +202,14 @@ var _ = ginkgo.Describe("BGP", func() {
 		var speakerPods []*corev1.Pod
 
 		ginkgo.BeforeEach(func() {
-			pods, err := cs.CoreV1().Pods("metallb-system").List(context.Background(), metav1.ListOptions{
+			pods, err := cs.CoreV1().Pods(testNameSpace).List(context.Background(), metav1.ListOptions{
 				LabelSelector: "component=controller",
 			})
 			framework.ExpectNoError(err)
 			framework.ExpectEqual(len(pods.Items), 1, "More than one controller found")
 			controllerPod = &pods.Items[0]
 
-			speakers, err := cs.CoreV1().Pods("metallb-system").List(context.Background(), metav1.ListOptions{
+			speakers, err := cs.CoreV1().Pods(testNameSpace).List(context.Background(), metav1.ListOptions{
 				LabelSelector: "component=speaker",
 			})
 			framework.ExpectNoError(err)
@@ -238,9 +223,7 @@ var _ = ginkgo.Describe("BGP", func() {
 		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
@@ -248,15 +231,6 @@ var _ = ginkgo.Describe("BGP", func() {
 				if ipFamily == "ipv6" {
 					address = c.Ipv6
 				}
-				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,
-				})
 				peerAddrs = append(peerAddrs, address+fmt.Sprintf(":%d", c.RouterConfig.BGPPort))
 			}
 
@@ -271,7 +245,7 @@ var _ = ginkgo.Describe("BGP", func() {
 						},
 					},
 				},
-				Peers: peers,
+				Peers: peersForContainers(frrContainers, ipFamily),
 			}
 			for _, c := range frrContainers {
 				pairExternalFRRWithNodes(cs, c)
@@ -286,7 +260,7 @@ var _ = ginkgo.Describe("BGP", func() {
 
 			ginkgo.By("checking the metrics when no service is added")
 			Eventually(func() error {
-				controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod)
+				controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod, testNameSpace)
 				if err != nil {
 					return err
 				}
@@ -305,7 +279,7 @@ var _ = ginkgo.Describe("BGP", func() {
 				ginkgo.By(fmt.Sprintf("checking speaker %s", speaker.Name))
 
 				Eventually(func() error {
-					speakerMetrics, err := metrics.ForPod(controllerPod, speaker)
+					speakerMetrics, err := metrics.ForPod(controllerPod, speaker, testNameSpace)
 					if err != nil {
 						return err
 					}
@@ -332,7 +306,7 @@ var _ = ginkgo.Describe("BGP", func() {
 
 			ginkgo.By("checking the metrics when a service is added")
 			Eventually(func() error {
-				controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod)
+				controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod, testNameSpace)
 				if err != nil {
 					return err
 				}
@@ -347,7 +321,7 @@ var _ = ginkgo.Describe("BGP", func() {
 				ginkgo.By(fmt.Sprintf("checking speaker %s", speaker.Name))
 
 				Eventually(func() error {
-					speakerMetrics, err := metrics.ForPod(controllerPod, speaker)
+					speakerMetrics, err := metrics.ForPod(controllerPod, speaker, testNameSpace)
 					if err != nil {
 						return err
 					}
@@ -388,27 +362,12 @@ var _ = ginkgo.Describe("BGP", func() {
 		})
 
 		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:     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{
-				Peers: peers,
+				Peers: peersForContainers(frrContainers, ipFamily),
 				Pools: addressPools,
 			}
 			for _, c := range frrContainers {
@@ -465,6 +424,124 @@ var _ = ginkgo.Describe("BGP", func() {
 			),
 		)
 	})
+
+	ginkgo.Context("BFD", func() {
+		table.DescribeTable("should work with the given bfd profile", func(bfd bfdProfile, ipFamily string) {
+			configData := configFile{
+				Pools: []addressPool{
+					{
+						Name:     "bfd-test",
+						Protocol: BGP,
+						Addresses: []string{
+							"192.168.10.0/24",
+							"fc00:f853:0ccd:e799::/124",
+						},
+					},
+				},
+				Peers:       withBFD(peersForContainers(frrContainers, ipFamily), bfd.Name),
+				BFDProfiles: []bfdProfile{bfd},
+			}
+			err := updateConfigMap(cs, configData)
+			framework.ExpectNoError(err)
+
+			for _, c := range frrContainers {
+				c.RouterConfig.IPFamily = ipFamily
+				c.NeighborConfig.IPFamily = ipFamily
+				pairExternalFRRWithNodes(cs, c, func(container *frrcontainer.FRR) {
+					container.NeighborConfig.BFDEnabled = true
+				})
+			}
+
+			svc, _ := createServiceWithBackend(cs, f.Namespace.Name, corev1.ServiceExternalTrafficPolicyTypeCluster)
+			defer func() {
+				err := cs.CoreV1().Services(svc.Namespace).Delete(context.TODO(), svc.Name, metav1.DeleteOptions{})
+				framework.ExpectNoError(err)
+			}()
+
+			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+			framework.ExpectNoError(err)
+
+			for _, c := range frrContainers {
+				validateFRRPeeredWithNodes(cs, c)
+			}
+
+			for _, c := range frrContainers {
+				validateService(cs, svc, allNodes.Items, c, ipFamily)
+			}
+
+			Eventually(func() error {
+				for _, c := range frrContainers {
+					bfdPeers, err := frr.BFDPeers(c.Executor)
+					if err != nil {
+						return err
+					}
+					toCompare := BFDProfileWithDefaults(bfd)
+					err = frr.BFDPeersMatchNodes(allNodes.Items, bfdPeers)
+					if err != nil {
+						return err
+					}
+					for _, peerConfig := range bfdPeers {
+						ginkgo.By(fmt.Sprintf("Checking bfd parameters on %s", peerConfig.Peer))
+						err := checkBFDConfigPropagated(toCompare, peerConfig)
+						if err != nil {
+							return err
+						}
+					}
+				}
+				return nil
+			}, 4*time.Minute, 1*time.Second).Should(BeNil())
+
+		},
+			table.Entry("IPV4 - default",
+				bfdProfile{
+					Name: "bar",
+				}, "ipv4"),
+			table.Entry("IPV4 - full params",
+				bfdProfile{
+					Name:             "full1",
+					ReceiveInterval:  uint32Ptr(60),
+					TransmitInterval: uint32Ptr(61),
+					EchoInterval:     uint32Ptr(62),
+					EchoMode:         boolPtr(false),
+					PassiveMode:      boolPtr(false),
+					MinimumTTL:       uint32Ptr(254),
+				}, "ipv4"),
+			table.Entry("IPV4 - echo mode enabled",
+				bfdProfile{
+					Name:             "echo",
+					ReceiveInterval:  uint32Ptr(80),
+					TransmitInterval: uint32Ptr(81),
+					EchoInterval:     uint32Ptr(82),
+					EchoMode:         boolPtr(true),
+					PassiveMode:      boolPtr(false),
+					MinimumTTL:       uint32Ptr(254),
+				}, "ipv4"),
+			table.Entry("IPV6 - default",
+				bfdProfile{
+					Name: "bar",
+				}, "ipv6"),
+			table.Entry("IPV6 - full params",
+				bfdProfile{
+					Name:             "full1",
+					ReceiveInterval:  uint32Ptr(60),
+					TransmitInterval: uint32Ptr(61),
+					EchoInterval:     uint32Ptr(62),
+					EchoMode:         boolPtr(false),
+					PassiveMode:      boolPtr(false),
+					MinimumTTL:       uint32Ptr(254),
+				}, "ipv6"),
+			table.Entry("IPV6 - echo mode enabled",
+				bfdProfile{
+					Name:             "echo",
+					ReceiveInterval:  uint32Ptr(80),
+					TransmitInterval: uint32Ptr(81),
+					EchoInterval:     uint32Ptr(82),
+					EchoMode:         boolPtr(true),
+					PassiveMode:      boolPtr(false),
+					MinimumTTL:       uint32Ptr(254),
+				}, "ipv6"),
+		)
+	})
 })
 
 func createServiceWithBackend(cs clientset.Interface, namespace string, policy corev1.ServiceExternalTrafficPolicyType) (*corev1.Service, *e2eservice.TestJig) {
@@ -528,8 +605,12 @@ func validateCounterValue(expectedMax int, metricName string, labels map[string]
 	return nil
 }
 
-func pairExternalFRRWithNodes(cs clientset.Interface, c *frrcontainer.FRR) {
-	bgpConfig, err := frrconfig.BGPPeersForAllNodes(cs, c.NeighborConfig, c.RouterConfig)
+func pairExternalFRRWithNodes(cs clientset.Interface, c *frrcontainer.FRR, modifiers ...func(c *frrcontainer.FRR)) {
+	config := *c
+	for _, m := range modifiers {
+		m(&config)
+	}
+	bgpConfig, err := frrconfig.BGPPeersForAllNodes(cs, config.NeighborConfig, config.RouterConfig)
 	framework.ExpectNoError(err)
 
 	err = c.UpdateBGPConfigFile(bgpConfig)
@@ -590,7 +671,7 @@ func validateService(cs clientset.Interface, svc *corev1.Service, nodes []corev1
 }
 
 func frrIsPairedOnPods(cs clientset.Interface, n *frrcontainer.FRR, ipFamily string) {
-	pods, err := cs.CoreV1().Pods("metallb-system").List(context.Background(), metav1.ListOptions{
+	pods, err := cs.CoreV1().Pods(testNameSpace).List(context.Background(), metav1.ListOptions{
 		LabelSelector: "component=speaker",
 	})
 	framework.ExpectNoError(err)
@@ -599,7 +680,7 @@ func frrIsPairedOnPods(cs clientset.Interface, n *frrcontainer.FRR, ipFamily str
 		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))
+		toParse, err := framework.RunKubectl(testNameSpace, "exec", pods.Items[0].Name, "-c", "frr", "--", "vtysh", "-c", fmt.Sprintf("show bgp neighbor %s json", address))
 		if err != nil {
 			return err
 		}
@@ -644,3 +725,58 @@ func stopFRRContainers(containers []*frrcontainer.FRR) error {
 
 	return g.Wait()
 }
+
+func peersForContainers(containers []*frrcontainer.FRR, ipFamily string) []peer {
+	var peers []peer
+	for i, c := range frrContainers {
+		address := c.Ipv4
+		if ipFamily == "ipv6" {
+			address = c.Ipv6
+		}
+		holdTime := ""
+		if i > 0 {
+			holdTime = fmt.Sprintf("%ds", i*180)
+		}
+		peers = append(peers, peer{
+			Addr:     address,
+			ASN:      c.RouterConfig.ASN,
+			MyASN:    c.NeighborConfig.ASN,
+			Port:     c.RouterConfig.BGPPort,
+			RouterID: fmt.Sprintf(baseRouterID, i),
+			Password: c.RouterConfig.Password,
+			HoldTime: holdTime,
+		})
+	}
+	return peers
+}
+
+func withBFD(peers []peer, bfdProfile string) []peer {
+	for i := range peers {
+		peers[i].BFDProfile = bfdProfile
+	}
+	return peers
+}
+
+func checkBFDConfigPropagated(nodeConfig bfdProfile, peerConfig bgpfrr.BFDPeer) error {
+	if peerConfig.Status != "up" {
+		return fmt.Errorf("Peer status not up")
+	}
+	if peerConfig.RemoteReceiveInterval != int(*nodeConfig.ReceiveInterval) {
+		return fmt.Errorf("RemoteReceiveInterval: expecting %d, got %d", *nodeConfig.ReceiveInterval, peerConfig.RemoteReceiveInterval)
+	}
+	if peerConfig.RemoteTransmitInterval != int(*nodeConfig.TransmitInterval) {
+		return fmt.Errorf("RemoteTransmitInterval: expecting %d, got %d", *nodeConfig.TransmitInterval, peerConfig.RemoteTransmitInterval)
+	}
+	if peerConfig.RemoteEchoInterval != int(*nodeConfig.EchoInterval) {
+		return fmt.Errorf("EchoInterval: expecting %d, got %d", *nodeConfig.EchoInterval, peerConfig.RemoteEchoInterval)
+	}
+	return nil
+}
+
+func uint32Ptr(n uint32) *uint32 {
+	return &n
+}
+
+func boolPtr(b bool) *bool {
+	return &b
+}
diff --git a/e2etest/config/frr/daemons b/e2etest/config/frr/daemons
index 587add00..0834dcea 100644
--- a/e2etest/config/frr/daemons
+++ b/e2etest/config/frr/daemons
@@ -27,7 +27,7 @@ eigrpd=no
 babeld=no
 sharpd=no
 pbrd=no
-bfdd=no
+bfdd=yes
 fabricd=no
 vrrpd=no
 
diff --git a/e2etest/e2etest_suite_test.go b/e2etest/e2etest_suite_test.go
index 100225b1..634cbb66 100644
--- a/e2etest/e2etest_suite_test.go
+++ b/e2etest/e2etest_suite_test.go
@@ -42,6 +42,7 @@ import (
 const (
 	defaultTestNameSpace     = "metallb-system"
 	defaultContainersNetwork = "kind"
+	defaultConfigMapName     = "config"
 )
 
 var (
@@ -51,6 +52,7 @@ var (
 	ipv4ServiceRange  string
 	ipv6ServiceRange  string
 	testNameSpace     = defaultTestNameSpace
+	configMapName     = defaultConfigMapName
 	containersNetwork = defaultContainersNetwork
 	hostIPv4          string
 	hostIPv6          string
@@ -103,6 +105,10 @@ var _ = ginkgo.BeforeSuite(func() {
 		testNameSpace = ns
 	}
 
+	if name := os.Getenv("CONFIGMAP_NAME"); len(name) != 0 {
+		configMapName = name
+	}
+
 	if _, res := os.LookupEnv("RUN_FRR_CONTAINER_ON_HOST_NETWORK"); res == true {
 		containersNetwork = "host"
 	}
@@ -125,13 +131,13 @@ var _ = ginkgo.BeforeSuite(func() {
 	cs, err := framework.LoadClientset()
 	framework.ExpectNoError(err)
 
-	_, err = cs.CoreV1().ConfigMaps(testNameSpace).Get(context.TODO(), "config", metav1.GetOptions{})
+	_, err = cs.CoreV1().ConfigMaps(testNameSpace).Get(context.TODO(), configMapName, metav1.GetOptions{})
 	framework.ExpectEqual(errors.IsNotFound(err), true)
 
 	// Init empty MetalLB ConfigMap.
 	_, err = cs.CoreV1().ConfigMaps(testNameSpace).Create(context.TODO(), &v1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      "config",
+			Name:      configMapName,
 			Namespace: testNameSpace,
 		},
 	}, metav1.CreateOptions{})
@@ -142,6 +148,6 @@ var _ = ginkgo.AfterSuite(func() {
 	cs, err := framework.LoadClientset()
 	framework.ExpectNoError(err)
 
-	err = cs.CoreV1().ConfigMaps(testNameSpace).Delete(context.TODO(), "config", metav1.DeleteOptions{})
+	err = cs.CoreV1().ConfigMaps(testNameSpace).Delete(context.TODO(), configMapName, metav1.DeleteOptions{})
 	framework.ExpectNoError(err)
 })
diff --git a/e2etest/layer2_test.go b/e2etest/layer2_test.go
index a0aa95eb..7544dba6 100644
--- a/e2etest/layer2_test.go
+++ b/e2etest/layer2_test.go
@@ -292,14 +292,14 @@ var _ = ginkgo.Describe("L2", func() {
 		var speakerPods map[string]*corev1.Pod
 
 		ginkgo.BeforeEach(func() {
-			pods, err := cs.CoreV1().Pods("metallb-system").List(context.Background(), metav1.ListOptions{
+			pods, err := cs.CoreV1().Pods(testNameSpace).List(context.Background(), metav1.ListOptions{
 				LabelSelector: "component=controller",
 			})
 			framework.ExpectNoError(err)
 			framework.ExpectEqual(len(pods.Items), 1, "More than one controller found")
 			controllerPod = &pods.Items[0]
 
-			speakers, err := cs.CoreV1().Pods("metallb-system").List(context.Background(), metav1.ListOptions{
+			speakers, err := cs.CoreV1().Pods(testNameSpace).List(context.Background(), metav1.ListOptions{
 				LabelSelector: "component=speaker",
 			})
 			framework.ExpectNoError(err)
@@ -333,7 +333,7 @@ var _ = ginkgo.Describe("L2", func() {
 
 			ginkgo.By("checking the metrics when no service is added")
 			gomega.Eventually(func() error {
-				controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod)
+				controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod, testNameSpace)
 				if err != nil {
 					return err
 				}
@@ -357,7 +357,7 @@ var _ = ginkgo.Describe("L2", func() {
 
 			ginkgo.By("checking the metrics when a service is added")
 			gomega.Eventually(func() error {
-				controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod)
+				controllerMetrics, err := metrics.ForPod(controllerPod, controllerPod, testNameSpace)
 				if err != nil {
 					return err
 				}
@@ -388,7 +388,7 @@ var _ = ginkgo.Describe("L2", func() {
 			delete(speakerPods, advSpeaker.Spec.NodeName)
 
 			gomega.Eventually(func() error {
-				speakerMetrics, err := metrics.ForPod(controllerPod, advSpeaker)
+				speakerMetrics, err := metrics.ForPod(controllerPod, advSpeaker, testNameSpace)
 				if err != nil {
 					return err
 				}
@@ -413,7 +413,7 @@ var _ = ginkgo.Describe("L2", func() {
 
 			// Negative - validate that the other speakers don't publish layer2 metrics
 			for _, p := range speakerPods {
-				speakerMetrics, err := metrics.ForPod(controllerPod, p)
+				speakerMetrics, err := metrics.ForPod(controllerPod, p, testNameSpace)
 				framework.ExpectNoError(err)
 
 				err = validateGaugeValue(1, "metallb_speaker_announced", map[string]string{"node": p.Spec.NodeName, "protocol": "layer2", "service": fmt.Sprintf("%s/%s", f.Namespace.Name, svc.Name)}, speakerMetrics)
diff --git a/e2etest/parse_config.go b/e2etest/parse_config.go
index cc0914e3..77985445 100644
--- a/e2etest/parse_config.go
+++ b/e2etest/parse_config.go
@@ -27,6 +27,7 @@ type configFile struct {
 	Peers          []peer            `yaml:"peers,omitempty"`
 	BGPCommunities map[string]string `yaml:"bgp-communities,omitempty"`
 	Pools          []addressPool     `yaml:"address-pools,omitempty"`
+	BFDProfiles    []bfdProfile      `yaml:"bfd-profiles"`
 }
 
 type peer struct {
@@ -39,6 +40,7 @@ type peer struct {
 	RouterID      string         `yaml:"router-id,omitempty"`
 	NodeSelectors []nodeSelector `yaml:"node-selectors,omitempty"`
 	Password      string         `yaml:"password,omitempty"`
+	BFDProfile    string         `yaml:"bfd-profile,omitempty"`
 }
 
 type nodeSelector struct {
@@ -67,6 +69,17 @@ type bgpAdvertisement struct {
 	Communities       []string `yaml:"communities,omitempty"`
 }
 
+type bfdProfile struct {
+	Name             string  `yaml:"name"`
+	ReceiveInterval  *uint32 `yaml:"receive-interval,omitempty"`
+	TransmitInterval *uint32 `yaml:"transmit-interval,omitempty"`
+	DetectMultiplier *uint32 `yaml:"detect-multiplier,omitempty"`
+	EchoInterval     *uint32 `yaml:"echo-interval,omitempty"`
+	EchoMode         *bool   `yaml:"echo-mode,omitempty"`
+	PassiveMode      *bool   `yaml:"passive-mode,omitempty"`
+	MinimumTTL       *uint32 `yaml:"minimum-ttl,omitempty"`
+}
+
 func updateConfigMap(cs clientset.Interface, data configFile) error {
 	resData, err := yaml.Marshal(data)
 	if err != nil {
@@ -75,7 +88,7 @@ func updateConfigMap(cs clientset.Interface, data configFile) error {
 
 	_, err = cs.CoreV1().ConfigMaps(testNameSpace).Update(context.TODO(), &v1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      "config",
+			Name:      configMapName,
 			Namespace: testNameSpace,
 		},
 		Data: map[string]string{"config": string(resData)},
@@ -86,3 +99,23 @@ func updateConfigMap(cs clientset.Interface, data configFile) error {
 
 	return nil
 }
+
+func BFDProfileWithDefaults(profile bfdProfile) bfdProfile {
+	res := bfdProfile{}
+	res.Name = profile.Name
+	res.ReceiveInterval = valueWithDefault(profile.ReceiveInterval, 300)
+	res.TransmitInterval = valueWithDefault(profile.TransmitInterval, 300)
+	res.DetectMultiplier = valueWithDefault(profile.DetectMultiplier, 3)
+	res.EchoInterval = valueWithDefault(profile.EchoInterval, 50)
+	res.MinimumTTL = valueWithDefault(profile.MinimumTTL, 254)
+	res.EchoMode = profile.EchoMode
+	res.PassiveMode = profile.PassiveMode
+	return res
+}
+
+func valueWithDefault(v *uint32, def uint32) *uint32 {
+	if v != nil {
+		return v
+	}
+	return &def
+}
diff --git a/e2etest/pkg/frr/bfd.go b/e2etest/pkg/frr/bfd.go
new file mode 100644
index 00000000..dfa50628
--- /dev/null
+++ b/e2etest/pkg/frr/bfd.go
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package frr
+
+import (
+	"github.com/pkg/errors"
+	"go.universe.tf/metallb/e2etest/pkg/executor"
+	bgpfrr "go.universe.tf/metallb/internal/bgp/frr"
+)
+
+// BFDPeers returns informations for the all the bfd peers in the given
+// executor.
+func BFDPeers(exec executor.Executor) (map[string]bgpfrr.BFDPeer, error) {
+	res, err := exec.Exec("vtysh", "-c", "show bfd peers json")
+	if err != nil {
+		return nil, errors.Wrapf(err, "Failed to query neighbours")
+	}
+	peers, err := bgpfrr.ParseBFDPeers(res)
+	if err != nil {
+		return nil, errors.Wrapf(err, "Failed to parse neighbours %s", res)
+	}
+	return peers, nil
+}
diff --git a/e2etest/pkg/frr/config/config.go b/e2etest/pkg/frr/config/config.go
index 63b9f50a..6d4b8443 100644
--- a/e2etest/pkg/frr/config/config.go
+++ b/e2etest/pkg/frr/config/config.go
@@ -30,6 +30,9 @@ router bgp {{.ASN}}
   {{ if .Password -}}
   neighbor {{.Addr}} password {{.Password}}
   {{- end }}
+{{- if .BFDEnabled }} 
+  neighbor {{.Addr}} bfd
+{{- end -}}
 {{- end }}
   address-family {{.IPFamily}} unicast
 {{range .Neighbors }}
@@ -45,21 +48,19 @@ log stdout debugging
 `
 
 type RouterConfig struct {
-	ASN           uint32
-	Neighbors     []*NeighborConfig
-	BGPPort       uint16
-	RouterID      string
-	Password      string
-	HoldTime      string
-	KeepaliveTime string
-	IPFamily      string
+	ASN       uint32
+	Neighbors []*NeighborConfig
+	BGPPort   uint16
+	Password  string
+	IPFamily  string
 }
 
 type NeighborConfig struct {
-	ASN      uint32
-	Addr     string
-	Password string
-	IPFamily string
+	ASN        uint32
+	Addr       string
+	Password   string
+	IPFamily   string
+	BFDEnabled bool
 }
 
 // Set the IP of each node in the cluster in the BGP router configuration.
@@ -84,7 +85,6 @@ func BGPPeersForAllNodes(cs clientset.Interface, nc NeighborConfig, rc RouterCon
 			}
 		}
 	}
-
 	t, err := template.New("bgp Config Template").Parse(bgpConfigTemplate)
 	if err != nil {
 		return "", errors.Wrapf(err, "Failed to create bgp template")
diff --git a/e2etest/pkg/frr/validation.go b/e2etest/pkg/frr/validation.go
index 74d63a15..517549b0 100644
--- a/e2etest/pkg/frr/validation.go
+++ b/e2etest/pkg/frr/validation.go
@@ -63,3 +63,28 @@ func RoutesMatchNodes(nodes []v1.Node, route bgpfrr.Route) error {
 	}
 	return nil
 }
+
+func BFDPeersMatchNodes(nodes []v1.Node, peers map[string]bgpfrr.BFDPeer) error {
+	nodesIPs := map[string]struct{}{}
+
+	for _, n := range nodes {
+		for _, a := range n.Status.Addresses {
+			if a.Type == v1.NodeInternalIP {
+				nodesIPs[a.Address] = struct{}{}
+				if _, ok := peers[a.Address]; !ok {
+					return fmt.Errorf("address %s not found in peers", a.Address)
+				}
+			}
+		}
+	}
+	for k := range peers {
+		if _, ok := nodesIPs[k]; !ok { // skipping neighbors that are not nodes
+			return fmt.Errorf("%s not found in nodes ips", k)
+		}
+		delete(nodesIPs, k)
+	}
+	if len(nodesIPs) != 0 { // some leftover, meaning more nodes than routes
+		return fmt.Errorf("IP %v found in nodes but not in bfd peers", nodesIPs)
+	}
+	return nil
+}
diff --git a/e2etest/pkg/metrics/metrics.go b/e2etest/pkg/metrics/metrics.go
index 636939aa..97136ebf 100644
--- a/e2etest/pkg/metrics/metrics.go
+++ b/e2etest/pkg/metrics/metrics.go
@@ -19,7 +19,7 @@ import (
 
 // MetricsForPod returns the parsed metrics for the given pod, scraping them
 // from the executor pod.
-func ForPod(executor, target *corev1.Pod) ([]map[string]*dto.MetricFamily, error) {
+func ForPod(executor, target *corev1.Pod, namespace string) ([]map[string]*dto.MetricFamily, error) {
 	ports := make([]int, 0)
 	allMetrics := make([]map[string]*dto.MetricFamily, 0)
 	for _, c := range target.Spec.Containers {
@@ -32,7 +32,7 @@ func ForPod(executor, target *corev1.Pod) ([]map[string]*dto.MetricFamily, error
 
 	for _, p := range ports {
 		metricsUrl := path.Join(net.JoinHostPort(target.Status.PodIP, strconv.Itoa(p)), "metrics")
-		metrics, err := framework.RunKubectl("metallb-system", "exec", executor.Name, "--", "wget", "-qO-", metricsUrl)
+		metrics, err := framework.RunKubectl(namespace, "exec", executor.Name, "--", "wget", "-qO-", metricsUrl)
 		if err != nil {
 			return nil, errors.Wrapf(err, "failed to scrape metrics for %s", target.Name)
 		}
diff --git a/internal/bgp/frr/frr_bfd_test.go b/internal/bgp/frr/frr_bfd_test.go
new file mode 100644
index 00000000..2fd1a673
--- /dev/null
+++ b/internal/bgp/frr/frr_bfd_test.go
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package frr
+
+import (
+	"net"
+	"testing"
+	"time"
+
+	"github.com/go-kit/kit/log"
+	"go.universe.tf/metallb/internal/config"
+)
+
+func TestBFDProfileNoSessions(t *testing.T) {
+	testSetup(t)
+
+	pp := map[string]*config.BFDProfile{
+		"foo": {
+			Name:             "foo",
+			ReceiveInterval:  uint32Ptr(60),
+			TransmitInterval: uint32Ptr(70),
+			DetectMultiplier: uint32Ptr(5),
+			EchoInterval:     uint32Ptr(90),
+			EchoMode:         false,
+			PassiveMode:      false,
+			MinimumTTL:       uint32Ptr(60),
+		},
+		"bar": {
+			Name:             "bar",
+			ReceiveInterval:  uint32Ptr(60),
+			TransmitInterval: uint32Ptr(70),
+			DetectMultiplier: uint32Ptr(5),
+			EchoInterval:     uint32Ptr(90),
+			EchoMode:         false,
+			PassiveMode:      false,
+			MinimumTTL:       uint32Ptr(60),
+		},
+	}
+	sessionManager := NewSessionManager()
+	err := sessionManager.SyncBFDProfiles(pp)
+	if err != nil {
+		t.Fatalf("Failed to sync bfd profiles: %s", err)
+	}
+
+	testCheckConfigFile(t)
+	err = sessionManager.SyncBFDProfiles(map[string]*config.BFDProfile{})
+	if err != nil {
+		t.Fatalf("Failed to sync bfd profiles: %s", err)
+	}
+}
+
+func TestBFDProfileCornerCases(t *testing.T) {
+	testSetup(t)
+
+	pp := map[string]*config.BFDProfile{
+		"foo": {
+			Name:             "foo",
+			ReceiveInterval:  uint32Ptr(60),
+			TransmitInterval: uint32Ptr(70),
+			DetectMultiplier: uint32Ptr(5),
+			EchoInterval:     uint32Ptr(90),
+			EchoMode:         true,
+			PassiveMode:      true,
+			MinimumTTL:       uint32Ptr(60),
+		},
+	}
+
+	sessionManager := NewSessionManager()
+	err := sessionManager.SyncBFDProfiles(pp)
+	if err != nil {
+		t.Fatalf("Failed to sync bfd profiles: %s", err)
+	}
+
+	testCheckConfigFile(t)
+	err = sessionManager.SyncBFDProfiles(map[string]*config.BFDProfile{})
+	if err != nil {
+		t.Fatalf("Failed to sync bfd profiles: %s", err)
+	}
+}
+
+func TestBFDWithSession(t *testing.T) {
+	testSetup(t)
+
+	pp := map[string]*config.BFDProfile{
+		"foo": {
+			Name:             "foo",
+			ReceiveInterval:  uint32Ptr(60),
+			TransmitInterval: uint32Ptr(70),
+			DetectMultiplier: uint32Ptr(5),
+			EchoInterval:     uint32Ptr(90),
+			EchoMode:         false,
+			PassiveMode:      false,
+			MinimumTTL:       uint32Ptr(60),
+		},
+		"bar": {
+			Name:             "bar",
+			ReceiveInterval:  uint32Ptr(60),
+			TransmitInterval: uint32Ptr(70),
+			DetectMultiplier: uint32Ptr(5),
+			EchoInterval:     uint32Ptr(90),
+			EchoMode:         false,
+			PassiveMode:      false,
+			MinimumTTL:       uint32Ptr(60),
+		},
+	}
+
+	sessionManager := NewSessionManager()
+	err := sessionManager.SyncBFDProfiles(pp)
+	if err != nil {
+		t.Fatalf("Failed to sync bfd profiles %s", err)
+	}
+
+	l := log.NewNopLogger()
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, 2*time.Second, "password", "hostname", "foo")
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+	err = sessionManager.SyncBFDProfiles(map[string]*config.BFDProfile{})
+	if err != nil {
+		t.Fatalf("Failed to sync bfd profiles %s", err)
+	}
+}
+
+func TestBFDProfileAllDefault(t *testing.T) {
+	testSetup(t)
+
+	pp := map[string]*config.BFDProfile{
+		"foo": {
+			Name:             "foo",
+			ReceiveInterval:  uint32Ptr(60),
+			TransmitInterval: uint32Ptr(70),
+			DetectMultiplier: uint32Ptr(5),
+			EchoInterval:     uint32Ptr(90),
+			EchoMode:         false,
+			PassiveMode:      false,
+			MinimumTTL:       uint32Ptr(60),
+		},
+		"bar": {
+			Name: "bar",
+		},
+	}
+
+	sessionManager := NewSessionManager()
+	err := sessionManager.SyncBFDProfiles(pp)
+	if err != nil {
+		t.Fatalf("Failed to sync bfd profiles %s", err)
+	}
+
+	testCheckConfigFile(t)
+	err = sessionManager.SyncBFDProfiles(map[string]*config.BFDProfile{})
+	if err != nil {
+		t.Fatalf("Failed to sync bfd profiles %s", err)
+	}
+}
+
+func uint32Ptr(n uint32) *uint32 {
+	return &n
+}
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index e245e36a..56eac1a8 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -82,7 +82,7 @@ func TestSingleSession(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -96,7 +96,7 @@ func TestSingleSessionClose(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -109,12 +109,12 @@ func TestTwoSessions(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
 	defer session1.Close()
-	session2, err := sessionManager.NewSession(l, "10.4.4.254:179", net.ParseIP("10.3.3.254"), 300, net.ParseIP("10.3.3.254"), 400, time.Second, time.Second, "password", "hostname")
+	session2, err := sessionManager.NewSession(l, "10.4.4.254:179", net.ParseIP("10.3.3.254"), 300, net.ParseIP("10.3.3.254"), 400, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -128,12 +128,12 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
 	defer session1.Close()
-	session2, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session2, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -147,12 +147,12 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session1, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
 	defer session1.Close()
-	session2, err := sessionManager.NewSession(l, "10.4.4.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 400, time.Second, time.Second, "password", "hostname")
+	session2, err := sessionManager.NewSession(l, "10.4.4.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 400, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -166,7 +166,7 @@ func TestSingleAdvertisement(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -195,7 +195,7 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, nil, 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, nil, 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -224,7 +224,7 @@ func TestSingleAdvertisementInvalidPrefix(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -250,7 +250,7 @@ func TestSingleAdvertisementInvalidNoPort(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err == nil {
 		session.Close()
 		t.Fatalf("Should not be able to create session")
@@ -265,7 +265,7 @@ func TestSingleAdvertisementInvalidNextHop(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -293,7 +293,7 @@ func TestSingleAdvertisementStop(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -327,7 +327,7 @@ func TestSingleAdvertisementChange(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
@@ -371,7 +371,7 @@ func TestTwoAdvertisements(t *testing.T) {
 
 	l := log.NewNopLogger()
 	sessionManager := NewSessionManager()
-	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname")
+	session, err := sessionManager.NewSession(l, "10.2.2.254:179", net.ParseIP("10.1.1.254"), 100, net.ParseIP("10.1.1.254"), 200, time.Second, time.Second, "password", "hostname", "")
 	if err != nil {
 		t.Fatalf("Could not create session: %s", err)
 	}
diff --git a/internal/bgp/frr/parse_test.go b/internal/bgp/frr/parse_test.go
index bd2cb85f..df94e5b3 100644
--- a/internal/bgp/frr/parse_test.go
+++ b/internal/bgp/frr/parse_test.go
@@ -596,3 +596,92 @@ func TestRoutes(t *testing.T) {
 		t.Fatal("neighbour ip not matching")
 	}
 }
+
+const bfdPeers = `[
+   {
+      "multihop":false,
+      "peer":"172.18.0.4",
+      "local":"172.18.0.5",
+      "vrf":"default",
+      "interface":"eth0",
+      "id":632314921,
+      "remote-id":2999817552,
+      "passive-mode":false,
+      "status":"up",
+      "uptime":52,
+      "diagnostic":"ok",
+      "remote-diagnostic":"ok",
+      "receive-interval":300,
+      "transmit-interval":300,
+      "echo-receive-interval":50,
+      "echo-transmit-interval":0,
+      "detect-multiplier":3,
+      "remote-receive-interval":300,
+      "remote-transmit-interval":300,
+      "remote-echo-receive-interval":50,
+      "remote-detect-multiplier":3
+   },
+   {
+      "multihop":false,
+      "peer":"172.18.0.2",
+      "local":"172.18.0.5",
+      "vrf":"default",
+      "interface":"eth0",
+      "id":3048501273,
+      "remote-id":2977557242,
+      "passive-mode":false,
+      "status":"up",
+      "uptime":52,
+      "diagnostic":"ok",
+      "remote-diagnostic":"ok",
+      "receive-interval":300,
+      "transmit-interval":300,
+      "echo-receive-interval":50,
+      "echo-transmit-interval":0,
+      "detect-multiplier":3,
+      "remote-receive-interval":300,
+      "remote-transmit-interval":300,
+      "remote-echo-receive-interval":50,
+      "remote-detect-multiplier":3
+   },
+   {
+      "multihop":false,
+      "peer":"172.18.0.3",
+      "local":"172.18.0.5",
+      "vrf":"default",
+      "interface":"eth0",
+      "id":2114932580,
+      "remote-id":493597049,
+      "passive-mode":false,
+      "status":"up",
+      "uptime":52,
+      "diagnostic":"ok",
+      "remote-diagnostic":"ok",
+      "receive-interval":300,
+      "transmit-interval":300,
+      "echo-receive-interval":50,
+      "echo-transmit-interval":0,
+      "detect-multiplier":3,
+      "remote-receive-interval":300,
+      "remote-transmit-interval":300,
+      "remote-echo-interval":50,
+      "remote-detect-multiplier":3
+   }
+]`
+
+func TestBFDPeers(t *testing.T) {
+	peers, err := ParseBFDPeers(bfdPeers)
+	if err != nil {
+		t.Fatalf("Failed to parse %s", err)
+	}
+	p, ok := peers["172.18.0.3"]
+	if !ok {
+		t.Fatal("Peer not found")
+	}
+	if p.Status != "up" {
+		t.Fatal("wrong status")
+	}
+	if p.RemoteEchoInterval != 50 {
+		t.Fatal("wrong echo interval")
+	}
+}
diff --git a/internal/bgp/frr/testdata/TestBFDProfileAllDefault.golden b/internal/bgp/frr/testdata/TestBFDProfileAllDefault.golden
new file mode 100644
index 00000000..8ceb83e8
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestBFDProfileAllDefault.golden
@@ -0,0 +1,16 @@
+
+log file /etc/frr/frr.log informational
+log timestamp precision 3
+hostname dummyhostname
+
+
+bfd
+  profile bar
+    
+  profile foo
+    receive-interval 60
+    transmit-interval 70
+    detect-multiplier 5
+    echo-interval 90
+    minimum-ttl 60
+    
diff --git a/internal/bgp/frr/testdata/TestBFDProfileCornerCases.golden b/internal/bgp/frr/testdata/TestBFDProfileCornerCases.golden
new file mode 100644
index 00000000..86d6b8ae
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestBFDProfileCornerCases.golden
@@ -0,0 +1,16 @@
+
+log file /etc/frr/frr.log informational
+log timestamp precision 3
+hostname dummyhostname
+
+
+bfd
+  profile foo
+    receive-interval 60
+    transmit-interval 70
+    detect-multiplier 5
+    echo-mode
+    echo-interval 90
+    passive-mode
+    minimum-ttl 60
+    
diff --git a/internal/bgp/frr/testdata/TestBFDProfileNoSessions.golden b/internal/bgp/frr/testdata/TestBFDProfileNoSessions.golden
new file mode 100644
index 00000000..d12dd5cb
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestBFDProfileNoSessions.golden
@@ -0,0 +1,21 @@
+
+log file /etc/frr/frr.log informational
+log timestamp precision 3
+hostname dummyhostname
+
+
+bfd
+  profile bar
+    receive-interval 60
+    transmit-interval 70
+    detect-multiplier 5
+    echo-interval 90
+    minimum-ttl 60
+    
+  profile foo
+    receive-interval 60
+    transmit-interval 70
+    detect-multiplier 5
+    echo-interval 90
+    minimum-ttl 60
+    
diff --git a/internal/bgp/frr/testdata/TestBFDWithSession.golden b/internal/bgp/frr/testdata/TestBFDWithSession.golden
new file mode 100644
index 00000000..6f077bd3
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestBFDWithSession.golden
@@ -0,0 +1,41 @@
+
+log file /etc/frr/frr.log informational
+log timestamp precision 3
+hostname dummyhostname
+
+router bgp 100
+  no bgp ebgp-requires-policy
+  no bgp network import-check
+  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 port 179
+  neighbor 10.2.2.254 timers 2 1
+  neighbor 10.2.2.254 password password
+  neighbor 10.2.2.254 update-source 10.1.1.254 
+  neighbor 10.2.2.254 bfd profile foo
+
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+  exit-address-family
+
+bfd
+  profile bar
+    receive-interval 60
+    transmit-interval 70
+    detect-multiplier 5
+    echo-interval 90
+    minimum-ttl 60
+    
+  profile foo
+    receive-interval 60
+    transmit-interval 70
+    detect-multiplier 5
+    echo-interval 90
+    minimum-ttl 60
+    
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisement.golden b/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
index 6e8b1907..da8d9cd3 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisement.golden
@@ -20,5 +20,3 @@ router bgp 100
     neighbor 10.2.2.254 activate
     network 172.16.1.10/24
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
index 9b9156c4..282f63a5 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementChange.golden
@@ -20,5 +20,3 @@ router bgp 100
     neighbor 10.2.2.254 activate
     network 172.16.1.11/24
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
index 120b24f6..e49fb270 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementInvalidPrefix.golden
@@ -22,5 +22,3 @@ router bgp 100
   address-family ipv6 unicast
     neighbor 10.2.2.254 activate
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
index d259cd3a..2807f2ac 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementNoRouterID.golden
@@ -19,5 +19,3 @@ router bgp 100
     neighbor 10.2.2.254 activate
     network 172.16.1.10/24
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden b/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
index 120b24f6..e49fb270 100644
--- a/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
+++ b/internal/bgp/frr/testdata/TestSingleAdvertisementStop.golden
@@ -22,5 +22,3 @@ router bgp 100
   address-family ipv6 unicast
     neighbor 10.2.2.254 activate
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestSingleSession.golden b/internal/bgp/frr/testdata/TestSingleSession.golden
index 120b24f6..e49fb270 100644
--- a/internal/bgp/frr/testdata/TestSingleSession.golden
+++ b/internal/bgp/frr/testdata/TestSingleSession.golden
@@ -22,5 +22,3 @@ router bgp 100
   address-family ipv6 unicast
     neighbor 10.2.2.254 activate
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestSingleSessionClose.golden b/internal/bgp/frr/testdata/TestSingleSessionClose.golden
index 111e444f..f72fb82d 100644
--- a/internal/bgp/frr/testdata/TestSingleSessionClose.golden
+++ b/internal/bgp/frr/testdata/TestSingleSessionClose.golden
@@ -3,4 +3,3 @@ 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 328eb680..a5f18ed5 100644
--- a/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
+++ b/internal/bgp/frr/testdata/TestTwoAdvertisements.golden
@@ -24,5 +24,3 @@ router bgp 100
     neighbor 10.2.2.254 activate
     network 172.16.1.11/24
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestTwoSessions.golden b/internal/bgp/frr/testdata/TestTwoSessions.golden
index 3a4e341f..17759e18 100644
--- a/internal/bgp/frr/testdata/TestTwoSessions.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessions.golden
@@ -22,7 +22,6 @@ router bgp 100
   address-family ipv6 unicast
     neighbor 10.2.2.254 activate
   exit-address-family
-
 router bgp 300
   no bgp ebgp-requires-policy
   no bgp network import-check
@@ -42,5 +41,3 @@ router bgp 300
   address-family ipv6 unicast
     neighbor 10.4.4.254 activate
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden b/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
index 120b24f6..e49fb270 100644
--- a/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessionsDuplicate.golden
@@ -22,5 +22,3 @@ router bgp 100
   address-family ipv6 unicast
     neighbor 10.2.2.254 activate
   exit-address-family
-
-
diff --git a/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden b/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
index b0821edc..cb33b5dd 100644
--- a/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
+++ b/internal/bgp/frr/testdata/TestTwoSessionsDuplicateRouter.golden
@@ -34,5 +34,3 @@ router bgp 100
   address-family ipv6 unicast
     neighbor 10.4.4.254 activate
   exit-address-family
-
-
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 91a94551..8a54a014 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -37,7 +37,8 @@ func TestParse(t *testing.T) {
 			desc: "empty config",
 			raw:  "",
 			want: &Config{
-				Pools: map[string]*Pool{},
+				Pools:       map[string]*Pool{},
+				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
 
@@ -179,6 +180,7 @@ address-pools:
 						AutoAssign: true,
 					},
 				},
+				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
 
@@ -202,7 +204,8 @@ peers:
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*Pool{},
+				Pools:       map[string]*Pool{},
+				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
 
@@ -286,7 +289,8 @@ peers:
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*Pool{},
+				Pools:       map[string]*Pool{},
+				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
 
@@ -463,6 +467,7 @@ address-pools:
 						},
 					},
 				},
+				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
 
@@ -489,6 +494,7 @@ address-pools:
 						},
 					},
 				},
+				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
 
@@ -640,6 +646,146 @@ address-pools:
   - 10.0.0.0/16
   bgp-advertisements:
   - communities: ["flarb"]
+`,
+		},
+		{
+			desc: "Session with default BFD Profile",
+			raw: `
+address-pools:
+- name: pool1
+  addresses: ["1.2.3.0/24"]
+  protocol: bgp
+bfd-profiles:
+- name: default
+peers:
+- my-asn: 42
+  peer-asn: 42
+  peer-address: 1.2.3.4
+  bfd-profile: default
+`,
+			want: &Config{
+				Peers: []*Peer{
+					{
+						MyASN:         42,
+						ASN:           42,
+						Addr:          net.ParseIP("1.2.3.4"),
+						Port:          179,
+						HoldTime:      90 * time.Second,
+						KeepaliveTime: 30 * time.Second,
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						BFDProfile:    "default",
+					},
+				},
+				Pools: map[string]*Pool{
+					"pool1": {
+						Protocol:   BGP,
+						AutoAssign: true,
+						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
+						BGPAdvertisements: []*BGPAdvertisement{
+							{
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
+							},
+						},
+					},
+				},
+				BFDProfiles: map[string]*BFDProfile{
+					"default": {
+						Name: "default",
+					},
+				},
+			},
+		},
+		{
+			desc: "Peer with non existing BFD Profile",
+			raw: `
+address-pools:
+- name: pool1
+  addresses: ["1.2.3.0/24"]
+  protocol: bgp
+bfd-profiles:
+- name: default
+peers:
+- my-asn: 42
+  peer-asn: 42
+  peer-address: 1.2.3.4
+  bfd-profile: zzz
+`,
+		},
+		{
+			desc: "Multiple BFD Profiles with the same name",
+			raw: `
+address-pools:
+- name: pool1
+  addresses: ["1.2.3.0/24"]
+  protocol: bgp
+bfd-profiles:
+- name: default
+- name: foo
+- name: foo
+`,
+		},
+		{
+			desc: "Session with nondefault BFD Profile",
+			raw: `
+address-pools:
+- name: pool1
+  addresses: ["1.2.3.0/24"]
+  protocol: bgp
+bfd-profiles:
+- name: nondefault
+  receive-interval: 50
+  transmit-interval: 51
+  detect-multiplier: 52
+  echo-interval: 54
+  echo-mode: true
+  passive-mode: true
+  minimum-ttl: 55
+`,
+			want: &Config{
+				Pools: map[string]*Pool{
+					"pool1": {
+						Protocol:   BGP,
+						AutoAssign: true,
+						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
+						BGPAdvertisements: []*BGPAdvertisement{
+							{
+								AggregationLength:   32,
+								AggregationLengthV6: 128,
+								Communities:         map[uint32]bool{},
+							},
+						},
+					},
+				},
+				BFDProfiles: map[string]*BFDProfile{
+					"nondefault": {
+						Name:             "nondefault",
+						ReceiveInterval:  uint32Ptr(50),
+						DetectMultiplier: uint32Ptr(52),
+						TransmitInterval: uint32Ptr(51),
+						EchoInterval:     uint32Ptr(54),
+						MinimumTTL:       uint32Ptr(55),
+						EchoMode:         true,
+						PassiveMode:      true,
+					},
+				},
+			},
+		},
+		{
+			desc: "BFD Profile with too low receive interval",
+			raw: `
+bfd-profiles:
+- name: default
+  receive-interval: 2
+`,
+		},
+		{
+			desc: "BFD Profile with too high range receive interval",
+			raw: `
+bfd-profiles:
+- name: default
+  receive-interval: 90000
 `,
 		},
 	}
@@ -679,3 +825,7 @@ address-pools:
 		})
 	}
 }
+
+func uint32Ptr(n uint32) *uint32 {
+	return &n
+}
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index dea685ec..69b07cd8 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -110,7 +110,7 @@ type fakeBGPSessionManager struct {
 	gotAds map[string][]*bgp.Advertisement
 }
 
-func (f *fakeBGPSessionManager) NewSession(_ log.Logger, addr string, _ net.IP, _ uint32, _ net.IP, _ uint32, _ time.Duration, _ time.Duration, _ string, _ string) (bgp.Session, error) {
+func (f *fakeBGPSessionManager) NewSession(_ log.Logger, addr string, _ net.IP, _ uint32, _ net.IP, _ uint32, _ time.Duration, _ time.Duration, _, _, _ string) (bgp.Session, error) {
 	f.Lock()
 	defer f.Unlock()
 
@@ -127,6 +127,10 @@ func (f *fakeBGPSessionManager) NewSession(_ log.Logger, addr string, _ net.IP,
 	}, nil
 }
 
+func (f *fakeBGPSessionManager) SyncBFDProfiles(profiles map[string]*config.BFDProfile) error {
+	return nil
+}
+
 func (f *fakeBGPSessionManager) Ads() map[string][]*bgp.Advertisement {
 	ret := map[string][]*bgp.Advertisement{}
 
diff --git a/speaker/validate_test.go b/speaker/validate_test.go
new file mode 100644
index 00000000..4a407085
--- /dev/null
+++ b/speaker/validate_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package main
+
+import (
+	"net"
+	"testing"
+
+	"go.universe.tf/metallb/internal/config"
+	"k8s.io/apimachinery/pkg/labels"
+)
+
+func TestValidate(t *testing.T) {
+	tests := []struct {
+		desc     string
+		config   *config.Config
+		mustFail bool
+	}{
+		{
+			desc: "peer with bfd profile",
+			config: &config.Config{
+				Peers: []*config.Peer{
+					{
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						BFDProfile:    "foo",
+					},
+				},
+			},
+			mustFail: true,
+		},
+		{
+			desc: "bfd profile set",
+			config: &config.Config{
+				Peers: []*config.Peer{
+					{
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+					},
+				},
+				BFDProfiles: map[string]*config.BFDProfile{
+					"default": {
+						Name: "default",
+					},
+				},
+			},
+			mustFail: true,
+		},
+		{
+			desc: "should pass",
+			config: &config.Config{
+				Peers: []*config.Peer{
+					{
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+					},
+				},
+			},
+			mustFail: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			err := validateFRROnlyConfiguration(test.config)
+			if test.mustFail && err == nil {
+				t.Fatalf("Expected error for %s", test.desc)
+			}
+			if !test.mustFail && err != nil {
+				t.Fatalf("Not expected error %s for %s", err, test.desc)
+			}
+		})
+	}
+}