diff options
-rw-r--r-- | README.md | 186 | ||||
-rw-r--r-- | dhcpv6/option_vendor_opts.go | 117 | ||||
-rw-r--r-- | dhcpv6/option_vendor_opts_test.go | 101 | ||||
-rw-r--r-- | dhcpv6/options.go | 13 | ||||
-rw-r--r-- | netboot/netboot.go | 63 | ||||
-rw-r--r-- | netboot/netconf.go | 120 |
6 files changed, 595 insertions, 5 deletions
@@ -2,9 +2,191 @@ DHCPv4 and DHCPv6 decoding/encoding library with client and server code, written in Go. -See examples at https://github.com/insomniacslk/exdhcp +# How to get the library -## Public projects that use it +The library is split into several parts: +* `dhcpv6`: implementation of DHCPv6 packet, client and server +* `dhcpv4`: implementation of DHCPv4 packet, client and server +* `netboot`: network booting wrappers on top of `dhcpv6` and `dhcpv4` +* `iana`: several IANA constants, and helpers used by `dhcpv6` and `dhcpv4` +* `rfc1035label`: simple implementation of RFC1035 labels, used by `dhcpv6` and + `dhcpv4` + +You will probably only need `dhcpv6` and/or `dhcpv4` explicitly. The rest is +pulled in automatically if necessary. + + +So, to get `dhcpv6` and `dhpv4` just run: +``` +go get -u github.com/insomniacslk/dhcp/dhcpv{4,6} +``` + + +# Examples + +The sections below will illustrate how to use the `dhcpv6` and `dhcpv4` +packages. + +See more example code at https://github.com/insomniacslk/exdhcp + + +## DHCPv6 client + +To run a DHCPv6 transaction on the interface "eth0": + +``` +package main + +import ( + "log" + + "github.com/insomniacslk/dhcp/dhcpv6" +) + + +func main() { + // NewClient sets up a new DHCPv6 client with default values + // for read and write timeouts, for destination address and listening + // address + client := dhcpv6.NewClient() + + // Exchange runs a Solicit-Advertise-Request-Reply transaction on the + // specified network interface, and returns a list of DHCPv6 packets + // (a "conversation") and an error if any. Notice that Exchange may + // return a non-empty packet list even if there is an error. This is + // intended, because the transaction may fail at any point, and we + // still want to know what packets were exchanged until then. + // The `nil` argument indicates that we want to use a default Solicit + // packet, instead of specifying a custom one ourselves. + conversation, err := client.Exchange("eth0", nil) + + // Summary() prints a verbose representation of the exchanged packets. + for _, packet := range conversation { + log.Print(packet.Summary()) + } + // error handling is done *after* printing, so we still print the + // exchanged packets if any, as explained above. + if err != nil { + log.Fatal(err) + } +} +``` + + +## DHCPv6 packet crafting and manipulation + +``` +package main + +import ( + "log" + "net" + + "github.com/insomniacslk/dhcp/dhcpv6" + "github.com/insomniacslk/dhcp/iana" +) + +func main() { + // In this example we create and manipulate a DHCPv6 solicit packet + // and encapsulate it in a relay packet. To to this, we use + // `dhcpv6.DHCPv6Message` and `dhcpv6.DHCPv6Relay`, two structures + // that implement the `dhcpv6.DHCPv6` interface. + // Then print the wire-format representation of the packet. + + // Create the DHCPv6 Solicit first, using the interface "eth0" + // to get the MAC address + msg, err := dhcpv6.NewSolicitForInterface("eth0") + if err != nil { + log.Fatal(err) + } + + // In this example I want to redact the MAC address of my + // network interface, so instead of replacing it manually, + // I will show how to use modifiers for the purpose. + // A Modifier is simply a function that can be applied on + // a DHCPv6 object to manipulate it. Here we use it to + // replace the MAC address with a dummy one. + // Modifiers can be passed to many functions, for example + // to constructors, `Exchange()`, `Solicit()`, etc. Check + // the source code to know where to use them. + // Existing modifiers are implemented in dhcpv6/modifiers.go . + mac, err := net.ParseMAC("00:fa:ce:b0:0c:00") + if err != nil { + log.Fatal(err) + } + duid := dhcpv6.Duid{ + Type: dhcpv6.DUID_LLT, + HwType: iana.HwTypeEthernet, + Time: dhcpv6.GetTime(), + LinkLayerAddr: mac, + } + // As suggested above, an alternative is to call + // dhcpv6.NewSolicitForInterface("eth0", dhcpv6.WithCLientID(duid)) + msg = dhcpv6.WithClientID(duid)(msg) + + // Now encapsulate the message in a DHCPv6 relay. + // As per RFC3315, the link-address and peer-address have + // to be set by the relay agent. We use dummy values here. + linkAddr := net.ParseIP("2001:0db8::1") + peerAddr := net.ParseIP("2001:0db8::2") + relay, err := dhcpv6.EncapsulateRelay(msg, dhcpv6.MessageTypeRelayForward, linkAddr, peerAddr) + if err != nil { + log.Fatal(err) + } + + // Print a verbose representation of the relay packet, that will also + // show a short representation of the inner Solicit message. + // To print a detailed summary of the inner packet, extract it + // first from the relay using `relay.GetInnerMessage()`. + log.Print(relay.Summary()) + + // And finally, print the bytes that would be sent on the wire + log.Print(relay.ToBytes()) + + // Note: there are many more functions in the library, check them + // out in the source code. For example, if you want to decode a + // byte stream into a DHCPv6 message or relay, you can use + // `dhcpv6.FromBytes`. +} +``` + +The output (slightly modified for readability) is +``` +$ go run main.go +2018/11/08 13:56:31 DHCPv6Relay + messageType=RELAY-FORW + hopcount=0 + linkaddr=2001:db8::1 + peeraddr=2001:db8::2 + options=[OptRelayMsg{relaymsg=DHCPv6Message(messageType=SOLICIT transactionID=0x9e0242, 4 options)}] + +2018/11/08 13:56:31 [12 0 32 1 13 184 0 0 0 0 0 0 0 0 0 0 0 1 32 1 13 184 + 0 0 0 0 0 0 0 0 0 0 0 2 0 9 0 52 1 158 2 66 0 1 0 14 + 0 1 0 1 35 118 253 15 0 250 206 176 12 0 0 6 0 4 0 23 + 0 24 0 8 0 2 0 0 0 3 0 12 250 206 176 12 0 0 14 16 0 + 0 21 24] +``` + +## DHCPv6 server + +TODO + +## DHCPv4 client + +TODO + + +## DHCPv4 packet parsing + +TODO + + +## DHCPv4 server + +TODO + + +# Public projects that use it * Facebook's DHCP load balancer, `dhcplb`, https://github.com/facebookincubator/dhcplb * Systemboot, a LinuxBoot distribution that runs as system firmware, https://github.com/systemboot/systemboot diff --git a/dhcpv6/option_vendor_opts.go b/dhcpv6/option_vendor_opts.go new file mode 100644 index 0000000..a2281e3 --- /dev/null +++ b/dhcpv6/option_vendor_opts.go @@ -0,0 +1,117 @@ +package dhcpv6 + +/* + This module defines the OptVendorOpts structure. + https://tools.ietf.org/html/rfc3315#section-22.17 + + Option 17 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | OPTION_VENDOR_OPTS | option-len | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | enterprise-number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . + . option-data (sub-options) . + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Sub-Option + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | opt-code | option-len | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . + . option-data . + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +*/ + +import ( + "encoding/binary" + "errors" + "fmt" +) + +// OptVendorOpts represents a DHCPv6 Status Code option +type OptVendorOpts struct { + EnterpriseNumber uint32 + VendorOpts []Option +} + +// Code returns the option code +func (op *OptVendorOpts) Code() OptionCode { + return OptionVendorOpts +} + +// ToBytes serializes the option and returns it as a sequence of bytes +func (op *OptVendorOpts) ToBytes() []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint16(buf[0:2], uint16(OptionVendorOpts)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + binary.BigEndian.PutUint32(buf[4:8], uint32(op.EnterpriseNumber)) + for _, opt := range op.VendorOpts { + buf = append(buf, opt.ToBytes()...) + } + return buf +} + +// Length returns the option length +func (op *OptVendorOpts) Length() int { + l := 4 // 4 bytes for Enterprise Number + for _, opt := range op.VendorOpts { + l += 4 + opt.Length() // 4 bytes for Code and Length from Vendor + } + return l +} + +// String returns a string representation of the VendorOpts data +func (op *OptVendorOpts) String() string { + return fmt.Sprintf("OptVendorOpts{enterprisenum=%v, vendorOpts=%v}", + op.EnterpriseNumber, op.VendorOpts, + ) +} + +// ParseOptVendorOpts builds an OptVendorOpts structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptVendorOpts(data []byte) (*OptVendorOpts, error) { + opt := OptVendorOpts{} + if len(data) < 4 { + return nil, fmt.Errorf("Invalid vendor opts data length. Expected at least 4 bytes, got %v", len(data)) + } + opt.EnterpriseNumber = binary.BigEndian.Uint32(data[:4]) + + var err error + opt.VendorOpts, err = OptionsFromBytesWithParser(data[4:], vendParseOption) + if err != nil { + return nil, err + } + return &opt, nil +} + +// vendParseOption builds a GenericOption from a slice of bytes +// We cannot use the exisitng ParseOption function in options.go because the +// sub-options include codes specific to each vendor. There are overlaps in these +// codes with RFC standard codes. +func vendParseOption(dataStart []byte) (Option, error) { + // Parse a sequence of bytes as a single DHCPv6 option. + // Returns the option structure, or an error if any. + + if len(dataStart) < 4 { + return nil, fmt.Errorf("Invalid DHCPv6 vendor option: less than 4 bytes") + } + code := OptionCode(binary.BigEndian.Uint16(dataStart[:2])) + length := int(binary.BigEndian.Uint16(dataStart[2:4])) + if len(dataStart) < length+4 { + return nil, fmt.Errorf("Invalid option length for vendor option %v. Declared %v, actual %v", + code, length, len(dataStart)-4, + ) + } + + optData := dataStart[4 : 4+length] + if len(optData) < 1 { + return nil, errors.New("vendParseOption: at least one vendor options data is required") + } + + return &OptionGeneric{OptionCode: code, OptionData: optData}, nil +} diff --git a/dhcpv6/option_vendor_opts_test.go b/dhcpv6/option_vendor_opts_test.go new file mode 100644 index 0000000..8e62506 --- /dev/null +++ b/dhcpv6/option_vendor_opts_test.go @@ -0,0 +1,101 @@ +package dhcpv6 + +import ( + "bytes" + "fmt" + "reflect" + "testing" +) + +func TestOptVendorOpts(t *testing.T) { + optData := []byte("Arista;DCS-7304;01.00;HSH14425148") + expected := []byte{0xaa, 0xbb, 0xcc, 0xdd} + expected = append(expected, []byte{0, 1, //code + 0, byte(len(optData)), //length + }...) + expected = append(expected, optData...) + expectedOpts := OptVendorOpts{} + var vendorOpts []Option + expectedOpts.VendorOpts = append(vendorOpts, &OptionGeneric{OptionCode: 1, OptionData: optData}) + opt, err := ParseOptVendorOpts(expected) + if err != nil { + t.Fatal(err) + } + + if optLen := opt.Length(); optLen != len(expected) { + t.Fatalf("Invalid length. Expected %v, got %v", len(expected), optLen) + } + if en := opt.EnterpriseNumber; en != 0xaabbccdd { + t.Fatalf("Invalid Enterprise Number. Expected 0xaabbccdd, got %v", en) + } + if !reflect.DeepEqual(opt.VendorOpts, expectedOpts.VendorOpts) { + t.Fatalf("Invalid Vendor Option Data. Expected %v, got %v", expected, expectedOpts.VendorOpts) + } + + shortData := make([]byte, 1) + opt, err = ParseOptVendorOpts(shortData) + if err == nil { + t.Fatalf("Short data (<4 bytes) did not cause an error when it should have") + } + +} + +func TestOptVendorOptsToBytes(t *testing.T) { + optData := []byte("Arista;DCS-7304;01.00;HSH14425148") + var opts []Option + opts = append(opts, &OptionGeneric{OptionCode: 1, OptionData: optData}) + + var expected []byte + expected = append(expected, []byte{0, 17, // VendorOption Code 17 + 0, byte(len(optData) + 8), // Length of optionData + 4 (code & length of sub-option) + 4 for EnterpriseNumber Length + 0, 0, 0, 0, // EnterpriseNumber + 0, 1, // Sub-Option code from vendor + 0, byte(len(optData))}...) // Length of optionData only + expected = append(expected, optData...) + + opt := OptVendorOpts{ + EnterpriseNumber: 0000, + VendorOpts: opts, + } + toBytes := opt.ToBytes() + if !bytes.Equal(toBytes, expected) { + t.Fatalf("Invalid ToBytes result. Expected %v, got %v", expected, toBytes) + } +} + +func TestVendParseOption(t *testing.T) { + var buf []byte + buf = append(buf, []byte{00, 1, 00, 33}...) + buf = append(buf, []byte("Arista;DCS-7304;01.00;HSH14425148")...) + + expected := &OptionGeneric{OptionCode: 1, OptionData: []byte("Arista;DCS-7304;01.00;HSH14425148")} + opt, err := vendParseOption(buf) + if err != nil { + fmt.Println(err) + } + if !reflect.DeepEqual(opt, expected) { + t.Fatalf("Invalid Vendor Parse Option result. Expected %v, got %v", expected, opt) + } + + + shortData := make([]byte, 1) // data length too small + opt, err = vendParseOption(shortData) + if err == nil { + t.Fatalf("Short data (<4 bytes) did not cause an error when it should have") + } + + shortData = []byte{0, 0, 0, 0} // missing actual vendor data. + opt, err = vendParseOption(shortData) + if err == nil { + t.Fatalf("Missing VendorData option. An error should have been returned but wasn't") + } + + shortData = []byte{0, 0, + 0, 4, // declared length + 0} // data starts here, length of 1 + opt, err = vendParseOption(shortData) + if err == nil { + t.Fatalf("Declared length does not match actual data length. An error should have been returned but wasn't") + } + +} diff --git a/dhcpv6/options.go b/dhcpv6/options.go index 5c4f321..8508b08 100644 --- a/dhcpv6/options.go +++ b/dhcpv6/options.go @@ -88,6 +88,8 @@ func ParseOption(dataStart []byte) (Option, error) { opt, err = ParseOptUserClass(optData) case OptionVendorClass: opt, err = ParseOptVendorClass(optData) + case OptionVendorOpts: + opt, err = ParseOptVendorOpts(optData) case OptionInterfaceID: opt, err = ParseOptInterfaceId(optData) case OptionDNSRecursiveNameServer: @@ -120,6 +122,15 @@ func ParseOption(dataStart []byte) (Option, error) { } func OptionsFromBytes(data []byte) ([]Option, error) { + return OptionsFromBytesWithParser(data, ParseOption) +} + +// OptionParser is a function signature for option parsing +type OptionParser func(data []byte) (Option, error) + +// OptionsFromBytesWithParser parses Options from byte sequences using the +// parsing function that is passed in as a paremeter +func OptionsFromBytesWithParser(data []byte, parser OptionParser) ([]Option, error) { // Parse a sequence of bytes until the end and build a list of options from // it. Returns an error if any invalid option or length is found. options := make([]Option, 0, 10) @@ -140,7 +151,7 @@ func OptionsFromBytes(data []byte) ([]Option, error) { // this should never happen return nil, fmt.Errorf("Error: reading past the end of options") } - opt, err := ParseOption(data[idx:]) + opt, err := parser(data[idx:]) if err != nil { return nil, err } 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 } |