Repository: metallb/metallb
Pull request #967 (2021-09-28 17:16:52)

Add BFD support
This PR adds BFD support on top of BGP.

It will be possible to enable BFD for a given BGP peer, following the configuration described in the design proposal: https://github.com/metallb/metallb/blob/main/design/bgp-bfd.md

oribon: lgtm
oribon: LGTM, the layer2 fail from the ci isn't related

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 53343da1..3210c316 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -38,7 +38,7 @@ jobs:
       - run: inv dev-env -i  << parameters.ip-family >> -b  << parameters.bgp-type >>
       - 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
+}


Test patch:
diff --git a/e2etest/bgp_test.go b/e2etest/bgp_test.go
index 87c7d85a..bce69dce 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)
 		}
 
@@ -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)
@@ -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)
@@ -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/parse_config.go b/e2etest/parse_config.go
index cc0914e3..2baf64d9 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 {
@@ -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/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)
+			}
+		})
+	}
+}