Repository: metallb/metallb
Pull request #2537 (2024-09-16 09:51:26)

Align hold/keepalive BGP timers with FRR
<!-- Thanks for sending a pull request!
1. If this is your first time, please read the [contributing guide](https://metallb.universe.tf/community/#contributing)
2. For non-trivial pull requests, please [file an
   issue](https://github.com/metallb/metallb/issues/new) first, and get
   agreement that the change is a good idea, and a general guideline
   for how it should be implemented, before sending code. Large PRs
   that weren't first discussed and agreed upon in an issue won't be
   accepted.
3. If the PR fixes a particular bug, please include the words "Fixed
   #<issue number>" in the PR text, so that the bug auto-closes when
   the PR is merged.
-->

**Is this a BUG FIX or a FEATURE ?**:
/kind bug


**What this PR does / why we need it**:
The default holdtime of frr-k8s was different from the metallb one, specifically frr-k8s delegates the defaults to FRR instead of hard-coding them. This means that a bgppeer created without specifiying the hold time will conflict with an frrconfiguration created without specifying the hold time.
Here we align the speaker with this approach, having a hardcoded default only on native mode. In addition, we get the benefit of being able to differentiate between `0 0` timers or no timers, which have a different purpose in FRR.

**Special notes for your reviewer**:

**Release note**:
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. Follow the instructions for writing a release note from k8s: https://git.k8s.io/community/contributors/guide/release-notes.md
3. If no release note is required, just write "NONE".
-->

```release-note
Fix default BGPPeer hold/keepalive timers being incompatible with FRRConfigurations. We now delegate the defaults to FRR itself instead of hard-coding them, supporting 0 timers as well.
```


fedepaol: we should definitely mention this in the release notes, as the default value is going to change. 

Patch:
diff --git a/api/v1beta1/bgppeer_conversion.go b/api/v1beta1/bgppeer_conversion.go
index 84e2c4b0..595ff146 100644
--- a/api/v1beta1/bgppeer_conversion.go
+++ b/api/v1beta1/bgppeer_conversion.go
@@ -16,8 +16,8 @@ func (src *BGPPeer) ConvertTo(dstRaw conversion.Hub) error {
 	dst.Spec.Address = src.Spec.Address
 	dst.Spec.SrcAddress = src.Spec.SrcAddress
 	dst.Spec.Port = src.Spec.Port
-	dst.Spec.HoldTime = src.Spec.HoldTime
-	dst.Spec.KeepaliveTime = src.Spec.KeepaliveTime
+	dst.Spec.HoldTime = &src.Spec.HoldTime
+	dst.Spec.KeepaliveTime = &src.Spec.KeepaliveTime
 	dst.Spec.RouterID = src.Spec.RouterID
 	dst.Spec.Password = src.Spec.Password
 	dst.Spec.BFDProfile = src.Spec.BFDProfile
@@ -30,14 +30,22 @@ func (src *BGPPeer) ConvertTo(dstRaw conversion.Hub) error {
 // ConvertFrom converts from the Hub version (v1beta2) to this version.
 func (dst *BGPPeer) ConvertFrom(srcRaw conversion.Hub) error {
 	src := srcRaw.(*v1beta2.BGPPeer)
+	var ht metav1.Duration
+	if src.Spec.HoldTime != nil {
+		ht = *src.Spec.HoldTime
+	}
+	var ka metav1.Duration
+	if src.Spec.KeepaliveTime != nil {
+		ka = *src.Spec.KeepaliveTime
+	}
 	dst.ObjectMeta = src.ObjectMeta
 	dst.Spec.MyASN = src.Spec.MyASN
 	dst.Spec.ASN = src.Spec.ASN
 	dst.Spec.Address = src.Spec.Address
 	dst.Spec.SrcAddress = src.Spec.SrcAddress
 	dst.Spec.Port = src.Spec.Port
-	dst.Spec.HoldTime = src.Spec.HoldTime
-	dst.Spec.KeepaliveTime = src.Spec.KeepaliveTime
+	dst.Spec.HoldTime = ht
+	dst.Spec.KeepaliveTime = ka
 	dst.Spec.RouterID = src.Spec.RouterID
 	dst.Spec.Password = src.Spec.Password
 	dst.Spec.BFDProfile = src.Spec.BFDProfile
diff --git a/api/v1beta2/bgppeer_types.go b/api/v1beta2/bgppeer_types.go
index 56e93eb6..ef3aec06 100644
--- a/api/v1beta2/bgppeer_types.go
+++ b/api/v1beta2/bgppeer_types.go
@@ -49,11 +49,11 @@ type BGPPeerSpec struct {
 
 	// Requested BGP hold time, per RFC4271.
 	// +optional
-	HoldTime metav1.Duration `json:"holdTime,omitempty"`
+	HoldTime *metav1.Duration `json:"holdTime,omitempty"`
 
 	// Requested BGP keepalive time, per RFC4271.
 	// +optional
-	KeepaliveTime metav1.Duration `json:"keepaliveTime,omitempty"`
+	KeepaliveTime *metav1.Duration `json:"keepaliveTime,omitempty"`
 
 	// Requested BGP connect time, controls how long BGP waits between connection attempts to a neighbor.
 	// +kubebuilder:validation:XValidation:message="connect time should be between 1 seconds to 65535",rule="duration(self).getSeconds() >= 1 && duration(self).getSeconds() <= 65535"
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index cefd6a0e..f27129a7 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -87,8 +87,16 @@ func (in *BGPPeerList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *BGPPeerSpec) DeepCopyInto(out *BGPPeerSpec) {
 	*out = *in
-	out.HoldTime = in.HoldTime
-	out.KeepaliveTime = in.KeepaliveTime
+	if in.HoldTime != nil {
+		in, out := &in.HoldTime, &out.HoldTime
+		*out = new(v1.Duration)
+		**out = **in
+	}
+	if in.KeepaliveTime != nil {
+		in, out := &in.KeepaliveTime, &out.KeepaliveTime
+		*out = new(v1.Duration)
+		**out = **in
+	}
 	if in.ConnectTime != nil {
 		in, out := &in.ConnectTime, &out.ConnectTime
 		*out = new(v1.Duration)
diff --git a/configmaptocrs/main.go b/configmaptocrs/main.go
index 7032e231..736745c0 100644
--- a/configmaptocrs/main.go
+++ b/configmaptocrs/main.go
@@ -391,7 +391,7 @@ func parsePeer(p peer) (*v1beta2.BGPPeer, error) {
 			Address:       p.Addr,
 			SrcAddress:    p.SrcAddr,
 			Port:          p.Port,
-			HoldTime:      metav1.Duration{Duration: holdTime},
+			HoldTime:      &metav1.Duration{Duration: holdTime},
 			RouterID:      p.RouterID,
 			NodeSelectors: nodeSels,
 			Password:      p.Password,
@@ -404,7 +404,7 @@ func parsePeer(p peer) (*v1beta2.BGPPeer, error) {
 		if err != nil {
 			return nil, err
 		}
-		res.Spec.KeepaliveTime = metav1.Duration{Duration: keepaliveTime}
+		res.Spec.KeepaliveTime = &metav1.Duration{Duration: keepaliveTime}
 	}
 
 	return res, nil
diff --git a/internal/bgp/bgp.go b/internal/bgp/bgp.go
index 86983553..54ad6a3e 100644
--- a/internal/bgp/bgp.go
+++ b/internal/bgp/bgp.go
@@ -68,8 +68,8 @@ type SessionParameters struct {
 	MyASN           uint32
 	RouterID        net.IP
 	PeerASN         uint32
-	HoldTime        time.Duration
-	KeepAliveTime   time.Duration
+	HoldTime        *time.Duration
+	KeepAliveTime   *time.Duration
 	ConnectTime     *time.Duration
 	Password        string
 	PasswordRef     v1.SecretReference
diff --git a/internal/bgp/frr/config.go b/internal/bgp/frr/config.go
index b18f4c22..57e8043e 100644
--- a/internal/bgp/frr/config.go
+++ b/internal/bgp/frr/config.go
@@ -70,8 +70,8 @@ type neighborConfig struct {
 	Addr                string
 	SrcAddr             string
 	Port                uint16
-	HoldTime            int64
-	KeepaliveTime       int64
+	HoldTime            *int64
+	KeepaliveTime       *int64
 	ConnectTime         int64
 	Password            string
 	Advertisements      []*advertisementConfig
diff --git a/internal/bgp/frr/frr.go b/internal/bgp/frr/frr.go
index f345e092..11e196f1 100644
--- a/internal/bgp/frr/frr.go
+++ b/internal/bgp/frr/frr.go
@@ -271,14 +271,25 @@ func (sm *sessionManager) createConfig() (*frrConfig, error) {
 				connectTime = int64(*s.ConnectTime / time.Second)
 			}
 
+			var holdTime *int64
+			var keepaliveTime *int64
+			if s.HoldTime != nil {
+				time := int64(*s.HoldTime / time.Second)
+				holdTime = &time
+			}
+			if s.KeepAliveTime != nil {
+				time := int64(*s.KeepAliveTime / time.Second)
+				keepaliveTime = &time
+			}
+
 			neighbor = &neighborConfig{
 				Name:            neighborName,
 				IPFamily:        family,
 				ASN:             s.PeerASN,
 				Addr:            host,
 				Port:            uint16(portUint),
-				HoldTime:        int64(s.HoldTime / time.Second),
-				KeepaliveTime:   int64(s.KeepAliveTime / time.Second),
+				HoldTime:        holdTime,
+				KeepaliveTime:   keepaliveTime,
 				ConnectTime:     connectTime,
 				Password:        s.Password,
 				Advertisements:  make([]*advertisementConfig, 0),
diff --git a/internal/bgp/frr/templates/neighborsession.tmpl b/internal/bgp/frr/templates/neighborsession.tmpl
index e97a2d70..840311bb 100644
--- a/internal/bgp/frr/templates/neighborsession.tmpl
+++ b/internal/bgp/frr/templates/neighborsession.tmpl
@@ -6,7 +6,9 @@
   {{ if .neighbor.Port -}}
   neighbor {{.neighbor.Addr}} port {{.neighbor.Port}}
   {{- end }}
+  {{- if and .neighbor.KeepaliveTime .neighbor.HoldTime }}
   neighbor {{.neighbor.Addr}} timers {{.neighbor.KeepaliveTime}} {{.neighbor.HoldTime}}
+  {{- end }}
   {{- if ne .neighbor.ConnectTime 0}}
   neighbor {{.neighbor.Addr}} timers connect {{.neighbor.ConnectTime}}
   {{- end }}
diff --git a/internal/bgp/frrk8s/frrk8s.go b/internal/bgp/frrk8s/frrk8s.go
index af2ce4bb..29e8383c 100644
--- a/internal/bgp/frrk8s/frrk8s.go
+++ b/internal/bgp/frrk8s/frrk8s.go
@@ -249,12 +249,21 @@ func (sm *sessionManager) updateConfig() error {
 				connectTime = &metav1.Duration{Duration: *s.ConnectTime}
 			}
 
+			var holdTime *metav1.Duration
+			var keepaliveTime *metav1.Duration
+			if s.HoldTime != nil {
+				holdTime = &metav1.Duration{Duration: *s.HoldTime}
+			}
+			if s.KeepAliveTime != nil {
+				keepaliveTime = &metav1.Duration{Duration: *s.KeepAliveTime}
+			}
+
 			neighbor = frrv1beta1.Neighbor{
 				ASN:                   s.PeerASN,
 				Address:               host,
 				Port:                  &portUint16,
-				HoldTime:              &metav1.Duration{Duration: s.HoldTime},
-				KeepaliveTime:         &metav1.Duration{Duration: s.KeepAliveTime},
+				HoldTime:              holdTime,
+				KeepaliveTime:         keepaliveTime,
 				ConnectTime:           connectTime,
 				BFDProfile:            s.BFDProfile,
 				EnableGracefulRestart: s.GracefulRestart,
diff --git a/internal/bgp/native/native.go b/internal/bgp/native/native.go
index 8d49f985..eea36036 100644
--- a/internal/bgp/native/native.go
+++ b/internal/bgp/native/native.go
@@ -59,8 +59,15 @@ func NewSessionManager(l log.Logger) bgp.SessionManager {
 // The session will immediately try to connect and synchronize its
 // local state with the peer.
 func (sm *sessionManager) NewSession(l log.Logger, args bgp.SessionParameters) (bgp.Session, error) {
+	sessionsParams := args
+	// native mode does not support empty holdtime,
+	// we explicitly set it to 90s in this case.
+	if args.HoldTime == nil {
+		ht := 90 * time.Second
+		sessionsParams.HoldTime = &ht
+	}
 	ret := &session{
-		SessionParameters: args,
+		SessionParameters: sessionsParams,
 		logger:            log.With(l, "peer", args.PeerAddress, "localASN", args.MyASN, "peerASN", args.PeerASN),
 		newHoldTime:       make(chan bool, 1),
 		advertised:        map[string]*bgp.Advertisement{},
@@ -238,7 +245,7 @@ func (s *session) connect() error {
 		}
 	}
 
-	if err = sendOpen(conn, s.MyASN, routerID, s.HoldTime); err != nil {
+	if err = sendOpen(conn, s.MyASN, routerID, *s.HoldTime); err != nil {
 		conn.Close()
 		return fmt.Errorf("send OPEN to %q: %s", s.PeerAddress, err)
 	}
@@ -274,7 +281,7 @@ func (s *session) connect() error {
 	}
 
 	// Set up regular keepalives from now on.
-	s.actualHoldTime = s.HoldTime
+	s.actualHoldTime = *s.HoldTime
 	if op.holdTime < s.actualHoldTime {
 		s.actualHoldTime = op.holdTime
 	}
diff --git a/internal/config/config.go b/internal/config/config.go
index b6efc665..f0be82e2 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -103,9 +103,9 @@ type Peer struct {
 	// Port to dial when establishing the session.
 	Port uint16
 	// Requested BGP hold time, per RFC4271.
-	HoldTime time.Duration
+	HoldTime *time.Duration
 	// Requested BGP keepalive time, per RFC4271.
-	KeepaliveTime time.Duration
+	KeepaliveTime *time.Duration
 	// Requested BGP connect time, controls how long BGP waits between connection attempts to a neighbor.
 	ConnectTime *time.Duration
 	// BGP router ID to advertise to the peer
@@ -385,23 +385,10 @@ func peerFromCR(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1.Secr
 	if ip == nil {
 		return nil, fmt.Errorf("invalid BGPPeer address %q", p.Spec.Address)
 	}
-	holdTime := p.Spec.HoldTime.Duration
-	if holdTime == 0 {
-		holdTime = 90 * time.Second
-	}
-	err := validateHoldTime(holdTime)
-	if err != nil {
-		return nil, err
-	}
-	keepaliveTime := p.Spec.KeepaliveTime.Duration
-	if keepaliveTime == 0 {
-		keepaliveTime = holdTime / 3
-	}
 
-	// keepalive must be lower than holdtime
-	if keepaliveTime > holdTime {
-		return nil, fmt.Errorf("invalid keepaliveTime %q must be smaller than holdtime %q",
-			keepaliveTime, holdTime)
+	holdTime, keepaliveTime, err := parseTimers(p.Spec.HoldTime, p.Spec.KeepaliveTime)
+	if err != nil {
+		return nil, fmt.Errorf("invalid BGPPeer timers: %w", err)
 	}
 
 	// Ideally we would set a default RouterID here, instead of having
@@ -820,12 +807,36 @@ func getCommunityValue(communityString string, communities map[string]community.
 	return c, nil
 }
 
-func validateHoldTime(ht time.Duration) error {
-	rounded := time.Duration(int(ht.Seconds())) * time.Second
+func parseTimers(ht, ka *metav1.Duration) (*time.Duration, *time.Duration, error) {
+	if ht == nil && ka == nil {
+		return nil, nil, nil
+	}
+
+	var holdTime *time.Duration
+	var keepaliveTime *time.Duration
+	if ht != nil && ka != nil {
+		holdTime = &ht.Duration
+		keepaliveTime = &ka.Duration
+	}
+	if ht != nil && ka == nil {
+		holdTime = &ht.Duration
+		keepaliveTime = ptr.To(ht.Duration / 3)
+	}
+	if ht == nil && ka != nil {
+		holdTime = ptr.To(ka.Duration * 3)
+		keepaliveTime = &ka.Duration
+	}
+
+	rounded := time.Duration(int(holdTime.Seconds())) * time.Second
 	if rounded != 0 && rounded < 3*time.Second {
-		return fmt.Errorf("invalid hold time %q: must be 0 or >=3s", ht)
+		return nil, nil, fmt.Errorf("invalid hold time %q: must be 0 or >=3s", ht)
 	}
-	return nil
+
+	if *keepaliveTime > *holdTime {
+		return nil, nil, fmt.Errorf("invalid keepaliveTime %q, must be lower than holdTime %q", ka, ht)
+	}
+
+	return holdTime, keepaliveTime, nil
 }
 
 func validateBGPAdvPerPool(adv *BGPAdvertisement, pool *Pool) error {
diff --git a/internal/config/validation.go b/internal/config/validation.go
index 92621b7f..56ed79d6 100644
--- a/internal/config/validation.go
+++ b/internal/config/validation.go
@@ -33,7 +33,7 @@ func DiscardFRROnly(c ClusterResources) error {
 		if p.Spec.BFDProfile != "" {
 			return fmt.Errorf("peer %s has bfd-profile set on native bgp mode", p.Spec.Address)
 		}
-		if p.Spec.KeepaliveTime.Duration != 0 {
+		if p.Spec.KeepaliveTime != nil && p.Spec.KeepaliveTime.Duration != 0 {
 			return fmt.Errorf("peer %s has keepalive-time set on native bgp mode", p.Spec.Address)
 		}
 		if p.Spec.VRFName != "" {


Test patch:
diff --git a/api/v1beta1/bgppeer_conversion_test.go b/api/v1beta1/bgppeer_conversion_test.go
index 3695a798..7421a05c 100644
--- a/api/v1beta1/bgppeer_conversion_test.go
+++ b/api/v1beta1/bgppeer_conversion_test.go
@@ -63,14 +63,14 @@ func TestValidateBGPPeerConvertTo(t *testing.T) {
 			ASN:            142,
 			Address:        "1.2.3.4",
 			Port:           1179,
-			HoldTime:       v1.Duration{Duration: 180 * time.Second},
+			HoldTime:       &v1.Duration{Duration: 180 * time.Second},
 			RouterID:       "10.20.30.40",
 			SrcAddress:     "10.20.30.40",
 			EBGPMultiHop:   true,
 			Password:       "nopass",
 			PasswordSecret: corev1.SecretReference{},
 			BFDProfile:     "default",
-			KeepaliveTime:  v1.Duration{Duration: time.Second},
+			KeepaliveTime:  &v1.Duration{Duration: time.Second},
 			NodeSelectors: []v1.LabelSelector{
 				{
 					MatchLabels: map[string]string{
@@ -111,7 +111,7 @@ func TestValidateBGPPeerConvertFrom(t *testing.T) {
 			ASN:          142,
 			Address:      "1.2.3.4",
 			Port:         1179,
-			HoldTime:     v1.Duration{Duration: 180 * time.Second},
+			HoldTime:     &v1.Duration{Duration: 180 * time.Second},
 			RouterID:     "10.20.30.40",
 			SrcAddress:   "10.20.30.40",
 			EBGPMultiHop: true,
@@ -119,7 +119,7 @@ func TestValidateBGPPeerConvertFrom(t *testing.T) {
 			PasswordSecret: corev1.SecretReference{Name: "nosecret",
 				Namespace: "metallb-system"},
 			BFDProfile:    "default",
-			KeepaliveTime: v1.Duration{Duration: time.Second},
+			KeepaliveTime: &v1.Duration{Duration: time.Second},
 			NodeSelectors: []v1.LabelSelector{
 				{
 					MatchLabels: map[string]string{
diff --git a/configmaptocrs/testdata/configmap-data/config-data-peers.golden b/configmaptocrs/testdata/configmap-data/config-data-peers.golden
index c119b8c9..1d47ae0c 100644
--- a/configmaptocrs/testdata/configmap-data/config-data-peers.golden
+++ b/configmaptocrs/testdata/configmap-data/config-data-peers.golden
@@ -7,7 +7,6 @@ metadata:
   namespace: metallb-system
 spec:
   holdTime: 1m30s
-  keepaliveTime: 0s
   myASN: 64512
   passwordSecret: {}
   peerASN: 64512
@@ -22,7 +21,6 @@ metadata:
   namespace: metallb-system
 spec:
   holdTime: 1m30s
-  keepaliveTime: 0s
   myASN: 64512
   passwordSecret: {}
   peerASN: 64512
diff --git a/configmaptocrs/testdata/configmap-yaml/config-yaml-peers.golden b/configmaptocrs/testdata/configmap-yaml/config-yaml-peers.golden
index c119b8c9..1d47ae0c 100644
--- a/configmaptocrs/testdata/configmap-yaml/config-yaml-peers.golden
+++ b/configmaptocrs/testdata/configmap-yaml/config-yaml-peers.golden
@@ -7,7 +7,6 @@ metadata:
   namespace: metallb-system
 spec:
   holdTime: 1m30s
-  keepaliveTime: 0s
   myASN: 64512
   passwordSecret: {}
   peerASN: 64512
@@ -22,7 +21,6 @@ metadata:
   namespace: metallb-system
 spec:
   holdTime: 1m30s
-  keepaliveTime: 0s
   myASN: 64512
   passwordSecret: {}
   peerASN: 64512
diff --git a/e2etest/bgptests/bgp.go b/e2etest/bgptests/bgp.go
index 473050e6..ae6370ee 100644
--- a/e2etest/bgptests/bgp.go
+++ b/e2etest/bgptests/bgp.go
@@ -1295,16 +1295,16 @@ var _ = ginkgo.Describe("BGP", func() {
 					r.VRF = p.Spec.VRFName
 
 					keepAliveTime := p.Spec.KeepaliveTime
-					if keepAliveTime.Duration == 0 {
-						keepAliveTime.Duration = p.Spec.HoldTime.Duration / 3
+					if keepAliveTime == nil && p.Spec.HoldTime != nil {
+						keepAliveTime = &metav1.Duration{Duration: p.Spec.HoldTime.Duration / 3}
 					}
 					r.Neighbors = append(r.Neighbors, frrk8sv1beta1.Neighbor{
 						ASN:           p.Spec.ASN,
 						Address:       p.Spec.Address,
 						Password:      p.Spec.Password,
 						Port:          &p.Spec.Port,
-						HoldTime:      &p.Spec.HoldTime,
-						KeepaliveTime: &keepAliveTime,
+						HoldTime:      p.Spec.HoldTime,
+						KeepaliveTime: keepAliveTime,
 						EBGPMultiHop:  p.Spec.EBGPMultiHop,
 						BFDProfile:    p.Spec.BFDProfile,
 						ToReceive: frrk8sv1beta1.Receive{
@@ -1466,8 +1466,8 @@ var _ = ginkgo.Describe("BGP", func() {
 			})
 
 			for i := range resources.Peers {
-				resources.Peers[i].Spec.KeepaliveTime = metav1.Duration{Duration: 13 * time.Second}
-				resources.Peers[i].Spec.HoldTime = metav1.Duration{Duration: 57 * time.Second}
+				resources.Peers[i].Spec.KeepaliveTime = &metav1.Duration{Duration: 13 * time.Second}
+				resources.Peers[i].Spec.HoldTime = &metav1.Duration{Duration: 57 * time.Second}
 			}
 
 			err := ConfigUpdater.Update(resources)
diff --git a/e2etest/pkg/metallb/config.go b/e2etest/pkg/metallb/config.go
index 917141e7..74c8e923 100644
--- a/e2etest/pkg/metallb/config.go
+++ b/e2etest/pkg/metallb/config.go
@@ -55,7 +55,7 @@ func PeersForContainers(containers []*frrcontainer.FRR, ipFamily ipfamily.Family
 					MyASN:                 c.NeighborConfig.ASN,
 					Port:                  c.RouterConfig.BGPPort,
 					Password:              c.RouterConfig.Password,
-					HoldTime:              metav1.Duration{Duration: holdTime},
+					HoldTime:              &metav1.Duration{Duration: holdTime},
 					EnableGracefulRestart: gracefulRestart,
 					EBGPMultiHop:          ebgpMultihop,
 					VRFName:               c.RouterConfig.VRF,
diff --git a/internal/bgp/frr/frr_bfd_test.go b/internal/bgp/frr/frr_bfd_test.go
index b5c9b200..e7cb6a0f 100644
--- a/internal/bgp/frr/frr_bfd_test.go
+++ b/internal/bgp/frr/frr_bfd_test.go
@@ -129,8 +129,8 @@ func TestBFDWithSession(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: 2 * time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(2 * time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
diff --git a/internal/bgp/frr/frr_test.go b/internal/bgp/frr/frr_test.go
index fff97b79..a999d4f4 100644
--- a/internal/bgp/frr/frr_test.go
+++ b/internal/bgp/frr/frr_test.go
@@ -124,8 +124,8 @@ func TestSingleEBGPSessionMultiHop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -151,8 +151,8 @@ func TestSingleEBGPSessionOneHop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -179,8 +179,8 @@ func TestSingleIPv6EBGPSessionOneHop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -207,8 +207,8 @@ func TestSingleIBGPSession(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       100,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -235,8 +235,8 @@ func TestSingleIPv6IBGPSession(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       100,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			ConnectTime:   ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
@@ -264,8 +264,8 @@ func TestSingleSessionClose(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -316,8 +316,8 @@ func TestTwoSessions(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			ConnectTime:   ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
@@ -335,8 +335,8 @@ func TestTwoSessions(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			ConnectTime:   ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
@@ -365,8 +365,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -382,8 +382,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -410,8 +410,8 @@ func TestIPv4AndIPv6SessionsDisableMP(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -429,8 +429,8 @@ func TestIPv4AndIPv6SessionsDisableMP(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -457,8 +457,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -475,8 +475,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -503,8 +503,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -520,8 +520,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -548,8 +548,8 @@ func TestSingleAdvertisement(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			ConnectTime:   ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
@@ -596,8 +596,8 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 			MyASN:         100,
 			RouterID:      nil,
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -638,8 +638,8 @@ func TestSingleAdvertisementInvalidNoPort(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -666,8 +666,8 @@ func TestSingleAdvertisementStop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -713,8 +713,8 @@ func TestSingleAdvertisementChange(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -768,8 +768,8 @@ func TestTwoAdvertisements(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -821,8 +821,8 @@ func TestTwoAdvertisementsDuplicate(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -868,8 +868,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
 					PeerASN:       200,
-					HoldTime:      time.Second,
-					KeepAliveTime: time.Second,
+					HoldTime:      ptr.To(time.Second),
+					KeepAliveTime: ptr.To(time.Second),
 					ConnectTime:   ptr.To(time.Second),
 					Password:      "password",
 					CurrentNode:   "hostname",
@@ -881,8 +881,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
 					PeerASN:       200,
-					HoldTime:      time.Second,
-					KeepAliveTime: time.Second,
+					HoldTime:      ptr.To(time.Second),
+					KeepAliveTime: ptr.To(time.Second),
 					ConnectTime:   ptr.To(time.Second),
 					Password:      "password",
 					CurrentNode:   "hostname",
@@ -970,8 +970,8 @@ func TestSingleSessionExtras(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			ConnectTime:   ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
@@ -1051,8 +1051,8 @@ func TestLargeCommunities(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -1100,8 +1100,8 @@ func TestManyAdvertisementsSameCommunity(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			ConnectTime:   ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
@@ -1135,6 +1135,50 @@ func TestManyAdvertisementsSameCommunity(t *testing.T) {
 	testCheckConfigFile(t)
 }
 
+func TestSingleSessionWithNoTimers(t *testing.T) {
+	testSetup(t)
+
+	l := log.NewNopLogger()
+	sessionManager := mockNewSessionManager(l, logging.LevelInfo)
+	defer close(sessionManager.reloadConfig)
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerAddress: "10.2.2.254:179",
+			MyASN:       102,
+			PeerASN:     100,
+			SessionName: "test-peer"})
+
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
+
+func TestSingleSessionWithZeroTimers(t *testing.T) {
+	testSetup(t)
+
+	l := log.NewNopLogger()
+	sessionManager := mockNewSessionManager(l, logging.LevelInfo)
+	defer close(sessionManager.reloadConfig)
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerAddress:   "10.2.2.254:179",
+			MyASN:         102,
+			HoldTime:      ptr.To(0 * time.Second),
+			KeepAliveTime: ptr.To(0 * time.Second),
+			PeerASN:       100,
+			SessionName:   "test-peer"})
+
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
+
 func TestAddToAdvertisements(t *testing.T) {
 	tests := []struct {
 		name      string
diff --git a/internal/bgp/frr/frr_vrf_test.go b/internal/bgp/frr/frr_vrf_test.go
index f52bdaa4..c21a1e1f 100644
--- a/internal/bgp/frr/frr_vrf_test.go
+++ b/internal/bgp/frr/frr_vrf_test.go
@@ -11,6 +11,7 @@ import (
 	"go.universe.tf/metallb/internal/bgp"
 	"go.universe.tf/metallb/internal/bgp/community"
 	"go.universe.tf/metallb/internal/logging"
+	"k8s.io/utils/ptr"
 )
 
 func TestVRFSingleEBGPSessionMultiHop(t *testing.T) {
@@ -26,8 +27,8 @@ func TestVRFSingleEBGPSessionMultiHop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -54,8 +55,8 @@ func TestSingleVRFIBGPSession(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       100,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -83,8 +84,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -101,8 +102,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -130,8 +131,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -148,8 +149,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -175,8 +176,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 			PeerAddress:   "10.2.2.254:179",
 			MyASN:         100,
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -191,8 +192,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 			PeerAddress:   "10.2.2.254:179",
 			MyASN:         100,
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -220,8 +221,8 @@ func TestSingleAdvertisementVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -268,8 +269,8 @@ func TestSingleAdvertisementChangeVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -324,8 +325,8 @@ func TestTwoAdvertisementsVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -378,8 +379,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -396,8 +397,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
diff --git a/internal/bgp/frr/testdata/TestSingleSessionWithGracefulRestart.golden b/internal/bgp/frr/testdata/TestSingleSessionWithGracefulRestart.golden
index 5c80852c..bc3563f4 100644
--- a/internal/bgp/frr/testdata/TestSingleSessionWithGracefulRestart.golden
+++ b/internal/bgp/frr/testdata/TestSingleSessionWithGracefulRestart.golden
@@ -25,7 +25,6 @@ router bgp 102
   bgp router-id 10.1.1.254
   neighbor 10.2.2.254 remote-as 100
   neighbor 10.2.2.254 port 179
-  neighbor 10.2.2.254 timers 0 0
   
   neighbor 10.2.2.254 update-source 10.1.1.254
   neighbor 10.2.2.254 graceful-restart
diff --git a/internal/bgp/frr/testdata/TestSingleSessionWithNoTimers.golden b/internal/bgp/frr/testdata/TestSingleSessionWithNoTimers.golden
new file mode 100644
index 00000000..92529f59
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestSingleSessionWithNoTimers.golden
@@ -0,0 +1,40 @@
+log file /etc/frr/frr.log 
+log timestamp precision 3
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+route-map 10.2.2.254-in deny 20
+
+
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 deny any
+ipv6 prefix-list 10.2.2.254-pl-ipv4 seq 2 deny any
+
+route-map 10.2.2.254-out permit 1
+  match ip address prefix-list 10.2.2.254-pl-ipv4
+route-map 10.2.2.254-out permit 2
+  match ipv6 address prefix-list 10.2.2.254-pl-ipv4
+
+router bgp 102
+  no bgp ebgp-requires-policy
+  no bgp network import-check
+  no bgp default ipv4-unicast
+  bgp graceful-restart preserve-fw-state
+
+  neighbor 10.2.2.254 remote-as 100
+  neighbor 10.2.2.254 port 179
+  
+  
+
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+
diff --git a/internal/bgp/frr/testdata/TestSingleSessionWithZeroTimers.golden b/internal/bgp/frr/testdata/TestSingleSessionWithZeroTimers.golden
new file mode 100644
index 00000000..c85b6bba
--- /dev/null
+++ b/internal/bgp/frr/testdata/TestSingleSessionWithZeroTimers.golden
@@ -0,0 +1,41 @@
+log file /etc/frr/frr.log 
+log timestamp precision 3
+hostname dummyhostname
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+route-map 10.2.2.254-in deny 20
+
+
+
+
+ip prefix-list 10.2.2.254-pl-ipv4 seq 1 deny any
+ipv6 prefix-list 10.2.2.254-pl-ipv4 seq 2 deny any
+
+route-map 10.2.2.254-out permit 1
+  match ip address prefix-list 10.2.2.254-pl-ipv4
+route-map 10.2.2.254-out permit 2
+  match ipv6 address prefix-list 10.2.2.254-pl-ipv4
+
+router bgp 102
+  no bgp ebgp-requires-policy
+  no bgp network import-check
+  no bgp default ipv4-unicast
+  bgp graceful-restart preserve-fw-state
+
+  neighbor 10.2.2.254 remote-as 100
+  neighbor 10.2.2.254 port 179
+  neighbor 10.2.2.254 timers 0 0
+  
+  
+
+  address-family ipv4 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+  address-family ipv6 unicast
+    neighbor 10.2.2.254 activate
+    neighbor 10.2.2.254 route-map 10.2.2.254-in in
+    neighbor 10.2.2.254 route-map 10.2.2.254-out out
+  exit-address-family
+
diff --git a/internal/bgp/frrk8s/frrk8s_bfd_test.go b/internal/bgp/frrk8s/frrk8s_bfd_test.go
index ed866b98..075e0f92 100644
--- a/internal/bgp/frrk8s/frrk8s_bfd_test.go
+++ b/internal/bgp/frrk8s/frrk8s_bfd_test.go
@@ -117,8 +117,8 @@ func TestBFDWithSession(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: 2 * time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(2 * time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
diff --git a/internal/bgp/frrk8s/frrk8s_test.go b/internal/bgp/frrk8s/frrk8s_test.go
index 635f3ff8..27b99be4 100644
--- a/internal/bgp/frrk8s/frrk8s_test.go
+++ b/internal/bgp/frrk8s/frrk8s_test.go
@@ -22,6 +22,7 @@ import (
 	"go.universe.tf/metallb/internal/logging"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/utils/ptr"
 )
 
 const testData = "testdata/"
@@ -125,8 +126,8 @@ func TestSingleEBGPSessionGracefullRestart(t *testing.T) {
 			MyASN:           100,
 			RouterID:        net.ParseIP("10.1.1.254"),
 			PeerASN:         200,
-			HoldTime:        time.Second,
-			KeepAliveTime:   time.Second,
+			HoldTime:        ptr.To(time.Second),
+			KeepAliveTime:   ptr.To(time.Second),
 			Password:        "password",
 			CurrentNode:     "hostname",
 			GracefulRestart: true,
@@ -150,8 +151,8 @@ func TestSingleEBGPSessionMultiHop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -174,8 +175,8 @@ func TestSingleEBGPSessionOneHop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -199,8 +200,8 @@ func TestSingleIPv6EBGPSessionOneHop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -224,8 +225,8 @@ func TestSingleIBGPSession(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       100,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -249,8 +250,8 @@ func TestSingleIPv6IBGPSession(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       100,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -274,8 +275,8 @@ func TestSingleSessionClose(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -298,8 +299,8 @@ func TestTwoSessions(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -316,8 +317,8 @@ func TestTwoSessions(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			PasswordRef: corev1.SecretReference{
 				Name:      "test-secret",
 				Namespace: testNamespace,
@@ -345,8 +346,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -362,8 +363,8 @@ func TestTwoIPv6Sessions(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -386,8 +387,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -404,8 +405,8 @@ func TestTwoSessionsDuplicate(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -429,8 +430,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -446,8 +447,8 @@ func TestTwoSessionsDuplicateRouter(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -471,8 +472,8 @@ func TestSingleAdvertisement(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -515,8 +516,8 @@ func TestSingleAdvertisementNoRouterID(t *testing.T) {
 			MyASN:         100,
 			RouterID:      nil,
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -555,8 +556,8 @@ func TestSingleAdvertisementInvalidNoPort(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -581,8 +582,8 @@ func TestSingleAdvertisementStop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -626,8 +627,8 @@ func TestSingleAdvertisementChange(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -679,8 +680,8 @@ func TestTwoAdvertisements(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -732,8 +733,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
 					PeerASN:       200,
-					HoldTime:      time.Second,
-					KeepAliveTime: time.Second,
+					HoldTime:      ptr.To(time.Second),
+					KeepAliveTime: ptr.To(time.Second),
 					Password:      "password",
 					CurrentNode:   "hostname",
 					EBGPMultiHop:  true,
@@ -744,8 +745,8 @@ func TestTwoAdvertisementsTwoSessions(t *testing.T) {
 					MyASN:         100,
 					RouterID:      net.ParseIP("10.1.1.254"),
 					PeerASN:       200,
-					HoldTime:      time.Second,
-					KeepAliveTime: time.Second,
+					HoldTime:      ptr.To(time.Second),
+					KeepAliveTime: ptr.To(time.Second),
 					Password:      "password",
 					CurrentNode:   "hostname",
 					EBGPMultiHop:  true,
@@ -825,8 +826,8 @@ func TestLargeCommunities(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -860,3 +861,43 @@ func TestLargeCommunities(t *testing.T) {
 
 	testCheckConfigFile(t)
 }
+
+func TestSingleSessionWithNoTimers(t *testing.T) {
+	l := log.NewNopLogger()
+	sessionManager := newTestSessionManager(t)
+
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerAddress: "10.2.2.254:179",
+			MyASN:       102,
+			PeerASN:     100,
+			SessionName: "test-peer"})
+
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
+
+func TestSingleSessionWithZeroTimers(t *testing.T) {
+	l := log.NewNopLogger()
+	sessionManager := newTestSessionManager(t)
+
+	session, err := sessionManager.NewSession(l,
+		bgp.SessionParameters{
+			PeerAddress:   "10.2.2.254:179",
+			MyASN:         102,
+			HoldTime:      ptr.To(0 * time.Second),
+			KeepAliveTime: ptr.To(0 * time.Second),
+			PeerASN:       100,
+			SessionName:   "test-peer"})
+
+	if err != nil {
+		t.Fatalf("Could not create session: %s", err)
+	}
+	defer session.Close()
+
+	testCheckConfigFile(t)
+}
diff --git a/internal/bgp/frrk8s/frrk8s_vrf_test.go b/internal/bgp/frrk8s/frrk8s_vrf_test.go
index 2ee70264..681cbfe5 100644
--- a/internal/bgp/frrk8s/frrk8s_vrf_test.go
+++ b/internal/bgp/frrk8s/frrk8s_vrf_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/go-kit/log"
 	"go.universe.tf/metallb/internal/bgp"
 	"go.universe.tf/metallb/internal/bgp/community"
+	"k8s.io/utils/ptr"
 )
 
 func TestVRFSingleEBGPSessionMultiHop(t *testing.T) {
@@ -23,8 +24,8 @@ func TestVRFSingleEBGPSessionMultiHop(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -49,8 +50,8 @@ func TestSingleVRFIBGPSession(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       100,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -76,8 +77,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -94,8 +95,8 @@ func TestTwoSessionsOneVRF(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -121,8 +122,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -139,8 +140,8 @@ func TestTwoSessionsSameIPVRF(t *testing.T) {
 			MyASN:         300,
 			RouterID:      net.ParseIP("10.3.3.254"),
 			PeerASN:       400,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -164,8 +165,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 			PeerAddress:   "10.2.2.254:179",
 			MyASN:         100,
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -180,8 +181,8 @@ func TestTwoSessionsSameIPRouterIDASNVRF(t *testing.T) {
 			PeerAddress:   "10.2.2.254:179",
 			MyASN:         100,
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  true,
@@ -207,8 +208,8 @@ func TestSingleAdvertisementVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -253,8 +254,8 @@ func TestSingleAdvertisementChangeVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -307,8 +308,8 @@ func TestTwoAdvertisementsVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -359,8 +360,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
@@ -377,8 +378,8 @@ func TestTwoAdvertisementsTwoSessionsOneVRF(t *testing.T) {
 			MyASN:         100,
 			RouterID:      net.ParseIP("10.1.1.254"),
 			PeerASN:       200,
-			HoldTime:      time.Second,
-			KeepAliveTime: time.Second,
+			HoldTime:      ptr.To(time.Second),
+			KeepAliveTime: ptr.To(time.Second),
 			Password:      "password",
 			CurrentNode:   "hostname",
 			EBGPMultiHop:  false,
diff --git a/internal/bgp/frrk8s/testdata/TestSingleSessionWithNoTimers.golden b/internal/bgp/frrk8s/testdata/TestSingleSessionWithNoTimers.golden
new file mode 100644
index 00000000..6ef69e9c
--- /dev/null
+++ b/internal/bgp/frrk8s/testdata/TestSingleSessionWithNoTimers.golden
@@ -0,0 +1,37 @@
+{
+    "metadata": {
+        "name": "metallb-testnodename",
+        "namespace": "testnamespace",
+        "creationTimestamp": null
+    },
+    "spec": {
+        "bgp": {
+            "routers": [
+                {
+                    "asn": 102,
+                    "neighbors": [
+                        {
+                            "asn": 100,
+                            "address": "10.2.2.254",
+                            "port": 179,
+                            "passwordSecret": {},
+                            "toAdvertise": {
+                                "allowed": {}
+                            },
+                            "toReceive": {
+                                "allowed": {}
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        "raw": {},
+        "nodeSelector": {
+            "matchLabels": {
+                "kubernetes.io/hostname": "testnodename"
+            }
+        }
+    },
+    "status": {}
+}
\ No newline at end of file
diff --git a/internal/bgp/frrk8s/testdata/TestSingleSessionWithZeroTimers.golden b/internal/bgp/frrk8s/testdata/TestSingleSessionWithZeroTimers.golden
new file mode 100644
index 00000000..d872e191
--- /dev/null
+++ b/internal/bgp/frrk8s/testdata/TestSingleSessionWithZeroTimers.golden
@@ -0,0 +1,39 @@
+{
+    "metadata": {
+        "name": "metallb-testnodename",
+        "namespace": "testnamespace",
+        "creationTimestamp": null
+    },
+    "spec": {
+        "bgp": {
+            "routers": [
+                {
+                    "asn": 102,
+                    "neighbors": [
+                        {
+                            "asn": 100,
+                            "address": "10.2.2.254",
+                            "port": 179,
+                            "passwordSecret": {},
+                            "holdTime": "0s",
+                            "keepaliveTime": "0s",
+                            "toAdvertise": {
+                                "allowed": {}
+                            },
+                            "toReceive": {
+                                "allowed": {}
+                            }
+                        }
+                    ]
+                }
+            ]
+        },
+        "raw": {},
+        "nodeSelector": {
+            "matchLabels": {
+                "kubernetes.io/hostname": "testnodename"
+            }
+        }
+    },
+    "status": {}
+}
\ No newline at end of file
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 0e887413..c7f3f3c3 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -73,7 +73,7 @@ func TestParse(t *testing.T) {
 							ASN:                   142,
 							Address:               "1.2.3.4",
 							Port:                  1179,
-							HoldTime:              metav1.Duration{Duration: 180 * time.Second},
+							HoldTime:              ptr.To(metav1.Duration{Duration: 180 * time.Second}),
 							ConnectTime:           ptr.To(metav1.Duration{Duration: time.Second}),
 							RouterID:              "10.20.30.40",
 							SrcAddress:            "10.20.30.40",
@@ -230,8 +230,8 @@ func TestParse(t *testing.T) {
 						Addr:                  net.ParseIP("1.2.3.4"),
 						SrcAddr:               net.ParseIP("10.20.30.40"),
 						Port:                  1179,
-						HoldTime:              180 * time.Second,
-						KeepaliveTime:         60 * time.Second,
+						HoldTime:              ptr.To(180 * time.Second),
+						KeepaliveTime:         ptr.To(60 * time.Second),
 						ConnectTime:           ptr.To(time.Second),
 						RouterID:              net.ParseIP("10.20.30.40"),
 						NodeSelectors:         []labels.Selector{labels.Everything()},
@@ -244,8 +244,6 @@ func TestParse(t *testing.T) {
 						MyASN:                 100,
 						ASN:                   200,
 						Addr:                  net.ParseIP("2.3.4.5"),
-						HoldTime:              90 * time.Second,
-						KeepaliveTime:         30 * time.Second,
 						ConnectTime:           ptr.To(time.Second),
 						NodeSelectors:         []labels.Selector{selector("bar in (quux),foo=bar")},
 						EnableGracefulRestart: false,
@@ -661,8 +659,6 @@ func TestParse(t *testing.T) {
 						MyASN:         42,
 						ASN:           42,
 						Addr:          net.ParseIP("1.2.3.4"),
-						HoldTime:      90 * time.Second,
-						KeepaliveTime: 30 * time.Second,
 						NodeSelectors: []labels.Selector{labels.Everything()},
 						EBGPMultiHop:  false,
 					},
@@ -729,7 +725,7 @@ func TestParse(t *testing.T) {
 			},
 		},
 		{
-			desc: "invalid keepalivetime larger than default holdtime",
+			desc: "invalid keepalivetime larger than holdtime",
 			crs: ClusterResources{
 				Peers: []v1beta2.BGPPeer{
 					{
@@ -737,41 +733,166 @@ func TestParse(t *testing.T) {
 							MyASN:         42,
 							ASN:           42,
 							Address:       "1.2.3.4",
-							KeepaliveTime: metav1.Duration{Duration: 91 * time.Second}},
+							HoldTime:      ptr.To(metav1.Duration{Duration: 30 * time.Second}),
+							KeepaliveTime: ptr.To(metav1.Duration{Duration: 90 * time.Second}),
+						},
 					},
 				},
 			},
 		},
+
 		{
-			desc: "invalid keepalivetime larger than holdtime",
+			desc: "invalid hold time (too short)",
+			crs: ClusterResources{
+				Peers: []v1beta2.BGPPeer{
+					{
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:    42,
+							ASN:      42,
+							Address:  "1.2.3.4",
+							HoldTime: ptr.To(metav1.Duration{Duration: time.Second}),
+						},
+					},
+				},
+			},
+		},
+		{
+			desc: "peer with holdtime only",
+			crs: ClusterResources{
+
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "peer1",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:    42,
+							ASN:      42,
+							Address:  "1.2.3.4",
+							HoldTime: ptr.To(metav1.Duration{Duration: 180 * time.Second}),
+						},
+					},
+				},
+			},
+			want: &Config{
+				Peers: map[string]*Peer{
+					"peer1": {
+						Name:          "peer1",
+						MyASN:         42,
+						ASN:           42,
+						HoldTime:      ptr.To(180 * time.Second),
+						KeepaliveTime: ptr.To(60 * time.Second),
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						EBGPMultiHop:  false,
+					},
+				},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
+		{
+			desc: "peer with keepalive only",
 			crs: ClusterResources{
+
 				Peers: []v1beta2.BGPPeer{
 					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "peer1",
+						},
 						Spec: v1beta2.BGPPeerSpec{
 							MyASN:         42,
 							ASN:           42,
 							Address:       "1.2.3.4",
-							HoldTime:      metav1.Duration{Duration: 30 * time.Second},
-							KeepaliveTime: metav1.Duration{Duration: 90 * time.Second}},
+							KeepaliveTime: ptr.To(metav1.Duration{Duration: 60 * time.Second}),
+						},
 					},
 				},
 			},
+			want: &Config{
+				Peers: map[string]*Peer{
+					"peer1": {
+						Name:          "peer1",
+						MyASN:         42,
+						ASN:           42,
+						HoldTime:      ptr.To(180 * time.Second),
+						KeepaliveTime: ptr.To(60 * time.Second),
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						EBGPMultiHop:  false,
+					},
+				},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
 		},
+		{
+			desc: "peer with zero hold/keepalive timers",
+			crs: ClusterResources{
 
+				Peers: []v1beta2.BGPPeer{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "peer1",
+						},
+						Spec: v1beta2.BGPPeerSpec{
+							MyASN:         42,
+							ASN:           42,
+							Address:       "1.2.3.4",
+							HoldTime:      ptr.To(metav1.Duration{Duration: 0 * time.Second}),
+							KeepaliveTime: ptr.To(metav1.Duration{Duration: 0 * time.Second}),
+						},
+					},
+				},
+			},
+			want: &Config{
+				Peers: map[string]*Peer{
+					"peer1": {
+						Name:          "peer1",
+						MyASN:         42,
+						ASN:           42,
+						HoldTime:      ptr.To(0 * time.Second),
+						KeepaliveTime: ptr.To(0 * time.Second),
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						EBGPMultiHop:  false,
+					},
+				},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
 		{
-			desc: "invalid hold time (too short)",
+			desc: "peer without hold/keepalive timers",
 			crs: ClusterResources{
+
 				Peers: []v1beta2.BGPPeer{
 					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "peer1",
+						},
 						Spec: v1beta2.BGPPeerSpec{
-							MyASN:    42,
-							ASN:      42,
-							Address:  "1.2.3.4",
-							HoldTime: metav1.Duration{Duration: time.Second},
+							MyASN:   42,
+							ASN:     42,
+							Address: "1.2.3.4",
 						},
 					},
 				},
 			},
+			want: &Config{
+				Peers: map[string]*Peer{
+					"peer1": {
+						Name:          "peer1",
+						MyASN:         42,
+						ASN:           42,
+						Addr:          net.ParseIP("1.2.3.4"),
+						NodeSelectors: []labels.Selector{labels.Everything()},
+						EBGPMultiHop:  false,
+					},
+				},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
 		},
 		{
 			desc: "invalid RouterID",
@@ -811,8 +932,6 @@ func TestParse(t *testing.T) {
 						MyASN:         42,
 						ASN:           42,
 						Addr:          net.ParseIP("1.2.3.4"),
-						HoldTime:      90 * time.Second,
-						KeepaliveTime: 30 * time.Second,
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
@@ -1898,8 +2017,6 @@ func TestParse(t *testing.T) {
 						MyASN:         42,
 						ASN:           42,
 						Addr:          net.ParseIP("1.2.3.4"),
-						HoldTime:      90 * time.Second,
-						KeepaliveTime: 30 * time.Second,
 						NodeSelectors: []labels.Selector{labels.Everything()},
 						BFDProfile:    "default",
 					},
@@ -2013,8 +2130,6 @@ func TestParse(t *testing.T) {
 						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:     "",
 						SecretPassword: "nopass",
@@ -2263,7 +2378,7 @@ func TestParse(t *testing.T) {
 							ASN:          142,
 							Address:      "1.2.3.4",
 							Port:         1179,
-							HoldTime:     metav1.Duration{Duration: 180 * time.Second},
+							HoldTime:     ptr.To(metav1.Duration{Duration: 180 * time.Second}),
 							RouterID:     "10.20.30.40",
 							SrcAddress:   "10.20.30.40",
 							EBGPMultiHop: true,
@@ -2328,8 +2443,6 @@ func TestParse(t *testing.T) {
 						ASN:           142,
 						Addr:          net.ParseIP("1.2.3.4"),
 						Port:          1179,
-						HoldTime:      90 * time.Second,
-						KeepaliveTime: 30 * time.Second,
 						NodeSelectors: []labels.Selector{labels.Everything()},
 						BFDProfile:    "with-echo"},
 				},
@@ -2370,7 +2483,7 @@ func TestParse(t *testing.T) {
 							ASN:          142,
 							Address:      "1.2.3.4",
 							Port:         1179,
-							HoldTime:     metav1.Duration{Duration: 180 * time.Second},
+							HoldTime:     ptr.To(metav1.Duration{Duration: 180 * time.Second}),
 							RouterID:     "10.20.30.40",
 							SrcAddress:   "10.20.30.40",
 							EBGPMultiHop: true,
@@ -2450,8 +2563,8 @@ func TestParse(t *testing.T) {
 						Addr:          net.ParseIP("1.2.3.4"),
 						SrcAddr:       net.ParseIP("10.20.30.40"),
 						Port:          1179,
-						HoldTime:      180 * time.Second,
-						KeepaliveTime: 60 * time.Second,
+						HoldTime:      ptr.To(180 * time.Second),
+						KeepaliveTime: ptr.To(60 * time.Second),
 						RouterID:      net.ParseIP("10.20.30.40"),
 						NodeSelectors: []labels.Selector{labels.Everything()},
 						EBGPMultiHop:  true,
@@ -3301,8 +3414,6 @@ func TestParse(t *testing.T) {
 						MyASN:         42,
 						ASN:           42,
 						Addr:          net.ParseIP("1.2.3.4"),
-						HoldTime:      90 * time.Second,
-						KeepaliveTime: 30 * time.Second,
 						NodeSelectors: []labels.Selector{labels.Everything()},
 						EBGPMultiHop:  false,
 					},
diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go
index c527a327..0502700e 100644
--- a/internal/config/validation_test.go
+++ b/internal/config/validation_test.go
@@ -102,7 +102,7 @@ func TestValidate(t *testing.T) {
 				Peers: []v1beta2.BGPPeer{
 					{
 						Spec: v1beta2.BGPPeerSpec{
-							KeepaliveTime: v1.Duration{Duration: time.Second},
+							KeepaliveTime: &v1.Duration{Duration: time.Second},
 						},
 					},
 				},