Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 111 additions & 15 deletions plugins/meta/sbr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/alexflint/go-filemutex"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"

"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
Expand All @@ -49,6 +50,16 @@ type PluginConf struct {

// Add plugin-specific flags here
Table *int `json:"table,omitempty"`
// Gateways allows specifying static/hardcoded gateway IP addresses
// If set, these will be used instead of the gateway from prevResult
// Supports dual-stack: provide one IPv4 and/or one IPv6 gateway
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if user provides

 ["192.168.1.1", "192.168.1.254"]:

(probably a validation that gateways is of size 2 max, one is ipv4, one is ipv6)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catch. Added these checks on lines 254 - 274.

// Note: Currently applies the same gateway to all IPs of the same family.
// Per-subnet gateway mapping is not yet supported.
Gateways []string `json:"gateways,omitempty"`
// AddSourceHints adds source IP hints to subnet routes in the main table,
// enabling destination-based routing when no explicit source IP is specified.
// This allows both source-based and destination-based routing to work together.
AddSourceHints bool `json:"addSourceHints,omitempty"`
}

// Wrapper that does a lock before and unlock after operations to serialise
Expand Down Expand Up @@ -168,7 +179,7 @@ func cmdAdd(args *skel.CmdArgs) error {
if conf.Table != nil {
return doRoutesWithTable(ipCfgs, *conf.Table)
}
return doRoutes(ipCfgs, args.IfName)
return doRoutes(ipCfgs, args.IfName, conf.Gateways, conf.AddSourceHints)
})
if err != nil {
return err
Expand Down Expand Up @@ -207,7 +218,7 @@ func getNextTableID(rules []netlink.Rule, routes []netlink.Route, candidateID in
}

// doRoutes does all the work to set up routes and rules during an add.
func doRoutes(ipCfgs []*current.IPConfig, iface string) error {
func doRoutes(ipCfgs []*current.IPConfig, iface string, staticGateways []string, addSourceHints bool) error {
// Get a list of rules and routes ready.
rules, err := netlinksafe.RuleList(netlink.FAMILY_ALL)
if err != nil {
Expand Down Expand Up @@ -238,6 +249,33 @@ func doRoutes(ipCfgs []*current.IPConfig, iface string) error {
return fmt.Errorf("Unable to list routes: %v", err)
}

// Parse static gateways if provided (supports dual-stack: one IPv4, one IPv6)
var staticGwV4, staticGwV6 net.IP
if len(staticGateways) > 2 {
return fmt.Errorf("gateways list cannot contain more than 2 entries (one IPv4, one IPv6)")
}
for _, gw := range staticGateways {
ip := net.ParseIP(gw)
if ip == nil {
return fmt.Errorf("invalid static gateway IP address: %s", gw)
}
if ip.To4() != nil {
if staticGwV4 != nil {
// We already have an IPv4 static gateway
return fmt.Errorf("gateways list contains multiple IPv4 addresses: %s and %s", staticGwV4, gw)
}
staticGwV4 = ip
log.Printf("Using static IPv4 gateway: %s", ip)
} else {
if staticGwV6 != nil {
// We already have an IPv6 static gateway
return fmt.Errorf("gateways list contains multiple IPv6 addresses: %s and %s", staticGwV6, gw)
}
staticGwV6 = ip
log.Printf("Using static IPv6 gateway: %s", ip)
}
}

// Loop through setting up source based rules and default routes.
for _, ipCfg := range ipCfgs {
log.Printf("Set rule for source %s", ipCfg.String())
Expand All @@ -260,10 +298,24 @@ func doRoutes(ipCfgs []*current.IPConfig, iface string) error {
return fmt.Errorf("Failed to add rule: %v", err)
}

// Determine which gateway to use: static gateway takes precedence
// Match gateway by IP family (IPv4 vs IPv6)
var gateway net.IP
isIPv4 := ipCfg.Address.IP.To4() != nil

switch {
case isIPv4 && staticGwV4 != nil:
gateway = staticGwV4
case !isIPv4 && staticGwV6 != nil:
gateway = staticGwV6
case ipCfg.Gateway != nil:
gateway = ipCfg.Gateway
}

// Add a default route, since this may have been removed by previous
// plugin.
if ipCfg.Gateway != nil {
log.Printf("Adding default route to gateway %s", ipCfg.Gateway.String())
if gateway != nil {
log.Printf("Adding default route to gateway %s", gateway.String())

var dest net.IPNet
if ipCfg.Address.IP.To4() != nil {
Expand All @@ -276,15 +328,15 @@ func doRoutes(ipCfgs []*current.IPConfig, iface string) error {

route := netlink.Route{
Dst: &dest,
Gw: ipCfg.Gateway,
Gw: gateway,
Table: table,
LinkIndex: linkIndex,
}

err = netlink.RouteAdd(&route)
if err != nil {
return fmt.Errorf("Failed to add default route to %s: %v",
ipCfg.Gateway.String(),
gateway.String(),
err)
}
}
Expand Down Expand Up @@ -320,15 +372,59 @@ func doRoutes(ipCfgs []*current.IPConfig, iface string) error {
table = getNextTableID(rules, routes, table)
}

// Delete all the interface routes in the default routing table, which were
// copied to source based routing tables.
// Not deleting them while copying to accommodate for multiple ipCfgs from
// the same subnet. Else, (error for network is unreachable while adding gateway)
for _, route := range routes {
log.Printf("Deleting route %s from table %d", route.String(), route.Table)
err := netlink.RouteDel(&route)
if err != nil {
return fmt.Errorf("Failed to delete route: %v", err)
// Handle routes in the default routing table
if addSourceHints {
// Keep or re-add subnet routes in main table with source IP hints
// for destination-based routing when no explicit source IP is specified
log.Printf("Adding source IP hints to subnet routes in main table")

for _, ipCfg := range ipCfgs {
// Find the subnet route for this IP
subnet := &net.IPNet{
IP: ipCfg.Address.IP.Mask(ipCfg.Address.Mask),
Mask: ipCfg.Address.Mask,
}

log.Printf("Adding/replacing route for subnet %s with src hint %s in main table",
subnet.String(), ipCfg.Address.IP.String())

// Add route to main table with source IP hint
route := netlink.Route{
LinkIndex: linkIndex,
Dst: subnet,
Src: ipCfg.Address.IP,
Table: unix.RT_TABLE_MAIN,
Scope: netlink.SCOPE_LINK,
}

// Use RouteReplace to update if it exists
err := netlink.RouteReplace(&route)
if err != nil {
log.Printf("Warning: Failed to add subnet route to main table: %v", err)
// Don't fail completely, just warn
}
}

// Delete non-subnet routes from main table (gateway routes, etc.)
for _, route := range routes {
// Skip subnet routes (scope link), only delete other routes
if route.Scope != netlink.SCOPE_LINK {
log.Printf("Deleting non-subnet route %s from table %d", route.String(), route.Table)
err := netlink.RouteDel(&route)
if err != nil {
log.Printf("Warning: Failed to delete route: %v", err)
}
}
}
} else {
// Not deleting them while copying to accommodate for multiple ipCfgs from
// the same subnet. Else, (error for network is unreachable while adding gateway)
for _, route := range routes {
log.Printf("Deleting route %s from table %d", route.String(), route.Table)
err := netlink.RouteDel(&route)
if err != nil {
return fmt.Errorf("Failed to delete route: %v", err)
}
}
}

Expand Down
Loading
Loading