dhcp: send client-id option and report NAKs

Most of the clients I have watched with tcpdump send the Client-ID
field (even though they only populate it with the MAC address which
is already sent in chaddr), so for consistency let's send it too.

Change-Id: I096908d6bc2d40ddfdad9a0f749fcc1868552c02
diff --git a/dhcp/client.go b/dhcp/client.go
index 4ba3fff..e4d6de2 100644
--- a/dhcp/client.go
+++ b/dhcp/client.go
@@ -149,6 +149,13 @@
 	if requestedAddr != "" {
 		options = append(options, option{optReqIPAddr, []byte(requestedAddr)})
 	}
+	var clientID []byte
+	if len(c.linkAddr) == 6 {
+		clientID = make([]byte, 7)
+		clientID[0] = 1 // htype: ARP Ethernet from RFC 1700
+		copy(clientID[1:], c.linkAddr)
+		options = append(options, option{optClientID, clientID})
+	}
 	h := make(header, headerBaseSize+options.len())
 	h.init()
 	h.setOp(opRequest)
@@ -229,11 +236,15 @@
 	for i, b := 0, h.yiaddr(); i < len(b); i++ {
 		b[i] = 0
 	}
-	h.setOptions([]option{
+	options = []option{
 		{optDHCPMsgType, []byte{byte(dhcpREQUEST)}},
 		{optReqIPAddr, []byte(addr)},
 		{optDHCPServer, []byte(cfg.ServerAddress)},
-	})
+	}
+	if len(clientID) != 0 {
+		options = append(options, option{optClientID, clientID})
+	}
+	h.setOptions(options)
 	if _, err := ep.Write([]byte(h), serverAddr); err != nil {
 		return fmt.Errorf("dhcp discovery write: %v", err)
 	}
@@ -266,6 +277,12 @@
 	if err != nil {
 		return fmt.Errorf("dhcp ack: %v", err)
 	}
+	if msgtype == dhcpNAK {
+		if msg := opts.message(); msg != "" {
+			return fmt.Errorf("dhcp: NAK %q", msg)
+		}
+		return fmt.Errorf("dhcp: NAK with no message")
+	}
 	ack = msgtype == dhcpACK
 	if !ack {
 		return fmt.Errorf("dhcp: request not acknowledged")
diff --git a/dhcp/dhcp.go b/dhcp/dhcp.go
index 63d0184..0633236 100644
--- a/dhcp/dhcp.go
+++ b/dhcp/dhcp.go
@@ -179,6 +179,8 @@
 	optDHCPMsgType      optionCode = 53 // dhcpMsgType
 	optDHCPServer       optionCode = 54
 	optParamReq         optionCode = 55
+	optMessage          optionCode = 56
+	optClientID         optionCode = 61
 )
 
 func (code optionCode) lenValid(l int) bool {
@@ -190,6 +192,8 @@
 		return l == 1
 	case optDomainNameServer:
 		return l%4 == 0
+	case optMessage, optClientID:
+		return l >= 1
 	case optParamReq:
 		return true // no fixed length
 	default:
@@ -215,6 +219,10 @@
 		return "option(server)"
 	case optParamReq:
 		return "option(parameter-request)"
+	case optMessage:
+		return "option(message)"
+	case optClientID:
+		return "option(client-id)"
 	default:
 		return fmt.Sprintf("option(%d)", code)
 	}
@@ -238,6 +246,15 @@
 	return 0, nil
 }
 
+func (opts options) message() string {
+	for _, opt := range opts {
+		if opt.code == optMessage {
+			return string(opt.body)
+		}
+	}
+	return ""
+}
+
 func (opts options) len() int {
 	l := 0
 	for _, opt := range opts {