diff --git a/plugins/ipam/dhcp/daemon.go b/plugins/ipam/dhcp/daemon.go index 67a49c9b7..b17a64614 100644 --- a/plugins/ipam/dhcp/daemon.go +++ b/plugins/ipam/dhcp/daemon.go @@ -29,13 +29,10 @@ import ( "time" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/types" current "github.com/containernetworking/cni/pkg/types/100" "github.com/coreos/go-systemd/v22/activation" ) -const listenFdsStart = 3 - var errNoMoreTries = errors.New("no more tries") type DHCP struct { @@ -55,21 +52,35 @@ func newDHCP(clientTimeout, clientResendMax time.Duration) *DHCP { } } +// TODO: current client ID is too long. At least the container ID should not be used directly. +// A seperate issue is necessary to ensure no breaking change is affecting other users. func generateClientID(containerID string, netName string, ifName string) string { - return containerID + "/" + netName + "/" + ifName + clientID := containerID + "/" + netName + "/" + ifName + // defined in RFC 2132, length size can not be larger than 1 octet. So we truncate 254 to make everyone happy. + if len(clientID) > 254 { + clientID = clientID[0:254] + } + return clientID } // Allocate acquires an IP from a DHCP server for a specified container. // The acquired lease will be maintained until Release() is called. func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error { - conf := types.NetConf{} + conf := NetConf{} if err := json.Unmarshal(args.StdinData, &conf); err != nil { return fmt.Errorf("error parsing netconf: %v", err) } + optsRequesting, optsProviding, err := prepareOptions(args.Args, conf.IPAM.ProvideOptions, conf.IPAM.RequestOptions) + 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 } @@ -94,7 +105,7 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error { // Release stops maintenance of the lease acquired in Allocate() // and sends a release msg to the DHCP server. func (d *DHCP) Release(args *skel.CmdArgs, reply *struct{}) error { - conf := types.NetConf{} + conf := NetConf{} if err := json.Unmarshal(args.StdinData, &conf); err != nil { return fmt.Errorf("error parsing netconf: %v", err) } diff --git a/plugins/ipam/dhcp/lease.go b/plugins/ipam/dhcp/lease.go index ef68931d4..943e6e2c6 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" @@ -36,6 +37,11 @@ import ( const resendDelay0 = 4 * time.Second const resendDelayMax = 62 * time.Second +// To speed up the retry for first few failures, we retry without +// backoff for a few times +const resendFastDelay = 2 * time.Second +const resendFastMax = 4 + const ( leaseStateBound = iota leaseStateRenewing @@ -62,6 +68,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 requestOptionsDefault = map[dhcp4.OptionCode]bool{ + dhcp4.OptionRouter: true, + dhcp4.OptionSubnetMask: true, +} + +func prepareOptions(cniArgs string, ProvideOptions []ProvideOption, RequestOptions []RequestOption) ( + 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 RequestOptions { + 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 requestOptionsDefault { + // 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 +143,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 +216,17 @@ 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 + } + // client identifier's first byte is "type" + newClientID := []byte{0} + newClientID = append(newClientID, opts[dhcp4.OptionClientIdentifier]...) + opts[dhcp4.OptionClientIdentifier] = newClientID pkt, err := backoffRetry(l.resendMax, func() (*dhcp4.Packet, error) { ok, ack, err := DhcpRequest(c, opts) @@ -345,7 +432,7 @@ func jitter(span time.Duration) time.Duration { func backoffRetry(resendMax time.Duration, f func() (*dhcp4.Packet, error)) (*dhcp4.Packet, error) { var baseDelay time.Duration = resendDelay0 var sleepTime time.Duration - + var fastRetryLimit = resendFastMax for { pkt, err := f() if err == nil { @@ -354,13 +441,19 @@ func backoffRetry(resendMax time.Duration, f func() (*dhcp4.Packet, error)) (*dh log.Print(err) - sleepTime = baseDelay + jitter(time.Second) + if fastRetryLimit == 0 { + sleepTime = baseDelay + jitter(time.Second) + } else { + sleepTime = resendFastDelay + jitter(time.Second) + fastRetryLimit-- + } log.Printf("retrying in %f seconds", sleepTime.Seconds()) time.Sleep(sleepTime) - if baseDelay < resendMax { + // only adjust delay time if we are in normal backoff stage + if baseDelay < resendMax && fastRetryLimit == 0 { baseDelay *= 2 } else { break diff --git a/plugins/ipam/dhcp/main.go b/plugins/ipam/dhcp/main.go index 72680607e..be18aff4c 100644 --- a/plugins/ipam/dhcp/main.go +++ b/plugins/ipam/dhcp/main.go @@ -33,6 +33,43 @@ import ( const defaultSocketPath = "/run/cni/dhcp.sock" +// The top-level network config - IPAM plugins are passed the full configuration +// of the calling plugin, not just the IPAM section. +type NetConf struct { + types.NetConf + IPAM *IPAMConfig `json:"ipam"` +} + +type IPAMConfig struct { + types.IPAM + 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 requesting fields, set `skipDefault` to `false`. + // If an field is not optional, but the server failed to provide it, error will be raised. + RequestOptions []RequestOption `json:"request"` +} + +// 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 RequestOption struct { + SkipDefault bool `json:"skipDefault"` + + Option DHCPOption `json:"option"` +} + func main() { if len(os.Args) > 1 && os.Args[1] == "daemon" { var pidfilePath string @@ -55,7 +92,7 @@ func main() { } if err := runDaemon(pidfilePath, hostPrefix, socketPath, timeout, resendMax, broadcast); err != nil { - log.Printf(err.Error()) + log.Print(err.Error()) os.Exit(1) } } else { @@ -88,8 +125,6 @@ func cmdDel(args *skel.CmdArgs) error { } func cmdCheck(args *skel.CmdArgs) error { - // TODO: implement - //return fmt.Errorf("not implemented") // Plugin must return result in same version as specified in netconf versionDecoder := &version.ConfigDecoder{} //confVersion, err := versionDecoder.Decode(args.StdinData) @@ -106,16 +141,8 @@ func cmdCheck(args *skel.CmdArgs) error { return nil } -type SocketPathConf struct { - DaemonSocketPath string `json:"daemonSocketPath,omitempty"` -} - -type TempNetConf struct { - IPAM SocketPathConf `json:"ipam,omitempty"` -} - func getSocketPath(stdinData []byte) (string, error) { - conf := TempNetConf{} + conf := NetConf{} if err := json.Unmarshal(stdinData, &conf); err != nil { return "", fmt.Errorf("error parsing socket path conf: %v", err) } diff --git a/plugins/ipam/dhcp/options.go b/plugins/ipam/dhcp/options.go index 910e1cc61..0adbc6550 100644 --- a/plugins/ipam/dhcp/options.go +++ b/plugins/ipam/dhcp/options.go @@ -18,12 +18,33 @@ 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, + "user-class": dhcp4.OptionUserClass, + "vendor-class-identifier": dhcp4.OptionVendorClassIdentifier, +} + +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 961070c23..ee18b8816 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) + } + }) + } +}