Skip to content

Commit c9b75b4

Browse files
committed
fact(bgp): separate bgp functionality into new package
1 parent 374e832 commit c9b75b4

File tree

6 files changed

+178
-151
lines changed

6 files changed

+178
-151
lines changed

pkg/bgp/id.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package bgp
2+
3+
import (
4+
"encoding/binary"
5+
"errors"
6+
"fmt"
7+
"hash/fnv"
8+
"net"
9+
"regexp"
10+
"strconv"
11+
12+
"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
13+
gobgp "github.com/osrg/gobgp/v3/pkg/packet/bgp"
14+
)
15+
16+
const (
17+
CommunityMaxSize = 32
18+
CommunityMaxPartSize = 16
19+
)
20+
21+
// GenerateRouterID will generate a router ID based upon the user's configuration (or lack there of) and the node's
22+
// primary IP address if the user has not specified. If the user has configured the router ID as "generate" then we
23+
// will generate a router ID based upon fnv hashing the node's primary IP address.
24+
func GenerateRouterID(nodeIPAware utils.NodeIPAware, configRouterID string) (string, error) {
25+
switch {
26+
case configRouterID == "generate":
27+
h := fnv.New32a()
28+
h.Write(nodeIPAware.GetPrimaryNodeIP())
29+
hs := h.Sum32()
30+
gip := make(net.IP, 4)
31+
binary.BigEndian.PutUint32(gip, hs)
32+
return gip.String(), nil
33+
case configRouterID != "":
34+
return configRouterID, nil
35+
}
36+
37+
if nodeIPAware.GetPrimaryNodeIP().To4() == nil {
38+
return "", errors.New("router-id must be specified when primary node IP is an IPv6 address")
39+
}
40+
return configRouterID, nil
41+
}
42+
43+
// ValidateCommunity takes in a string and attempts to parse a BGP community out of it in a way that is similar to
44+
// gobgp (internal/pkg/table/policy.go:ParseCommunity()). If it is not able to parse the community information it
45+
// returns an error.
46+
func ValidateCommunity(arg string) error {
47+
_, err := strconv.ParseUint(arg, 10, CommunityMaxSize)
48+
if err == nil {
49+
return nil
50+
}
51+
52+
_regexpCommunity := regexp.MustCompile(`(\d+):(\d+)`)
53+
elems := _regexpCommunity.FindStringSubmatch(arg)
54+
if len(elems) == 3 {
55+
if _, err := strconv.ParseUint(elems[1], 10, CommunityMaxPartSize); err == nil {
56+
if _, err = strconv.ParseUint(elems[2], 10, CommunityMaxPartSize); err == nil {
57+
return nil
58+
}
59+
}
60+
}
61+
for _, v := range gobgp.WellKnownCommunityNameMap {
62+
if arg == v {
63+
return nil
64+
}
65+
}
66+
return fmt.Errorf("failed to parse %s as community", arg)
67+
}

pkg/bgp/id_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package bgp
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func Test_ValidateCommunity(t *testing.T) {
10+
t.Run("BGP community specified as a 32-bit integer should pass validation", func(t *testing.T) {
11+
assert.Nil(t, ValidateCommunity("4294967041"))
12+
assert.Nil(t, ValidateCommunity("4294967295"))
13+
})
14+
t.Run("BGP community specified as 2 16-bit integers should pass validation", func(t *testing.T) {
15+
assert.Nil(t, ValidateCommunity("65535:65281"))
16+
assert.Nil(t, ValidateCommunity("65535:65535"))
17+
})
18+
t.Run("Well known BGP communities passed as a string should pass validation", func(t *testing.T) {
19+
assert.Nil(t, ValidateCommunity("no-export"))
20+
assert.Nil(t, ValidateCommunity("internet"))
21+
assert.Nil(t, ValidateCommunity("planned-shut"))
22+
assert.Nil(t, ValidateCommunity("accept-own"))
23+
assert.Nil(t, ValidateCommunity("blackhole"))
24+
assert.Nil(t, ValidateCommunity("no-advertise"))
25+
assert.Nil(t, ValidateCommunity("no-peer"))
26+
})
27+
t.Run("BGP community that is greater than 32-bit integer should fail validation", func(t *testing.T) {
28+
assert.Error(t, ValidateCommunity("4294967296"))
29+
})
30+
t.Run("BGP community that is greater than 2 16-bit integers should fail validation", func(t *testing.T) {
31+
assert.Error(t, ValidateCommunity("65536:65535"))
32+
assert.Error(t, ValidateCommunity("65535:65536"))
33+
assert.Error(t, ValidateCommunity("65536:65536"))
34+
})
35+
t.Run("BGP community that is not a number should fail validation", func(t *testing.T) {
36+
assert.Error(t, ValidateCommunity("0xFFFFFFFF"))
37+
assert.Error(t, ValidateCommunity("community"))
38+
})
39+
}

