First implementation of DHCP server, compiles but not tested yet.
This commit is contained in:
parent
73f71364b3
commit
8b4a1ca713
48
config.go
48
config.go
|
@ -6,8 +6,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
|
||||
"github.com/AdguardTeam/AdGuardHome/dnsforward"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
@ -24,15 +24,15 @@ type configuration struct {
|
|||
ourConfigFilename string // Config filename (can be overriden via the command line arguments)
|
||||
ourBinaryDir string // Location of our directory, used to protect against CWD being somewhere else
|
||||
|
||||
BindHost string `yaml:"bind_host"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
AuthName string `yaml:"auth_name"`
|
||||
AuthPass string `yaml:"auth_pass"`
|
||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
||||
DNS dnsConfig `yaml:"dns"`
|
||||
Filters []filter `yaml:"filters"`
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
DHCP dhcpState `yaml:"dhcp"`
|
||||
BindHost string `yaml:"bind_host"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
AuthName string `yaml:"auth_name"`
|
||||
AuthPass string `yaml:"auth_pass"`
|
||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
||||
DNS dnsConfig `yaml:"dns"`
|
||||
Filters []filter `yaml:"filters"`
|
||||
UserRules []string `yaml:"user_rules"`
|
||||
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
|
||||
|
||||
sync.RWMutex `yaml:"-"`
|
||||
|
||||
|
@ -50,31 +50,6 @@ type dnsConfig struct {
|
|||
|
||||
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type dhcpState struct {
|
||||
Config dhcpConfig
|
||||
Leases []dhcpLease
|
||||
}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type dhcpConfig struct {
|
||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||
InterfaceName string `json:"interface_name" yaml:"interface_name"` // eth0, en0 and so on
|
||||
GatewayIP string `json:"gateway_ip" yaml:"gateway_ip"`
|
||||
SubnetMask string `json:"subnet_mask" yaml:"subnet_mask"`
|
||||
RangeStart string `json:"range_start" yaml:"range_start"`
|
||||
RangeEnd string `json:"range_end" yaml:"range_end"`
|
||||
LeaseDuration uint64 `json:"lease_duration" yaml:"lease_duration"` // in seconds
|
||||
}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type dhcpLease struct {
|
||||
HWAddr [6]byte `json:"mac" yaml:"hwaddr"`
|
||||
IP string `json:"ip"` // json by default keeps IP uppercase but we need lowercase
|
||||
Hostname string
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
// initialize to default values, will be changed later when reading config or parsing command line
|
||||
var config = configuration{
|
||||
ourConfigFilename: "AdGuardHome.yaml",
|
||||
|
@ -99,9 +74,6 @@ var config = configuration{
|
|||
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
|
||||
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
|
||||
},
|
||||
DHCP: dhcpState{Config: dhcpConfig{
|
||||
LeaseDuration: 12 * 60 * 60, // in seconds
|
||||
}},
|
||||
SchemaVersion: currentSchemaVersion,
|
||||
}
|
||||
|
||||
|
|
36
dhcp.go
36
dhcp.go
|
@ -2,15 +2,18 @@ package main
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
)
|
||||
|
||||
var dhcpServer = dhcpd.Server{}
|
||||
|
||||
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := map[string]interface{}{
|
||||
"config": config.DHCP.Config,
|
||||
"leases": config.DHCP.Leases,
|
||||
"config": config.DHCP,
|
||||
"leases": dhcpServer.Leases(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
@ -22,14 +25,24 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
newconfig := dhcpConfig{}
|
||||
newconfig := dhcpd.ServerConfig{}
|
||||
err := json.NewDecoder(r.Body).Decode(&newconfig)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
config.DHCP.Config = newconfig
|
||||
if newconfig.Enabled {
|
||||
err := dhcpServer.Start(&newconfig)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !newconfig.Enabled {
|
||||
dhcpServer.Stop()
|
||||
}
|
||||
config.DHCP = newconfig
|
||||
}
|
||||
|
||||
func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -93,13 +106,18 @@ func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: implement
|
||||
// implement
|
||||
func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
|
||||
found := map[string]bool{
|
||||
"found": rand.Intn(2) == 1,
|
||||
found, err := dhcpd.CheckIfOtherDHCPServersPresent(config.DHCP.InterfaceName)
|
||||
result := map[string]interface{}{
|
||||
"found": found,
|
||||
}
|
||||
if err != nil {
|
||||
result["found"] = false
|
||||
result["error"] = err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(found)
|
||||
err = json.NewEncoder(w).Encode(result)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "Failed to marshal DHCP found json: %s", err)
|
||||
return
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
package dhcpd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/krolaw/dhcp4"
|
||||
)
|
||||
|
||||
func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't find interface by name %s", ifaceName)
|
||||
}
|
||||
|
||||
// get ipv4 address of an interface
|
||||
ifaceIPNet := getIfaceIPv4(iface)
|
||||
if ifaceIPNet == nil {
|
||||
return false, fmt.Errorf("Couldn't find IPv4 address of interface %s %+v", ifaceName, iface)
|
||||
}
|
||||
|
||||
srcIP := ifaceIPNet.IP
|
||||
src := net.JoinHostPort(srcIP.String(), "68")
|
||||
dst := "255.255.255.255:67"
|
||||
|
||||
// form a DHCP request packet, try to emulate existing client as much as possible
|
||||
xId := make([]byte, 8)
|
||||
n, err := rand.Read(xId)
|
||||
if n != 8 && err == nil {
|
||||
err = fmt.Errorf("Generated less than 8 bytes")
|
||||
}
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't generate 8 random bytes")
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't get hostname")
|
||||
}
|
||||
requestList := []byte{
|
||||
byte(dhcp4.OptionSubnetMask),
|
||||
byte(dhcp4.OptionClasslessRouteFormat),
|
||||
byte(dhcp4.OptionRouter),
|
||||
byte(dhcp4.OptionDomainNameServer),
|
||||
byte(dhcp4.OptionDomainName),
|
||||
byte(dhcp4.OptionDomainSearch),
|
||||
252, // private/proxy autodiscovery
|
||||
95, // LDAP
|
||||
byte(dhcp4.OptionNetBIOSOverTCPIPNameServer),
|
||||
byte(dhcp4.OptionNetBIOSOverTCPIPNodeType),
|
||||
}
|
||||
maxUDPsizeRaw := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(maxUDPsizeRaw, 1500)
|
||||
leaseTimeRaw := make([]byte, 4)
|
||||
leaseTime := uint32(math.RoundToEven(time.Duration(time.Hour * 24 * 90).Seconds()))
|
||||
binary.BigEndian.PutUint32(leaseTimeRaw, leaseTime)
|
||||
options := []dhcp4.Option{
|
||||
{dhcp4.OptionParameterRequestList, requestList},
|
||||
{dhcp4.OptionMaximumDHCPMessageSize, maxUDPsizeRaw},
|
||||
{dhcp4.OptionClientIdentifier, append([]byte{0x01}, iface.HardwareAddr...)},
|
||||
{dhcp4.OptionIPAddressLeaseTime, leaseTimeRaw},
|
||||
{dhcp4.OptionHostName, []byte(hostname)},
|
||||
}
|
||||
packet := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, nil, xId, false, options)
|
||||
|
||||
// resolve 0.0.0.0:68
|
||||
udpAddr, err := net.ResolveUDPAddr("udp4", src)
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", src)
|
||||
}
|
||||
// spew.Dump(udpAddr, err)
|
||||
|
||||
if !udpAddr.IP.To4().Equal(srcIP) {
|
||||
return false, wrapErrPrint(err, "Resolved UDP address is not %s", src)
|
||||
}
|
||||
|
||||
// resolve 255.255.255.255:67
|
||||
dstAddr, err := net.ResolveUDPAddr("udp4", dst)
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", dst)
|
||||
}
|
||||
|
||||
// bind to 0.0.0.0:68
|
||||
trace("Listening to udp4 %+v", udpAddr)
|
||||
c, err := net.ListenPacket("udp4", src)
|
||||
if c != nil {
|
||||
defer c.Close()
|
||||
}
|
||||
// spew.Dump(c, err)
|
||||
// spew.Printf("net.ListenUDP returned %v, %v\n", c, err)
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't listen to %s", src)
|
||||
}
|
||||
|
||||
// send to 255.255.255.255:67
|
||||
n, err = c.WriteTo(packet, dstAddr)
|
||||
// spew.Dump(n, err)
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst)
|
||||
}
|
||||
|
||||
// wait for answer
|
||||
trace("Waiting %v for an answer", defaultDiscoverTime)
|
||||
// TODO: replicate dhclient's behaviour of retrying several times with progressively bigger timeouts
|
||||
b := make([]byte, 1500)
|
||||
c.SetReadDeadline(time.Now().Add(defaultDiscoverTime))
|
||||
n, _, err = c.ReadFrom(b)
|
||||
if isTimeout(err) {
|
||||
// timed out -- no DHCP servers
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, wrapErrPrint(err, "Couldn't receive packet")
|
||||
}
|
||||
if n > 0 {
|
||||
b = b[:n]
|
||||
}
|
||||
// spew.Dump(n, fromAddr, err, b)
|
||||
|
||||
if n < 240 {
|
||||
// packet too small for dhcp
|
||||
return false, wrapErrPrint(err, "got packet that's too small for DHCP")
|
||||
}
|
||||
|
||||
response := dhcp4.Packet(b[:n])
|
||||
if response.HLen() > 16 {
|
||||
// invalid size
|
||||
return false, wrapErrPrint(err, "got malformed packet with HLen() > 16")
|
||||
}
|
||||
|
||||
parsedOptions := response.ParseOptions()
|
||||
_, ok := parsedOptions[dhcp4.OptionDHCPMessageType]
|
||||
if !ok {
|
||||
return false, wrapErrPrint(err, "got malformed packet without DHCP message type")
|
||||
}
|
||||
|
||||
// that's a DHCP server there
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,389 @@
|
|||
package dhcpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/krolaw/dhcp4"
|
||||
)
|
||||
|
||||
const defaultDiscoverTime = time.Second * 3
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type Lease struct {
|
||||
hwaddr net.HardwareAddr `json:"mac" yaml:"hwaddr"`
|
||||
ip net.IP `json:"ip"`
|
||||
expiry time.Time `json:"expires"`
|
||||
}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type ServerConfig struct {
|
||||
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||
InterfaceName string `json:"interface_name" yaml:"interface_name"` // eth0, en0 and so on
|
||||
GatewayIP string `json:"gateway_ip" yaml:"gateway_ip"`
|
||||
SubnetMask string `json:"subnet_mask" yaml:"subnet_mask"`
|
||||
RangeStart string `json:"range_start" yaml:"range_start"`
|
||||
RangeEnd string `json:"range_end" yaml:"range_end"`
|
||||
LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
conn *filterConn // listening UDP socket
|
||||
|
||||
ipnet *net.IPNet // if interface name changes, this needs to be reset
|
||||
|
||||
// leases
|
||||
leases []*Lease
|
||||
leaseStart net.IP // parsed from config RangeStart
|
||||
leaseStop net.IP // parsed from config RangeEnd
|
||||
leaseTime time.Duration // parsed from config LeaseDuration
|
||||
leaseOptions dhcp4.Options // parsed from config GatewayIP and SubnetMask
|
||||
|
||||
// IP address pool -- if entry is in the pool, then it's attached to a lease
|
||||
IPpool map[[4]byte]net.HardwareAddr
|
||||
|
||||
ServerConfig
|
||||
}
|
||||
|
||||
// Start will listen on port 67 and serve DHCP requests.
|
||||
// Even though config can be nil, it is not optional (at least for now), since there are no default values (yet).
|
||||
func (s *Server) Start(config *ServerConfig) error {
|
||||
if config != nil {
|
||||
s.ServerConfig = *config
|
||||
}
|
||||
|
||||
iface, err := net.InterfaceByName(s.InterfaceName)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName)
|
||||
}
|
||||
|
||||
// get ipv4 address of an interface
|
||||
s.ipnet = getIfaceIPv4(iface)
|
||||
if s.ipnet == nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", s.InterfaceName, iface)
|
||||
}
|
||||
|
||||
if s.LeaseDuration == 0 {
|
||||
s.leaseTime = time.Hour * 2
|
||||
s.LeaseDuration = uint(s.leaseTime.Seconds())
|
||||
} else {
|
||||
s.leaseTime = time.Second * time.Duration(s.LeaseDuration)
|
||||
}
|
||||
|
||||
s.leaseStart, err = parseIPv4(s.RangeStart)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart)
|
||||
}
|
||||
|
||||
s.leaseStop, err = parseIPv4(s.RangeEnd)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Failed to parse range end address %s", s.RangeEnd)
|
||||
}
|
||||
|
||||
subnet, err := parseIPv4(s.SubnetMask)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Failed to parse subnet mask %s", s.SubnetMask)
|
||||
}
|
||||
|
||||
// if !bytes.Equal(subnet, s.ipnet.Mask) {
|
||||
// s.closeConn() // in case it was already started
|
||||
// return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask)
|
||||
// }
|
||||
|
||||
router, err := parseIPv4(s.GatewayIP)
|
||||
if err != nil {
|
||||
s.closeConn() // in case it was already started
|
||||
return wrapErrPrint(err, "Failed to parse gateway IP %s", s.GatewayIP)
|
||||
}
|
||||
|
||||
s.leaseOptions = dhcp4.Options{
|
||||
dhcp4.OptionSubnetMask: subnet,
|
||||
dhcp4.OptionRouter: router,
|
||||
dhcp4.OptionDomainNameServer: s.ipnet.IP,
|
||||
}
|
||||
|
||||
// TODO: don't close if interface and addresses are the same
|
||||
if s.conn != nil {
|
||||
s.closeConn()
|
||||
}
|
||||
|
||||
c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets
|
||||
if err != nil {
|
||||
return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67")
|
||||
}
|
||||
|
||||
s.conn = c
|
||||
|
||||
go func() {
|
||||
// operate on c instead of c.conn because c.conn can change over time
|
||||
err := dhcp4.Serve(c, s)
|
||||
if err != nil {
|
||||
log.Printf("dhcp4.Serve() returned with error: %s", err)
|
||||
}
|
||||
c.Close() // in case Serve() exits for other reason than listening socket closure
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
if s.conn == nil {
|
||||
// nothing to do, return silently
|
||||
return nil
|
||||
}
|
||||
err := s.closeConn()
|
||||
if err != nil {
|
||||
return wrapErrPrint(err, "Couldn't close UDP listening socket")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeConn will close the connection and set it to zero
|
||||
func (s *Server) closeConn() error {
|
||||
if s.conn == nil {
|
||||
return nil
|
||||
}
|
||||
err := s.conn.Close()
|
||||
s.conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
|
||||
// WARNING: do not remove copy()
|
||||
// the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call
|
||||
// since we need to retain it we need to make our own copy
|
||||
hwaddrCOW := p.CHAddr()
|
||||
hwaddr := make(net.HardwareAddr, len(hwaddrCOW))
|
||||
copy(hwaddr, hwaddrCOW)
|
||||
foundLease := s.locateLease(p)
|
||||
if foundLease != nil {
|
||||
// trace("found lease for %s: %+v", hwaddr, foundLease)
|
||||
return foundLease, nil
|
||||
}
|
||||
// not assigned a lease, create new one, find IP from LRU
|
||||
trace("Lease not found for %s: creating new one", hwaddr)
|
||||
ip, err := s.findFreeIP(p, hwaddr)
|
||||
if err != nil {
|
||||
return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String())
|
||||
}
|
||||
trace("Assigning to %s IP address %s", hwaddr, ip.String())
|
||||
lease := &Lease{hwaddr: hwaddr, ip: ip}
|
||||
s.leases = append(s.leases, lease)
|
||||
return lease, nil
|
||||
}
|
||||
|
||||
func (s *Server) locateLease(p dhcp4.Packet) *Lease {
|
||||
hwaddr := p.CHAddr()
|
||||
for i := range s.leases {
|
||||
if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].hwaddr)) {
|
||||
// trace("bytes.Equal(%s, %s) returned true", hwaddr, s.leases[i].hwaddr)
|
||||
return s.leases[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) findFreeIP(p dhcp4.Packet, hwaddr net.HardwareAddr) (net.IP, error) {
|
||||
// if IP pool is nil, lazy initialize it
|
||||
if s.IPpool == nil {
|
||||
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
||||
}
|
||||
|
||||
// go from start to end, find unreserved IP
|
||||
var foundIP net.IP
|
||||
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
|
||||
newIP := dhcp4.IPAdd(s.leaseStart, i)
|
||||
foundHWaddr := s.getIPpool(newIP)
|
||||
trace("tried IP %v, got hwaddr %v", newIP, foundHWaddr)
|
||||
if foundHWaddr != nil && len(foundHWaddr) != 0 {
|
||||
// if !bytes.Equal(foundHWaddr, hwaddr) {
|
||||
// trace("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr)
|
||||
// }
|
||||
trace("will try again")
|
||||
continue
|
||||
}
|
||||
foundIP = newIP
|
||||
break
|
||||
}
|
||||
|
||||
if foundIP == nil {
|
||||
// TODO: LRU
|
||||
return nil, fmt.Errorf("Couldn't find free entry in IP pool")
|
||||
}
|
||||
|
||||
s.reserveIP(foundIP, hwaddr)
|
||||
|
||||
return foundIP, nil
|
||||
}
|
||||
|
||||
func (s *Server) getIPpool(ip net.IP) net.HardwareAddr {
|
||||
rawIP := []byte(ip)
|
||||
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
|
||||
return s.IPpool[IP4]
|
||||
}
|
||||
|
||||
func (s *Server) reserveIP(ip net.IP, hwaddr net.HardwareAddr) {
|
||||
rawIP := []byte(ip)
|
||||
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
|
||||
s.IPpool[IP4] = hwaddr
|
||||
}
|
||||
|
||||
func (s *Server) unreserveIP(ip net.IP) {
|
||||
rawIP := []byte(ip)
|
||||
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
|
||||
delete(s.IPpool, IP4)
|
||||
}
|
||||
|
||||
func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet {
|
||||
trace("Got %v message", msgType)
|
||||
trace("Leases:")
|
||||
for i, lease := range s.leases {
|
||||
trace("Lease #%d: hwaddr %s, ip %s, expiry %s", i, lease.hwaddr, lease.ip, lease.expiry)
|
||||
}
|
||||
trace("IP pool:")
|
||||
for ip, hwaddr := range s.IPpool {
|
||||
trace("IP pool entry %s -> %s", net.IPv4(ip[0], ip[1], ip[2], ip[3]), hwaddr)
|
||||
}
|
||||
// spew.Dump(s.leases, s.IPpool)
|
||||
// log.Printf("Called with msgType = %v, options = %+v", msgType, options)
|
||||
// spew.Dump(p)
|
||||
// log.Printf("%14s %v", "p.Broadcast", p.Broadcast()) // false
|
||||
// log.Printf("%14s %v", "p.CHAddr", p.CHAddr()) // 2c:f0:a2:f2:31:00
|
||||
// log.Printf("%14s %v", "p.CIAddr", p.CIAddr()) // 0.0.0.0
|
||||
// log.Printf("%14s %v", "p.Cookie", p.Cookie()) // [99 130 83 99]
|
||||
// log.Printf("%14s %v", "p.File", p.File()) // []
|
||||
// log.Printf("%14s %v", "p.Flags", p.Flags()) // [0 0]
|
||||
// log.Printf("%14s %v", "p.GIAddr", p.GIAddr()) // 0.0.0.0
|
||||
// log.Printf("%14s %v", "p.HLen", p.HLen()) // 6
|
||||
// log.Printf("%14s %v", "p.HType", p.HType()) // 1
|
||||
// log.Printf("%14s %v", "p.Hops", p.Hops()) // 0
|
||||
// log.Printf("%14s %v", "p.OpCode", p.OpCode()) // BootRequest
|
||||
// log.Printf("%14s %v", "p.Options", p.Options()) // [53 1 1 55 10 1 121 3 6 15 119 252 95 44 46 57 2 5 220 61 7 1 44 240 162 242 49 0 51 4 0 118 167 0 12 4 119 104 109 100 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
||||
// log.Printf("%14s %v", "p.ParseOptions", p.ParseOptions()) // map[OptionParameterRequestList:[1 121 3 6 15 119 252 95 44 46] OptionDHCPMessageType:[1] OptionMaximumDHCPMessageSize:[5 220] OptionClientIdentifier:[1 44 240 162 242 49 0] OptionIPAddressLeaseTime:[0 118 167 0] OptionHostName:[119 104 109 100]]
|
||||
// log.Printf("%14s %v", "p.SIAddr", p.SIAddr()) // 0.0.0.0
|
||||
// log.Printf("%14s %v", "p.SName", p.SName()) // []
|
||||
// log.Printf("%14s %v", "p.Secs", p.Secs()) // [0 8]
|
||||
// log.Printf("%14s %v", "p.XId", p.XId()) // [211 184 20 44]
|
||||
// log.Printf("%14s %v", "p.YIAddr", p.YIAddr()) // 0.0.0.0
|
||||
|
||||
switch msgType {
|
||||
case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP?
|
||||
// find a lease, but don't update lease time
|
||||
trace("Got from client: Discover")
|
||||
lease, err := s.reserveLease(p)
|
||||
if err != nil {
|
||||
trace("Couldn't find free lease: %s", err)
|
||||
// couldn't find lease, don't respond
|
||||
return nil
|
||||
}
|
||||
reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.ip, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
|
||||
trace("Replying with offer: offered IP %v for %v with options %+v", lease.ip, s.leaseTime, reply.ParseOptions())
|
||||
return reply
|
||||
case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals)
|
||||
// start/renew a lease -- update lease time
|
||||
// some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request
|
||||
trace("Got from client: Request")
|
||||
if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) {
|
||||
trace("Request message not for this DHCP server (%v vs %v)", p, server, s.ipnet.IP)
|
||||
return nil // Message not for this dhcp server
|
||||
}
|
||||
|
||||
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
|
||||
if reqIP == nil {
|
||||
reqIP = net.IP(p.CIAddr())
|
||||
}
|
||||
|
||||
if reqIP.To4() == nil {
|
||||
trace("Replying with NAK: request IP isn't valid IPv4: %s", reqIP)
|
||||
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
|
||||
}
|
||||
|
||||
if reqIP.Equal(net.IPv4zero) {
|
||||
trace("Replying with NAK: request IP is 0.0.0.0")
|
||||
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
|
||||
}
|
||||
|
||||
trace("requested IP is %s", reqIP)
|
||||
lease, err := s.reserveLease(p)
|
||||
if err != nil {
|
||||
trace("Couldn't find free lease: %s", err)
|
||||
// couldn't find lease, don't respond
|
||||
return nil
|
||||
}
|
||||
|
||||
if lease.ip.Equal(reqIP) {
|
||||
// IP matches lease IP, nothing else to do
|
||||
lease.expiry = time.Now().Add(s.leaseTime)
|
||||
trace("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.ip, p.CHAddr())
|
||||
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.ip, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
|
||||
}
|
||||
|
||||
//
|
||||
// requested IP different from lease
|
||||
//
|
||||
|
||||
trace("lease IP is different from requested IP: %s vs %s", lease.ip, reqIP)
|
||||
|
||||
hwaddr := s.getIPpool(reqIP)
|
||||
if hwaddr == nil {
|
||||
// not in pool, check if it's in DHCP range
|
||||
if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) {
|
||||
// okay, we can give it to our client -- it's in our DHCP range and not taken, so let them use their IP
|
||||
trace("Replying with ACK: request IP %v is not taken, so assigning lease IP %v to it, for %v", reqIP, lease.ip, p.CHAddr())
|
||||
s.unreserveIP(lease.ip)
|
||||
lease.ip = reqIP
|
||||
s.reserveIP(reqIP, p.CHAddr())
|
||||
lease.expiry = time.Now().Add(s.leaseTime)
|
||||
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.ip, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
|
||||
}
|
||||
}
|
||||
|
||||
if hwaddr != nil && !bytes.Equal(hwaddr, lease.hwaddr) {
|
||||
log.Printf("SHOULD NOT HAPPEN: IP pool hwaddr does not match lease hwaddr: %s vs %s", hwaddr, lease.hwaddr)
|
||||
}
|
||||
|
||||
// requsted IP is not sufficient, reply with NAK
|
||||
if hwaddr != nil {
|
||||
trace("Replying with NAK: request IP %s is taken, asked by %v", reqIP, p.CHAddr())
|
||||
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
|
||||
}
|
||||
|
||||
// requested IP is outside of DHCP range
|
||||
trace("Replying with NAK: request IP %s is outside of DHCP range [%s, %s], asked by %v", reqIP, s.leaseStart, s.leaseStop, p.CHAddr())
|
||||
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
|
||||
case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP
|
||||
trace("Got from client: Decline")
|
||||
|
||||
case dhcp4.Release: // From Client, I don't need that IP anymore
|
||||
trace("Got from client: Release")
|
||||
|
||||
case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it
|
||||
trace("Got from client: Inform")
|
||||
// do nothing
|
||||
|
||||
// from server -- ignore those but enumerate just in case
|
||||
case dhcp4.Offer: // Broadcast From Server - Here's an IP
|
||||
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: Offer")
|
||||
case dhcp4.ACK: // From Server, Yes you can have that IP
|
||||
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: ACK")
|
||||
case dhcp4.NAK: // From Server, No you cannot have that IP
|
||||
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK")
|
||||
default:
|
||||
log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Leases() []*Lease {
|
||||
return s.leases
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package dhcpd
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/joomcode/errorx"
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
// TODO: on windows, controlmessage does not work, try to find out another way
|
||||
// https://github.com/golang/net/blob/master/ipv4/payload.go#L13
|
||||
|
||||
type filterConn struct {
|
||||
iface net.Interface
|
||||
conn *ipv4.PacketConn
|
||||
// cm *ipv4.ControlMessage
|
||||
}
|
||||
|
||||
func newFilterConn(iface net.Interface, address string) (*filterConn, error) {
|
||||
c, err := net.ListenPacket("udp4", address)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "Couldn't listen to %s on UDP4", address)
|
||||
}
|
||||
|
||||
p := ipv4.NewPacketConn(c)
|
||||
err = p.SetControlMessage(ipv4.FlagInterface, true)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, errorx.Decorate(err, "Couldn't set control message FlagInterface on connection")
|
||||
}
|
||||
|
||||
return &filterConn{iface: iface, conn: p}, nil
|
||||
}
|
||||
|
||||
func (f *filterConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
for { // read until we find a suitable packet
|
||||
n, cm, addr, err := f.conn.ReadFrom(b)
|
||||
if err != nil {
|
||||
return 0, addr, errorx.Decorate(err, "Error when reading from socket")
|
||||
}
|
||||
if cm == nil {
|
||||
// no controlmessage was passed, so pass the packet to the caller
|
||||
return n, addr, nil
|
||||
}
|
||||
if cm.IfIndex == f.iface.Index {
|
||||
return n, addr, nil
|
||||
}
|
||||
// packet doesn't match criteria, drop it
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
func (f *filterConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
cm := ipv4.ControlMessage{
|
||||
IfIndex: f.iface.Index,
|
||||
}
|
||||
return f.conn.WriteTo(b, &cm, addr)
|
||||
}
|
||||
|
||||
func (f *filterConn) Close() error {
|
||||
return f.conn.Close()
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package dhcpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/joomcode/errorx"
|
||||
)
|
||||
|
||||
func trace(format string, args ...interface{}) {
|
||||
pc := make([]uintptr, 10) // at least 1 entry needed
|
||||
runtime.Callers(2, pc)
|
||||
f := runtime.FuncForPC(pc[0])
|
||||
var buf strings.Builder
|
||||
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
|
||||
text := fmt.Sprintf(format, args...)
|
||||
buf.WriteString(text)
|
||||
if len(text) == 0 || text[len(text)-1] != '\n' {
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
fmt.Fprint(os.Stderr, buf.String())
|
||||
}
|
||||
|
||||
func isTimeout(err error) bool {
|
||||
operr, ok := err.(*net.OpError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return operr.Timeout()
|
||||
}
|
||||
|
||||
// return first IPv4 address of an interface, if there is any
|
||||
func getIfaceIPv4(iface *net.Interface) *net.IPNet {
|
||||
ifaceAddrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, addr := range ifaceAddrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
// not an IPNet, should not happen
|
||||
log.Fatalf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet", addr)
|
||||
}
|
||||
|
||||
if ipnet.IP.To4() == nil {
|
||||
log.Printf("Got IP that is not IPv4: %v", ipnet.IP)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Got IP that is IPv4: %v", ipnet.IP)
|
||||
return &net.IPNet{
|
||||
IP: ipnet.IP.To4(),
|
||||
Mask: ipnet.Mask,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isConnClosed(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
nerr, ok := err.(*net.OpError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.Contains(nerr.Err.Error(), "use of closed network connection") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func wrapErrPrint(err error, message string, args ...interface{}) error {
|
||||
var errx error
|
||||
if err == nil {
|
||||
errx = fmt.Errorf(message, args...)
|
||||
} else {
|
||||
errx = errorx.Decorate(err, message, args...)
|
||||
}
|
||||
log.Println(errx.Error())
|
||||
return errx
|
||||
}
|
||||
|
||||
func parseIPv4(text string) (net.IP, error) {
|
||||
result := net.ParseIP(text)
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("%s is not an IP address", text)
|
||||
}
|
||||
if result.To4() == nil {
|
||||
return nil, fmt.Errorf("%s is not an IPv4 address", text)
|
||||
}
|
||||
return result.To4(), nil
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/dhcpd"
|
||||
"github.com/krolaw/dhcp4"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
log.Printf("Usage: %s <interface name>", os.Args[0])
|
||||
os.Exit(64)
|
||||
}
|
||||
|
||||
ifaceName := os.Args[1]
|
||||
present, err := dhcpd.CheckIfOtherDHCPServersPresent(ifaceName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("Found DHCP server? %v", present)
|
||||
if present {
|
||||
log.Printf("Will not start DHCP server because there's already running one on the network")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// get ipv4 address of an interface
|
||||
ifaceIPNet := getIfaceIPv4(iface)
|
||||
if ifaceIPNet == nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// append 10 to server's IP address as start
|
||||
start := dhcp4.IPAdd(ifaceIPNet.IP, 10)
|
||||
// lease range is 100 IP's, but TODO: don't go beyond end of subnet mask
|
||||
stop := dhcp4.IPAdd(start, 100)
|
||||
|
||||
server := dhcpd.Server{}
|
||||
config := dhcpd.ServerConfig{
|
||||
InterfaceName: ifaceName,
|
||||
RangeStart: start.String(),
|
||||
RangeEnd: stop.String(),
|
||||
SubnetMask: "255.255.255.0",
|
||||
GatewayIP: "192.168.7.1",
|
||||
}
|
||||
log.Printf("Starting DHCP server")
|
||||
err = server.Start(&config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
log.Printf("Stopping DHCP server")
|
||||
err = server.Stop()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("Starting DHCP server")
|
||||
err = server.Start(&config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("Starting DHCP server while it's already running")
|
||||
err = server.Start(&config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("Now serving DHCP")
|
||||
signal_channel := make(chan os.Signal)
|
||||
signal.Notify(signal_channel, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-signal_channel
|
||||
|
||||
}
|
||||
|
||||
// return first IPv4 address of an interface, if there is any
|
||||
func getIfaceIPv4(iface *net.Interface) *net.IPNet {
|
||||
ifaceAddrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, addr := range ifaceAddrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
// not an IPNet, should not happen
|
||||
log.Fatalf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet", addr)
|
||||
}
|
||||
|
||||
if ipnet.IP.To4() == nil {
|
||||
log.Printf("Got IP that is not IPv4: %v", ipnet.IP)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Got IP that is IPv4: %v", ipnet.IP)
|
||||
return &net.IPNet{
|
||||
IP: ipnet.IP.To4(),
|
||||
Mask: ipnet.Mask,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
2
go.mod
2
go.mod
|
@ -6,11 +6,13 @@ require (
|
|||
github.com/ameshkov/dnscrypt v1.0.1
|
||||
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6
|
||||
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-ole/go-ole v1.2.1 // indirect
|
||||
github.com/go-test/deep v1.0.1
|
||||
github.com/gobuffalo/packr v1.19.0
|
||||
github.com/jedisct1/go-dnsstamps v0.0.0-20180418170050-1e4999280f86
|
||||
github.com/joomcode/errorx v0.1.0
|
||||
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414
|
||||
github.com/miekg/dns v1.1.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.8.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -45,6 +45,8 @@ github.com/joomcode/errorx v0.1.0 h1:QmJMiI1DE1UFje2aI1ZWO/VMT5a32qBoXUclGOt8vsc
|
|||
github.com/joomcode/errorx v0.1.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32BM74ph7bPgqMztWopMYKgVyEho=
|
||||
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
|
||||
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc=
|
||||
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o=
|
||||
|
|
Loading…
Reference in New Issue