dhcp ipam: support customizing dhcp options

Signed-off-by: SilverBut <SilverBut@users.noreply.github.com>
This commit is contained in:
SilverBut 2021-10-02 20:48:17 +08:00
parent be383cf30d
commit 2bebd89aa2
5 changed files with 169 additions and 8 deletions

View File

@ -71,9 +71,16 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
return fmt.Errorf("error parsing netconf: %v", err) 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) clientID := generateClientID(args.ContainerID, conf.Name, args.IfName)
hostNetns := d.hostNetnsPrefix + args.Netns 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 { if err != nil {
return err return err
} }

View File

@ -19,6 +19,7 @@ import (
"log" "log"
"math/rand" "math/rand"
"net" "net"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -62,6 +63,74 @@ type DHCPLease struct {
stopping uint32 stopping uint32
stop chan struct{} stop chan struct{}
wg sync.WaitGroup 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 // AcquireLease gets an DHCP lease and then maintains it in the background
@ -69,6 +138,7 @@ type DHCPLease struct {
// calling DHCPLease.Stop() // calling DHCPLease.Stop()
func AcquireLease( func AcquireLease(
clientID, netns, ifName string, clientID, netns, ifName string,
optsRequesting map[dhcp4.OptionCode]bool, optsProviding map[dhcp4.OptionCode][]byte,
timeout, resendMax time.Duration, broadcast bool, timeout, resendMax time.Duration, broadcast bool,
) (*DHCPLease, error) { ) (*DHCPLease, error) {
errCh := make(chan error, 1) errCh := make(chan error, 1)
@ -78,6 +148,8 @@ func AcquireLease(
timeout: timeout, timeout: timeout,
resendMax: resendMax, resendMax: resendMax,
broadcast: broadcast, broadcast: broadcast,
optsRequesting: optsRequesting,
optsProviding: optsProviding,
} }
log.Printf("%v: acquiring lease", clientID) log.Printf("%v: acquiring lease", clientID)
@ -139,7 +211,13 @@ func (l *DHCPLease) acquire() error {
opts := make(dhcp4.Options) opts := make(dhcp4.Options)
opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID) 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) { pkt, err := backoffRetry(l.resendMax, func() (*dhcp4.Packet, error) {
ok, ack, err := DhcpRequest(c, opts) ok, ack, err := DhcpRequest(c, opts)

View File

@ -42,7 +42,32 @@ type NetConf struct {
type IPAMConfig struct { type IPAMConfig struct {
types.IPAM 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() { func main() {

View File

@ -18,12 +18,31 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"net" "net"
"strconv"
"time" "time"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/d2g/dhcp4" "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 { func parseRouter(opts dhcp4.Options) net.IP {
if opts, ok := opts[dhcp4.OptionRouter]; ok { if opts, ok := opts[dhcp4.OptionRouter]; ok {
if len(opts) == 4 { if len(opts) == 4 {

View File

@ -16,6 +16,7 @@ package main
import ( import (
"net" "net"
"reflect"
"testing" "testing"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
@ -73,3 +74,34 @@ func TestParseCIDRRoutes(t *testing.T) {
validateRoutes(t, routes) 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)
}
})
}
}