dhcp4: 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:
parent
30e9a6677b
commit
8c55c5ba44
@ -28,6 +28,9 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/jpillora/backoff"
|
||||
"github.com/rtr7/router7/internal/dhcp4"
|
||||
"github.com/rtr7/router7/internal/notify"
|
||||
"github.com/rtr7/router7/internal/teelogger"
|
||||
@ -45,9 +48,15 @@ func logic() error {
|
||||
return err
|
||||
}
|
||||
const ackFn = "/perm/dhcp4/wire/ack"
|
||||
ack, err := ioutil.ReadFile(ackFn)
|
||||
var ack *layers.DHCPv4
|
||||
ackB, err := ioutil.ReadFile(ackFn)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("Loading previous DHCPACK packet from %s: %v", ackFn, err)
|
||||
} else {
|
||||
pkt := gopacket.NewPacket(ackB, layers.LayerTypeDHCPv4, gopacket.DecodeOptions{})
|
||||
if dhcp, ok := pkt.Layer(layers.LayerTypeDHCPv4).(*layers.DHCPv4); ok {
|
||||
ack = dhcp
|
||||
}
|
||||
}
|
||||
c := dhcp4.Client{
|
||||
Interface: iface,
|
||||
@ -55,12 +64,20 @@ func logic() error {
|
||||
}
|
||||
usr2 := make(chan os.Signal, 1)
|
||||
signal.Notify(usr2, syscall.SIGUSR2)
|
||||
backoff := backoff.Backoff{
|
||||
Factor: 2,
|
||||
Jitter: true,
|
||||
Min: 10 * time.Second,
|
||||
Max: 1 * time.Minute,
|
||||
}
|
||||
for c.ObtainOrRenew() {
|
||||
if err := c.Err(); err != nil {
|
||||
log.Printf("Temporary error: %v", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
dur := backoff.Duration()
|
||||
log.Printf("Temporary error: %v (waiting %v)", err, dur)
|
||||
time.Sleep(dur)
|
||||
continue
|
||||
}
|
||||
backoff.Reset()
|
||||
log.Printf("lease: %+v", c.Config())
|
||||
b, err := json.Marshal(c.Config())
|
||||
if err != nil {
|
||||
@ -69,7 +86,15 @@ func logic() error {
|
||||
if err := ioutil.WriteFile(leasePath, b, 0644); err != nil {
|
||||
return fmt.Errorf("persisting lease to %s: %v", leasePath, err)
|
||||
}
|
||||
if err := ioutil.WriteFile(ackFn, c.Ack, 0644); err != nil {
|
||||
buf := gopacket.NewSerializeBuffer()
|
||||
gopacket.SerializeLayers(buf,
|
||||
gopacket.SerializeOptions{
|
||||
FixLengths: true,
|
||||
ComputeChecksums: true,
|
||||
},
|
||||
c.Ack,
|
||||
)
|
||||
if err := ioutil.WriteFile(ackFn, buf.Bytes(), 0644); err != nil {
|
||||
return fmt.Errorf("persisting DHCPACK to %s: %v", ackFn, err)
|
||||
}
|
||||
if err := notify.Process("/user/netconfigd", syscall.SIGUSR1); err != nil {
|
||||
|
@ -17,7 +17,6 @@ package dhcp4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@ -25,10 +24,10 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/mdlayher/raw"
|
||||
"github.com/rtr7/dhcp4"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/d2g/dhcp4"
|
||||
"github.com/d2g/dhcp4client"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@ -44,15 +43,40 @@ type Client struct {
|
||||
|
||||
err error
|
||||
once sync.Once
|
||||
dhcp *dhcp4client.Client
|
||||
connection dhcp4client.ConnectionInt
|
||||
connection net.PacketConn
|
||||
hardwareAddr net.HardwareAddr
|
||||
hostname string
|
||||
cfg Config
|
||||
timeNow func() time.Time
|
||||
generateXID func([]byte)
|
||||
generateXID func() uint32
|
||||
|
||||
// last DHCPACK packet for renewal/release
|
||||
Ack dhcp4.Packet
|
||||
Ack *layers.DHCPv4
|
||||
}
|
||||
|
||||
func serverID(pkt *layers.DHCPv4) []layers.DHCPOption {
|
||||
for _, o := range pkt.Options {
|
||||
if o.Type == layers.DHCPOptServerID {
|
||||
return []layers.DHCPOption{o}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainOrRenew returns false when encountering a permanent error.
|
||||
@ -62,12 +86,14 @@ func (c *Client) ObtainOrRenew() bool {
|
||||
c.timeNow = time.Now
|
||||
}
|
||||
if c.connection == nil && c.Interface != nil {
|
||||
pktsock, err := dhcp4client.NewPacketSock(c.Interface.Index)
|
||||
conn, err := raw.ListenPacket(c.Interface, syscall.ETH_P_IP, &raw.Config{
|
||||
LinuxSockDGRAM: true,
|
||||
})
|
||||
if err != nil {
|
||||
c.err = err
|
||||
return
|
||||
}
|
||||
c.connection = pktsock
|
||||
c.connection = conn
|
||||
}
|
||||
if c.connection == nil && c.Interface == nil {
|
||||
c.err = fmt.Errorf("Interface is nil")
|
||||
@ -76,21 +102,20 @@ func (c *Client) ObtainOrRenew() bool {
|
||||
if c.hardwareAddr == nil {
|
||||
c.hardwareAddr = c.Interface.HardwareAddr
|
||||
}
|
||||
dhcp, err := dhcp4client.New(
|
||||
dhcp4client.HardwareAddr(c.hardwareAddr),
|
||||
dhcp4client.Timeout(10*time.Second),
|
||||
dhcp4client.Broadcast(false),
|
||||
dhcp4client.Connection(c.connection),
|
||||
dhcp4client.GenerateXID(c.generateXID),
|
||||
)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
return
|
||||
if c.generateXID == nil {
|
||||
c.generateXID = dhcp4.XIDGenerator(c.hardwareAddr)
|
||||
}
|
||||
if c.hostname == "" {
|
||||
var utsname unix.Utsname
|
||||
if err := unix.Uname(&utsname); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
c.hostname = string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
|
||||
}
|
||||
c.dhcp = dhcp
|
||||
})
|
||||
// TODO: handle c.err from c.once
|
||||
c.err = nil // clear previous error
|
||||
ok, ack, err := c.dhcpRequest()
|
||||
ack, err := c.dhcpRequest()
|
||||
if err != nil {
|
||||
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN {
|
||||
c.err = fmt.Errorf("DHCP: timeout (server(s) unreachable)")
|
||||
@ -99,59 +124,53 @@ func (c *Client) ObtainOrRenew() bool {
|
||||
c.err = fmt.Errorf("DHCP: %v", err)
|
||||
return true // temporary error
|
||||
}
|
||||
if !ok {
|
||||
c.err = fmt.Errorf("received DHCPNAK")
|
||||
return true // temporary error
|
||||
}
|
||||
c.Ack = ack
|
||||
opts := ack.ParseOptions()
|
||||
|
||||
// DHCPACK (described in RFC2131 4.3.1)
|
||||
// - yiaddr: IP address assigned to client
|
||||
c.cfg.ClientIP = ack.YIAddr().String()
|
||||
|
||||
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
|
||||
mask := net.IPMask(b)
|
||||
c.cfg.SubnetMask = fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3])
|
||||
}
|
||||
|
||||
// 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, ok := opts[dhcp4.OptionRouter]; ok {
|
||||
c.cfg.Router = net.IP(b).String()
|
||||
}
|
||||
|
||||
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok {
|
||||
c.cfg.DNS = nil
|
||||
for len(b) > 0 {
|
||||
c.cfg.DNS = append(c.cfg.DNS, net.IP(b[:4]).String())
|
||||
b = b[4:]
|
||||
}
|
||||
}
|
||||
|
||||
c.cfg.ClientIP = ack.YourClientIP.String()
|
||||
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)
|
||||
var renewalTime *time.Duration
|
||||
|
||||
for _, opt := range dhcp4.ParseOptions(c.Ack.Options) {
|
||||
switch o := opt.(type) {
|
||||
case *dhcp4.OptSubnetMask:
|
||||
c.cfg.SubnetMask = fmt.Sprintf("%d.%d.%d.%d", o.Mask[0], o.Mask[1], o.Mask[2], o.Mask[3])
|
||||
|
||||
case *dhcp4.OptRouter:
|
||||
c.cfg.Router = o.Router.String()
|
||||
|
||||
case *dhcp4.OptDNS:
|
||||
c.cfg.DNS = make([]string, len(o.DNS))
|
||||
for idx, ip := range o.DNS {
|
||||
c.cfg.DNS[idx] = ip.String()
|
||||
}
|
||||
|
||||
case *dhcp4.OptLeaseTime:
|
||||
leaseTime = o.LeaseTime
|
||||
|
||||
case *dhcp4.OptT1:
|
||||
renewalTime = &o.T1
|
||||
}
|
||||
}
|
||||
c.cfg.RenewAfter = c.timeNow().Add(renewalTime)
|
||||
if renewalTime == nil {
|
||||
d := time.Duration(float64(leaseTime) * 0.5)
|
||||
renewalTime = &d
|
||||
}
|
||||
c.cfg.RenewAfter = c.timeNow().Add(*renewalTime)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Client) Release() error {
|
||||
err := c.dhcp.Release(c.Ack)
|
||||
release := c.packet(c.generateXID(), append([]layers.DHCPOption{
|
||||
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeRelease),
|
||||
}, serverID(c.Ack)...))
|
||||
release.ClientIP = c.Ack.YourClientIP
|
||||
if err := dhcp4.Write(c.connection, release); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Ack = nil
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Err() error {
|
||||
@ -162,78 +181,77 @@ func (c *Client) Config() Config {
|
||||
return c.cfg
|
||||
}
|
||||
|
||||
func parseDHCPDuration(b []byte) time.Duration {
|
||||
return time.Duration(binary.BigEndian.Uint32(b)) * time.Second
|
||||
}
|
||||
func (c *Client) dhcpRequest() (*layers.DHCPv4, error) {
|
||||
var last *layers.DHCPv4
|
||||
|
||||
func (c *Client) 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)
|
||||
}
|
||||
|
||||
func (c *Client) addClientId(p *dhcp4.Packet) {
|
||||
id := make([]byte, len(c.hardwareAddr)+1)
|
||||
id[0] = 1 // hardware type ethernet, https://tools.ietf.org/html/rfc1700
|
||||
copy(id[1:], c.hardwareAddr)
|
||||
p.AddOption(dhcp4.OptionClientIdentifier, id)
|
||||
}
|
||||
|
||||
func (c *Client) addOptionParameterRequest(p *dhcp4.Packet, opts []dhcp4.OptionCode) {
|
||||
op := make([]byte, len(opts))
|
||||
for i, o := range opts {
|
||||
op[i] = byte(o)
|
||||
}
|
||||
p.AddOption(dhcp4.OptionParameterRequestList, op)
|
||||
}
|
||||
|
||||
// dhcpRequest is a copy of (dhcp4client/Client).Request which
|
||||
// includes the hostname.
|
||||
func (c *Client) dhcpRequest() (bool, dhcp4.Packet, error) {
|
||||
var last dhcp4.Packet
|
||||
if c.Ack == nil {
|
||||
discoveryPacket := c.dhcp.DiscoverPacket()
|
||||
c.addHostname(&discoveryPacket)
|
||||
c.addClientId(&discoveryPacket)
|
||||
c.addOptionParameterRequest(&discoveryPacket, []dhcp4.OptionCode{dhcp4.OptionDomainNameServer, dhcp4.OptionRouter, dhcp4.OptionSubnetMask})
|
||||
discoveryPacket.PadToMinSize()
|
||||
|
||||
if err := c.dhcp.SendPacket(discoveryPacket); err != nil {
|
||||
return false, discoveryPacket, err
|
||||
}
|
||||
|
||||
offerPacket, err := c.dhcp.GetOffer(&discoveryPacket)
|
||||
if err != nil {
|
||||
return false, offerPacket, err
|
||||
}
|
||||
last = offerPacket
|
||||
} else {
|
||||
if c.Ack != nil {
|
||||
last = c.Ack
|
||||
} else {
|
||||
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.connection, discover); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Look for DHCPOFFER packet (TODO: RFC)
|
||||
c.connection.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
for {
|
||||
offer, err := dhcp4.Read(c.connection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if offer == nil {
|
||||
continue // not a DHCPv4 packet
|
||||
}
|
||||
if offer.Xid != discover.Xid {
|
||||
continue // broadcast reply for different DHCP transaction
|
||||
}
|
||||
if !dhcp4.HasMessageType(offer.Options, layers.DHCPMsgTypeOffer) {
|
||||
continue
|
||||
}
|
||||
last = offer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
requestPacket := c.dhcp.RequestPacket(&last)
|
||||
c.addHostname(&requestPacket)
|
||||
c.addClientId(&requestPacket)
|
||||
c.addOptionParameterRequest(&requestPacket, []dhcp4.OptionCode{dhcp4.OptionDomainNameServer, dhcp4.OptionRouter, dhcp4.OptionSubnetMask})
|
||||
requestPacket.PadToMinSize()
|
||||
|
||||
if err := c.dhcp.SendPacket(requestPacket); err != nil {
|
||||
return false, requestPacket, err
|
||||
// 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),
|
||||
}, serverID(last)...))
|
||||
if err := dhcp4.Write(c.connection, request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acknowledgement, err := c.dhcp.GetAcknowledgement(&requestPacket)
|
||||
if err != nil {
|
||||
return false, acknowledgement, err
|
||||
c.connection.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
for {
|
||||
// Look for DHCPACK packet (described in RFC2131 4.3.1):
|
||||
ack, err := dhcp4.Read(c.connection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
acknowledgementOptions := acknowledgement.ParseOptions()
|
||||
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
|
||||
c.Ack = nil // start over
|
||||
return false, acknowledgement, nil
|
||||
}
|
||||
|
||||
return true, acknowledgement, nil
|
||||
}
|
||||
|
@ -46,12 +46,9 @@ func TestDHCP4(t *testing.T) {
|
||||
hardwareAddr: mac,
|
||||
timeNow: func() time.Time { return now },
|
||||
connection: conn,
|
||||
generateXID: func(b []byte) {
|
||||
if got, want := len(b), 4; got != want {
|
||||
t.Fatalf("github.com/d2g/dhcp4client request unexpected amount of bytes: got %d, want %d", got, want)
|
||||
}
|
||||
generateXID: func() uint32 {
|
||||
// TODO: read the transaction ID from the pcap file
|
||||
copy(b, []byte{0x77, 0x08, 0xd7, 0x24})
|
||||
return 0x7708d724
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,6 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/d2g/dhcp4client"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcapgo"
|
||||
@ -182,19 +181,23 @@ type dhcp4conn struct {
|
||||
// pcap file input, writing packets to pcap file output (if non-empty).
|
||||
//
|
||||
// See https://en.wikipedia.org/wiki/Pcap for details on pcap.
|
||||
func NewDHCP4Conn(input, output string) (dhcp4client.ConnectionInt, error) {
|
||||
func NewDHCP4Conn(input, output string) (net.PacketConn, error) {
|
||||
pcapr, pcapw, err := pcapopen(input, output)
|
||||
return &dhcp4conn{pcapr, pcapw}, err
|
||||
return &dhcp4conn{pcapr: pcapr, pcapw: pcapw}, err
|
||||
}
|
||||
|
||||
func (r *dhcp4conn) Close() error { return nil }
|
||||
func (r *dhcp4conn) SetReadTimeout(t time.Duration) error { return nil }
|
||||
func (r *dhcp4conn) LocalAddr() net.Addr { return nil }
|
||||
func (r *dhcp4conn) Close() error { return nil }
|
||||
func (r *dhcp4conn) SetDeadline(t time.Time) error { return nil }
|
||||
func (r *dhcp4conn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (r *dhcp4conn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (r *dhcp4conn) Write(b []byte) error {
|
||||
func (r *dhcp4conn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||
if r.pcapw == nil {
|
||||
return nil
|
||||
return len(b), nil
|
||||
}
|
||||
return pcapwrite(r.pcapw,
|
||||
|
||||
return len(b), pcapwrite(r.pcapw,
|
||||
&layers.IPv4{
|
||||
Version: 4,
|
||||
TTL: 255,
|
||||
@ -209,8 +212,20 @@ func (r *dhcp4conn) Write(b []byte) error {
|
||||
b)
|
||||
}
|
||||
|
||||
func (r *dhcp4conn) ReadFrom() ([]byte, net.IP, error) {
|
||||
buf := make([]byte, 9000)
|
||||
_, ip, err := readFrom(r.pcapr, buf)
|
||||
return buf, ip, err
|
||||
func (r *dhcp4conn) ReadFrom(buf []byte) (int, net.Addr, error) {
|
||||
data, _, err := r.pcapr.ReadPacketData()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
pkt := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.DecodeOptions{})
|
||||
// TODO: get source IP
|
||||
eth := pkt.Layer(layers.LayerTypeEthernet)
|
||||
if eth == nil {
|
||||
return 0, nil, fmt.Errorf("pcap contained unexpected non-IPv4 packet")
|
||||
}
|
||||
|
||||
//log.Printf("ReadFrom(): %x, %v, pkt = %+v", udp.LayerPayload(), err, pkt)
|
||||
copy(buf, eth.LayerPayload())
|
||||
ip := net.ParseIP("192.168.23.1")
|
||||
return len(eth.LayerPayload()), &net.IPAddr{IP: ip}, nil
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user