Skip to content

Commit

Permalink
Add option BootfileParam
Browse files Browse the repository at this point in the history
See RFC5970 section 3.2.

* Added support of option OptBootFileParam
* Added BootfileParam to netboot results

Signed-off-by: Your Name <[email protected]>
  • Loading branch information
Your Name committed Dec 10, 2019
1 parent 3997b8a commit 603f39f
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 41 deletions.
76 changes: 76 additions & 0 deletions dhcpv6/option_bootfileparam.go
Original file line number Diff line number Diff line change
@@ -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
}
31 changes: 31 additions & 0 deletions dhcpv6/option_bootfileparam_test.go
Original file line number Diff line number Diff line change
@@ -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()))
}
2 changes: 2 additions & 0 deletions dhcpv6/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
90 changes: 49 additions & 41 deletions netboot/netboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}

0 comments on commit 603f39f

Please sign in to comment.