diff options
author | insomniac <insomniacslk@users.noreply.github.com> | 2018-04-22 20:09:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-04-22 20:09:55 +0200 |
commit | ed883f5cb71409cf27b4d57f0fe3229c6c9307f9 (patch) | |
tree | 58e74558d231a91b2c1a6a920c3ee7415ecf7c9c /netboot | |
parent | 9fcc7d1ee0fdbc07d369e88c8aefc0dbb731359b (diff) |
Added netboot package (#45)
Added netboot package
Diffstat (limited to 'netboot')
-rw-r--r-- | netboot/netboot.go | 93 | ||||
-rw-r--r-- | netboot/netconf.go | 131 |
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) +} |