From 4da458ed1d2abe3eaa398b551d91ac79c03682f9 Mon Sep 17 00:00:00 2001 From: Marco Guerri Date: Wed, 7 Nov 2018 23:38:39 +0000 Subject: Add netboot/netconf support for DHCPv4 (#185) --- netboot/netboot.go | 63 +++++++++++++++++++++++++++- netboot/netconf.go | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 181 insertions(+), 2 deletions(-) diff --git a/netboot/netboot.go b/netboot/netboot.go index 2f4cc8f..8bb9ac4 100644 --- a/netboot/netboot.go +++ b/netboot/netboot.go @@ -6,9 +6,14 @@ import ( "log" "time" + "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv6" ) +var sleeper = func(d time.Duration) { + time.Sleep(d) +} + // 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) { @@ -36,7 +41,38 @@ func RequestNetbootv6(ifname string, timeout time.Duration, retries int, modifie // don't wait at the end of the last attempt break } - time.Sleep(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 := dhcpv4.NewClient() + client.ReadTimeout = timeout + conversation, err = client.Exchange(ifname, nil, 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 + } + sleeper(delay) // TODO add random splay delay = delay * 2 continue @@ -95,3 +131,28 @@ func ConversationToNetconf(conversation []dhcpv6.DHCPv6) (*NetConf, string, erro } return netconf, bootfile, 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) { + 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.BootFileNameToString() + reply = m + break + } + } + if reply == nil { + return nil, "", errors.New("no OFFER with valid bootfile URL received") + } + netconf, err := GetNetConfFromPacketv4(reply) + if err != nil { + return nil, "", fmt.Errorf("could not get netconf: %v", err) + } + return netconf, bootFileUrl, nil +} diff --git a/netboot/netconf.go b/netboot/netconf.go index 84fb263..50e339c 100644 --- a/netboot/netconf.go +++ b/netboot/netconf.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/vishvananda/netlink" ) @@ -25,6 +26,7 @@ type NetConf struct { Addresses []AddrConf DNSServers []net.IP DNSSearchList []string + Routers []net.IP } // GetNetConfFromPacketv6 extracts network configuration information from a DHCPv6 @@ -74,6 +76,89 @@ func GetNetConfFromPacketv6(d *dhcpv6.DHCPv6Message) (*NetConf, error) { return &netconf, nil } +// GetNetConfFromPacketv4 extracts network configuration information from a DHCPv4 +// Reply packet and returns a populated NetConf structure +func GetNetConfFromPacketv4(d *dhcpv4.DHCPv4) (*NetConf, error) { + // extract the address from the DHCPv4 address + ipAddr := d.YourIPAddr() + if ipAddr.Equal(net.IPv4zero) { + return nil, errors.New("ip address is null (0.0.0.0)") + } + netconf := NetConf{} + + // get the subnet mask from OptionSubnetMask. If the netmask is not defined + // in the packet, an error is returned + netmaskOption := d.GetOneOption(dhcpv4.OptionSubnetMask) + if netmaskOption == nil { + return nil, errors.New("no netmask option in response packet") + } + netmask := netmaskOption.(*dhcpv4.OptSubnetMask).SubnetMask + ones, _ := netmask.Size() + if ones == 0 { + return nil, errors.New("netmask extracted from OptSubnetMask options is null") + } + + // netconf struct requires a valid lifetime to be specified. ValidLifetime is a dhcpv6 + // concept, the closest mapping in dhcpv4 world is "IP Address Lease Time". If the lease + // time option is nil, we set it to 0 + leaseTimeOption := d.GetOneOption(dhcpv4.OptionIPAddressLeaseTime) + leaseTime := uint32(0) + if leaseTimeOption != nil { + leaseTime = leaseTimeOption.(*dhcpv4.OptIPAddressLeaseTime).LeaseTime + } + + if int(leaseTime) < 0 { + return nil, fmt.Errorf("lease time overflow, Original lease time: %d", leaseTime) + } + + netconf.Addresses = append(netconf.Addresses, AddrConf{ + IPNet: net.IPNet{ + IP: ipAddr, + Mask: netmask, + }, + PreferredLifetime: 0, + ValidLifetime: int(leaseTime), + }) + + // get DNS configuration + dnsServersOption := d.GetOneOption(dhcpv4.OptionDomainNameServer) + if dnsServersOption == nil { + return nil, errors.New("name servers option is empty") + } + dnsServers := dnsServersOption.(*dhcpv4.OptDomainNameServer).NameServers + if len(dnsServers) == 0 { + return nil, errors.New("no dns servers options in response packet") + } + netconf.DNSServers = dnsServers + + // get domain search list + dnsDomainSearchListOption := d.GetOneOption(dhcpv4.OptionDNSDomainSearchList) + if dnsDomainSearchListOption == nil { + return nil, errors.New("no domain search list option in response packet") + + } + dnsSearchList := dnsDomainSearchListOption.(*dhcpv4.OptDomainSearch).DomainSearch + if len(dnsSearchList) == 0 { + return nil, errors.New("dns search list is empty") + } + netconf.DNSSearchList = dnsSearchList + + // get default gateway + routerOption := d.GetOneOption(dhcpv4.OptionRouter) + if routerOption == nil { + return nil, errors.New("no router option specified in response packet") + } + + routersList := routerOption.(*dhcpv4.OptRouter).Routers + if len(routersList) == 0 { + return nil, errors.New("no routers specified in the corresponding option") + } + + netconf.Routers = routersList + + 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() @@ -127,5 +212,38 @@ func ConfigureInterface(ifname string, netconf *NetConf) error { 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) + if err = ioutil.WriteFile("/etc/resolv.conf", []byte(resolvconf), 0644); err != nil { + return fmt.Errorf("could not write resolv.conf file %v", err) + } + + // add default route information for v4 space. only one default route is allowed + // so ignore the others if there are multiple ones + if len(netconf.Routers) > 0 { + iface, err = netlink.LinkByName(ifname) + if err != nil { + return fmt.Errorf("could not obtain interface when adding default route: %v", err) + } + // if there is a default v4 route, remove it, as we want to add the one we just got during + // the dhcp transaction. if the route is not present, which is the final state we want, + // an error is returned so ignore it + dst := &net.IPNet{ + IP: net.IPv4(0, 0, 0, 0), + Mask: net.CIDRMask(0, 32), + } + // Remove a possible default route (dst 0.0.0.0) to the L2 domain (gw: 0.0.0.0), which is what + // a client would want to add before initiating the DHCP transaction in order not to fail with + // ENETUNREACH. If this default route has a specific metric assigned, it doesn't get removed. + // The code doesn't remove any other default route (i.e. gw != 0.0.0.0). + route := netlink.Route{LinkIndex: iface.Attrs().Index, Dst: dst, Src: net.IPv4(0, 0, 0, 0)} + netlink.RouteDel(&route) + + src := netconf.Addresses[0].IPNet.IP + route = netlink.Route{LinkIndex: iface.Attrs().Index, Dst: dst, Src: src, Gw: netconf.Routers[0]} + err = netlink.RouteAdd(&route) + if err != nil { + return fmt.Errorf("could not add default route (%+v) to interface %s: %v", route, iface.Attrs().Name, err) + } + } + + return nil } -- cgit v1.2.3