Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option BootfileParam #340

Merged
merged 2 commits into from
Dec 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dhcpv6/client6/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ func (c *Client) sendReceive(ifname string, packet dhcpv6.DHCPv6, expectedType d
adv, err = dhcpv6.FromBytes(buf[:n])
if err != nil {
// skip non-DHCP packets
//
// TODO: It also skips DHCP packets with any errors (for example
xaionaro marked this conversation as resolved.
Show resolved Hide resolved
// if bootfile params are encoded incorrectly). We need to
// log such cases instead of silently skip them.
continue
}
if recvMsg, ok := adv.(*dhcpv6.Message); ok && isMessage {
Expand Down
60 changes: 60 additions & 0 deletions dhcpv6/option_bootfileparam.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package dhcpv6

import (
"fmt"

"github.com/u-root/u-root/pkg/uio"
)

// 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)
xaionaro marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
buf := uio.NewBigEndianBuffer(nil)
for _, param := range op {
if len(param) >= 1<<16 {
// TODO: say something here instead of silently ignoring a parameter
continue
}
buf.Write16(uint16(len(param)))
buf.WriteBytes([]byte(param))
/*if err := buf.Error(); err != nil {
// TODO: description of `WriteBytes` says it could return
// an error via `buf.Error()`. But a quick look into implementation of
// `WriteBytes` at the moment of this comment showed it does not set any
// errors to `Error()` output. It's required to make a decision:
// to fix `WriteBytes` or it's description or
// to find a way to handle an error here.
}*/
}
return buf.Data()
}

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) {
buf := uio.NewBigEndianBuffer(data)
for buf.Has(2) {
length := buf.Read16()
result = append(result, string(buf.CopyN(int(length))))
}
if err := buf.FinError(); err != nil {
xaionaro marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}
return
}
71 changes: 71 additions & 0 deletions dhcpv6/option_bootfileparam_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dhcpv6

import (
"bytes"
"encoding/binary"
"fmt"
"testing"

"github.com/stretchr/testify/require"
)

var (
testBootfileParams0Compiled = "\x00\x0eroot=/dev/sda1\x00\x00\x00\x02rw"
testBootfileParams1 = []string{
"initrd=http://myserver.mycompany.local/initrd.xz",
"",
"root=/dev/sda1",
"rw",
"netconsole=..:\000:.something\000here.::..",
string(make([]byte, (1<<16) - 1)),
}
)

// compileTestBootfileParams is an independent implementation of bootfile param encoder
func compileTestBootfileParams(t *testing.T, params []string) []byte {
var length [2]byte
buf := bytes.Buffer{}
for _, param := range params {
if len(param) >= 1<<16 {
panic("a too long parameter")
}
binary.BigEndian.PutUint16(length[:], uint16(len(param)))
_, err := buf.Write(length[:])
require.NoError(t, err)
_, err = buf.WriteString(param)
require.NoError(t, err)
}

return buf.Bytes()
}
xaionaro marked this conversation as resolved.
Show resolved Hide resolved

func TestOptBootFileParam(t *testing.T) {
expected := string(compileTestBootfileParams(t, testBootfileParams1))
opt, err := ParseOptBootFileParam([]byte(expected))
xaionaro marked this conversation as resolved.
Show resolved Hide resolved
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) {
tryParse := func(compiled []byte, expected []string) {
opt, err := ParseOption(OptionBootfileParam, compiled)
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, compiled, bootfileParamOpt.ToBytes())
require.Equal(t, expected, ([]string)(bootfileParamOpt))
}

tryParse(
[]byte(testBootfileParams0Compiled),
[]string{"root=/dev/sda1", "", "rw"},
)
tryParse(
compileTestBootfileParams(t, testBootfileParams1),
testBootfileParams1,
)
}
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: 50 additions & 40 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,71 @@ 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")
if advertise.GetOneOption(dhcpv6.OptionBootfileURL) != nil {
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
}