summaryrefslogtreecommitdiffhomepage
path: root/netboot
diff options
context:
space:
mode:
authorinsomniac <insomniacslk@users.noreply.github.com>2018-04-22 20:09:55 +0200
committerGitHub <noreply@github.com>2018-04-22 20:09:55 +0200
commited883f5cb71409cf27b4d57f0fe3229c6c9307f9 (patch)
tree58e74558d231a91b2c1a6a920c3ee7415ecf7c9c /netboot
parent9fcc7d1ee0fdbc07d369e88c8aefc0dbb731359b (diff)
Added netboot package (#45)
Added netboot package
Diffstat (limited to 'netboot')
-rw-r--r--netboot/netboot.go93
-rw-r--r--netboot/netconf.go131
2 files changed, 224 insertions, 0 deletions
diff --git a/netboot/netboot.go b/netboot/netboot.go
new file mode 100644
index 0000000..6b10c3c
--- /dev/null
+++ b/netboot/netboot.go
@@ -0,0 +1,93 @@
+package netboot
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/insomniacslk/dhcp/dhcpv6"
+)
+
+// 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
+ )
+ modifiers = append(modifiers, dhcpv6.WithNetboot)
+ delay := 2 * time.Second
+ for i := 0; i <= retries; i++ {
+ log.Printf("sending request, attempt #%d", i+1)
+ solicit, err := dhcpv6.NewSolicitForInterface(ifname, modifiers...)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create SOLICIT for interface %s: %v", ifname, err)
+ }
+
+ client := dhcpv6.NewClient()
+ client.ReadTimeout = timeout
+ conversation, err = client.Exchange(ifname, solicit, modifiers...)
+ if err != nil {
+ log.Printf("Client.Exchange failed: %v", err)
+ log.Printf("sleeping %v before retrying", delay)
+ if i >= retries {
+ // don't wait at the end of the last attempt
+ break
+ }
+ time.Sleep(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) (*NetConf, string, error) {
+ var reply dhcpv6.DHCPv6
+ for _, m := range conversation {
+ // look for a REPLY
+ if m.Type() == dhcpv6.REPLY {
+ reply = m
+ break
+ }
+ }
+ if reply == nil {
+ return nil, "", errors.New("no REPLY received")
+ }
+ netconf, err := GetNetConfFromPacketv6(reply.(*dhcpv6.DHCPv6Message))
+ if err != nil {
+ 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.OPT_BOOTFILE_URL)
+ 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.ADVERTISE {
+ advertise = m
+ break
+ }
+ }
+ if advertise == nil {
+ return nil, "", errors.New("no ADVERTISE found")
+ }
+ opt = advertise.GetOneOption(dhcpv6.OPT_BOOTFILE_URL)
+ if opt == nil {
+ return nil, "", errors.New("no bootfile URL option found in ADVERTISE")
+ }
+ }
+ obf := opt.(*dhcpv6.OptBootFileURL)
+ bootfile = string(obf.BootFileURL())
+ return netconf, bootfile, nil
+}
diff --git a/netboot/netconf.go b/netboot/netconf.go
new file mode 100644
index 0000000..4740b0f
--- /dev/null
+++ b/netboot/netconf.go
@@ -0,0 +1,131 @@
+package netboot
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/insomniacslk/dhcp/dhcpv6"
+ "github.com/vishvananda/netlink"
+)
+
+// AddrConf holds a single IP address configuration for a NIC
+type AddrConf struct {
+ IPNet net.IPNet
+ PreferredLifetime int
+ ValidLifetime int
+}
+
+// NetConf holds multiple IP configuration for a NIC, and DNS configuration
+type NetConf struct {
+ Addresses []AddrConf
+ DNSServers []net.IP
+ DNSSearchList []string
+}
+
+// GetNetConfFromPacketv6 extracts network configuration information from a DHCPv6
+// Reply packet and returns a populated NetConf structure
+func GetNetConfFromPacketv6(d *dhcpv6.DHCPv6Message) (*NetConf, error) {
+ opt := d.GetOneOption(dhcpv6.OPTION_IA_NA)
+ if opt == nil {
+ return nil, errors.New("No option IA NA found")
+ }
+ netconf := NetConf{}
+ // get IP configuration
+ oiana := opt.(*dhcpv6.OptIANA)
+ iaaddrs := make([]*dhcpv6.OptIAAddress, 0)
+ for _, o := range oiana.Options() {
+ if o.Code() == dhcpv6.OPTION_IAADDR {
+ iaaddrs = append(iaaddrs, o.(*dhcpv6.OptIAAddress))
+ }
+ }
+ netmask := net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::"))
+ for _, iaaddr := range iaaddrs {
+ netconf.Addresses = append(netconf.Addresses, AddrConf{
+ IPNet: net.IPNet{
+ IP: iaaddr.IPv6Addr(),
+ Mask: netmask,
+ },
+ PreferredLifetime: int(iaaddr.PreferredLifetime()),
+ ValidLifetime: int(iaaddr.ValidLifetime()),
+ })
+ }
+ // get DNS configuration
+ opt = d.GetOneOption(dhcpv6.DNS_RECURSIVE_NAME_SERVER)
+ if opt == nil {
+ return nil, errors.New("No option DNS Recursive Name Servers found ")
+ }
+ odnsserv := opt.(*dhcpv6.OptDNSRecursiveNameServer)
+ // TODO should this be copied?
+ netconf.DNSServers = odnsserv.NameServers()
+
+ opt = d.GetOneOption(dhcpv6.DOMAIN_SEARCH_LIST)
+ if opt == nil {
+ return nil, errors.New("No option DNS Domain Search List found")
+ }
+ odomains := opt.(*dhcpv6.OptDomainSearchList)
+ // TODO should this be copied?
+ netconf.DNSSearchList = odomains.DomainSearchList()
+
+ return &netconf, nil
+}
+
+// IfUp brings up an interface by name, and waits for it to come up until a timeout expires
+func IfUp(ifname string, timeout time.Duration) (netlink.Link, error) {
+ start := time.Now()
+ for time.Since(start) < timeout {
+ iface, err := netlink.LinkByName(ifname)
+ if err != nil {
+ return nil, fmt.Errorf("cannot get interface %q by name: %v", ifname, err)
+ }
+
+ // if the interface is up, return
+ if iface.Attrs().OperState == netlink.OperUp {
+ // XXX despite the OperUp state, upon the first attempt I
+ // consistently get a "cannot assign requested address" error. This
+ // may be a bug in the netlink library. Need to investigate more.
+ time.Sleep(time.Second)
+ return iface, nil
+ }
+ // otherwise try to bring it up
+ if err := netlink.LinkSetUp(iface); err != nil {
+ return nil, fmt.Errorf("interface %q: %v can't make it up: %v", ifname, iface, err)
+ }
+ }
+
+ return nil, fmt.Errorf("timed out while waiting for %s to come up", ifname)
+
+}
+
+// ConfigureInterface configures a network interface with the configuration held by a
+// NetConf structure
+func ConfigureInterface(ifname string, netconf *NetConf) error {
+ iface, err := netlink.LinkByName(ifname)
+ if err != nil {
+ return fmt.Errorf("error getting interface information for %s: %v", ifname, err)
+ }
+ // configure interfaces
+ for _, addr := range netconf.Addresses {
+ dest := &netlink.Addr{
+ IPNet: &addr.IPNet,
+ PreferedLft: addr.PreferredLifetime,
+ ValidLft: addr.ValidLifetime,
+ }
+ if err := netlink.AddrReplace(iface, dest); err != nil {
+ if os.IsExist(err) {
+ return fmt.Errorf("cannot configure %s on %s,%d,%d: %v", ifname, addr.IPNet, addr.PreferredLifetime, addr.ValidLifetime, err)
+ }
+ }
+ }
+ // configure /etc/resolv.conf
+ resolvconf := ""
+ for _, ns := range netconf.DNSServers {
+ resolvconf += fmt.Sprintf("nameserver %s\n", ns)
+ }
+ resolvconf += fmt.Sprintf("search %s\n", strings.Join(netconf.DNSSearchList, " "))
+ return ioutil.WriteFile("/etc/resolv.conf", []byte(resolvconf), 0644)
+}