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
19 changes: 17 additions & 2 deletions plugins/ipam/dhcp/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,26 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {

d.setLease(clientID, l)

suppressGW, err := parseSuppress(conf.IPAM.Suppress)
if err != nil {
return err
}

gw := l.Gateway()
routes := l.Routes()
if suppressGW {
// Clear gateway and drop default routes so main plugins do not install
// a default route from this attachment. Keep any non-default routes
// (e.g. classless static routes / option 121).
gw = nil
routes = filterDefaultRoutes(routes)
}

result.IPs = []*current.IPConfig{{
Address: *ipn,
Gateway: l.Gateway(),
Gateway: gw,
}}
result.Routes = l.Routes()
result.Routes = routes
if conf.IPAM.Priority != 0 {
for _, r := range result.Routes {
r.Priority = conf.IPAM.Priority
Expand Down
15 changes: 15 additions & 0 deletions plugins/ipam/dhcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ type NetConf struct {
IPAM *IPAMConfig `json:"ipam"`
}

// Supported values for IPAMConfig.Suppress.
const (
// suppressGateway omits the DHCP-provided default gateway from the CNI
// result (IPConfig.Gateway and any 0.0.0.0/0 / ::/0 routes). Non-default
// routes from the lease are still returned. Useful with Multus so a
// secondary interface does not overwrite the pod default route.
suppressGateway = "gateway"
)

type IPAMConfig struct {
types.IPAM
DaemonSocketPath string `json:"daemonSocketPath"`
Expand All @@ -53,6 +62,12 @@ type IPAMConfig struct {
RequestOptions []RequestOption `json:"request"`
// The metric of routes
Priority int `json:"priority,omitempty"`
// Suppress is a list of result fields to omit from the CNI result even if
// the DHCP server provided them. Currently supported: "gateway".
// Note that skipDefault only controls which options are requested; some
// servers still send a router option unsolicited. Use suppress: ["gateway"]
// to ignore it in the result.
Suppress []string `json:"suppress,omitempty"`
}

// DHCPOption represents a DHCP option. It can be a number, or a string defined in manual dhcp-options(5).
Expand Down
35 changes: 35 additions & 0 deletions plugins/ipam/dhcp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,41 @@ import (
"github.com/containernetworking/cni/pkg/types"
)

// parseSuppress validates the suppress list and reports which known items are set.
func parseSuppress(items []string) (bool, error) {
gateway := false
for _, item := range items {
switch item {
case suppressGateway:
gateway = true
default:
return false, fmt.Errorf("unknown suppress value %q (supported: %q)", item, suppressGateway)
}
}
return gateway, nil
}

// isDefaultRoute reports whether dst is a default route (prefix length 0).
func isDefaultRoute(dst net.IPNet) bool {
ones, bits := dst.Mask.Size()
return bits != 0 && ones == 0
}

// filterDefaultRoutes returns a copy of routes without default routes.
func filterDefaultRoutes(routes []*types.Route) []*types.Route {
if len(routes) == 0 {
return routes
}
out := make([]*types.Route, 0, len(routes))
for _, r := range routes {
if r == nil || isDefaultRoute(r.Dst) {
continue
}
out = append(out, r)
}
return out
}

var optionNameToID = map[string]dhcp4.OptionCode{
"dhcp-client-identifier": dhcp4.OptionClientIdentifier,
"subnet-mask": dhcp4.OptionSubnetMask,
Expand Down
82 changes: 82 additions & 0 deletions plugins/ipam/dhcp/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,85 @@ func TestParseOptionName(t *testing.T) {
})
}
}

func TestParseSuppress(t *testing.T) {
tests := []struct {
name string
items []string
wantGateway bool
wantErr bool
}{
{name: "nil", items: nil, wantGateway: false},
{name: "empty", items: []string{}, wantGateway: false},
{name: "gateway", items: []string{"gateway"}, wantGateway: true},
{name: "unknown", items: []string{"routes"}, wantErr: true},
{name: "gateway and unknown", items: []string{"gateway", "nope"}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseSuppress(tt.items)
if (err != nil) != tt.wantErr {
t.Fatalf("parseSuppress() error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.wantGateway {
t.Errorf("parseSuppress() = %v, want %v", got, tt.wantGateway)
}
})
}
}

func TestIsDefaultRoute(t *testing.T) {
_, def4, err := net.ParseCIDR("0.0.0.0/0")
if err != nil {
t.Fatal(err)
}
_, def6, err := net.ParseCIDR("::/0")
if err != nil {
t.Fatal(err)
}
_, nonDef, err := net.ParseCIDR("10.0.0.0/8")
if err != nil {
t.Fatal(err)
}

if !isDefaultRoute(*def4) {
t.Errorf("expected 0.0.0.0/0 to be default")
}
if !isDefaultRoute(*def6) {
t.Errorf("expected ::/0 to be default")
}
if isDefaultRoute(*nonDef) {
t.Errorf("expected 10.0.0.0/8 not to be default")
}
}

func TestFilterDefaultRoutes(t *testing.T) {
_, def4, err := net.ParseCIDR("0.0.0.0/0")
if err != nil {
t.Fatal(err)
}
_, lan, err := net.ParseCIDR("10.0.0.0/8")
if err != nil {
t.Fatal(err)
}

routes := []*types.Route{
{Dst: *def4, GW: net.IPv4(192, 168, 1, 1)},
{Dst: *lan, GW: net.IPv4(192, 168, 1, 1)},
nil,
}
got := filterDefaultRoutes(routes)
if len(got) != 1 {
t.Fatalf("expected 1 route, got %d", len(got))
}
if got[0].Dst.String() != "10.0.0.0/8" {
t.Errorf("unexpected route: %v", got[0].Dst)
}

if filterDefaultRoutes(nil) != nil {
t.Errorf("nil input should return nil")
}
if len(filterDefaultRoutes([]*types.Route{})) != 0 {
t.Errorf("empty input should return empty")
}
}
Loading