Skip to content

Commit

Permalink
Merge pull request containernetworking#670 from SilverBut/ipam-dhcp-m…
Browse files Browse the repository at this point in the history
…ore-options

dhcp ipam: support customizing dhcp options from CNI args
  • Loading branch information
dcbw authored Dec 15, 2021
2 parents b768495 + c9d0423 commit cc32993
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 28 deletions.
25 changes: 18 additions & 7 deletions plugins/ipam/dhcp/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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)
}
Expand Down
111 changes: 102 additions & 9 deletions plugins/ipam/dhcp/lease.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"log"
"math/rand"
"net"
"strings"
"sync"
"sync/atomic"
"time"
Expand All @@ -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
Expand All @@ -62,22 +68,93 @@ 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
// by periodically renewing it. The acquired lease can be released by
// 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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
51 changes: 39 additions & 12 deletions plugins/ipam/dhcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
21 changes: 21 additions & 0 deletions plugins/ipam/dhcp/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions plugins/ipam/dhcp/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package main

import (
"net"
"reflect"
"testing"

"github.com/containernetworking/cni/pkg/types"
Expand Down Expand Up @@ -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)
}
})
}
}

0 comments on commit cc32993

Please sign in to comment.