github.com/cilium/cilium@v1.16.2/pkg/bgpv1/gobgp/state_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package gobgp
     5  
     6  import (
     7  	"context"
     8  	"net/netip"
     9  	"testing"
    10  
    11  	"github.com/osrg/gobgp/v3/pkg/packet/bgp"
    12  	"github.com/stretchr/testify/require"
    13  	"k8s.io/utils/ptr"
    14  
    15  	"github.com/cilium/cilium/api/v1/models"
    16  	"github.com/cilium/cilium/pkg/bgpv1/types"
    17  	v2alpha1api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
    18  	"github.com/cilium/cilium/pkg/logging"
    19  	"github.com/cilium/cilium/pkg/logging/logfields"
    20  )
    21  
    22  var (
    23  	log = logging.DefaultLogger.WithField(logfields.LogSubsys, "bgp-test")
    24  
    25  	neighbor64125 = &v2alpha1api.CiliumBGPNeighbor{
    26  		PeerASN:                 64125,
    27  		PeerAddress:             "192.168.0.1/32",
    28  		PeerPort:                ptr.To[int32](v2alpha1api.DefaultBGPPeerPort),
    29  		EBGPMultihopTTL:         ptr.To[int32](1),
    30  		ConnectRetryTimeSeconds: ptr.To[int32](99),
    31  		HoldTimeSeconds:         ptr.To[int32](9),
    32  		KeepAliveTimeSeconds:    ptr.To[int32](3),
    33  	}
    34  
    35  	// changed ConnectRetryTime
    36  	neighbor64125Update = &v2alpha1api.CiliumBGPNeighbor{
    37  		PeerASN:                 64125,
    38  		PeerAddress:             "192.168.0.1/32",
    39  		PeerPort:                ptr.To[int32](v2alpha1api.DefaultBGPPeerPort),
    40  		EBGPMultihopTTL:         ptr.To[int32](1),
    41  		ConnectRetryTimeSeconds: ptr.To[int32](101),
    42  		HoldTimeSeconds:         ptr.To[int32](9),
    43  		KeepAliveTimeSeconds:    ptr.To[int32](3),
    44  	}
    45  
    46  	// enabled graceful restart
    47  	neighbor64125UpdateGR = &v2alpha1api.CiliumBGPNeighbor{
    48  		PeerASN:                 64125,
    49  		PeerAddress:             "192.168.0.1/32",
    50  		PeerPort:                ptr.To[int32](v2alpha1api.DefaultBGPPeerPort),
    51  		EBGPMultihopTTL:         ptr.To[int32](1),
    52  		ConnectRetryTimeSeconds: ptr.To[int32](99),
    53  		HoldTimeSeconds:         ptr.To[int32](9),
    54  		KeepAliveTimeSeconds:    ptr.To[int32](3),
    55  		GracefulRestart: &v2alpha1api.CiliumBGPNeighborGracefulRestart{
    56  			Enabled:            true,
    57  			RestartTimeSeconds: ptr.To[int32](120),
    58  		},
    59  	}
    60  
    61  	// enabled graceful restart - updated restart time
    62  	neighbor64125UpdateGRTimer = &v2alpha1api.CiliumBGPNeighbor{
    63  		PeerASN:                 64125,
    64  		PeerAddress:             "192.168.0.1/32",
    65  		PeerPort:                ptr.To[int32](v2alpha1api.DefaultBGPPeerPort),
    66  		EBGPMultihopTTL:         ptr.To[int32](1),
    67  		ConnectRetryTimeSeconds: ptr.To[int32](99),
    68  		HoldTimeSeconds:         ptr.To[int32](9),
    69  		KeepAliveTimeSeconds:    ptr.To[int32](3),
    70  		GracefulRestart: &v2alpha1api.CiliumBGPNeighborGracefulRestart{
    71  			Enabled:            true,
    72  			RestartTimeSeconds: ptr.To[int32](20),
    73  		},
    74  	}
    75  
    76  	neighbor64126 = &v2alpha1api.CiliumBGPNeighbor{
    77  		PeerASN:                 64126,
    78  		PeerAddress:             "192.168.66.1/32",
    79  		PeerPort:                ptr.To[int32](v2alpha1api.DefaultBGPPeerPort),
    80  		EBGPMultihopTTL:         ptr.To[int32](1),
    81  		ConnectRetryTimeSeconds: ptr.To[int32](99),
    82  		HoldTimeSeconds:         ptr.To[int32](9),
    83  		KeepAliveTimeSeconds:    ptr.To[int32](3),
    84  	}
    85  
    86  	// changed HoldTime & KeepAliveTime
    87  	neighbor64126Update = &v2alpha1api.CiliumBGPNeighbor{
    88  		PeerASN:                 64126,
    89  		PeerAddress:             "192.168.66.1/32",
    90  		PeerPort:                ptr.To[int32](v2alpha1api.DefaultBGPPeerPort),
    91  		EBGPMultihopTTL:         ptr.To[int32](1),
    92  		ConnectRetryTimeSeconds: ptr.To[int32](99),
    93  		HoldTimeSeconds:         ptr.To[int32](12),
    94  		KeepAliveTimeSeconds:    ptr.To[int32](4),
    95  	}
    96  
    97  	neighbor64127 = &v2alpha1api.CiliumBGPNeighbor{
    98  		PeerASN:                 64127,
    99  		PeerAddress:             "192.168.88.1/32",
   100  		EBGPMultihopTTL:         ptr.To[int32](1),
   101  		ConnectRetryTimeSeconds: ptr.To[int32](99),
   102  		HoldTimeSeconds:         ptr.To[int32](9),
   103  		KeepAliveTimeSeconds:    ptr.To[int32](3),
   104  	}
   105  
   106  	// changed EBGPMultihopTTL
   107  	neighbor64127Update = &v2alpha1api.CiliumBGPNeighbor{
   108  		PeerASN:                 64127,
   109  		PeerAddress:             "192.168.88.1/32",
   110  		EBGPMultihopTTL:         ptr.To[int32](10),
   111  		ConnectRetryTimeSeconds: ptr.To[int32](99),
   112  		HoldTimeSeconds:         ptr.To[int32](9),
   113  		KeepAliveTimeSeconds:    ptr.To[int32](3),
   114  	}
   115  
   116  	neighbor64128 = &v2alpha1api.CiliumBGPNeighbor{
   117  		PeerASN:                 64128,
   118  		PeerAddress:             "192.168.77.1/32",
   119  		PeerPort:                ptr.To[int32](v2alpha1api.DefaultBGPPeerPort),
   120  		EBGPMultihopTTL:         ptr.To[int32](1),
   121  		ConnectRetryTimeSeconds: ptr.To[int32](99),
   122  		HoldTimeSeconds:         ptr.To[int32](9),
   123  		KeepAliveTimeSeconds:    ptr.To[int32](3),
   124  	}
   125  )
   126  
   127  // TestGetPeerState confirms the parsing of go bgp ListPeers to cilium modes work as intended
   128  func TestGetPeerState(t *testing.T) {
   129  	var table = []struct {
   130  		// name of the test
   131  		name string
   132  		// neighbors to configure
   133  		neighbors []*v2alpha1api.CiliumBGPNeighbor
   134  		// neighbors to update
   135  		neighborsAfterUpdate []*v2alpha1api.CiliumBGPNeighbor
   136  		// localASN is local autonomous number
   137  		localASN uint32
   138  		// expected error message on AddNeighbor() or empty string for no error
   139  		errStr string
   140  		// expected error message  on UpdateNeighbor() or empty string for no error
   141  		updateErrStr string
   142  	}{
   143  		{
   144  			name:      "test add neighbor",
   145  			neighbors: []*v2alpha1api.CiliumBGPNeighbor{neighbor64125},
   146  			localASN:  64124,
   147  			errStr:    "",
   148  		},
   149  		{
   150  			name: "test add neighbor with port",
   151  			neighbors: []*v2alpha1api.CiliumBGPNeighbor{
   152  				{
   153  					PeerASN:                 64125,
   154  					PeerAddress:             "192.168.0.1/32",
   155  					PeerPort:                ptr.To[int32](175),
   156  					ConnectRetryTimeSeconds: ptr.To[int32](99),
   157  					HoldTimeSeconds:         ptr.To[int32](9),
   158  					KeepAliveTimeSeconds:    ptr.To[int32](3),
   159  				},
   160  			},
   161  			localASN: 64124,
   162  			errStr:   "",
   163  		},
   164  		{
   165  			name: "test add + update neighbors",
   166  			neighbors: []*v2alpha1api.CiliumBGPNeighbor{
   167  				neighbor64125,
   168  				neighbor64126,
   169  				neighbor64127,
   170  				neighbor64128,
   171  			},
   172  			neighborsAfterUpdate: []*v2alpha1api.CiliumBGPNeighbor{
   173  				// changed ConnectRetryTime
   174  				neighbor64125Update,
   175  				// changed HoldTime & KeepAliveTime
   176  				neighbor64126Update,
   177  				// changed EBGPMultihopTTL
   178  				neighbor64127Update,
   179  				// no change
   180  				neighbor64128,
   181  			},
   182  			localASN: 64124,
   183  			errStr:   "",
   184  		},
   185  		{
   186  			name: "test graceful restart - update enable",
   187  			neighbors: []*v2alpha1api.CiliumBGPNeighbor{
   188  				neighbor64125,
   189  			},
   190  			neighborsAfterUpdate: []*v2alpha1api.CiliumBGPNeighbor{
   191  				// enabled GR
   192  				neighbor64125UpdateGR,
   193  			},
   194  			localASN: 64124,
   195  			errStr:   "",
   196  		},
   197  		{
   198  			name: "test graceful restart - update restart time",
   199  			neighbors: []*v2alpha1api.CiliumBGPNeighbor{
   200  				neighbor64125UpdateGR,
   201  			},
   202  			neighborsAfterUpdate: []*v2alpha1api.CiliumBGPNeighbor{
   203  				// changed gr restart time
   204  				neighbor64125UpdateGRTimer,
   205  			},
   206  			localASN: 64124,
   207  			errStr:   "",
   208  		},
   209  		{
   210  			name: "test add invalid neighbor",
   211  			neighbors: []*v2alpha1api.CiliumBGPNeighbor{
   212  				// invalid PeerAddress
   213  				{
   214  					PeerASN:                 64125,
   215  					PeerAddress:             "192.168.0.XYZ",
   216  					ConnectRetryTimeSeconds: ptr.To[int32](101),
   217  					HoldTimeSeconds:         ptr.To[int32](30),
   218  					KeepAliveTimeSeconds:    ptr.To[int32](10),
   219  				},
   220  			},
   221  			localASN: 64124,
   222  			errStr:   "failed to parse PeerAddress: netip.ParsePrefix(\"192.168.0.XYZ\"): no '/'",
   223  		},
   224  		{
   225  			name: "test invalid neighbor update",
   226  			neighbors: []*v2alpha1api.CiliumBGPNeighbor{
   227  				{
   228  					PeerASN:                 64125,
   229  					PeerAddress:             "192.168.0.1/32",
   230  					ConnectRetryTimeSeconds: ptr.To[int32](101),
   231  					HoldTimeSeconds:         ptr.To[int32](30),
   232  					KeepAliveTimeSeconds:    ptr.To[int32](10),
   233  				},
   234  			},
   235  			neighborsAfterUpdate: []*v2alpha1api.CiliumBGPNeighbor{
   236  				// different ASN
   237  				{
   238  					PeerASN:                 64999,
   239  					PeerAddress:             "192.168.0.1/32",
   240  					ConnectRetryTimeSeconds: ptr.To[int32](101),
   241  					HoldTimeSeconds:         ptr.To[int32](30),
   242  					KeepAliveTimeSeconds:    ptr.To[int32](10),
   243  				},
   244  			},
   245  			localASN:     64124,
   246  			errStr:       "",
   247  			updateErrStr: "failed retrieving peer: could not find existing peer with ASN: 64999 and IP: 192.168.0.1",
   248  		},
   249  	}
   250  	for _, tt := range table {
   251  		srvParams := types.ServerParameters{
   252  			Global: types.BGPGlobal{
   253  				ASN:        tt.localASN,
   254  				RouterID:   "127.0.0.1",
   255  				ListenPort: -1,
   256  			},
   257  		}
   258  		t.Run(tt.name, func(t *testing.T) {
   259  			testSC, err := NewGoBGPServer(context.Background(), log, srvParams)
   260  			require.NoError(t, err)
   261  
   262  			t.Cleanup(func() {
   263  				testSC.Stop()
   264  			})
   265  
   266  			// add neighbours
   267  			for _, n := range tt.neighbors {
   268  				n.SetDefaults()
   269  
   270  				err = testSC.AddNeighbor(context.Background(), types.NeighborRequest{
   271  					Neighbor: n,
   272  				})
   273  				if tt.errStr != "" {
   274  					require.EqualError(t, err, tt.errStr)
   275  					return // no more checks
   276  				} else {
   277  					require.NoError(t, err)
   278  				}
   279  			}
   280  
   281  			res, err := testSC.GetPeerState(context.Background())
   282  			require.NoError(t, err)
   283  
   284  			// validate neighbors count
   285  			require.Len(t, res.Peers, len(tt.neighbors))
   286  
   287  			// validate peers
   288  			validatePeers(t, tt.localASN, tt.neighbors, res.Peers)
   289  
   290  			// update neighbours
   291  			for _, n := range tt.neighborsAfterUpdate {
   292  				n.SetDefaults()
   293  				err = testSC.UpdateNeighbor(context.Background(), types.NeighborRequest{
   294  					Neighbor: n,
   295  				})
   296  				if tt.updateErrStr != "" {
   297  					require.EqualError(t, err, tt.updateErrStr)
   298  					return // no more checks
   299  				} else {
   300  					require.NoError(t, err)
   301  				}
   302  			}
   303  
   304  			res, err = testSC.GetPeerState(context.Background())
   305  			require.NoError(t, err)
   306  
   307  			// validate peers
   308  			validatePeers(t, tt.localASN, tt.neighborsAfterUpdate, res.Peers)
   309  		})
   310  	}
   311  }
   312  
   313  // validatePeers validates that peers returned from GoBGP GetPeerState match expected list of CiliumBGPNeighbors
   314  func validatePeers(t *testing.T, localASN uint32, neighbors []*v2alpha1api.CiliumBGPNeighbor, peers []*models.BgpPeer) {
   315  	for _, n := range neighbors {
   316  		p := findMatchingPeer(t, peers, n)
   317  		require.NotNilf(t, p, "no matching peer for PeerASN %d and PeerAddress %s", n.PeerASN, n.PeerAddress)
   318  
   319  		// validate basic data is returned correctly
   320  		require.Equal(t, int64(localASN), p.LocalAsn)
   321  
   322  		expConnectRetry := ptr.Deref[int32](n.ConnectRetryTimeSeconds, v2alpha1api.DefaultBGPConnectRetryTimeSeconds)
   323  		expHoldTime := ptr.Deref[int32](n.HoldTimeSeconds, v2alpha1api.DefaultBGPHoldTimeSeconds)
   324  		expKeepAlive := ptr.Deref[int32](n.KeepAliveTimeSeconds, ptr.Deref[int32](n.KeepAliveTimeSeconds, v2alpha1api.DefaultBGPKeepAliveTimeSeconds))
   325  		require.EqualValues(t, expConnectRetry, p.ConnectRetryTimeSeconds)
   326  		require.EqualValues(t, expHoldTime, p.ConfiguredHoldTimeSeconds)
   327  		require.EqualValues(t, expKeepAlive, p.ConfiguredKeepAliveTimeSeconds)
   328  
   329  		if n.GracefulRestart != nil {
   330  			require.EqualValues(t, n.GracefulRestart.Enabled, p.GracefulRestart.Enabled)
   331  			expGRRestartTime := ptr.Deref[int32](n.GracefulRestart.RestartTimeSeconds, v2alpha1api.DefaultBGPGRRestartTimeSeconds)
   332  			require.EqualValues(t, expGRRestartTime, p.GracefulRestart.RestartTimeSeconds)
   333  		} else {
   334  			require.False(t, p.GracefulRestart.Enabled)
   335  		}
   336  
   337  		if n.EBGPMultihopTTL != nil && *n.EBGPMultihopTTL > 0 {
   338  			require.EqualValues(t, *n.EBGPMultihopTTL, p.EbgpMultihopTTL)
   339  		}
   340  
   341  		// since there is no real neighbor, bgp session state will be either idle or active.
   342  		require.Contains(t, []string{"idle", "active"}, p.SessionState)
   343  	}
   344  }
   345  
   346  // findMatchingPeer finds models.BgpPeer matching to the provided v2alpha1api.CiliumBGPNeighbor based on the peer ASN and IP
   347  func findMatchingPeer(t *testing.T, peers []*models.BgpPeer, n *v2alpha1api.CiliumBGPNeighbor) *models.BgpPeer {
   348  	for _, p := range peers {
   349  		nPrefix, err := netip.ParsePrefix(n.PeerAddress)
   350  		require.NoError(t, err)
   351  		pIP, err := netip.ParseAddr(p.PeerAddress)
   352  		require.NoError(t, err)
   353  
   354  		if p.PeerAsn == int64(n.PeerASN) && pIP.Compare(nPrefix.Addr()) == 0 {
   355  			return p
   356  		}
   357  	}
   358  	return nil
   359  }
   360  
   361  func TestGetRoutes(t *testing.T) {
   362  	testSC, err := NewGoBGPServer(context.Background(), log, types.ServerParameters{
   363  		Global: types.BGPGlobal{
   364  			ASN:        65000,
   365  			RouterID:   "127.0.0.1",
   366  			ListenPort: -1,
   367  		},
   368  	})
   369  	require.NoError(t, err)
   370  
   371  	t.Cleanup(func() {
   372  		testSC.Stop()
   373  	})
   374  
   375  	err = testSC.AddNeighbor(context.TODO(), types.NeighborRequest{
   376  		Neighbor: neighbor64125,
   377  	})
   378  	require.NoError(t, err)
   379  
   380  	_, err = testSC.AdvertisePath(context.TODO(), types.PathRequest{
   381  		Path: types.NewPathForPrefix(netip.MustParsePrefix("10.0.0.0/24")),
   382  	})
   383  	require.NoError(t, err)
   384  
   385  	_, err = testSC.AdvertisePath(context.TODO(), types.PathRequest{
   386  		Path: types.NewPathForPrefix(netip.MustParsePrefix("fd00::/64")),
   387  	})
   388  	require.NoError(t, err)
   389  
   390  	// test IPv4 address family
   391  	res, err := testSC.GetRoutes(context.TODO(), &types.GetRoutesRequest{
   392  		TableType: types.TableTypeLocRIB,
   393  		Family: types.Family{
   394  			Afi:  types.AfiIPv4,
   395  			Safi: types.SafiUnicast,
   396  		},
   397  	})
   398  	require.NoError(t, err)
   399  	require.Equal(t, 1, len(res.Routes))
   400  	require.Equal(t, 1, len(res.Routes[0].Paths))
   401  	require.Equal(t, uint16(bgp.AFI_IP), res.Routes[0].Paths[0].NLRI.AFI())
   402  	require.Equal(t, uint8(bgp.SAFI_UNICAST), res.Routes[0].Paths[0].NLRI.SAFI())
   403  	require.IsType(t, &bgp.IPAddrPrefix{}, res.Routes[0].Paths[0].NLRI)
   404  
   405  	// test IPv6 address family
   406  	res, err = testSC.GetRoutes(context.TODO(), &types.GetRoutesRequest{
   407  		TableType: types.TableTypeLocRIB,
   408  		Family: types.Family{
   409  			Afi:  types.AfiIPv6,
   410  			Safi: types.SafiUnicast,
   411  		},
   412  	})
   413  	require.NoError(t, err)
   414  	require.Equal(t, 1, len(res.Routes))
   415  	require.Equal(t, 1, len(res.Routes[0].Paths))
   416  	require.Equal(t, uint16(bgp.AFI_IP6), res.Routes[0].Paths[0].NLRI.AFI())
   417  	require.Equal(t, uint8(bgp.SAFI_UNICAST), res.Routes[0].Paths[0].NLRI.SAFI())
   418  	require.IsType(t, &bgp.IPv6AddrPrefix{}, res.Routes[0].Paths[0].NLRI)
   419  
   420  	// test adj-rib-out
   421  	res, err = testSC.GetRoutes(context.TODO(), &types.GetRoutesRequest{
   422  		TableType: types.TableTypeAdjRIBOut,
   423  		Family: types.Family{
   424  			Afi:  types.AfiIPv4,
   425  			Safi: types.SafiUnicast,
   426  		},
   427  		Neighbor: netip.MustParsePrefix(neighbor64125.PeerAddress).Addr(),
   428  	})
   429  	require.NoError(t, err)
   430  	require.Equal(t, 0, len(res.Routes)) // adj-rib is empty as there is no actual peering up
   431  
   432  	// test adj-rib-in
   433  	res, err = testSC.GetRoutes(context.TODO(), &types.GetRoutesRequest{
   434  		TableType: types.TableTypeAdjRIBIn,
   435  		Family: types.Family{
   436  			Afi:  types.AfiIPv6,
   437  			Safi: types.SafiUnicast,
   438  		},
   439  		Neighbor: netip.MustParsePrefix(neighbor64125.PeerAddress).Addr(),
   440  	})
   441  	require.NoError(t, err)
   442  	require.Equal(t, 0, len(res.Routes)) // adj-rib is empty as there is no actual peering up
   443  }