summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarco Guerri <marcoguerri@users.noreply.github.com>2018-11-07 23:38:39 +0000
committerinsomniac <insomniacslk@users.noreply.github.com>2018-11-07 23:38:39 +0000
commit4da458ed1d2abe3eaa398b551d91ac79c03682f9 (patch)
tree6429271745967c1ebee056c2a199a2467b945e90
parent7af404c171f0b1e63112b579e246dd3dc77e56ce (diff)
Add netboot/netconf support for DHCPv4 (#185)
-rw-r--r--netboot/netboot.go63
-rw-r--r--netboot/netconf.go120
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
}