summaryrefslogtreecommitdiffhomepage
path: root/netboot/netboot.go
blob: adfc7f38faee09a8ef57e33cc67cb7ec9ced27a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package netboot

import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/insomniacslk/dhcp/dhcpv4"
	"github.com/insomniacslk/dhcp/dhcpv4/client4"
	"github.com/insomniacslk/dhcp/dhcpv6"
	"github.com/insomniacslk/dhcp/dhcpv6/client6"
)

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) {
	var (
		conversation []dhcpv6.DHCPv6
		err          error
	)
	modifiers = append(modifiers, dhcpv6.WithNetboot)
	delay := 2 * time.Second
	for i := 0; i <= retries; i++ {
		log.Printf("sending request, attempt #%d", i+1)

		client := client6.NewClient()
		client.ReadTimeout = timeout
		conversation, err = client.Exchange(ifname, modifiers...)
		if err != nil {
			log.Printf("Client.Exchange failed: %v", err)
			if i >= retries {
				return nil, fmt.Errorf("netboot failed after %d attempts: %v", retries+1, err)
			}
			log.Printf("sleeping %v before retrying", delay)
			sleeper(delay)
			// TODO add random splay
			delay = delay * 2
			continue
		}
		break
	}
	return conversation, nil
}

// RequestNetbootv4 sends a netboot request via DHCPv4 and returns the exchanged packets. Additional modifiers
// can be passed to manipulate both the discover and offer packets.
func RequestNetbootv4(ifname string, timeout time.Duration, retries int, modifiers ...dhcpv4.Modifier) ([]*dhcpv4.DHCPv4, error) {
	var (
		conversation []*dhcpv4.DHCPv4
		err          error
	)
	delay := 2 * time.Second
	modifiers = append(modifiers, dhcpv4.WithNetboot)
	for i := 0; i <= retries; i++ {
		log.Printf("sending request, attempt #%d", i+1)
		client := client4.NewClient()
		client.ReadTimeout = timeout
		conversation, err = client.Exchange(ifname, modifiers...)
		if err != nil {
			log.Printf("Client.Exchange failed: %v", err)
			log.Printf("sleeping %v before retrying", delay)
			if i >= retries {
				return nil, fmt.Errorf("netboot failed after %d attempts: %v", retries+1, err)
			}
			sleeper(delay)
			// TODO add random splay
			delay = delay * 2
			continue
		}
		break
	}
	return conversation, nil
}

// 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) (*BootConf, error) {
	var advertise, reply *dhcpv6.Message
	for _, m := range conversation {
		switch m.Type() {
		case dhcpv6.MessageTypeAdvertise:
			advertise = m.(*dhcpv6.Message)
		case dhcpv6.MessageTypeReply:
			reply = m.(*dhcpv6.Message)
		}
	}
	if reply == nil {
		return nil, errors.New("no REPLY received")
	}

	bootconf := &BootConf{}
	netconf, err := GetNetConfFromPacketv6(reply)
	if err != nil {
		return nil, fmt.Errorf("cannot get netconf from packet: %v", err)
	}
	bootconf.NetConf = *netconf

	if u := reply.Options.BootFileURL(); len(u) > 0 {
		bootconf.BootfileURL = u
		bootconf.BootfileParam = reply.Options.BootFileParam()
	} else {
		log.Printf("no bootfile URL option found in REPLY, fallback to ADVERTISE's value")
		if u := advertise.Options.BootFileURL(); len(u) > 0 {
			bootconf.BootfileURL = u
			bootconf.BootfileParam = advertise.Options.BootFileParam()
		}
	}
	if len(bootconf.BootfileURL) == 0 {
		return nil, errors.New("no bootfile URL option found")
	}
	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) (*BootConf, error) {
	var reply *dhcpv4.DHCPv4
	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 {
			reply = m
			break
		}
	}
	if reply == nil {
		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)
	}
	bootconf.NetConf = *netconf

	bootconf.BootfileURL = reply.BootFileName
	// TODO: should we support bootfile parameters here somehow? (see netconf.BootfileParam)
	return bootconf, nil
}