dhcp: switch to github.com/rtr7/dhcp4

All existing DHCPv4 packages I looked at were unappealing for one reason or
another, so we’re now using a little helper to glue github.com/google/gopacket
and github.com/mdlayher/raw together, which suffices for our use-case and gives
us more control.
This commit is contained in:
Michael Stapelberg 2018-11-21 08:43:49 +01:00
parent a6ce446055
commit 14287515bc

View File

@ -9,7 +9,6 @@ package main
import (
"bytes"
"encoding/binary"
"fmt"
"io/ioutil"
"log"
@ -19,101 +18,109 @@ import (
"syscall"
"time"
"golang.org/x/sys/unix"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
"github.com/gokrazy/gokrazy/internal/iface"
"github.com/google/gopacket/layers"
"github.com/mdlayher/raw"
"github.com/rtr7/dhcp4"
"golang.org/x/sys/unix"
)
func parseDHCPDuration(b []byte) time.Duration {
return time.Duration(binary.BigEndian.Uint32(b)) * time.Second
}
var (
defaultDst = net.IP([]byte{0, 0, 0, 0})
defaultNetmask = net.IPMask([]byte{0, 0, 0, 0})
hardwareAddr net.HardwareAddr
)
func addHostname(p *dhcp4.Packet) {
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
log.Fatal(err)
}
nnb := utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)]
p.AddOption(dhcp4.OptionHostName, nnb)
type client struct {
hostname string
hardwareAddr net.HardwareAddr
generateXID func() uint32
conn net.PacketConn
}
func addClientId(p *dhcp4.Packet) {
id := make([]byte, len(hardwareAddr)+1)
id[0] = 1 // hardware type ethernet, https://tools.ietf.org/html/rfc1700
copy(id[1:], hardwareAddr)
p.AddOption(dhcp4.OptionClientIdentifier, id)
func (c *client) packet(xid uint32, opts []layers.DHCPOption) *layers.DHCPv4 {
return &layers.DHCPv4{
Operation: layers.DHCPOpRequest,
HardwareType: layers.LinkTypeEthernet,
HardwareLen: uint8(len(layers.EthernetBroadcast)),
HardwareOpts: 0, // TODO: document
Xid: xid,
Secs: 0, // TODO: fill in?
Flags: 0, // TODO: document
ClientHWAddr: c.hardwareAddr,
ServerName: nil,
File: nil,
Options: opts,
}
}
// dhcpRequest is a copy of (dhcp4client/Client).Request which
// includes the hostname.
func dhcpRequest(c *dhcp4client.Client) (bool, dhcp4.Packet, error) {
discoveryPacket := c.DiscoverPacket()
addHostname(&discoveryPacket)
addClientId(&discoveryPacket)
discoveryPacket.PadToMinSize()
if err := c.SendPacket(discoveryPacket); err != nil {
return false, discoveryPacket, err
func (c *client) discover() (*layers.DHCPv4, error) {
discover := c.packet(c.generateXID(), []layers.DHCPOption{
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeDiscover),
dhcp4.HostnameOpt(c.hostname),
dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
dhcp4.ParamsRequestOpt(
layers.DHCPOptDNS,
layers.DHCPOptRouter,
layers.DHCPOptSubnetMask),
})
if err := dhcp4.Write(c.conn, discover); err != nil {
return nil, err
}
offerPacket, err := c.GetOffer(&discoveryPacket)
// Look for DHCPOFFER packet (TODO: RFC)
c.conn.SetDeadline(time.Now().Add(5 * time.Second))
for {
offer, err := dhcp4.Read(c.conn)
if err != nil {
return false, offerPacket, err
return nil, err
}
requestPacket := c.RequestPacket(&offerPacket)
addHostname(&requestPacket)
addClientId(&requestPacket)
requestPacket.PadToMinSize()
if err := c.SendPacket(requestPacket); err != nil {
return false, requestPacket, err
if offer == nil {
continue // not a DHCPv4 packet
}
acknowledgement, err := c.GetAcknowledgement(&requestPacket)
if err != nil {
return false, acknowledgement, err
if offer.Xid != discover.Xid {
continue // broadcast reply for different DHCP transaction
}
acknowledgementOptions := acknowledgement.ParseOptions()
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
return false, acknowledgement, nil
if !dhcp4.HasMessageType(offer.Options, layers.DHCPMsgTypeOffer) {
continue
}
return offer, nil
}
return true, acknowledgement, nil
}
// dhcpRenew is a copy of (dhcp4client/Client).Renew which
// includes the hostname.
func dhcpRenew(c *dhcp4client.Client, packet dhcp4.Packet) (bool, dhcp4.Packet, error) {
addHostname(&packet)
addClientId(&packet)
packet.PadToMinSize()
if err := c.SendPacket(packet); err != nil {
return false, packet, err
func (c *client) request(last *layers.DHCPv4) (*layers.DHCPv4, error) {
// Build a DHCPREQUEST packet:
request := c.packet(last.Xid, append([]layers.DHCPOption{
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeRequest),
dhcp4.RequestIPOpt(last.YourClientIP),
dhcp4.HostnameOpt(c.hostname),
dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
dhcp4.ParamsRequestOpt(
layers.DHCPOptDNS,
layers.DHCPOptRouter,
layers.DHCPOptSubnetMask),
}, dhcp4.ServerID(last.Options)...))
if err := dhcp4.Write(c.conn, request); err != nil {
return nil, err
}
acknowledgement, err := c.GetAcknowledgement(&packet)
c.conn.SetDeadline(time.Now().Add(10 * time.Second))
for {
// Look for DHCPACK packet (described in RFC2131 4.3.1):
ack, err := dhcp4.Read(c.conn)
if err != nil {
return false, acknowledgement, err
return nil, err
}
acknowledgementOptions := acknowledgement.ParseOptions()
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
return false, acknowledgement, nil
if ack == nil {
continue // not a DHCPv4 packet
}
if ack.Xid != request.Xid {
continue // broadcast reply for different DHCP transaction
}
if !dhcp4.HasMessageType(ack.Options, layers.DHCPMsgTypeAck) {
continue
}
return ack, nil
}
return true, acknowledgement, nil
}
func main() {
@ -122,6 +129,12 @@ func main() {
// NOTE: cannot gokrazy.WaitForClock() here, since the clock can only be
// initialized once the network is up.
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
log.Fatal(err)
}
hostname := string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
eth0, err := net.InterfaceByName("eth0")
if err != nil {
log.Fatal(err)
@ -148,73 +161,67 @@ func main() {
time.Sleep(1 * time.Second)
}
hardwareAddr = eth0.HardwareAddr
pktsock, err := dhcp4client.NewPacketSock(eth0.Index)
if err != nil {
log.Fatal(err)
}
dhcp, err := dhcp4client.New(
dhcp4client.HardwareAddr(eth0.HardwareAddr),
dhcp4client.Timeout(5*time.Second),
dhcp4client.Broadcast(false),
dhcp4client.Connection(pktsock),
)
conn, err := raw.ListenPacket(eth0, syscall.ETH_P_IP, &raw.Config{
LinuxSockDGRAM: true,
})
if err != nil {
log.Fatal(err)
}
ok, ack, err := dhcpRequest(dhcp)
c := &client{
hostname: hostname,
hardwareAddr: eth0.HardwareAddr,
generateXID: dhcp4.XIDGenerator(eth0.HardwareAddr),
conn: conn,
}
offer, err := c.discover()
if err != nil {
log.Fatal(err)
}
last := offer
for {
last, err = c.request(last)
if err != nil {
log.Fatal(err)
}
if !ok {
log.Fatal("received DHCPNAK")
}
opts := ack.ParseOptions()
// DHCPACK (described in RFC2131 4.3.1)
// - yiaddr: IP address assigned to client
lease := dhcp4.LeaseFromACK(last)
// Log the received DHCPACK packet:
details := []string{
fmt.Sprintf("IP %v", ack.YIAddr()),
fmt.Sprintf("IP %v", lease.IP),
}
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
if len(lease.Netmask) > 0 {
ipnet := net.IPNet{
IP: ack.YIAddr(),
Mask: net.IPMask(b),
IP: lease.IP,
Mask: lease.Netmask,
}
details[0] = fmt.Sprintf("IP %v", ipnet.String())
}
if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok {
details = append(details, fmt.Sprintf("broadcast %v", net.IP(b)))
if len(lease.Router) > 0 {
details = append(details, fmt.Sprintf("router %v", lease.Router))
}
if b, ok := opts[dhcp4.OptionRouter]; ok {
details = append(details, fmt.Sprintf("router %v", net.IP(b)))
if len(lease.DNS) > 0 {
details = append(details, fmt.Sprintf("DNS %v", lease.DNS))
}
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok {
details = append(details, fmt.Sprintf("DNS %v", net.IP(b)))
if len(lease.Broadcast) > 0 {
details = append(details, fmt.Sprintf("broadcast %v", lease.Broadcast))
}
log.Printf("DHCPACK: %v", strings.Join(details, ", "))
if err := cs.SetAddress(ack.YIAddr()); err != nil {
// Apply the received settings:
if err := cs.SetAddress(lease.IP); err != nil {
log.Fatal(err)
}
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
if err := cs.SetNetmask(net.IPMask(b)); err != nil {
log.Fatalf("setNetmask(%v): %v", net.IPMask(b), err)
if len(lease.Netmask) > 0 {
if err := cs.SetNetmask(lease.Netmask); err != nil {
log.Fatalf("setNetmask(%v): %v", lease.Netmask, err)
}
}
if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok {
if err := cs.SetBroadcast(net.IP(b)); err != nil {
log.Fatalf("setBroadcast(%v): %v", net.IP(b), err)
if b := lease.Broadcast; len(b) > 0 {
if err := cs.SetBroadcast(b); err != nil {
log.Fatalf("setBroadcast(%v): %v", b, err)
}
}
@ -222,22 +229,22 @@ func main() {
log.Fatal(err)
}
if b, ok := opts[dhcp4.OptionRouter]; ok {
if errno := cs.AddRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
if r := lease.Router; len(r) > 0 {
if errno := cs.AddRoute(defaultDst, r, defaultNetmask); errno != 0 {
if errno == syscall.EEXIST {
if errno := cs.DelRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
log.Printf("delRoute(%v): %v", net.IP(b), errno)
if errno := cs.DelRoute(defaultDst, r, defaultNetmask); errno != 0 {
log.Printf("delRoute(%v): %v", r, errno)
}
if errno := cs.AddRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
log.Fatalf("addRoute(%v): %v", net.IP(b), errno)
if errno := cs.AddRoute(defaultDst, r, defaultNetmask); errno != 0 {
log.Fatalf("addRoute(%v): %v", r, errno)
}
} else {
log.Fatalf("addRoute(%v): %v", net.IP(b), errno)
log.Fatalf("addRoute(%v): %v", r, errno)
}
}
}
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok {
if len(lease.DNS) > 0 {
resolvConf := "/etc/resolv.conf"
if dest, err := os.Readlink("/etc/resolv.conf"); err == nil && dest == "/tmp/resolv.conf" {
resolvConf = "/tmp/resolv.conf"
@ -247,11 +254,13 @@ func main() {
log.Fatalf("resolv.conf: %v", err)
}
var lines []string
if domain, ok := opts[dhcp4.OptionDomainName]; ok {
lines = append(lines, fmt.Sprintf("domain %s", string(domain)))
lines = append(lines, fmt.Sprintf("search %s", string(domain)))
if domain := lease.Domain; domain != "" {
lines = append(lines, fmt.Sprintf("domain %s", domain))
lines = append(lines, fmt.Sprintf("search %s", domain))
}
for _, ns := range lease.DNS {
lines = append(lines, fmt.Sprintf("nameserver %v", ns))
}
lines = append(lines, fmt.Sprintf("nameserver %v", net.IP(b)))
if err := ioutil.WriteFile(resolvConf, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil {
log.Fatalf("resolv.conf: %v", err)
}
@ -263,19 +272,6 @@ func main() {
log.Printf("send SIGHUP to init: %v", err)
}
leaseTime := 10 * time.Minute // seems sensible as a fallback
if b, ok := opts[dhcp4.OptionIPAddressLeaseTime]; ok && len(b) == 4 {
leaseTime = parseDHCPDuration(b)
}
// As per RFC 2131 section 4.4.5:
// renewal time defaults to 50% of the lease time
renewalTime := time.Duration(float64(leaseTime) * 0.5)
if b, ok := opts[dhcp4.OptionRenewalTimeValue]; ok && len(b) == 4 {
renewalTime = parseDHCPDuration(b)
}
time.Sleep(renewalTime)
ok, ack, err = dhcpRenew(dhcp, dhcp.RenewalRequestPacket(&ack))
time.Sleep(lease.RenewalTime)
}
}