mirror of
https://github.com/cloudnativelabs/kube-router.git
synced 2026-05-04 22:26:16 +02:00
164 lines
5.4 KiB
Go
164 lines
5.4 KiB
Go
package bgp
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
|
|
"github.com/osrg/gobgp/v4/pkg/apiutil"
|
|
bgp "github.com/osrg/gobgp/v4/pkg/packet/bgp"
|
|
)
|
|
|
|
// PathBuilder helps construct BGP paths with native GoBGP v4 types.
|
|
// It automatically handles IPv4 vs IPv6 differences including:
|
|
// - Correct Family (AFI_IP/AFI_IP6 + SAFI_UNICAST)
|
|
// - NextHop attribute for IPv4
|
|
// - MpReachNLRI attribute for IPv6
|
|
type PathBuilder struct {
|
|
prefix netip.Prefix
|
|
nextHop netip.Addr
|
|
isIPv6 bool
|
|
withdrawal bool
|
|
family bgp.Family
|
|
nlri bgp.NLRI
|
|
attrs []bgp.PathAttributeInterface
|
|
}
|
|
|
|
// NewPathBuilder creates a new PathBuilder for the given CIDR prefix and next-hop IP.
|
|
// It automatically detects IPv4 vs IPv6 from the CIDR and creates the appropriate
|
|
// BGP path attributes.
|
|
//
|
|
// Example:
|
|
//
|
|
// pb, err := bgp.NewPathBuilder("10.244.1.0/24", "192.168.1.1")
|
|
// pb, err := bgp.NewPathBuilder("2001:db8::/64", "2001:db8::1")
|
|
func NewPathBuilder(cidr string, nextHop string) (*PathBuilder, error) {
|
|
pb := &PathBuilder{}
|
|
|
|
// Parse the CIDR prefix
|
|
prefix, err := netip.ParsePrefix(cidr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse CIDR prefix: %w", err)
|
|
}
|
|
pb.prefix = prefix
|
|
|
|
// Parse the next-hop address
|
|
nextHopAddr, err := netip.ParseAddr(nextHop)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse next hop address: %w", err)
|
|
}
|
|
pb.nextHop = nextHopAddr
|
|
|
|
// Detect IPv4 vs IPv6 from the prefix
|
|
pb.isIPv6 = prefix.Addr().Is6()
|
|
|
|
// Create NLRI using native BGP types
|
|
pb.nlri, err = bgp.NewIPAddrPrefix(prefix)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create IP address prefix: %w", err)
|
|
}
|
|
|
|
// Create path attributes based on IP version
|
|
if pb.isIPv6 {
|
|
// IPv6: Use MpReachNLRI attribute
|
|
pb.family = bgp.NewFamily(bgp.AFI_IP6, bgp.SAFI_UNICAST)
|
|
|
|
originAttr := bgp.NewPathAttributeOrigin(0) // IGP origin
|
|
mpReachAttr, err := bgp.NewPathAttributeMpReachNLRI(
|
|
pb.family,
|
|
[]bgp.PathNLRI{{NLRI: pb.nlri}},
|
|
nextHopAddr,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create MP reach NLRI attribute: %w", err)
|
|
}
|
|
|
|
pb.attrs = []bgp.PathAttributeInterface{originAttr, mpReachAttr}
|
|
} else {
|
|
// IPv4: Use standard NextHop attribute
|
|
pb.family = bgp.NewFamily(bgp.AFI_IP, bgp.SAFI_UNICAST)
|
|
|
|
originAttr := bgp.NewPathAttributeOrigin(0) // IGP origin
|
|
nextHopAttr, err := bgp.NewPathAttributeNextHop(nextHopAddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create next hop attribute: %w", err)
|
|
}
|
|
|
|
pb.attrs = []bgp.PathAttributeInterface{originAttr, nextHopAttr}
|
|
}
|
|
|
|
return pb, nil
|
|
}
|
|
|
|
// WithWithdrawal marks this path as a withdrawal.
|
|
// Returns the builder for method chaining.
|
|
func (pb *PathBuilder) WithWithdrawal() *PathBuilder {
|
|
pb.withdrawal = true
|
|
return pb
|
|
}
|
|
|
|
// Build creates the final apiutil.Path ready to be sent to GoBGP.
|
|
func (pb *PathBuilder) Build() (*apiutil.Path, error) {
|
|
return &apiutil.Path{
|
|
Family: pb.family,
|
|
Nlri: pb.nlri,
|
|
Attrs: pb.attrs,
|
|
Withdrawal: pb.withdrawal,
|
|
}, nil
|
|
}
|
|
|
|
// ParseNextHop takes in a GoBGP Path and parses out the destination's next hop from its attributes. If it
|
|
// can't parse a next hop IP from the GoBGP Path, it returns an error.
|
|
func ParseNextHop(path *apiutil.Path) (net.IP, error) {
|
|
// In v4, path attributes are already native Go types (not protobuf)
|
|
for _, attr := range path.Attrs {
|
|
switch t := attr.(type) {
|
|
case *bgp.PathAttributeNextHop:
|
|
// This is the primary way that we receive NextHops and happens when both the client and the server exchange
|
|
// next hops on the same IP family that they negotiated BGP on
|
|
nextHopIP := net.IP(t.Value.AsSlice())
|
|
if nextHopIP != nil && (nextHopIP.To4() != nil || nextHopIP.To16() != nil) {
|
|
return nextHopIP, nil
|
|
}
|
|
return nil, fmt.Errorf("invalid nextHop address: %s", t.Value.String())
|
|
case *bgp.PathAttributeMpReachNLRI:
|
|
// in the case where the server and the client are exchanging next-hops that don't relate to their primary
|
|
// IP family, we get MpReachNLRIAttribute instead of NextHopAttributes
|
|
// TODO: here we only take the first next hop, at some point in the future it would probably be best to
|
|
// consider multiple next hops
|
|
nextHopIP := net.IP(t.Nexthop.AsSlice())
|
|
if nextHopIP != nil && (nextHopIP.To4() != nil || nextHopIP.To16() != nil) {
|
|
return nextHopIP, nil
|
|
}
|
|
return nil, fmt.Errorf("invalid nextHop address: %s", t.Nexthop.String())
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("could not parse next hop received from GoBGP for path: NLRI=%s, Family=%s",
|
|
path.Nlri.String(), path.Family.String())
|
|
}
|
|
|
|
// ParsePath takes in a GoBGP Path and parses out the destination subnet and the next hop from its attributes.
|
|
// If successful, it will return the destination of the BGP path as a subnet form and the next hop. If it
|
|
// can't parse the destination or the next hop IP, it returns an error.
|
|
func ParsePath(path *apiutil.Path) (*net.IPNet, net.IP, error) {
|
|
nextHop, err := ParseNextHop(path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// In v4, NLRI is already a native Go type (not protobuf)
|
|
// Type assert to IPAddrPrefix to extract the prefix
|
|
ipPrefix, ok := path.Nlri.(*bgp.IPAddrPrefix)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("NLRI is not an IPAddrPrefix: %T", path.Nlri)
|
|
}
|
|
|
|
// Convert the prefix to a net.IPNet
|
|
_, dst, err := net.ParseCIDR(ipPrefix.Prefix.String())
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to parse destination network: %w", err)
|
|
}
|
|
|
|
return dst, nextHop, nil
|
|
}
|