pkg/bgp/parse.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package bgp
2+
3+
import (
4+
"fmt"
5+
"net"
6+
7+
gobgpapi "github.com/osrg/gobgp/v3/api"
8+
"github.com/vishvananda/netlink"
9+
)
10+
11+
// ParseNextHop takes in a GoBGP Path and parses out the destination's next hop from its attributes. If it
12+
// can't parse a next hop IP from the GoBGP Path, it returns an error.
13+
func ParseNextHop(path *gobgpapi.Path) (net.IP, error) {
14+
for _, pAttr := range path.GetPattrs() {
15+
unmarshalNew, err := pAttr.UnmarshalNew()
16+
if err != nil {
17+
return nil, fmt.Errorf("failed to unmarshal path attribute: %s", err)
18+
}
19+
switch t := unmarshalNew.(type) {
20+
case *gobgpapi.NextHopAttribute:
21+
// This is the primary way that we receive NextHops and happens when both the client and the server exchange
22+
// next hops on the same IP family that they negotiated BGP on
23+
nextHopIP := net.ParseIP(t.NextHop)
24+
if nextHopIP != nil && (nextHopIP.To4() != nil || nextHopIP.To16() != nil) {
25+
return nextHopIP, nil
26+
}
27+
return nil, fmt.Errorf("invalid nextHop address: %s", t.NextHop)
28+
case *gobgpapi.MpReachNLRIAttribute:
29+
// in the case where the server and the client are exchanging next-hops that don't relate to their primary
30+
// IP family, we get MpReachNLRIAttribute instead of NextHopAttributes
31+
// TODO: here we only take the first next hop, at some point in the future it would probably be best to
32+
// consider multiple next hops
33+
nextHopIP := net.ParseIP(t.NextHops[0])
34+
if nextHopIP != nil && (nextHopIP.To4() != nil || nextHopIP.To16() != nil) {
35+
return nextHopIP, nil
36+
}
37+
return nil, fmt.Errorf("invalid nextHop address: %s", t.NextHops[0])
38+
}
39+
}
40+
return nil, fmt.Errorf("could not parse next hop received from GoBGP for path: %s", path)
41+
}
42+
43+
// ParsePath takes in a GoBGP Path and parses out the destination subnet and the next hop from its attributes.
44+
// If successful, it will return the destination of the BGP path as a subnet form and the next hop. If it
45+
// can't parse the destination or the next hop IP, it returns an error.
46+
func ParsePath(path *gobgpapi.Path) (*net.IPNet, net.IP, error) {
47+
nextHop, err := ParseNextHop(path)
48+
if err != nil {
49+
return nil, nil, err
50+
}
51+
52+
nlri := path.GetNlri()
53+
var prefix gobgpapi.IPAddressPrefix
54+
err = nlri.UnmarshalTo(&prefix)
55+
if err != nil {
56+
return nil, nil, fmt.Errorf("invalid nlri in advertised path")
57+
}
58+
dstSubnet, err := netlink.ParseIPNet(prefix.Prefix + "/" + fmt.Sprint(prefix.PrefixLen))
59+
if err != nil {
60+
return nil, nil, fmt.Errorf("couldn't parse IP subnet from nlri advertised path")
61+
}
62+
return dstSubnet, nextHop, nil
63+
}

pkg/controllers/routing/network_routes_controller.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"google.golang.org/protobuf/types/known/anypb"
1616

17+
"github.com/cloudnativelabs/kube-router/v2/pkg/bgp"
1718
"github.com/cloudnativelabs/kube-router/v2/pkg/healthcheck"
1819
"github.com/cloudnativelabs/kube-router/v2/pkg/metrics"
1920
"github.com/cloudnativelabs/kube-router/v2/pkg/options"
@@ -62,12 +63,10 @@ const (
6263
ClusterIPST = "ClusterIP"
6364
NodePortST = "NodePort"
6465

65-
prependPathMaxBits = 8
66-
asnMaxBitSize = 32
67-
bgpCommunityMaxSize = 32
68-
bgpCommunityMaxPartSize = 16
69-
routeReflectorMaxID = 32
70-
ipv4MaskMinBits = 32
66+
prependPathMaxBits = 8
67+
asnMaxBitSize = 32
68+
routeReflectorMaxID = 32
69+
ipv4MaskMinBits = 32
7170
)
7271

