kube-router/pkg/bgp/path.go
2026-02-13 14:31:16 -08:00

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
}