diff --git a/dhcpv6/option_bootfileparam.go b/dhcpv6/option_bootfileparam.go new file mode 100644 index 00000000..84eea8b9 --- /dev/null +++ b/dhcpv6/option_bootfileparam.go @@ -0,0 +1,76 @@ +package dhcpv6 + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +var ( + // bootpEndian is the endianness used to parse BOOTP option values + // See RFC2132, section 2. Also keep in mind + // "network byte order" is "big-endian order". + bootpEndian = binary.BigEndian +) + +// ErrBootFileParamUnexpectedEnd is returned when option Bootfile param is +// encoded wrongly. See https://www.ietf.org/rfc/rfc5970.txt (section 3.2). +type ErrBootFileParamUnexpectedEnd struct { + Expected uint16 + Left uint16 +} + +// Error just implements interface "error" +func (err *ErrBootFileParamUnexpectedEnd) Error() string { + return fmt.Sprintf("[bootfile param] unexpected end (expected: %d; left %d)", err.Expected, err.Left) +} + +// OptBootFileParam implements the OptionBootfileParam option +// +// This module defines the OPT_BOOTFILE_PARAM structure. +// https://www.ietf.org/rfc/rfc5970.txt (section 3.2) +type OptBootFileParam []string + +var _ Option = OptBootFileParam(nil) + +// Code returns the option code +func (op OptBootFileParam) Code() OptionCode { + return OptionBootfileParam +} + +// ToBytes serializes the option and returns it as a sequence of bytes +func (op OptBootFileParam) ToBytes() []byte { + var length [2]byte + var buf bytes.Buffer + for _, param := range op { + bootpEndian.PutUint16(length[:], uint16(len(param))) + buf.Write(length[:]) + buf.WriteString(param) + } + return buf.Bytes() +} + +func (op OptBootFileParam) String() string { + return fmt.Sprintf("OptBootFileParam(%v)", ([]string)(op)) +} + +// ParseOptBootFileParam builds an OptBootFileParam structure from a sequence +// of bytes. The input data does not include option code and length bytes. +func ParseOptBootFileParam(data []byte) (result OptBootFileParam, err error) { + for len(data) > 0 { + if len(data) < 2 { + return nil, &ErrBootFileParamUnexpectedEnd{Expected: 2, Left: uint16(len(data))} + } + length := bootpEndian.Uint16(data) + data = data[2:] + + if len(data) < int(length) { + return nil, &ErrBootFileParamUnexpectedEnd{Expected: length, Left: uint16(len(data))} + } + param := string(data[:length]) + data = data[length:] + + result = append(result, param) + } + return +} diff --git a/dhcpv6/option_bootfileparam_test.go b/dhcpv6/option_bootfileparam_test.go new file mode 100644 index 00000000..6c2dcc45 --- /dev/null +++ b/dhcpv6/option_bootfileparam_test.go @@ -0,0 +1,31 @@ +package dhcpv6 + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + testBootfileParam = "\x00\x30initrd=http://myserver.mycompany.local/initrd.xz\x00\x0eroot=/dev/sda1\x00\x02rw\x00\x24netconsole=..:\000:.something\000here.::.." +) + +func TestOptBootFileParam(t *testing.T) { + expected := testBootfileParam + opt, err := ParseOptBootFileParam([]byte(expected)) + if err != nil { + t.Fatal(err) + } + if string(opt.ToBytes()) != expected { + t.Fatalf("Invalid boot file parameter. Expected %v, got %v", expected, opt) + } +} + +func TestParsedTypeOptBootFileParam(t *testing.T) { + opt, err := ParseOption(OptionBootfileParam, []byte(testBootfileParam)) + require.NoError(t, err) + bootfileParamOpt, ok := opt.(OptBootFileParam) + require.True(t, ok, fmt.Sprintf("invalid type: %T instead of %T", opt, bootfileParamOpt)) + require.Equal(t, testBootfileParam, string(bootfileParamOpt.ToBytes())) +} diff --git a/dhcpv6/options.go b/dhcpv6/options.go index cf192fc5..fb36a386 100644 --- a/dhcpv6/options.go +++ b/dhcpv6/options.go @@ -75,6 +75,8 @@ func ParseOption(code OptionCode, optData []byte) (Option, error) { opt, err = ParseOptRemoteId(optData) case OptionBootfileURL: opt, err = ParseOptBootFileURL(optData) + case OptionBootfileParam: + opt, err = ParseOptBootFileParam(optData) case OptionClientArchType: opt, err = ParseOptClientArchType(optData) case OptionNII: diff --git a/netboot/netboot.go b/netboot/netboot.go index 6f113b66..2ad57e52 100644 --- a/netboot/netboot.go +++ b/netboot/netboot.go @@ -16,6 +16,20 @@ var sleeper = func(d time.Duration) { time.Sleep(d) } +// BootConf is a structure describes everything a host needs to know to boot over network +type BootConf struct { + // NetConf is the network configuration of the client + NetConf + + // BootfileURL is "where is the image (kernel)". + // See RFC5970 section 3.1 for IPv6 and RFC2132 section 9.5 ("Bootfile name") for IPv4 + BootfileURL string + + // BootfileParam is "what arguments should we pass (cmdline)". + // See RFC5970 section 3.2 for IPv6. + BootfileParam []string +} + // RequestNetbootv6 sends a netboot request via DHCPv6 and returns the exchanged packets. Additional modifiers // can be passed to manipulate both solicit and advertise packets. func RequestNetbootv6(ifname string, timeout time.Duration, retries int, modifiers ...dhcpv6.Modifier) ([]dhcpv6.DHCPv6, error) { @@ -81,75 +95,69 @@ func RequestNetbootv4(ifname string, timeout time.Duration, retries int, modifie // ConversationToNetconf extracts network configuration and boot file URL from a // DHCPv6 4-way conversation and returns them, or an error if any. -func ConversationToNetconf(conversation []dhcpv6.DHCPv6) (*NetConf, string, error) { - var reply dhcpv6.DHCPv6 +func ConversationToNetconf(conversation []dhcpv6.DHCPv6) (*BootConf, error) { + var advertise, reply, optionsSource dhcpv6.DHCPv6 for _, m := range conversation { - // look for a REPLY - if m.Type() == dhcpv6.MessageTypeReply { + switch m.Type() { + case dhcpv6.MessageTypeAdvertise: + advertise = m + case dhcpv6.MessageTypeReply: reply = m - break } } if reply == nil { - return nil, "", errors.New("no REPLY received") + return nil, errors.New("no REPLY received") } + + bootconf := &BootConf{} netconf, err := GetNetConfFromPacketv6(reply.(*dhcpv6.Message)) if err != nil { - return nil, "", fmt.Errorf("cannot get netconf from packet: %v", err) + return nil, fmt.Errorf("cannot get netconf from packet: %v", err) } - // look for boot file - var ( - opt dhcpv6.Option - bootfile string - ) - opt = reply.GetOneOption(dhcpv6.OptionBootfileURL) - if opt == nil { - log.Printf("no bootfile URL option found in REPLY, looking for it in ADVERTISE") - // as a fallback, look for bootfile URL in the advertise - var advertise dhcpv6.DHCPv6 - for _, m := range conversation { - // look for an ADVERTISE - if m.Type() == dhcpv6.MessageTypeAdvertise { - advertise = m - break - } - } - if advertise == nil { - return nil, "", errors.New("no ADVERTISE found") - } - opt = advertise.GetOneOption(dhcpv6.OptionBootfileURL) - if opt == nil { - return nil, "", errors.New("no bootfile URL option found in ADVERTISE") - } + bootconf.NetConf = *netconf + + if reply.GetOneOption(dhcpv6.OptionBootfileURL) != nil { + optionsSource = reply + } else { + log.Printf("no bootfile URL option found in REPLY, fallback to ADVERTISE's value") + optionsSource = advertise } - if opt != nil { - obf := opt.(dhcpv6.OptBootFileURL) - bootfile = string(obf) + if optionsSource == nil { + return nil, errors.New("no bootfile URL option found") } - return netconf, bootfile, nil + bootconf.BootfileURL = string(optionsSource.GetOneOption(dhcpv6.OptionBootfileURL).(dhcpv6.OptBootFileURL)) + + if bootfileParamOption := optionsSource.GetOneOption(dhcpv6.OptionBootfileParam); bootfileParamOption != nil { + bootconf.BootfileParam = bootfileParamOption.(dhcpv6.OptBootFileParam) + } + return bootconf, nil } // ConversationToNetconfv4 extracts network configuration and boot file URL from a // DHCPv4 4-way conversation and returns them, or an error if any. -func ConversationToNetconfv4(conversation []*dhcpv4.DHCPv4) (*NetConf, string, error) { +func ConversationToNetconfv4(conversation []*dhcpv4.DHCPv4) (*BootConf, error) { var reply *dhcpv4.DHCPv4 - var bootFileURL string for _, m := range conversation { // look for a BootReply packet of type Offer containing the bootfile URL. // Normally both packets with Message Type OFFER or ACK do contain // the bootfile URL. if m.OpCode == dhcpv4.OpcodeBootReply && m.MessageType() == dhcpv4.MessageTypeOffer { - bootFileURL = m.BootFileName reply = m break } } if reply == nil { - return nil, "", errors.New("no OFFER with valid bootfile URL received") + return nil, errors.New("no OFFER with valid bootfile URL received") } + + bootconf := &BootConf{} netconf, err := GetNetConfFromPacketv4(reply) if err != nil { - return nil, "", fmt.Errorf("could not get netconf: %v", err) + return nil, fmt.Errorf("could not get netconf: %v", err) } - return netconf, bootFileURL, nil + bootconf.NetConf = *netconf + + bootconf.BootfileURL = reply.BootFileName + // TODO: should we support bootfile parameters here somehow? (see netconf.BootfileParam) + return bootconf, nil }