7372
// NetworkRoutingController is struct to hold necessary information required by controller
@@ -567,7 +566,7 @@ func (nrc *NetworkRoutingController) injectRoute(path *gobgpapi.Path) error {
567566
var route *netlink.Route
568567
var link netlink.Link
569568

570-
dst, nextHop, err := parseBGPPath(path)
569+
dst, nextHop, err := bgp.ParsePath(path)
571570
if err != nil {
572571
return err
573572
}
@@ -965,7 +964,7 @@ func (nrc *NetworkRoutingController) startBgpServer(grpcServer bool) error {
965964
} else {
966965
nodeCommunities = stringToSlice(nodeBGPCommunitiesAnnotation, ",")
967966
for _, nodeCommunity := range nodeCommunities {
968-
if err = validateCommunity(nodeCommunity); err != nil {
967+
if err = bgp.ValidateCommunity(nodeCommunity); err != nil {
969968
klog.Warningf("cannot add BGP community '%s' from node annotation as it does not appear "+
970969
"to be a valid community identifier", nodeCommunity)
971970
continue
@@ -1302,7 +1301,7 @@ func NewNetworkRoutingController(clientset kubernetes.Interface,
13021301
}
13031302
}
13041303

1305-
nrc.routerID, err = generateRouterID(nrc.krNode, kubeRouterConfig.RouterID)
1304+
nrc.routerID, err = bgp.GenerateRouterID(nrc.krNode, kubeRouterConfig.RouterID)
13061305
if err != nil {
13071306
return nil, err
13081307
}

pkg/controllers/routing/utils.go

Lines changed: 1 addition & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,15 @@ package routing
22

33
import (
44
"encoding/base64"
5-
"encoding/binary"
6-
"errors"
75
"fmt"
8-
"hash/fnv"
96
"net"
10-
"regexp"
117
"strconv"
128
"strings"
139

14-
"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
1510
gobgpapi "github.com/osrg/gobgp/v3/api"
16-
"github.com/osrg/gobgp/v3/pkg/packet/bgp"
11+
1712
v1core "k8s.io/api/core/v1"
1813
"k8s.io/klog/v2"
19-
20-
"github.com/vishvananda/netlink"
2114
)
2215

2316
// Used for processing Annotations that may contain multiple items
@@ -117,108 +110,6 @@ func statementsEqualByName(a, b []*gobgpapi.Statement) bool {
117110
return true
118111
}
119112

120-
// generateRouterID will generate a router ID based upon the user's configuration (or lack there of) and the node's
121-
// primary IP address if the user has not specified. If the user has configured the router ID as "generate" then we
122-
// will generate a router ID based upon fnv hashing the node's primary IP address.
123-
func generateRouterID(nodeIPAware utils.NodeIPAware, configRouterID string) (string, error) {
124-
switch {
125-
case configRouterID == "generate":
126-
h := fnv.New32a()
127-
h.Write(nodeIPAware.GetPrimaryNodeIP())
128-
hs := h.Sum32()
129-
gip := make(net.IP, 4)
130-
binary.BigEndian.PutUint32(gip, hs)
131-
return gip.String(), nil
132-
case configRouterID != "":
133-
return configRouterID, nil
134-
}
135-
136-
if nodeIPAware.GetPrimaryNodeIP().To4() == nil {
137-
return "", errors.New("router-id must be specified when primary node IP is an IPv6 address")
138-
}
139-
return configRouterID, nil
140-
}
141-
142-
// validateCommunity takes in a string and attempts to parse a BGP community out of it in a way that is similar to
143-
// gobgp (internal/pkg/table/policy.go:ParseCommunity()). If it is not able to parse the community information it
144-
// returns an error.
145-
func validateCommunity(arg string) error {
146-
_, err := strconv.ParseUint(arg, 10, bgpCommunityMaxSize)
147-
if err == nil {
148-
return nil
149-
}
150-
151-
_regexpCommunity := regexp.MustCompile(`(\d+):(\d+)`)
152-
elems := _regexpCommunity.FindStringSubmatch(arg)
153-
if len(elems) == 3 {
154-
if _, err := strconv.ParseUint(elems[1], 10, bgpCommunityMaxPartSize); err == nil {
155-
if _, err = strconv.ParseUint(elems[2], 10, bgpCommunityMaxPartSize); err == nil {
156-
return nil
157-
}
158-
}
159-
}
160-
for _, v := range bgp.WellKnownCommunityNameMap {
161-
if arg == v {
162-
return nil
163-
}
164-
}
165-
return fmt.Errorf("failed to parse %s as community", arg)
166-
}
167-
168-
// parseBGPNextHop takes in a GoBGP Path and parses out the destination's next hop from its attributes. If it
169-
// can't parse a next hop IP from the GoBGP Path, it returns an error.
170-
func parseBGPNextHop(path *gobgpapi.Path) (net.IP, error) {
171-
for _, pAttr := range path.GetPattrs() {
172-
unmarshalNew, err := pAttr.UnmarshalNew()
173-
if err != nil {
174-
return nil, fmt.Errorf("failed to unmarshal path attribute: %s", err)
175-
}
176-
switch t := unmarshalNew.(type) {
177-
case *gobgpapi.NextHopAttribute:
178-
// This is the primary way that we receive NextHops and happens when both the client and the server exchange
179-
// next hops on the same IP family that they negotiated BGP on
180-
nextHopIP := net.ParseIP(t.NextHop)
181-
if nextHopIP != nil && (nextHopIP.To4() != nil || nextHopIP.To16() != nil) {
182-
return nextHopIP, nil
183-
}
184-
return nil, fmt.Errorf("invalid nextHop address: %s", t.NextHop)
185-
case *gobgpapi.MpReachNLRIAttribute:
186-
// in the case where the server and the client are exchanging next-hops that don't relate to their primary
187-
// IP family, we get MpReachNLRIAttribute instead of NextHopAttributes
188-
// TODO: here we only take the first next hop, at some point in the future it would probably be best to
189-
// consider multiple next hops
190-
nextHopIP := net.ParseIP(t.NextHops[0])
191-
if nextHopIP != nil && (nextHopIP.To4() != nil || nextHopIP.To16() != nil) {
192-
return nextHopIP, nil
193-
}
194-
return nil, fmt.Errorf("invalid nextHop address: %s", t.NextHops[0])
195-
}
196-
}
197-
return nil, fmt.Errorf("could not parse next hop received from GoBGP for path: %s", path)
198-
}
199-
200-
// parseBGPPath takes in a GoBGP Path and parses out the destination subnet and the next hop from its attributes.
201-
// If successful, it will return the destination of the BGP path as a subnet form and the next hop. If it
202-
// can't parse the destination or the next hop IP, it returns an error.
203-
func parseBGPPath(path *gobgpapi.Path) (*net.IPNet, net.IP, error) {
204-
nextHop, err := parseBGPNextHop(path)
205-
if err != nil {
206-
return nil, nil, err
207-
}
208-
209-
nlri := path.GetNlri()
210-
var prefix gobgpapi.IPAddressPrefix
211-
err = nlri.UnmarshalTo(&prefix)
212-
if err != nil {
213-
return nil, nil, fmt.Errorf("invalid nlri in advertised path")
214-
}
215-
dstSubnet, err := netlink.ParseIPNet(prefix.Prefix + "/" + fmt.Sprint(prefix.PrefixLen))
216-
if err != nil {
217-
return nil, nil, fmt.Errorf("couldn't parse IP subnet from nlri advertised path")
218-
}
219-
return dstSubnet, nextHop, nil
220-
}
221-
222113
// getPodCIDRsFromAllNodeSources gets the pod CIDRs for all available sources on a given node in a specific order. The
223114
// order of preference is:
224115
// 1. From the kube-router.io/pod-cidr annotation (preserves backwards compatibility)

pkg/controllers/routing/utils_test.go

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,35 +50,3 @@ func Test_stringSliceToIPNets(t *testing.T) {
5050
assert.Nil(t, ips)
5151
})
5252
}
53-
54-
func Test_validateCommunity(t *testing.T) {
55-
t.Run("BGP community specified as a 32-bit integer should pass validation", func(t *testing.T) {
56-
assert.Nil(t, validateCommunity("4294967041"))
57-
assert.Nil(t, validateCommunity("4294967295"))
58-
})
59-
t.Run("BGP community specified as 2 16-bit integers should pass validation", func(t *testing.T) {
60-
assert.Nil(t, validateCommunity("65535:65281"))
61-
assert.Nil(t, validateCommunity("65535:65535"))
62-
})
63-
t.Run("Well known BGP communities passed as a string should pass validation", func(t *testing.T) {
64-
assert.Nil(t, validateCommunity("no-export"))
65-
assert.Nil(t, validateCommunity("internet"))
66-
assert.Nil(t, validateCommunity("planned-shut"))
67-
assert.Nil(t, validateCommunity("accept-own"))
68-
assert.Nil(t, validateCommunity("blackhole"))
69-
assert.Nil(t, validateCommunity("no-advertise"))
70-
assert.Nil(t, validateCommunity("no-peer"))
71-
})
72-
t.Run("BGP community that is greater than 32-bit integer should fail validation", func(t *testing.T) {
73-
assert.Error(t, validateCommunity("4294967296"))
74-
})
75-
t.Run("BGP community that is greater than 2 16-bit integers should fail validation", func(t *testing.T) {
76-
assert.Error(t, validateCommunity("65536:65535"))
77-
assert.Error(t, validateCommunity("65535:65536"))
78-
assert.Error(t, validateCommunity("65536:65536"))
79-
})
80-
t.Run("BGP community that is not a number should fail validation", func(t *testing.T) {
81-
assert.Error(t, validateCommunity("0xFFFFFFFF"))
82-
assert.Error(t, validateCommunity("community"))
83-
})
84-
}

0 commit comments

Comments
 (0)