diff --git a/plugins/ipam/dhcp/daemon.go b/plugins/ipam/dhcp/daemon.go index f1bceeab..cdf11a8f 100644 --- a/plugins/ipam/dhcp/daemon.go +++ b/plugins/ipam/dhcp/daemon.go @@ -71,9 +71,16 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error { return fmt.Errorf("error parsing netconf: %v", err) } + optsRequesting, optsProviding, err := prepareOptions(args.Args, conf.IPAM.ProvideOptions, conf.IPAM.RequireOptions) + if err != nil { + return err + } + clientID := generateClientID(args.ContainerID, conf.Name, args.IfName) hostNetns := d.hostNetnsPrefix + args.Netns - l, err := AcquireLease(clientID, hostNetns, args.IfName, d.clientTimeout, d.clientResendMax, d.broadcast) + l, err := AcquireLease(clientID, hostNetns, args.IfName, + optsRequesting, optsProviding, + d.clientTimeout, d.clientResendMax, d.broadcast) if err != nil { return err } diff --git a/plugins/ipam/dhcp/lease.go b/plugins/ipam/dhcp/lease.go index ef68931d..9a7e5e48 100644 --- a/plugins/ipam/dhcp/lease.go +++ b/plugins/ipam/dhcp/lease.go @@ -19,6 +19,7 @@ import ( "log" "math/rand" "net" + "strings" "sync" "sync/atomic" "time" @@ -62,6 +63,74 @@ type DHCPLease struct { stopping uint32 stop chan struct{} wg sync.WaitGroup + // list of requesting and providing options and if they are necessary / their value + optsRequesting map[dhcp4.OptionCode]bool + optsProviding map[dhcp4.OptionCode][]byte +} + +var acquireOptionsDefault = map[dhcp4.OptionCode]bool{ + dhcp4.OptionRouter: true, + dhcp4.OptionSubnetMask: true, +} + +func prepareOptions(cniArgs string, ProvideOptions []ProvideOption, RequireOptions []RequireOption) ( + optsRequesting map[dhcp4.OptionCode]bool, optsProviding map[dhcp4.OptionCode][]byte, err error) { + + // parse CNI args + cniArgsParsed := map[string]string{} + for _, argPair := range strings.Split(cniArgs, ";") { + args := strings.SplitN(argPair, "=", 2) + if len(args) > 1 { + cniArgsParsed[args[0]] = args[1] + } + } + + // parse providing options map + var optParsed dhcp4.OptionCode + optsProviding = make(map[dhcp4.OptionCode][]byte) + for _, opt := range ProvideOptions { + optParsed, err = parseOptionName(string(opt.Option)) + if err != nil { + err = fmt.Errorf("Can not parse option %q: %w", opt.Option, err) + return + } + if len(opt.Value) > 0 { + if len(opt.Value) > 255 { + err = fmt.Errorf("value too long for option %q: %q", opt.Option, opt.Value) + return + } + optsProviding[optParsed] = []byte(opt.Value) + } + if value, ok := cniArgsParsed[opt.ValueFromCNIArg]; ok { + if len(value) > 255 { + err = fmt.Errorf("value too long for option %q from CNI_ARGS %q: %q", opt.Option, opt.ValueFromCNIArg, opt.Value) + return + } + optsProviding[optParsed] = []byte(value) + } + } + + // parse necessary options map + optsRequesting = make(map[dhcp4.OptionCode]bool) + skipRequireDefault := false + for _, opt := range RequireOptions { + if opt.SkipDefault { + skipRequireDefault = true + } + optParsed, err = parseOptionName(string(opt.Option)) + if err != nil { + err = fmt.Errorf("Can not parse option %q: %w", opt.Option, err) + return + } + optsRequesting[optParsed] = true + } + for k, v := range acquireOptionsDefault { + // only set if not skipping default and this value does not exists + if _, ok := optsRequesting[k]; !ok && !skipRequireDefault { + optsRequesting[k] = v + } + } + return } // AcquireLease gets an DHCP lease and then maintains it in the background @@ -69,15 +138,18 @@ type DHCPLease struct { // calling DHCPLease.Stop() func AcquireLease( clientID, netns, ifName string, + optsRequesting map[dhcp4.OptionCode]bool, optsProviding map[dhcp4.OptionCode][]byte, timeout, resendMax time.Duration, broadcast bool, ) (*DHCPLease, error) { errCh := make(chan error, 1) l := &DHCPLease{ - clientID: clientID, - stop: make(chan struct{}), - timeout: timeout, - resendMax: resendMax, - broadcast: broadcast, + clientID: clientID, + stop: make(chan struct{}), + timeout: timeout, + resendMax: resendMax, + broadcast: broadcast, + optsRequesting: optsRequesting, + optsProviding: optsProviding, } log.Printf("%v: acquiring lease", clientID) @@ -139,7 +211,13 @@ func (l *DHCPLease) acquire() error { opts := make(dhcp4.Options) opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID) - opts[dhcp4.OptionParameterRequestList] = []byte{byte(dhcp4.OptionRouter), byte(dhcp4.OptionSubnetMask)} + opts[dhcp4.OptionParameterRequestList] = []byte{} + for k := range l.optsRequesting { + opts[dhcp4.OptionParameterRequestList] = append(opts[dhcp4.OptionParameterRequestList], byte(k)) + } + for k, v := range l.optsProviding { + opts[k] = v + } pkt, err := backoffRetry(l.resendMax, func() (*dhcp4.Packet, error) { ok, ack, err := DhcpRequest(c, opts) diff --git a/plugins/ipam/dhcp/main.go b/plugins/ipam/dhcp/main.go index e0a63a8b..e8e2771f 100644 --- a/plugins/ipam/dhcp/main.go +++ b/plugins/ipam/dhcp/main.go @@ -42,7 +42,32 @@ type NetConf struct { type IPAMConfig struct { types.IPAM - DaemonSocketPath string `json:"daemonSocketPath,omitempty"` + DaemonSocketPath string `json:"daemonSocketPath"` + // When requesting IP from DHCP server, carry these options for management purpose. + // Some fields have default values, and can be override by setting a new option with the same name at here. + ProvideOptions []ProvideOption `json:"provide"` + // When requesting IP from DHCP server, claiming these options are necessary. Options are necessary unless `optional` + // is set to `false`. + // To override default required fields, set `skipDefault` to `false`. + // If an field is not optional, but the server failed to provide it, error will be raised. + RequireOptions []RequireOption `json:"require"` +} + +// DHCPOption represents a DHCP option. It can be a number, or a string defined in manual dhcp-options(5). +// Note that not all DHCP options are supported at all time. Error will be raised if unsupported options are used. +type DHCPOption string + +type ProvideOption struct { + Option DHCPOption `json:"option"` + + Value string `json:"value"` + ValueFromCNIArg string `json:"fromArg"` +} + +type RequireOption struct { + SkipDefault bool `json:"skipDefault"` + + Option DHCPOption `json:"option"` } func main() { diff --git a/plugins/ipam/dhcp/options.go b/plugins/ipam/dhcp/options.go index 910e1cc6..6415a9ac 100644 --- a/plugins/ipam/dhcp/options.go +++ b/plugins/ipam/dhcp/options.go @@ -18,12 +18,31 @@ import ( "encoding/binary" "fmt" "net" + "strconv" "time" "github.com/containernetworking/cni/pkg/types" "github.com/d2g/dhcp4" ) +var optionNameToID = map[string]dhcp4.OptionCode{ + "dhcp-client-identifier": dhcp4.OptionClientIdentifier, + "subnet-mask": dhcp4.OptionSubnetMask, + "routers": dhcp4.OptionRouter, + "host-name": dhcp4.OptionHostName, +} + +func parseOptionName(option string) (dhcp4.OptionCode, error) { + if val, ok := optionNameToID[option]; ok { + return val, nil + } + i, err := strconv.ParseUint(option, 10, 8) + if err != nil { + return 0, fmt.Errorf("Can not parse option: %w", err) + } + return dhcp4.OptionCode(i), nil +} + func parseRouter(opts dhcp4.Options) net.IP { if opts, ok := opts[dhcp4.OptionRouter]; ok { if len(opts) == 4 { diff --git a/plugins/ipam/dhcp/options_test.go b/plugins/ipam/dhcp/options_test.go index 961070c2..ee18b881 100644 --- a/plugins/ipam/dhcp/options_test.go +++ b/plugins/ipam/dhcp/options_test.go @@ -16,6 +16,7 @@ package main import ( "net" + "reflect" "testing" "github.com/containernetworking/cni/pkg/types" @@ -73,3 +74,34 @@ func TestParseCIDRRoutes(t *testing.T) { validateRoutes(t, routes) } + +func TestParseOptionName(t *testing.T) { + tests := []struct { + name string + option string + want dhcp4.OptionCode + wantErr bool + }{ + { + "hostname", "host-name", dhcp4.OptionHostName, false, + }, + { + "hostname in number", "12", dhcp4.OptionHostName, false, + }, + { + "random string", "doNotparseMe", 0, true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseOptionName(tt.option) + if (err != nil) != tt.wantErr { + t.Errorf("parseOptionName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseOptionName() = %v, want %v", got, tt.want) + } + }) + } +}