summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--README.md186
-rw-r--r--dhcpv6/option_vendor_opts.go117
-rw-r--r--dhcpv6/option_vendor_opts_test.go101
-rw-r--r--dhcpv6/options.go13
-rw-r--r--netboot/netboot.go63
-rw-r--r--netboot/netconf.go120
6 files changed, 595 insertions, 5 deletions
diff --git a/README.md b/README.md
index 9c8c2a8..f977363 100644
--- a/README.md
+++ b/README.md
@@ -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
}