diff --git a/config.go b/config.go index 89ec9e20..15ecffc1 100644 --- a/config.go +++ b/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, } diff --git a/dhcp.go b/dhcp.go index eb30ab75..b86fa29c 100644 --- a/dhcp.go +++ b/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 diff --git a/dhcpd/check_other_dhcp.go b/dhcpd/check_other_dhcp.go new file mode 100644 index 00000000..7aed85ce --- /dev/null +++ b/dhcpd/check_other_dhcp.go @@ -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 +} diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go new file mode 100644 index 00000000..8dc500a1 --- /dev/null +++ b/dhcpd/dhcpd.go @@ -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 +} diff --git a/dhcpd/filter_conn.go b/dhcpd/filter_conn.go new file mode 100644 index 00000000..cd943ab1 --- /dev/null +++ b/dhcpd/filter_conn.go @@ -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() +} diff --git a/dhcpd/helpers.go b/dhcpd/helpers.go new file mode 100644 index 00000000..cf721f31 --- /dev/null +++ b/dhcpd/helpers.go @@ -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 +} diff --git a/dhcpd/standalone/main.go b/dhcpd/standalone/main.go new file mode 100644 index 00000000..f6dac3ac --- /dev/null +++ b/dhcpd/standalone/main.go @@ -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 ", 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 +} diff --git a/go.mod b/go.mod index 36ddb6a2..bdc4c0d3 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 1a32fbbb..4c18efc7 100644 --- a/go.sum +++ b/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=