270 lines
7.0 KiB
Go

// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package dhcp4 implements a DHCPv4 client.
package dhcp4
import (
"bytes"
"errors"
"fmt"
"net"
"sync"
"syscall"
"time"
"github.com/google/gopacket/layers"
"github.com/mdlayher/packet"
"github.com/rtr7/dhcp4"
"golang.org/x/sys/unix"
)
type Config struct {
RenewAfter time.Time `json:"valid_until"`
ClientIP string `json:"client_ip"` // e.g. 85.195.207.62
SubnetMask string `json:"subnet_mask"` // e.g. 255.255.255.128
Router string `json:"router"` // e.g. 85.195.207.1
DNS []string `json:"dns"` // e.g. 77.109.128.2, 213.144.129.20
}
type Client struct {
Interface *net.Interface // e.g. net.InterfaceByName("eth0")
HWAddr net.HardwareAddr
err error
once sync.Once
onceErr error
connection net.PacketConn
hardwareAddr net.HardwareAddr
hostname string
cfg Config
timeNow func() time.Time
generateXID func() uint32
timeoutCount int
// last DHCPACK packet for renewal/release
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, // clients set this to zero (used by relay agents)
Xid: xid,
Secs: 0, // TODO: fill in?
Flags: 0, // we can receive IP packets via unicast
ClientHWAddr: c.hardwareAddr,
ServerName: nil,
File: nil,
Options: opts,
}
}
var errNAK = errors.New("received DHCPNAK")
// ObtainOrRenew returns false when encountering a permanent error.
func (c *Client) ObtainOrRenew() bool {
c.once.Do(func() {
if c.timeNow == nil {
c.timeNow = time.Now
}
if c.connection == nil && c.Interface != nil {
conn, err := packet.Listen(c.Interface, packet.Datagram, syscall.ETH_P_IP, nil)
if err != nil {
c.onceErr = err
return
}
c.connection = conn
}
if c.connection == nil && c.Interface == nil {
c.onceErr = fmt.Errorf("c.Interface is nil")
return
}
if c.hardwareAddr == nil && c.HWAddr != nil {
c.hardwareAddr = c.HWAddr
}
if c.hardwareAddr == nil {
c.hardwareAddr = c.Interface.HardwareAddr
}
if c.generateXID == nil {
c.generateXID = dhcp4.XIDGenerator(c.hardwareAddr)
}
if c.hostname == "" {
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
c.onceErr = err
return
}
c.hostname = string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
}
})
if c.onceErr != nil {
c.err = c.onceErr
return false // permanent error
}
c.err = nil // clear previous error
ack, err := c.dhcpRequest()
if err != nil {
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN {
var serverip net.IP
for _, opt := range c.Ack.Options {
if opt.Type == layers.DHCPOptServerID {
serverip = opt.Data
}
}
c.err = fmt.Errorf("DHCP: timeout (server(s) unreachable: %v)", serverip)
c.timeoutCount++
if c.timeoutCount > 3 {
c.timeoutCount = 0
c.Ack = nil // start over at DHCPDISCOVER it has failed 3 times
}
return true // temporary error
}
if err == errNAK {
c.Ack = nil // start over at DHCPDISCOVER
}
c.err = fmt.Errorf("DHCP: %v", err)
return true // temporary error
}
c.Ack = ack
c.cfg.ClientIP = ack.YourClientIP.String()
lease := dhcp4.LeaseFromACK(ack)
if mask := lease.Netmask; len(mask) > 0 {
c.cfg.SubnetMask = fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3])
}
if len(lease.Router) > 0 {
c.cfg.Router = lease.Router.String()
}
if len(lease.DNS) > 0 {
c.cfg.DNS = make([]string, len(lease.DNS))
for idx, ip := range lease.DNS {
c.cfg.DNS[idx] = ip.String()
}
}
c.cfg.RenewAfter = c.timeNow().Add(lease.RenewalTime)
c.timeoutCount = 0
return true
}
func (c *Client) Release() error {
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 nil
}
func (c *Client) Err() error {
return c.err
}
func (c *Client) Config() Config {
return c.cfg
}
func (c *Client) dhcpRequest() (*layers.DHCPv4, error) {
var last *layers.DHCPv4
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 (described in RFC2131 4.3.1):
c.connection.SetReadDeadline(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
}
}
// 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
}
c.connection.SetReadDeadline(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) {
if dhcp4.HasMessageType(ack.Options, layers.DHCPMsgTypeNak) {
return nil, errNAK
}
continue
}
return ack, nil
}
}