From a3317c08c495f28d1d8c2e3905d1cc968ecc15c5 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 3 Jul 2020 18:20:01 +0300 Subject: [PATCH 01/12] + dhcpv6 server; rewrite dhcpv4 server; changed API --- AGHTechDoc.md | 78 +++- dhcpd/check_other_dhcp.go | 89 ++-- dhcpd/check_other_dhcp_windows.go | 7 + dhcpd/db.go | 75 +-- dhcpd/dhcp_http.go | 189 ++++++-- dhcpd/dhcpd.go | 749 ++++-------------------------- dhcpd/dhcpd_test.go | 249 ++-------- dhcpd/filter_conn.go | 63 --- dhcpd/helpers.go | 28 +- dhcpd/network_utils_test.go | 2 + dhcpd/server.go | 81 ++++ dhcpd/standalone/main.go | 115 ----- dhcpd/v4.go | 623 +++++++++++++++++++++++++ dhcpd/v46_windows.go | 47 ++ dhcpd/v4_test.go | 232 +++++++++ dhcpd/v6.go | 638 +++++++++++++++++++++++++ dhcpd/v6_test.go | 225 +++++++++ dnsforward/dnsforward.go | 10 +- dnsforward/dnsforward_test.go | 23 +- go.mod | 6 +- go.sum | 24 +- home/clients.go | 5 +- home/config.go | 8 +- home/control.go | 1 + home/dhcp.go | 36 -- home/home.go | 17 +- openapi/openapi.yaml | 44 +- 27 files changed, 2384 insertions(+), 1280 deletions(-) create mode 100644 dhcpd/check_other_dhcp_windows.go delete mode 100644 dhcpd/filter_conn.go create mode 100644 dhcpd/server.go delete mode 100644 dhcpd/standalone/main.go create mode 100644 dhcpd/v4.go create mode 100644 dhcpd/v46_windows.go create mode 100644 dhcpd/v4_test.go create mode 100644 dhcpd/v6.go create mode 100644 dhcpd/v6_test.go delete mode 100644 home/dhcp.go diff --git a/AGHTechDoc.md b/AGHTechDoc.md index e50982ba..809b0c72 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -12,6 +12,7 @@ Contents: * Updating * Get version command * Update command +* API: Get global status * TLS * API: Get TLS configuration * API: Set TLS configuration @@ -24,6 +25,7 @@ Contents: * API: Find clients by IP * DHCP server * DHCP server in DNS + * "Show DHCP interfaces" command * "Show DHCP status" command * "Check DHCP" command * "Enable DHCP" command @@ -376,6 +378,28 @@ Error response: UI shows error message "Auto-update has failed" +## API: Get global status + +Request: + + GET /control/status + +Response: + + 200 OK + + { + "dns_addresses":["..."], + "dns_port":53, + "http_port":3000, + "language":"en", + "protection_enabled":true, + "running":true, + "dhcp_available":true, + "version":"undefined" + } + + ## DHCP server Enable DHCP server algorithm: @@ -405,6 +429,28 @@ DHCP leases are used in several ways by DNS module. > PTR 100.1.168.192.in-addr.arpa. = bills-notebook. +### "Show DHCP interfaces" command + +Request: + + GET /control/dhcp/interfaces + +Response: + + 200 OK + + { + "iface_name":{ + "name":"iface_name", + "mtu":1500, + "hardware_address":"...", + "ip_addresses":["ipv4 addr","ipv6 addr", ...], + "flags":"up|broadcast|multicast" + } + ... + } + + ### "Show DHCP status" command Request: @@ -416,16 +462,19 @@ Response: 200 OK { - "config":{ - "enabled":false, - "interface_name":"...", + "enabled":false, + "interface_name":"...", + "v4":{ "gateway_ip":"...", "subnet_mask":"...", - "range_start":"...", + "range_start":"...", // if empty: DHCPv4 won't be enabled "range_end":"...", "lease_duration":60, - "icmp_timeout_msec":0 }, + "v6":{ + "range_start":"...", // if empty: DHCPv6 won't be enabled + "lease_duration":60, + } "leases":[ {"ip":"...","mac":"...","hostname":"...","expires":"..."} ... @@ -484,14 +533,19 @@ Request: POST /control/dhcp/set_config { - "enabled":true, - "interface_name":"vboxnet0", + "enabled":true, + "interface_name":"vboxnet0", + "v4":{ "gateway_ip":"192.169.56.1", "subnet_mask":"255.255.255.0", - "range_start":"192.169.56.3", - "range_end":"192.169.56.3", + "range_start":"192.169.56.100", + "range_end":"192.169.56.200", // Note: first 3 octects must match "range_start" "lease_duration":60, - "icmp_timeout_msec":0 + }, + "v6":{ + "range_start":"...", + "lease_duration":60, + } } Response: @@ -500,6 +554,10 @@ Response: OK +For v4, if range_start = "1.2.3.4", the range_end must be "1.2.3.X" where X > 4. + +For v6, if range_start = "2001::1", the last IP is "2001:ff". + ### Static IP check/set diff --git a/dhcpd/check_other_dhcp.go b/dhcpd/check_other_dhcp.go index fb7fe98c..83c9ca1b 100644 --- a/dhcpd/check_other_dhcp.go +++ b/dhcpd/check_other_dhcp.go @@ -1,23 +1,22 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + package dhcpd import ( "bytes" - "crypto/rand" - "encoding/binary" "fmt" - "math" "net" "os" "time" "github.com/AdguardTeam/golibs/log" - "github.com/krolaw/dhcp4" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/iana" "golang.org/x/net/ipv4" ) // CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface, // and waits for a response for a period defined by defaultDiscoverTime -// nolint func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { iface, err := net.InterfaceByName(ifaceName) if err != nil { @@ -25,60 +24,29 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { } // 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) + ifaceIPNet := getIfaceIPv4(*iface) + if len(ifaceIPNet) == 0 { + return false, fmt.Errorf("couldn't find IPv4 address of interface %s %+v", ifaceName, iface) } - srcIP := ifaceIPNet.IP + srcIP := ifaceIPNet[0] 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, 4) - n, err := rand.Read(xID) - if n != 4 && err == nil { - err = fmt.Errorf("Generated less than 4 bytes") - } + hostname, _ := os.Hostname() + + req, err := dhcpv4.NewDiscovery(iface.HardwareAddr) if err != nil { - return false, wrapErrPrint(err, "Couldn't generate random bytes") + return false, fmt.Errorf("dhcpv4.NewDiscovery: %s", err) } - 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.Hour * 24 * 90).Seconds())) - binary.BigEndian.PutUint32(leaseTimeRaw, leaseTime) - options := []dhcp4.Option{ - {Code: dhcp4.OptionParameterRequestList, Value: requestList}, - {Code: dhcp4.OptionMaximumDHCPMessageSize, Value: maxUDPsizeRaw}, - {Code: dhcp4.OptionClientIdentifier, Value: append([]byte{0x01}, iface.HardwareAddr...)}, - {Code: dhcp4.OptionIPAddressLeaseTime, Value: leaseTimeRaw}, - {Code: dhcp4.OptionHostName, Value: []byte(hostname)}, - } - packet := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, nil, xID, false, options) + req.Options.Update(dhcpv4.OptClientIdentifier(iface.HardwareAddr)) + req.Options.Update(dhcpv4.OptHostName(hostname)) // 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) @@ -102,7 +70,7 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { // send to 255.255.255.255:67 cm := ipv4.ControlMessage{} - _, err = c.WriteTo(packet, &cm, dstAddr) + _, err = c.WriteTo(req.ToBytes(), &cm, dstAddr) if err != nil { return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst) } @@ -113,7 +81,7 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { // 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) + n, _, _, err := c.ReadFrom(b) if isTimeout(err) { // timed out -- no DHCP servers return false, nil @@ -121,27 +89,24 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { if err != nil { return false, wrapErrPrint(err, "Couldn't receive packet") } - // spew.Dump(n, fromAddr, err, b) log.Tracef("Received packet (%v bytes)", n) - if n < 240 { - // packet too small for dhcp + response, err := dhcpv4.FromBytes(b[:n]) + if err != nil { + log.Debug("DHCPv4: dhcpv4.FromBytes: %s", err) continue } - response := dhcp4.Packet(b[:n]) - if response.OpCode() != dhcp4.BootReply || - response.HType() != 1 /*Ethernet*/ || - response.HLen() > 16 || - !bytes.Equal(response.CHAddr(), iface.HardwareAddr) || - !bytes.Equal(response.XId(), xID) { - continue - } + log.Debug("DHCPv4: received message from server: %s", response.Summary()) - parsedOptions := response.ParseOptions() - if t := parsedOptions[dhcp4.OptionDHCPMessageType]; len(t) != 1 { - continue //packet without DHCP message type + if !(response.OpCode == dhcpv4.OpcodeBootReply && + response.HWType == iana.HWTypeEthernet && + bytes.Equal(response.ClientHWAddr, iface.HardwareAddr) && + bytes.Equal(response.TransactionID[:], req.TransactionID[:]) && + response.Options.Has(dhcpv4.OptionDHCPMessageType)) { + log.Debug("DHCPv4: received message from server doesn't match our request") + continue } log.Tracef("The packet is from an active DHCP server") diff --git a/dhcpd/check_other_dhcp_windows.go b/dhcpd/check_other_dhcp_windows.go new file mode 100644 index 00000000..bd3849b8 --- /dev/null +++ b/dhcpd/check_other_dhcp_windows.go @@ -0,0 +1,7 @@ +package dhcpd + +import "fmt" + +func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { + return false, fmt.Errorf("not supported") +} diff --git a/dhcpd/db.go b/dhcpd/db.go index fdf94059..b01f9635 100644 --- a/dhcpd/db.go +++ b/dhcpd/db.go @@ -11,7 +11,6 @@ import ( "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" - "github.com/krolaw/dhcp4" ) const dbFilename = "leases.db" @@ -31,21 +30,12 @@ func normalizeIP(ip net.IP) net.IP { return ip } -// Safe version of dhcp4.IPInRange() -func ipInRange(start, stop, ip net.IP) bool { - if len(start) != len(stop) || - len(start) != len(ip) { - return false - } - return dhcp4.IPInRange(start, stop, ip) -} - // Load lease table from DB func (s *Server) dbLoad() { - s.leases = nil - s.IPpool = make(map[[4]byte]net.HardwareAddr) dynLeases := []*Lease{} staticLeases := []*Lease{} + v6StaticLeases := []*Lease{} + v6DynLeases := []*Lease{} data, err := ioutil.ReadFile(s.conf.DBFilePath) if err != nil { @@ -66,10 +56,8 @@ func (s *Server) dbLoad() { for i := range obj { obj[i].IP = normalizeIP(obj[i].IP) - if obj[i].Expiry != leaseExpireStatic && - !ipInRange(s.leaseStart, s.leaseStop, obj[i].IP) { - - log.Tracef("Skipping a lease with IP %v: not within current IP range", obj[i].IP) + if !(len(obj[i].IP) == 4 || len(obj[i].IP) == 16) { + log.Info("DHCP: invalid IP: %s", obj[i].IP) continue } @@ -80,20 +68,32 @@ func (s *Server) dbLoad() { Expiry: time.Unix(obj[i].Expiry, 0), } - if obj[i].Expiry == leaseExpireStatic { - staticLeases = append(staticLeases, &lease) + if len(obj[i].IP) == 16 { + if obj[i].Expiry == leaseExpireStatic { + v6StaticLeases = append(v6StaticLeases, &lease) + } else { + v6DynLeases = append(v6DynLeases, &lease) + } + } else { - dynLeases = append(dynLeases, &lease) + if obj[i].Expiry == leaseExpireStatic { + staticLeases = append(staticLeases, &lease) + } else { + dynLeases = append(dynLeases, &lease) + } } } - s.leases = normalizeLeases(staticLeases, dynLeases) + leases4 := normalizeLeases(staticLeases, dynLeases) + s.srv4.ResetLeases(leases4) - for _, lease := range s.leases { - s.reserveIP(lease.IP, lease.HWAddr) + leases6 := normalizeLeases(v6StaticLeases, v6DynLeases) + if s.srv6 != nil { + s.srv6.ResetLeases(leases6) } - log.Info("DHCP: loaded %d (%d) leases from DB", len(s.leases), numLeases) + log.Info("DHCP: loaded leases v4:%d v6:%d total-read:%d from DB", + len(leases4), len(leases6), numLeases) } // Skip duplicate leases @@ -127,19 +127,36 @@ func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease { func (s *Server) dbStore() { var leases []leaseJSON - for i := range s.leases { - if s.leases[i].Expiry.Unix() == 0 { + leases4 := s.srv4.GetLeasesRef() + for _, l := range leases4 { + if l.Expiry.Unix() == 0 { continue } lease := leaseJSON{ - HWAddr: s.leases[i].HWAddr, - IP: s.leases[i].IP, - Hostname: s.leases[i].Hostname, - Expiry: s.leases[i].Expiry.Unix(), + HWAddr: l.HWAddr, + IP: l.IP, + Hostname: l.Hostname, + Expiry: l.Expiry.Unix(), } leases = append(leases, lease) } + if s.srv6 != nil { + leases6 := s.srv6.GetLeasesRef() + for _, l := range leases6 { + if l.Expiry.Unix() == 0 { + continue + } + lease := leaseJSON{ + HWAddr: l.HWAddr, + IP: l.IP, + Hostname: l.Hostname, + Expiry: l.Expiry.Unix(), + } + leases = append(leases, lease) + } + } + data, err := json.Marshal(leases) if err != nil { log.Error("json.Marshal: %v", err) diff --git a/dhcpd/dhcp_http.go b/dhcpd/dhcp_http.go index 9105d76b..7ae6075a 100644 --- a/dhcpd/dhcp_http.go +++ b/dhcpd/dhcp_http.go @@ -40,13 +40,70 @@ func convertLeases(inputLeases []Lease, includeExpires bool) []map[string]string return leases } +type v4ServerConfJSON struct { + GatewayIP string `json:"gateway_ip"` + SubnetMask string `json:"subnet_mask"` + RangeStart string `json:"range_start"` + RangeEnd string `json:"range_end"` + LeaseDuration uint32 `json:"lease_duration"` +} + +func v4ServerConfToJSON(c V4ServerConf) v4ServerConfJSON { + return v4ServerConfJSON{ + GatewayIP: c.GatewayIP, + SubnetMask: c.SubnetMask, + RangeStart: c.RangeStart, + RangeEnd: c.RangeEnd, + LeaseDuration: c.LeaseDuration, + } +} + +func v4JSONToServerConf(j v4ServerConfJSON) V4ServerConf { + return V4ServerConf{ + GatewayIP: j.GatewayIP, + SubnetMask: j.SubnetMask, + RangeStart: j.RangeStart, + RangeEnd: j.RangeEnd, + LeaseDuration: j.LeaseDuration, + } +} + +type v6ServerConfJSON struct { + RangeStart string `json:"range_start"` + LeaseDuration uint32 `json:"lease_duration"` +} + +func v6ServerConfToJSON(c V6ServerConf) v6ServerConfJSON { + return v6ServerConfJSON{ + RangeStart: c.RangeStart, + LeaseDuration: c.LeaseDuration, + } +} + +func v6JSONToServerConf(j v6ServerConfJSON) V6ServerConf { + return V6ServerConf{ + RangeStart: j.RangeStart, + LeaseDuration: j.LeaseDuration, + } +} + func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) { leases := convertLeases(s.Leases(LeasesDynamic), true) staticLeases := convertLeases(s.Leases(LeasesStatic), false) + + v4conf := V4ServerConf{} + s.srv4.WriteDiskConfig4(&v4conf) + + v6conf := V6ServerConf{} + s.srv6.WriteDiskConfig6(&v6conf) + status := map[string]interface{}{ - "config": s.conf, - "leases": leases, - "static_leases": staticLeases, + "enabled": s.conf.Enabled, + "interface_name": s.conf.InterfaceName, + "v4": v4ServerConfToJSON(v4conf), + "v6": v6ServerConfToJSON(v6conf), + "leases": leases, + "static_leases": staticLeases, } w.Header().Set("Content-Type", "application/json") @@ -64,8 +121,10 @@ type staticLeaseJSON struct { } type dhcpServerConfigJSON struct { - ServerConfig `json:",inline"` - StaticLeases []staticLeaseJSON `json:"static_leases"` + Enabled bool `json:"enabled"` + InterfaceName string `json:"interface_name"` + V4 v4ServerConfJSON `json:"v4"` + V6 v6ServerConfJSON `json:"v6"` } func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { @@ -76,22 +135,49 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { return } - err = s.CheckConfig(newconfig.ServerConfig) + v4conf := v4JSONToServerConf(newconfig.V4) + v4conf.Enabled = newconfig.Enabled + if len(v4conf.RangeStart) == 0 { + v4conf.Enabled = false + } + v4conf.InterfaceName = newconfig.InterfaceName + + c4 := V4ServerConf{} + s.srv4.WriteDiskConfig4(&c4) + v4conf.notify = c4.notify + v4conf.ICMPTimeout = c4.ICMPTimeout + + s4, err := v4Create(v4conf) if err != nil { - httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err) + httpError(r, w, http.StatusBadRequest, "Invalid DHCPv4 configuration: %s", err) return } - err = s.Stop() - if err != nil { - log.Error("failed to stop the DHCP server: %s", err) + v6conf := v6JSONToServerConf(newconfig.V6) + v6conf.Enabled = newconfig.Enabled + if len(v6conf.RangeStart) == 0 { + v6conf.Enabled = false } - - err = s.Init(newconfig.ServerConfig) - if err != nil { - httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err) + v6conf.InterfaceName = newconfig.InterfaceName + v6conf.notify = s.onNotify + s6, err := v6Create(v6conf) + if s6 == nil { + httpError(r, w, http.StatusBadRequest, "Invalid DHCPv6 configuration: %s", err) return } + + if newconfig.Enabled && !v4conf.Enabled && !v6conf.Enabled { + httpError(r, w, http.StatusBadRequest, "DHCPv4 or DHCPv6 configuration must be complete") + return + } + + s.Stop() + + s.conf.Enabled = newconfig.Enabled + s.conf.InterfaceName = newconfig.InterfaceName + s.srv4 = s4 + s.srv6 = s6 + s.conf.ConfigModified() if newconfig.Enabled { @@ -246,20 +332,45 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request return } - ip, _ := parseIPv4(lj.IP) + ip := net.ParseIP(lj.IP) + if ip != nil && ip.To4() == nil { + mac, err := net.ParseMAC(lj.HWAddr) + if err != nil { + httpError(r, w, http.StatusBadRequest, "invalid MAC") + return + } + + lease := Lease{ + IP: ip, + HWAddr: mac, + } + + err = s.srv6.AddStaticLease(lease) + if err != nil { + httpError(r, w, http.StatusBadRequest, "%s", err) + return + } + return + } + + ip, _ = parseIPv4(lj.IP) if ip == nil { httpError(r, w, http.StatusBadRequest, "invalid IP") return } - mac, _ := net.ParseMAC(lj.HWAddr) + mac, err := net.ParseMAC(lj.HWAddr) + if err != nil { + httpError(r, w, http.StatusBadRequest, "invalid MAC") + return + } lease := Lease{ IP: ip, HWAddr: mac, Hostname: lj.Hostname, } - err = s.AddStaticLease(lease) + err = s.srv4.AddStaticLease(lease) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) return @@ -275,7 +386,28 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ return } - ip, _ := parseIPv4(lj.IP) + ip := net.ParseIP(lj.IP) + if ip != nil && ip.To4() == nil { + mac, err := net.ParseMAC(lj.HWAddr) + if err != nil { + httpError(r, w, http.StatusBadRequest, "invalid MAC") + return + } + + lease := Lease{ + IP: ip, + HWAddr: mac, + } + + err = s.srv6.RemoveStaticLease(lease) + if err != nil { + httpError(r, w, http.StatusBadRequest, "%s", err) + return + } + return + } + + ip, _ = parseIPv4(lj.IP) if ip == nil { httpError(r, w, http.StatusBadRequest, "invalid IP") return @@ -288,7 +420,7 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ HWAddr: mac, Hostname: lj.Hostname, } - err = s.RemoveStaticLease(lease) + err = s.srv4.RemoveStaticLease(lease) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) return @@ -296,24 +428,29 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ } func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) { - err := s.Stop() - if err != nil { - log.Error("DHCP: Stop: %s", err) - } + s.Stop() - err = os.Remove(s.conf.DBFilePath) + err := os.Remove(s.conf.DBFilePath) if err != nil && !os.IsNotExist(err) { log.Error("DHCP: os.Remove: %s: %s", s.conf.DBFilePath, err) } oldconf := s.conf s.conf = ServerConfig{} - s.conf.LeaseDuration = 86400 - s.conf.ICMPTimeout = 1000 s.conf.WorkDir = oldconf.WorkDir s.conf.HTTPRegister = oldconf.HTTPRegister s.conf.ConfigModified = oldconf.ConfigModified s.conf.DBFilePath = oldconf.DBFilePath + + v4conf := V4ServerConf{} + v4conf.ICMPTimeout = 1000 + v4conf.notify = s.onNotify + s.srv4, _ = v4Create(v4conf) + + v6conf := V6ServerConf{} + v6conf.notify = s.onNotify + s.srv6, _ = v6Create(v6conf) + s.conf.ConfigModified() } diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 0ec83370..11937eb1 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -1,18 +1,12 @@ package dhcpd import ( - "bytes" - "fmt" "net" "net/http" "path/filepath" - "strings" - "sync" "time" "github.com/AdguardTeam/golibs/log" - "github.com/krolaw/dhcp4" - ping "github.com/sparrc/go-ping" ) const defaultDiscoverTime = time.Second * 3 @@ -21,9 +15,8 @@ const leaseExpireStatic = 1 var webHandlersRegistered = false // Lease contains the necessary information about a DHCP lease -// field ordering is important -- yaml fields will mirror ordering from here type Lease struct { - HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"` + HWAddr net.HardwareAddr `json:"mac"` IP net.IP `json:"ip"` Hostname string `json:"hostname"` @@ -35,29 +28,23 @@ type Lease struct { // ServerConfig - DHCP server configuration // 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 uint32 `json:"lease_duration" yaml:"lease_duration"` // in seconds + Enabled bool `yaml:"enabled"` + InterfaceName string `yaml:"interface_name"` - // IP conflict detector: time (ms) to wait for ICMP reply. - // 0: disable - ICMPTimeout uint32 `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"` + Conf4 V4ServerConf `yaml:"dhcpv4"` + Conf6 V6ServerConf `yaml:"dhcpv6"` - WorkDir string `json:"-" yaml:"-"` - DBFilePath string `json:"-" yaml:"-"` // path to DB file + WorkDir string `yaml:"-"` + DBFilePath string `yaml:"-"` // path to DB file // Called when the configuration is changed by HTTP request - ConfigModified func() `json:"-" yaml:"-"` + ConfigModified func() `yaml:"-"` // Register an HTTP handler - HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `json:"-" yaml:"-"` + HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"` } -type onLeaseChangedT func(flags int) +type OnLeaseChangedT func(flags int) // flags for onLeaseChanged() const ( @@ -65,87 +52,91 @@ const ( LeaseChangedAddedStatic LeaseChangedRemovedStatic LeaseChangedBlacklisted + + LeaseChangedDBStore ) // Server - the current state of the DHCP server type Server struct { - conn *filterConn // listening UDP socket - - ipnet *net.IPNet // if interface name changes, this needs to be reset - - cond *sync.Cond // Synchronize worker thread with main thread - mutex sync.Mutex // Mutex for 'cond' - running bool // Set if the worker thread is running - stopping bool // Set if the worker thread should be stopped - - // leases - leases []*Lease - leasesLock sync.RWMutex - 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 + srv4 DHCPServer + srv6 DHCPServer conf ServerConfig // Called when the leases DB is modified - onLeaseChanged []onLeaseChangedT + onLeaseChanged []OnLeaseChangedT } -// Print information about the available network interfaces -func printInterfaces() { - ifaces, _ := net.Interfaces() - var buf strings.Builder - for i := range ifaces { - buf.WriteString(fmt.Sprintf("\"%s\", ", ifaces[i].Name)) - } - log.Info("Available network interfaces: %s", buf.String()) +type ServerInterface interface { + Leases(flags int) []Lease + SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) } // CheckConfig checks the configuration func (s *Server) CheckConfig(config ServerConfig) error { - tmpServer := Server{} - return tmpServer.setConfig(config) + return nil } // Create - create object func Create(config ServerConfig) *Server { s := Server{} - s.conf = config + s.conf.Enabled = config.Enabled + s.conf.InterfaceName = config.InterfaceName + s.conf.HTTPRegister = config.HTTPRegister + s.conf.ConfigModified = config.ConfigModified s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename) - if s.conf.Enabled { - err := s.setConfig(config) - if err != nil { - log.Error("DHCP: %s", err) - return nil - } - } if !webHandlersRegistered && s.conf.HTTPRegister != nil { webHandlersRegistered = true s.registerHandlers() } + var err4, err6 error + v4conf := config.Conf4 + v4conf.Enabled = s.conf.Enabled + if len(v4conf.RangeStart) == 0 { + v4conf.Enabled = false + } + v4conf.InterfaceName = s.conf.InterfaceName + v4conf.notify = s.onNotify + s.srv4, err4 = v4Create(v4conf) + + v6conf := config.Conf6 + v6conf.Enabled = s.conf.Enabled + if len(v6conf.RangeStart) == 0 { + v6conf.Enabled = false + } + v6conf.InterfaceName = s.conf.InterfaceName + v6conf.notify = s.onNotify + s.srv6, err6 = v6Create(v6conf) + + if err4 != nil { + log.Error("%s", err4) + return nil + } + if err6 != nil { + log.Error("%s", err6) + return nil + } + // we can't delay database loading until DHCP server is started, // because we need static leases functionality available beforehand s.dbLoad() return &s } -// Init checks the configuration and initializes the server -func (s *Server) Init(config ServerConfig) error { - err := s.setConfig(config) - if err != nil { - return err +// server calls this function after DB is updated +func (s *Server) onNotify(flags uint32) { + if flags == LeaseChangedDBStore { + s.dbStore() + return } - return nil + + s.notify(int(flags)) } // SetOnLeaseChanged - set callback -func (s *Server) SetOnLeaseChanged(onLeaseChanged onLeaseChangedT) { +func (s *Server) SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) { s.onLeaseChanged = append(s.onLeaseChanged, onLeaseChanged) } @@ -160,563 +151,33 @@ func (s *Server) notify(flags int) { // WriteDiskConfig - write configuration func (s *Server) WriteDiskConfig(c *ServerConfig) { - *c = s.conf -} - -func (s *Server) setConfig(config ServerConfig) error { - iface, err := net.InterfaceByName(config.InterfaceName) - if err != nil { - printInterfaces() - return wrapErrPrint(err, "Couldn't find interface by name %s", config.InterfaceName) - } - - // get ipv4 address of an interface - s.ipnet = getIfaceIPv4(iface) - if s.ipnet == nil { - return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", config.InterfaceName, iface) - } - - if config.LeaseDuration == 0 { - s.leaseTime = time.Hour * 2 - } else { - s.leaseTime = time.Second * time.Duration(config.LeaseDuration) - } - - s.leaseStart, err = parseIPv4(config.RangeStart) - if err != nil { - return wrapErrPrint(err, "Failed to parse range start address %s", config.RangeStart) - } - - s.leaseStop, err = parseIPv4(config.RangeEnd) - if err != nil { - return wrapErrPrint(err, "Failed to parse range end address %s", config.RangeEnd) - } - if dhcp4.IPRange(s.leaseStart, s.leaseStop) <= 0 { - return wrapErrPrint(err, "DHCP: Incorrect range_start/range_end values") - } - - subnet, err := parseIPv4(config.SubnetMask) - if err != nil || !isValidSubnetMask(subnet) { - return wrapErrPrint(err, "Failed to parse subnet mask %s", config.SubnetMask) - } - - // if !bytes.Equal(subnet, s.ipnet.Mask) { - // 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(config.GatewayIP) - if err != nil { - return wrapErrPrint(err, "Failed to parse gateway IP %s", config.GatewayIP) - } - - s.leaseOptions = dhcp4.Options{ - dhcp4.OptionSubnetMask: subnet, - dhcp4.OptionRouter: router, - dhcp4.OptionDomainNameServer: s.ipnet.IP, - } - - oldconf := s.conf - s.conf = config - s.conf.WorkDir = oldconf.WorkDir - s.conf.HTTPRegister = oldconf.HTTPRegister - s.conf.ConfigModified = oldconf.ConfigModified - s.conf.DBFilePath = oldconf.DBFilePath - return nil + c.Enabled = s.conf.Enabled + c.InterfaceName = s.conf.InterfaceName + s.srv4.WriteDiskConfig4(&c.Conf4) + s.srv6.WriteDiskConfig6(&c.Conf6) } // Start will listen on port 67 and serve DHCP requests. func (s *Server) Start() error { - // TODO: don't close if interface and addresses are the same - if s.conn != nil { - _ = s.closeConn() - } - - iface, err := net.InterfaceByName(s.conf.InterfaceName) + err := s.srv4.Start() if err != nil { - return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName) + log.Error("DHCPv4: start: %s", err) + return err } - 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 + err = s.srv6.Start() if err != nil { - return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67") + log.Error("DHCPv6: start: %s", err) + return err } - log.Info("DHCP: listening on 0.0.0.0:67") - - s.conn = c - s.cond = sync.NewCond(&s.mutex) - - s.running = true - go func() { - // operate on c instead of c.conn because c.conn can change over time - err := dhcp4.Serve(c, s) - if err != nil && !s.stopping { - log.Printf("dhcp4.Serve() returned with error: %s", err) - } - _ = c.Close() // in case Serve() exits for other reason than listening socket closure - s.running = false - s.cond.Signal() - }() return nil } // Stop closes the listening UDP socket -func (s *Server) Stop() error { - if s.conn == nil { - // nothing to do, return silently - return nil - } - - s.stopping = true - - err := s.closeConn() - if err != nil { - return wrapErrPrint(err, "Couldn't close UDP listening socket") - } - - // We've just closed the listening socket. - // Worker thread should exit right after it tries to read from the socket. - s.mutex.Lock() - for s.running { - s.cond.Wait() - } - s.mutex.Unlock() - 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 -} - -// Reserve a lease for the client -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) - // not assigned a lease, create new one, find IP from LRU - hostname := p.ParseOptions()[dhcp4.OptionHostName] - lease := &Lease{HWAddr: hwaddr, Hostname: string(hostname)} - - log.Tracef("Lease not found for %s: creating new one", hwaddr) - - s.leasesLock.Lock() - defer s.leasesLock.Unlock() - - ip, err := s.findFreeIP(hwaddr) - if err != nil { - i := s.findExpiredLease() - if i < 0 { - return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String()) - } - - log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)", - s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry) - lease.IP = s.leases[i].IP - s.leases[i] = lease - - s.reserveIP(lease.IP, hwaddr) - return lease, nil - } - - log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String()) - lease.IP = ip - s.leases = append(s.leases, lease) - return lease, nil -} - -// Find a lease for the client -func (s *Server) findLease(p dhcp4.Packet) *Lease { - hwaddr := p.CHAddr() - for i := range s.leases { - if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) { - // log.Tracef("bytes.Equal(%s, %s) returned true", hwaddr, s.leases[i].hwaddr) - return s.leases[i] - } - } - return nil -} - -// Find an expired lease and return its index or -1 -func (s *Server) findExpiredLease() int { - now := time.Now().Unix() - for i, lease := range s.leases { - if lease.Expiry.Unix() <= now && lease.Expiry.Unix() != leaseExpireStatic { - return i - } - } - return -1 -} - -func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { - // 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.findReservedHWaddr(newIP) - log.Tracef("tried IP %v, got hwaddr %v", newIP, foundHWaddr) - if foundHWaddr != nil && len(foundHWaddr) != 0 { - // if !bytes.Equal(foundHWaddr, hwaddr) { - // log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr) - // } - 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) findReservedHWaddr(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) -} - -// ServeDHCP handles an incoming DHCP request -func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { - s.printLeases() - - switch msgType { - case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP? - return s.handleDiscover(p, options) - - 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 - return s.handleDHCP4Request(p, options) - - case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP - return s.handleDecline(p, options) - - case dhcp4.Release: // From Client, I don't need that IP anymore - return s.handleRelease(p, options) - - case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it - return s.handleInform(p, options) - - // from server -- ignore those but enumerate just in case - case dhcp4.Offer: // Broadcast From Server - Here's an IP - log.Printf("DHCP: received message from %s: Offer", p.CHAddr()) - - case dhcp4.ACK: // From Server, Yes you can have that IP - log.Printf("DHCP: received message from %s: ACK", p.CHAddr()) - - case dhcp4.NAK: // From Server, No you cannot have that IP - log.Printf("DHCP: received message from %s: NAK", p.CHAddr()) - - default: - log.Printf("DHCP: unknown packet %v from %s", msgType, p.CHAddr()) - return nil - } - return nil -} - -// Send ICMP to the specified machine -// Return TRUE if it doesn't reply, which probably means that the IP is available -func (s *Server) addrAvailable(target net.IP) bool { - - if s.conf.ICMPTimeout == 0 { - return true - } - - pinger, err := ping.NewPinger(target.String()) - if err != nil { - log.Error("ping.NewPinger(): %v", err) - return true - } - - pinger.SetPrivileged(true) - pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond - pinger.Count = 1 - reply := false - pinger.OnRecv = func(pkt *ping.Packet) { - // log.Tracef("Received ICMP Reply from %v", target) - reply = true - } - log.Tracef("Sending ICMP Echo to %v", target) - pinger.Run() - - if reply { - log.Info("DHCP: IP conflict: %v is already used by another device", target) - return false - } - - log.Tracef("ICMP procedure is complete: %v", target) - return true -} - -// Add the specified IP to the black list for a time period -func (s *Server) blacklistLease(lease *Lease) { - hw := make(net.HardwareAddr, 6) - s.leasesLock.Lock() - s.reserveIP(lease.IP, hw) - lease.HWAddr = hw - lease.Hostname = "" - lease.Expiry = time.Now().Add(s.leaseTime) - s.dbStore() - s.leasesLock.Unlock() - s.notify(LeaseChangedBlacklisted) -} - -// Return TRUE if DHCP packet is correct -func isValidPacket(p dhcp4.Packet) bool { - hw := p.CHAddr() - zeroes := make([]byte, len(hw)) - if bytes.Equal(hw, zeroes) { - log.Tracef("Packet has empty CHAddr") - return false - } - return true -} - -func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - // find a lease, but don't update lease time - var lease *Lease - var err error - - reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) - hostname := p.ParseOptions()[dhcp4.OptionHostName] - log.Tracef("Message from client: Discover. ReqIP: %s HW: %s Hostname: %s", - reqIP, p.CHAddr(), hostname) - - if !isValidPacket(p) { - return nil - } - - lease = s.findLease(p) - for lease == nil { - lease, err = s.reserveLease(p) - if err != nil { - log.Error("Couldn't find free lease: %s", err) - return nil - } - - if !s.addrAvailable(lease.IP) { - s.blacklistLease(lease) - lease = nil - continue - } - - break - } - - opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) - reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, opt) - log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions()) - return reply -} - -func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - var lease *Lease - - reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) - log.Tracef("Message from client: Request. IP: %s ReqIP: %s HW: %s", - p.CIAddr(), reqIP, p.CHAddr()) - - if !isValidPacket(p) { - return nil - } - - server := options[dhcp4.OptionServerIdentifier] - if server != nil && !net.IP(server).Equal(s.ipnet.IP) { - log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP) - return nil // Message not for this dhcp server - } - - if reqIP == nil { - reqIP = p.CIAddr() - - } else if reqIP == nil || reqIP.To4() == nil { - log.Tracef("Requested IP isn't a valid IPv4: %s", reqIP) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) - } - - lease = s.findLease(p) - if lease == nil { - log.Tracef("Lease for %s isn't found", p.CHAddr()) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) - } - - if !lease.IP.Equal(reqIP) { - log.Tracef("Lease for %s doesn't match requested/client IP: %s vs %s", - lease.HWAddr, lease.IP, reqIP) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) - } - - if lease.Expiry.Unix() != leaseExpireStatic { - lease.Expiry = time.Now().Add(s.leaseTime) - s.leasesLock.Lock() - s.dbStore() - s.leasesLock.Unlock() - s.notify(LeaseChangedAdded) // Note: maybe we shouldn't call this function if only expiration time is updated - } - log.Tracef("Replying with ACK. IP: %s HW: %s Expire: %s", - lease.IP, lease.HWAddr, lease.Expiry) - opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) - return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt) -} - -func (s *Server) handleInform(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - log.Tracef("Message from client: Inform. IP: %s HW: %s", - p.CIAddr(), p.CHAddr()) - - return nil -} - -func (s *Server) handleRelease(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - log.Tracef("Message from client: Release. IP: %s HW: %s", - p.CIAddr(), p.CHAddr()) - - return nil -} - -func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) - log.Tracef("Message from client: Decline. IP: %s HW: %s", - reqIP, p.CHAddr()) - - return nil -} - -// AddStaticLease adds a static lease (thread-safe) -func (s *Server) AddStaticLease(l Lease) error { - if len(l.IP) != 4 { - return fmt.Errorf("invalid IP") - } - if len(l.HWAddr) != 6 { - return fmt.Errorf("invalid MAC") - } - l.Expiry = time.Unix(leaseExpireStatic, 0) - - s.leasesLock.Lock() - - if s.findReservedHWaddr(l.IP) != nil { - err := s.rmDynamicLeaseWithIP(l.IP) - if err != nil { - s.leasesLock.Unlock() - return err - } - } else { - err := s.rmDynamicLeaseWithMAC(l.HWAddr) - if err != nil { - s.leasesLock.Unlock() - return err - } - } - s.leases = append(s.leases, &l) - s.reserveIP(l.IP, l.HWAddr) - s.dbStore() - s.leasesLock.Unlock() - s.notify(LeaseChangedAddedStatic) - return nil -} - -// Remove a dynamic lease by IP address -func (s *Server) rmDynamicLeaseWithIP(ip net.IP) error { - var newLeases []*Lease - for _, lease := range s.leases { - if net.IP.Equal(lease.IP.To4(), ip) { - if lease.Expiry.Unix() == leaseExpireStatic { - return fmt.Errorf("static lease with the same IP already exists") - } - continue - } - newLeases = append(newLeases, lease) - } - s.leases = newLeases - s.unreserveIP(ip) - return nil -} - -// Remove a dynamic lease by IP address -func (s *Server) rmDynamicLeaseWithMAC(mac net.HardwareAddr) error { - var newLeases []*Lease - for _, lease := range s.leases { - if bytes.Equal(lease.HWAddr, mac) { - if lease.Expiry.Unix() == leaseExpireStatic { - return fmt.Errorf("static lease with the same IP already exists") - } - s.unreserveIP(lease.IP) - continue - } - newLeases = append(newLeases, lease) - } - s.leases = newLeases - return nil -} - -// Remove a lease -func (s *Server) rmLease(l Lease) error { - var newLeases []*Lease - for _, lease := range s.leases { - if net.IP.Equal(lease.IP.To4(), l.IP) { - if !bytes.Equal(lease.HWAddr, l.HWAddr) || - lease.Hostname != l.Hostname { - return fmt.Errorf("Lease not found") - } - continue - } - newLeases = append(newLeases, lease) - } - s.leases = newLeases - s.unreserveIP(l.IP) - return nil -} - -// RemoveStaticLease removes a static lease (thread-safe) -func (s *Server) RemoveStaticLease(l Lease) error { - if len(l.IP) != 4 { - return fmt.Errorf("invalid IP") - } - if len(l.HWAddr) != 6 { - return fmt.Errorf("invalid MAC") - } - - s.leasesLock.Lock() - - if s.findReservedHWaddr(l.IP) == nil { - s.leasesLock.Unlock() - return fmt.Errorf("lease not found") - } - - err := s.rmLease(l) - if err != nil { - s.leasesLock.Unlock() - return err - } - s.dbStore() - s.leasesLock.Unlock() - s.notify(LeaseChangedRemovedStatic) - return nil +func (s *Server) Stop() { + s.srv4.Stop() + s.srv6.Stop() } // flags for Leases() function @@ -728,69 +189,23 @@ const ( // Leases returns the list of current DHCP leases (thread-safe) func (s *Server) Leases(flags int) []Lease { - var result []Lease - now := time.Now().Unix() - s.leasesLock.RLock() - for _, lease := range s.leases { - if ((flags&LeasesDynamic) != 0 && lease.Expiry.Unix() > now) || - ((flags&LeasesStatic) != 0 && lease.Expiry.Unix() == leaseExpireStatic) { - result = append(result, *lease) - } - } - s.leasesLock.RUnlock() + result := s.srv4.GetLeases(flags) + + v6leases := s.srv6.GetLeases(flags) + result = append(result, v6leases...) return result } -// Print information about the current leases -func (s *Server) printLeases() { - log.Tracef("Leases:") - for i, lease := range s.leases { - log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", - i, lease.HWAddr, lease.IP, lease.Expiry) - } -} - -// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases -func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP { - now := time.Now().Unix() - s.leasesLock.RLock() - defer s.leasesLock.RUnlock() - for _, l := range s.leases { - if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) { - return l.IP - } - } - return nil -} - // FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr { - now := time.Now().Unix() - - s.leasesLock.RLock() - defer s.leasesLock.RUnlock() - - ip4 := ip.To4() - if ip4 == nil { - return nil + if ip.To4() != nil { + return s.srv4.FindMACbyIP(ip) } - - for _, l := range s.leases { - if l.IP.Equal(ip4) { - unix := l.Expiry.Unix() - if unix > now || unix == leaseExpireStatic { - return l.HWAddr - } - } - } - return nil + return s.srv6.FindMACbyIP(ip) } -// Reset internal state -func (s *Server) reset() { - s.leasesLock.Lock() - s.leases = nil - s.IPpool = make(map[[4]byte]net.HardwareAddr) - s.leasesLock.Unlock() +// AddStaticLease - add static v4 lease +func (s *Server) AddStaticLease(lease Lease) error { + return s.srv4.AddStaticLease(lease) } diff --git a/dhcpd/dhcpd_test.go b/dhcpd/dhcpd_test.go index b08ffb86..ab31c900 100644 --- a/dhcpd/dhcpd_test.go +++ b/dhcpd/dhcpd_test.go @@ -1,3 +1,5 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + package dhcpd import ( @@ -7,7 +9,6 @@ import ( "testing" "time" - "github.com/krolaw/dhcp4" "github.com/stretchr/testify/assert" ) @@ -17,234 +18,66 @@ func check(t *testing.T, result bool, msg string) { } } -// Tests performed: -// . Handle Discover message (lease reserve) -// . Handle Request message (lease commit) -// . Static leases -func TestDHCP(t *testing.T) { - var s = Server{} - s.conf.DBFilePath = dbFilename - defer func() { _ = os.Remove(dbFilename) }() - var p, p2 dhcp4.Packet - var hw net.HardwareAddr - var lease *Lease - var opt dhcp4.Options - - s.reset() - s.leaseStart = []byte{1, 1, 1, 1} - s.leaseStop = []byte{1, 1, 1, 2} - s.leaseTime = 5 * time.Second - s.leaseOptions = dhcp4.Options{} - s.ipnet = &net.IPNet{ - IP: []byte{1, 2, 3, 4}, - Mask: []byte{0xff, 0xff, 0xff, 0xff}, - } - - p = make(dhcp4.Packet, 241) - - // Discover and reserve an IP - hw = []byte{3, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - p.SetCIAddr([]byte{0, 0, 0, 0}) - opt = make(dhcp4.Options, 10) - p2 = s.handleDiscover(p, opt) - opt = p2.ParseOptions() - check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.Offer)}), "dhcp4.Offer") - check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr") - check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr") - check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime") - check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier") - - lease = s.findLease(p) - check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr") - check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP") - - // Reserve an IP - the next IP from the range - hw = []byte{2, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - lease, _ = s.reserveLease(p) - check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr") - check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 2}), "lease.IP") - - // Reserve an IP - we have no more available IPs, - // so the first expired (or, in our case, not yet committed) lease is returned - hw = []byte{1, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - lease, _ = s.reserveLease(p) - check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr") - check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP") - - // Decline request for a lease which doesn't match our internal state - hw = []byte{1, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - p.SetCIAddr([]byte{0, 0, 0, 0}) - opt = make(dhcp4.Options, 10) - // ask a different IP - opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2} - p2 = s.handleDHCP4Request(p, opt) - opt = p2.ParseOptions() - check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK") - - // Commit the previously reserved lease - hw = []byte{1, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - p.SetCIAddr([]byte{0, 0, 0, 0}) - opt = make(dhcp4.Options, 10) - opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1} - p2 = s.handleDHCP4Request(p, opt) - opt = p2.ParseOptions() - check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.ACK)}), "dhcp4.ACK") - check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr") - check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr") - check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime") - check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier") - - check(t, bytes.Equal(s.FindIPbyMAC(hw), []byte{1, 1, 1, 1}), "FindIPbyMAC") - - // Commit the previously reserved lease #2 - hw = []byte{2, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - p.SetCIAddr([]byte{0, 0, 0, 0}) - opt = make(dhcp4.Options, 10) - opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2} - p2 = s.handleDHCP4Request(p, opt) - check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 2}), "p2.YIAddr") - - // Reserve an IP - we have no more available IPs - hw = []byte{3, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - lease, _ = s.reserveLease(p) - check(t, lease == nil, "lease == nil") - - s.reset() - testStaticLeases(t, &s) - testStaticLeaseReplaceByMAC(t, &s) - - s.reset() - misc(t, &s) -} - -func testStaticLeases(t *testing.T, s *Server) { - var err error - var l Lease - l.IP = []byte{1, 1, 1, 1} - - l.HWAddr = []byte{1, 2, 3, 4, 5, 6} - s.leases = append(s.leases, &l) - - // replace dynamic lease with a static (same IP) - l.HWAddr = []byte{2, 2, 3, 4, 5, 6} - err = s.AddStaticLease(l) - check(t, err == nil, "AddStaticLease") - - ll := s.Leases(LeasesAll) - assert.True(t, len(ll) == 1) - assert.True(t, bytes.Equal(ll[0].IP, []byte{1, 1, 1, 1})) - assert.True(t, bytes.Equal(ll[0].HWAddr, []byte{2, 2, 3, 4, 5, 6})) - assert.True(t, ll[0].Expiry.Unix() == leaseExpireStatic) - - err = s.RemoveStaticLease(l) - assert.True(t, err == nil) - - ll = s.Leases(LeasesAll) - assert.True(t, len(ll) == 0) -} - -func testStaticLeaseReplaceByMAC(t *testing.T, s *Server) { - var err error - var l Lease - l.HWAddr = []byte{1, 2, 3, 4, 5, 6} - - l.IP = []byte{1, 1, 1, 1} - l.Expiry = time.Now().Add(time.Hour) - s.leases = append(s.leases, &l) - - // replace dynamic lease with a static (same MAC) - l.IP = []byte{2, 1, 1, 1} - err = s.AddStaticLease(l) - assert.True(t, err == nil) - - ll := s.Leases(LeasesAll) - assert.True(t, len(ll) == 1) - assert.True(t, bytes.Equal(ll[0].IP, []byte{2, 1, 1, 1})) - assert.True(t, bytes.Equal(ll[0].HWAddr, []byte{1, 2, 3, 4, 5, 6})) -} - -// Small tests that don't require a static server's state -func misc(t *testing.T, s *Server) { - var p, p2 dhcp4.Packet - var hw net.HardwareAddr - var opt dhcp4.Options - - p = make(dhcp4.Packet, 241) - - // Try to commit a lease for an IP without prior Discover-Offer packets - hw = []byte{2, 2, 3, 4, 5, 6} - p.SetCHAddr(hw) - p.SetCIAddr([]byte{0, 0, 0, 0}) - opt = make(dhcp4.Options, 10) - opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1} - p2 = s.handleDHCP4Request(p, opt) - opt = p2.ParseOptions() - check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK") +func testNotify(flags uint32) { } // Leases database store/load func TestDB(t *testing.T) { - var s = Server{} + var err error + s := Server{} s.conf.DBFilePath = dbFilename - var p dhcp4.Packet - var hw1, hw2 net.HardwareAddr - var lease *Lease - s.reset() - s.leaseStart = []byte{1, 1, 1, 1} - s.leaseStop = []byte{1, 1, 1, 2} - s.leaseTime = 5 * time.Second - s.leaseOptions = dhcp4.Options{} - s.ipnet = &net.IPNet{ - IP: []byte{1, 2, 3, 4}, - Mask: []byte{0xff, 0xff, 0xff, 0xff}, + conf := V4ServerConf{ + Enabled: true, + RangeStart: "192.168.10.100", + RangeEnd: "192.168.10.200", + GatewayIP: "192.168.10.1", + SubnetMask: "255.255.255.0", + notify: testNotify, } + s.srv4, err = v4Create(conf) + assert.True(t, err == nil) - p = make(dhcp4.Packet, 241) + s.srv6, err = v6Create(V6ServerConf{}) + assert.True(t, err == nil) - hw1 = []byte{1, 2, 3, 4, 5, 6} - p.SetCHAddr(hw1) - lease, _ = s.reserveLease(p) - lease.Expiry = time.Unix(4000000001, 0) + l := Lease{} + l.IP = net.ParseIP("192.168.10.100").To4() + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + exp1 := time.Now().Add(time.Hour) + l.Expiry = exp1 + s.srv4.(*v4Server).addLease(&l) - hw2 = []byte{2, 2, 3, 4, 5, 6} - p.SetCHAddr(hw2) - lease, _ = s.reserveLease(p) - lease.Expiry = time.Unix(4000000002, 0) + l2 := Lease{} + l2.IP = net.ParseIP("192.168.10.101").To4() + l2.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:bb") + s.srv4.AddStaticLease(l2) _ = os.Remove("leases.db") s.dbStore() - s.reset() + s.srv4.ResetLeases(nil) s.dbLoad() - check(t, bytes.Equal(s.leases[0].HWAddr, hw1), "leases[0].HWAddr") - check(t, bytes.Equal(s.leases[0].IP, []byte{1, 1, 1, 1}), "leases[0].IP") - check(t, s.leases[0].Expiry.Unix() == 4000000001, "leases[0].Expiry") - check(t, bytes.Equal(s.leases[1].HWAddr, hw2), "leases[1].HWAddr") - check(t, bytes.Equal(s.leases[1].IP, []byte{1, 1, 1, 2}), "leases[1].IP") - check(t, s.leases[1].Expiry.Unix() == 4000000002, "leases[1].Expiry") + ll := s.srv4.GetLeases(LeasesAll) + + assert.Equal(t, "aa:aa:aa:aa:aa:bb", ll[0].HWAddr.String()) + assert.Equal(t, "192.168.10.101", ll[0].IP.String()) + assert.Equal(t, int64(leaseExpireStatic), ll[0].Expiry.Unix()) + + assert.Equal(t, "aa:aa:aa:aa:aa:aa", ll[1].HWAddr.String()) + assert.Equal(t, "192.168.10.100", ll[1].IP.String()) + assert.Equal(t, exp1.Unix(), ll[1].Expiry.Unix()) _ = os.Remove("leases.db") } func TestIsValidSubnetMask(t *testing.T) { - if !isValidSubnetMask([]byte{255, 255, 255, 0}) { - t.Fatalf("isValidSubnetMask([]byte{255,255,255,0})") - } - if isValidSubnetMask([]byte{255, 255, 253, 0}) { - t.Fatalf("isValidSubnetMask([]byte{255,255,253,0})") - } - if isValidSubnetMask([]byte{0, 255, 255, 255}) { - t.Fatalf("isValidSubnetMask([]byte{255,255,253,0})") - } + assert.True(t, isValidSubnetMask([]byte{255, 255, 255, 0})) + assert.True(t, isValidSubnetMask([]byte{255, 255, 254, 0})) + assert.True(t, isValidSubnetMask([]byte{255, 255, 252, 0})) + assert.True(t, !isValidSubnetMask([]byte{255, 255, 253, 0})) + assert.True(t, !isValidSubnetMask([]byte{255, 255, 255, 1})) } func TestNormalizeLeases(t *testing.T) { diff --git a/dhcpd/filter_conn.go b/dhcpd/filter_conn.go deleted file mode 100644 index 70cfa939..00000000 --- a/dhcpd/filter_conn.go +++ /dev/null @@ -1,63 +0,0 @@ -package dhcpd - -import ( - "net" - - "github.com/joomcode/errorx" - "golang.org/x/net/ipv4" -) - -// filterConn listens to 0.0.0.0:67, but accepts packets only from specific interface -// This is necessary for DHCP daemon to work, since binding to IP address doesn't -// us access to see Discover/Request packets from clients. -// -// 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 -} - -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 - } -} - -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 index 9553fca9..6c02110f 100644 --- a/dhcpd/helpers.go +++ b/dhcpd/helpers.go @@ -17,32 +17,24 @@ func isTimeout(err error) bool { 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() +// Get IPv4 address list +func getIfaceIPv4(iface net.Interface) []net.IP { + addrs, err := iface.Addrs() if err != nil { - panic(err) + return nil } - for _, addr := range ifaceAddrs { - ipnet, ok := addr.(*net.IPNet) + var res []net.IP + for _, a := range addrs { + ipnet, ok := a.(*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.Tracef("Got IP that is not IPv4: %v", ipnet.IP) continue } - - log.Tracef("Got IP that is IPv4: %v", ipnet.IP) - return &net.IPNet{ - IP: ipnet.IP.To4(), - Mask: ipnet.Mask, + if ipnet.IP.To4() != nil { + res = append(res, ipnet.IP.To4()) } } - return nil + return res } func wrapErrPrint(err error, message string, args ...interface{}) error { diff --git a/dhcpd/network_utils_test.go b/dhcpd/network_utils_test.go index 2957a411..fb6ef11e 100644 --- a/dhcpd/network_utils_test.go +++ b/dhcpd/network_utils_test.go @@ -1,3 +1,5 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + package dhcpd import ( diff --git a/dhcpd/server.go b/dhcpd/server.go new file mode 100644 index 00000000..5aea9497 --- /dev/null +++ b/dhcpd/server.go @@ -0,0 +1,81 @@ +package dhcpd + +import ( + "net" + "time" +) + +// DHCPServer - DHCP server interface +type DHCPServer interface { + // ResetLeases - reset leases + ResetLeases(leases []*Lease) + // GetLeases - get leases + GetLeases(flags int) []Lease + // GetLeasesRef - get reference to leases array + GetLeasesRef() []*Lease + // AddStaticLease - add a static lease + AddStaticLease(lease Lease) error + // RemoveStaticLease - remove a static lease + RemoveStaticLease(l Lease) error + // FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases + FindMACbyIP(ip net.IP) net.HardwareAddr + + // WriteDiskConfig4 - copy disk configuration + WriteDiskConfig4(c *V4ServerConf) + // WriteDiskConfig6 - copy disk configuration + WriteDiskConfig6(c *V6ServerConf) + + // Start - start server + Start() error + // Stop - stop server + Stop() +} + +// V4ServerConf - server configuration +type V4ServerConf struct { + Enabled bool `yaml:"-"` + InterfaceName string `yaml:"-"` + + GatewayIP string `yaml:"gateway_ip"` + SubnetMask string `yaml:"subnet_mask"` + + // The first & the last IP address for dynamic leases + // Bytes [0..2] of the last allowed IP address must match the first IP + RangeStart string `yaml:"range_start"` + RangeEnd string `yaml:"range_end"` + + LeaseDuration uint32 `yaml:"lease_duration"` // in seconds + + // IP conflict detector: time (ms) to wait for ICMP reply + // 0: disable + ICMPTimeout uint32 `yaml:"icmp_timeout_msec"` + + ipStart net.IP // starting IP address for dynamic leases + ipEnd net.IP // ending IP address for dynamic leases + leaseTime time.Duration // the time during which a dynamic lease is considered valid + dnsIPAddrs []net.IP // IPv4 addresses to return to DHCP clients as DNS server addresses + routerIP net.IP // value for Option Router + subnetMask net.IPMask // value for Option SubnetMask + + // Server calls this function when leases data changes + notify func(uint32) +} + +// V6ServerConf - server configuration +type V6ServerConf struct { + Enabled bool `yaml:"-"` + InterfaceName string `yaml:"-"` + + // The first IP address for dynamic leases + // The last allowed IP address ends with 0xff byte + RangeStart string `yaml:"range_start"` + + LeaseDuration uint32 `yaml:"lease_duration"` // in seconds + + ipStart net.IP // starting IP address for dynamic leases + leaseTime time.Duration // the time during which a dynamic lease is considered valid + dnsIPAddrs []net.IP // IPv6 addresses to return to DHCP clients as DNS server addresses + + // Server calls this function when leases data changes + notify func(uint32) +} diff --git a/dhcpd/standalone/main.go b/dhcpd/standalone/main.go deleted file mode 100644 index a9aab9ee..00000000 --- a/dhcpd/standalone/main.go +++ /dev/null @@ -1,115 +0,0 @@ -package main - -import ( - "net" - "os" - "os/signal" - "syscall" - "time" - - "github.com/AdguardTeam/AdGuardHome/dhcpd" - "github.com/AdguardTeam/golibs/log" - "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.Init(config) - if err != nil { - panic(err) - } - err = server.Start() - 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() - if err != nil { - panic(err) - } - log.Printf("Starting DHCP server while it's already running") - err = server.Start() - if err != nil { - panic(err) - } - log.Printf("Now serving DHCP") - signalChannel := make(chan os.Signal, 1) - signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) - <-signalChannel - -} - -// 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/dhcpd/v4.go b/dhcpd/v4.go new file mode 100644 index 00000000..ac0f9808 --- /dev/null +++ b/dhcpd/v4.go @@ -0,0 +1,623 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package dhcpd + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "sync" + "time" + + "github.com/AdguardTeam/golibs/log" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv4/server4" + "github.com/sparrc/go-ping" +) + +// v4Server - DHCPv4 server +type v4Server struct { + srv *server4.Server + leasesLock sync.Mutex + leases []*Lease + ipAddrs [256]byte + + conf V4ServerConf +} + +// WriteDiskConfig4 - write configuration +func (s *v4Server) WriteDiskConfig4(c *V4ServerConf) { + *c = s.conf +} + +// WriteDiskConfig6 - write configuration +func (s *v4Server) WriteDiskConfig6(c *V6ServerConf) { +} + +// Return TRUE if IP address is within range [start..stop] +func ip4InRange(start net.IP, stop net.IP, ip net.IP) bool { + if len(start) != 4 || len(stop) != 4 { + return false + } + from := binary.BigEndian.Uint32(start) + to := binary.BigEndian.Uint32(stop) + check := binary.BigEndian.Uint32(ip) + return from <= check && check <= to +} + +// ResetLeases - reset leases +func (s *v4Server) ResetLeases(leases []*Lease) { + s.leases = nil + + for _, l := range leases { + + if l.Expiry.Unix() != leaseExpireStatic && + !ip4InRange(s.conf.ipStart, s.conf.ipEnd, l.IP) { + + log.Debug("DHCPv4: skipping a lease with IP %v: not within current IP range", l.IP) + continue + } + + s.addLease(l) + } +} + +// GetLeasesRef - get leases +func (s *v4Server) GetLeasesRef() []*Lease { + return s.leases +} + +// Return TRUE if this lease holds a blacklisted IP +func (s *v4Server) blacklisted(l *Lease) bool { + return l.HWAddr.String() == "00:00:00:00:00:00" +} + +// GetLeases returns the list of current DHCP leases (thread-safe) +func (s *v4Server) GetLeases(flags int) []Lease { + var result []Lease + now := time.Now().Unix() + + s.leasesLock.Lock() + for _, lease := range s.leases { + if ((flags&LeasesDynamic) != 0 && lease.Expiry.Unix() > now && !s.blacklisted(lease)) || + ((flags&LeasesStatic) != 0 && lease.Expiry.Unix() == leaseExpireStatic) { + result = append(result, *lease) + } + } + s.leasesLock.Unlock() + + return result +} + +// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases +func (s *v4Server) FindMACbyIP(ip net.IP) net.HardwareAddr { + now := time.Now().Unix() + + s.leasesLock.Lock() + defer s.leasesLock.Unlock() + + ip4 := ip.To4() + if ip4 == nil { + return nil + } + + for _, l := range s.leases { + if l.IP.Equal(ip4) { + unix := l.Expiry.Unix() + if unix > now || unix == leaseExpireStatic { + return l.HWAddr + } + } + } + return nil +} + +// Add the specified IP to the black list for a time period +func (s *v4Server) blacklistLease(lease *Lease) { + hw := make(net.HardwareAddr, 6) + lease.HWAddr = hw + lease.Hostname = "" + lease.Expiry = time.Now().Add(s.conf.leaseTime) +} + +// Remove (swap) lease by index +func (s *v4Server) leaseRemoveSwapByIndex(i int) { + s.ipAddrs[s.leases[i].IP[3]] = 0 + log.Debug("DHCPv4: removed lease %s", s.leases[i].HWAddr) + + n := len(s.leases) + if i != n-1 { + s.leases[i] = s.leases[n-1] // swap with the last element + } + s.leases = s.leases[:n-1] +} + +// Remove a dynamic lease with the same properties +// Return error if a static lease is found +func (s *v4Server) rmDynamicLease(lease Lease) error { + for i := 0; i < len(s.leases); i++ { + l := s.leases[i] + + if bytes.Equal(l.HWAddr, lease.HWAddr) { + + if l.Expiry.Unix() == leaseExpireStatic { + return fmt.Errorf("static lease already exists") + } + + s.leaseRemoveSwapByIndex(i) + l = s.leases[i] + } + + if net.IP.Equal(l.IP, lease.IP) { + + if l.Expiry.Unix() == leaseExpireStatic { + return fmt.Errorf("static lease already exists") + } + + s.leaseRemoveSwapByIndex(i) + } + } + return nil +} + +// Add a lease +func (s *v4Server) addLease(l *Lease) { + s.leases = append(s.leases, l) + s.ipAddrs[l.IP[3]] = 1 + log.Debug("DHCPv4: added lease %s <-> %s", l.IP, l.HWAddr) +} + +// Remove a lease with the same properties +func (s *v4Server) rmLease(lease Lease) error { + for i, l := range s.leases { + if net.IP.Equal(l.IP, lease.IP) { + + if !bytes.Equal(l.HWAddr, lease.HWAddr) || + l.Hostname != lease.Hostname { + + return fmt.Errorf("Lease not found") + } + + s.leaseRemoveSwapByIndex(i) + return nil + } + } + return fmt.Errorf("lease not found") +} + +// AddStaticLease adds a static lease (thread-safe) +func (s *v4Server) AddStaticLease(lease Lease) error { + if len(lease.IP) != 4 { + return fmt.Errorf("invalid IP") + } + if len(lease.HWAddr) != 6 { + return fmt.Errorf("invalid MAC") + } + lease.Expiry = time.Unix(leaseExpireStatic, 0) + + s.leasesLock.Lock() + err := s.rmDynamicLease(lease) + if err != nil { + s.leasesLock.Unlock() + return err + } + s.addLease(&lease) + s.conf.notify(LeaseChangedDBStore) + s.leasesLock.Unlock() + + s.conf.notify(LeaseChangedAddedStatic) + return nil +} + +// RemoveStaticLease removes a static lease (thread-safe) +func (s *v4Server) RemoveStaticLease(l Lease) error { + if len(l.IP) != 4 { + return fmt.Errorf("invalid IP") + } + if len(l.HWAddr) != 6 { + return fmt.Errorf("invalid MAC") + } + + s.leasesLock.Lock() + err := s.rmLease(l) + if err != nil { + s.leasesLock.Unlock() + return err + } + s.conf.notify(LeaseChangedDBStore) + s.leasesLock.Unlock() + + s.conf.notify(LeaseChangedRemovedStatic) + return nil +} + +// Send ICMP to the specified machine +// Return TRUE if it doesn't reply, which probably means that the IP is available +func (s *v4Server) addrAvailable(target net.IP) bool { + + if s.conf.ICMPTimeout == 0 { + return true + } + + pinger, err := ping.NewPinger(target.String()) + if err != nil { + log.Error("ping.NewPinger(): %v", err) + return true + } + + pinger.SetPrivileged(true) + pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond + pinger.Count = 1 + reply := false + pinger.OnRecv = func(pkt *ping.Packet) { + reply = true + } + log.Debug("DHCPv4: Sending ICMP Echo to %v", target) + pinger.Run() + + if reply { + log.Info("DHCPv4: IP conflict: %v is already used by another device", target) + return false + } + + log.Debug("DHCPv4: ICMP procedure is complete: %v", target) + return true +} + +// Find lease by MAC +func (s *v4Server) findLease(mac net.HardwareAddr) *Lease { + for i := range s.leases { + if bytes.Equal(mac, s.leases[i].HWAddr) { + return s.leases[i] + } + } + return nil +} + +// Get next free IP +func (s *v4Server) findFreeIP() net.IP { + for i := s.conf.ipStart[3]; ; i++ { + if s.ipAddrs[i] == 0 { + ip := make([]byte, 4) + copy(ip, s.conf.ipStart) + ip[3] = i + return ip + } + if i == s.conf.ipEnd[3] { + break + } + } + return nil +} + +// Find an expired lease and return its index or -1 +func (s *v4Server) findExpiredLease() int { + now := time.Now().Unix() + for i, lease := range s.leases { + if lease.Expiry.Unix() != leaseExpireStatic && + lease.Expiry.Unix() <= now { + return i + } + } + return -1 +} + +// Reserve lease for MAC +func (s *v4Server) reserveLease(mac net.HardwareAddr) *Lease { + l := Lease{} + l.HWAddr = make([]byte, 6) + copy(l.HWAddr, mac) + + l.IP = s.findFreeIP() + if l.IP == nil { + i := s.findExpiredLease() + if i < 0 { + return nil + } + copy(s.leases[i].HWAddr, mac) + return s.leases[i] + } + + s.addLease(&l) + return &l +} + +func (s *v4Server) commitLease(l *Lease) { + l.Expiry = time.Now().Add(s.conf.leaseTime) + + s.leasesLock.Lock() + s.conf.notify(LeaseChangedDBStore) + s.leasesLock.Unlock() + + s.conf.notify(LeaseChangedAdded) +} + +// Process Discover request and return lease +func (s *v4Server) processDiscover(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) *Lease { + mac := req.ClientHWAddr + + s.leasesLock.Lock() + defer s.leasesLock.Unlock() + + lease := s.findLease(mac) + if lease == nil { + toStore := false + for lease == nil { + lease = s.reserveLease(mac) + if lease == nil { + log.Debug("DHCPv4: No more IP addresses") + if toStore { + s.conf.notify(LeaseChangedDBStore) + } + return nil + } + + toStore = true + + if !s.addrAvailable(lease.IP) { + s.blacklistLease(lease) + lease = nil + continue + } + break + } + + s.conf.notify(LeaseChangedDBStore) + + // s.conf.notify(LeaseChangedBlacklisted) + + } else { + reqIP := req.Options.Get(dhcpv4.OptionRequestedIPAddress) + if len(reqIP) != 0 && + !bytes.Equal(reqIP, lease.IP) { + log.Debug("DHCPv4: different RequestedIP: %v != %v", reqIP, lease.IP) + } + } + + hostname := req.Options.Get(dhcpv4.OptionHostName) + lease.Hostname = string(hostname) + + resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) + return lease +} + +// Process Request request and return lease +// Return false if we don't need to reply +func (s *v4Server) processRequest(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) (*Lease, bool) { + var lease *Lease + mac := req.ClientHWAddr + hostname := req.Options.Get(dhcpv4.OptionHostName) + reqIP := req.Options.Get(dhcpv4.OptionRequestedIPAddress) + if reqIP == nil { + reqIP = req.ClientIPAddr + } + + sid := req.Options.Get(dhcpv4.OptionServerIdentifier) + if len(sid) != 0 && + !bytes.Equal(sid, s.conf.dnsIPAddrs[0]) { + log.Debug("DHCPv4: Bad OptionServerIdentifier in Request message for %s", mac) + return nil, false + } + + if len(reqIP) != 4 { + log.Debug("DHCPv4: Bad OptionRequestedIPAddress in Request message for %s", mac) + return nil, false + } + + s.leasesLock.Lock() + for _, l := range s.leases { + if bytes.Equal(l.HWAddr, mac) { + if !bytes.Equal(l.IP, reqIP) { + s.leasesLock.Unlock() + log.Debug("DHCPv4: Mismatched OptionRequestedIPAddress in Request message for %s", mac) + return nil, true + } + + if !bytes.Equal([]byte(l.Hostname), hostname) { + s.leasesLock.Unlock() + log.Debug("DHCPv4: Mismatched OptionHostName in Request message for %s", mac) + return nil, true + } + + lease = l + break + } + } + s.leasesLock.Unlock() + + if lease == nil { + log.Debug("DHCPv4: No lease for %s", mac) + return nil, true + } + + if lease.Expiry.Unix() != leaseExpireStatic { + s.commitLease(lease) + } + + resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) + return lease, true +} + +// Find a lease associated with MAC and prepare response +// Return 1: OK +// Return 0: error; reply with Nak +// Return -1: error; don't reply +func (s *v4Server) process(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) int { + + var lease *Lease + + resp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0])) + + switch req.MessageType() { + + case dhcpv4.MessageTypeDiscover: + lease = s.processDiscover(req, resp) + if lease == nil { + return 0 + } + + case dhcpv4.MessageTypeRequest: + var toReply bool + lease, toReply = s.processRequest(req, resp) + if lease == nil { + if toReply { + return 0 + } + return -1 // drop packet + } + } + + resp.YourIPAddr = make([]byte, 4) + copy(resp.YourIPAddr, lease.IP) + + resp.UpdateOption(dhcpv4.OptIPAddressLeaseTime(s.conf.leaseTime)) + resp.UpdateOption(dhcpv4.OptRouter(s.conf.routerIP)) + resp.UpdateOption(dhcpv4.OptSubnetMask(s.conf.subnetMask)) + resp.UpdateOption(dhcpv4.OptDNS(s.conf.dnsIPAddrs...)) + return 1 +} + +// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Discover,ClientID,ReqIP,HostName) -> server(255.255.255.255:67) +// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Type=Offer,ServerID,SubnetMask,LeaseTime) <- server(:67) +// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Request,ClientID,ReqIP||ClientIP,HostName,ServerID,ParamReqList) -> server(255.255.255.255:67) +// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Type=ACK,ServerID,SubnetMask,LeaseTime) <- server(:67) +func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) { + log.Debug("DHCPv4: received message: %s", req.Summary()) + + switch req.MessageType() { + case dhcpv4.MessageTypeDiscover, + dhcpv4.MessageTypeRequest: + // + + default: + log.Debug("DHCPv4: unsupported message type %d", req.MessageType()) + return + } + + resp, err := dhcpv4.NewReplyFromRequest(req) + if err != nil { + log.Debug("DHCPv4: dhcpv4.New: %s", err) + return + } + + if len(req.ClientHWAddr) != 6 { + log.Debug("DHCPv4: Invalid ClientHWAddr") + return + } + + r := s.process(req, resp) + if r < 0 { + return + } else if r == 0 { + resp.Options.Update(dhcpv4.OptMessageType(dhcpv4.MessageTypeNak)) + } + + log.Debug("DHCPv4: sending: %s", resp.Summary()) + + _, err = conn.WriteTo(resp.ToBytes(), peer) + if err != nil { + log.Error("DHCPv4: conn.Write to %s failed: %s", peer, err) + return + } +} + +// Start - start server +func (s *v4Server) Start() error { + if !s.conf.Enabled { + return nil + } + + iface, err := net.InterfaceByName(s.conf.InterfaceName) + if err != nil { + return fmt.Errorf("DHCPv4: Couldn't find interface by name %s: %s", s.conf.InterfaceName, err) + } + + log.Debug("DHCPv4: starting...") + s.conf.dnsIPAddrs = getIfaceIPv4(*iface) + if len(s.conf.dnsIPAddrs) == 0 { + log.Debug("DHCPv4: no IPv6 address for interface %s", iface.Name) + return nil + } + + laddr := &net.UDPAddr{ + IP: net.ParseIP("0.0.0.0"), + Port: dhcpv4.ServerPort, + } + s.srv, err = server4.NewServer(iface.Name, laddr, s.packetHandler, server4.WithDebugLogger()) + if err != nil { + return err + } + + log.Info("DHCPv4: listening") + + go func() { + err = s.srv.Serve() + log.Debug("DHCPv4: srv.Serve: %s", err) + }() + return nil +} + +// Stop - stop server +func (s *v4Server) Stop() { + if s.srv == nil { + return + } + + log.Debug("DHCPv4: stopping") + err := s.srv.Close() + if err != nil { + log.Error("DHCPv4: srv.Close: %s", err) + } + // now s.srv.Serve() will return + s.srv = nil +} + +// Create DHCPv4 server +func v4Create(conf V4ServerConf) (DHCPServer, error) { + s := &v4Server{} + s.conf = conf + + if !conf.Enabled { + return s, nil + } + + var err error + s.conf.routerIP, err = parseIPv4(s.conf.GatewayIP) + if err != nil { + return s, fmt.Errorf("DHCPv4: %s", err) + } + + subnet, err := parseIPv4(s.conf.SubnetMask) + if err != nil || !isValidSubnetMask(subnet) { + return s, fmt.Errorf("DHCPv4: invalid subnet mask: %s", s.conf.SubnetMask) + } + s.conf.subnetMask = make([]byte, 4) + copy(s.conf.subnetMask, subnet) + + s.conf.ipStart, err = parseIPv4(conf.RangeStart) + if s.conf.ipStart == nil { + return s, fmt.Errorf("DHCPv4: %s", err) + } + if s.conf.ipStart[0] == 0 { + return s, fmt.Errorf("DHCPv4: invalid range start IP") + } + + s.conf.ipEnd, err = parseIPv4(conf.RangeEnd) + if s.conf.ipEnd == nil { + return s, fmt.Errorf("DHCPv4: %s", err) + } + if !net.IP.Equal(s.conf.ipStart[:3], s.conf.ipEnd[:3]) || + s.conf.ipStart[3] > s.conf.ipEnd[3] { + return s, fmt.Errorf("DHCPv4: range end IP should match range start IP") + } + + if conf.LeaseDuration == 0 { + s.conf.leaseTime = time.Hour * 24 + s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds()) + } else { + s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration) + } + + return s, nil +} diff --git a/dhcpd/v46_windows.go b/dhcpd/v46_windows.go new file mode 100644 index 00000000..152899b9 --- /dev/null +++ b/dhcpd/v46_windows.go @@ -0,0 +1,47 @@ +package dhcpd + +// 'u-root/u-root' package, a dependency of 'insomniacslk/dhcp' package, doesn't build on Windows + +import "net" + +type winServer struct { +} + +func (s *winServer) ResetLeases(leases []*Lease) { +} +func (s *winServer) GetLeases(flags int) []Lease { + return nil +} +func (s *winServer) GetLeasesRef() []*Lease { + return nil +} +func (s *winServer) AddStaticLease(lease Lease) error { + return nil +} +func (s *winServer) RemoveStaticLease(l Lease) error { + return nil +} +func (s *winServer) FindMACbyIP(ip net.IP) net.HardwareAddr { + return nil +} + +func (s *winServer) WriteDiskConfig4(c *V4ServerConf) { +} +func (s *winServer) WriteDiskConfig6(c *V6ServerConf) { +} + +func (s *winServer) Start() error { + return nil +} +func (s *winServer) Stop() { +} +func (s *winServer) Reset() { +} + +func v4Create(conf V4ServerConf) (DHCPServer, error) { + return &winServer{}, nil +} + +func v6Create(conf V6ServerConf) (DHCPServer, error) { + return &winServer{}, nil +} diff --git a/dhcpd/v4_test.go b/dhcpd/v4_test.go new file mode 100644 index 00000000..208dd8fb --- /dev/null +++ b/dhcpd/v4_test.go @@ -0,0 +1,232 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package dhcpd + +import ( + "net" + "testing" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/stretchr/testify/assert" +) + +func notify4(flags uint32) { +} + +func TestV4StaticLeaseAddRemove(t *testing.T) { + conf := V4ServerConf{ + Enabled: true, + RangeStart: "192.168.10.100", + RangeEnd: "192.168.10.200", + GatewayIP: "192.168.10.1", + SubnetMask: "255.255.255.0", + notify: notify4, + } + s, err := v4Create(conf) + assert.True(t, err == nil) + + ls := s.GetLeases(LeasesStatic) + assert.Equal(t, 0, len(ls)) + + // add static lease + l := Lease{} + l.IP = net.ParseIP("192.168.10.150").To4() + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + assert.True(t, s.AddStaticLease(l) == nil) + + // try to add the same static lease - fail + assert.True(t, s.AddStaticLease(l) != nil) + + // check + ls = s.GetLeases(LeasesStatic) + assert.Equal(t, 1, len(ls)) + assert.Equal(t, "192.168.10.150", ls[0].IP.String()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) + assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic) + + // try to remove static lease - fail + l.IP = net.ParseIP("192.168.10.110").To4() + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + assert.True(t, s.RemoveStaticLease(l) != nil) + + // remove static lease + l.IP = net.ParseIP("192.168.10.150").To4() + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + assert.True(t, s.RemoveStaticLease(l) == nil) + + // check + ls = s.GetLeases(LeasesStatic) + assert.Equal(t, 0, len(ls)) +} + +func TestV4StaticLeaseAddReplaceDynamic(t *testing.T) { + conf := V4ServerConf{ + Enabled: true, + RangeStart: "192.168.10.100", + RangeEnd: "192.168.10.200", + GatewayIP: "192.168.10.1", + SubnetMask: "255.255.255.0", + notify: notify4, + } + sIface, err := v4Create(conf) + s := sIface.(*v4Server) + assert.True(t, err == nil) + + // add dynamic lease + ld := Lease{} + ld.IP = net.ParseIP("192.168.10.150").To4() + ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa") + s.addLease(&ld) + + // add dynamic lease + { + ld := Lease{} + ld.IP = net.ParseIP("192.168.10.151").To4() + ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa") + s.addLease(&ld) + } + + // add static lease with the same IP + l := Lease{} + l.IP = net.ParseIP("192.168.10.150").To4() + l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa") + assert.True(t, s.AddStaticLease(l) == nil) + + // add static lease with the same MAC + l = Lease{} + l.IP = net.ParseIP("192.168.10.152").To4() + l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa") + assert.True(t, s.AddStaticLease(l) == nil) + + // check + ls := s.GetLeases(LeasesStatic) + assert.Equal(t, 2, len(ls)) + + assert.Equal(t, "192.168.10.150", ls[0].IP.String()) + assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) + assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic) + + assert.Equal(t, "192.168.10.152", ls[1].IP.String()) + assert.Equal(t, "22:aa:aa:aa:aa:aa", ls[1].HWAddr.String()) + assert.True(t, ls[1].Expiry.Unix() == leaseExpireStatic) +} + +func TestV4StaticLeaseGet(t *testing.T) { + conf := V4ServerConf{ + Enabled: true, + RangeStart: "192.168.10.100", + RangeEnd: "192.168.10.200", + GatewayIP: "192.168.10.1", + SubnetMask: "255.255.255.0", + notify: notify4, + } + sIface, err := v4Create(conf) + s := sIface.(*v4Server) + assert.True(t, err == nil) + s.conf.dnsIPAddrs = []net.IP{net.ParseIP("192.168.10.1").To4()} + + l := Lease{} + l.IP = net.ParseIP("192.168.10.150").To4() + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + assert.True(t, s.AddStaticLease(l) == nil) + + // "Discover" + mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa") + req, _ := dhcpv4.NewDiscovery(mac) + resp, _ := dhcpv4.NewReplyFromRequest(req) + assert.Equal(t, 1, s.process(req, resp)) + + // check "Offer" + assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String()) + assert.Equal(t, "192.168.10.150", resp.YourIPAddr.String()) + assert.Equal(t, "192.168.10.1", resp.Router()[0].String()) + assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) + assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) + assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + + // "Request" + req, _ = dhcpv4.NewRequestFromOffer(resp) + resp, _ = dhcpv4.NewReplyFromRequest(req) + assert.Equal(t, 1, s.process(req, resp)) + + // check "Ack" + assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String()) + assert.Equal(t, "192.168.10.150", resp.YourIPAddr.String()) + assert.Equal(t, "192.168.10.1", resp.Router()[0].String()) + assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) + assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) + assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + + dnsAddrs := resp.DNS() + assert.Equal(t, 1, len(dnsAddrs)) + assert.Equal(t, "192.168.10.1", dnsAddrs[0].String()) + + // check lease + ls := s.GetLeases(LeasesStatic) + assert.Equal(t, 1, len(ls)) + assert.Equal(t, "192.168.10.150", ls[0].IP.String()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) +} + +func TestV4DynamicLeaseGet(t *testing.T) { + conf := V4ServerConf{ + Enabled: true, + RangeStart: "192.168.10.100", + RangeEnd: "192.168.10.200", + GatewayIP: "192.168.10.1", + SubnetMask: "255.255.255.0", + notify: notify4, + } + sIface, err := v4Create(conf) + s := sIface.(*v4Server) + assert.True(t, err == nil) + s.conf.dnsIPAddrs = []net.IP{net.ParseIP("192.168.10.1").To4()} + + // "Discover" + mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa") + req, _ := dhcpv4.NewDiscovery(mac) + resp, _ := dhcpv4.NewReplyFromRequest(req) + assert.Equal(t, 1, s.process(req, resp)) + + // check "Offer" + assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String()) + assert.Equal(t, "192.168.10.100", resp.YourIPAddr.String()) + assert.Equal(t, "192.168.10.1", resp.Router()[0].String()) + assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) + assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) + assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + + // "Request" + req, _ = dhcpv4.NewRequestFromOffer(resp) + resp, _ = dhcpv4.NewReplyFromRequest(req) + assert.Equal(t, 1, s.process(req, resp)) + + // check "Ack" + assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String()) + assert.Equal(t, "192.168.10.100", resp.YourIPAddr.String()) + assert.Equal(t, "192.168.10.1", resp.Router()[0].String()) + assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) + assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) + assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + + dnsAddrs := resp.DNS() + assert.Equal(t, 1, len(dnsAddrs)) + assert.Equal(t, "192.168.10.1", dnsAddrs[0].String()) + + // check lease + ls := s.GetLeases(LeasesDynamic) + assert.Equal(t, 1, len(ls)) + assert.Equal(t, "192.168.10.100", ls[0].IP.String()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) + + start := net.ParseIP("192.168.10.100").To4() + stop := net.ParseIP("192.168.10.200").To4() + assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.10.99").To4())) + assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.11.100").To4())) + assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.11.201").To4())) + assert.True(t, ip4InRange(start, stop, net.ParseIP("192.168.10.100").To4())) +} diff --git a/dhcpd/v6.go b/dhcpd/v6.go new file mode 100644 index 00000000..8f042e25 --- /dev/null +++ b/dhcpd/v6.go @@ -0,0 +1,638 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package dhcpd + +import ( + "bytes" + "fmt" + "net" + "sync" + "time" + + "github.com/AdguardTeam/golibs/log" + "github.com/insomniacslk/dhcp/dhcpv6" + "github.com/insomniacslk/dhcp/dhcpv6/server6" + "github.com/insomniacslk/dhcp/iana" +) + +const valueIAID = "ADGH" // value for IANA.ID + +// v6Server - DHCPv6 server +type v6Server struct { + srv *server6.Server + leasesLock sync.Mutex + leases []*Lease + ipAddrs [256]byte + sid dhcpv6.Duid + + conf V6ServerConf +} + +// WriteDiskConfig4 - write configuration +func (s *v6Server) WriteDiskConfig4(c *V4ServerConf) { +} + +// WriteDiskConfig6 - write configuration +func (s *v6Server) WriteDiskConfig6(c *V6ServerConf) { + *c = s.conf +} + +// Return TRUE if IP address is within range [start..0xff] +// nolint(staticcheck) +func ip6InRange(start net.IP, ip net.IP) bool { + if len(start) != 16 { + return false + } + if !bytes.Equal(start[:15], ip[:15]) { + return false + } + return start[15] <= ip[15] +} + +// ResetLeases - reset leases +func (s *v6Server) ResetLeases(ll []*Lease) { + s.leases = nil + for _, l := range ll { + + if l.Expiry.Unix() != leaseExpireStatic && + !ip6InRange(s.conf.ipStart, l.IP) { + + log.Debug("DHCPv6: skipping a lease with IP %v: not within current IP range", l.IP) + continue + } + + s.addLease(l) + } +} + +// GetLeases - get current leases +func (s *v6Server) GetLeases(flags int) []Lease { + var result []Lease + s.leasesLock.Lock() + for _, lease := range s.leases { + + if lease.Expiry.Unix() == leaseExpireStatic { + if (flags & LeasesStatic) != 0 { + result = append(result, *lease) + } + + } else { + if (flags & LeasesDynamic) != 0 { + result = append(result, *lease) + } + } + } + s.leasesLock.Unlock() + return result +} + +// GetLeasesRef - get leases +func (s *v6Server) GetLeasesRef() []*Lease { + return s.leases +} + +// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases +func (s *v6Server) FindMACbyIP(ip net.IP) net.HardwareAddr { + now := time.Now().Unix() + + s.leasesLock.Lock() + defer s.leasesLock.Unlock() + + for _, l := range s.leases { + if l.IP.Equal(ip) { + unix := l.Expiry.Unix() + if unix > now || unix == leaseExpireStatic { + return l.HWAddr + } + } + } + return nil +} + +// Remove (swap) lease by index +func (s *v6Server) leaseRemoveSwapByIndex(i int) { + s.ipAddrs[s.leases[i].IP[15]] = 0 + log.Debug("DHCPv6: removed lease %s", s.leases[i].HWAddr) + + n := len(s.leases) + if i != n-1 { + s.leases[i] = s.leases[n-1] // swap with the last element + } + s.leases = s.leases[:n-1] +} + +// Remove a dynamic lease with the same properties +// Return error if a static lease is found +func (s *v6Server) rmDynamicLease(lease Lease) error { + for i := 0; i < len(s.leases); i++ { + l := s.leases[i] + + if bytes.Equal(l.HWAddr, lease.HWAddr) { + + if l.Expiry.Unix() == leaseExpireStatic { + return fmt.Errorf("static lease already exists") + } + + s.leaseRemoveSwapByIndex(i) + l = s.leases[i] + } + + if net.IP.Equal(l.IP, lease.IP) { + + if l.Expiry.Unix() == leaseExpireStatic { + return fmt.Errorf("static lease already exists") + } + + s.leaseRemoveSwapByIndex(i) + } + } + return nil +} + +// AddStaticLease - add a static lease +func (s *v6Server) AddStaticLease(l Lease) error { + if len(l.IP) != 16 { + return fmt.Errorf("invalid IP") + } + if len(l.HWAddr) != 6 { + return fmt.Errorf("invalid MAC") + } + + l.Expiry = time.Unix(leaseExpireStatic, 0) + + s.leasesLock.Lock() + err := s.rmDynamicLease(l) + if err != nil { + s.leasesLock.Unlock() + return err + } + s.addLease(&l) + s.conf.notify(LeaseChangedDBStore) + s.leasesLock.Unlock() + + s.conf.notify(LeaseChangedAddedStatic) + return nil +} + +// RemoveStaticLease - remove a static lease +func (s *v6Server) RemoveStaticLease(l Lease) error { + if len(l.IP) != 16 { + return fmt.Errorf("invalid IP") + } + if len(l.HWAddr) != 6 { + return fmt.Errorf("invalid MAC") + } + + s.leasesLock.Lock() + err := s.rmLease(l) + if err != nil { + s.leasesLock.Unlock() + return err + } + s.conf.notify(LeaseChangedDBStore) + s.leasesLock.Unlock() + s.conf.notify(LeaseChangedRemovedStatic) + return nil +} + +// Add a lease +func (s *v6Server) addLease(l *Lease) { + s.leases = append(s.leases, l) + s.ipAddrs[l.IP[15]] = 1 + log.Debug("DHCPv6: added lease %s <-> %s", l.IP, l.HWAddr) +} + +// Remove a lease with the same properties +func (s *v6Server) rmLease(lease Lease) error { + for i, l := range s.leases { + if net.IP.Equal(l.IP, lease.IP) { + + if !bytes.Equal(l.HWAddr, lease.HWAddr) || + l.Hostname != lease.Hostname { + + return fmt.Errorf("Lease not found") + } + + s.leaseRemoveSwapByIndex(i) + return nil + } + } + return fmt.Errorf("lease not found") +} + +// Find lease by MAC +func (s *v6Server) findLease(mac net.HardwareAddr) *Lease { + s.leasesLock.Lock() + defer s.leasesLock.Unlock() + + for i := range s.leases { + if bytes.Equal(mac, s.leases[i].HWAddr) { + return s.leases[i] + } + } + return nil +} + +// Find an expired lease and return its index or -1 +func (s *v6Server) findExpiredLease() int { + now := time.Now().Unix() + for i, lease := range s.leases { + if lease.Expiry.Unix() != leaseExpireStatic && + lease.Expiry.Unix() <= now { + return i + } + } + return -1 +} + +// Get next free IP +func (s *v6Server) findFreeIP() net.IP { + for i := s.conf.ipStart[15]; ; i++ { + if s.ipAddrs[i] == 0 { + ip := make([]byte, 16) + copy(ip, s.conf.ipStart) + ip[15] = i + return ip + } + if i == 0xff { + break + } + } + return nil +} + +// Reserve lease for MAC +func (s *v6Server) reserveLease(mac net.HardwareAddr) *Lease { + l := Lease{} + l.HWAddr = make([]byte, 6) + copy(l.HWAddr, mac) + + s.leasesLock.Lock() + defer s.leasesLock.Unlock() + + copy(l.IP, s.conf.ipStart) + l.IP = s.findFreeIP() + if l.IP == nil { + i := s.findExpiredLease() + if i < 0 { + return nil + } + copy(s.leases[i].HWAddr, mac) + return s.leases[i] + } + + s.addLease(&l) + return &l +} + +func (s *v6Server) commitDynamicLease(l *Lease) { + l.Expiry = time.Now().Add(s.conf.leaseTime) + + s.leasesLock.Lock() + s.conf.notify(LeaseChangedDBStore) + s.leasesLock.Unlock() + s.conf.notify(LeaseChangedAdded) +} + +// Check Client ID +func (s *v6Server) checkCID(msg *dhcpv6.Message) error { + if msg.Options.ClientID() == nil { + return fmt.Errorf("DHCPv6: no ClientID option in request") + } + return nil +} + +// Check ServerID policy +func (s *v6Server) checkSID(msg *dhcpv6.Message) error { + sid := msg.Options.ServerID() + + switch msg.Type() { + case dhcpv6.MessageTypeSolicit, + dhcpv6.MessageTypeConfirm, + dhcpv6.MessageTypeRebind: + + if sid != nil { + return fmt.Errorf("DHCPv6: drop packet: ServerID option in message %s", msg.Type().String()) + } + + case dhcpv6.MessageTypeRequest, + dhcpv6.MessageTypeRenew, + dhcpv6.MessageTypeRelease, + dhcpv6.MessageTypeDecline: + + if sid == nil { + return fmt.Errorf("DHCPv6: drop packet: no ServerID option in message %s", msg.Type().String()) + } + if !sid.Equal(s.sid) { + return fmt.Errorf("DHCPv6: drop packet: mismatched ServerID option in message %s: %s", + msg.Type().String(), sid.String()) + } + } + + return nil +} + +// . IAAddress must be equal to the lease's IP +func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *Lease) error { + switch msg.Type() { + case dhcpv6.MessageTypeRequest, + dhcpv6.MessageTypeConfirm, + dhcpv6.MessageTypeRenew, + dhcpv6.MessageTypeRebind: + + oia := msg.Options.OneIANA() + if oia == nil { + return fmt.Errorf("no IANA option in %s", msg.Type().String()) + } + + oiaAddr := oia.Options.OneAddress() + if oiaAddr == nil { + return fmt.Errorf("no IANA.Addr option in %s", msg.Type().String()) + } + + if !oiaAddr.IPv6Addr.Equal(lease.IP) { + return fmt.Errorf("invalid IANA.Addr option in %s", msg.Type().String()) + } + } + return nil +} + +// Store lease in DB (if necessary) and return lease life time +func (s *v6Server) commitLease(msg *dhcpv6.Message, lease *Lease) time.Duration { + lifetime := s.conf.leaseTime + + switch msg.Type() { + case dhcpv6.MessageTypeSolicit: + // + + case dhcpv6.MessageTypeConfirm: + lifetime = lease.Expiry.Sub(time.Now()) + + case dhcpv6.MessageTypeRequest, + dhcpv6.MessageTypeRenew, + dhcpv6.MessageTypeRebind: + + if lease.Expiry.Unix() != leaseExpireStatic { + s.commitDynamicLease(lease) + } + } + return lifetime +} + +// Find a lease associated with MAC and prepare response +func (s *v6Server) process(msg *dhcpv6.Message, req dhcpv6.DHCPv6, resp dhcpv6.DHCPv6) bool { + switch msg.Type() { + case dhcpv6.MessageTypeSolicit, + dhcpv6.MessageTypeRequest, + dhcpv6.MessageTypeConfirm, + dhcpv6.MessageTypeRenew, + dhcpv6.MessageTypeRebind: + // continue + + default: + return false + } + + mac, err := dhcpv6.ExtractMAC(req) + if err != nil { + log.Debug("DHCPv6: dhcpv6.ExtractMAC: %s", err) + return false + } + + lease := s.findLease(mac) + if lease == nil { + log.Debug("DHCPv6: no lease for: %s", mac) + + switch msg.Type() { + + case dhcpv6.MessageTypeSolicit: + lease = s.reserveLease(mac) + if lease == nil { + return false + } + + default: + return false + } + } + + err = s.checkIA(msg, lease) + if err != nil { + log.Debug("DHCPv6: %s", err) + return false + } + + lifetime := s.commitLease(msg, lease) + + oia := &dhcpv6.OptIANA{ + T1: lifetime / 2, + T2: time.Duration(float32(lifetime) / 1.5), + } + roia := msg.Options.OneIANA() + if roia != nil { + copy(oia.IaId[:], roia.IaId[:]) + } else { + copy(oia.IaId[:], []byte(valueIAID)) + } + oiaAddr := &dhcpv6.OptIAAddress{ + IPv6Addr: lease.IP, + PreferredLifetime: lifetime, + ValidLifetime: lifetime, + } + oia.Options = dhcpv6.IdentityOptions{ + Options: []dhcpv6.Option{oiaAddr}, + } + resp.AddOption(oia) + + if msg.IsOptionRequested(dhcpv6.OptionDNSRecursiveNameServer) { + resp.UpdateOption(dhcpv6.OptDNS(s.conf.dnsIPAddrs...)) + } + + fqdn := msg.GetOneOption(dhcpv6.OptionFQDN) + if fqdn != nil { + resp.AddOption(fqdn) + } + + resp.AddOption(&dhcpv6.OptStatusCode{ + StatusCode: iana.StatusSuccess, + StatusMessage: "success", + }) + return true +} + +// 1. +// fe80::* (client) --(Solicit + ClientID+IANA())-> ff02::1:2 +// server -(Advertise + ClientID+ServerID+IANA(IAAddress)> fe80::* +// fe80::* --(Request + ClientID+ServerID+IANA(IAAddress))-> ff02::1:2 +// server -(Reply + ClientID+ServerID+IANA(IAAddress)+DNS)> fe80::* +// +// 2. +// fe80::* --(Confirm|Renew|Rebind + ClientID+IANA(IAAddress))-> ff02::1:2 +// server -(Reply + ClientID+ServerID+IANA(IAAddress)+DNS)> fe80::* +// +// 3. +// fe80::* --(Release + ClientID+ServerID+IANA(IAAddress))-> ff02::1:2 +func (s *v6Server) packetHandler(conn net.PacketConn, peer net.Addr, req dhcpv6.DHCPv6) { + msg, err := req.GetInnerMessage() + if err != nil { + log.Error("DHCPv6: %s", err) + return + } + + log.Debug("DHCPv6: received: %s", req.Summary()) + + err = s.checkCID(msg) + if err != nil { + log.Debug("%s", err) + return + } + + err = s.checkSID(msg) + if err != nil { + log.Debug("%s", err) + return + } + + var resp dhcpv6.DHCPv6 + + switch msg.Type() { + case dhcpv6.MessageTypeSolicit: + if msg.GetOneOption(dhcpv6.OptionRapidCommit) == nil { + resp, err = dhcpv6.NewAdvertiseFromSolicit(msg) + break + } + fallthrough + + case dhcpv6.MessageTypeRequest, + dhcpv6.MessageTypeConfirm, + dhcpv6.MessageTypeRenew, + dhcpv6.MessageTypeRebind, + dhcpv6.MessageTypeRelease, + dhcpv6.MessageTypeInformationRequest: + resp, err = dhcpv6.NewReplyFromMessage(msg) + + default: + log.Error("DHCPv6: message type %d not supported", msg.Type()) + return + } + + if err != nil { + log.Error("DHCPv6: %s", err) + return + } + + resp.AddOption(dhcpv6.OptServerID(s.sid)) + + _ = s.process(msg, req, resp) + + log.Debug("DHCPv6: sending: %s", resp.Summary()) + + _, err = conn.WriteTo(resp.ToBytes(), peer) + if err != nil { + log.Error("DHCPv6: conn.Write to %s failed: %s", peer, err) + return + } +} + +// Get IPv6 address list +func getIfaceIPv6(iface net.Interface) []net.IP { + addrs, err := iface.Addrs() + if err != nil { + return nil + } + + var res []net.IP + for _, a := range addrs { + ipnet, ok := a.(*net.IPNet) + if !ok { + continue + } + if ipnet.IP.To4() == nil { + res = append(res, ipnet.IP) + } + } + return res +} + +// Start - start server +func (s *v6Server) Start() error { + if !s.conf.Enabled { + return nil + } + + iface, err := net.InterfaceByName(s.conf.InterfaceName) + if err != nil { + return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName) + } + + log.Debug("DHCPv6: starting...") + s.conf.dnsIPAddrs = getIfaceIPv6(*iface) + if len(s.conf.dnsIPAddrs) == 0 { + log.Debug("DHCPv6: no IPv6 address for interface %s", iface.Name) + return nil + } + + if len(iface.HardwareAddr) != 6 { + return fmt.Errorf("DHCPv6: invalid MAC %s", iface.HardwareAddr) + } + s.sid = dhcpv6.Duid{ + Type: dhcpv6.DUID_LLT, + HwType: iana.HWTypeEthernet, + LinkLayerAddr: iface.HardwareAddr, + Time: dhcpv6.GetTime(), + } + + laddr := &net.UDPAddr{ + IP: net.ParseIP("::"), + Port: dhcpv6.DefaultServerPort, + } + s.srv, err = server6.NewServer(iface.Name, laddr, s.packetHandler, server6.WithDebugLogger()) + if err != nil { + return err + } + + go func() { + err = s.srv.Serve() + log.Debug("DHCPv6: srv.Serve: %s", err) + }() + return nil +} + +// Stop - stop server +func (s *v6Server) Stop() { + if s.srv == nil { + return + } + + log.Debug("DHCPv6: stopping") + err := s.srv.Close() + if err != nil { + log.Error("DHCPv6: srv.Close: %s", err) + } + // now server.Serve() will return + s.srv = nil +} + +// Create DHCPv6 server +func v6Create(conf V6ServerConf) (DHCPServer, error) { + s := &v6Server{} + s.conf = conf + + if !conf.Enabled { + return s, nil + } + + s.conf.ipStart = net.ParseIP(conf.RangeStart) + if s.conf.ipStart == nil { + return s, fmt.Errorf("DHCPv6: invalid range-start IP: %s", conf.RangeStart) + } + + if conf.LeaseDuration == 0 { + s.conf.leaseTime = time.Hour * 24 + s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds()) + } else { + s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration) + } + + return s, nil +} diff --git a/dhcpd/v6_test.go b/dhcpd/v6_test.go new file mode 100644 index 00000000..7d7dd678 --- /dev/null +++ b/dhcpd/v6_test.go @@ -0,0 +1,225 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package dhcpd + +import ( + "net" + "testing" + + "github.com/insomniacslk/dhcp/dhcpv6" + "github.com/insomniacslk/dhcp/iana" + "github.com/stretchr/testify/assert" +) + +func notify6(flags uint32) { +} + +func TestV6StaticLeaseAddRemove(t *testing.T) { + conf := V6ServerConf{ + Enabled: true, + RangeStart: "2001::1", + notify: notify6, + } + s, err := v6Create(conf) + assert.True(t, err == nil) + + ls := s.GetLeases(LeasesStatic) + assert.Equal(t, 0, len(ls)) + + // add static lease + l := Lease{} + l.IP = net.ParseIP("2001::1") + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + assert.True(t, s.AddStaticLease(l) == nil) + + // try to add static lease - fail + assert.True(t, s.AddStaticLease(l) != nil) + + // check + ls = s.GetLeases(LeasesStatic) + assert.Equal(t, 1, len(ls)) + assert.Equal(t, "2001::1", ls[0].IP.String()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) + assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic) + + // try to remove static lease - fail + l.IP = net.ParseIP("2001::2") + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + assert.True(t, s.RemoveStaticLease(l) != nil) + + // remove static lease + l.IP = net.ParseIP("2001::1") + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + assert.True(t, s.RemoveStaticLease(l) == nil) + + // check + ls = s.GetLeases(LeasesStatic) + assert.Equal(t, 0, len(ls)) +} + +func TestV6StaticLeaseAddReplaceDynamic(t *testing.T) { + conf := V6ServerConf{ + Enabled: true, + RangeStart: "2001::1", + notify: notify6, + } + sIface, err := v6Create(conf) + s := sIface.(*v6Server) + assert.True(t, err == nil) + + // add dynamic lease + ld := Lease{} + ld.IP = net.ParseIP("2001::1") + ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa") + s.addLease(&ld) + + // add dynamic lease + { + ld := Lease{} + ld.IP = net.ParseIP("2001::2") + ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa") + s.addLease(&ld) + } + + // add static lease with the same IP + l := Lease{} + l.IP = net.ParseIP("2001::1") + l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa") + assert.True(t, s.AddStaticLease(l) == nil) + + // add static lease with the same MAC + l = Lease{} + l.IP = net.ParseIP("2001::3") + l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa") + assert.True(t, s.AddStaticLease(l) == nil) + + // check + ls := s.GetLeases(LeasesStatic) + assert.Equal(t, 2, len(ls)) + + assert.Equal(t, "2001::1", ls[0].IP.String()) + assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) + assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic) + + assert.Equal(t, "2001::3", ls[1].IP.String()) + assert.Equal(t, "22:aa:aa:aa:aa:aa", ls[1].HWAddr.String()) + assert.True(t, ls[1].Expiry.Unix() == leaseExpireStatic) +} + +func TestV6GetLease(t *testing.T) { + conf := V6ServerConf{ + Enabled: true, + RangeStart: "2001::1", + notify: notify6, + } + sIface, err := v6Create(conf) + s := sIface.(*v6Server) + assert.True(t, err == nil) + s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")} + s.sid = dhcpv6.Duid{ + Type: dhcpv6.DUID_LLT, + HwType: iana.HWTypeEthernet, + } + s.sid.LinkLayerAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + + l := Lease{} + l.IP = net.ParseIP("2001::1") + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + assert.True(t, s.AddStaticLease(l) == nil) + + // "Solicit" + mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa") + req, _ := dhcpv6.NewSolicit(mac) + msg, _ := req.GetInnerMessage() + resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg) + assert.True(t, s.process(msg, req, resp)) + resp.AddOption(dhcpv6.OptServerID(s.sid)) + + // check "Advertise" + assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type()) + oia := resp.Options.OneIANA() + oiaAddr := oia.Options.OneAddress() + assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String()) + assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds()) + + // "Request" + req, _ = dhcpv6.NewRequestFromAdvertise(resp) + msg, _ = req.GetInnerMessage() + resp, _ = dhcpv6.NewReplyFromMessage(msg) + assert.True(t, s.process(msg, req, resp)) + + // check "Reply" + assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type()) + oia = resp.Options.OneIANA() + oiaAddr = oia.Options.OneAddress() + assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String()) + assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds()) + + dnsAddrs := resp.Options.DNS() + assert.Equal(t, 1, len(dnsAddrs)) + assert.Equal(t, "2000::1", dnsAddrs[0].String()) + + // check lease + ls := s.GetLeases(LeasesStatic) + assert.Equal(t, 1, len(ls)) + assert.Equal(t, "2001::1", ls[0].IP.String()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) +} + +func TestV6GetDynamicLease(t *testing.T) { + conf := V6ServerConf{ + Enabled: true, + RangeStart: "2001::2", + notify: notify6, + } + sIface, err := v6Create(conf) + s := sIface.(*v6Server) + assert.True(t, err == nil) + s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")} + s.sid = dhcpv6.Duid{ + Type: dhcpv6.DUID_LLT, + HwType: iana.HWTypeEthernet, + } + s.sid.LinkLayerAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + + // "Solicit" + mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa") + req, _ := dhcpv6.NewSolicit(mac) + msg, _ := req.GetInnerMessage() + resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg) + assert.True(t, s.process(msg, req, resp)) + resp.AddOption(dhcpv6.OptServerID(s.sid)) + + // check "Advertise" + assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type()) + oia := resp.Options.OneIANA() + oiaAddr := oia.Options.OneAddress() + assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String()) + + // "Request" + req, _ = dhcpv6.NewRequestFromAdvertise(resp) + msg, _ = req.GetInnerMessage() + resp, _ = dhcpv6.NewReplyFromMessage(msg) + assert.True(t, s.process(msg, req, resp)) + + // check "Reply" + assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type()) + oia = resp.Options.OneIANA() + oiaAddr = oia.Options.OneAddress() + assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String()) + + dnsAddrs := resp.Options.DNS() + assert.Equal(t, 1, len(dnsAddrs)) + assert.Equal(t, "2000::1", dnsAddrs[0].String()) + + // check lease + ls := s.GetLeases(LeasesDynamic) + assert.Equal(t, 1, len(ls)) + assert.Equal(t, "2001::2", ls[0].IP.String()) + assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String()) + + assert.True(t, !ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::1"))) + assert.True(t, !ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2002::2"))) + assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::2"))) + assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::3"))) +} diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go index e73824b0..8d5ec767 100644 --- a/dnsforward/dnsforward.go +++ b/dnsforward/dnsforward.go @@ -44,10 +44,10 @@ var webRegistered bool // // The zero Server is empty and ready for use. type Server struct { - dnsProxy *proxy.Proxy // DNS proxy instance - dnsFilter *dnsfilter.Dnsfilter // DNS filter instance - dhcpServer *dhcpd.Server // DHCP server instance (optional) - queryLog querylog.QueryLog // Query log instance + dnsProxy *proxy.Proxy // DNS proxy instance + dnsFilter *dnsfilter.Dnsfilter // DNS filter instance + dhcpServer dhcpd.ServerInterface // DHCP server instance (optional) + queryLog querylog.QueryLog // Query log instance stats stats.Stats access *accessCtx @@ -72,7 +72,7 @@ type DNSCreateParams struct { DNSFilter *dnsfilter.Dnsfilter Stats stats.Stats QueryLog querylog.QueryLog - DHCPServer *dhcpd.Server + DHCPServer dhcpd.ServerInterface } // NewServer creates a new instance of the dnsforward.Server diff --git a/dnsforward/dnsforward_test.go b/dnsforward/dnsforward_test.go index 051178cd..a3eb94f1 100644 --- a/dnsforward/dnsforward_test.go +++ b/dnsforward/dnsforward_test.go @@ -1018,9 +1018,22 @@ func TestMatchDNSName(t *testing.T) { assert.True(t, !matchDNSName(dnsNames, "*.host2")) } +type testDHCP struct { +} + +func (d *testDHCP) Leases(flags int) []dhcpd.Lease { + l := dhcpd.Lease{} + l.IP = net.ParseIP("127.0.0.1").To4() + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + l.Hostname = "localhost" + return []dhcpd.Lease{l} +} +func (d *testDHCP) SetOnLeaseChanged(onLeaseChanged dhcpd.OnLeaseChangedT) { + return +} + func TestPTRResponse(t *testing.T) { - dhcp := &dhcpd.Server{} - dhcp.IPpool = make(map[[4]byte]net.HardwareAddr) + dhcp := &testDHCP{} c := dnsfilter.Config{} f := dnsfilter.New(&c, nil) @@ -1033,12 +1046,6 @@ func TestPTRResponse(t *testing.T) { assert.True(t, err == nil) assert.Nil(t, s.Start()) - l := dhcpd.Lease{} - l.IP = net.ParseIP("127.0.0.1").To4() - l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") - l.Hostname = "localhost" - dhcp.AddStaticLease(l) - addr := s.dnsProxy.Addr(proxy.ProtoUDP) req := createTestMessage("1.0.0.127.in-addr.arpa.") req.Question[0].Qtype = dns.TypePTR diff --git a/go.mod b/go.mod index a9f5a605..fc774a4f 100644 --- a/go.mod +++ b/go.mod @@ -9,13 +9,17 @@ require ( github.com/NYTimes/gziphandler v1.1.1 github.com/fsnotify/fsnotify v1.4.7 github.com/gobuffalo/packr v1.30.1 + github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 // indirect + github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7 github.com/joomcode/errorx v1.0.1 github.com/kardianos/service v1.1.0 - github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 + github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect + github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect github.com/miekg/dns v1.1.29 github.com/pkg/errors v0.9.1 github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c github.com/stretchr/testify v1.5.1 + github.com/u-root/u-root v6.0.0+incompatible // indirect go.etcd.io/bbolt v1.3.4 golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e diff --git a/go.sum b/go.sum index 4271f61e..2df1d656 100644 --- a/go.sum +++ b/go.sum @@ -49,9 +49,18 @@ github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wK github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= github.com/gobuffalo/packr/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7 h1:iaCm+9nZdYb8XCSU2TfIb0qYTcAlIv2XzyKR2d2xZ38= +github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7/go.mod h1:CfMdguCK66I5DAUJgGKyNz8aB6vO5dZzkm9Xep6WGvw= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= @@ -69,9 +78,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -104,11 +116,14 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/u-root/u-root v6.0.0+incompatible h1:YqPGmRoRyYmeg17KIWFRSyVq6LX5T6GSzawyA6wG6EE= +github.com/u-root/u-root v6.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= @@ -123,6 +138,9 @@ golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= @@ -134,8 +152,10 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/home/clients.go b/home/clients.go index 369be2bc..1e6ae413 100644 --- a/home/clients.go +++ b/home/clients.go @@ -86,6 +86,7 @@ type clientsContainer struct { } // Init initializes clients container +// dhcpServer: optional // Note: this function must be called only once func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd.Server, autoHosts *util.AutoHosts) { if clients.list != nil { @@ -106,7 +107,9 @@ func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd. if !clients.testing { clients.addFromDHCP() - clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged) + if clients.dhcpServer != nil { + clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged) + } clients.autoHosts.SetOnChanged(clients.onHostsChanged) } } diff --git a/home/config.go b/home/config.go index 42e08cc2..d0fc6396 100644 --- a/home/config.go +++ b/home/config.go @@ -127,10 +127,6 @@ var config = configuration{ PortHTTPS: 443, PortDNSOverTLS: 853, // needs to be passed through to dnsproxy }, - DHCP: dhcpd.ServerConfig{ - LeaseDuration: 86400, - ICMPTimeout: 1000, - }, logSettings: logSettings{ LogCompress: false, LogLocalTime: false, @@ -156,6 +152,10 @@ func initConfig() { config.DNS.DnsfilterConf.ParentalCacheSize = 1 * 1024 * 1024 config.DNS.DnsfilterConf.CacheTime = 30 config.Filters = defaultFilters() + + config.DHCP.Conf4.LeaseDuration = 86400 + config.DHCP.Conf4.ICMPTimeout = 1000 + config.DHCP.Conf6.LeaseDuration = 86400 } // getConfigFilename returns path to the current config file diff --git a/home/control.go b/home/control.go index 744fc327..e8b2fa8a 100644 --- a/home/control.go +++ b/home/control.go @@ -56,6 +56,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { "protection_enabled": c.ProtectionEnabled, } + data["dhcp_available"] = (Context.dhcpServer != nil) jsonVal, err := json.Marshal(data) if err != nil { diff --git a/home/dhcp.go b/home/dhcp.go deleted file mode 100644 index 799333b0..00000000 --- a/home/dhcp.go +++ /dev/null @@ -1,36 +0,0 @@ -package home - -import ( - "github.com/joomcode/errorx" -) - -func startDHCPServer() error { - if !config.DHCP.Enabled { - // not enabled, don't do anything - return nil - } - - err := Context.dhcpServer.Init(config.DHCP) - if err != nil { - return errorx.Decorate(err, "Couldn't init DHCP server") - } - - err = Context.dhcpServer.Start() - if err != nil { - return errorx.Decorate(err, "Couldn't start DHCP server") - } - return nil -} - -func stopDHCPServer() error { - if !config.DHCP.Enabled { - return nil - } - - err := Context.dhcpServer.Stop() - if err != nil { - return errorx.Decorate(err, "Couldn't stop DHCP server") - } - - return nil -} diff --git a/home/home.go b/home/home.go index e297173c..4482b6b2 100644 --- a/home/home.go +++ b/home/home.go @@ -219,10 +219,8 @@ func run(args options) { config.DHCP.WorkDir = Context.workDir config.DHCP.HTTPRegister = httpRegister config.DHCP.ConfigModified = onConfigModified - Context.dhcpServer = dhcpd.Create(config.DHCP) - if Context.dhcpServer == nil { - log.Error("Failed to initialize DHCP server, exiting") - os.Exit(1) + if runtime.GOOS != "windows" { + Context.dhcpServer = dhcpd.Create(config.DHCP) } Context.autoHosts.Init("") @@ -317,9 +315,8 @@ func run(args options) { } }() - err = startDHCPServer() - if err != nil { - log.Fatal(err) + if Context.dhcpServer != nil { + _ = Context.dhcpServer.Start() } } @@ -505,9 +502,9 @@ func cleanup() { if err != nil { log.Error("Couldn't stop DNS server: %s", err) } - err = stopDHCPServer() - if err != nil { - log.Error("Couldn't stop DHCP server: %s", err) + + if Context.dhcpServer != nil { + Context.dhcpServer.Stop() } Context.autoHosts.Close() diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 22579caa..7b29f96c 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -348,8 +348,6 @@ paths: application/json: schema: $ref: "#/components/schemas/DhcpConfig" - description: DHCP configuration JSON - required: true responses: "200": description: OK @@ -965,6 +963,8 @@ components: maximum: 65535 protection_enabled: type: boolean + dhcp_available: + type: boolean querylog_enabled: type: boolean running: @@ -999,6 +999,8 @@ components: - tls://1.0.0.1 protection_enabled: type: boolean + dhcp_available: + type: boolean ratelimit: type: integer blocking_mode: @@ -1259,17 +1261,18 @@ components: description: Time period to keep data (1 | 7 | 30 | 90) DhcpConfig: type: object - description: Built-in DHCP server configuration - required: - - enabled - - gateway_ip - - subnet_mask - - range_start - - range_end - - lease_duration properties: enabled: type: boolean + interface_name: + type: string + v4: + $ref: "#/components/schemas/DhcpConfigV4" + v6: + $ref: "#/components/schemas/DhcpConfigV6" + DhcpConfigV4: + type: object + properties: gateway_ip: type: string example: 192.168.1.1 @@ -1283,8 +1286,14 @@ components: type: string example: 192.168.10.50 lease_duration: + type: integer + DhcpConfigV6: + type: object + properties: + range_start: type: string - example: 12h + lease_duration: + type: integer DhcpLease: type: object description: DHCP lease information @@ -1305,8 +1314,7 @@ components: example: dell expires: type: string - format: date-time - example: 2017-07-21T17:32:28Z + example: "2017-07-21T17:32:28Z" DhcpStaticLease: type: object description: DHCP static lease information @@ -1332,8 +1340,14 @@ components: - config - leases properties: - config: - $ref: "#/components/schemas/DhcpConfig" + enabled: + type: boolean + interface_name: + type: string + v4: + $ref: "#/components/schemas/DhcpConfigV4" + v6: + $ref: "#/components/schemas/DhcpConfigV6" leases: type: array items: From 89c3926ba502e7e91afd001f4fbc1ec16b40cf67 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 4 Aug 2020 14:18:35 +0300 Subject: [PATCH 02/12] * dhcp: fail on startup if couldn't initialize DHCP module --- dhcpd/dhcpd.go | 5 +++++ home/home.go | 3 +++ 2 files changed, 8 insertions(+) diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 11937eb1..f6e19d5a 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -119,6 +119,11 @@ func Create(config ServerConfig) *Server { return nil } + if s.conf.Enabled && !v4conf.Enabled && !v6conf.Enabled { + log.Error("Can't enable DHCP server because neither DHCPv4 nor DHCPv6 servers are configured") + return nil + } + // we can't delay database loading until DHCP server is started, // because we need static leases functionality available beforehand s.dbLoad() diff --git a/home/home.go b/home/home.go index 4482b6b2..b9d7a760 100644 --- a/home/home.go +++ b/home/home.go @@ -221,6 +221,9 @@ func run(args options) { config.DHCP.ConfigModified = onConfigModified if runtime.GOOS != "windows" { Context.dhcpServer = dhcpd.Create(config.DHCP) + if Context.dhcpServer == nil { + log.Fatalf("Can't initialize DHCP module") + } } Context.autoHosts.Init("") From ec24d18f83a032446116428cb3fc2acb390b8bd2 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Fri, 7 Aug 2020 11:47:24 +0300 Subject: [PATCH 03/12] * GET /control/dhcp/interfaces: split IPv4 and IPv6 addresses --- AGHTechDoc.md | 3 ++- dhcpd/dhcp_http.go | 11 ++++++++--- openapi/openapi.yaml | 8 +++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 809b0c72..76e1171c 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -444,7 +444,8 @@ Response: "name":"iface_name", "mtu":1500, "hardware_address":"...", - "ip_addresses":["ipv4 addr","ipv6 addr", ...], + "ipv4_addresses":["ipv4 addr", ...], + "ipv6_addresses":["ipv6 addr", ...], "flags":"up|broadcast|multicast" } ... diff --git a/dhcpd/dhcp_http.go b/dhcpd/dhcp_http.go index 7ae6075a..f85c00b4 100644 --- a/dhcpd/dhcp_http.go +++ b/dhcpd/dhcp_http.go @@ -202,7 +202,8 @@ type netInterfaceJSON struct { Name string `json:"name"` MTU int `json:"mtu"` HardwareAddr string `json:"hardware_address"` - Addresses []string `json:"ip_addresses"` + Addrs4 []string `json:"ipv4_addresses"` + Addrs6 []string `json:"ipv6_addresses"` Flags string `json:"flags"` } @@ -251,9 +252,13 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { if ipnet.IP.IsLinkLocalUnicast() { continue } - jsonIface.Addresses = append(jsonIface.Addresses, ipnet.IP.String()) + if ipnet.IP.To4() != nil { + jsonIface.Addrs4 = append(jsonIface.Addrs4, ipnet.IP.String()) + } else { + jsonIface.Addrs6 = append(jsonIface.Addrs6, ipnet.IP.String()) + } } - if len(jsonIface.Addresses) != 0 { + if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 { response[iface.Name] = jsonIface } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 7b29f96c..ef288f04 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1626,12 +1626,14 @@ components: name: type: string example: eth0 - ip_addresses: + ipv4_addresses: + type: array + items: + type: string + ipv6_addresses: type: array items: type: string - example: - - 127.0.0.1 AddressInfo: type: object description: Port information From 6090400ea00f2ea4b97cac780ad54c271e466376 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Mon, 10 Aug 2020 12:43:38 +0300 Subject: [PATCH 04/12] * GET /control/dhcp/interfaces: remove 'mtu'; add 'gateway_ip' --- AGHTechDoc.md | 2 +- dhcpd/dhcp_http.go | 5 ++--- openapi/openapi.yaml | 6 ++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 76e1171c..8661e39c 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -442,10 +442,10 @@ Response: { "iface_name":{ "name":"iface_name", - "mtu":1500, "hardware_address":"...", "ipv4_addresses":["ipv4 addr", ...], "ipv6_addresses":["ipv6 addr", ...], + "gateway_ip":"...", "flags":"up|broadcast|multicast" } ... diff --git a/dhcpd/dhcp_http.go b/dhcpd/dhcp_http.go index f85c00b4..34e833ec 100644 --- a/dhcpd/dhcp_http.go +++ b/dhcpd/dhcp_http.go @@ -200,7 +200,7 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { type netInterfaceJSON struct { Name string `json:"name"` - MTU int `json:"mtu"` + GatewayIP string `json:"gateway_ip"` HardwareAddr string `json:"hardware_address"` Addrs4 []string `json:"ipv4_addresses"` Addrs6 []string `json:"ipv6_addresses"` @@ -233,7 +233,6 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { jsonIface := netInterfaceJSON{ Name: iface.Name, - MTU: iface.MTU, HardwareAddr: iface.HardwareAddr.String(), } @@ -259,9 +258,9 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { } } if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 { + jsonIface.GatewayIP = getGatewayIP(iface.Name) response[iface.Name] = jsonIface } - } err = json.NewEncoder(w).Encode(response) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index ef288f04..bd56fe7c 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1619,10 +1619,6 @@ components: hardware_address: type: string example: 52:54:00:11:09:ba - mtu: - type: integer - format: int32 - example: 1500 name: type: string example: eth0 @@ -1634,6 +1630,8 @@ components: type: array items: type: string + gateway_ip: + type: string AddressInfo: type: object description: Port information From 23752377b76895dbcd78f25ed6df58e1bbcae153 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Wed, 12 Aug 2020 18:13:07 +0300 Subject: [PATCH 05/12] - dhcp: CheckIfOtherDHCPServersPresent: fix Sometimes request from DHCP server couldn't be received because we were bound to a specific IP address. --- dhcpd/check_other_dhcp.go | 15 ++++++++----- dhcpd/os_linux.go | 45 --------------------------------------- dhcpd/os_unix.go | 44 -------------------------------------- 3 files changed, 10 insertions(+), 94 deletions(-) delete mode 100644 dhcpd/os_linux.go delete mode 100644 dhcpd/os_unix.go diff --git a/dhcpd/check_other_dhcp.go b/dhcpd/check_other_dhcp.go index 83c9ca1b..099673f0 100644 --- a/dhcpd/check_other_dhcp.go +++ b/dhcpd/check_other_dhcp.go @@ -7,12 +7,13 @@ import ( "fmt" "net" "os" + "runtime" "time" "github.com/AdguardTeam/golibs/log" "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv4/nclient4" "github.com/insomniacslk/dhcp/iana" - "golang.org/x/net/ipv4" ) // CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface, @@ -29,6 +30,10 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { return false, fmt.Errorf("couldn't find IPv4 address of interface %s %+v", ifaceName, iface) } + if runtime.GOOS == "darwin" { + return false, fmt.Errorf("Can't find DHCP server: not supported on macOS") + } + srcIP := ifaceIPNet[0] src := net.JoinHostPort(srcIP.String(), "68") dst := "255.255.255.255:67" @@ -60,7 +65,7 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { // bind to 0.0.0.0:68 log.Tracef("Listening to udp4 %+v", udpAddr) - c, err := newBroadcastPacketConn(net.IPv4(0, 0, 0, 0), 68, ifaceName) + c, err := nclient4.NewRawUDPConn(ifaceName, 68) if c != nil { defer c.Close() } @@ -69,8 +74,7 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { } // send to 255.255.255.255:67 - cm := ipv4.ControlMessage{} - _, err = c.WriteTo(req.ToBytes(), &cm, dstAddr) + _, err = c.WriteTo(req.ToBytes(), dstAddr) if err != nil { return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst) } @@ -81,9 +85,10 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { // 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) + n, _, err := c.ReadFrom(b) if isTimeout(err) { // timed out -- no DHCP servers + log.Debug("DHCPv4: didn't receive DHCP response") return false, nil } if err != nil { diff --git a/dhcpd/os_linux.go b/dhcpd/os_linux.go deleted file mode 100644 index 5cbcfece..00000000 --- a/dhcpd/os_linux.go +++ /dev/null @@ -1,45 +0,0 @@ -package dhcpd - -import ( - "net" - "os" - "syscall" - - "golang.org/x/net/ipv4" -) - -// Create a socket for receiving broadcast packets -func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) { - s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) - if err != nil { - return nil, err - } - - if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1); err != nil { - return nil, err - } - if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { - return nil, err - } - if err := syscall.SetsockoptString(s, syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, ifname); err != nil { - return nil, err - } - - addr := syscall.SockaddrInet4{Port: port} - copy(addr.Addr[:], bindAddr.To4()) - err = syscall.Bind(s, &addr) - if err != nil { - syscall.Close(s) - return nil, err - } - - f := os.NewFile(uintptr(s), "") - c, err := net.FilePacketConn(f) - f.Close() - if err != nil { - return nil, err - } - - p := ipv4.NewPacketConn(c) - return p, nil -} diff --git a/dhcpd/os_unix.go b/dhcpd/os_unix.go deleted file mode 100644 index 11592d33..00000000 --- a/dhcpd/os_unix.go +++ /dev/null @@ -1,44 +0,0 @@ -// +build aix darwin dragonfly freebsd netbsd openbsd solaris - -package dhcpd - -import ( - "net" - "os" - "syscall" - - "golang.org/x/net/ipv4" -) - -// Create a socket for receiving broadcast packets -func newBroadcastPacketConn(bindAddr net.IP, port int, ifname string) (*ipv4.PacketConn, error) { - s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) - if err != nil { - return nil, err - } - - if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1); err != nil { - return nil, err - } - if err := syscall.SetsockoptInt(s, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { - return nil, err - } - - addr := syscall.SockaddrInet4{Port: port} - copy(addr.Addr[:], bindAddr.To4()) - err = syscall.Bind(s, &addr) - if err != nil { - syscall.Close(s) - return nil, err - } - - f := os.NewFile(uintptr(s), "") - c, err := net.FilePacketConn(f) - f.Close() - if err != nil { - return nil, err - } - - p := ipv4.NewPacketConn(c) - return p, nil -} From e7bef3a4480ea0cd6d4d6d612ca3f0e3d737a222 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Thu, 13 Aug 2020 18:45:27 +0300 Subject: [PATCH 06/12] * POST /control/dhcp/find_active_dhcp: add dhcpv6 server info --- AGHTechDoc.md | 22 +++-- dhcpd/check_other_dhcp.go | 104 +++++++++++++++++++++-- dhcpd/check_other_dhcp_windows.go | 6 +- dhcpd/dhcp_http.go | 132 ++++++++++++++++++++---------- openapi/openapi.yaml | 20 ++++- 5 files changed, 225 insertions(+), 59 deletions(-) diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 8661e39c..99bc6a8c 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -500,13 +500,21 @@ Response: 200 OK { - "other_server": { - "found": "yes|no|error", - "error": "Error message", // set if found=error - }, - "static_ip": { - "static": "yes|no|error", - "ip": "", // set if static=no + v4: { + "other_server": { + "found": "yes|no|error", + "error": "Error message", // set if found=error + }, + "static_ip": { + "static": "yes|no|error", + "ip": "", // set if static=no + } + } + v6: { + "other_server": { + "found": "yes|no|error", + "error": "Error message", // set if found=error + }, } } diff --git a/dhcpd/check_other_dhcp.go b/dhcpd/check_other_dhcp.go index 099673f0..d8c8aa33 100644 --- a/dhcpd/check_other_dhcp.go +++ b/dhcpd/check_other_dhcp.go @@ -13,12 +13,14 @@ import ( "github.com/AdguardTeam/golibs/log" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" + "github.com/insomniacslk/dhcp/dhcpv6" + "github.com/insomniacslk/dhcp/dhcpv6/nclient6" "github.com/insomniacslk/dhcp/iana" ) -// CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface, +// CheckIfOtherDHCPServersPresentV4 sends a DHCP request to the specified network interface, // and waits for a response for a period defined by defaultDiscoverTime -func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { +func CheckIfOtherDHCPServersPresentV4(ifaceName string) (bool, error) { iface, err := net.InterfaceByName(ifaceName) if err != nil { return false, wrapErrPrint(err, "Couldn't find interface by name %s", ifaceName) @@ -66,12 +68,12 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { // bind to 0.0.0.0:68 log.Tracef("Listening to udp4 %+v", udpAddr) c, err := nclient4.NewRawUDPConn(ifaceName, 68) - if c != nil { - defer c.Close() - } if err != nil { return false, wrapErrPrint(err, "Couldn't listen on :68") } + if c != nil { + defer c.Close() + } // send to 255.255.255.255:67 _, err = c.WriteTo(req.ToBytes(), dstAddr) @@ -119,3 +121,95 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { return true, nil } } + +// CheckIfOtherDHCPServersPresentV6 sends a DHCP request to the specified network interface, +// and waits for a response for a period defined by defaultDiscoverTime +func CheckIfOtherDHCPServersPresentV6(ifaceName string) (bool, error) { + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return false, fmt.Errorf("DHCPv6: net.InterfaceByName: %s: %s", ifaceName, err) + } + + ifaceIPNet := getIfaceIPv6(*iface) + if len(ifaceIPNet) == 0 { + return false, fmt.Errorf("DHCPv6: couldn't find IPv6 address of interface %s %+v", ifaceName, iface) + } + + srcIP := ifaceIPNet[0] + src := net.JoinHostPort(srcIP.String(), "546") + dst := "[ff02::1:2]:547" + + req, err := dhcpv6.NewSolicit(iface.HardwareAddr) + if err != nil { + return false, fmt.Errorf("DHCPv6: dhcpv6.NewSolicit: %s", err) + } + + udpAddr, err := net.ResolveUDPAddr("udp6", src) + if err != nil { + return false, wrapErrPrint(err, "DHCPv6: Couldn't resolve UDP address %s", src) + } + + if !udpAddr.IP.To16().Equal(srcIP) { + return false, wrapErrPrint(err, "DHCPv6: Resolved UDP address is not %s", src) + } + + dstAddr, err := net.ResolveUDPAddr("udp6", dst) + if err != nil { + return false, fmt.Errorf("DHCPv6: Couldn't resolve UDP address %s: %s", dst, err) + } + + log.Debug("DHCPv6: Listening to udp6 %+v", udpAddr) + c, err := nclient6.NewIPv6UDPConn(ifaceName, dhcpv6.DefaultClientPort) + if err != nil { + return false, fmt.Errorf("DHCPv6: Couldn't listen on :546: %s", err) + } + if c != nil { + defer c.Close() + } + + _, err = c.WriteTo(req.ToBytes(), dstAddr) + if err != nil { + return false, fmt.Errorf("DHCPv6: Couldn't send a packet to %s: %s", dst, err) + } + + for { + log.Debug("DHCPv6: Waiting %v for an answer", defaultDiscoverTime) + b := make([]byte, 4096) + _ = c.SetReadDeadline(time.Now().Add(defaultDiscoverTime)) + n, _, err := c.ReadFrom(b) + if isTimeout(err) { + log.Debug("DHCPv6: didn't receive DHCP response") + return false, nil + } + if err != nil { + return false, wrapErrPrint(err, "Couldn't receive packet") + } + + log.Debug("DHCPv6: Received packet (%v bytes)", n) + + resp, err := dhcpv6.FromBytes(b[:n]) + if err != nil { + log.Debug("DHCPv6: dhcpv6.FromBytes: %s", err) + continue + } + + log.Debug("DHCPv6: received message from server: %s", resp.Summary()) + + cid := req.Options.ClientID() + msg, err := resp.GetInnerMessage() + if err != nil { + log.Debug("DHCPv6: resp.GetInnerMessage: %s", err) + continue + } + rcid := msg.Options.ClientID() + if resp.Type() == dhcpv6.MessageTypeAdvertise && + msg.TransactionID == req.TransactionID && + rcid != nil && + cid.Equal(*rcid) { + log.Debug("DHCPv6: The packet is from an active DHCP server") + return true, nil + } + + log.Debug("DHCPv6: received message from server doesn't match our request") + } +} diff --git a/dhcpd/check_other_dhcp_windows.go b/dhcpd/check_other_dhcp_windows.go index bd3849b8..49d90f25 100644 --- a/dhcpd/check_other_dhcp_windows.go +++ b/dhcpd/check_other_dhcp_windows.go @@ -2,6 +2,10 @@ package dhcpd import "fmt" -func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { +func CheckIfOtherDHCPServersPresentV4(ifaceName string) (bool, error) { + return false, fmt.Errorf("not supported") +} + +func CheckIfOtherDHCPServersPresentV6(ifaceName string) (bool, error) { return false, fmt.Errorf("not supported") } diff --git a/dhcpd/dhcp_http.go b/dhcpd/dhcp_http.go index 34e833ec..7f2e8b8a 100644 --- a/dhcpd/dhcp_http.go +++ b/dhcpd/dhcp_http.go @@ -12,6 +12,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/util" + "github.com/AdguardTeam/golibs/jsonutil" "github.com/AdguardTeam/golibs/log" ) @@ -129,58 +130,82 @@ type dhcpServerConfigJSON struct { func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { newconfig := dhcpServerConfigJSON{} - err := json.NewDecoder(r.Body).Decode(&newconfig) + newconfig.Enabled = s.conf.Enabled + newconfig.InterfaceName = s.conf.InterfaceName + + js, err := jsonutil.DecodeObject(&newconfig, r.Body) if err != nil { httpError(r, w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err) return } - v4conf := v4JSONToServerConf(newconfig.V4) - v4conf.Enabled = newconfig.Enabled - if len(v4conf.RangeStart) == 0 { - v4conf.Enabled = false - } - v4conf.InterfaceName = newconfig.InterfaceName + var s4 DHCPServer + var s6 DHCPServer + v4Enabled := false + v6Enabled := false - c4 := V4ServerConf{} - s.srv4.WriteDiskConfig4(&c4) - v4conf.notify = c4.notify - v4conf.ICMPTimeout = c4.ICMPTimeout + if js.Exists("v4") { + v4conf := v4JSONToServerConf(newconfig.V4) + v4conf.Enabled = newconfig.Enabled + if len(v4conf.RangeStart) == 0 { + v4conf.Enabled = false + } + v4Enabled = v4conf.Enabled + v4conf.InterfaceName = newconfig.InterfaceName - s4, err := v4Create(v4conf) - if err != nil { - httpError(r, w, http.StatusBadRequest, "Invalid DHCPv4 configuration: %s", err) - return + c4 := V4ServerConf{} + s.srv4.WriteDiskConfig4(&c4) + v4conf.notify = c4.notify + v4conf.ICMPTimeout = c4.ICMPTimeout + + s4, err = v4Create(v4conf) + if err != nil { + httpError(r, w, http.StatusBadRequest, "Invalid DHCPv4 configuration: %s", err) + return + } } - v6conf := v6JSONToServerConf(newconfig.V6) - v6conf.Enabled = newconfig.Enabled - if len(v6conf.RangeStart) == 0 { - v6conf.Enabled = false - } - v6conf.InterfaceName = newconfig.InterfaceName - v6conf.notify = s.onNotify - s6, err := v6Create(v6conf) - if s6 == nil { - httpError(r, w, http.StatusBadRequest, "Invalid DHCPv6 configuration: %s", err) - return + if js.Exists("v6") { + v6conf := v6JSONToServerConf(newconfig.V6) + v6conf.Enabled = newconfig.Enabled + if len(v6conf.RangeStart) == 0 { + v6conf.Enabled = false + } + v6Enabled = v6conf.Enabled + v6conf.InterfaceName = newconfig.InterfaceName + v6conf.notify = s.onNotify + s6, err = v6Create(v6conf) + if s6 == nil { + httpError(r, w, http.StatusBadRequest, "Invalid DHCPv6 configuration: %s", err) + return + } } - if newconfig.Enabled && !v4conf.Enabled && !v6conf.Enabled { + if newconfig.Enabled && !v4Enabled && !v6Enabled { httpError(r, w, http.StatusBadRequest, "DHCPv4 or DHCPv6 configuration must be complete") return } s.Stop() - s.conf.Enabled = newconfig.Enabled - s.conf.InterfaceName = newconfig.InterfaceName - s.srv4 = s4 - s.srv6 = s6 + if js.Exists("enabled") { + s.conf.Enabled = newconfig.Enabled + } + if js.Exists("interface_name") { + s.conf.InterfaceName = newconfig.InterfaceName + } + + if s4 != nil { + s.srv4 = s4 + } + if s6 != nil { + s.srv6 = s6 + } s.conf.ConfigModified() + s.dbLoad() - if newconfig.Enabled { + if s.conf.Enabled { staticIP, err := HasStaticIP(newconfig.InterfaceName) if !staticIP && err == nil { err = SetStaticIP(newconfig.InterfaceName) @@ -291,17 +316,7 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque return } - found, err := CheckIfOtherDHCPServersPresent(interfaceName) - - othSrv := map[string]interface{}{} - foundVal := "no" - if found { - foundVal = "yes" - } else if err != nil { - foundVal = "error" - othSrv["error"] = err.Error() - } - othSrv["found"] = foundVal + found4, err4 := CheckIfOtherDHCPServersPresentV4(interfaceName) staticIP := map[string]interface{}{} isStaticIP, err := HasStaticIP(interfaceName) @@ -315,9 +330,36 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque } staticIP["static"] = staticIPStatus + v4 := map[string]interface{}{} + othSrv := map[string]interface{}{} + foundVal := "no" + if found4 { + foundVal = "yes" + } else if err != nil { + foundVal = "error" + othSrv["error"] = err4.Error() + } + othSrv["found"] = foundVal + v4["other_server"] = othSrv + v4["static_ip"] = staticIP + + found6, err6 := CheckIfOtherDHCPServersPresentV6(interfaceName) + + v6 := map[string]interface{}{} + othSrv = map[string]interface{}{} + foundVal = "no" + if found6 { + foundVal = "yes" + } else if err6 != nil { + foundVal = "error" + othSrv["error"] = err6.Error() + } + othSrv["found"] = foundVal + v6["other_server"] = othSrv + result := map[string]interface{}{} - result["other_server"] = othSrv - result["static_ip"] = staticIP + result["v4"] = v4 + result["v6"] = v6 w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(result) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index bd56fe7c..57d63c70 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: AdGuard Home description: AdGuard Home REST API. Admin web interface is built on top of this REST API. - version: "0.102" + version: "0.104" contact: name: "AdGuard Home" url: "https://github.com/AdguardTeam/AdGuardHome" @@ -1356,14 +1356,30 @@ components: type: array items: $ref: "#/components/schemas/DhcpStaticLease" + DhcpSearchResult: type: object description: Information about a DHCP server discovered in the current network + properties: + v4: + $ref: "#/components/schemas/DhcpSearchV4" + v6: + $ref: "#/components/schemas/DhcpSearchV6" + + DhcpSearchV4: + type: object properties: other_server: $ref: "#/components/schemas/DhcpSearchResultOtherServer" static_ip: $ref: "#/components/schemas/DhcpSearchResultStaticIP" + + DhcpSearchV6: + type: object + properties: + other_server: + $ref: "#/components/schemas/DhcpSearchResultOtherServer" + DhcpSearchResultOtherServer: type: object properties: @@ -1375,6 +1391,7 @@ components: type: string description: Set if found=error example: "" + DhcpSearchResultStaticIP: type: object properties: @@ -1386,6 +1403,7 @@ components: type: string description: Set if static=no example: "" + DnsAnswer: type: object description: DNS answer section From dd46cd5f36ae31dfb4f31dcb4dec65921510db17 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Wed, 19 Aug 2020 18:35:54 +0300 Subject: [PATCH 07/12] * copy dhcpv4/nclient4 package with minor enhancement The original package can be built only on Linux. --- dhcpd/check_other_dhcp.go | 2 +- dhcpd/nclient4/client.go | 584 ++++++++++++++++++++++++++++++++++ dhcpd/nclient4/client_test.go | 333 +++++++++++++++++++ dhcpd/nclient4/conn_unix.go | 144 +++++++++ dhcpd/nclient4/ipv4.go | 376 ++++++++++++++++++++++ go.mod | 22 +- go.sum | 23 +- 7 files changed, 1471 insertions(+), 13 deletions(-) create mode 100644 dhcpd/nclient4/client.go create mode 100644 dhcpd/nclient4/client_test.go create mode 100644 dhcpd/nclient4/conn_unix.go create mode 100644 dhcpd/nclient4/ipv4.go diff --git a/dhcpd/check_other_dhcp.go b/dhcpd/check_other_dhcp.go index d8c8aa33..ebcfd363 100644 --- a/dhcpd/check_other_dhcp.go +++ b/dhcpd/check_other_dhcp.go @@ -10,9 +10,9 @@ import ( "runtime" "time" + "github.com/AdguardTeam/AdGuardHome/dhcpd/nclient4" "github.com/AdguardTeam/golibs/log" "github.com/insomniacslk/dhcp/dhcpv4" - "github.com/insomniacslk/dhcp/dhcpv4/nclient4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/nclient6" "github.com/insomniacslk/dhcp/iana" diff --git a/dhcpd/nclient4/client.go b/dhcpd/nclient4/client.go new file mode 100644 index 00000000..f2eabb5d --- /dev/null +++ b/dhcpd/nclient4/client.go @@ -0,0 +1,584 @@ +// Copyright 2018 the u-root Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.12 + +// Package nclient4 is a small, minimum-functionality client for DHCPv4. +// +// It only supports the 4-way DHCPv4 Discover-Offer-Request-Ack handshake as +// well as the Request-Ack renewal process. +// Originally from here: github.com/insomniacslk/dhcp/dhcpv4/nclient4 +// with the difference that this package can be built on UNIX (not just Linux), +// because github.com/mdlayher/raw package supports it. +package nclient4 + +import ( + "bytes" + "context" + "errors" + "fmt" + "log" + "net" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +const ( + defaultBufferCap = 5 + + // DefaultTimeout is the default value for read-timeout if option WithTimeout is not set + DefaultTimeout = 5 * time.Second + + // DefaultRetries is amount of retries will be done if no answer was received within read-timeout amount of time + DefaultRetries = 3 + + // MaxMessageSize is the value to be used for DHCP option "MaxMessageSize". + MaxMessageSize = 1500 + + // ClientPort is the port that DHCP clients listen on. + ClientPort = 68 + + // ServerPort is the port that DHCP servers and relay agents listen on. + ServerPort = 67 +) + +var ( + // DefaultServers is the address of all link-local DHCP servers and + // relay agents. + DefaultServers = &net.UDPAddr{ + IP: net.IPv4bcast, + Port: ServerPort, + } +) + +var ( + // ErrNoResponse is returned when no response packet is received. + ErrNoResponse = errors.New("no matching response packet received") + + // ErrNoConn is returned when NewWithConn is called with nil-value as conn. + ErrNoConn = errors.New("conn is nil") + + // ErrNoIfaceHWAddr is returned when NewWithConn is called with nil-value as ifaceHWAddr + ErrNoIfaceHWAddr = errors.New("ifaceHWAddr is nil") +) + +// pendingCh is a channel associated with a pending TransactionID. +type pendingCh struct { + // SendAndRead closes done to indicate that it wishes for no more + // messages for this particular XID. + done <-chan struct{} + + // ch is used by the receive loop to distribute DHCP messages. + ch chan<- *dhcpv4.DHCPv4 +} + +// Logger is a handler which will be used to output logging messages +type Logger interface { + // PrintMessage print _all_ DHCP messages + PrintMessage(prefix string, message *dhcpv4.DHCPv4) + + // Printf is use to print the rest debugging information + Printf(format string, v ...interface{}) +} + +// EmptyLogger prints nothing +type EmptyLogger struct{} + +// Printf is just a dummy function that does nothing +func (e EmptyLogger) Printf(format string, v ...interface{}) {} + +// PrintMessage is just a dummy function that does nothing +func (e EmptyLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) {} + +// Printfer is used for actual output of the logger. For example *log.Logger is a Printfer. +type Printfer interface { + // Printf is the function for logging output. Arguments are handled in the manner of fmt.Printf. + Printf(format string, v ...interface{}) +} + +// ShortSummaryLogger is a wrapper for Printfer to implement interface Logger. +// DHCP messages are printed in the short format. +type ShortSummaryLogger struct { + // Printfer is used for actual output of the logger + Printfer +} + +// Printf prints a log message as-is via predefined Printfer +func (s ShortSummaryLogger) Printf(format string, v ...interface{}) { + s.Printfer.Printf(format, v...) +} + +// PrintMessage prints a DHCP message in the short format via predefined Printfer +func (s ShortSummaryLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) { + s.Printf("%s: %s", prefix, message) +} + +// DebugLogger is a wrapper for Printfer to implement interface Logger. +// DHCP messages are printed in the long format. +type DebugLogger struct { + // Printfer is used for actual output of the logger + Printfer +} + +// Printf prints a log message as-is via predefined Printfer +func (d DebugLogger) Printf(format string, v ...interface{}) { + d.Printfer.Printf(format, v...) +} + +// PrintMessage prints a DHCP message in the long format via predefined Printfer +func (d DebugLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) { + d.Printf("%s: %s", prefix, message.Summary()) +} + +// Client is an IPv4 DHCP client. +type Client struct { + ifaceHWAddr net.HardwareAddr + conn net.PacketConn + timeout time.Duration + retry int + logger Logger + + // bufferCap is the channel capacity for each TransactionID. + bufferCap int + + // serverAddr is the UDP address to send all packets to. + // + // This may be an actual broadcast address, or a unicast address. + serverAddr *net.UDPAddr + + // closed is an atomic bool set to 1 when done is closed. + closed uint32 + + // done is closed to unblock the receive loop. + done chan struct{} + + // wg protects any spawned goroutines, namely the receiveLoop. + wg sync.WaitGroup + + pendingMu sync.Mutex + // pending stores the distribution channels for each pending + // TransactionID. receiveLoop uses this map to determine which channel + // to send a new DHCP message to. + pending map[dhcpv4.TransactionID]*pendingCh +} + +// New returns a client usable with an unconfigured interface. +func New(iface string, opts ...ClientOpt) (*Client, error) { + return new(iface, nil, nil, opts...) +} + +// NewWithConn creates a new DHCP client that sends and receives packets on the +// given interface. +func NewWithConn(conn net.PacketConn, ifaceHWAddr net.HardwareAddr, opts ...ClientOpt) (*Client, error) { + return new(``, conn, ifaceHWAddr, opts...) +} + +func new(iface string, conn net.PacketConn, ifaceHWAddr net.HardwareAddr, opts ...ClientOpt) (*Client, error) { + c := &Client{ + ifaceHWAddr: ifaceHWAddr, + timeout: DefaultTimeout, + retry: DefaultRetries, + serverAddr: DefaultServers, + bufferCap: defaultBufferCap, + conn: conn, + logger: EmptyLogger{}, + + done: make(chan struct{}), + pending: make(map[dhcpv4.TransactionID]*pendingCh), + } + + for _, opt := range opts { + err := opt(c) + if err != nil { + return nil, fmt.Errorf("unable to apply option: %w", err) + } + } + + if c.ifaceHWAddr == nil { + if iface == `` { + return nil, ErrNoIfaceHWAddr + } + + i, err := net.InterfaceByName(iface) + if err != nil { + return nil, fmt.Errorf("unable to get interface information: %w", err) + } + + c.ifaceHWAddr = i.HardwareAddr + } + + if c.conn == nil { + var err error + if iface == `` { + return nil, ErrNoConn + } + c.conn, err = NewRawUDPConn(iface, ClientPort) // broadcast + if err != nil { + return nil, fmt.Errorf("unable to open a broadcasting socket: %w", err) + } + } + c.wg.Add(1) + go c.receiveLoop() + return c, nil +} + +// Close closes the underlying connection. +func (c *Client) Close() error { + // Make sure not to close done twice. + if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) { + return nil + } + + err := c.conn.Close() + + // Closing c.done sets off a chain reaction: + // + // Any SendAndRead unblocks trying to receive more messages, which + // means rem() gets called. + // + // rem() should be unblocking receiveLoop if it is blocked. + // + // receiveLoop should then exit gracefully. + close(c.done) + + // Wait for receiveLoop to stop. + c.wg.Wait() + + return err +} + +func (c *Client) isClosed() bool { + return atomic.LoadUint32(&c.closed) != 0 +} + +func (c *Client) receiveLoop() { + defer c.wg.Done() + for { + // TODO: Clients can send a "max packet size" option in their + // packets, IIRC. Choose a reasonable size and set it. + b := make([]byte, MaxMessageSize) + n, _, err := c.conn.ReadFrom(b) + if err != nil { + if !c.isClosed() { + c.logger.Printf("error reading from UDP connection: %v", err) + } + return + } + + msg, err := dhcpv4.FromBytes(b[:n]) + if err != nil { + // Not a valid DHCP packet; keep listening. + continue + } + + if msg.OpCode != dhcpv4.OpcodeBootReply { + // Not a response message. + continue + } + + // This is a somewhat non-standard check, by the looks + // of RFC 2131. It should work as long as the DHCP + // server is spec-compliant for the HWAddr field. + if c.ifaceHWAddr != nil && !bytes.Equal(c.ifaceHWAddr, msg.ClientHWAddr) { + // Not for us. + continue + } + + c.pendingMu.Lock() + p, ok := c.pending[msg.TransactionID] + if ok { + select { + case <-p.done: + close(p.ch) + delete(c.pending, msg.TransactionID) + + // This send may block. + case p.ch <- msg: + } + } + c.pendingMu.Unlock() + } +} + +// ClientOpt is a function that configures the Client. +type ClientOpt func(c *Client) error + +// WithTimeout configures the retransmission timeout. +// +// Default is 5 seconds. +func WithTimeout(d time.Duration) ClientOpt { + return func(c *Client) (err error) { + c.timeout = d + return + } +} + +// WithSummaryLogger logs one-line DHCPv4 message summaries when sent & received. +func WithSummaryLogger() ClientOpt { + return func(c *Client) (err error) { + c.logger = ShortSummaryLogger{ + Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), + } + return + } +} + +// WithDebugLogger logs multi-line full DHCPv4 messages when sent & received. +func WithDebugLogger() ClientOpt { + return func(c *Client) (err error) { + c.logger = DebugLogger{ + Printfer: log.New(os.Stderr, "[dhcpv4] ", log.LstdFlags), + } + return + } +} + +// WithLogger set the logger (see interface Logger). +func WithLogger(newLogger Logger) ClientOpt { + return func(c *Client) (err error) { + c.logger = newLogger + return + } +} + +// WithUnicast forces client to send messages as unicast frames. +// By default client sends messages as broadcast frames even if server address is defined. +// +// srcAddr is both: +// * The source address of outgoing frames. +// * The address to be listened for incoming frames. +func WithUnicast(srcAddr *net.UDPAddr) ClientOpt { + return func(c *Client) (err error) { + if srcAddr == nil { + srcAddr = &net.UDPAddr{Port: ServerPort} + } + c.conn, err = net.ListenUDP("udp4", srcAddr) + if err != nil { + err = fmt.Errorf("unable to start listening UDP port: %w", err) + } + return + } +} + +// WithHWAddr tells to the Client to receive messages destinated to selected +// hardware address +func WithHWAddr(hwAddr net.HardwareAddr) ClientOpt { + return func(c *Client) (err error) { + c.ifaceHWAddr = hwAddr + return + } +} + +func withBufferCap(n int) ClientOpt { + return func(c *Client) (err error) { + c.bufferCap = n + return + } +} + +// WithRetry configures the number of retransmissions to attempt. +// +// Default is 3. +func WithRetry(r int) ClientOpt { + return func(c *Client) (err error) { + c.retry = r + return + } +} + +// WithServerAddr configures the address to send messages to. +func WithServerAddr(n *net.UDPAddr) ClientOpt { + return func(c *Client) (err error) { + c.serverAddr = n + return + } +} + +// Matcher matches DHCP packets. +type Matcher func(*dhcpv4.DHCPv4) bool + +// IsMessageType returns a matcher that checks for the message type. +// +// If t is MessageTypeNone, all packets are matched. +func IsMessageType(t dhcpv4.MessageType) Matcher { + return func(p *dhcpv4.DHCPv4) bool { + return p.MessageType() == t || t == dhcpv4.MessageTypeNone + } +} + +// DiscoverOffer sends a DHCPDiscover message and returns the first valid offer +// received. +func (c *Client) DiscoverOffer(ctx context.Context, modifiers ...dhcpv4.Modifier) (offer *dhcpv4.DHCPv4, err error) { + // RFC 2131, Section 4.4.1, Table 5 details what a DISCOVER packet should + // contain. + discover, err := dhcpv4.NewDiscovery(c.ifaceHWAddr, dhcpv4.PrependModifiers(modifiers, + dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)))...) + if err != nil { + err = fmt.Errorf("unable to create a discovery request: %w", err) + return + } + + offer, err = c.SendAndRead(ctx, c.serverAddr, discover, IsMessageType(dhcpv4.MessageTypeOffer)) + if err != nil { + err = fmt.Errorf("got an error while the discovery request: %w", err) + return + } + + return +} + +// Request completes the 4-way Discover-Offer-Request-Ack handshake. +// +// Note that modifiers will be applied *both* to Discover and Request packets. +func (c *Client) Request(ctx context.Context, modifiers ...dhcpv4.Modifier) (offer, ack *dhcpv4.DHCPv4, err error) { + offer, err = c.DiscoverOffer(ctx, modifiers...) + if err != nil { + err = fmt.Errorf("unable to receive an offer: %w", err) + return + } + + // TODO(chrisko): should this be unicast to the server? + request, err := dhcpv4.NewRequestFromOffer(offer, dhcpv4.PrependModifiers(modifiers, + dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)))...) + if err != nil { + err = fmt.Errorf("unable to create a request: %w", err) + return + } + + ack, err = c.SendAndRead(ctx, c.serverAddr, request, nil) + if err != nil { + err = fmt.Errorf("got an error while processing the request: %w", err) + return + } + + return +} + +// ErrTransactionIDInUse is returned if there were an attempt to send a message +// with the same TransactionID as we are already waiting an answer for. +type ErrTransactionIDInUse struct { + // TransactionID is the transaction ID of the message which the error is related to. + TransactionID dhcpv4.TransactionID +} + +// Error is just the method to comply interface "error". +func (err *ErrTransactionIDInUse) Error() string { + return fmt.Sprintf("transaction ID %s already in use", err.TransactionID) +} + +// send sends p to destination and returns a response channel. +// +// Responses will be matched by transaction ID and ClientHWAddr. +// +// The returned lambda function must be called after all desired responses have +// been received in order to return the Transaction ID to the usable pool. +func (c *Client) send(dest *net.UDPAddr, msg *dhcpv4.DHCPv4) (resp <-chan *dhcpv4.DHCPv4, cancel func(), err error) { + c.pendingMu.Lock() + if _, ok := c.pending[msg.TransactionID]; ok { + c.pendingMu.Unlock() + return nil, nil, &ErrTransactionIDInUse{msg.TransactionID} + } + + ch := make(chan *dhcpv4.DHCPv4, c.bufferCap) + done := make(chan struct{}) + c.pending[msg.TransactionID] = &pendingCh{done: done, ch: ch} + c.pendingMu.Unlock() + + cancel = func() { + // Why can't we just close ch here? + // + // Because receiveLoop may potentially be blocked trying to + // send on ch. We gotta unblock it first, and then we can take + // the lock and remove the XID from the pending transaction + // map. + close(done) + + c.pendingMu.Lock() + if p, ok := c.pending[msg.TransactionID]; ok { + close(p.ch) + delete(c.pending, msg.TransactionID) + } + c.pendingMu.Unlock() + } + + if _, err := c.conn.WriteTo(msg.ToBytes(), dest); err != nil { + cancel() + return nil, nil, fmt.Errorf("error writing packet to connection: %w", err) + } + return ch, cancel, nil +} + +// This error should never be visible to users. +// It is used only to increase the timeout in retryFn. +var errDeadlineExceeded = errors.New("INTERNAL ERROR: deadline exceeded") + +// SendAndRead sends a packet p to a destination dest and waits for the first +// response matching `match` as well as its Transaction ID and ClientHWAddr. +// +// If match is nil, the first packet matching the Transaction ID and +// ClientHWAddr is returned. +func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcpv4.DHCPv4, match Matcher) (*dhcpv4.DHCPv4, error) { + var response *dhcpv4.DHCPv4 + err := c.retryFn(func(timeout time.Duration) error { + ch, rem, err := c.send(dest, p) + if err != nil { + return err + } + c.logger.PrintMessage("sent message", p) + defer rem() + + for { + select { + case <-c.done: + return ErrNoResponse + + case <-time.After(timeout): + return errDeadlineExceeded + + case <-ctx.Done(): + return ctx.Err() + + case packet := <-ch: + if match == nil || match(packet) { + c.logger.PrintMessage("received message", packet) + response = packet + return nil + } + } + } + }) + if err == errDeadlineExceeded { + return nil, ErrNoResponse + } + if err != nil { + return nil, err + } + return response, nil +} + +func (c *Client) retryFn(fn func(timeout time.Duration) error) error { + timeout := c.timeout + + // Each retry takes the amount of timeout at worst. + for i := 0; i < c.retry || c.retry < 0; i++ { // TODO: why is this called "retry" if this is "tries" ("retries"+1)? + switch err := fn(timeout); err { + case nil: + // Got it! + return nil + + case errDeadlineExceeded: + // Double timeout, then retry. + timeout *= 2 + + default: + return err + } + } + + return errDeadlineExceeded +} diff --git a/dhcpd/nclient4/client_test.go b/dhcpd/nclient4/client_test.go new file mode 100644 index 00000000..ddad1144 --- /dev/null +++ b/dhcpd/nclient4/client_test.go @@ -0,0 +1,333 @@ +// Copyright 2018 the u-root Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux +// github.com/hugelgupf/socketpair is Linux-only +// +build go1.12 + +package nclient4 + +import ( + "bytes" + "context" + "fmt" + "net" + "sync" + "testing" + "time" + + "github.com/hugelgupf/socketpair" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv4/server4" +) + +type handler struct { + mu sync.Mutex + received []*dhcpv4.DHCPv4 + + // Each received packet can have more than one response (in theory, + // from different servers sending different Advertise, for example). + responses [][]*dhcpv4.DHCPv4 +} + +func (h *handler) handle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) { + h.mu.Lock() + defer h.mu.Unlock() + + h.received = append(h.received, m) + + if len(h.responses) > 0 { + for _, resp := range h.responses[0] { + _, _ = conn.WriteTo(resp.ToBytes(), peer) + } + h.responses = h.responses[1:] + } +} + +func serveAndClient(ctx context.Context, responses [][]*dhcpv4.DHCPv4, opts ...ClientOpt) (*Client, net.PacketConn) { + // Fake PacketConn connection. + clientRawConn, serverRawConn, err := socketpair.PacketSocketPair() + if err != nil { + panic(err) + } + + clientConn := NewBroadcastUDPConn(clientRawConn, &net.UDPAddr{Port: ClientPort}) + serverConn := NewBroadcastUDPConn(serverRawConn, &net.UDPAddr{Port: ServerPort}) + + o := []ClientOpt{WithRetry(1), WithTimeout(2 * time.Second)} + o = append(o, opts...) + mc, err := NewWithConn(clientConn, net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf}, o...) + if err != nil { + panic(err) + } + + h := &handler{responses: responses} + s, err := server4.NewServer("", nil, h.handle, server4.WithConn(serverConn)) + if err != nil { + panic(err) + } + go func() { + _ = s.Serve() + }() + + return mc, serverConn +} + +func ComparePacket(got *dhcpv4.DHCPv4, want *dhcpv4.DHCPv4) error { + if got == nil && got == want { + return nil + } + if (want == nil || got == nil) && (got != want) { + return fmt.Errorf("packet got %v, want %v", got, want) + } + if !bytes.Equal(got.ToBytes(), want.ToBytes()) { + return fmt.Errorf("packet got %v, want %v", got, want) + } + return nil +} + +func pktsExpected(got []*dhcpv4.DHCPv4, want []*dhcpv4.DHCPv4) error { + if len(got) != len(want) { + return fmt.Errorf("got %d packets, want %d packets", len(got), len(want)) + } + + for i := range got { + if err := ComparePacket(got[i], want[i]); err != nil { + return err + } + } + return nil +} + +func newPacketWeirdHWAddr(op dhcpv4.OpcodeType, xid dhcpv4.TransactionID) *dhcpv4.DHCPv4 { + p, err := dhcpv4.New() + if err != nil { + panic(fmt.Sprintf("newpacket: %v", err)) + } + p.OpCode = op + p.TransactionID = xid + p.ClientHWAddr = net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 1, 2, 3, 4, 5, 6} + return p +} + +func newPacket(op dhcpv4.OpcodeType, xid dhcpv4.TransactionID) *dhcpv4.DHCPv4 { + p, err := dhcpv4.New() + if err != nil { + panic(fmt.Sprintf("newpacket: %v", err)) + } + p.OpCode = op + p.TransactionID = xid + p.ClientHWAddr = net.HardwareAddr{0xa, 0xb, 0xc, 0xd, 0xe, 0xf} + return p +} + +func TestSendAndRead(t *testing.T) { + for _, tt := range []struct { + desc string + send *dhcpv4.DHCPv4 + server []*dhcpv4.DHCPv4 + + // If want is nil, we assume server[0] contains what is wanted. + want *dhcpv4.DHCPv4 + wantErr error + }{ + { + desc: "two response packets", + send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), + server: []*dhcpv4.DHCPv4{ + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + }, + want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + }, + { + desc: "one response packet", + send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), + server: []*dhcpv4.DHCPv4{ + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + }, + want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + }, + { + desc: "one response packet, one invalid XID, one invalid opcode, one invalid hwaddr", + send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), + server: []*dhcpv4.DHCPv4{ + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x77, 0x33, 0x33, 0x33}), + newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), + newPacketWeirdHWAddr(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + }, + want: newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + }, + { + desc: "discard wrong XID", + send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), + server: []*dhcpv4.DHCPv4{ + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0, 0, 0, 0}), + }, + want: nil, // Explicitly empty. + wantErr: ErrNoResponse, + }, + { + desc: "no response, timeout", + send: newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), + wantErr: ErrNoResponse, + }, + } { + t.Run(tt.desc, func(t *testing.T) { + // Both server and client only get 2 seconds. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{tt.server}, + // Use an unbuffered channel to make sure we + // have no deadlocks. + withBufferCap(0)) + defer mc.Close() + + rcvd, err := mc.SendAndRead(context.Background(), DefaultServers, tt.send, nil) + if err != tt.wantErr { + t.Error(err) + } + + if err := ComparePacket(rcvd, tt.want); err != nil { + t.Errorf("got unexpected packets: %v", err) + } + }) + } +} + +func TestParallelSendAndRead(t *testing.T) { + pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}) + + // Both the server and client only get 2 seconds. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{}, + WithTimeout(10*time.Second), + // Use an unbuffered channel to make sure nothing blocks. + withBufferCap(0)) + defer mc.Close() + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse { + t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + + time.Sleep(4 * time.Second) + + if err := mc.Close(); err != nil { + t.Errorf("closing failed: %v", err) + } + }() + + wg.Wait() +} + +func TestReuseXID(t *testing.T) { + pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}) + + // Both the server and client only get 2 seconds. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + mc, _ := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{}) + defer mc.Close() + + if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse { + t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse) + } + + if _, err := mc.SendAndRead(context.Background(), DefaultServers, pkt, nil); err != ErrNoResponse { + t.Errorf("SendAndRead(%v) = %v, want %v", pkt, err, ErrNoResponse) + } +} + +func TestSimpleSendAndReadDiscardGarbage(t *testing.T) { + pkt := newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}) + + responses := newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}) + + // Both the server and client only get 2 seconds. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + mc, udpConn := serveAndClient(ctx, [][]*dhcpv4.DHCPv4{{responses}}) + defer mc.Close() + + // Too short for valid DHCPv4 packet. + _, _ = udpConn.WriteTo([]byte{0x01}, nil) + _, _ = udpConn.WriteTo([]byte{0x01, 0x2}, nil) + + rcvd, err := mc.SendAndRead(ctx, DefaultServers, pkt, nil) + if err != nil { + t.Errorf("SendAndRead(%v) = %v, want nil", pkt, err) + } + + if err := ComparePacket(rcvd, responses); err != nil { + t.Errorf("got unexpected packets: %v", err) + } +} + +func TestMultipleSendAndRead(t *testing.T) { + for _, tt := range []struct { + desc string + send []*dhcpv4.DHCPv4 + server [][]*dhcpv4.DHCPv4 + wantErr []error + }{ + { + desc: "two requests, two responses", + send: []*dhcpv4.DHCPv4{ + newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x33, 0x33, 0x33, 0x33}), + newPacket(dhcpv4.OpcodeBootRequest, [4]byte{0x44, 0x44, 0x44, 0x44}), + }, + server: [][]*dhcpv4.DHCPv4{ + []*dhcpv4.DHCPv4{ // Response for first packet. + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x33, 0x33, 0x33, 0x33}), + }, + []*dhcpv4.DHCPv4{ // Response for second packet. + newPacket(dhcpv4.OpcodeBootReply, [4]byte{0x44, 0x44, 0x44, 0x44}), + }, + }, + wantErr: []error{ + nil, + nil, + }, + }, + } { + // Both server and client only get 2 seconds. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + mc, _ := serveAndClient(ctx, tt.server) + defer mc.Close() + + for i, send := range tt.send { + ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + rcvd, err := mc.SendAndRead(ctx, DefaultServers, send, nil) + + if wantErr := tt.wantErr[i]; err != wantErr { + t.Errorf("SendAndReadOne(%v): got %v, want %v", send, err, wantErr) + } + if err := pktsExpected([]*dhcpv4.DHCPv4{rcvd}, tt.server[i]); err != nil { + t.Errorf("got unexpected packets: %v", err) + } + } + } +} diff --git a/dhcpd/nclient4/conn_unix.go b/dhcpd/nclient4/conn_unix.go new file mode 100644 index 00000000..51ec98cb --- /dev/null +++ b/dhcpd/nclient4/conn_unix.go @@ -0,0 +1,144 @@ +// Copyright 2018 the u-root Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin dragonfly freebsd linux netbsd openbsd solaris +// +build go1.12 + +package nclient4 + +import ( + "errors" + "io" + "net" + + "github.com/mdlayher/ethernet" + "github.com/mdlayher/raw" + "github.com/u-root/u-root/pkg/uio" +) + +var ( + // BroadcastMac is the broadcast MAC address. + // + // Any UDP packet sent to this address is broadcast on the subnet. + BroadcastMac = net.HardwareAddr([]byte{255, 255, 255, 255, 255, 255}) +) + +var ( + // ErrUDPAddrIsRequired is an error used when a passed argument is not of type "*net.UDPAddr". + ErrUDPAddrIsRequired = errors.New("must supply UDPAddr") +) + +// NewRawUDPConn returns a UDP connection bound to the interface and port +// given based on a raw packet socket. All packets are broadcasted. +// +// The interface can be completely unconfigured. +func NewRawUDPConn(iface string, port int) (net.PacketConn, error) { + ifc, err := net.InterfaceByName(iface) + if err != nil { + return nil, err + } + rawConn, err := raw.ListenPacket(ifc, uint16(ethernet.EtherTypeIPv4), &raw.Config{LinuxSockDGRAM: true}) + if err != nil { + return nil, err + } + return NewBroadcastUDPConn(rawConn, &net.UDPAddr{Port: port}), nil +} + +// BroadcastRawUDPConn uses a raw socket to send UDP packets to the broadcast +// MAC address. +type BroadcastRawUDPConn struct { + // PacketConn is a raw DGRAM socket. + net.PacketConn + + // boundAddr is the address this RawUDPConn is "bound" to. + // + // Calls to ReadFrom will only return packets destined to this address. + boundAddr *net.UDPAddr +} + +// NewBroadcastUDPConn returns a PacketConn that marshals and unmarshals UDP +// packets, sending them to the broadcast MAC at on rawPacketConn. +// +// Calls to ReadFrom will only return packets destined to boundAddr. +func NewBroadcastUDPConn(rawPacketConn net.PacketConn, boundAddr *net.UDPAddr) net.PacketConn { + return &BroadcastRawUDPConn{ + PacketConn: rawPacketConn, + boundAddr: boundAddr, + } +} + +func udpMatch(addr *net.UDPAddr, bound *net.UDPAddr) bool { + if bound == nil { + return true + } + if bound.IP != nil && !bound.IP.Equal(addr.IP) { + return false + } + return bound.Port == addr.Port +} + +// ReadFrom implements net.PacketConn.ReadFrom. +// +// ReadFrom reads raw IP packets and will try to match them against +// upc.boundAddr. Any matching packets are returned via the given buffer. +func (upc *BroadcastRawUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { + ipHdrMaxLen := IPv4MaximumHeaderSize + udpHdrLen := UDPMinimumSize + + for { + pkt := make([]byte, ipHdrMaxLen+udpHdrLen+len(b)) + n, _, err := upc.PacketConn.ReadFrom(pkt) + if err != nil { + return 0, nil, err + } + if n == 0 { + return 0, nil, io.EOF + } + pkt = pkt[:n] + buf := uio.NewBigEndianBuffer(pkt) + + // To read the header length, access data directly. + ipHdr := IPv4(buf.Data()) + ipHdr = IPv4(buf.Consume(int(ipHdr.HeaderLength()))) + + if ipHdr.TransportProtocol() != UDPProtocolNumber { + continue + } + udpHdr := UDP(buf.Consume(udpHdrLen)) + + addr := &net.UDPAddr{ + IP: ipHdr.DestinationAddress(), + Port: int(udpHdr.DestinationPort()), + } + if !udpMatch(addr, upc.boundAddr) { + continue + } + srcAddr := &net.UDPAddr{ + IP: ipHdr.SourceAddress(), + Port: int(udpHdr.SourcePort()), + } + // Extra padding after end of IP packet should be ignored, + // if not dhcp option parsing will fail. + dhcpLen := int(ipHdr.PayloadLength()) - udpHdrLen + return copy(b, buf.Consume(dhcpLen)), srcAddr, nil + } +} + +// WriteTo implements net.PacketConn.WriteTo and broadcasts all packets at the +// raw socket level. +// +// WriteTo wraps the given packet in the appropriate UDP and IP header before +// sending it on the packet conn. +func (upc *BroadcastRawUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { + udpAddr, ok := addr.(*net.UDPAddr) + if !ok { + return 0, ErrUDPAddrIsRequired + } + + // Using the boundAddr is not quite right here, but it works. + packet := udp4pkt(b, udpAddr, upc.boundAddr) + + // Broadcasting is not always right, but hell, what the ARP do I know. + return upc.PacketConn.WriteTo(packet, &raw.Addr{HardwareAddr: BroadcastMac}) +} diff --git a/dhcpd/nclient4/ipv4.go b/dhcpd/nclient4/ipv4.go new file mode 100644 index 00000000..5733eb46 --- /dev/null +++ b/dhcpd/nclient4/ipv4.go @@ -0,0 +1,376 @@ +// Copyright 2018 Google LLC +// +// 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. +// +// This file contains code taken from gVisor. + +// +build go1.12 + +package nclient4 + +import ( + "encoding/binary" + "net" + + "github.com/u-root/u-root/pkg/uio" +) + +const ( + versIHL = 0 + tos = 1 + totalLen = 2 + id = 4 + flagsFO = 6 + ttl = 8 + protocol = 9 + checksum = 10 + srcAddr = 12 + dstAddr = 16 +) + +// TransportProtocolNumber is the number of a transport protocol. +type TransportProtocolNumber uint32 + +// IPv4Fields contains the fields of an IPv4 packet. It is used to describe the +// fields of a packet that needs to be encoded. +type IPv4Fields struct { + // IHL is the "internet header length" field of an IPv4 packet. + IHL uint8 + + // TOS is the "type of service" field of an IPv4 packet. + TOS uint8 + + // TotalLength is the "total length" field of an IPv4 packet. + TotalLength uint16 + + // ID is the "identification" field of an IPv4 packet. + ID uint16 + + // Flags is the "flags" field of an IPv4 packet. + Flags uint8 + + // FragmentOffset is the "fragment offset" field of an IPv4 packet. + FragmentOffset uint16 + + // TTL is the "time to live" field of an IPv4 packet. + TTL uint8 + + // Protocol is the "protocol" field of an IPv4 packet. + Protocol uint8 + + // Checksum is the "checksum" field of an IPv4 packet. + Checksum uint16 + + // SrcAddr is the "source ip address" of an IPv4 packet. + SrcAddr net.IP + + // DstAddr is the "destination ip address" of an IPv4 packet. + DstAddr net.IP +} + +// IPv4 represents an ipv4 header stored in a byte array. +// Most of the methods of IPv4 access to the underlying slice without +// checking the boundaries and could panic because of 'index out of range'. +// Always call IsValid() to validate an instance of IPv4 before using other methods. +type IPv4 []byte + +const ( + // IPv4MinimumSize is the minimum size of a valid IPv4 packet. + IPv4MinimumSize = 20 + + // IPv4MaximumHeaderSize is the maximum size of an IPv4 header. Given + // that there are only 4 bits to represents the header length in 32-bit + // units, the header cannot exceed 15*4 = 60 bytes. + IPv4MaximumHeaderSize = 60 + + // IPv4AddressSize is the size, in bytes, of an IPv4 address. + IPv4AddressSize = 4 + + // IPv4Version is the version of the ipv4 protocol. + IPv4Version = 4 +) + +var ( + // IPv4Broadcast is the broadcast address of the IPv4 protocol. + IPv4Broadcast = net.IP{0xff, 0xff, 0xff, 0xff} + + // IPv4Any is the non-routable IPv4 "any" meta address. + IPv4Any = net.IP{0, 0, 0, 0} +) + +// Flags that may be set in an IPv4 packet. +const ( + IPv4FlagMoreFragments = 1 << iota + IPv4FlagDontFragment +) + +// HeaderLength returns the value of the "header length" field of the ipv4 +// header. +func (b IPv4) HeaderLength() uint8 { + return (b[versIHL] & 0xf) * 4 +} + +// Protocol returns the value of the protocol field of the ipv4 header. +func (b IPv4) Protocol() uint8 { + return b[protocol] +} + +// SourceAddress returns the "source address" field of the ipv4 header. +func (b IPv4) SourceAddress() net.IP { + return net.IP(b[srcAddr : srcAddr+IPv4AddressSize]) +} + +// DestinationAddress returns the "destination address" field of the ipv4 +// header. +func (b IPv4) DestinationAddress() net.IP { + return net.IP(b[dstAddr : dstAddr+IPv4AddressSize]) +} + +// TransportProtocol implements Network.TransportProtocol. +func (b IPv4) TransportProtocol() TransportProtocolNumber { + return TransportProtocolNumber(b.Protocol()) +} + +// Payload implements Network.Payload. +func (b IPv4) Payload() []byte { + return b[b.HeaderLength():][:b.PayloadLength()] +} + +// PayloadLength returns the length of the payload portion of the ipv4 packet. +func (b IPv4) PayloadLength() uint16 { + return b.TotalLength() - uint16(b.HeaderLength()) +} + +// TotalLength returns the "total length" field of the ipv4 header. +func (b IPv4) TotalLength() uint16 { + return binary.BigEndian.Uint16(b[totalLen:]) +} + +// SetTotalLength sets the "total length" field of the ipv4 header. +func (b IPv4) SetTotalLength(totalLength uint16) { + binary.BigEndian.PutUint16(b[totalLen:], totalLength) +} + +// SetChecksum sets the checksum field of the ipv4 header. +func (b IPv4) SetChecksum(v uint16) { + binary.BigEndian.PutUint16(b[checksum:], v) +} + +// SetFlagsFragmentOffset sets the "flags" and "fragment offset" fields of the +// ipv4 header. +func (b IPv4) SetFlagsFragmentOffset(flags uint8, offset uint16) { + v := (uint16(flags) << 13) | (offset >> 3) + binary.BigEndian.PutUint16(b[flagsFO:], v) +} + +// SetSourceAddress sets the "source address" field of the ipv4 header. +func (b IPv4) SetSourceAddress(addr net.IP) { + copy(b[srcAddr:srcAddr+IPv4AddressSize], addr.To4()) +} + +// SetDestinationAddress sets the "destination address" field of the ipv4 +// header. +func (b IPv4) SetDestinationAddress(addr net.IP) { + copy(b[dstAddr:dstAddr+IPv4AddressSize], addr.To4()) +} + +// CalculateChecksum calculates the checksum of the ipv4 header. +func (b IPv4) CalculateChecksum() uint16 { + return Checksum(b[:b.HeaderLength()], 0) +} + +// Encode encodes all the fields of the ipv4 header. +func (b IPv4) Encode(i *IPv4Fields) { + b[versIHL] = (4 << 4) | ((i.IHL / 4) & 0xf) + b[tos] = i.TOS + b.SetTotalLength(i.TotalLength) + binary.BigEndian.PutUint16(b[id:], i.ID) + b.SetFlagsFragmentOffset(i.Flags, i.FragmentOffset) + b[ttl] = i.TTL + b[protocol] = i.Protocol + b.SetChecksum(i.Checksum) + copy(b[srcAddr:srcAddr+IPv4AddressSize], i.SrcAddr) + copy(b[dstAddr:dstAddr+IPv4AddressSize], i.DstAddr) +} + +const ( + udpSrcPort = 0 + udpDstPort = 2 + udpLength = 4 + udpChecksum = 6 +) + +// UDPFields contains the fields of a UDP packet. It is used to describe the +// fields of a packet that needs to be encoded. +type UDPFields struct { + // SrcPort is the "source port" field of a UDP packet. + SrcPort uint16 + + // DstPort is the "destination port" field of a UDP packet. + DstPort uint16 + + // Length is the "length" field of a UDP packet. + Length uint16 + + // Checksum is the "checksum" field of a UDP packet. + Checksum uint16 +} + +// UDP represents a UDP header stored in a byte array. +type UDP []byte + +const ( + // UDPMinimumSize is the minimum size of a valid UDP packet. + UDPMinimumSize = 8 + + // UDPProtocolNumber is UDP's transport protocol number. + UDPProtocolNumber TransportProtocolNumber = 17 +) + +// SourcePort returns the "source port" field of the udp header. +func (b UDP) SourcePort() uint16 { + return binary.BigEndian.Uint16(b[udpSrcPort:]) +} + +// DestinationPort returns the "destination port" field of the udp header. +func (b UDP) DestinationPort() uint16 { + return binary.BigEndian.Uint16(b[udpDstPort:]) +} + +// Length returns the "length" field of the udp header. +func (b UDP) Length() uint16 { + return binary.BigEndian.Uint16(b[udpLength:]) +} + +// SetSourcePort sets the "source port" field of the udp header. +func (b UDP) SetSourcePort(port uint16) { + binary.BigEndian.PutUint16(b[udpSrcPort:], port) +} + +// SetDestinationPort sets the "destination port" field of the udp header. +func (b UDP) SetDestinationPort(port uint16) { + binary.BigEndian.PutUint16(b[udpDstPort:], port) +} + +// SetChecksum sets the "checksum" field of the udp header. +func (b UDP) SetChecksum(checksum uint16) { + binary.BigEndian.PutUint16(b[udpChecksum:], checksum) +} + +// Payload returns the data contained in the UDP datagram. +func (b UDP) Payload() []byte { + return b[UDPMinimumSize:] +} + +// Checksum returns the "checksum" field of the udp header. +func (b UDP) Checksum() uint16 { + return binary.BigEndian.Uint16(b[udpChecksum:]) +} + +// CalculateChecksum calculates the checksum of the udp packet, given the total +// length of the packet and the checksum of the network-layer pseudo-header +// (excluding the total length) and the checksum of the payload. +func (b UDP) CalculateChecksum(partialChecksum uint16, totalLen uint16) uint16 { + // Add the length portion of the checksum to the pseudo-checksum. + tmp := make([]byte, 2) + binary.BigEndian.PutUint16(tmp, totalLen) + checksum := Checksum(tmp, partialChecksum) + + // Calculate the rest of the checksum. + return Checksum(b[:UDPMinimumSize], checksum) +} + +// Encode encodes all the fields of the udp header. +func (b UDP) Encode(u *UDPFields) { + binary.BigEndian.PutUint16(b[udpSrcPort:], u.SrcPort) + binary.BigEndian.PutUint16(b[udpDstPort:], u.DstPort) + binary.BigEndian.PutUint16(b[udpLength:], u.Length) + binary.BigEndian.PutUint16(b[udpChecksum:], u.Checksum) +} + +func calculateChecksum(buf []byte, initial uint32) uint16 { + v := initial + + l := len(buf) + if l&1 != 0 { + l-- + v += uint32(buf[l]) << 8 + } + + for i := 0; i < l; i += 2 { + v += (uint32(buf[i]) << 8) + uint32(buf[i+1]) + } + + return ChecksumCombine(uint16(v), uint16(v>>16)) +} + +// Checksum calculates the checksum (as defined in RFC 1071) of the bytes in the +// given byte array. +// +// The initial checksum must have been computed on an even number of bytes. +func Checksum(buf []byte, initial uint16) uint16 { + return calculateChecksum(buf, uint32(initial)) +} + +// ChecksumCombine combines the two uint16 to form their checksum. This is done +// by adding them and the carry. +// +// Note that checksum a must have been computed on an even number of bytes. +func ChecksumCombine(a, b uint16) uint16 { + v := uint32(a) + uint32(b) + return uint16(v + v>>16) +} + +// PseudoHeaderChecksum calculates the pseudo-header checksum for the +// given destination protocol and network address, ignoring the length +// field. Pseudo-headers are needed by transport layers when calculating +// their own checksum. +func PseudoHeaderChecksum(protocol TransportProtocolNumber, srcAddr net.IP, dstAddr net.IP) uint16 { + xsum := Checksum([]byte(srcAddr), 0) + xsum = Checksum([]byte(dstAddr), xsum) + return Checksum([]byte{0, uint8(protocol)}, xsum) +} + +func udp4pkt(packet []byte, dest *net.UDPAddr, src *net.UDPAddr) []byte { + ipLen := IPv4MinimumSize + udpLen := UDPMinimumSize + + h := make([]byte, 0, ipLen+udpLen+len(packet)) + hdr := uio.NewBigEndianBuffer(h) + + ipv4fields := &IPv4Fields{ + IHL: IPv4MinimumSize, + TotalLength: uint16(ipLen + udpLen + len(packet)), + TTL: 64, // Per RFC 1700's recommendation for IP time to live + Protocol: uint8(UDPProtocolNumber), + SrcAddr: src.IP.To4(), + DstAddr: dest.IP.To4(), + } + ipv4hdr := IPv4(hdr.WriteN(ipLen)) + ipv4hdr.Encode(ipv4fields) + ipv4hdr.SetChecksum(^ipv4hdr.CalculateChecksum()) + + udphdr := UDP(hdr.WriteN(udpLen)) + udphdr.Encode(&UDPFields{ + SrcPort: uint16(src.Port), + DstPort: uint16(dest.Port), + Length: uint16(udpLen + len(packet)), + }) + + xsum := Checksum(packet, PseudoHeaderChecksum( + ipv4hdr.TransportProtocol(), ipv4fields.SrcAddr, ipv4fields.DstAddr)) + udphdr.SetChecksum(^udphdr.CalculateChecksum(xsum, udphdr.Length())) + + hdr.WriteBytes(packet) + return hdr.Data() +} diff --git a/go.mod b/go.mod index fc774a4f..7bae4404 100644 --- a/go.mod +++ b/go.mod @@ -7,23 +7,25 @@ require ( github.com/AdguardTeam/golibs v0.4.2 github.com/AdguardTeam/urlfilter v0.11.2 github.com/NYTimes/gziphandler v1.1.1 - github.com/fsnotify/fsnotify v1.4.7 + github.com/fsnotify/fsnotify v1.4.9 github.com/gobuffalo/packr v1.30.1 - github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 // indirect - github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7 + github.com/google/go-cmp v0.4.0 // indirect + github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 + github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8 github.com/joomcode/errorx v1.0.1 github.com/kardianos/service v1.1.0 - github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect - github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect + github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 + github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 github.com/miekg/dns v1.1.29 github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.6.0 // indirect github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c github.com/stretchr/testify v1.5.1 - github.com/u-root/u-root v6.0.0+incompatible // indirect + github.com/u-root/u-root v6.0.0+incompatible go.etcd.io/bbolt v1.3.4 - golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 - golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e - golang.org/x/sys v0.0.0-20200331124033-c3d80250170d + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/net v0.0.0-20200625001655-4c5254603344 + golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 gopkg.in/natefinch/lumberjack.v2 v2.0.0 - gopkg.in/yaml.v2 v2.2.8 + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index 2df1d656..d043c5ec 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-test/deep v1.0.5 h1:AKODKU3pDH1RzZzm6YZu77YWtEAq6uh1rLIAQlay2qc= @@ -52,14 +54,16 @@ github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWS github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7 h1:iaCm+9nZdYb8XCSU2TfIb0qYTcAlIv2XzyKR2d2xZ38= -github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7/go.mod h1:CfMdguCK66I5DAUJgGKyNz8aB6vO5dZzkm9Xep6WGvw= +github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8 h1:u+vle+5E78+cT/CSMD5/Y3NUpMgA83Yu2KhG+Zbco/k= +github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8/go.mod h1:CfMdguCK66I5DAUJgGKyNz8aB6vO5dZzkm9Xep6WGvw= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= @@ -73,6 +77,8 @@ github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0L github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -105,6 +111,8 @@ github.com/shirou/gopsutil v2.20.3+incompatible h1:0JVooMPsT7A7HqEYdydp/OfjSOYSj github.com/shirou/gopsutil v2.20.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c h1:gqEdF4VwBu3lTKGHS9rXE9x1/pEaSwCXRLOZRF6qtlw= github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -135,6 +143,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk= golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -145,6 +155,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -157,10 +169,13 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -169,6 +184,8 @@ golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= @@ -179,3 +196,5 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From c9f58ce4a73e57f4d23db9e99315b5bfd982fb16 Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Tue, 18 Aug 2020 17:34:02 +0300 Subject: [PATCH 08/12] * upgrade yaml schema 6->7: DHCP settings --- home/upgrade.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/home/upgrade.go b/home/upgrade.go index b2936a3e..4021c206 100644 --- a/home/upgrade.go +++ b/home/upgrade.go @@ -13,7 +13,7 @@ import ( yaml "gopkg.in/yaml.v2" ) -const currentSchemaVersion = 6 // used for upgrading from old configs to new config +const currentSchemaVersion = 7 // used for upgrading from old configs to new config // Performs necessary upgrade operations if needed func upgradeConfig() error { @@ -90,6 +90,12 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err if err != nil { return err } + fallthrough + case 6: + err := upgradeSchema6to7(diskConfig) + if err != nil { + return err + } default: err := fmt.Errorf("configuration file contains unknown schema_version, abort") log.Println(err) @@ -345,3 +351,87 @@ func upgradeSchema5to6(diskConfig *map[string]interface{}) error { return nil } + +// dhcp: +// enabled: false +// interface_name: vboxnet0 +// gateway_ip: 192.168.56.1 +// ... +// +// -> +// +// dhcp: +// enabled: false +// interface_name: vboxnet0 +// dhcpv4: +// gateway_ip: 192.168.56.1 +// ... +func upgradeSchema6to7(diskConfig *map[string]interface{}) error { + log.Printf("Upgrade yaml: 6 to 7") + + (*diskConfig)["schema_version"] = 7 + + _dhcp, ok := (*diskConfig)["dhcp"] + if !ok { + return nil + } + + switch dhcp := _dhcp.(type) { + case map[interface{}]interface{}: + dhcpv4 := map[string]interface{}{} + val, ok := dhcp["gateway_ip"].(string) + if !ok { + log.Fatalf("expecting dhcp.%s to be a string", "gateway_ip") + return nil + } + dhcpv4["gateway_ip"] = val + delete(dhcp, "gateway_ip") + + val, ok = dhcp["subnet_mask"].(string) + if !ok { + log.Fatalf("expecting dhcp.%s to be a string", "subnet_mask") + return nil + } + dhcpv4["subnet_mask"] = val + delete(dhcp, "subnet_mask") + + val, ok = dhcp["range_start"].(string) + if !ok { + log.Fatalf("expecting dhcp.%s to be a string", "range_start") + return nil + } + dhcpv4["range_start"] = val + delete(dhcp, "range_start") + + val, ok = dhcp["range_end"].(string) + if !ok { + log.Fatalf("expecting dhcp.%s to be a string", "range_end") + return nil + } + dhcpv4["range_end"] = val + delete(dhcp, "range_end") + + intVal, ok := dhcp["lease_duration"].(int) + if !ok { + log.Fatalf("expecting dhcp.%s to be an integer", "lease_duration") + return nil + } + dhcpv4["lease_duration"] = intVal + delete(dhcp, "lease_duration") + + intVal, ok = dhcp["icmp_timeout_msec"].(int) + if !ok { + log.Fatalf("expecting dhcp.%s to be an integer", "icmp_timeout_msec") + return nil + } + dhcpv4["icmp_timeout_msec"] = intVal + delete(dhcp, "icmp_timeout_msec") + + dhcp["dhcpv4"] = dhcpv4 + + default: + return nil + } + + return nil +} From 1d35d73fc515923b7d001d99314ac7ee87afcd95 Mon Sep 17 00:00:00 2001 From: Artem Baskal Date: Wed, 19 Aug 2020 18:23:05 +0300 Subject: [PATCH 09/12] + client: Refactor DHCP settings --- .twosky.json | 4 +- README.md | 4 +- client/.eslintrc.json | 83 ++- client/package-lock.json | 18 +- client/package.json | 2 +- client/src/__locales/en.json | 8 +- client/src/__locales/hu.json | 565 ++++++++++++++++++ client/src/__locales/si-lk.json | 443 ++++++++++++++ client/src/actions/index.js | 61 +- client/src/api/Api.js | 300 ++-------- client/src/components/App/index.css | 15 +- client/src/components/App/index.js | 59 +- client/src/components/Dashboard/index.js | 240 ++++---- client/src/components/Filters/Form.js | 8 +- client/src/components/Header/index.js | 68 +-- client/src/components/Logs/Filters/index.js | 34 +- .../src/components/Settings/Clients/Form.js | 4 +- client/src/components/Settings/Dhcp/Form.js | 235 -------- .../components/Settings/Dhcp/FormDHCPv4.js | 145 +++++ .../components/Settings/Dhcp/FormDHCPv6.js | 120 ++++ .../components/Settings/Dhcp/Interfaces.js | 109 ++++ .../Settings/Dhcp/StaticLeases/Form.js | 41 +- .../Settings/Dhcp/StaticLeases/Modal.js | 24 +- .../Settings/Dhcp/StaticLeases/index.js | 177 +++--- client/src/components/Settings/Dhcp/index.js | 507 ++++++++-------- .../components/Settings/Dns/Config/Form.js | 4 +- .../components/Settings/Encryption/Form.js | 12 +- .../components/Settings/FiltersConfig/Form.js | 4 +- .../components/Settings/LogsConfig/Form.js | 6 +- client/src/components/Settings/Settings.css | 10 +- client/src/components/ui/Accordion.js | 43 -- client/src/helpers/constants.js | 42 +- client/src/helpers/form.js | 68 ++- client/src/helpers/helpers.js | 89 ++- client/src/helpers/validators.js | 82 ++- client/src/i18n.js | 8 + client/src/install/Setup/AddressList.js | 76 ++- client/src/install/Setup/Devices.js | 2 +- client/src/install/Setup/Settings.js | 129 ++-- client/src/install/Setup/Setup.css | 2 +- client/src/login/Login/Login.css | 2 +- client/src/reducers/dashboard.js | 175 ++++++ client/src/reducers/dhcp.js | 203 +++++++ client/src/reducers/dnsConfig.js | 4 +- client/src/reducers/index.js | 296 +-------- client/src/reducers/install.js | 12 +- client/src/reducers/settings.js | 63 ++ client/src/reducers/toasts.js | 5 +- home/i18n.go | 2 + 49 files changed, 2953 insertions(+), 1660 deletions(-) create mode 100644 client/src/__locales/hu.json create mode 100644 client/src/__locales/si-lk.json delete mode 100644 client/src/components/Settings/Dhcp/Form.js create mode 100644 client/src/components/Settings/Dhcp/FormDHCPv4.js create mode 100644 client/src/components/Settings/Dhcp/FormDHCPv6.js create mode 100644 client/src/components/Settings/Dhcp/Interfaces.js delete mode 100644 client/src/components/ui/Accordion.js create mode 100644 client/src/reducers/dashboard.js create mode 100644 client/src/reducers/dhcp.js create mode 100644 client/src/reducers/settings.js diff --git a/.twosky.json b/.twosky.json index e5579669..368a003e 100644 --- a/.twosky.json +++ b/.twosky.json @@ -13,6 +13,7 @@ "hr": "Hrvatski", "id": "Indonesian", "it": "Italiano", + "hu": "Magyar", "no": "Norsk", "pl": "Polski", "pt-br": "Português (BR)", @@ -32,7 +33,8 @@ "zh-tw": "正體中文", "zh-cn": "简体中文", "ko": "한국어", - "th": "ภาษาไทย" + "th": "ภาษาไทย", + "si-lk": "සිංහල" } } ] diff --git a/README.md b/README.md index 3fa1b634..a7ee6ee3 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ You may need to prepare before using these builds: You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls -Please note, that we don't expect people to contribute to both UI and golang parts of the program simultaneously. Ideally, the golang part is implemented first, i.e. configuration, API, and the functionality itself. The UI part can be implemented later in a different pull request by a different person. +Please note that we don't expect people to contribute to both UI and golang parts of the program simultaneously. Ideally, the golang part is implemented first, i.e. configuration, API, and the functionality itself. The UI part can be implemented later in a different pull request by a different person. ### Test unstable versions @@ -235,7 +235,7 @@ There are three options how you can install an unstable version: There are three options how you can install an unstable version. -1. You can either install a beta or edge version of AdGuard Home which we update periodically. If you're already using stable version of AdGuard Home, just replace the executable with a new one. +1. You can either install AdGuard Home from "beta" or "edge" distribution channel which we update periodically. If you're already using stable version of AdGuard Home, just replace the executable file with a new one. 2. You can use the Docker image from the `edge` tag, which is synced with the repo master branch. 3. You can install AdGuard Home from `beta` or `edge` channels on the Snap Store. diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 56b5a7a6..0098e76a 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -1,18 +1,15 @@ { "parser": "babel-eslint", - "extends": [ "plugin:react/recommended", "airbnb-base" ], - "env": { "jest": true, "node": true, "browser": true, "commonjs": true }, - "settings": { "react": { "pragma": "React", @@ -24,35 +21,65 @@ } } }, - "rules": { - "indent": ["error", 4, { - "SwitchCase": 1, - "VariableDeclarator": 1, - "outerIIFEBody": 1, - "FunctionDeclaration": { - "parameters": 1, - "body": 1 - }, - "FunctionExpression": { - "parameters": 1, - "body": 1 - }, - "CallExpression": { - "arguments": 1 - }, - "ArrayExpression": 1, - "ObjectExpression": 1, - "ImportDeclaration": 1, - "flatTernaryExpressions": false, - "ignoredNodes": ["JSXElement", "JSXElement > *", "JSXAttribute", "JSXIdentifier", "JSXNamespacedName", "JSXMemberExpression", "JSXSpreadAttribute", "JSXExpressionContainer", "JSXOpeningElement", "JSXClosingElement", "JSXText", "JSXEmptyExpression", "JSXSpreadChild"], - "ignoreComments": false - }], + "indent": [ + "error", + 4, + { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "FunctionDeclaration": { + "parameters": 1, + "body": 1 + }, + "FunctionExpression": { + "parameters": 1, + "body": 1 + }, + "CallExpression": { + "arguments": 1 + }, + "ArrayExpression": 1, + "ObjectExpression": 1, + "ImportDeclaration": 1, + "flatTernaryExpressions": false, + "ignoredNodes": [ + "JSXElement", + "JSXElement > *", + "JSXAttribute", + "JSXIdentifier", + "JSXNamespacedName", + "JSXMemberExpression", + "JSXSpreadAttribute", + "JSXExpressionContainer", + "JSXOpeningElement", + "JSXClosingElement", + "JSXText", + "JSXEmptyExpression", + "JSXSpreadChild" + ], + "ignoreComments": false + } + ], "class-methods-use-this": "off", "no-shadow": "off", "camelcase": "off", - "no-console": ["warn", { "allow": ["warn", "error"] }], - "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], + "no-console": [ + "warn", + { + "allow": [ + "warn", + "error" + ] + } + ], + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": true + } + ], "import/prefer-default-export": "off", "no-alert": "off" } diff --git a/client/package-lock.json b/client/package-lock.json index bff06737..951dac9a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -7412,11 +7412,21 @@ "dev": true }, "i18next": { - "version": "19.4.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.4.4.tgz", - "integrity": "sha512-ofaHtdsDdX3A5nYur1HWblB7J4hIcjr2ACdnwTAJgc8hTfPbyzZfGX0hVkKpI3vzDIgO6Uzc4v1ffW2W6gG6zw==", + "version": "19.6.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.6.2.tgz", + "integrity": "sha512-Zyd/Z32FY+sD+Eg6sLj5DeDSlrIN3WZ4onuOBRGcjDx/rvodsyUZ9TJ2Y+3aD9Vu8MPbiMU2WesIER/rs1ioyw==", "requires": { - "@babel/runtime": "^7.3.1" + "@babel/runtime": "^7.10.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.5.tgz", + "integrity": "sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } } }, "i18next-browser-languagedetector": { diff --git a/client/package.json b/client/package.json index 41690780..4ab14662 100644 --- a/client/package.json +++ b/client/package.json @@ -18,7 +18,7 @@ "axios": "^0.19.2", "classnames": "^2.2.6", "date-fns": "^1.29.0", - "i18next": "^19.4.4", + "i18next": "^19.6.2", "i18next-browser-languagedetector": "^4.2.0", "ipaddr.js": "^1.9.1", "js-yaml": "^3.14.0", diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 81cc91e0..647ab6fa 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -11,6 +11,8 @@ "save_config": "Save config", "enabled_dhcp": "DHCP server enabled", "disabled_dhcp": "DHCP server disabled", + "unavailable_dhcp": "DHCP is unavailable", + "unavailable_dhcp_desc": "AdGuard Home cannot run a DHCP server on your OS", "dhcp_title": "DHCP server (experimental!)", "dhcp_description": "If your router does not provide DHCP settings, you can use AdGuard's own built-in DHCP server.", "dhcp_enable": "Enable DHCP server", @@ -21,6 +23,8 @@ "dhcp_static_leases": "DHCP static leases", "dhcp_leases_not_found": "No DHCP leases found", "dhcp_config_saved": "DHCP config successfully saved", + "dhcp_ipv4_settings": "DHCP IPv4 Settings", + "dhcp_ipv6_settings": "DHCP IPv6 Settings", "form_error_required": "Required field", "form_error_ip4_format": "Invalid IPv4 format", "form_error_ip6_format": "Invalid IPv6 format", @@ -29,6 +33,7 @@ "form_error_client_id_format": "Invalid client ID format", "form_error_positive": "Must be greater than 0", "form_error_negative": "Must be equal to 0 or greater", + "range_end_error": "Must be greater than range start", "dhcp_form_gateway_input": "Gateway IP", "dhcp_form_subnet_input": "Subnet mask", "dhcp_form_range_title": "Range of IP addresses", @@ -561,7 +566,8 @@ "filter_category_security_desc": "Lists that specialize on blocking malware, phishing or scam domains", "filter_category_regional_desc": "Lists that focus on regional ads and tracking servers", "filter_category_other_desc": "Other blocklists", + "setup_config_to_enable_dhcp_server": "Setup config to enable DHCP server", "original_response": "Original response", "click_to_view_queries": "Click to view queries", "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction on how to resolve this." -} \ No newline at end of file +} diff --git a/client/src/__locales/hu.json b/client/src/__locales/hu.json new file mode 100644 index 00000000..2a99c3c9 --- /dev/null +++ b/client/src/__locales/hu.json @@ -0,0 +1,565 @@ +{ + "client_settings": "Kliens beállítások", + "example_upstream_reserved": "megadhat egy DNS-t <0> felfelé egy adott tartományhoz ", + "upstream_parallel": "A párhuzamos lekérdezések segítségével felgyorsíthatja a megoldást az összes upstream kiszolgáló egyidejű lekérdezésével", + "parallel_requests": "Párhuzamos kérelmek", + "load_balancing": "Terheléselosztás", + "load_balancing_desc": "Egyszerre csak egy szerverről történjen lekérdezés. Az AdGuard Home egy súlyozott, véletlenszerű algoritmust fog használni a megfelelő szerver kiválasztására, így a leggyorsabb szervert gyakrabban fogja használni.", + "bootstrap_dns": "Bootstrap DNS kiszolgálók", + "bootstrap_dns_desc": "A Bootstrap DNS-kiszolgálók a DoH / DoT-megoldók IP-címeinek feloldására szolgálnak", + "check_dhcp_servers": "Ellenőrizze a DHCP-kiszolgálókat", + "save_config": "Konfiguráció mentése", + "enabled_dhcp": "A DHCP-kiszolgáló engedélyezve van", + "disabled_dhcp": "A DHCP-kiszolgáló le van tiltva", + "dhcp_title": "DHCP-kiszolgáló (kísérleti!)", + "dhcp_description": "Ha az útválasztó nem nyújt DHCP-beállításokat, akkor az AdGuard saját beépített DHCP-kiszolgálóját használhatod", + "dhcp_enable": "A DHCP-kiszolgáló engedélyezése", + "dhcp_disable": "A DHCP-kiszolgáló letiltása", + "dhcp_not_found": "Biztonságos a beépített DHCP-kiszolgáló engedélyezése - nem találtunk aktív DHCP-kiszolgálókat a hálózaton. Javasoljuk azonban, hogy kézzel ellenőrizze, hogy az automatikus tesztünk jelenleg nem ad 100% -os garanciát.", + "dhcp_found": "Aktív DHCP-kiszolgáló található a hálózaton. Nem biztonságos a beépített DHCP-kiszolgáló engedélyezése.", + "dhcp_leases": "DHCP bérlés", + "dhcp_static_leases": "Statikus DHCP", + "dhcp_leases_not_found": "Nem találhatóak DHCP kliensek", + "dhcp_config_saved": "DHCP beállítások elmentve", + "form_error_required": "Kötelező mező", + "form_error_ip4_format": "Helytelen IPv4 formátum", + "form_error_ip6_format": "Érvénytelen IPv6 formátum", + "form_error_ip_format": "Érvénytelen IPv4 formátum", + "form_error_mac_format": "Érvénytelen MAC formátum", + "form_error_client_id_format": "Érvénytelen kliens ID formátum", + "form_error_positive": "Legfeljebb nulla legyen", + "form_error_negative": "Legalább 0-nak kell lennie", + "dhcp_form_gateway_input": "Átjáró IP", + "dhcp_form_subnet_input": "Alhálózati maszk", + "dhcp_form_range_title": "IP címtartomány", + "dhcp_form_range_start": "Tartomány kezdete", + "dhcp_form_range_end": "Tartomány vége", + "dhcp_form_lease_title": "DHCP bérlési ideje (másodpercben)", + "dhcp_form_lease_input": "Bérlési idő", + "dhcp_interface_select": "Válaszd ki a DHCP interface-t", + "dhcp_hardware_address": "Hardvercím", + "dhcp_ip_addresses": "Ip címek", + "ip": "IP", + "dhcp_table_hostname": "Host név", + "dhcp_table_expires": "Lejár", + "dhcp_warning": "Ha egyébként engedélyezni szeretné a DHCP-kiszolgálót, győződjön meg arról, hogy nincs-e más aktív DHCP-kiszolgáló a hálózaton. Ellenkező esetben a csatlakoztatott eszközöket megszakíthatja az interneten!", + "dhcp_error": "Nem tudtuk meghatározni, hogy van-e másik DHCP-kiszolgáló a hálózaton.", + "dhcp_static_ip_error": "A DHCP-kiszolgáló használatához statikus IP-címet kell beállítani. Nem sikerült meghatározni, hogy ez a hálózati interfész statikus IP-cím használatával van-e beállítva. Állítsa be kézzel egy statikus IP-címet.", + "dhcp_dynamic_ip_found": "A rendszer dinamikus IP-címkonfigurációt használ az <0> {{interfaceName}} interfészhez. A DHCP-kiszolgáló használatához statikus IP-címet kell beállítani. Jelenlegi IP-címe <0> {{ipAddress}} . Ha automatikusan megnyomja az Enable DHCP gombot, automatikusan beállítjuk ezt az IP-címet statikusnak.", + "dhcp_lease_added": "Statikus bérlet \"{{key}}\" sikeresen hozzáadva", + "dhcp_lease_deleted": "Statikus bérlet \"{{key}}\" sikeresen törölve", + "dhcp_new_static_lease": "Új statikus bérlet", + "dhcp_static_leases_not_found": "Nincs DHCP statikus bérlet", + "dhcp_add_static_lease": "Statikus bérlet hozzáadása", + "dhcp_reset": "Biztosan visszaállítod a DHCP beállításokat?", + "country": "Ország", + "city": "Város", + "delete_confirm": "Biztosan törli a \"{{key}}\" -t?", + "form_enter_hostname": "Adja meg a hosztnevet", + "error_details": "Hiba részletei", + "response_details": "Válasz adatai", + "request_details": "Kérés adatai", + "client_details": "Kliens részletei", + "details": "Részletek", + "back": "Vissza", + "dashboard": "Irányítópult", + "settings": "Beállítások", + "filters": "Szűrők", + "filter": "Szűrő", + "query_log": "Lekérdezési napló", + "compact": "Kompakt", + "nothing_found": "Nincs találat", + "faq": "GYIK", + "version": "verzió", + "address": "Cím", + "protocol": "Protokoll", + "on": "Be", + "off": "Ki", + "copyright": "Szerzői jog", + "homepage": "Honlap", + "report_an_issue": "Probléma bejelentése", + "privacy_policy": "Adatvédelmi irányelvek", + "enable_protection": "Védelem engedélyezése", + "enabled_protection": "Engedélyezett védelem", + "disable_protection": "Védelem letiltása", + "disabled_protection": "Letiltott védelem", + "refresh_statics": "Statisztikák frissítése", + "dns_query": "DNS lekérdezések", + "blocked_by": "<0>Szűrők által blokkolt", + "stats_malware_phishing": "Blokkolt vírusok/adathalászat", + "stats_adult": "Blokkolt felnőtt tartalmak", + "stats_query_domain": "A legjobban lekérdezett területek", + "for_last_24_hours": "Utolsó 24 órában", + "for_last_days": "a legutolsó {{count}} napra", + "for_last_days_plural": "a legutolsó {{count}} napra", + "no_domains_found": "Nem található domain", + "requests_count": "Kérések száma", + "top_blocked_domains": "A legjobban blokkolt tartományok", + "top_clients": "Legaktívabb kliensek", + "no_clients_found": "Nem található kliens", + "general_statistics": "Általános statisztikák", + "number_of_dns_query_days": "Lekérdezések száma az utolsó {{count}} napban", + "number_of_dns_query_days_plural": "Feldolgozott lekérdezések száma az utolsó {{count}} napban", + "number_of_dns_query_24_hours": "Számos DNS lekérdezések feldolgozása az elmúlt 24 órában", + "number_of_dns_query_blocked_24_hours": "Számos DNS-kérés blokkolva van az adblock-szűrők által, és blokklistákat tartalmaz", + "number_of_dns_query_blocked_24_hours_by_sec": "Számos DNS-kérés blokkolva van az AdGuard böngésző biztonsági moduljában", + "number_of_dns_query_blocked_24_hours_adult": "Blokkolt felnőtt tartalmak száma", + "enforced_save_search": "Erősített biztonságos keresés", + "number_of_dns_query_to_safe_search": "Számos DNS-kérelem olyan keresőmotorokhoz, amelyekre a Biztonságos keresés érvényesült", + "average_processing_time": "Átlagos feldolgozási idő", + "average_processing_time_hint": "Átlagos idő milliszekundumban a DNS-kérelem feldolgozásakor", + "block_domain_use_filters_and_hosts": "A tartományok blokkolása szűrőkkel és házigazdákkal", + "filters_block_toggle_hint": "A Szűrők beállításaiban beállíthatja a blokkolási szabályokat.", + "use_adguard_browsing_sec": "Használja az AdGuard böngészés biztonsági webszolgáltatását", + "use_adguard_browsing_sec_hint": "Az AdGuard Home ellenőrzi, hogy a böngésző biztonsági webszolgáltatás a tartományt feketelistára tette-e. Az ellenőrzés elvégzéséhez adatvédelmi keresési API-t fog használni: az SHA256-os hash-név csak egy rövid előtagot küld a kiszolgálónak.", + "use_adguard_parental": "Használja az AdGuard szülői felügyelet webszolgáltatását", + "use_adguard_parental_hint": "Az AdGuard Home ellenőrzi, hogy a domain tartalmaz-e felnőtt anyagokat. Ugyanazokat az adatvédelmi API-kat használja, mint a böngésző biztonsági webszolgáltatás.", + "enforce_safe_search": "A biztonságos keresés végrehajtása", + "enforce_save_search_hint": "Az AdGuard Home a következő keresőmotorokban biztosíthatja a biztonságos keresést: Google, Youtube, Bing, DuckDuckGo és Yandex.", + "no_servers_specified": "Nincsenek megadott kiszolgálók", + "general_settings": "Általános beállítások", + "dns_settings": "DNS beállítások", + "dns_blocklists": "DNS blokkolási listák", + "dns_allowlists": "DNS engedélyezési listák", + "dns_blocklists_desc": "Az AdGuard Home blokkolni fogja azokat a domaineket, amik szerepelnek a blokkolási listán.", + "dns_allowlists_desc": "A DNS engedélyezési listán szereplő domainek engedélyezve lesznek, akkor is, ha szerepelnek bármelyik blokkolási listán.", + "custom_filtering_rules": "Egyéni szűrési szabályok", + "encryption_settings": "Titkosítási beállítások", + "dhcp_settings": "DHCP beállítások", + "upstream_dns": "Upstream DNS-kiszolgálók", + "upstream_dns_hint": "Ha üresen hagyod ezt a mezőt, az AdGuard a(z) Quad9-t fogja használni.", + "test_upstream_btn": "Upstreamek tesztelése", + "upstreams": "Feltöltés", + "apply_btn": "Alkalmaz", + "disabled_filtering_toast": "Letiltott szűrés", + "enabled_filtering_toast": "Engedélyezett szűrés", + "disabled_safe_browsing_toast": "Letiltott biztonságos böngészés", + "enabled_safe_browsing_toast": "Engedélyezett biztonságos böngészés", + "disabled_parental_toast": "Letiltott szülői felügyelet", + "enabled_parental_toast": "Engedélyezett szülői felügyelet", + "disabled_safe_search_toast": "Letiltott biztonságos keresés", + "enabled_save_search_toast": "Engedélyezett biztonságos keresés", + "enabled_table_header": "Engedélyezve", + "name_table_header": "Név", + "list_url_table_header": "Lista URL-je", + "rules_count_table_header": "Szabályok száma", + "last_time_updated_table_header": "Utoljára frissítve", + "actions_table_header": "Akciók", + "request_table_header": "Kérelem", + "edit_table_action": "Szerkesztés", + "delete_table_action": "Törlés", + "elapsed": "Eltelt időtartam", + "filters_and_hosts_hint": "Az AdGuard Home megérti az alapvető adblock szabályokat és a fájlokat tartalmazó szintaxist.", + "no_blocklist_added": "Nincsnek blokkolási listák hozzáadva", + "no_whitelist_added": "Nincsenek engedélyezési listák hozzáadva", + "add_blocklist": "Blokkolási lista hozzáadása", + "add_allowlist": "Engedélyezési lista hozzáadása", + "cancel_btn": "Megszünteti", + "enter_name_hint": "Adja meg a nevet", + "enter_url_or_path_hint": "Írjon be egy URL-t vagy egy útvonalat a listához", + "check_updates_btn": "Frissítések keresése", + "new_blocklist": "Új blokkolási lista", + "new_allowlist": "Új engedélyezési lista", + "edit_blocklist": "Blokkolási lista módosítása", + "edit_allowlist": "Engedélyezési lista módosítása", + "choose_blocklist": "Blokkolási lista választás", + "choose_allowlist": "Engedélyezési lista választás", + "enter_valid_blocklist": "Adjon meg egy érvényes URL-t a blokkolási listához.", + "enter_valid_allowlist": "Adjon meg egy érvényes URL-t az engedélyezési listához.", + "form_error_url_format": "Egyedi URL formátum", + "form_error_url_or_path_format": "Helytelen URL vagy elérési út a listához", + "custom_filter_rules": "Egyéni szűrési szabályok", + "custom_filter_rules_hint": "Adjon meg egy szabályt egy sorban. Használhatja az adblock szabályokat vagy a fájlokat tartalmazó szintaxist.", + "examples_title": "Példák", + "example_meaning_filter_block": "blokkolja a example.org domain és az összes aldomain hozzáférését", + "example_meaning_filter_whitelist": "example.org tartomány és az összes aldomain hozzáférésének feloldása", + "example_meaning_host_block": "Az AdGuard Home most visszatér a 127.0.0.1 címre a example.org domainhez (de nem az aldomainjeihez).", + "example_comment": "! Ide írhat egy megjegyzést", + "example_comment_meaning": "Csak egy megjegyzés", + "example_comment_hash": "# Megjegyzés is", + "example_regex_meaning": "megakadályozza a hozzáférést a reguláris kifejezéssel egyező domainek-nél", + "example_upstream_regular": "rendszeres DNS (UDP felett)", + "example_upstream_dot": "titkosított <0>DNS-over-TLS", + "example_upstream_doh": "titkosított <0>DNS-over-HTTPS", + "example_upstream_sdns": "a <0> DNS bélyegek használatával <1> DNSCrypt vagy <2> DNS-over-HTTPS ", + "example_upstream_tcp": "hagyományos DNS (TCP felett)", + "all_lists_up_to_date_toast": "Már minden lista naprakész", + "updated_upstream_dns_toast": "Frissítette az upstream DNS-kiszolgálókat", + "dns_test_ok_toast": "A megadott DNS-kiszolgálók megfelelően működnek", + "dns_test_not_ok_toast": "Szerver \"{{key}}\": nem használható, ellenőrizze, hogy helyesen írta-e be", + "unblock": "Blokkolás feloldása", + "block": "Blokkolás", + "time_table_header": "Idő", + "date": "Dátum", + "domain_name_table_header": "Domain név", + "domain_or_client": "Webcím vagy kliens", + "type_table_header": "Típus", + "response_table_header": "Válasz", + "response_code": "Válaszkód", + "client_table_header": "Kliens", + "empty_response_status": "Üres", + "show_all_filter_type": "Mutasd az összeset", + "show_filtered_type": "Szűrés megjelenítése", + "no_logs_found": "Nem található napló", + "refresh_btn": "Frissítés", + "previous_btn": "Előző", + "next_btn": "Következő", + "loading_table_status": "Töltés...", + "page_table_footer_text": "Oldal", + "rows_table_footer_text": "sor", + "updated_custom_filtering_toast": "Egyéni szűrési módok frissítése", + "rule_removed_from_custom_filtering_toast": "Szabály eltávolítva az egyedi szűrési módok közül", + "rule_added_to_custom_filtering_toast": "Szabály hozzáadva az egyedi szűrési módokhoz", + "query_log_response_status": "Státusz: {{value}}", + "query_log_filtered": "{{filter}} által szűrve", + "query_log_confirm_clear": "Biztosan törlöd a lekérdezési naplót?", + "query_log_cleared": "A lekérdezési napló sikeresen törölve", + "query_log_updated": "A lekérdezési napló sikeresen frissítve lett", + "query_log_clear": "Lekérdezési napló törlése", + "query_log_retention": "Lekérdezési naplók megtartása", + "query_log_enable": "Naplózás engedélyezése", + "query_log_configuration": "Naplózás beállítása", + "query_log_disabled": "Lekérdezési napló kikapcsolva. Bekapcsolható a <0>Beállításokban", + "query_log_strict_search": "Használj \"dupla idézőjelet\" a pontos kereséshez", + "query_log_retention_confirm": "Biztos benne, hogy megváltoztatja a kérések naplójának megőrzési idejét? Ha csökkentette az értéket, a megadottnál korábbi adatok elvesznek", + "anonymize_client_ip": "Kliens IP-címének anonimizálása", + "anonymize_client_ip_desc": "Ne mentse el a kliens teljes IP-címét a naplókban és a statisztikákban", + "dns_config": "DNS szerver beállításai", + "dns_cache_config": "DNS gyorsítótár beállításai", + "dns_cache_config_desc": "Itt tudja konfigurálni a DNS gyorsítótárat", + "blocking_mode": "Blokkolás módja", + "default": "Alapértelmezett", + "nxdomain": "NXDOMAIN", + "null_ip": "Null IP-cím", + "custom_ip": "Egyedi IP", + "blocking_ipv4": "IPv4 blokkolása", + "blocking_ipv6": "IPv6 blokkolása", + "dns_over_https": "DNS-over-HTTPS", + "dns_over_tls": "DNS-over-TLS", + "plain_dns": "Egyszerű DNS", + "form_enter_rate_limit": "Adja meg a kérések maximális számát", + "rate_limit": "Kérések korlátozása", + "edns_enable": "EDNS kliens alhálózat engedélyezése", + "edns_cs_desc": "Ha engedélyezve van, az AdGuard Home a kliensek alhálózatait küldi el a DNS-kiszolgálóknak.", + "rate_limit_desc": "Maximálisan hány kérést küldhet egy kliens másodpercenként (0: korlátlan)", + "blocking_ipv4_desc": "A blokkolt A kéréshez visszaadandó IP-cím", + "blocking_ipv6_desc": "A blokkolt AAAA kéréshez visszaadandó IP-cím", + "blocking_mode_default": "Alapértelmezés: Adblock-stílusú szabály esetén NXDOMAIN válasz küldése, /etc/hosts-stílusú szabály esetén pedig a szabályban meghatározott IP-címmel való válasz küldése", + "blocking_mode_nxdomain": "NXDOMAIN: Az NXDOMAIN kóddal fog válaszolni", + "blocking_mode_null_ip": "Null IP: Nullákból álló IP-címmel válaszol (0.0.0.0 for A; :: for AAAA)", + "blocking_mode_custom_ip": "Egyedi IP: Válasz egy kézzel beállított IP címmel", + "upstream_dns_client_desc": "Ha üresen hagyja ezt a mezőt, az AdGuard Home azokat a szervereket fogja használni, amik a <0>DNS beállításokban vannak beállítva.", + "tracker_source": "Követő forrása", + "source_label": "Forrás", + "found_in_known_domain_db": "Benne van az ismert domainek listájában.", + "category_label": "Kategória", + "rule_label": "Szabály", + "list_label": "Lista", + "unknown_filter": "Ismeretlen szűrő: {{filterId}}", + "known_tracker": "Ismert követő", + "install_welcome_title": "Üdvözli az AdGuard Home!", + "install_welcome_desc": "Az AdGuard Home egy, a teljes hálózatot lefedő hirdetés és követő blokkoló DNS szerver. Az a célja, hogy lehetővé tegye a teljes hálózat és az összes eszköz vezérlését, és nem igényel kliensoldali programot.", + "install_settings_title": "Webes admin felület", + "install_settings_listen": "Figyelő felület", + "install_settings_port": "Port", + "install_settings_interface_link": "Az AdGuard Home webes admin felülete elérhető a következő címe(ke)n:", + "form_error_port": "Adjon meg egy érvényes portot", + "install_settings_dns": "DNS szerver", + "install_settings_dns_desc": "Be kell állítania az eszközeit vagy a routerét, hogy használni tudja a DNS szervert a következő címeken:", + "install_settings_all_interfaces": "Minden felület", + "install_auth_title": "Hitelesítés", + "install_auth_desc": "Erősen ajánlott a jelszavas hitelesítés beállítása az AdGuard Home webes admin felületéhez. Még akkor is, ha csak a helyi hálózaton érhető el, óvja meg az illetéktelen hozzáférésektől.", + "install_auth_username": "Felhasználónév", + "install_auth_password": "Jelszó", + "install_auth_confirm": "Jelszó megerősítése", + "install_auth_username_enter": "Felhasználónév megadása", + "install_auth_password_enter": "Jelszó megadása", + "install_step": "Lépés", + "install_devices_title": "Állítsa be az eszközeit", + "install_devices_desc": "Az AdGuard Home használatának megkezdéséhez be kell állítania az eszközeit, hogy azok használni tudják.", + "install_submit_title": "Gratulálunk!", + "install_submit_desc": "A telepítési folyamat befejeződött, minden készen áll az AdGuard Home használatára.", + "install_devices_router": "Router", + "install_devices_router_desc": "Ez a beállítás lefed minden eszközt, amik az Ön routeréhez csatlakoznak, így azokat nem kell külön, kézzel beállítania.", + "install_devices_address": "Az AdGuard DNS szerver a következő címeket figyeli", + "install_devices_router_list_1": "Nyissa meg a router beállításait. Ez általában a böngészőn keresztül történik egy URL megadásával (pl. http://192.168.0.1/ vagy http://192.168.1.1/). Ez az oldal valószínűleg felhasználónevet és jelszót fog kérni. Ha nem tudja a belépési adatokat, nézze meg a router dobozát, a router alján levő fehér címkét vagy a technikai dokumentációt az interneten, végső esetben pedig visszaállíthatja a routert. Néhány router speciális alkalmazást igényel, amik lehetséges, hogy már telepítve vannak a számítógépére vagy a mobil eszközére.", + "install_devices_router_list_2": "Keresse meg a DHCP/DNS beállításokat. Keresse a DNS szót egy olyan mező mellett, amely egy 4 csoportból álló, 1-3 számjegyű számsort vár.", + "install_devices_router_list_3": "Adja meg az AdGuard Home szerver címét itt.", + "install_devices_router_list_4": "Bizonyos típusú routereknél nem állíthat be egyéni DNS-kiszolgálót. Ebben az esetben segíthet, ha az AdGuard Home-t DHCP-szerverként állítja be. Ellenkező esetben keresse meg az adott router kézikönyvében a DNS-kiszolgálók testreszabását.", + "install_devices_windows_list_1": "Nyissa meg a Vezérlőpultot a Start menün vagy a Windows keresőn keresztül.", + "install_devices_windows_list_2": "Válassza a Hálózat és internet kategóriát, majd pedig a Hálózati és megosztási központot.", + "install_devices_windows_list_3": "A képernyő bal oldalán keresse meg az Adapterbeállítások módosítása lehetőséget és kattintson rá.", + "install_devices_windows_list_4": "Válassza ki a jelenleg is használt kapcsolatot, majd jobb egérgombbal kattintson rá és a megjelenő menüből válassza a Tulajdonságok elemet.", + "install_devices_windows_list_5": "Keresse meg az Internet Protocol Version 4 (TCP/IP) elemet a listában, válassza ki, majd ismét kattintson a Tulajdonságokra.", + "install_devices_windows_list_6": "Válassza a Következő DNS címek használata lehetőséget és adja meg az AdGuard Home szerver címeit.", + "install_devices_macos_list_1": "Kattintson az Apple ikonra és válassza a Rendszerbeállításokat.", + "install_devices_macos_list_2": "Kattintson a Hálózat lehetőségre.", + "install_devices_macos_list_3": "Válassza ki az első kapcsolatot a listából és kattintson a Haladó beállításokra.", + "install_devices_macos_list_4": "Válassza ki a DNS fület és adja meg az AdGuard Home szerver címeit.", + "install_devices_android_list_1": "Az Android kezdőképernyőjén érintse meg a Beállítások gombot.", + "install_devices_android_list_2": "Érintse meg a Wi-Fi gombot a menüben. Ekkor a képernyőre kerül az összes elérhető hálózat (mobilinternethez nem lehet egyedi DNS-t megadni).", + "install_devices_android_list_3": "Nyomjon hosszan arra a hálózatra a listából, amelyikre éppen csatlakozva van, majd válassza a Hálózat módosítása lehetőséget.", + "install_devices_android_list_4": "Egyes eszközökön előfordulhat, hogy a további beállítások megtekintéséhez a Speciális/haladó beállítások részt kell megnyitni. Az Android DNS-beállításainak módosításához ekkor az IP-beállításokat DHCP-ről statikusra kell váltania.", + "install_devices_android_list_5": "Változtassa meg a DNS 1 és a DNS 2 értékét az AdGuard Home szerver címeire.", + "install_devices_ios_list_1": "A kezdőképernyőn érintse meg a Beállítások gombot.", + "install_devices_ios_list_2": "Válassza ki a Wi-Fi-t a bal oldali menüből (mobilinternetnél nem lehetséges a DNS beállítása).", + "install_devices_ios_list_3": "Érintse meg a jelenleg használt hálózat nevét.", + "install_devices_ios_list_4": "A DNS mezőbe adja meg az AdGuard Home szerver címeit.", + "get_started": "Kezdés", + "next": "Következő", + "open_dashboard": "Irányítópult megnyitása", + "install_saved": "Sikeres mentés", + "encryption_title": "Titkosítás", + "encryption_desc": "Titkosítás (HTTPS/TLS) támogatása mind a DNS, mind pedig a webes admin felület számára", + "encryption_config_saved": "Titkosítási beállítások mentve", + "encryption_server": "Szerver neve", + "encryption_server_enter": "Adja meg az Ön domain címét", + "encryption_server_desc": "A HTTPS használatához be kell írnia egy, az SSL-tanúsítvánnyal megegyező kiszolgálónevet.", + "encryption_redirect": "Automatikus átirányítás HTTPS kapcsolatra", + "encryption_redirect_desc": "Ha be van jelölve, az AdGuard Home automatikusan átirányítja a HTTP kapcsolatokat a biztonságos HTTPS protokollra.", + "encryption_https": "HTTPS port", + "encryption_https_desc": "Ha a HTTPS port konfigurálva van, akkor az AdGuard Home admin felülete elérhető lesz a HTTPS-en keresztül, és ezenkívül DNS-over-HTTPS-t is biztosít a '/dns-query' helyen.", + "encryption_dot": "DNS-over-TLS port", + "encryption_dot_desc": "Ha ez a port be van állítva, az AdGuard Home DNS-over-TLS szerverként tud futni ezen a porton.", + "encryption_certificates": "Tanúsítványok", + "encryption_certificates_desc": "A titkosítás használatához érvényes SSL tanúsítványláncot kell megadnia a domainjéhez. Ingyenes tanúsítványt kaphat a <0>{{link}} webhelyen, vagy megvásárolhatja az egyik megbízható tanúsítványkibocsátó hatóságtól.", + "encryption_certificates_input": "Másolja be ide a PEM-kódolt tanúsítványt.", + "encryption_status": "Állapot", + "encryption_expire": "Lejár", + "encryption_key": "Privát kulcs", + "encryption_key_input": "Másolja ki és illessze be ide a tanúsítványa PEM-kódolt privát kulcsát.", + "encryption_enable": "Titkosítás engedélyezése (HTTPS, DNS-over-HTTPS, és DNS-over-TLS)", + "encryption_enable_desc": "Ha a titkosítás engedélyezve van, az AdGuard Home admin felülete működik HTTPS-en keresztül, és a DNS szerver is várja a kéréseket DNS-over-HTTPS-en, valamint DNS-over-TLS-en keresztül.", + "encryption_chain_valid": "A tanúsítványlánc érvényes", + "encryption_chain_invalid": "A tanúsítványlánc érvénytelen", + "encryption_key_valid": "Ez egy érvényes {{type}} privát kulcs", + "encryption_key_invalid": "Ez egy érvénytelen {{type}} privát kulcs", + "encryption_subject": "Tárgy", + "encryption_issuer": "Kibocsátó", + "encryption_hostnames": "Hosztnevek", + "encryption_reset": "Biztosan visszaállítja a titkosítási beállításokat?", + "topline_expiring_certificate": "Az SSL-tanúsítványa hamarosan lejár. Frissítse a <0>Titkosítási beállításokat.", + "topline_expired_certificate": "Az SSL-tanúsítványa lejárt. Frissítse a <0>Titkosítási beállításokat.", + "form_error_port_range": "A port értékét a 80-65535 tartományban adja meg", + "form_error_port_unsafe": "Ez a port nem biztonságos", + "form_error_equal": "Nem egyezhetnek", + "form_error_password": "A jelszavak nem egyeznek", + "reset_settings": "Beállítások visszaállítása", + "update_announcement": "Az AdGuard Home {{version}} verziója elérhető! <0>Kattintson ide további információkért.", + "setup_guide": "Beállítási útmutató", + "dns_addresses": "DNS címek", + "dns_start": "A DNS szerver indul", + "dns_status_error": "Hiba történt a DNS szerver állapotának ellenőrzésekor", + "down": "Nem elérhető", + "fix": "Állandó", + "dns_providers": "Itt van az <0>ismert DNS szolgáltatók listája, amelyekből választhat.", + "update_now": "Frissítés most", + "update_failed": "Az automatikus frissítés nem sikerült. Kérjük, hogy kövesse ezeket a lépéseket a manuális frissítéshez.", + "processing_update": "Kérjük várjon, az AdGuard Home frissítése folyamatban van", + "clients_title": "Kliensek", + "clients_desc": "Az AdGuard Home-hoz csatlakozó eszközök kezelése", + "settings_global": "Globális", + "settings_custom": "Egyéni", + "table_client": "Kliens", + "table_name": "Név", + "save_btn": "Mentés", + "client_add": "Kliens hozzáadása", + "client_new": "Új kliens", + "client_edit": "Kliens módosítása", + "client_identifier": "Azonosító", + "ip_address": "IP cím", + "client_identifier_desc": "A klienseket be lehet azonosítani IP-cím, CIDR, valamint MAC-cím alapján. Kérjük, vegye figyelembe, hogy a MAC-cím alapján történő azonosítás csak akkor működik, ha az AdGuard Home egyben <0>DHCP szerverként is funkcionál", + "form_enter_ip": "IP-cím megadása", + "form_enter_mac": "MAC-cím megadása", + "form_enter_id": "Azonosító megadása", + "form_add_id": "Azonosító hozzáadása", + "form_client_name": "Adja meg a kliens nevét", + "name": "Név", + "client_global_settings": "Globális beállítások használata", + "client_deleted": "A(z) \"{{key}}\" kliens sikeresen el lett távolítva", + "client_added": "A(z) \"{{key}}\" kliens sikeresen hozzá lett adva", + "client_updated": "A(z) \"{{key}}\" kliens sikeresen frissítve lett", + "clients_not_found": "Nem található kliens", + "client_confirm_delete": "Biztosan törölni szeretné az \"{{key}}\" klienst?", + "list_confirm_delete": "Biztosan törölni kívánja ezt a listát?", + "auto_clients_title": "Kliensek (futási idő)", + "auto_clients_desc": "Az AdGuard Home-ot használó, de a konfigurációban nem tárolt ügyfelek adatai", + "access_title": "Hozzáférési beállítások", + "access_desc": "Itt konfigurálhatja az AdGuard Home DNS-kiszolgáló hozzáférési szabályait.", + "access_allowed_title": "Engedélyezett kliensek", + "access_allowed_desc": "A CIDR vagy IP címek listája. Ha konfigurálva van, az AdGuard Home csak az alábbi IP-címekről fogadja el a kéréseket.", + "access_disallowed_title": "Nem engedélyezett kliensek", + "access_disallowed_desc": "A CIDR vagy IP címek listája. Ha konfigurálva van, az AdGuard Home ebből az IP-címből lekéri a kéréseket.", + "access_blocked_title": "Blokkolt tartományok", + "access_blocked_desc": "Ne keverje össze ezt a szűrőkkel. Az AdGuard Home a DNS-lekérdezéseket a lekérdezés kérdésében el fogja hagyni ezeken a tartományokon", + "access_settings_saved": "A hozzáférési beállítások sikeresen mentésre kerültek", + "updates_checked": "A frissítések sikeresen ellenőrizve lettek", + "updates_version_equal": "Az AdGuard Home naprakész", + "check_updates_now": "Frissítések ellenőrzése most", + "dns_privacy": "DNS Adatvédelem", + "setup_dns_privacy_1": "<0>DNS-over-TLS: Használja a(z) <1>{{address}} szöveget.", + "setup_dns_privacy_2": "<0>DNS-over-HTTPS: Használja a(z) <1>{{address}} szöveget.", + "setup_dns_privacy_3": "<0>Kérjük, vegye figyelembe, hogy a titkosított DNS protokollok csak Android 9-től vannak támogatva. Tehát további szoftvert kell telepítenie más operációs rendszerekhez.<0>Itt megtalálja azon szoftverek listáját, amelyeket használhat.", + "setup_dns_privacy_android_1": "Az Android 9 natív módon támogatja a DNS-over-TLS-t. A beállításához menjen a Beállítások → Hálózat & internet → Speciális → Privát DNS menübe, és adja meg itt a domaint.", + "setup_dns_privacy_android_2": "Az <0>AdGuard for Android támogatja a <1>DNS-over-HTTPS-t és a <1>DNS-over-TLS-t.", + "setup_dns_privacy_android_3": "Az <0>Intra hozzáadja a <1>DNS-over-HTTPS támogatást az Androidhoz.", + "setup_dns_privacy_ios_1": "A <0>DNSCloak támogatja a <1>DNS-over-HTTPS-t, de ahhoz, hogy a saját szerverhez konfigurálhassa, létre kell hoznia egy <2>DNS bélyeget hozzá.", + "setup_dns_privacy_ios_2": "Az <0>AdGuard for iOS támogatja a <1>DNS-over-HTTPS és a <1>DNS-over-TLS beállításokat.", + "setup_dns_privacy_other_title": "Egyéb megvalósítások", + "setup_dns_privacy_other_1": "Maga az AdGuard Home bármilyen platformon biztonságos DNS-kliens lehet.", + "setup_dns_privacy_other_2": "A <0>dnsproxy támogatja az összes ismert biztonságos DNS protokollt.", + "setup_dns_privacy_other_3": "A <0>dnscrypt-proxy támogatja a <1>DNS-over-HTTPS-t.", + "setup_dns_privacy_other_4": "A <0>Mozilla Firefox támogatja a <1>DNS-over-HTTPS-t.", + "setup_dns_privacy_other_5": "További megvalósításokat találhat <0>ide és <1>ide kattintva.", + "setup_dns_notice": "Ahhoz, hogy a <1>DNS-over-HTTPS vagy a <1>DNS-over-TLS valamelyikét használja, muszáj <0>beállítania a titkosítást az AdGuard Home beállításaiban.", + "rewrite_added": "DNS átírás a(z) \"{{key}}\" kulcshoz sikeresen hozzáadva", + "rewrite_deleted": "DNS átírás a(z) \"{{key}}\" kulcshoz sikeresen törölve", + "rewrite_add": "DNS átírás hozzáadása", + "rewrite_not_found": "Nem találhatók DNS átírások", + "rewrite_confirm_delete": "Biztosan törölni szeretné a DNS átírást ehhez: \"{{key}}\"?", + "rewrite_desc": "Lehetővé teszi, hogy egyszerűen beállítson egyéni DNS választ egy adott domain névhez.", + "rewrite_applied": "Alkalmazott átírási szabály", + "rewrite_hosts_applied": "Átírt a gazda fájl szabálya által", + "dns_rewrites": "DNS átírások", + "form_domain": "Adja meg a domain nevet vagy a helyettesítő karaktert", + "form_answer": "Adjon meg egy IP-címet vagy egy domain nevet", + "form_error_domain_format": "Érvénytelen domain formátum", + "form_error_answer_format": "Érvénytelen válasz formátum", + "configure": "Beállítás", + "main_settings": "Fő beállítások", + "block_services": "Speciális szolgáltatások blokkolása", + "blocked_services": "Blokkolt szolgáltatások", + "blocked_services_desc": "Lehetővé teszi a népszerű oldalak és szolgáltatások blokkolását.", + "blocked_services_saved": "Blokkolt szolgáltatások sikeresen mentve", + "blocked_services_global": "A globálisan tiltott szolgáltatások használata", + "blocked_service": "Blokkolt szolgáltatás", + "block_all": "Összes blokkolása", + "unblock_all": "Összes feloldása", + "encryption_certificate_path": "Tanúsítvány útvonala", + "encryption_private_key_path": "Privát kulcs útvonala", + "encryption_certificates_source_path": "Tanúsítványfájl útvonalának megadása", + "encryption_certificates_source_content": "Tanúsítvány tartalmának megadása", + "encryption_key_source_path": "Privát kulcsfájl beállítása", + "encryption_key_source_content": "Privát kulcs tartalmának megadása", + "stats_params": "Statisztikai beállítások", + "config_successfully_saved": "A beállítások sikeresen el lettek mentve", + "interval_24_hour": "24 óra", + "interval_days": "{{count}} nap", + "interval_days_plural": "{{count}} nap", + "domain": "Domain", + "answer": "Válasz", + "filter_added_successfully": "A lista sikeresen hozzá lett adva", + "filter_removed_successfully": "A lista sikeresen el lett távolítva", + "filter_updated": "A lista sikeresen frissítve lett", + "statistics_configuration": "Statisztikai beállítások", + "statistics_retention": "Statisztika megőrzése", + "statistics_retention_desc": "Ha csökkenti az intervallum értékét, az előtte levő adatok elvesznek", + "statistics_clear": " Statisztikák visszaállítása", + "statistics_clear_confirm": "Biztosan vissza akarja állítani a statisztikákat?", + "statistics_retention_confirm": "Biztos benne, hogy megváltoztatja a statisztika megőrzési idejét? Ha csökkentette az értéket, a megadottnál korábbi adatok elvesznek", + "statistics_cleared": "A statisztikák sikeresen vissza lettek állítva", + "interval_hours": "{{count}} óra", + "interval_hours_plural": "{{count}} óra", + "filters_configuration": "Szűrők beállításai", + "filters_enable": "Szűrők engedélyezése", + "filters_interval": "Szűrőfrissítési gyakoriság", + "disabled": "Kikapcsolva", + "username_label": "Felhasználónév", + "username_placeholder": "Felhasználónév megadása", + "password_label": "Jelszó", + "password_placeholder": "Jelszó megadása", + "sign_in": "Bejelentkezés", + "sign_out": "Kijelentkezés", + "forgot_password": "Elfelejtette a jelszót?", + "forgot_password_desc": "Kérjük, hogy kövesse <0>ezeket a lépéseket a jelszó visszaállításához.", + "location": "Helyzet", + "orgname": "Szervezet neve", + "netname": "Hálózat neve", + "network": "Hálózat", + "descr": "Leírás", + "whois": "Whois", + "filtering_rules_learn_more": "<0>Tudjon meg többet a saját hosztlisták létrehozásáról.", + "blocked_by_response": "Blokkolva a CNAME vagy a válasz IP-címe alapján", + "blocked_by_cname_or_ip": "CNAME vagy IP által blokkolva", + "try_again": "Próbálja újra", + "domain_desc": "Adja meg a domain nevet vagy a helyettesítő karaktert ahhoz a címhez, amit át kíván íratni.", + "example_rewrite_domain": "csak ehhez a domainhez írja át a válaszokat.", + "example_rewrite_wildcard": "az <0>example.org összes aldomainjéhez átírja a válaszokat.", + "rewrite_ip_address": "IP-cím: használja ezt az IP-t A vagy AAAA válaszban", + "rewrite_domain_name": "Domain név: CNAME rekord hozzáadása", + "rewrite_A": "<0>A: speciális érték, megtartja az upstream felől érkező <0>A rekordokat", + "rewrite_AAAA": "<0>AAAA: speciális érték, megtartja az upstream felől érkező <0>AAAA rekordokat", + "disable_ipv6": "IPv6 letiltása", + "disable_ipv6_desc": "Ha ez a szolgáltatás engedélyezve van, akkor az összes IPv6-cím (AAAA típus) DNS-lekérdezése elveszik.", + "fastest_addr": "Leggyorsabb IP-cím", + "fastest_addr_desc": "Kérdezze le az összes DNS szervert és küldje vissza a leggyorsabb IP-címet az összes válasz alapján", + "autofix_warning_text": "Ha a \"Javítás\" lehetőségre kattint, az AdGuard Home megpróbálja beállítani a rendszerét, hogy használja az AdGuard Home DNS szervert.", + "autofix_warning_list": "A következő feladatokat hajtja végre: <0>A DNSStubListener rendszer kikapcsolása<0>Beállítja a DNS-kiszolgáló címét 127.0.0.1-re.<0>Lecseréli az /etc/resolv.conf szimbolikus útvonalat erre: /run/systemd/resolve/resolv.conf<0>A DNSStubListener leállítása (a rendszer által feloldott szolgáltatás újratöltése)", + "autofix_warning_result": "Mindennek eredményeként az Ön rendszeréből származó összes DNS-kérést alapértelmezés szerint az AdGuard Home dolgozza fel.", + "tags_title": "Címkék", + "tags_desc": "Kiválaszthatja a klienseknek megfelelő címkéket. A címkék beilleszthetők a szűrési szabályokba, és lehetővé teszik azok pontosabb alkalmazását. <0>További információ", + "form_select_tags": "Válasszon kliens címkéket", + "check_title": "Szűrés ellenőrzése", + "check_desc": "Ellenőrzi, hogy a hosztnév szűrve van-e", + "check": "Ellenőrzés", + "form_enter_host": "Adja meg a hosztnevet", + "filtered_custom_rules": "Szűrve van az egyéni szűrési szabályok alapján", + "choose_from_list": "Választás a listából", + "add_custom_list": "Egyedi lista hozzáadása", + "host_whitelisted": "Ez a hoszt a kivételek között szerepel", + "check_ip": "IP-címek: {{ip}}", + "check_cname": "CNAME: {{cname}}", + "check_reason": "Indok: {{reason}}", + "check_rule": "Szabály: {{rule}}", + "check_service": "Szolgáltatás neve: {{service}}", + "check_not_found": "Nem található az Ön szűrőlistái között", + "client_confirm_block": "Biztosan blokkolni szeretné a(z) \"{{ip}}\" klienst?", + "client_confirm_unblock": "Biztosan fel szeretné oldani a(z) \"{{ip}}\" kliens blokkolását?", + "client_blocked": "A(z) \"{{ip}}\" kliens sikeresen blokkolva", + "client_unblocked": "A(z) \"{{ip}}\" kliens blokkolása sikeresen feloldva", + "static_ip": "Statikus IP-cím", + "static_ip_desc": "Az AdGuard Home egy szerver, tehát statikus IP-címre van szüksége a megfelelő működéshez. Ellenkező esetben a router valamikor más IP-címet rendelhet ehhez az eszközhöz.", + "set_static_ip": "Statikus IP-cím beállítása", + "install_static_ok": "Jó hír! A statikus IP-cím már be van állítva", + "install_static_error": "Az AdGuard Home nem tudja automatikusan konfigurálni ezt a hálózati felületet. Kérjük, nézzen utána, hogyan kell ezt manuálisan elvégezni.", + "install_static_configure": "Úgy észleltük, hogy dinamikus IP-cím van használatban — <0>{{ip}}. Szeretné ezt statikus IP-címként használni?", + "confirm_static_ip": "Az AdGuard Home beállítja az {{ip}} IP-címet az Ön statikus IP-címének. Biztosan folytatni kívánja?", + "list_updated": "{{count}} lista frissítve lett", + "list_updated_plural": "{{count}} lista frissítve lett", + "dnssec_enable": "DNSSEC engedélyezése", + "dnssec_enable_desc": "Állítsa be a DNSSEC jelzőt a kimenő DNS-lekérdezésekbe, és ellenőrizze az eredményt (szükséges a DNSSEC-kompatibilis feloldás)", + "validated_with_dnssec": "DNSSEC-kel ellenőrizve", + "all_queries": "Minden kérés", + "show_blocked_responses": "Tiltva", + "show_whitelisted_responses": "Kivételezett", + "show_processed_responses": "Feldolgozott", + "blocked_safebrowsing": "Blokkolva a biztonságos böngészés által", + "blocked_adult_websites": "Blokkolva a felnőtt tartalmak által", + "blocked_threats": "Blokkolt fenyegetések", + "allowed": "Engedélyezve", + "filtered": "Megszűrt", + "rewritten": "Átírt", + "safe_search": "Biztonságos keresés", + "blocklist": "Tiltási lista", + "milliseconds_abbreviation": "ms", + "cache_size": "Gyorsítótár mérete", + "cache_size_desc": "DNS gyorsítótár mérete (bájtokban)", + "enter_cache_size": "Adja meg a gyorsítótár méretét", + "enter_cache_ttl_min_override": "Adja meg a minimális TTL-t", + "enter_cache_ttl_max_override": "Adja meg a maximális TTL-t", + "cache_ttl_min_override_desc": "Felülbírálja az upstream kiszolgálótól kapott (minimális) TTL értéket. Ez az érték nem lehet nagyobb, mint 3600 másodperc (vagyis 1 óra)", + "cache_ttl_max_override_desc": "Felülbírálja az upstream kiszolgálótól kapott (maximális) TTL értéket", + "min_exceeds_max_value": "A minimális érték meghaladja a maximális értéket", + "value_not_larger_than": "Az érték nem lehet nagyobb, mint {{maximum}}", + "filter_category_general": "Általános", + "filter_category_security": "Biztonság", + "filter_category_regional": "Regionális", + "filter_category_other": "Egyéb", + "filter_category_general_desc": "Olyan listák, amelyek blokkolják a nyomkövetést és a hirdetéseket a legtöbb eszközön", + "filter_category_security_desc": "Olyan listák, amelyek a kártékony, adathalász vagy átverős oldalak tiltására vannak kifejlesztve", + "filter_category_regional_desc": "Olyan listák, amelyek a regionális hirdetések, valamint a nyomkövető szerverek ellen vannak kifejlesztve", + "filter_category_other_desc": "További tiltólisták", + "original_response": "Eredeti válasz", + "click_to_view_queries": "Kattintson a lekérésekért", + "port_53_faq_link": "Az 53-as portot gyakran a \"DNSStubListener\" vagy a \"systemd-resolved\" (rendszer által feloldott) szolgáltatások használják. Kérjük, olvassa el <0>ezt az útmutatót a probléma megoldásához." +} \ No newline at end of file diff --git a/client/src/__locales/si-lk.json b/client/src/__locales/si-lk.json new file mode 100644 index 00000000..f3e655df --- /dev/null +++ b/client/src/__locales/si-lk.json @@ -0,0 +1,443 @@ +{ + "client_settings": "අනුග්‍රාහක සැකසුම්", + "check_dhcp_servers": "DHCP සේවාදායකයන් සඳහා පරීක්ෂා කරන්න", + "save_config": "වින්‍යාසය සුරකින්න", + "enabled_dhcp": "DHCP සේවාදායකය සබල කර ඇත", + "disabled_dhcp": "DHCP සේවාදායකය අබල කර ඇත", + "dhcp_title": "ග.ධා.වි.කෙ. සේවාදායකය (පර්යේෂණාත්මක!)", + "dhcp_description": "ඔබගේ මාර්ගකාරකය ග.ධා.වි.කෙ. (DHCP) සැකසුම් ලබා නොදෙන්නේ නම්, ඔබට AdGuardHome හි ඇති ග.ධා.වි.කෙ. සේවාදායකය භාවිතා කළ හැකිය.", + "dhcp_enable": "DHCP සේවාදායකය සබල කරන්න", + "dhcp_disable": "DHCP සේවාදායකය අබල කරන්න", + "dhcp_config_saved": "DHCP වින්‍යාසය සාර්ථකව සුරකින ලදි", + "form_error_required": "අවශ්‍ය ක්ෂේත්‍රයකි", + "form_error_ip4_format": "වලංගු නොවන IPv4 ආකෘතියකි", + "form_error_ip6_format": "වලංගු නොවන IPv6 ආකෘතියකි", + "form_error_ip_format": "වලංගු නොවන අ.ජා. කෙ. (IP) ආකෘතියකි", + "form_error_mac_format": "වලංගු නොවන MAC ආකෘතියකි", + "form_error_client_id_format": "වලංගු නොවන අනුග්‍රාහක හැඳුනුම් ආකෘතියකි", + "form_error_positive": "0 ට වඩා වැඩි විය යුතුය", + "form_error_negative": "0 හෝ ඊට වැඩි විය යුතුය", + "dhcp_form_range_title": "අ.ජා. කෙ. (IP) ලිපින පරාසය", + "dhcp_form_range_start": "පරාසය ආරම්භය", + "dhcp_form_range_end": "පරාසය අවසානය", + "dhcp_interface_select": "ග.ධා.වි.කෙ. (DHCP) අතුරුමුහුණත තෝරන්න", + "dhcp_hardware_address": "දෘඩාංග ලිපිනය", + "dhcp_ip_addresses": "අ.ජා. කෙ. (IP) ලිපින", + "ip": "අ.ජා. කෙ. (IP)", + "dhcp_table_hostname": "ධාරක නාමය", + "dhcp_table_expires": "කල් ඉකුත් වීම", + "dhcp_warning": "ඔබට කෙසේ හෝ ග.ධා.වි.කෙ. (DHCP) සේවාදායකය සබල කිරීමට අවශ්‍ය නම්, ඔබේ ජාලයේ වෙනත් ක්‍රියාකාරී ග.ධා.වි.කෙ. සේවාදායකයක් නොමැති බව තහවුරු කරගන්න. එසේ නොමැති නම්, එය සම්බන්ධිත උපාංග සඳහා අන්තර්ජාලය බිඳ දැමිය හැකිය!", + "dhcp_error": "ජාලයේ තවත් ග.ධා.වි.කෙ. (DHCP) සේවාදායකයක් තිබේද යන්න අපට තීරණය කළ නොහැකි විය.", + "dhcp_static_ip_error": "ග.ධා.වි.කෙ. (DHCP) සේවාදායකය භාවිතා කිරීම සඳහා ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනයක් සැකසිය යුතුය. මෙම ජාල අතුරුමුහුණත ස්ථිතික අ.ජා. කෙ. ලිපිනයක් භාවිතයෙන් වින්‍යාසගත කර තිබේද යන්න තීරණය කිරීමට අප අසමත් විය. කරුණාකර ස්ථිතික අ.ජා. කෙ. ලිපිනයක් අතින් සකසන්න.", + "dhcp_dynamic_ip_found": "ඔබේ පද්ධතිය <0>{{interfaceName}} අතුරු මුහුණත සඳහා ගතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපින වින්‍යාසය භාවිතා කරයි. ග.ධා.වි.කෙ. (DHCP) සේවාදායකය භාවිතා කිරීම සඳහා ස්ථිතික අ.ජා. කෙ. ලිපිනයක් සැකසිය යුතුය. ඔබගේ වර්තමාන අ.ජා. කෙ. ලිපිනය <0>{{ipAddress}} වේ. ඔබ ග.ධා.වි.කෙ. සබල කරන්න බොත්තම එබුවහොත් අපි ස්වයංක්‍රීයව මෙම අ.ජා. කෙ. ලිපිනය ස්ථිතික ලෙස සකසන්නෙමු.", + "dhcp_reset": "ග.ධා.වි.කෙ. (DHCP) වින්‍යාසය යළි පිහිටුවීමට අවශ්‍ය බව ඔබට විශ්වාස ද?", + "country": "රට", + "city": "නගරය", + "delete_confirm": "\"{{key}}\" මකා දැමීමට අවශ්‍ය බව ඔබට විශ්වාසද?", + "form_enter_hostname": "ධාරක නාමය ඇතුළත් කරන්න", + "error_details": "දෝෂ විස්තර", + "response_details": "ප්‍රතිචාරය‌ෙහි විස්තර", + "request_details": "ඉල්ලීමෙහි විස්තර", + "client_details": "අනුග්‍රාහකයේ විස්තර", + "details": "විස්තර", + "back": "ආපසු", + "dashboard": "උපකරණ පුවරුව", + "settings": "සැකසුම්", + "filters": "පෙරහන්", + "filter": "පෙරහන", + "query_log": "විමසුම් ලොගය", + "nothing_found": "කිසිවක් සොයාගත නොහැක", + "faq": "නිති අසන පැණ", + "version": "අනුවාදය", + "address": "ලිපිනය", + "protocol": "කෙටුම්පත", + "on": "සක්‍රියයි", + "off": "අක්‍රියයි", + "copyright": "ප්‍රකාශන හිමිකම", + "homepage": "මුල් පිටුව", + "report_an_issue": "ගැටලුවක් වාර්තා කරන්න", + "privacy_policy": "රහස්‍යතා ප්‍රතිපත්තිය", + "enable_protection": "ආරක්ෂණය සබල කරන්න", + "enabled_protection": "ආරක්ෂණය සබල කර ඇත", + "disable_protection": "ආරක්ෂණය අබල කරන්න", + "disabled_protection": "ආරක්ෂණය අබල කර ඇත", + "refresh_statics": "සංඛ්‍යාලේඛන නැවුම් කරන්න", + "dns_query": "ව.නා.ප. (DNS) විමසුම්", + "blocked_by": "<0>පෙරහන් මගින් අවහිර කරන ලද", + "stats_malware_phishing": "අවහිර කළ ද්වේශාංග/තතුබෑම්", + "stats_adult": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි", + "stats_query_domain": "ජනප්‍රිය විමසන ලද වසම්", + "for_last_24_hours": "පසුගිය පැය 24 සඳහා", + "for_last_days": "පසුගිය දින {{count}} සඳහා", + "for_last_days_plural": "පසුගිය දින {{count}} සඳහා", + "no_domains_found": "වසම් කිසිවක් සොයා ගත නොහැකි විය", + "requests_count": "ඉල්ලීම් ගණන", + "top_blocked_domains": "ජනප්‍රිය අවහිර කළ වසම්", + "top_clients": "ජනප්‍රිය අනුග්‍රාහකයන්", + "general_statistics": "පොදු සංඛ්යාලේඛන", + "number_of_dns_query_blocked_24_hours": "දැන්වීම් වාරණ පෙරහන් සහ ධාරක වාරණ ලැයිස්තු මගින් අවහිර කරන ලද DNS ඉල්ලීම් ගණන", + "number_of_dns_query_blocked_24_hours_by_sec": "AdGuard browsing security ඒකකය මගින් අවහිර කරන ලද DNS ඉල්ලීම් ගණන", + "number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි ගණන", + "enforced_save_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන ලද", + "number_of_dns_query_to_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ සෙවුම් යන්ත්‍ර සඳහා DNS ඉල්ලීම් ගණන", + "average_processing_time": "සාමාන්‍ය සැකසුම් කාලය", + "average_processing_time_hint": "DNS ඉල්ලීමක් සැකසීමේ සාමාන්‍ය කාලය මිලි තත්පර වලින්", + "block_domain_use_filters_and_hosts": "පෙරහන් සහ ධාරක ගොනු භාවිතා කරමින් වසම් අවහිර කරන්න", + "filters_block_toggle_hint": "ඔබට අවහිර කිරීමේ නීති පෙරහන් තුළ පිහිටුවිය හැකිය.", + "use_adguard_browsing_sec": "AdGuard browsing security web service භාවිතා කරන්න", + "use_adguard_parental": "AdGuard parental control වෙබ් සේවාව භාවිතා කරන්න", + "use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි AdGuard Home විසින් පරීක්ෂා කරනු ඇත. එය browsing security වෙබ් සේවාව මෙන් රහස්‍යතා හිතකාමී API භාවිතා කරයි.", + "enforce_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කරන්න", + "enforce_save_search_hint": "AdGuard Home හට පහත සෙවුම් යන්ත්‍ර තුළ ආරක්ෂිත සෙවීම බලාත්මක කළ හැකිය: Google, Youtube, Bing, DuckDuckGo සහ Yandex.", + "no_servers_specified": "සේවාදායක කිසිවක් නිශ්චිතව දක්වා නැත", + "general_settings": "පොදු සැකසුම්", + "dns_settings": "DNS සැකසුම්", + "dns_blocklists": "DNS අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තු", + "dns_allowlists": "DNS අවසර දීමේ ලැයිස්තු", + "dns_allowlists_desc": "DNS අවසර දීමේ ලැයිස්තුවල වසම් කිසියම් අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව අඩංගු වුවද එය න‌ොසලකා හැර අවසර දෙනු ලැබේ.", + "custom_filtering_rules": "අභිරුචි පෙරීමේ නීති", + "encryption_settings": "සංකේතාංකන සැකසුම්", + "dhcp_settings": "DHCP සැකසුම්", + "disabled_filtering_toast": "පෙරීම අබල කර ඇත", + "enabled_filtering_toast": "පෙරීම සබල කර ඇත", + "disabled_parental_toast": "Parental control අබල කර ඇත", + "enabled_parental_toast": "Parental control සබල කර ඇත", + "disabled_safe_search_toast": "ආරක්ෂිත සෙවීම අබල කර ඇත", + "enabled_save_search_toast": "ආරක්ෂිත සෙවීම සබල කර ඇත", + "enabled_table_header": "සබල කර ඇත", + "name_table_header": "නම", + "list_url_table_header": "URL ලැයිස්තුව", + "rules_count_table_header": "නීති ගණන", + "last_time_updated_table_header": "අවසන් වරට යාවත්කාලීන කරන ලද", + "actions_table_header": "ක්‍රියාමාර්ග", + "request_table_header": "ඉල්ලීම", + "edit_table_action": "සංස්කරණය කරන්න", + "delete_table_action": "මකන්න", + "no_blocklist_added": "අවහිර කිරීමේ ලැයිස්තු එකතු කර නැත", + "no_whitelist_added": "අවසර දීමේ ලැයිස්තු එකතු කර නැත", + "add_blocklist": "අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුවක් එකතු කරන්න", + "add_allowlist": "අවසර දීමේ ලැයිස්තුවක් එකතු කරන්න", + "cancel_btn": "අහෝසි කරන්න", + "enter_name_hint": "නම ඇතුළත් කරන්න", + "enter_url_or_path_hint": "ලැයිස්තුවක URL හෝ ස්ථීර මාර්ගයක් ඇතුළත් කරන්න", + "check_updates_btn": "යාවත්කාල පරීක්ෂා කරන්න", + "new_blocklist": "නව අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව", + "new_allowlist": "නව අවසර දීමේ ලැයිස්තුව", + "edit_blocklist": "අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව සංස්කරණය කරන්න", + "edit_allowlist": "අවසර දීමේ ලැයිස්තුව සංස්කරණය කරන්න", + "enter_valid_blocklist": "අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුවට වලංගු URL ලිපිනයක් ඇතුළත් කරන්න.", + "enter_valid_allowlist": "අවසර දීමේ ලැයිස්තුවට වලංගු URL ලිපිනයක් ඇතුළත් කරන්න.", + "form_error_url_format": "වලංගු නොවන URL ආකෘතියකි", + "form_error_url_or_path_format": "ලැයිස්තුවක වලංගු නොවන URL හෝ ස්ථීර මාර්ගයකි", + "custom_filter_rules": "අභිරුචි පෙරීමේ නීති", + "examples_title": "උදාහරණ", + "example_meaning_filter_block": "example.org වසමට සහ එහි සියලුම උප වසම් වලට පිවිසීම අවහිර කරන්න", + "example_meaning_filter_whitelist": "example.org වසමට සහ එහි සියලුම උප වසම් වලට ප්‍රවේශය අවහිර නොකරන්න", + "example_meaning_host_block": "AdGuard Home දැන් example.org වසම සඳහා 127.0.0.1 ලිපිනය ලබා දෙනු ඇත (නමුත් එහි උප ලිපින නොවේ).", + "example_comment": "! මෙතැන අදහස් දැක්වීමක්", + "example_comment_meaning": "විස්තර කිිරීමක්", + "example_comment_hash": "# එසේම අදහස් දැක්වීමක්", + "example_regex_meaning": "නිශ්චිතව දක්වා ඇති නිත්‍ය වාක්‍යවිධියට ගැලපෙන වසම් වෙත පිවිසීම අවහිර කරන්න", + "example_upstream_regular": "සාමාන්‍ය DNS (UDP හරහා)", + "example_upstream_dot": "සංකේතාංකනය කළ <0>DNS-over-TLS", + "example_upstream_doh": "සංකේතාංකනය කළ <0>DNS-over-HTTPS", + "example_upstream_tcp": "සාමාන්‍ය DNS (TCP හරහා)", + "all_lists_up_to_date_toast": "සියලුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි", + "dns_test_ok_toast": "සඳහන් කළ DNS සේවාදායකයන් නිවැරදිව ක්‍රියා කරයි", + "dns_test_not_ok_toast": "සේවාදායකය \"{{key}}\": භාවිතා කළ නොහැකි විය, කරුණාකර ඔබ එය නිවැරදිව ලියා ඇත්දැයි පරීක්ෂා කරන්න", + "unblock": "අනවහිර කරන්න", + "block": "අවහිර කරන්න", + "time_table_header": "වේලාව", + "date": "දිනය", + "domain_name_table_header": "වසම් නාමය", + "domain_or_client": "වසම හෝ අනුග්‍රාහකය", + "type_table_header": "වර්ගය", + "response_table_header": "ප්‍රතිචාරය", + "response_code": "ප්‍රතිචාර කේතය", + "client_table_header": "අනුග්‍රාහකය", + "empty_response_status": "හිස්", + "show_all_filter_type": "සියල්ල පෙන්වන්න", + "refresh_btn": "නැවුම් කරන්න", + "previous_btn": "පෙර", + "next_btn": "ඊළඟ", + "loading_table_status": "පූරණය ‌වෙමින්...", + "page_table_footer_text": "පිටුව", + "rows_table_footer_text": "පේළි", + "updated_custom_filtering_toast": "අභිරුචි පෙරීමේ නීති යාවත්කාල කරන ලදි", + "rule_removed_from_custom_filtering_toast": "අභිරුචි පෙරීමේ නීති තුළින් නීතියක් ඉවත් කරන ලදි", + "rule_added_to_custom_filtering_toast": "අභිරුචි පෙරීමේ නීති තුළට මෙම නීතිය එකතු කරන ලදි", + "query_log_response_status": "තත්ත්වය: {{value}}", + "query_log_filtered": "{{filter}} මගින් පෙරහන් කරන ලදි", + "query_log_confirm_clear": "සම්පූර්ණ විමසුම් ලොගය ඉවත් කිරීමට අවශ්‍ය යැයි ඔබට විශ්වාසද?", + "query_log_clear": "විමසුම් ලොග ඉවත් කරන්න", + "query_log_retention": "විමසුම් ලොග රඳවා තබා ගැනීම", + "query_log_enable": "ලොගය සබල කරන්න", + "query_log_configuration": "ලොග වින්‍යාසය", + "query_log_disabled": "විමසුම් ලොගය අබල කර ඇති අතර එය <0>සැකසුම් තුළ වින්‍යාසගත කළ හැකිය", + "query_log_strict_search": "ඉතා නිවැරදිව සෙවීම සඳහා ද්විත්ව උද්ධෘතය භාවිතා කරන්න", + "query_log_retention_confirm": "විමසුම් ලොගය රඳවා තබා ගැනීම වෙනස් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද? ඔබ කාල පරතරයෙහි අගය අඩු කළහොත් සමහර දත්ත නැති වී යනු ඇත", + "anonymize_client_ip": "අනුග්‍රාහකයෙහි අ.ජා. කෙ. (IP) නිර්නාමික කරන්න", + "dns_config": "DNS සේවාදායක වින්‍යාසය", + "default": "සුපුරුදු", + "nxdomain": "නොපවතින වසම", + "null_ip": "අභිශූන්‍යය අ.ජා. කෙ. (IP)", + "custom_ip": "අභිරුචි අ.ජා. කෙ. (IP)", + "dns_over_https": "DNS-over-HTTPS", + "dns_over_tls": "DNS-over-TLS", + "form_enter_rate_limit": "අනුපාත සීමාව ඇතුළත් කරන්න", + "rate_limit": "අනුපාත සීමාව", + "edns_enable": "EDNS අනුග්‍රාහක අනුජාලය සබල කරන්න", + "blocking_ipv4_desc": "අවහිර කළ A ඉල්ලීමක් සඳහා ආපසු එවිය යුතු අ.ජා. කෙ. (IP) ලිපිනය", + "blocking_ipv6_desc": "අවහිර කළ AAAA ඉල්ලීමක් සඳහා ආපසු එවිය යුතු අ.ජා. කෙ. (IP) ලිපිනය", + "blocking_mode_nxdomain": "නොපවතින වසම (NXDOMAIN): NXDOMAIN කේතය සමඟ ප්‍රතිචාර දක්වයි", + "blocking_mode_null_ip": "අභිශූන්‍යය අන්තර්ජාල කෙටුම්පත: ශුන්‍ය අ.ජා. කෙ. (IP) ලිපිනය සමඟ ප්‍රතිචාර දක්වයි (A සඳහා 0.0.0.0; AAAA සඳහා ::)", + "blocking_mode_custom_ip": "අභිරුචි අන්තර්ජාල කෙටුම්පත: අතින් සැකසූ අ.ජා. කෙ. (IP) ලිපිනයක් සමඟ ප්‍රතිචාර දක්වයි", + "upstream_dns_client_desc": "ඔබ මෙම ක්ෂේත්‍රය හිස්ව තබා ගන්නේ නම්, AdGuard Home විසින් <0>DNS සැකසුම් හි වින්‍යාසගත කර ඇති සේවාදායකයන් භාවිතා කරනු ඇත.", + "source_label": "මූලාශ්‍රය", + "category_label": "ප්‍රවර්ගය", + "rule_label": "නීතිය", + "list_label": "ලැයිස්තුව", + "unknown_filter": "{{filterId}} නොදන්නා පෙරහනකි", + "known_tracker": "දන්නා ලුහුබැඳීමක්", + "install_welcome_title": "AdGuard Home වෙත සාදරයෙන් පිළිගනිමු!", + "install_welcome_desc": "AdGuard Home යනු ජාලය පුරා ඇති දැන්වීම් සහ ලුහුබැඳීම අවහිර කරන DNS සේවාදායකි. ඔබගේ මුළු ජාලය සහ සියලුම උපාංග පාලනය කිරීමට ඉඩ සලසා දීම එහි පරමාර්ථය යි, එයට අනුග්‍රාහක පාර්ශවීය වැඩසටහනක් භාවිතා කිරීම අවශ්‍ය නොවේ.", + "install_settings_title": "පරිපාලක වෙබ් අතුරු මුහුණත", + "install_settings_listen": "සවන් දෙන අතුරු මුහුණත", + "install_settings_port": "කවුළුව", + "form_error_port": "වලංගු කවුළුවක අගයක් ඇතුළත් කරන්න", + "install_settings_dns": "DNS සේවාදායකය", + "install_settings_all_interfaces": "සියලුම අතුරුමුහුණත්", + "install_auth_title": "සත්‍යාපනය", + "install_auth_desc": "ඔබගේ AdGuard Home පරිපාලක වෙබ් අතුරු මුහුණතට මුරපද සත්‍යාපනය වින්‍යාසගත කිරීම අතිශයින් නිර්දේශ කෙරේ. එය ඔබගේ ස්ථානීය ජාල‌ය‌ෙ‌න් පමණක් ප්‍රවේශ විය හැකි වුවද, එය තව දුරටත් සීමා රහිත ප්‍රවේශයකින් ආරක්ෂා කර ගැනීම වැදගත් ය.", + "install_auth_username": "පරිශීලක නාමය", + "install_auth_password": "මුරපදය", + "install_auth_confirm": "මුරපදය තහවුරු කරන්න", + "install_auth_username_enter": "පරිශීලක නාමය ඇතුළත් කරන්න", + "install_auth_password_enter": "මුරපදය ඇතුළත් කරන්න", + "install_step": "පියවර", + "install_devices_title": "ඔබගේ උපාංග වින්‍යාසගත කරන්න", + "install_devices_desc": "AdGuard Home භාවිතා කිරීම ආරම්භයට, ඔබගේ උපාංග එය පරිශ්‍රීලනයට වින්‍යාසගත කිරීම අවශ්‍ය වේ.", + "install_submit_title": "සුභ පැතුම්!", + "install_submit_desc": "පිහිටුවීමේ ක්‍රියා පටිපාටිය අවසන් වී ඇති අතර ඔබ AdGuard Home භාවිතය ආරම්භ කිරීමට සූදානම්ය.", + "install_devices_router": "මාර්ගකාරකය", + "install_devices_router_desc": "මෙම පිහිටුම ඔබගේ නිවසේ මාර්ගකාරකයට සම්බන්ධ සියලුම උපාංග ස්වයංක්‍රීයව ආවරණය කරන අතර ඔබට ඒවා අතින් වින්‍යාසගත කිරීමට අවශ්‍ය නොවනු ඇත.", + "install_devices_router_list_3": "ඔබගේ AdGuard Home සේවාදායක ලිපින එහි ඇතුළත් කරන්න.", + "install_devices_router_list_4": "ඔබට සමහර වර්ගයේ රවුටර වල අභිරුචි ව.නා.ප. (DNS) සේවාදායකයක් සැකසිය නොහැක. මෙම අවස්ථාවේදී AdGuard Home <0>ග.ධා.වි.කෙ. (DHCP) සේවාදායකයක් ලෙස පිහිටුවන්නේ නම් එය උපකාර වනු ඇත. එසේ නොමැතිනම්, ඔබග‌ේ විශේෂිත මාර්ගකාරක මාදිළිය සඳහා වූ ව.නා.ප. සේවාදායකයන් රිසිකරණය කරන්නේ කෙසේද යන්න පිළිබඳ අත්පොත සෙවිය යුතුය.", + "install_devices_windows_list_1": "ආරම්භක මෙනුව හෝ වින්ඩෝස් සෙවුම හරහා පාලක පැනලය විවෘත කරන්න.", + "install_devices_windows_list_2": "ජාල සහ අන්තර්ජාල ප්‍රවර්ගයට ගොස් පසුව ජාල සහ බෙදාගැනීමේ මධ්‍යස්ථානය වෙත යන්න.", + "install_devices_windows_list_3": "උපයුක්තක‌‌‌යෙහි සැකසුම් වෙනස් කිරීම තිරයේ වම් පසින් සොයාගෙන එය මත ක්ලික් කරන්න.", + "install_devices_windows_list_4": "ඔබගේ ක්‍රියාකාරී සම්බන්ධතාවය තෝරන්න, එය මත දකුණු-ක්ලික් කර ගුණාංග තෝරන්න.", + "install_devices_windows_list_5": "ලැයිස්තුවේ ඇති අන්තර්ජාල කෙටුම්පත් අනුවාදය 4 (TCP/IP) සොයාගෙන එය තෝරා ඉන්පසු ගුණාංග මත නැවත ක්ලික් කරන්න.", + "install_devices_windows_list_6": "'පහත දැක්වෙන DNS සේවාදායක ලිපින භාවිතා කරන්න' යන්න තෝරා ඔබගේ AdGuard Home සේවාදායක ලිපින ඇතුළත් කරන්න.", + "install_devices_macos_list_1": "ඇපල් අයිකනය මත ක්ලික් කර පද්ධති මනාපයන් වෙත යන්න.", + "install_devices_macos_list_2": "ජාලය මත ක්ලික් කරන්න.", + "install_devices_macos_list_3": "ඔබගේ ලැයිස්තුවේ පළමු සම්බන්ධතාවය තෝරා උසස් මත ක්ලික් කරන්න.", + "install_devices_macos_list_4": "DNS තීරුව තෝරා ඔබගේ AdGuard Home සේවාදායක ලිපින ඇතුළත් කරන්න.", + "install_devices_android_list_1": "ඇන්ඩ්‍රොයිඩ් මෙනුවෙහි මුල් තිරයෙන්, සැකසීම් මත තට්ටු කරන්න.", + "install_devices_android_list_2": "මෙනුවේ Wi-Fi මත තට්ටු කරන්න. පවතින සියලුම ජාල ලැයිස්තුගත කර ඇති තිරය පෙන්වනු ඇත (ජංගම සම්බන්ධතාවය සඳහා අභිරුචි DNS සැකසිය නොහැක).", + "install_devices_android_list_3": "ඔබ සම්බන්ධ වී ඇති ජාලය මත දිගු වේලාවක් ඔබන්න, ඉන්පසුව ජාලය වෙනස් කිරීම මත තට්ටු කරන්න.", + "install_devices_android_list_4": "ඔබට සමහර උපාංගවල වැඩිදුර සැකසුම් බැලීමට \"උසස්\" සඳහා වූ කොටුව සලකුණු කිරීමට අවශ්‍ය විය හැකිය. එමෙන්ම ඔබගේ ඇන්ඩ්‍රොයිඩ් DNS සැකසුම් වෙනස් කිරීමට, අ.ජා. කෙ. (IP) සැකසුම් ග.ධා.වි.කෙ. (DHCP) සිට ස්ථිතික වෙත මාරු කළ යුතුය.", + "install_devices_android_list_5": "DNS 1 සහ DNS 2 පිහිටුවීම් අගයන් ඔබගේ AdGuard Home සේවාදායක ලිපින වලට වෙනස් කරන්න.", + "install_devices_ios_list_1": "මුල් තිරයේ සිට, සැකසුම් මත තට්ටු කරන්න.", + "install_devices_ios_list_2": "වම්පස මෙනුවෙහි Wi-Fi තෝරන්න (ජංගම දුරකථන සඳහා DNS වින්‍යාසගත කිරීමට නොහැකිය).", + "install_devices_ios_list_3": "දැනට ක්‍රියාකාරී ජාලයයහෙි නම මත තට්ටු කරන්න.", + "install_devices_ios_list_4": "DNS ක්ෂේත්‍රය තුළ ඔබගේ AdGuard Home සේවාදායක ලිපින ඇතුළත් කරන්න.", + "get_started": "ආරම්භ කර ගන්න", + "next": "ඊළඟ", + "open_dashboard": "උපකරණ පුවරුව විවෘත කරන්න", + "install_saved": "සාර්ථකව සුරකින ලදි", + "encryption_title": "සංකේතාංකනය", + "encryption_desc": "ගුප්තකේතනය (HTTPS/TLS) සඳහා DNS සහ පරිපාලක වෙබ් අතුරු මුහුණත සහය දක්වයි", + "encryption_config_saved": "සංකේතාංකන වින්‍යාසය සුරකින ලදි", + "encryption_server": "සේවාදායක‌‌‌‌යේ නම", + "encryption_server_enter": "ඔබගේ වසම් නාමය ඇතුළත් කරන්න", + "encryption_redirect": "ස්වයංක්‍රීයව HTTPS වෙත හරවා යවන්න", + "encryption_redirect_desc": "සබල කර ඇත්නම්, AdGuard Home ඔබව ස්වයංක්‍රීයව HTTP සිට HTTPS ලිපින වෙත හරවා යවනු ඇත.", + "encryption_https": "HTTPS කවුළුව", + "encryption_https_desc": "HTTPS කවුළුව වින්‍යාසගත කර ඇත්නම්, AdGuard Home පරිපාලක අතුරුමුහුණත HTTPS හරහා ප්‍රවේශ විය හැකි අතර එය '/ dns-query' ස්ථානයේ DNS-over-HTTPS ද ලබා දෙනු ඇත.", + "encryption_dot": "DNS-over-TLS කවුළුව", + "encryption_dot_desc": "මෙම කවුළුව වින්‍යාසගත කර ඇත්නම්, AdGuard Home විසින් මෙම කවුළුව හරහා DNS-over-TLS සේවාදායකයක් ක්‍රියාත්මක කරනු ඇත.", + "encryption_certificates": "සහතික", + "encryption_certificates_input": "ඔබගේ PEM-කේතාංකනය කළ සහතික පිටපත් කර මෙහි අලවන්න.", + "encryption_status": "තත්ත්වය", + "encryption_expire": "කල් ඉකුත් වීම", + "encryption_key": "පුද්ගලික යතුර", + "encryption_key_input": "ඔබගේ සහතිකය සඳහා PEM-කේතාංකනය කළ පුද්ගලික යතුර පිටපත් කර මෙහි අලවන්න.", + "encryption_enable": "සංකේතාංකනය සබල කරන්න (HTTPS, DNS-over-HTTPS සහ DNS-over-TLS)", + "encryption_enable_desc": "සංකේතාංකනය සබල කර ඇත්නම්, AdGuard Home පරිපාලක අතුරුමුහුණත HTTPS හරහා ක්‍රියා කරනු ඇති අතර DNS සේවාදායකය DNS-over-HTTPS සහ DNS-over-TLS හරහා ලැබෙන ඉල්ලීම් සඳහා සවන් දෙනු ඇත.", + "encryption_key_valid": "මෙය වලංගු {{type}} පුද්ගලික යතුරකි", + "encryption_key_invalid": "මෙය වලංගු නොවන {{type}} පුද්ගලික යතුරකි", + "encryption_subject": "මාතෘකාව", + "encryption_issuer": "නිකුත් කරන්නා", + "encryption_hostnames": "ධාරක නාම", + "encryption_reset": "සංකේතාංකන සැකසුම් යළි පිහිටුවීමට අවශ්‍ය බව ඔබට විශ්වාස ද?", + "topline_expiring_certificate": "ඔබගේ SSL සහතිකය කල් ඉකුත්වීමට ආසන්න වී ඇත. <0>සංකේතාංකන සැකසුම් යාවත්කාල කරන්න.", + "topline_expired_certificate": "ඔබගේ SSL සහතිකය කල් ඉකුත් වී ඇත. <0>සංකේතාංකන සැකසුම් යාවත්කාල කරන්න.", + "form_error_port_range": "80-65535 පරාසය‌ෙ‌හි කවුළුවක අගයක් ඇතුළත් කරන්න", + "form_error_port_unsafe": "මෙය අනාරක්ෂිත කවුළුවකි", + "form_error_equal": "සමාන නොවිය යුතුය", + "form_error_password": "මුරපදය නොගැලපුුුුුුණි", + "reset_settings": "සැකසුම් යළි පිහිටුවන්න", + "update_announcement": "AdGuard Home {{version}} දැන් ලබා ගත හැකිය! වැඩි විස්තර සඳහා <0>මෙහි ක්ලික් කරන්න.", + "setup_guide": "පිහිටුවීමේ මාර්ගෝපදේශය", + "dns_addresses": "DNS ලිපින", + "dns_start": "DNS සේවාදායකය ආරම්භ වෙමින් පවතී", + "down": "පහත", + "fix": "නිරාකරණය කරන්න", + "dns_providers": "මෙහි තෝරා ගැනීමට <0>දන්නා DNS සපයන්නන්ගේ ලැයිස්තුවක් ඇත.", + "update_now": "දැන් \tයාවත්කාල කරන්න", + "update_failed": "ස්වයංක්‍රීය යාවත්කාල කිරීම අසාර්ථක විය. අතින් යාවත්කාල කිරීමට කරුණාකර පියවර අනුගමනය කරන්න.", + "processing_update": "කරුණාකර රැඳී සිටින්න, AdGuard Home යාවත්කාලීන වෙමින් පවතී", + "clients_title": "අනුග්‍රාහකයන්", + "clients_desc": "AdGuard Home වෙත සම්බන්ධ කර ඇති උපාංග වින්‍යාසගත කරන්න", + "settings_global": "ගෝලීය", + "settings_custom": "අභිරුචි", + "table_client": "අනුග්‍රාහකය", + "table_name": "නම", + "save_btn": "සුරකින්න", + "client_add": "අනුග්‍රාහකයක් එකතු කරන්න", + "client_new": "නව අනුග්‍රාහකය", + "client_edit": "අනුග්‍රාහකය සංස්කරණය කරන්න", + "client_identifier": "හඳුන්වනය", + "ip_address": "අ.ජා. කෙ. (IP) ලිපිනය", + "client_identifier_desc": "අනුග්‍රාහකයන් අ.ජා. කෙ. (IP) ලිපිනයක් හෝ මා.ප්‍ර.පා. (MAC) ලිපිනයක් මගින් හඳුනාගත හැකිය. මා.ප්‍ර.පා. හඳුන්වනයක් ලෙස භාවිතා කළ හැක්කේ AdGuard Home ද <0>DHCP සේවාදායකයක් නම් පමණක් බව කරුණාවෙන් සලකන්න. ", + "form_enter_ip": "අ.ජා. කෙ. (IP) ඇතුළත් කරන්න", + "form_enter_mac": "MAC ඇතුළත් කරන්න", + "form_enter_id": "හඳුන්වනය ඇතුළත් කරන්න", + "form_add_id": "හඳුන්වනයක් එක් කරන්න", + "form_client_name": "අනුග්‍රාහකයේ නම ඇතුළත් කරන්න", + "name": "නම", + "client_global_settings": "ගෝලීය සැකසුම් භාවිතා කරන්න", + "client_deleted": "\"{{key}}\" අනුග්‍රාහකය සාර්ථකව ඉවත් කරන ලදි", + "client_added": "\"{{key}}\" අනුග්‍රාහකය සාර්ථකව එකතු කරන ලදි", + "client_updated": "\"{{key}}\" අනුග්‍රාහකය සාර්ථකව යාවත්කාල කරන ලදි", + "client_confirm_delete": "\"{{key}}\" අනුග්‍රාහකය ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?", + "list_confirm_delete": "මෙම ලැයිස්තුව ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාස ද?", + "auto_clients_desc": "AdGuard Home භාවිතා කරන අනුග්‍රාහකයන්‌ග‌ේ දත්ත, නමුත් වින්‍යාසය තුළ ගබඩා කර නොමැති", + "access_title": "ප්‍රවේශවීමට සැකසුම්", + "access_desc": "මෙහිදී ඔබට AdGuard Home DNS සේවාදායකය සඳහා ප්‍රවේශ වී‌‌‌‌මේ නීති වින්‍යාසගත කළ හැකිය.", + "access_allowed_title": "අවසර ලත් අනුග්‍රාහකයන්", + "access_allowed_desc": "CIDR හෝ අ.ජා. කෙ. (IP) ලිපින ලැයිස්තුවක් වින්‍යාසගත කර ඇත්නම්, AdGuard Home විසින් එම අ.ජා. කෙ. ලිපින වලින් පමණක් ඉල්ලීම් පිළිගනු ඇත.", + "access_disallowed_title": "අවසර නොලත් අනුග්‍රාහකයන්", + "access_disallowed_desc": "CIDR හෝ අ.ජා. කෙ. (IP) ලිපින ලැයිස්තුවක් වින්‍යාසගත කර ඇත්නම්, AdGuard Home විසින් එම අ.ජා. කෙ. ලිපින වලින් ඉල්ලීම් අත්හරිනු ඇත.", + "access_blocked_title": "අවහිර කළ වසම්", + "access_settings_saved": "ප්‍රවේශ වීමේ සැකසුම් සාර්ථකව සුරකින ලදි", + "updates_checked": "යාවත්කාලීන කිරීම් සාර්ථකව පරික්ෂා කර ඇත", + "updates_version_equal": "AdGuard Home යාවත්කාලීනයි", + "check_updates_now": "යාවත්කාල කිරීම සඳහා දැන් පරීක්ෂා කරන්න", + "dns_privacy": "DNS රහස්‍යතා", + "setup_dns_privacy_1": "<0>DNS-over-TLS: <1>{{address}} තන්තුව භාවිතා කරයි.", + "setup_dns_privacy_2": "<0>DNS-over-HTTPS: <1>{{address}} තන්තුව භාවිතා කරයි.", + "setup_dns_privacy_3": "<0>සංකේතාංකන ව.නා.ප. (DNS) කෙටුම්පත් සඳහා සහය දක්වන්නේ ඇන්ඩ්‍රොයිඩ් 9 පමණක් බව කරුණාවෙන් සලකන්න. එබැවින් ඔබ වෙනත් මෙහෙයුම් පද්ධති සඳහා අතිරේක මෘදුකාංග ස්ථාපනය කළ යුතුය.<0> ඔබට භාවිතා කළ හැකි මෘදුකාංග ලැයිස්තුවක් පහත දැක්‌වේ.", + "setup_dns_privacy_android_2": "<1>DNS-over-HTTPS සහ <1>DNS-over-TLS සඳහා <0>AdGuard for Android සහය දක්වයි.", + "setup_dns_privacy_ios_2": "<1>DNS-over-HTTPS සහ <1>DNS-over-TLS පිහිටුවීම් සඳහා <0>AdGuard for iOS සහය දක්වයි.", + "setup_dns_privacy_other_2": "<0>dnsproxy දන්නා සියලුම ආරක්ෂිත DNS කෙටුම්පත් සඳහා සහය දක්වයි.", + "setup_dns_privacy_other_3": "<1>DNS-over-HTTPS සඳහා <0>dnscrypt-පෙරකලාසිය සහය දක්වයි.", + "setup_dns_privacy_other_4": "<1>DNS-over-HTTPS සඳහා <0>මොසිල්ලා ෆයර්ෆොක්ස් සහය දක්වයි.", + "setup_dns_notice": "ඔබට <1>DNS-over-HTTPS හෝ <1>DNS-over-TLS භාවිතා කිරීම සඳහා AdGuard Home සැකසුම් තුළ <0>සංකේතාංකනය වින්‍යාසගත කිරීමට අවශ්‍ය වේ.", + "rewrite_add": "DNS නැවත ලිවීමක් එකතු කරන්න", + "rewrite_confirm_delete": "\"{{key}}\" සඳහා DNS නැවත ලිවීම ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?", + "dns_rewrites": "DNS නැවත ලිවීම්", + "form_domain": "වසම ඇතුළත් කරන්න", + "form_answer": "අ.ජා. කෙ. (IP) ලිපිනය ‌හෝ වසම ඇතුළත් කරන්න ", + "form_error_domain_format": "වලංගු නොවන වසම් ආකෘතියකි", + "form_error_answer_format": "වලංගු නොවන පිළිතුරු ආකෘතියකි", + "configure": "වින්‍යාසගත කරන්න", + "main_settings": "ප්‍රධාන සැකසුම්", + "block_services": "විශේෂිත සේවාවන් අවහිර කරන්න", + "blocked_services": "අවහිර කළ සේවාවන්", + "blocked_services_desc": "ජනප්‍රිය අඩවි සහ සේවාවන් ඉක්මනින් අවහිර කිරීමට ඉඩ දෙයි.", + "blocked_services_saved": "අවහිර කළ සේවාවන් සාර්ථකව සුරකින ලදි", + "blocked_services_global": "ගෝලීය අවහිර කළ සේවාවන් භාවිතා කරන්න", + "blocked_service": "අවහිර කළ සේවාව", + "block_all": "සියල්ල අවහිර කරන්න", + "unblock_all": "සියල්ල අනවහිර කරන්න", + "encryption_certificate_path": "සහතිකයේ මාර්ගය", + "encryption_private_key_path": "පුද්ගලික යතුරෙහි මාර්ගය", + "encryption_certificates_source_path": "සහතික ගොනු‌ව‌ෙහි මාර්ගය සකසන්න", + "encryption_certificates_source_content": "සහතිකවල අන්තර්ගත අලවන්න", + "encryption_key_source_path": "පුද්ගලික යතුරක ගොනුවක් සකසන්න", + "encryption_key_source_content": "පුද්ගලික යතු‌රෙහි අන්තර්ගත අලවන්න", + "stats_params": "සංඛ්‍යාලේඛන වින්‍යාසය", + "config_successfully_saved": "වින්‍යාසය සාර්ථකව සුරකින ලදි", + "interval_24_hour": "පැය 24", + "interval_days": "{{count}} දිනය", + "interval_days_plural": "දින {{count}}", + "domain": "වසම", + "answer": "පිළිතුර", + "filter_added_successfully": "පෙරහන සාර්ථකව එකතු කරන ලදි", + "filter_updated": "ලැයිස්තුව සාර්ථකව යාවත්කාලීන කර ඇත", + "statistics_configuration": "සංඛ්‍යාලේඛන වින්‍යාසය", + "statistics_retention": "සංඛ්‍යාලේඛන රඳවා තබා ගැනීම", + "statistics_retention_desc": "ඔබ කාල පරතරය අඩු කළහොත් සමහර දත්ත නැති වනු ඇත", + "statistics_clear": " සංඛ්‍යාලේඛන ඉවත් කරන්න", + "statistics_clear_confirm": "සංඛ්‍යාලේඛන ඉවත් කිරීමට අවශ්‍ය බව ඔබට විශ්වාස ද?", + "statistics_retention_confirm": "සංඛ්‍යාලේඛන රඳවා තබා ගැනීම වෙනස් කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද? ඔබ කාල පරතරයෙහි අගය අඩු කළහොත් සමහර දත්ත නැති වී යනු ඇත", + "statistics_cleared": "සංඛ්‍යාලේඛන සාර්ථකව ඉවත් කරන ලදි", + "interval_hours": "පැය {{count}}", + "interval_hours_plural": "පැය {{count}}", + "filters_configuration": "පෙරහන් වින්‍යාසය", + "filters_enable": "පෙරහන් සබල කරන්න", + "filters_interval": "පෙරහන් යාවත්කාල කාල පරතරය", + "disabled": "අබල කර ඇත", + "username_label": "පරිශීලක නාමය", + "username_placeholder": "පරිශීලක නාමය ඇතුළත් කරන්න", + "password_label": "මුරපදය", + "password_placeholder": "මුරපදය ඇතුළත් කරන්න", + "sign_in": "පුරන්න", + "sign_out": "වරන්න", + "forgot_password": "මුරපදය අමතක වුණා ද?", + "forgot_password_desc": "ඔබගේ පරිශීලක ගිණුම සඳහා නව මුරපදයක් සෑදීමට කරුණාකර <0>මෙම පියවර අනුගමනය කරන්න.", + "location": "ස්ථානය", + "orgname": "සංවිධානයේ නම", + "netname": "ජාල‌යේ නම", + "network": "ජාලය", + "descr": "විස්තරය", + "whois": "Whois", + "blocked_by_response": "ප්‍රතිචාරය අන්වර්ථ නාමයක් (CNAME) හෝ අ.ජා. කෙ. (IP) මගින් අවහිර කර ඇත", + "try_again": "නැවත උත්සහා කරන්න", + "example_rewrite_domain": "මෙම වසම් නාමය සඳහා පමණක් ප්‍රතිචාර නැවත ලියන්න.", + "example_rewrite_wildcard": "<0>example.org සහ එහි සියලුම උප වසම් සඳහා ප්‍රතිචාර නැවත ලියන්න.", + "disable_ipv6": "IPv6 අබල කරන්න", + "disable_ipv6_desc": "මෙම අංගය සක්‍රීය කර ඇත්නම්, IPv6 ලිපින සඳහා වන සියලුම DNS විමසුම් (AAAA වර්ගය) අතහැර දමනු ලැබේ.", + "fastest_addr": "වේගවත්ම අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනය", + "autofix_warning_text": "ඔබ \"නිරාකරණය කරන්න\" මත ක්ලික් කළහොත්, AdGuard Home ඔබගේ පද්ධතිය AdGuard Home DNS සේවාදායකය භාවිතා කිරීමට වින්‍යාසගත කරනු ඇත.", + "tags_title": "හැඳුනුම් සංකේත", + "tags_desc": "අනුග්‍රාහකයට අනුරූප වන හැඳුනුම් සංකේත ඔබට තෝරා ගත හැකිය. පෙරහන් නීති වලට හැඳුනුම් සංකේත ඇතුළත් කළ හැකි අතර ඒවා වඩාත් නිවැරදිව යෙදීමට ඔබට ඉඩ සලසයි. <0>වැඩිදුර ඉගෙන ගන්න", + "form_select_tags": "අනුග්‍රාහක හැඳුනුම් සංකේත", + "check_title": "පෙරීම පරීක්ෂා කරන්න", + "check": "පරීක්ෂා කරන්න", + "form_enter_host": "ධාරක නාමයක් ඇතුළත් කරන්න", + "filtered_custom_rules": "අභිරුචි පෙරීමේ නීති මගින් පෙරහන් කරන ලදි", + "check_ip": "අ.ජා. කෙ. (IP) ලිපින: {{ip}}", + "check_cname": "CNAME: {{cname}}", + "check_reason": "හේතුව: {{reason}}", + "check_rule": "නීතිය: {{rule}}", + "check_service": "සේවාවෙහි නම: {{service}}", + "check_not_found": "ඔබගේ පෙරහන් ලැයිස්තු තුළ සොයා ගත නොහැක", + "client_confirm_block": "{{ip}} අනුග්‍රාහකය අවහිර කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?", + "client_confirm_unblock": "{{ip}} අනුග්‍රාහකය අනවහිර කිරීමට අවශ්‍ය බව ඔබට විශ්වාසද?", + "client_blocked": "අනුග්‍රාහකය \"{{ip}}\" සාර්ථකව අවහිර කරන ලදි", + "client_unblocked": "අනුග්‍රාහකය \"{{ip}}\" සාර්ථකව අනවහිර කරන ලදි", + "static_ip": "ස්ථිතික අ.ජා. කෙ. (IP) ලිපිනය", + "static_ip_desc": "AdGuard Home යනු සේවාදායකයක් බැවින් එය නිසි ලෙස ක්‍රියා කිරීමට ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනයක් අවශ්‍ය වේ. එසේ නොමැතිනම්, යම් අවස්ථාවක දී ඔබගේ මාර්ගකාරකය මෙම උපාංගයට වෙනත් අ.ජා. කෙ. ලිපිනයක් ලබා දිය හැකිය.", + "set_static_ip": "ස්ථිතික අ.ජා. කෙ. (IP) ලිපිනයක් සකසන්න", + "install_static_ok": "සුභ තොරතුරක්! ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනය දැනටමත් වින්‍යාසගත කර ඇත", + "install_static_error": "මෙම ජාල අතුරුමුහුණත සඳහා AdGuard Home හට එය ස්වයංක්‍රීයව වින්‍යාසගත කළ නොහැක. කරුණාකර මෙය අතින් කරන්නේ කෙසේද යන්න පිළිබඳ උපදෙස් සොයා ගන්න.", + "install_static_configure": "ගතික අ.ජා. කෙ. (IP) ලිපිනයක් භාවිතා කරන බව අපි අනාවරණය කර ‌ග‌ෙන ඇත්තෙමු - <0>{{ip}}. එය ඔබගේ ස්ථිතික ලිපිනය ලෙස භාවිතා කිරීමට අවශ්‍යද?", + "confirm_static_ip": "AdGuard Home ඔබේ ස්ථිතික අ.ජා. කෙ. (IP) ලිපිනය ලෙස {{ip}} වින්‍යාසගත කරනු ඇත. ඔබට ඉදිරියට යාමට අවශ්‍යද?", + "list_updated": "{{count}} ලැයිස්තුව යාවත්කාලීන කරන ලදි", + "list_updated_plural": "ලැයිස්තු {{count}} ක් යාවත්කාලීන කරන ලදි", + "dnssec_enable": "DNSSEC සබල කරන්න", + "validated_with_dnssec": "DNSSEC සමඟ තහවුරු කර ඇත", + "show_blocked_responses": "අවහිර කර ඇත", + "blocked_safebrowsing": "ආරක්ෂිත සෙවීම මගින් අවහිර කරන ලද", + "blocked_adult_websites": "අවහිර කළ වැඩිහිටි වෙබ් අඩවි", + "blocked_threats": "අවහිර කළ තර්ජන", + "allowed": "අවසර ලත්", + "filtered": "පෙරහන් කරන ලද", + "rewritten": "නැවත ලියන ලද", + "safe_search": "ආරක්ෂිත සෙවීම", + "blocklist": "අවහිර කිරී‌‌‌‌‌මේ ලැයිස්තුව", + "milliseconds_abbreviation": "මිලි තත්." +} \ No newline at end of file diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 29338e35..a2fa788d 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -3,7 +3,7 @@ import i18next from 'i18next'; import axios from 'axios'; import { splitByNewLine, sortClients } from '../helpers/helpers'; -import { CHECK_TIMEOUT, SETTINGS_NAMES } from '../helpers/constants'; +import { CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES } from '../helpers/constants'; import { areEqualVersions } from '../helpers/version'; import { getTlsStatus } from './encryption'; import apiClient from '../api/Api'; @@ -342,6 +342,8 @@ export const getDhcpStatus = () => async (dispatch) => { dispatch(getDhcpStatusRequest()); try { const status = await apiClient.getDhcpStatus(); + const globalStatus = await apiClient.getGlobalStatus(); + status.dhcp_available = globalStatus.dhcp_available; dispatch(getDhcpStatusSuccess(status)); } catch (error) { dispatch(addErrorToast({ error })); @@ -368,11 +370,56 @@ export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST'); export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS'); export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE'); -export const findActiveDhcp = (name) => async (dispatch) => { +export const findActiveDhcp = (name) => async (dispatch, getState) => { dispatch(findActiveDhcpRequest()); try { const activeDhcp = await apiClient.findActiveDhcp(name); dispatch(findActiveDhcpSuccess(activeDhcp)); + const { check, interface_name } = getState().dhcp; + const v4 = check?.v4 ?? { static_ip: {}, other_server: {} }; + const v6 = check?.v6 ?? { other_server: {} }; + + let isError = false; + + if (v4.other_server.found === STATUS_RESPONSE.ERROR + || v6.other_server.found === STATUS_RESPONSE.ERROR) { + isError = true; + dispatch(addErrorToast({ error: 'dhcp_error' })); + if (v4.other_server.error) { + dispatch(addErrorToast({ error: v4.other_server.error })); + } + if (v6.other_server.error) { + dispatch(addErrorToast({ error: v6.other_server.error })); + } + } + + if (v4.static_ip.static === STATUS_RESPONSE.ERROR) { + isError = true; + dispatch(addErrorToast({ error: 'dhcp_static_ip_error' })); + } + + if (isError) { + return; + } + + if (v4.other_server.found === STATUS_RESPONSE.YES + || v6.other_server.found === STATUS_RESPONSE.YES) { + dispatch(addErrorToast({ error: 'dhcp_found' })); + } else if (v4.static_ip.static === STATUS_RESPONSE.NO + && v4.static_ip.ip + && interface_name) { + const warning = i18next.t('dhcp_dynamic_ip_found', { + interfaceName: interface_name, + ipAddress: v4.static_ip.ip, + interpolation: { + prefix: '<0>{{', + suffix: '}}', + }, + }); + dispatch(addErrorToast({ error: warning })); + } else { + dispatch(addSuccessToast('dhcp_not_found')); + } } catch (error) { dispatch(addErrorToast({ error })); dispatch(findActiveDhcpFailure()); @@ -383,14 +430,11 @@ export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST'); export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS'); export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE'); -export const setDhcpConfig = (values) => async (dispatch, getState) => { - const { config } = getState().dhcp; - const updatedConfig = { ...config, ...values }; +export const setDhcpConfig = (values) => async (dispatch) => { dispatch(setDhcpConfigRequest()); - dispatch(findActiveDhcp(values.interface_name)); try { - await apiClient.setDhcpConfig(updatedConfig); - dispatch(setDhcpConfigSuccess(updatedConfig)); + await apiClient.setDhcpConfig(values); + dispatch(setDhcpConfigSuccess(values)); dispatch(addSuccessToast('dhcp_config_saved')); } catch (error) { dispatch(addErrorToast({ error })); @@ -416,7 +460,6 @@ export const toggleDhcp = (values) => async (dispatch) => { enabled: true, }; successMessage = 'enabled_dhcp'; - dispatch(findActiveDhcp(values.interface_name)); } try { diff --git a/client/src/api/Api.js b/client/src/api/Api.js index e309e3bd..62248fd0 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -32,25 +32,13 @@ class Api { } // Global methods - GLOBAL_STATUS = { - path: 'status', - method: 'GET', - }; + GLOBAL_STATUS = { path: 'status', method: 'GET' } - GLOBAL_TEST_UPSTREAM_DNS = { - path: 'test_upstream_dns', - method: 'POST', - }; + GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' }; - GLOBAL_VERSION = { - path: 'version.json', - method: 'POST', - }; + GLOBAL_VERSION = { path: 'version.json', method: 'POST' }; - GLOBAL_UPDATE = { - path: 'update', - method: 'POST', - }; + GLOBAL_UPDATE = { path: 'update', method: 'POST' }; getGlobalStatus() { const { path, method } = this.GLOBAL_STATUS; @@ -81,45 +69,21 @@ class Api { } // Filtering - FILTERING_STATUS = { - path: 'filtering/status', - method: 'GET', - }; + FILTERING_STATUS = { path: 'filtering/status', method: 'GET' }; - FILTERING_ADD_FILTER = { - path: 'filtering/add_url', - method: 'POST', - }; + FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' }; - FILTERING_REMOVE_FILTER = { - path: 'filtering/remove_url', - method: 'POST', - }; + FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' }; - FILTERING_SET_RULES = { - path: 'filtering/set_rules', - method: 'POST', - }; + FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' }; - FILTERING_REFRESH = { - path: 'filtering/refresh', - method: 'POST', - }; + FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' }; - FILTERING_SET_URL = { - path: 'filtering/set_url', - method: 'POST', - }; + FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' }; - FILTERING_CONFIG = { - path: 'filtering/config', - method: 'POST', - }; + FILTERING_CONFIG = { path: 'filtering/config', method: 'POST' }; - FILTERING_CHECK_HOST = { - path: 'filtering/check_host', - method: 'GET', - }; + FILTERING_CHECK_HOST = { path: 'filtering/check_host', method: 'GET' }; getFilteringStatus() { const { path, method } = this.FILTERING_STATUS; @@ -190,20 +154,11 @@ class Api { } // Parental - PARENTAL_STATUS = { - path: 'parental/status', - method: 'GET', - }; + PARENTAL_STATUS = { path: 'parental/status', method: 'GET' }; - PARENTAL_ENABLE = { - path: 'parental/enable', - method: 'POST', - }; + PARENTAL_ENABLE = { path: 'parental/enable', method: 'POST' }; - PARENTAL_DISABLE = { - path: 'parental/disable', - method: 'POST', - }; + PARENTAL_DISABLE = { path: 'parental/disable', method: 'POST' }; getParentalStatus() { const { path, method } = this.PARENTAL_STATUS; @@ -226,20 +181,11 @@ class Api { } // Safebrowsing - SAFEBROWSING_STATUS = { - path: 'safebrowsing/status', - method: 'GET', - }; + SAFEBROWSING_STATUS = { path: 'safebrowsing/status', method: 'GET' }; - SAFEBROWSING_ENABLE = { - path: 'safebrowsing/enable', - method: 'POST', - }; + SAFEBROWSING_ENABLE = { path: 'safebrowsing/enable', method: 'POST' }; - SAFEBROWSING_DISABLE = { - path: 'safebrowsing/disable', - method: 'POST', - }; + SAFEBROWSING_DISABLE = { path: 'safebrowsing/disable', method: 'POST' }; getSafebrowsingStatus() { const { path, method } = this.SAFEBROWSING_STATUS; @@ -257,20 +203,11 @@ class Api { } // Safesearch - SAFESEARCH_STATUS = { - path: 'safesearch/status', - method: 'GET', - }; + SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' }; - SAFESEARCH_ENABLE = { - path: 'safesearch/enable', - method: 'POST', - }; + SAFESEARCH_ENABLE = { path: 'safesearch/enable', method: 'POST' }; - SAFESEARCH_DISABLE = { - path: 'safesearch/disable', - method: 'POST', - }; + SAFESEARCH_DISABLE = { path: 'safesearch/disable', method: 'POST' }; getSafesearchStatus() { const { path, method } = this.SAFESEARCH_STATUS; @@ -288,15 +225,9 @@ class Api { } // Language - CURRENT_LANGUAGE = { - path: 'i18n/current_language', - method: 'GET', - }; + CURRENT_LANGUAGE = { path: 'i18n/current_language', method: 'GET' }; - CHANGE_LANGUAGE = { - path: 'i18n/change_language', - method: 'POST', - }; + CHANGE_LANGUAGE = { path: 'i18n/change_language', method: 'POST' }; getCurrentLanguage() { const { path, method } = this.CURRENT_LANGUAGE; @@ -313,40 +244,19 @@ class Api { } // DHCP - DHCP_STATUS = { - path: 'dhcp/status', - method: 'GET', - }; + DHCP_STATUS = { path: 'dhcp/status', method: 'GET' }; - DHCP_SET_CONFIG = { - path: 'dhcp/set_config', - method: 'POST', - }; + DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' }; - DHCP_FIND_ACTIVE = { - path: 'dhcp/find_active_dhcp', - method: 'POST', - }; + DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' }; - DHCP_INTERFACES = { - path: 'dhcp/interfaces', - method: 'GET', - }; + DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' }; - DHCP_ADD_STATIC_LEASE = { - path: 'dhcp/add_static_lease', - method: 'POST', - }; + DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' }; - DHCP_REMOVE_STATIC_LEASE = { - path: 'dhcp/remove_static_lease', - method: 'POST', - }; + DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' }; - DHCP_RESET = { - path: 'dhcp/reset', - method: 'POST', - }; + DHCP_RESET = { path: 'dhcp/reset', method: 'POST' }; getDhcpStatus() { const { path, method } = this.DHCP_STATUS; @@ -400,20 +310,11 @@ class Api { } // Installation - INSTALL_GET_ADDRESSES = { - path: 'install/get_addresses', - method: 'GET', - }; + INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' }; - INSTALL_CONFIGURE = { - path: 'install/configure', - method: 'POST', - }; + INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' }; - INSTALL_CHECK_CONFIG = { - path: 'install/check_config', - method: 'POST', - }; + INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' }; getDefaultAddresses() { const { path, method } = this.INSTALL_GET_ADDRESSES; @@ -439,20 +340,11 @@ class Api { } // DNS-over-HTTPS and DNS-over-TLS - TLS_STATUS = { - path: 'tls/status', - method: 'GET', - }; + TLS_STATUS = { path: 'tls/status', method: 'GET' }; - TLS_CONFIG = { - path: 'tls/configure', - method: 'POST', - }; + TLS_CONFIG = { path: 'tls/configure', method: 'POST' }; - TLS_VALIDATE = { - path: 'tls/validate', - method: 'POST', - }; + TLS_VALIDATE = { path: 'tls/validate', method: 'POST' }; getTlsStatus() { const { path, method } = this.TLS_STATUS; @@ -478,30 +370,15 @@ class Api { } // Per-client settings - GET_CLIENTS = { - path: 'clients', - method: 'GET', - }; + GET_CLIENTS = { path: 'clients', method: 'GET' }; - FIND_CLIENTS = { - path: 'clients/find', - method: 'GET', - }; + FIND_CLIENTS = { path: 'clients/find', method: 'GET' }; - ADD_CLIENT = { - path: 'clients/add', - method: 'POST', - }; + ADD_CLIENT = { path: 'clients/add', method: 'POST' }; - DELETE_CLIENT = { - path: 'clients/delete', - method: 'POST', - }; + DELETE_CLIENT = { path: 'clients/delete', method: 'POST' }; - UPDATE_CLIENT = { - path: 'clients/update', - method: 'POST', - }; + UPDATE_CLIENT = { path: 'clients/update', method: 'POST' }; getClients() { const { path, method } = this.GET_CLIENTS; @@ -542,15 +419,9 @@ class Api { } // DNS access settings - ACCESS_LIST = { - path: 'access/list', - method: 'GET', - }; + ACCESS_LIST = { path: 'access/list', method: 'GET' }; - ACCESS_SET = { - path: 'access/set', - method: 'POST', - }; + ACCESS_SET = { path: 'access/set', method: 'POST' }; getAccessList() { const { path, method } = this.ACCESS_LIST; @@ -567,20 +438,11 @@ class Api { } // DNS rewrites - REWRITES_LIST = { - path: 'rewrite/list', - method: 'GET', - }; + REWRITES_LIST = { path: 'rewrite/list', method: 'GET' }; - REWRITE_ADD = { - path: 'rewrite/add', - method: 'POST', - }; + REWRITE_ADD = { path: 'rewrite/add', method: 'POST' }; - REWRITE_DELETE = { - path: 'rewrite/delete', - method: 'POST', - }; + REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' }; getRewritesList() { const { path, method } = this.REWRITES_LIST; @@ -606,15 +468,9 @@ class Api { } // Blocked services - BLOCKED_SERVICES_LIST = { - path: 'blocked_services/list', - method: 'GET', - }; + BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' }; - BLOCKED_SERVICES_SET = { - path: 'blocked_services/set', - method: 'POST', - }; + BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' }; getBlockedServices() { const { path, method } = this.BLOCKED_SERVICES_LIST; @@ -631,25 +487,13 @@ class Api { } // Settings for statistics - GET_STATS = { - path: 'stats', - method: 'GET', - }; + GET_STATS = { path: 'stats', method: 'GET' }; - STATS_INFO = { - path: 'stats_info', - method: 'GET', - }; + STATS_INFO = { path: 'stats_info', method: 'GET' }; - STATS_CONFIG = { - path: 'stats_config', - method: 'POST', - }; + STATS_CONFIG = { path: 'stats_config', method: 'POST' }; - STATS_RESET = { - path: 'stats_reset', - method: 'POST', - }; + STATS_RESET = { path: 'stats_reset', method: 'POST' }; getStats() { const { path, method } = this.GET_STATS; @@ -676,25 +520,13 @@ class Api { } // Query log - GET_QUERY_LOG = { - path: 'querylog', - method: 'GET', - }; + GET_QUERY_LOG = { path: 'querylog', method: 'GET' }; - QUERY_LOG_CONFIG = { - path: 'querylog_config', - method: 'POST', - }; + QUERY_LOG_CONFIG = { path: 'querylog_config', method: 'POST' }; - QUERY_LOG_INFO = { - path: 'querylog_info', - method: 'GET', - }; + QUERY_LOG_INFO = { path: 'querylog_info', method: 'GET' }; - QUERY_LOG_CLEAR = { - path: 'querylog_clear', - method: 'POST', - }; + QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' }; getQueryLog(params) { const { path, method } = this.GET_QUERY_LOG; @@ -722,10 +554,7 @@ class Api { } // Login - LOGIN = { - path: 'login', - method: 'POST', - }; + LOGIN = { path: 'login', method: 'POST' }; login(data) { const { path, method } = this.LOGIN; @@ -737,10 +566,7 @@ class Api { } // Profile - GET_PROFILE = { - path: 'profile', - method: 'GET', - }; + GET_PROFILE = { path: 'profile', method: 'GET' }; getProfile() { const { path, method } = this.GET_PROFILE; @@ -748,15 +574,9 @@ class Api { } // DNS config - GET_DNS_CONFIG = { - path: 'dns_info', - method: 'GET', - }; + GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' }; - SET_DNS_CONFIG = { - path: 'dns_config', - method: 'POST', - }; + SET_DNS_CONFIG = { path: 'dns_config', method: 'POST' }; getDnsConfig() { const { path, method } = this.GET_DNS_CONFIG; diff --git a/client/src/components/App/index.css b/client/src/components/App/index.css index 18d5c2f4..091a6612 100644 --- a/client/src/components/App/index.css +++ b/client/src/components/App/index.css @@ -42,13 +42,6 @@ body { background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%); } -@media (max-width: 575px) { - .container { - padding-right: 0; - padding-left: 0; - } -} - .modal-body--medium { max-height: 20rem; overflow-y: scroll; @@ -65,3 +58,11 @@ body { .mw-75 { max-width: 75% !important; } + +.cursor--not-allowed { + cursor: not-allowed; +} + +.select--no-warning { + margin-bottom: 1.375rem; +} diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js index 8ccd30ef..ca2f13e2 100644 --- a/client/src/components/App/index.js +++ b/client/src/components/App/index.js @@ -31,7 +31,7 @@ import SetupGuide from '../../containers/SetupGuide'; import Settings from '../../containers/Settings'; import Dns from '../../containers/Dns'; import Encryption from '../../containers/Encryption'; -import Dhcp from '../../containers/Dhcp'; +import Dhcp from '../Settings/Dhcp'; import Clients from '../../containers/Clients'; import DnsBlocklist from '../../containers/DnsBlocklist'; import DnsAllowlist from '../../containers/DnsAllowlist'; @@ -39,6 +39,7 @@ import DnsRewrites from '../../containers/DnsRewrites'; import CustomRules from '../../containers/CustomRules'; import Services from '../Filters/Services'; + const ROUTES = [ { path: MENU_URLS.root, @@ -96,10 +97,10 @@ const ROUTES = [ ]; const renderRoute = ({ path, component, exact }, idx) => ; const App = () => { @@ -142,34 +143,28 @@ const App = () => { window.location.reload(); }; - return ( - - <> - {updateAvailable && <> - - - } - {!processingEncryption && } - -
-
- {processing && } - {!isCoreRunning && ( -
-
- - -
-
- )} - {!processing && isCoreRunning && ROUTES.map(renderRoute)} + return + {updateAvailable && <> + + + } + {!processingEncryption && } + +
+
+ {processing && } + {!isCoreRunning &&
+
+ +
-
- - - - - ); +
} + {!processing && isCoreRunning && ROUTES.map(renderRoute)} +
+
+ + + ; }; renderRoute.propTypes = { diff --git a/client/src/components/Dashboard/index.js b/client/src/components/Dashboard/index.js index 71a1366b..1aa426f8 100644 --- a/client/src/components/Dashboard/index.js +++ b/client/src/components/Dashboard/index.js @@ -1,6 +1,6 @@ -import React, { Component, Fragment } from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Trans, withTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import Statistics from './Statistics'; import Counters from './Counters'; @@ -13,144 +13,141 @@ import Loading from '../ui/Loading'; import { BLOCK_ACTIONS } from '../../helpers/constants'; import './Dashboard.css'; -class Dashboard extends Component { - componentDidMount() { - this.getAllStats(); - } +const Dashboard = ({ + getAccessList, + getStats, + getStatsConfig, + dashboard, + toggleProtection, + toggleClientBlock, + stats, + access, +}) => { + const { t } = useTranslation(); - getAllStats = () => { - this.props.getAccessList(); - this.props.getStats(); - this.props.getStatsConfig(); + const getAllStats = () => { + getAccessList(); + getStats(); + getStatsConfig(); }; - getToggleFilteringButton = () => { - const { protectionEnabled, processingProtection } = this.props.dashboard; + useEffect(() => { + getAllStats(); + }, []); + + const getToggleFilteringButton = () => { + const { protectionEnabled, processingProtection } = dashboard; const buttonText = protectionEnabled ? 'disable_protection' : 'enable_protection'; const buttonClass = protectionEnabled ? 'btn-gray' : 'btn-success'; - return ( - - ); + > + {buttonText} + ; }; - toggleClientStatus = (type, ip) => { + const toggleClientStatus = (type, ip) => { const confirmMessage = type === BLOCK_ACTIONS.BLOCK ? 'client_confirm_block' : 'client_confirm_unblock'; - if (window.confirm(this.props.t(confirmMessage, { ip }))) { - this.props.toggleClientBlock(type, ip); + if (window.confirm(t(confirmMessage, { ip }))) { + toggleClientBlock(type, ip); } }; - render() { - const { - dashboard, stats, access, t, - } = this.props; - const statsProcessing = stats.processingStats + const refreshButton = ; + + const subtitle = stats.interval === 1 + ? t('for_last_24_hours') + : t('for_last_days', { count: stats.interval }); + + const refreshFullButton = ; + + const statsProcessing = stats.processingStats || stats.processingGetConfig || access.processing; - const subtitle = stats.interval === 1 - ? t('for_last_24_hours') - : t('for_last_days', { count: stats.interval }); + return <> + +
+ {getToggleFilteringButton()} + {refreshFullButton} +
+
+ {statsProcessing && } + {!statsProcessing &&
+
+ +
+
+ this.getAllStats()} - > - refresh_statics - - ); - - const refreshButton = ( - - ); - - return ( - - -
- {this.getToggleFilteringButton()} - {refreshFullButton} -
-
- {statsProcessing && } - {!statsProcessing && ( -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- )} -
- ); - } -} + refreshButton={refreshButton} + /> +
+
+ +
+
+ +
+
+ +
+
} + ; +}; Dashboard.propTypes = { dashboard: PropTypes.object.isRequired, @@ -160,9 +157,8 @@ Dashboard.propTypes = { getStatsConfig: PropTypes.func.isRequired, toggleProtection: PropTypes.func.isRequired, getClients: PropTypes.func.isRequired, - t: PropTypes.func.isRequired, toggleClientBlock: PropTypes.func.isRequired, getAccessList: PropTypes.func.isRequired, }; -export default withTranslation()(Dashboard); +export default Dashboard; diff --git a/client/src/components/Filters/Form.js b/client/src/components/Filters/Form.js index ac8cc190..a994c1ec 100644 --- a/client/src/components/Filters/Form.js +++ b/client/src/components/Filters/Form.js @@ -5,7 +5,7 @@ import { withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; import classNames from 'classnames'; import { validatePath, validateRequiredValue } from '../../helpers/validators'; -import { renderInputField, renderSelectField } from '../../helpers/form'; +import { renderCheckboxField, renderInputField } from '../../helpers/form'; import { MODAL_OPEN_TIMEOUT, MODAL_TYPE, FORM_NAME } from '../../helpers/constants'; const getIconsData = (homepage, source) => ([ @@ -60,7 +60,7 @@ const renderFilters = ({ categories, filters }, selectedSources, t) => Object.ke { > {t('cancel_btn')} - + }
; }; diff --git a/client/src/components/Header/index.js b/client/src/components/Header/index.js index 1a126b02..071f950f 100644 --- a/client/src/components/Header/index.js +++ b/client/src/components/Header/index.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { shallowEqual, useSelector } from 'react-redux'; -import { Trans } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import classnames from 'classnames'; import Menu from './Menu'; import logo from '../ui/svg/logo.svg'; @@ -9,6 +9,7 @@ import './Header.css'; const Header = () => { const [isMenuOpen, setIsMenuOpen] = useState(false); + const { t } = useTranslation(); const { protectionEnabled, @@ -33,45 +34,42 @@ const Header = () => { 'badge-danger': !protectionEnabled, }); - return ( -
-
-
-
- + return
+
+
+
+ +
+
+
+ + + + {!processing && isCoreRunning + && {t(protectionEnabled ? 'on' : 'off')} + }
-
-
- - - - {!processing && isCoreRunning && ( - - {protectionEnabled ? 'on' : 'off'} - - )} -
-
- -
-
- {!processingProfile && name - && - sign_out - } -
+
+ +
+
+ {!processingProfile && name + && + {t('sign_out')} + }
- ); +
; }; export default Header; diff --git a/client/src/components/Logs/Filters/index.js b/client/src/components/Logs/Filters/index.js index 89901001..fae8b06c 100644 --- a/client/src/components/Logs/Filters/index.js +++ b/client/src/components/Logs/Filters/index.js @@ -1,29 +1,31 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Trans } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import Form from './Form'; -const Filters = ({ filter, refreshLogs, setIsLoading }) => ( -
-

- query_log - -

-
+ + + + + + -
-); + /> +
; +}; Filters.propTypes = { filter: PropTypes.object.isRequired, diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index cb5458dd..2142b1a5 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -15,7 +15,7 @@ import { toggleAllServices } from '../../../helpers/helpers'; import { renderInputField, renderGroupField, - renderSelectField, + renderCheckboxField, renderServiceField, } from '../../../helpers/form'; import { validateClientId, validateRequiredValue } from '../../../helpers/validators'; @@ -151,7 +151,7 @@ let Form = (props) => { ( - Object.keys(interfaces).map((item) => { - const option = interfaces[item]; - const { name } = option; - const onlyIPv6 = option.ip_addresses.every((ip) => ip.includes(':')); - let interfaceIP = option.ip_addresses[0]; - - if (!onlyIPv6) { - option.ip_addresses.forEach((ip) => { - if (!ip.includes(':')) { - interfaceIP = ip; - } - }); - } - - return ( - - ); - }) -)); - -const renderInterfaceValues = ((interfaceValues) => ( -
    -
  • - MTU: - {interfaceValues.mtu} -
  • -
  • - dhcp_hardware_address: - {interfaceValues.hardware_address} -
  • -
  • - dhcp_ip_addresses: - { - interfaceValues.ip_addresses - .map((ip) => {ip}) - } -
  • -
-)); - -const clearFields = (change, resetDhcp, t) => { - const fields = { - interface_name: '', - gateway_ip: '', - subnet_mask: '', - range_start: '', - range_end: '', - lease_duration: 86400, - }; - - // eslint-disable-next-line no-alert - if (window.confirm(t('dhcp_reset'))) { - Object.keys(fields).forEach((field) => change(field, fields[field])); - resetDhcp(); - } -}; - -let Form = (props) => { - const { - t, - handleSubmit, - submitting, - invalid, - enabled, - interfaces, - interfaceValue, - processingConfig, - processingInterfaces, - resetDhcp, - change, - } = props; - - return ( - - {!processingInterfaces && interfaces - &&
-
-
- - - - {renderInterfaces(interfaces)} - -
-
- {interfaceValue - &&
- {interfaces[interfaceValue] - && renderInterfaceValues(interfaces[interfaceValue])} -
- } -
- } -
-
-
-
- - -
-
- - -
-
-
-
-
-
- -
-
- -
-
- -
-
-
-
- - -
-
-
- -
- - -
- - ); -}; - -Form.propTypes = { - handleSubmit: PropTypes.func.isRequired, - submitting: PropTypes.bool.isRequired, - invalid: PropTypes.bool.isRequired, - interfaces: PropTypes.object.isRequired, - interfaceValue: PropTypes.string, - initialValues: PropTypes.object.isRequired, - processingConfig: PropTypes.bool.isRequired, - processingInterfaces: PropTypes.bool.isRequired, - enabled: PropTypes.bool.isRequired, - t: PropTypes.func.isRequired, - resetDhcp: PropTypes.func.isRequired, - change: PropTypes.func.isRequired, -}; - -const selector = formValueSelector(FORM_NAME.DHCP); - -Form = connect((state) => { - const interfaceValue = selector(state, 'interface_name'); - return { - interfaceValue, - }; -})(Form); - -export default flow([ - withTranslation(), - reduxForm({ form: FORM_NAME.DHCP }), -])(Form); diff --git a/client/src/components/Settings/Dhcp/FormDHCPv4.js b/client/src/components/Settings/Dhcp/FormDHCPv4.js new file mode 100644 index 00000000..b938b6f0 --- /dev/null +++ b/client/src/components/Settings/Dhcp/FormDHCPv4.js @@ -0,0 +1,145 @@ +import React, { useCallback } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { useTranslation } from 'react-i18next'; + +import { + renderInputField, + toNumber, +} from '../../../helpers/form'; +import { FORM_NAME } from '../../../helpers/constants'; +import { + validateIpv4, + validateIsPositiveValue, + validateRequiredValue, + validateIpv4RangeEnd, +} from '../../../helpers/validators'; + +const FormDHCPv4 = ({ + handleSubmit, + submitting, + processingConfig, + ipv4placeholders, +}) => { + const { t } = useTranslation(); + const dhcp = useSelector((state) => state.form[FORM_NAME.DHCPv4], shallowEqual); + const interfaces = useSelector((state) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual); + const interface_name = interfaces?.values?.interface_name; + + const isInterfaceIncludesIpv4 = useSelector( + (state) => !!state.dhcp?.interfaces?.[interface_name]?.ipv4_addresses, + ); + + const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}) + .some(Boolean); + + const invalid = dhcp?.syncErrors || interfaces?.syncErrors || !isInterfaceIncludesIpv4 + || isEmptyConfig || submitting || processingConfig; + + const validateRequired = useCallback((value) => { + if (isEmptyConfig) { + return undefined; + } + return validateRequiredValue(value); + }, [isEmptyConfig]); + + return
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
+ +
+
; +}; + +FormDHCPv4.propTypes = { + handleSubmit: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + initialValues: PropTypes.object.isRequired, + processingConfig: PropTypes.bool.isRequired, + change: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + ipv4placeholders: PropTypes.object.isRequired, +}; + +export default reduxForm({ + form: FORM_NAME.DHCPv4, +})(FormDHCPv4); diff --git a/client/src/components/Settings/Dhcp/FormDHCPv6.js b/client/src/components/Settings/Dhcp/FormDHCPv6.js new file mode 100644 index 00000000..80219fd6 --- /dev/null +++ b/client/src/components/Settings/Dhcp/FormDHCPv6.js @@ -0,0 +1,120 @@ +import React, { useCallback } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Field, reduxForm } from 'redux-form'; +import { useTranslation } from 'react-i18next'; + +import { + renderInputField, + toNumber, +} from '../../../helpers/form'; +import { FORM_NAME } from '../../../helpers/constants'; +import { + validateIpv6, + validateIsPositiveValue, + validateRequiredValue, +} from '../../../helpers/validators'; + +const FormDHCPv6 = ({ + handleSubmit, + submitting, + processingConfig, + ipv6placeholders, +}) => { + const { t } = useTranslation(); + const dhcp = useSelector((state) => state.form[FORM_NAME.DHCPv6], shallowEqual); + const interfaces = useSelector((state) => state.form[FORM_NAME.DHCP_INTERFACES], shallowEqual); + const interface_name = interfaces?.values?.interface_name; + + const isInterfaceIncludesIpv6 = useSelector( + (state) => !!state.dhcp?.interfaces?.[interface_name]?.ipv6_addresses, + ); + + const isEmptyConfig = !Object.values(dhcp?.values?.v6 ?? {}) + .some(Boolean); + + const invalid = dhcp?.syncErrors || interfaces?.syncErrors || !isInterfaceIncludesIpv6 + || isEmptyConfig || submitting || processingConfig; + + const validateRequired = useCallback((value) => { + if (isEmptyConfig) { + return undefined; + } + return validateRequiredValue(value); + }, [isEmptyConfig]); + + return
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ + +
+
+
+ +
+
; +}; + +FormDHCPv6.propTypes = { + handleSubmit: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + initialValues: PropTypes.object.isRequired, + processingConfig: PropTypes.bool.isRequired, + change: PropTypes.func.isRequired, + reset: PropTypes.func.isRequired, + ipv6placeholders: PropTypes.object.isRequired, +}; + +export default reduxForm({ + form: FORM_NAME.DHCPv6, +})(FormDHCPv6); diff --git a/client/src/components/Settings/Dhcp/Interfaces.js b/client/src/components/Settings/Dhcp/Interfaces.js new file mode 100644 index 00000000..0674c3fd --- /dev/null +++ b/client/src/components/Settings/Dhcp/Interfaces.js @@ -0,0 +1,109 @@ +import React from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import { Field, reduxForm } from 'redux-form'; +import { Trans, useTranslation } from 'react-i18next'; +import propTypes from 'prop-types'; +import { renderSelectField } from '../../../helpers/form'; +import { validateRequiredValue } from '../../../helpers/validators'; +import { FORM_NAME } from '../../../helpers/constants'; + +const renderInterfaces = (interfaces) => Object.keys(interfaces) + .map((item) => { + const option = interfaces[item]; + const { name } = option; + + const [interfaceIPv4] = option?.ipv4_addresses ?? []; + const [interfaceIPv6] = option?.ipv6_addresses ?? []; + + const optionContent = [name, interfaceIPv4, interfaceIPv6].filter(Boolean).join(' - '); + + return ; + }); + + +const getInterfaceValues = ({ + gateway_ip, + hardware_address, + ip_addresses, +}) => [ + { + name: 'dhcp_form_gateway_input', + value: gateway_ip, + }, + { + name: 'dhcp_hardware_address', + value: hardware_address, + }, + { + name: 'dhcp_ip_addresses', + value: ip_addresses, + render: (ip_addresses) => ip_addresses + .map((ip) => {ip}), + }, +]; + +const renderInterfaceValues = ({ + gateway_ip, + hardware_address, + ip_addresses, +}) =>
+
    + {getInterfaceValues({ + gateway_ip, + hardware_address, + ip_addresses, + }).map(({ name, value, render }) => value &&
  • + {name}: + {render?.(value) || value} +
  • )} +
+
; + +const Interfaces = () => { + const { t } = useTranslation(); + + const { + processingInterfaces, + interfaces, + enabled, + } = useSelector((store) => store.dhcp, shallowEqual); + + const interface_name = useSelector( + (store) => store.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name, + ); + + const interfaceValue = interface_name && interfaces[interface_name]; + + return !processingInterfaces + && interfaces + && <> +
+
+ + + {renderInterfaces(interfaces)} + +
+ {interfaceValue + && renderInterfaceValues(interfaceValue)} +
+ ; +}; + +renderInterfaceValues.propTypes = { + gateway_ip: propTypes.string.isRequired, + hardware_address: propTypes.string.isRequired, + ip_addresses: propTypes.arrayOf(propTypes.string).isRequired, +}; + +export default reduxForm({ + form: FORM_NAME.DHCP_INTERFACES, +})(Interfaces); diff --git a/client/src/components/Settings/Dhcp/StaticLeases/Form.js b/client/src/components/Settings/Dhcp/StaticLeases/Form.js index 40854f6e..57db976d 100644 --- a/client/src/components/Settings/Dhcp/StaticLeases/Form.js +++ b/client/src/components/Settings/Dhcp/StaticLeases/Form.js @@ -1,22 +1,27 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Field, reduxForm } from 'redux-form'; -import { Trans, withTranslation } from 'react-i18next'; -import flow from 'lodash/flow'; +import { Trans, useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; import { renderInputField } from '../../../../helpers/form'; import { validateIpv4, validateMac, validateRequiredValue } from '../../../../helpers/validators'; import { FORM_NAME } from '../../../../helpers/constants'; +import { toggleLeaseModal } from '../../../../actions'; -const Form = (props) => { - const { - t, - handleSubmit, - reset, - pristine, - submitting, - toggleLeaseModal, - processingAdding, - } = props; +const Form = ({ + handleSubmit, + reset, + pristine, + submitting, + processingAdding, +}) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const onClick = () => { + reset(); + dispatch(toggleLeaseModal()); + }; return (
@@ -61,10 +66,7 @@ const Form = (props) => { type="button" className="btn btn-secondary btn-standard" disabled={submitting} - onClick={() => { - reset(); - toggleLeaseModal(); - }} + onClick={onClick} > cancel_btn @@ -86,12 +88,7 @@ Form.propTypes = { handleSubmit: PropTypes.func.isRequired, reset: PropTypes.func.isRequired, submitting: PropTypes.bool.isRequired, - toggleLeaseModal: PropTypes.func.isRequired, processingAdding: PropTypes.bool.isRequired, - t: PropTypes.func.isRequired, }; -export default flow([ - withTranslation(), - reduxForm({ form: FORM_NAME.LEASE }), -])(Form); +export default reduxForm({ form: FORM_NAME.LEASE })(Form); diff --git a/client/src/components/Settings/Dhcp/StaticLeases/Modal.js b/client/src/components/Settings/Dhcp/StaticLeases/Modal.js index 1626159f..1d5f4e8a 100644 --- a/client/src/components/Settings/Dhcp/StaticLeases/Modal.js +++ b/client/src/components/Settings/Dhcp/StaticLeases/Modal.js @@ -2,36 +2,37 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Trans, withTranslation } from 'react-i18next'; import ReactModal from 'react-modal'; - +import { useDispatch } from 'react-redux'; import Form from './Form'; +import { toggleLeaseModal } from '../../../../actions'; -const Modal = (props) => { - const { - isModalOpen, - handleSubmit, - toggleLeaseModal, - processingAdding, - } = props; +const Modal = ({ + isModalOpen, + handleSubmit, + processingAdding, +}) => { + const dispatch = useDispatch(); + + const toggleModal = () => dispatch(toggleLeaseModal()); return ( toggleLeaseModal()} + onRequestClose={toggleModal} >

dhcp_new_static_lease

-
@@ -42,7 +43,6 @@ const Modal = (props) => { Modal.propTypes = { isModalOpen: PropTypes.bool.isRequired, handleSubmit: PropTypes.func.isRequired, - toggleLeaseModal: PropTypes.func.isRequired, processingAdding: PropTypes.bool.isRequired, }; diff --git a/client/src/components/Settings/Dhcp/StaticLeases/index.js b/client/src/components/Settings/Dhcp/StaticLeases/index.js index 72383cc4..d170be40 100644 --- a/client/src/components/Settings/Dhcp/StaticLeases/index.js +++ b/client/src/components/Settings/Dhcp/StaticLeases/index.js @@ -1,115 +1,116 @@ -import React, { Component, Fragment } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import ReactTable from 'react-table'; -import { Trans, withTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../../helpers/constants'; import { sortIp } from '../../../../helpers/helpers'; import Modal from './Modal'; +import { addStaticLease, removeStaticLease } from '../../../../actions'; -class StaticLeases extends Component { - cellWrap = ({ value }) => ( -
+const cellWrap = ({ value }) => ( +
{value} -
- ); +
+); - handleSubmit = (data) => { - this.props.addStaticLease(data); +const StaticLeases = ({ + isModalOpen, + processingAdding, + processingDeleting, + staticLeases, +}) => { + const [t] = useTranslation(); + const dispatch = useDispatch(); + + const handleSubmit = (data) => { + dispatch(addStaticLease(data)); }; - handleDelete = (ip, mac, hostname = '') => { + const handleDelete = (ip, mac, hostname = '') => { const name = hostname || ip; // eslint-disable-next-line no-alert - if (window.confirm(this.props.t('delete_confirm', { key: name }))) { - this.props.removeStaticLease({ ip, mac, hostname }); + if (window.confirm(t('delete_confirm', { key: name }))) { + dispatch(removeStaticLease({ + ip, + mac, + hostname, + })); } }; - render() { - const { - isModalOpen, - toggleLeaseModal, - processingAdding, - processingDeleting, - staticLeases, - t, - } = this.props; - return ( - - dhcp_table_hostname, - accessor: 'hostname', - Cell: this.cellWrap, - }, - { - Header: actions_table_header, - accessor: 'actions', - maxWidth: 150, - Cell: (row) => { - const { ip, mac, hostname } = row.original; + return ( + <> + dhcp_table_hostname, + accessor: 'hostname', + Cell: cellWrap, + }, + { + Header: actions_table_header, + accessor: 'actions', + maxWidth: 150, + // eslint-disable-next-line react/display-name + Cell: (row) => { + const { ip, mac, hostname } = row.original; - return ( -
- -
- ); - }, + return
+ +
; }, - ]} - pageSize={LEASES_TABLE_DEFAULT_PAGE_SIZE} - showPageSizeOptions={false} - showPagination={staticLeases.length > LEASES_TABLE_DEFAULT_PAGE_SIZE} - noDataText={t('dhcp_static_leases_not_found')} - className="-striped -highlight card-table-overflow" - minRows={6} - /> - -
- ); - } -} + }, + ]} + pageSize={LEASES_TABLE_DEFAULT_PAGE_SIZE} + showPageSizeOptions={false} + showPagination={staticLeases.length > LEASES_TABLE_DEFAULT_PAGE_SIZE} + noDataText={t('dhcp_static_leases_not_found')} + className="-striped -highlight card-table-overflow" + minRows={6} + /> + + + ); +}; StaticLeases.propTypes = { staticLeases: PropTypes.array.isRequired, isModalOpen: PropTypes.bool.isRequired, - toggleLeaseModal: PropTypes.func.isRequired, - removeStaticLease: PropTypes.func.isRequired, - addStaticLease: PropTypes.func.isRequired, processingAdding: PropTypes.bool.isRequired, processingDeleting: PropTypes.bool.isRequired, - t: PropTypes.func.isRequired, }; -export default withTranslation()(StaticLeases); +cellWrap.propTypes = { + value: PropTypes.string.isRequired, +}; + +export default StaticLeases; diff --git a/client/src/components/Settings/Dhcp/index.js b/client/src/components/Settings/Dhcp/index.js index 061cde39..1def906c 100644 --- a/client/src/components/Settings/Dhcp/index.js +++ b/client/src/components/Settings/Dhcp/index.js @@ -1,274 +1,277 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { Trans, withTranslation } from 'react-i18next'; +import React, { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; -import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants'; -import Form from './Form'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import classNames from 'classnames'; +import { destroy } from 'redux-form'; +import { + DHCP_DESCRIPTION_PLACEHOLDERS, + DHCP_FORM_NAMES, + STATUS_RESPONSE, + FORM_NAME, +} from '../../../helpers/constants'; import Leases from './Leases'; import StaticLeases from './StaticLeases/index'; import Card from '../../ui/Card'; -import Accordion from '../../ui/Accordion'; import PageTitle from '../../ui/PageTitle'; import Loading from '../../ui/Loading'; +import { + findActiveDhcp, + getDhcpInterfaces, + getDhcpStatus, + resetDhcp, + setDhcpConfig, + toggleDhcp, + toggleLeaseModal, +} from '../../../actions'; +import FormDHCPv4 from './FormDHCPv4'; +import FormDHCPv6 from './FormDHCPv6'; +import Interfaces from './Interfaces'; +import { + calculateDhcpPlaceholdersIpv4, + calculateDhcpPlaceholdersIpv6, +} from '../../../helpers/helpers'; -class Dhcp extends Component { - componentDidMount() { - this.props.getDhcpStatus(); - this.props.getDhcpInterfaces(); - } +const Dhcp = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const { + processingStatus, + processingConfig, + processing, + processingInterfaces, + check, + leases, + staticLeases, + isModalOpen, + processingAdding, + processingDeleting, + processingDhcp, + v4, + v6, + interface_name: interfaceName, + enabled, + dhcp_available, + interfaces, + } = useSelector((state) => state.dhcp, shallowEqual); - handleFormSubmit = (values) => { - if (values.interface_name) { - this.props.setDhcpConfig(values); + const interface_name = useSelector( + (state) => state.form[FORM_NAME.DHCP_INTERFACES]?.values?.interface_name, + ); + + const [ipv4placeholders, setIpv4Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv4); + const [ipv6placeholders, setIpv6Placeholders] = useState(DHCP_DESCRIPTION_PLACEHOLDERS.ipv6); + + useEffect(() => { + dispatch(getDhcpStatus()); + dispatch(getDhcpInterfaces()); + }, []); + + useEffect(() => { + const [ipv4] = interfaces?.[interface_name]?.ipv4_addresses ?? []; + const [ipv6] = interfaces?.[interface_name]?.ipv6_addresses ?? []; + const gateway_ip = interfaces?.[interface_name]?.gateway_ip; + + const v4placeholders = ipv4 + ? calculateDhcpPlaceholdersIpv4(ipv4, gateway_ip) + : DHCP_DESCRIPTION_PLACEHOLDERS.ipv4; + + const v6placeholders = ipv6 + ? calculateDhcpPlaceholdersIpv6() + : DHCP_DESCRIPTION_PLACEHOLDERS.ipv6; + + setIpv4Placeholders(v4placeholders); + setIpv6Placeholders(v6placeholders); + }, [interface_name]); + + const clear = () => { + // eslint-disable-next-line no-alert + if (window.confirm(t('dhcp_reset'))) { + Object.values(DHCP_FORM_NAMES) + .forEach((formName) => dispatch(destroy(formName))); + dispatch(resetDhcp()); } }; - handleToggle = (config) => { - this.props.toggleDhcp(config); + const handleSubmit = (values) => { + dispatch(setDhcpConfig({ + interface_name, + ...values, + })); }; - getToggleDhcpButton = () => { - const { - config, check, processingDhcp, processingConfig, - } = this.props.dhcp; - const otherDhcpFound = check?.otherServer - && check.otherServer.found === DHCP_STATUS_RESPONSE.YES; - const filledConfig = Object.keys(config) - .every((key) => { - if (key === 'enabled' || key === 'icmp_timeout_msec') { - return true; - } + const enteredSomeV4Value = Object.values(v4) + .some(Boolean); + const enteredSomeV6Value = Object.values(v6) + .some(Boolean); + const enteredSomeValue = enteredSomeV4Value || enteredSomeV6Value || interfaceName; - return config[key]; - }); + const getToggleDhcpButton = () => { + const otherDhcpFound = check && (check.v4.other_server.found === STATUS_RESPONSE.YES + || check.v6.other_server.found === STATUS_RESPONSE.YES); - if (config.enabled) { - return ( - - ); - } + const filledConfig = interface_name && (Object.values(v4) + .every(Boolean) || Object.values(v6) + .every(Boolean)); - return ( - - ); - }; - - getActiveDhcpMessage = (t, check) => { - const { found } = check.otherServer; - - if (found === DHCP_STATUS_RESPONSE.ERROR) { - return ( -
- dhcp_error -
- - {check.otherServer.error} - -
-
- ); - } - - return ( -
- {found === DHCP_STATUS_RESPONSE.YES ? ( -
- dhcp_found -
- ) : ( -
- dhcp_not_found -
- )} -
- ); - }; - - getDhcpWarning = (check) => { - if (check.otherServer.found === DHCP_STATUS_RESPONSE.NO) { - return ''; - } - - return ( -
- dhcp_warning -
- ); - }; - - getStaticIpWarning = (t, check, interfaceName) => { - if (check.staticIP.static === DHCP_STATUS_RESPONSE.ERROR) { - return <> -
- dhcp_static_ip_error -
- - {check.staticIP.error} - -
-
-
- ; - } - if (check.staticIP.static === DHCP_STATUS_RESPONSE.NO - && check.staticIP.ip - && interfaceName) { - return <> -
- example]} - values={{ - interfaceName, - ipAddress: check.staticIP.ip, - }} - > - dhcp_dynamic_ip_found - -
-
- ; - } - - return ''; - }; - - render() { - const { - t, - dhcp, - resetDhcp, - findActiveDhcp, - addStaticLease, - removeStaticLease, - toggleLeaseModal, - } = this.props; - - const statusButtonClass = classnames({ - 'btn btn-primary btn-standard': true, - 'btn btn-primary btn-standard btn-loading': dhcp.processingStatus, + const className = classNames('btn btn-sm mr-2', { + 'btn-gray': enabled, + 'btn-outline-success': !enabled, }); - const { enabled, interface_name, ...values } = dhcp.config; - return <> - - {(dhcp.processing || dhcp.processingInterfaces) && } - {!dhcp.processing && !dhcp.processingInterfaces && <> - -
- <> - -
-
- {this.getToggleDhcpButton()} - -
- {!enabled && dhcp.check && ( - <> - {this.getStaticIpWarning(t, dhcp.check, interface_name)} - {this.getActiveDhcpMessage(t, dhcp.check)} - {this.getDhcpWarning(dhcp.check)} - - )} - -
-
- {dhcp.config.enabled && ( - -
-
- -
-
-
- )} - -
-
- -
-
- -
-
-
- } - ; + const onClickDisable = () => dispatch(toggleDhcp({ enabled })); + const onClickEnable = () => { + const values = { + enabled, + interface_name, + v4: enteredSomeV4Value ? v4 : {}, + v6: enteredSomeV6Value ? v6 : {}, + }; + dispatch(toggleDhcp(values)); + }; + + return ; + }; + + const statusButtonClass = classNames('btn btn-sm mx-2', { + 'btn-loading btn-primary': processingStatus, + 'btn-outline-primary': !processingStatus, + }); + + const onClick = () => dispatch(findActiveDhcp(interface_name)); + + const toggleModal = () => dispatch(toggleLeaseModal()); + + const initialV4 = enteredSomeV4Value ? v4 : {}; + const initialV6 = enteredSomeV6Value ? v6 : {}; + + if (processing || processingInterfaces) { + return ; } -} -Dhcp.propTypes = { - dhcp: PropTypes.object.isRequired, - toggleDhcp: PropTypes.func.isRequired, - getDhcpStatus: PropTypes.func.isRequired, - setDhcpConfig: PropTypes.func.isRequired, - findActiveDhcp: PropTypes.func.isRequired, - addStaticLease: PropTypes.func.isRequired, - removeStaticLease: PropTypes.func.isRequired, - toggleLeaseModal: PropTypes.func.isRequired, - getDhcpInterfaces: PropTypes.func.isRequired, - t: PropTypes.func.isRequired, - resetDhcp: PropTypes.func.isRequired, + if (!processing && !dhcp_available) { + return
+

+ unavailable_dhcp +

+

+ unavailable_dhcp_desc +

+
; + } + + const toggleDhcpButton = getToggleDhcpButton(); + + return <> + +
+
+ {toggleDhcpButton} + + +
+
+
+ {!processing && !processingInterfaces + && <> + {!enabled + && check + && (check.v4.other_server.found !== STATUS_RESPONSE.NO + || check.v6.other_server.found !== STATUS_RESPONSE.NO) + &&
+
+
+ dhcp_warning +
+
} + + +
+ +
+
+ +
+ +
+
+ {enabled + && +
+
+ +
+
+
} + +
+
+ +
+
+ +
+
+
+ } + ; }; -export default withTranslation()(Dhcp); +export default Dhcp; diff --git a/client/src/components/Settings/Dns/Config/Form.js b/client/src/components/Settings/Dns/Config/Form.js index 58b7d2c0..0d4a23ee 100644 --- a/client/src/components/Settings/Dns/Config/Form.js +++ b/client/src/components/Settings/Dns/Config/Form.js @@ -6,7 +6,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { renderInputField, renderRadioField, - renderSelectField, + renderCheckboxField, toNumber, } from '../../../../helpers/form'; import { @@ -96,7 +96,7 @@ const Form = ({ { const errors = {}; @@ -36,8 +36,8 @@ const clearFields = (change, setTlsConfig, t) => { certificate_chain: '', private_key_path: '', certificate_path: '', - port_https: 443, - port_dns_over_tls: 853, + port_https: STANDARD_HTTPS_PORT, + port_dns_over_tls: DNS_OVER_TLS_PORT, server_name: '', force_https: false, enabled: false, @@ -96,7 +96,7 @@ let Form = (props) => { @@ -133,7 +133,7 @@ let Form = (props) => { { @@ -49,7 +49,7 @@ const Form = (props) => { name="enabled" type="checkbox" modifier="checkbox--settings" - component={renderSelectField} + component={renderCheckboxField} placeholder={t('block_domain_use_filters_and_hosts')} subtitle={t('filters_block_toggle_hint')} onChange={handleChange} diff --git a/client/src/components/Settings/LogsConfig/Form.js b/client/src/components/Settings/LogsConfig/Form.js index 74454bac..aa3b61b3 100644 --- a/client/src/components/Settings/LogsConfig/Form.js +++ b/client/src/components/Settings/LogsConfig/Form.js @@ -4,7 +4,7 @@ import { Field, reduxForm } from 'redux-form'; import { Trans, withTranslation } from 'react-i18next'; import flow from 'lodash/flow'; -import { renderSelectField, renderRadioField, toNumber } from '../../../helpers/form'; +import { renderCheckboxField, renderRadioField, toNumber } from '../../../helpers/form'; import { FORM_NAME, QUERY_LOG_INTERVALS_DAYS } from '../../../helpers/constants'; const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.map((interval) => { @@ -35,7 +35,7 @@ const Form = (props) => { @@ -44,7 +44,7 @@ const Form = (props) => { { - this.setState((prevState) => ({ isOpen: !prevState.isOpen })); - }; - - render() { - const accordionClass = this.state.isOpen - ? 'accordion__label accordion__label--open' - : 'accordion__label'; - - return ( -
-
- {this.props.label} -
- {this.state.isOpen && ( -
- {this.props.children} -
- )} -
- ); - } -} - -Accordion.propTypes = { - children: PropTypes.node.isRequired, - label: PropTypes.string.isRequired, -}; - -export default Accordion; diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index d948601d..46290b9a 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -48,7 +48,6 @@ export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html'; export const PORT_53_FAQ_LINK = 'https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ#bindinuse'; export const ADDRESS_IN_USE_TEXT = 'address already in use'; -export const UBUNTU_SYSTEM_PORT = 53; export const INSTALL_FIRST_STEP = 1; export const INSTALL_TOTAL_STEPS = 5; @@ -63,6 +62,8 @@ export const SETTINGS_NAMES = { export const STANDARD_DNS_PORT = 53; export const STANDARD_WEB_PORT = 80; export const STANDARD_HTTPS_PORT = 443; +export const DNS_OVER_TLS_PORT = 853; +export const MAX_PORT = 65535; export const EMPTY_DATE = '0001-01-01T00:00:00Z'; @@ -144,7 +145,7 @@ export const UNSAFE_PORTS = [ export const ALL_INTERFACES_IP = '0.0.0.0'; -export const DHCP_STATUS_RESPONSE = { +export const STATUS_RESPONSE = { YES: 'yes', NO: 'no', ERROR: 'error', @@ -458,6 +459,12 @@ export const IP_MATCH_LIST_STATUS = { CIDR: 'CIDR', // the ip is in the specified CIDR range }; +export const DHCP_FORM_NAMES = { + DHCPv4: 'dhcpv4', + DHCPv6: 'dhcpv6', + DHCP_INTERFACES: 'dhcpInterfaces', +}; + export const FORM_NAME = { UPSTREAM: 'upstream', DOMAIN_CHECK: 'domainCheck', @@ -465,7 +472,6 @@ export const FORM_NAME = { REWRITES: 'rewrites', LOGS_FILTER: 'logsFilter', CLIENT: 'client', - DHCP: 'dhcp', LEASE: 'lease', ACCESS: 'access', BLOCKING_MODE: 'blockingMode', @@ -477,9 +483,39 @@ export const FORM_NAME = { INSTALL: 'install', LOGIN: 'login', CACHE: 'cache', + ...DHCP_FORM_NAMES, }; export const SMALL_SCREEN_SIZE = 767; export const MEDIUM_SCREEN_SIZE = 1023; export const SECONDS_IN_HOUR = 60 * 60; + +export const SECONDS_IN_DAY = SECONDS_IN_HOUR * 24; + +export const DHCP_VALUES_PLACEHOLDERS = { + ipv4: { + subnet_mask: '255.255.255.0', + lease_duration: SECONDS_IN_DAY.toString(), + }, + ipv6: { + range_start: '2001::1', + range_end: 'ff', + lease_duration: SECONDS_IN_DAY.toString(), + }, +}; + +export const DHCP_DESCRIPTION_PLACEHOLDERS = { + ipv4: { + gateway_ip: 'dhcp_form_gateway_input', + subnet_mask: 'dhcp_form_subnet_input', + range_start: 'dhcp_form_range_start', + range_end: 'dhcp_form_range_end', + lease_duration: 'dhcp_form_lease_input', + }, + ipv6: { + range_start: 'dhcp_form_range_start', + range_end: 'dhcp_form_range_end', + lease_duration: 'dhcp_form_lease_input', + }, +}; diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index b1755038..f26c64a5 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -1,5 +1,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; +import { Trans } from 'react-i18next'; +import classNames from 'classnames'; import { createOnBlurHandler } from './helpers'; import { R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from './constants'; @@ -24,11 +26,12 @@ export const renderField = (props, elementType) => { step, onBlur, }); + return ( <> {element} {!disabled && touched && error - && {error}} + && {error}} ); }; @@ -47,7 +50,7 @@ renderField.propTypes = { step: PropTypes.number, meta: PropTypes.shape({ touched: PropTypes.bool, - error: PropTypes.object, + error: PropTypes.string, }).isRequired, }; @@ -71,7 +74,7 @@ export const renderGroupField = ({ const onBlur = (event) => createOnBlurHandler(event, input, normalizeOnBlur); return ( - + <>
{!disabled && touched && error - && {error}} - + && {error}} + ); }; @@ -115,7 +118,7 @@ renderGroupField.propTypes = { removeField: PropTypes.func, meta: PropTypes.shape({ touched: PropTypes.bool, - error: PropTypes.object, + error: PropTypes.string, }).isRequired, normalizeOnBlur: PropTypes.func, }; @@ -137,7 +140,8 @@ export const renderRadioField = ({ {!disabled && touched - && (error && {error})} + && error + && {error}} ; renderRadioField.propTypes = { @@ -147,11 +151,11 @@ renderRadioField.propTypes = { disabled: PropTypes.bool, meta: PropTypes.shape({ touched: PropTypes.bool, - error: PropTypes.object, + error: PropTypes.string, }).isRequired, }; -export const renderSelectField = ({ +export const renderCheckboxField = ({ input, placeholder, subtitle, @@ -163,7 +167,8 @@ export const renderSelectField = ({ }) => <> {!disabled && touched - && error && {error}} + && error + && {error}} ; -renderSelectField.propTypes = { +renderCheckboxField.propTypes = { input: PropTypes.object.isRequired, placeholder: PropTypes.string, subtitle: PropTypes.string, @@ -191,7 +197,37 @@ renderSelectField.propTypes = { checked: PropTypes.bool, meta: PropTypes.shape({ touched: PropTypes.bool, - error: PropTypes.object, + error: PropTypes.string, + }).isRequired, +}; + +export const renderSelectField = ({ + input, + meta: { touched, error }, + children, + label, +}) => { + const showWarning = touched && error; + const selectClass = classNames('form-control custom-select', { + 'select--no-warning': !showWarning, + }); + + return <> + {label && } + + {showWarning + && {error}} + ; +}; + +renderSelectField.propTypes = { + input: PropTypes.object.isRequired, + disabled: PropTypes.bool, + label: PropTypes.string, + children: PropTypes.oneOfType([PropTypes.array, PropTypes.element]).isRequired, + meta: PropTypes.shape({ + touched: PropTypes.bool, + error: PropTypes.string, }).isRequired, }; @@ -218,7 +254,7 @@ export const renderServiceField = ({ {!disabled && touched && error - && {error}} + && {error}} ; renderServiceField.propTypes = { @@ -229,10 +265,12 @@ renderServiceField.propTypes = { icon: PropTypes.string, meta: PropTypes.shape({ touched: PropTypes.bool, - error: PropTypes.object, + error: PropTypes.string, }).isRequired, }; +export const getLastIpv4Octet = (ipv4) => parseInt(ipv4.slice(ipv4.lastIndexOf('.') + 1), 10); + /** * @param value {string} * @returns {*|number} diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 6003d0b8..baae34f5 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -19,6 +19,7 @@ import { DEFAULT_LANGUAGE, DEFAULT_TIME_FORMAT, DETAILED_DATE_FORMAT_OPTIONS, + DHCP_VALUES_PLACEHOLDERS, FILTERED, FILTERED_STATUS, IP_MATCH_LIST_STATUS, @@ -190,7 +191,7 @@ export const captitalizeWords = (text) => text.split(/[ -_]/g) export const getInterfaceIp = (option) => { const onlyIPv6 = option.ip_addresses.every((ip) => ip.includes(':')); - let interfaceIP = option.ip_addresses[0]; + let [interfaceIP] = option.ip_addresses; if (!onlyIPv6) { option.ip_addresses.forEach((ip) => { @@ -203,16 +204,9 @@ export const getInterfaceIp = (option) => { return interfaceIP; }; -export const getIpList = (interfaces) => { - let list = []; - - Object.keys(interfaces) - .forEach((item) => { - list = [...list, ...interfaces[item].ip_addresses]; - }); - - return list.sort(); -}; +export const getIpList = (interfaces) => Object.values(interfaces) + .reduce((acc, curr) => acc.concat(curr.ip_addresses), []) + .sort(); export const getDnsAddress = (ip, port = '') => { const isStandardDnsPort = port === STANDARD_DNS_PORT; @@ -668,11 +662,12 @@ export const getLogsUrlParams = (search, response_status) => `?${queryString.str response_status, })}`; -export const processContent = (content) => (Array.isArray(content) - ? content.filter(([, value]) => value).reduce((acc, val) => acc.concat(val), []) - : content -); - +export const processContent = ( + content, +) => (Array.isArray(content) + ? content.filter(([, value]) => value) + .reduce((acc, val) => acc.concat(val), []) + : content); /** * @param object {object} * @param sortKey {string} @@ -746,3 +741,65 @@ export const sortIp = (a, b) => { return 0; } }; + +/** + * @param ip {string} + * @param gateway_ip {string} + * @returns {{range_end: string, subnet_mask: string, range_start: string, + * lease_duration: string, gateway_ip: string}} + */ +export const calculateDhcpPlaceholdersIpv4 = (ip, gateway_ip) => { + const LAST_OCTET_IDX = 3; + const LAST_OCTET_RANGE_START = 100; + const LAST_OCTET_RANGE_END = 200; + + const addr = ipaddr.parse(ip); + addr.octets[LAST_OCTET_IDX] = LAST_OCTET_RANGE_START; + const range_start = addr.toString(); + + addr.octets[LAST_OCTET_IDX] = LAST_OCTET_RANGE_END; + const range_end = addr.toString(); + + const { + subnet_mask, + lease_duration, + } = DHCP_VALUES_PLACEHOLDERS.ipv4; + + return { + gateway_ip: gateway_ip || ip, + subnet_mask, + range_start, + range_end, + lease_duration, + }; +}; + +export const calculateDhcpPlaceholdersIpv6 = () => { + const { + range_start, + range_end, + lease_duration, + } = DHCP_VALUES_PLACEHOLDERS.ipv6; + + return { + range_start, + range_end, + lease_duration, + }; +}; + +/** + * Add ip_addresses property - concatenated ipv4_addresses and ipv6_addresses for every interface + * @param interfaces + * @param interfaces.ipv4_addresses {string[]} + * @param interfaces.ipv6_addresses {string[]} + * @returns interfaces Interfaces enriched with ip_addresses property + */ +export const enrichWithConcatenatedIpAddresses = (interfaces) => Object.entries(interfaces) + .reduce((acc, [k, v]) => { + const ipv4_addresses = v.ipv4_addresses ?? []; + const ipv6_addresses = v.ipv6_addresses ?? []; + + acc[k].ip_addresses = ipv4_addresses.concat(ipv6_addresses); + return acc; + }, interfaces); diff --git a/client/src/helpers/validators.js b/client/src/helpers/validators.js index 947cc96f..45055154 100644 --- a/client/src/helpers/validators.js +++ b/client/src/helpers/validators.js @@ -1,7 +1,6 @@ -import { Trans } from 'react-i18next'; -import React from 'react'; import i18next from 'i18next'; import { + MAX_PORT, R_CIDR, R_CIDR_IPV6, R_HOST, @@ -9,10 +8,10 @@ import { R_IPV6, R_MAC, R_URL_REQUIRES_PROTOCOL, + STANDARD_WEB_PORT, UNSAFE_PORTS, } from './constants'; -import { isValidAbsolutePath } from './form'; - +import { getLastIpv4Octet, isValidAbsolutePath } from './form'; // Validation functions // https://redux-form.com/8.3.0/examples/fieldlevelvalidation/ @@ -26,7 +25,7 @@ export const validateRequiredValue = (value) => { if (formattedValue || formattedValue === 0 || (formattedValue && formattedValue.length !== 0)) { return undefined; } - return form_error_required; + return 'form_error_required'; }; /** @@ -35,11 +34,28 @@ export const validateRequiredValue = (value) => { */ export const getMaxValueValidator = (maximum) => (value) => { if (value && value > maximum) { - i18next.t('value_not_larger_than', { maximum }); + return i18next.t('value_not_larger_than', { maximum }); } return undefined; }; +/** + * @param value {string} + * @returns {undefined|string} + */ +export const validateIpv4RangeEnd = (_, allValues) => { + if (!allValues || !allValues.v4 || !allValues.v4.range_end || !allValues.v4.range_start) { + return undefined; + } + + const { range_end, range_start } = allValues.v4; + + if (getLastIpv4Octet(range_end) <= getLastIpv4Octet(range_start)) { + return 'range_end_error'; + } + + return undefined; +}; /** * @param value {string} @@ -47,7 +63,7 @@ export const getMaxValueValidator = (maximum) => (value) => { */ export const validateIpv4 = (value) => { if (value && !R_IPV4.test(value)) { - return form_error_ip4_format; + return 'form_error_ip4_format'; } return undefined; }; @@ -63,12 +79,12 @@ export const validateClientId = (value) => { const formattedValue = value ? value.trim() : value; if (formattedValue && !( R_IPV4.test(formattedValue) - || R_IPV6.test(formattedValue) - || R_MAC.test(formattedValue) - || R_CIDR.test(formattedValue) - || R_CIDR_IPV6.test(formattedValue) + || R_IPV6.test(formattedValue) + || R_MAC.test(formattedValue) + || R_CIDR.test(formattedValue) + || R_CIDR_IPV6.test(formattedValue) )) { - return form_error_client_id_format; + return 'form_error_client_id_format'; } return undefined; }; @@ -79,7 +95,7 @@ export const validateClientId = (value) => { */ export const validateIpv6 = (value) => { if (value && !R_IPV6.test(value)) { - return form_error_ip6_format; + return 'form_error_ip6_format'; } return undefined; }; @@ -90,7 +106,7 @@ export const validateIpv6 = (value) => { */ export const validateIp = (value) => { if (value && !R_IPV4.test(value) && !R_IPV6.test(value)) { - return form_error_ip_format; + return 'form_error_ip_format'; } return undefined; }; @@ -101,76 +117,76 @@ export const validateIp = (value) => { */ export const validateMac = (value) => { if (value && !R_MAC.test(value)) { - return form_error_mac_format; + return 'form_error_mac_format'; } return undefined; }; /** - * @param value {string} + * @param value {number} * @returns {undefined|string} */ export const validateIsPositiveValue = (value) => { if ((value || value === 0) && value <= 0) { - return form_error_positive; + return 'form_error_positive'; } return undefined; }; /** - * @param value {string} + * @param value {number} * @returns {boolean|*} */ export const validateBiggerOrEqualZeroValue = (value) => { if (value < 0) { - return form_error_negative; + return 'form_error_negative'; } return false; }; /** - * @param value {string} + * @param value {number} * @returns {undefined|string} */ export const validatePort = (value) => { - if ((value || value === 0) && (value < 80 || value > 65535)) { - return form_error_port_range; + if ((value || value === 0) && (value < STANDARD_WEB_PORT || value > MAX_PORT)) { + return 'form_error_port_range'; } return undefined; }; /** - * @param value {string} + * @param value {number} * @returns {undefined|string} */ export const validateInstallPort = (value) => { - if (value < 1 || value > 65535) { - return form_error_port; + if (value < 1 || value > MAX_PORT) { + return 'form_error_port'; } return undefined; }; /** - * @param value {string} + * @param value {number} * @returns {undefined|string} */ export const validatePortTLS = (value) => { if (value === 0) { return undefined; } - if (value && (value < 80 || value > 65535)) { - return form_error_port_range; + if (value && (value < STANDARD_WEB_PORT || value > MAX_PORT)) { + return 'form_error_port_range'; } return undefined; }; /** - * @param value {string} + * @param value {number} * @returns {undefined|string} */ export const validateIsSafePort = (value) => { if (UNSAFE_PORTS.includes(value)) { - return form_error_port_unsafe; + return 'form_error_port_unsafe'; } return undefined; }; @@ -181,7 +197,7 @@ export const validateIsSafePort = (value) => { */ export const validateDomain = (value) => { if (value && !R_HOST.test(value)) { - return form_error_domain_format; + return 'form_error_domain_format'; } return undefined; }; @@ -192,7 +208,7 @@ export const validateDomain = (value) => { */ export const validateAnswer = (value) => { if (value && (!R_IPV4.test(value) && !R_IPV6.test(value) && !R_HOST.test(value))) { - return form_error_answer_format; + return 'form_error_answer_format'; } return undefined; }; @@ -203,7 +219,7 @@ export const validateAnswer = (value) => { */ export const validatePath = (value) => { if (value && !isValidAbsolutePath(value) && !R_URL_REQUIRES_PROTOCOL.test(value)) { - return form_error_url_or_path_format; + return 'form_error_url_or_path_format'; } return undefined; }; diff --git a/client/src/i18n.js b/client/src/i18n.js index ee776147..b8bfffc4 100644 --- a/client/src/i18n.js +++ b/client/src/i18n.js @@ -30,9 +30,11 @@ import sl from './__locales/sl.json'; import tr from './__locales/tr.json'; import srCS from './__locales/sr-cs.json'; import hr from './__locales/hr.json'; +import hu from './__locales/hu.json'; import fa from './__locales/fa.json'; import th from './__locales/th.json'; import ro from './__locales/ro.json'; +import siLk from './__locales/si-lk.json'; import { setHtmlLangAttr } from './helpers/helpers'; const resources = { @@ -114,6 +116,9 @@ const resources = { hr: { translation: hr, }, + hu: { + translation: hu, + }, fa: { translation: fa, }, @@ -123,6 +128,9 @@ const resources = { ro: { translation: ro, }, + 'si-lk': { + translation: siLk, + }, }; const availableLanguages = Object.keys(LANGUAGES); diff --git a/client/src/install/Setup/AddressList.js b/client/src/install/Setup/AddressList.js index aeeed246..90bccfd2 100644 --- a/client/src/install/Setup/AddressList.js +++ b/client/src/install/Setup/AddressList.js @@ -4,50 +4,40 @@ import PropTypes from 'prop-types'; import { getIpList, getDnsAddress, getWebAddress } from '../../helpers/helpers'; import { ALL_INTERFACES_IP } from '../../helpers/constants'; -const AddressList = (props) => { - let webAddress = getWebAddress(props.address, props.port); - let dnsAddress = getDnsAddress(props.address, props.port); +const renderItem = ({ + ip, port, isDns, +}) => { + const webAddress = getWebAddress(ip, port); + const dnsAddress = getDnsAddress(ip, port); - if (props.address === ALL_INTERFACES_IP) { - return getIpList(props.interfaces).map((ip) => { - webAddress = getWebAddress(ip, props.port); - dnsAddress = getDnsAddress(ip, props.port); - - if (props.isDns) { - return ( -
  • - - {dnsAddress} - -
  • - ); - } - - return ( -
  • - - {webAddress} - -
  • - ); - }); + return
  • {isDns + ? {dnsAddress} + : {webAddress} } - - if (props.isDns) { - return ( - - {dnsAddress} - - ); - } - - return ( - - {webAddress} - - ); +
  • ; }; +const AddressList = ({ + address, + interfaces, + port, + isDns, +}) =>
      { + address === ALL_INTERFACES_IP + ? getIpList(interfaces) + .map((ip) => renderItem({ + ip, + port, + isDns, + })) + : renderItem({ + ip: address, + port, + isDns, + }) +} +
    ; + AddressList.propTypes = { interfaces: PropTypes.object.isRequired, address: PropTypes.string.isRequired, @@ -58,4 +48,10 @@ AddressList.propTypes = { isDns: PropTypes.bool, }; +renderItem.propTypes = { + ip: PropTypes.string.isRequired, + port: PropTypes.string.isRequired, + isDns: PropTypes.bool.isRequired, +}; + export default AddressList; diff --git a/client/src/install/Setup/Devices.js b/client/src/install/Setup/Devices.js index 59f25dea..c6d0af90 100644 --- a/client/src/install/Setup/Devices.js +++ b/client/src/install/Setup/Devices.js @@ -26,7 +26,7 @@ let Devices = (props) => ( interfaces={props.interfaces} address={props.dnsIp} port={props.dnsPort} - isDns={true} + isDns />
    diff --git a/client/src/install/Setup/Settings.js b/client/src/install/Setup/Settings.js index 443b8036..b98f8d1f 100644 --- a/client/src/install/Setup/Settings.js +++ b/client/src/install/Setup/Settings.js @@ -10,51 +10,36 @@ import AddressList from './AddressList'; import { getInterfaceIp } from '../../helpers/helpers'; import { - ALL_INTERFACES_IP, FORM_NAME, ADDRESS_IN_USE_TEXT, PORT_53_FAQ_LINK, UBUNTU_SYSTEM_PORT, + ALL_INTERFACES_IP, + FORM_NAME, + ADDRESS_IN_USE_TEXT, + PORT_53_FAQ_LINK, + STATUS_RESPONSE, + STANDARD_DNS_PORT, + STANDARD_WEB_PORT, } from '../../helpers/constants'; import { renderInputField, toNumber } from '../../helpers/form'; import { validateRequiredValue, validateInstallPort } from '../../helpers/validators'; -const STATIC_STATUS = { - ENABLED: 'yes', - DISABLED: 'no', - ERROR: 'error', -}; +const renderInterfaces = (interfaces) => Object.values(interfaces) + .map((option) => { + const { + name, + ip_addresses, + flags, + } = option; -const renderInterfaces = ((interfaces) => ( - Object.keys(interfaces) - .map((item) => { - const option = interfaces[item]; - const { - name, - ip_addresses, - flags, - } = option; + if (option && ip_addresses?.length > 0) { + const ip = getInterfaceIp(option); + const isDown = flags?.includes('down'); - if (option && ip_addresses?.length > 0) { - const ip = getInterfaceIp(option); - const isDown = flags?.includes('down'); + return ; + } - if (isDown) { - return ( - - ); - } - - return ( - - ); - } - - return false; - }) -)); + return null; + }); class Settings extends Component { componentDidMount() { @@ -77,42 +62,36 @@ class Settings extends Component { getStaticIpMessage = (staticIp) => { const { static: status, ip } = staticIp; - if (!status) { - return ''; - } - - return ( - <> - {status === STATIC_STATUS.DISABLED && ( - <> -
    - text]}> - install_static_configure - -
    - - - )} - {status === STATIC_STATUS.ERROR && ( -
    - install_static_error -
    - )} - {status === STATIC_STATUS.ENABLED && ( -
    - - install_static_ok + switch (status) { + case STATUS_RESPONSE.NO: { + return <> +
    + text]}> + install_static_configure
    - )} - - ); + + ; + } + case STATUS_RESPONSE.ERROR: { + return
    + install_static_error +
    ; + } + case STATUS_RESPONSE.YES: { + return
    + install_static_ok +
    ; + } + default: + return null; + } }; handleAutofix = (type) => { @@ -229,7 +208,7 @@ class Settings extends Component { component={renderInputField} type="number" className="form-control" - placeholder="80" + placeholder={STANDARD_WEB_PORT.toString()} validate={[validateInstallPort, validateRequiredValue]} normalize={toNumber} onChange={handleChange} @@ -297,7 +276,7 @@ class Settings extends Component { component={renderInputField} type="number" className="form-control" - placeholder="80" + placeholder={STANDARD_WEB_PORT.toString()} validate={[validateInstallPort, validateRequiredValue]} normalize={toNumber} onChange={handleChange} @@ -332,7 +311,7 @@ class Settings extends Component {

    } } - {dnsPort === UBUNTU_SYSTEM_PORT && !isDnsFixAvailable + {dnsPort === STANDARD_DNS_PORT && !isDnsFixAvailable && dnsStatus.includes(ADDRESS_IN_USE_TEXT) && link]}> diff --git a/client/src/install/Setup/Setup.css b/client/src/install/Setup/Setup.css index aac7ea0e..9a404222 100644 --- a/client/src/install/Setup/Setup.css +++ b/client/src/install/Setup/Setup.css @@ -107,7 +107,7 @@ } .form__message--error { - color: #cd201f; + color: var(--red); } .setup__button { diff --git a/client/src/login/Login/Login.css b/client/src/login/Login/Login.css index 628ff62b..c38f27bf 100644 --- a/client/src/login/Login/Login.css +++ b/client/src/login/Login/Login.css @@ -43,5 +43,5 @@ } .form__message--error { - color: #cd201f; + color: var(--red); } diff --git a/client/src/reducers/dashboard.js b/client/src/reducers/dashboard.js new file mode 100644 index 00000000..3709f10a --- /dev/null +++ b/client/src/reducers/dashboard.js @@ -0,0 +1,175 @@ +import { handleActions } from 'redux-actions'; +import * as actions from '../actions'; +import { areEqualVersions } from '../helpers/version'; +import { STANDARD_DNS_PORT, STANDARD_WEB_PORT } from '../helpers/constants'; + +const dashboard = handleActions( + { + [actions.setDnsRunningStatus]: (state, { payload }) => ( + { + ...state, + isCoreRunning: payload, + } + ), + [actions.dnsStatusRequest]: (state) => ({ + ...state, + processing: true, + }), + [actions.dnsStatusFailure]: (state) => ({ + ...state, + processing: false, + }), + [actions.dnsStatusSuccess]: (state, { payload }) => { + const { + version, + dns_port: dnsPort, + dns_addresses: dnsAddresses, + protection_enabled: protectionEnabled, + http_port: httpPort, + language, + } = payload; + const newState = { + ...state, + isCoreRunning: true, + processing: false, + dnsVersion: version, + dnsPort, + dnsAddresses, + protectionEnabled, + language, + httpPort, + }; + return newState; + }, + + [actions.getVersionRequest]: (state) => ({ + ...state, + processingVersion: true, + }), + [actions.getVersionFailure]: (state) => ({ + ...state, + processingVersion: false, + }), + [actions.getVersionSuccess]: (state, { payload }) => { + const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion; + + if (!payload.disabled && !areEqualVersions(currentVersion, payload.new_version)) { + const { + announcement_url: announcementUrl, + new_version: newVersion, + can_autoupdate: canAutoUpdate, + } = payload; + + const newState = { + ...state, + announcementUrl, + newVersion, + canAutoUpdate, + isUpdateAvailable: true, + processingVersion: false, + checkUpdateFlag: !payload.disabled, + }; + return newState; + } + + return { + ...state, + processingVersion: false, + checkUpdateFlag: !payload.disabled, + }; + }, + + [actions.getUpdateRequest]: (state) => ({ + ...state, + processingUpdate: true, + }), + [actions.getUpdateFailure]: (state) => ({ + ...state, + processingUpdate: false, + }), + [actions.getUpdateSuccess]: (state) => { + const newState = { + ...state, + processingUpdate: false, + }; + return newState; + }, + + [actions.toggleProtectionRequest]: (state) => ({ + ...state, + processingProtection: true, + }), + [actions.toggleProtectionFailure]: (state) => ({ + ...state, + processingProtection: false, + }), + [actions.toggleProtectionSuccess]: (state) => { + const newState = { + ...state, + protectionEnabled: !state.protectionEnabled, + processingProtection: false, + }; + return newState; + }, + + [actions.getLanguageSuccess]: (state, { payload }) => { + const newState = { + ...state, + language: payload, + }; + return newState; + }, + + [actions.getClientsRequest]: (state) => ({ + ...state, + processingClients: true, + }), + [actions.getClientsFailure]: (state) => ({ + ...state, + processingClients: false, + }), + [actions.getClientsSuccess]: (state, { payload }) => { + const newState = { + ...state, + ...payload, + processingClients: false, + }; + return newState; + }, + + [actions.getProfileRequest]: (state) => ({ + ...state, + processingProfile: true, + }), + [actions.getProfileFailure]: (state) => ({ + ...state, + processingProfile: false, + }), + [actions.getProfileSuccess]: (state, { payload }) => ({ + ...state, + name: payload.name, + processingProfile: false, + }), + }, + { + processing: true, + isCoreRunning: true, + processingVersion: true, + processingClients: true, + processingUpdate: false, + processingProfile: true, + protectionEnabled: false, + processingProtection: false, + httpPort: STANDARD_WEB_PORT, + dnsPort: STANDARD_DNS_PORT, + dnsAddresses: [], + dnsVersion: '', + clients: [], + autoClients: [], + supportedTags: [], + name: '', + checkUpdateFlag: false, + }, +); + +export default dashboard; diff --git a/client/src/reducers/dhcp.js b/client/src/reducers/dhcp.js new file mode 100644 index 00000000..5f9f0f58 --- /dev/null +++ b/client/src/reducers/dhcp.js @@ -0,0 +1,203 @@ +import { handleActions } from 'redux-actions'; +import * as actions from '../actions'; +import { enrichWithConcatenatedIpAddresses } from '../helpers/helpers'; + +// todo: figure out if we cat get rid of redux-form state duplication +const dhcp = handleActions( + { + [actions.getDhcpStatusRequest]: (state) => ({ + ...state, + processing: true, + }), + [actions.getDhcpStatusFailure]: (state) => ({ + ...state, + processing: false, + }), + [actions.getDhcpStatusSuccess]: (state, { payload }) => { + const { static_leases: staticLeases, ...values } = payload; + + const newState = { + ...state, + staticLeases, + processing: false, + ...values, + }; + + return newState; + }, + + [actions.getDhcpInterfacesRequest]: (state) => ({ + ...state, + processingInterfaces: true, + }), + [actions.getDhcpInterfacesFailure]: (state) => ({ + ...state, + processingInterfaces: false, + }), + [actions.getDhcpInterfacesSuccess]: (state, { payload }) => { + const newState = { + ...state, + interfaces: enrichWithConcatenatedIpAddresses(payload), + processingInterfaces: false, + }; + return newState; + }, + + [actions.findActiveDhcpRequest]: (state) => ({ + ...state, + processingStatus: true, + }), + [actions.findActiveDhcpFailure]: (state) => ({ + ...state, + processingStatus: false, + }), + [actions.findActiveDhcpSuccess]: (state, { payload }) => { + const newState = { + ...state, + check: payload, + processingStatus: false, + }; + return newState; + }, + + [actions.toggleDhcpRequest]: (state) => ({ + ...state, + processingDhcp: true, + }), + [actions.toggleDhcpFailure]: (state) => ({ + ...state, + processingDhcp: false, + }), + [actions.toggleDhcpSuccess]: (state) => { + const { enabled } = state; + const newState = { + ...state, + enabled: !enabled, + check: null, + processingDhcp: false, + }; + return newState; + }, + + [actions.setDhcpConfigRequest]: (state) => ({ + ...state, + processingConfig: true, + }), + [actions.setDhcpConfigFailure]: (state) => ({ + ...state, + processingConfig: false, + }), + [actions.setDhcpConfigSuccess]: (state, { payload }) => { + const { v4, v6 } = state; + const newConfigV4 = { ...v4, ...payload.v4 }; + const newConfigV6 = { ...v6, ...payload.v6 }; + + const newState = { + ...state, + v4: newConfigV4, + v6: newConfigV6, + interface_name: payload.interface_name, + processingConfig: false, + }; + + return newState; + }, + + [actions.resetDhcpRequest]: (state) => ({ + ...state, + processingReset: true, + }), + [actions.resetDhcpFailure]: (state) => ({ + ...state, + processingReset: false, + }), + [actions.resetDhcpSuccess]: (state) => ({ + ...state, + processingReset: false, + enabled: false, + v4: {}, + v6: {}, + interface_name: '', + }), + + [actions.toggleLeaseModal]: (state) => { + const newState = { + ...state, + isModalOpen: !state.isModalOpen, + }; + return newState; + }, + + [actions.addStaticLeaseRequest]: (state) => ({ + ...state, + processingAdding: true, + }), + [actions.addStaticLeaseFailure]: (state) => ({ + ...state, + processingAdding: false, + }), + [actions.addStaticLeaseSuccess]: (state, { payload }) => { + const { ip, mac, hostname } = payload; + const newLease = { + ip, + mac, + hostname: hostname || '', + }; + const leases = [...state.staticLeases, newLease]; + const newState = { + ...state, + staticLeases: leases, + processingAdding: false, + }; + return newState; + }, + + [actions.removeStaticLeaseRequest]: (state) => ({ + ...state, + processingDeleting: true, + }), + [actions.removeStaticLeaseFailure]: (state) => ({ + ...state, + processingDeleting: false, + }), + [actions.removeStaticLeaseSuccess]: (state, { payload }) => { + const leaseToRemove = payload.ip; + const leases = state.staticLeases.filter((item) => item.ip !== leaseToRemove); + const newState = { + ...state, + staticLeases: leases, + processingDeleting: false, + }; + return newState; + }, + }, + { + processing: true, + processingStatus: false, + processingInterfaces: false, + processingDhcp: false, + processingConfig: false, + processingAdding: false, + processingDeleting: false, + enabled: false, + interface_name: '', + check: null, + v4: { + gateway_ip: '', + subnet_mask: '', + range_start: '', + range_end: '', + lease_duration: 0, + }, + v6: { + range_start: '', + lease_duration: 0, + }, + leases: [], + staticLeases: [], + isModalOpen: false, + dhcp_available: false, + }, +); + +export default dhcp; diff --git a/client/src/reducers/dnsConfig.js b/client/src/reducers/dnsConfig.js index b3978217..91e95818 100644 --- a/client/src/reducers/dnsConfig.js +++ b/client/src/reducers/dnsConfig.js @@ -1,9 +1,9 @@ import { handleActions } from 'redux-actions'; import * as actions from '../actions/dnsConfig'; -import { BLOCKING_MODES } from '../helpers/constants'; +import { ALL_INTERFACES_IP, BLOCKING_MODES } from '../helpers/constants'; -const DEFAULT_BLOCKING_IPV4 = '0.0.0.0'; +const DEFAULT_BLOCKING_IPV4 = ALL_INTERFACES_IP; const DEFAULT_BLOCKING_IPV6 = '::'; const dnsConfig = handleActions( diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js index 56d26fb7..80665cf7 100644 --- a/client/src/reducers/index.js +++ b/client/src/reducers/index.js @@ -1,9 +1,6 @@ import { combineReducers } from 'redux'; -import { handleActions } from 'redux-actions'; import { loadingBarReducer } from 'react-redux-loading-bar'; import { reducer as formReducer } from 'redux-form'; - -import * as actions from '../actions'; import toasts from './toasts'; import encryption from './encryption'; import clients from './clients'; @@ -14,296 +11,9 @@ import stats from './stats'; import queryLogs from './queryLogs'; import dnsConfig from './dnsConfig'; import filtering from './filtering'; -import { areEqualVersions } from '../helpers/version'; - -const settings = handleActions( - { - [actions.initSettingsRequest]: (state) => ({ ...state, processing: true }), - [actions.initSettingsFailure]: (state) => ({ ...state, processing: false }), - [actions.initSettingsSuccess]: (state, { payload }) => { - const { settingsList } = payload; - const newState = { ...state, settingsList, processing: false }; - return newState; - }, - [actions.toggleSettingStatus]: (state, { payload }) => { - const { settingsList } = state; - const { settingKey } = payload; - - const setting = settingsList[settingKey]; - - const newSetting = { ...setting, enabled: !setting.enabled }; - const newSettingsList = { ...settingsList, [settingKey]: newSetting }; - return { ...state, settingsList: newSettingsList }; - }, - [actions.testUpstreamRequest]: (state) => ({ ...state, processingTestUpstream: true }), - [actions.testUpstreamFailure]: (state) => ({ ...state, processingTestUpstream: false }), - [actions.testUpstreamSuccess]: (state) => ({ ...state, processingTestUpstream: false }), - }, - { - processing: true, - processingTestUpstream: false, - processingDhcpStatus: false, - settingsList: {}, - }, -); - -const dashboard = handleActions( - { - [actions.setDnsRunningStatus]: (state, { payload }) => ( - { ...state, isCoreRunning: payload } - ), - [actions.dnsStatusRequest]: (state) => ({ ...state, processing: true }), - [actions.dnsStatusFailure]: (state) => ({ ...state, processing: false }), - [actions.dnsStatusSuccess]: (state, { payload }) => { - const { - version, - dns_port: dnsPort, - dns_addresses: dnsAddresses, - protection_enabled: protectionEnabled, - http_port: httpPort, - language, - } = payload; - const newState = { - ...state, - isCoreRunning: true, - processing: false, - dnsVersion: version, - dnsPort, - dnsAddresses, - protectionEnabled, - language, - httpPort, - }; - return newState; - }, - - [actions.getVersionRequest]: (state) => ({ ...state, processingVersion: true }), - [actions.getVersionFailure]: (state) => ({ ...state, processingVersion: false }), - [actions.getVersionSuccess]: (state, { payload }) => { - const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion; - - if (!payload.disabled && !areEqualVersions(currentVersion, payload.new_version)) { - const { - announcement_url: announcementUrl, - new_version: newVersion, - can_autoupdate: canAutoUpdate, - } = payload; - - const newState = { - ...state, - announcementUrl, - newVersion, - canAutoUpdate, - isUpdateAvailable: true, - processingVersion: false, - checkUpdateFlag: !payload.disabled, - }; - return newState; - } - - return { - ...state, - processingVersion: false, - checkUpdateFlag: !payload.disabled, - }; - }, - - [actions.getUpdateRequest]: (state) => ({ ...state, processingUpdate: true }), - [actions.getUpdateFailure]: (state) => ({ ...state, processingUpdate: false }), - [actions.getUpdateSuccess]: (state) => { - const newState = { ...state, processingUpdate: false }; - return newState; - }, - - [actions.toggleProtectionRequest]: (state) => ({ ...state, processingProtection: true }), - [actions.toggleProtectionFailure]: (state) => ({ ...state, processingProtection: false }), - [actions.toggleProtectionSuccess]: (state) => { - const newState = { - ...state, - protectionEnabled: !state.protectionEnabled, - processingProtection: false, - }; - return newState; - }, - - [actions.getLanguageSuccess]: (state, { payload }) => { - const newState = { ...state, language: payload }; - return newState; - }, - - [actions.getClientsRequest]: (state) => ({ ...state, processingClients: true }), - [actions.getClientsFailure]: (state) => ({ ...state, processingClients: false }), - [actions.getClientsSuccess]: (state, { payload }) => { - const newState = { - ...state, - ...payload, - processingClients: false, - }; - return newState; - }, - - [actions.getProfileRequest]: (state) => ({ ...state, processingProfile: true }), - [actions.getProfileFailure]: (state) => ({ ...state, processingProfile: false }), - [actions.getProfileSuccess]: (state, { payload }) => ({ - ...state, - name: payload.name, - processingProfile: false, - }), - }, - { - processing: true, - isCoreRunning: true, - processingVersion: true, - processingClients: true, - processingUpdate: false, - processingProfile: true, - protectionEnabled: false, - processingProtection: false, - httpPort: 80, - dnsPort: 53, - dnsAddresses: [], - dnsVersion: '', - clients: [], - autoClients: [], - supportedTags: [], - name: '', - checkUpdateFlag: false, - }, -); - -const dhcp = handleActions( - { - [actions.getDhcpStatusRequest]: (state) => ({ ...state, processing: true }), - [actions.getDhcpStatusFailure]: (state) => ({ ...state, processing: false }), - [actions.getDhcpStatusSuccess]: (state, { payload }) => { - const { static_leases: staticLeases, ...values } = payload; - - const newState = { - ...state, - staticLeases, - processing: false, - ...values, - }; - - return newState; - }, - - [actions.getDhcpInterfacesRequest]: (state) => ({ ...state, processingInterfaces: true }), - [actions.getDhcpInterfacesFailure]: (state) => ({ ...state, processingInterfaces: false }), - [actions.getDhcpInterfacesSuccess]: (state, { payload }) => { - const newState = { - ...state, - interfaces: payload, - processingInterfaces: false, - }; - return newState; - }, - - [actions.findActiveDhcpRequest]: (state) => ({ ...state, processingStatus: true }), - [actions.findActiveDhcpFailure]: (state) => ({ ...state, processingStatus: false }), - [actions.findActiveDhcpSuccess]: (state, { payload }) => { - const { other_server: otherServer, static_ip: staticIP } = payload; - - const newState = { - ...state, - check: { - otherServer, - staticIP, - }, - processingStatus: false, - }; - return newState; - }, - - [actions.toggleDhcpRequest]: (state) => ({ ...state, processingDhcp: true }), - [actions.toggleDhcpFailure]: (state) => ({ ...state, processingDhcp: false }), - [actions.toggleDhcpSuccess]: (state) => { - const { config } = state; - const newConfig = { ...config, enabled: !config.enabled }; - const newState = { - ...state, - config: newConfig, - check: null, - processingDhcp: false, - }; - return newState; - }, - - [actions.setDhcpConfigRequest]: (state) => ({ ...state, processingConfig: true }), - [actions.setDhcpConfigFailure]: (state) => ({ ...state, processingConfig: false }), - [actions.setDhcpConfigSuccess]: (state, { payload }) => { - const { config } = state; - const newConfig = { ...config, ...payload }; - const newState = { ...state, config: newConfig, processingConfig: false }; - return newState; - }, - - [actions.resetDhcpRequest]: (state) => ({ ...state, processingReset: true }), - [actions.resetDhcpFailure]: (state) => ({ ...state, processingReset: false }), - [actions.resetDhcpSuccess]: (state) => ({ - ...state, - processingReset: false, - config: { - enabled: false, - }, - }), - - [actions.toggleLeaseModal]: (state) => { - const newState = { - ...state, - isModalOpen: !state.isModalOpen, - }; - return newState; - }, - - [actions.addStaticLeaseRequest]: (state) => ({ ...state, processingAdding: true }), - [actions.addStaticLeaseFailure]: (state) => ({ ...state, processingAdding: false }), - [actions.addStaticLeaseSuccess]: (state, { payload }) => { - const { ip, mac, hostname } = payload; - const newLease = { - ip, - mac, - hostname: hostname || '', - }; - const leases = [...state.staticLeases, newLease]; - const newState = { - ...state, - staticLeases: leases, - processingAdding: false, - }; - return newState; - }, - - [actions.removeStaticLeaseRequest]: (state) => ({ ...state, processingDeleting: true }), - [actions.removeStaticLeaseFailure]: (state) => ({ ...state, processingDeleting: false }), - [actions.removeStaticLeaseSuccess]: (state, { payload }) => { - const leaseToRemove = payload.ip; - const leases = state.staticLeases.filter((item) => item.ip !== leaseToRemove); - const newState = { - ...state, - staticLeases: leases, - processingDeleting: false, - }; - return newState; - }, - }, - { - processing: true, - processingStatus: false, - processingInterfaces: false, - processingDhcp: false, - processingConfig: false, - processingAdding: false, - processingDeleting: false, - config: { - enabled: false, - }, - check: null, - leases: [], - staticLeases: [], - isModalOpen: false, - }, -); +import settings from './settings'; +import dashboard from './dashboard'; +import dhcp from './dhcp'; export default combineReducers({ settings, diff --git a/client/src/reducers/install.js b/client/src/reducers/install.js index 5ffee899..03af34c2 100644 --- a/client/src/reducers/install.js +++ b/client/src/reducers/install.js @@ -4,7 +4,9 @@ import { reducer as formReducer } from 'redux-form'; import * as actions from '../actions/install'; import toasts from './toasts'; -import { INSTALL_FIRST_STEP } from '../helpers/constants'; +import { + ALL_INTERFACES_IP, INSTALL_FIRST_STEP, STANDARD_DNS_PORT, STANDARD_WEB_PORT, +} from '../helpers/constants'; const install = handleActions({ [actions.getDefaultAddressesRequest]: (state) => ({ ...state, processingDefault: true }), @@ -45,14 +47,14 @@ const install = handleActions({ processingSubmit: false, processingCheck: false, web: { - ip: '0.0.0.0', - port: 80, + ip: ALL_INTERFACES_IP, + port: STANDARD_WEB_PORT, status: '', can_autofix: false, }, dns: { - ip: '0.0.0.0', - port: 53, + ip: ALL_INTERFACES_IP, + port: STANDARD_DNS_PORT, status: '', can_autofix: false, }, diff --git a/client/src/reducers/settings.js b/client/src/reducers/settings.js new file mode 100644 index 00000000..f5c2a30c --- /dev/null +++ b/client/src/reducers/settings.js @@ -0,0 +1,63 @@ +import { handleActions } from 'redux-actions'; +import * as actions from '../actions'; + +const settings = handleActions( + { + [actions.initSettingsRequest]: (state) => ({ + ...state, + processing: true, + }), + [actions.initSettingsFailure]: (state) => ({ + ...state, + processing: false, + }), + [actions.initSettingsSuccess]: (state, { payload }) => { + const { settingsList } = payload; + const newState = { + ...state, + settingsList, + processing: false, + }; + return newState; + }, + [actions.toggleSettingStatus]: (state, { payload }) => { + const { settingsList } = state; + const { settingKey } = payload; + + const setting = settingsList[settingKey]; + + const newSetting = { + ...setting, + enabled: !setting.enabled, + }; + const newSettingsList = { + ...settingsList, + [settingKey]: newSetting, + }; + return { + ...state, + settingsList: newSettingsList, + }; + }, + [actions.testUpstreamRequest]: (state) => ({ + ...state, + processingTestUpstream: true, + }), + [actions.testUpstreamFailure]: (state) => ({ + ...state, + processingTestUpstream: false, + }), + [actions.testUpstreamSuccess]: (state) => ({ + ...state, + processingTestUpstream: false, + }), + }, + { + processing: true, + processingTestUpstream: false, + processingDhcpStatus: false, + settingsList: {}, + }, +); + +export default settings; diff --git a/client/src/reducers/toasts.js b/client/src/reducers/toasts.js index 8da15547..7a48c72d 100644 --- a/client/src/reducers/toasts.js +++ b/client/src/reducers/toasts.js @@ -8,9 +8,12 @@ import { removeToast } from '../actions'; const toasts = handleActions({ [addErrorToast]: (state, { payload }) => { + const message = payload.error.toString(); + console.error(message); + const errorToast = { id: nanoid(), - message: payload.error.toString(), + message, type: 'error', }; diff --git a/home/i18n.go b/home/i18n.go index 4b46f7ce..6d6f3f84 100644 --- a/home/i18n.go +++ b/home/i18n.go @@ -39,9 +39,11 @@ var allowedLanguages = map[string]bool{ "tr": true, "sr-cs": true, "hr": true, + "hu": true, "fa": true, "th": true, "ro": true, + "si-lk": true, } func isLanguageAllowed(language string) bool { From fcc34ca80bedd34f2dd170bebe15dd2d06174f5d Mon Sep 17 00:00:00 2001 From: ArtemBaskal Date: Thu, 20 Aug 2020 13:50:40 +0300 Subject: [PATCH 10/12] Check interface before showing alert --- client/src/actions/index.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/client/src/actions/index.js b/client/src/actions/index.js index a2fa788d..60f58453 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -3,7 +3,9 @@ import i18next from 'i18next'; import axios from 'axios'; import { splitByNewLine, sortClients } from '../helpers/helpers'; -import { CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES } from '../helpers/constants'; +import { + CHECK_TIMEOUT, STATUS_RESPONSE, SETTINGS_NAMES, FORM_NAME, +} from '../helpers/constants'; import { areEqualVersions } from '../helpers/version'; import { getTlsStatus } from './encryption'; import apiClient from '../api/Api'; @@ -375,14 +377,18 @@ export const findActiveDhcp = (name) => async (dispatch, getState) => { try { const activeDhcp = await apiClient.findActiveDhcp(name); dispatch(findActiveDhcpSuccess(activeDhcp)); - const { check, interface_name } = getState().dhcp; + const { check, interface_name, interfaces } = getState().dhcp; + const selectedInterface = getState().form[FORM_NAME.DHCP_INTERFACES].values.interface_name; const v4 = check?.v4 ?? { static_ip: {}, other_server: {} }; const v6 = check?.v6 ?? { other_server: {} }; let isError = false; - if (v4.other_server.found === STATUS_RESPONSE.ERROR - || v6.other_server.found === STATUS_RESPONSE.ERROR) { + const hasV4Interface = !!interfaces[selectedInterface]?.ipv4_addresses; + const hasV6Interface = !!interfaces[selectedInterface]?.ipv6_addresses; + + if ((hasV4Interface && v4.other_server.found === STATUS_RESPONSE.ERROR) + || (hasV6Interface && v6.other_server.found === STATUS_RESPONSE.ERROR)) { isError = true; dispatch(addErrorToast({ error: 'dhcp_error' })); if (v4.other_server.error) { @@ -393,7 +399,7 @@ export const findActiveDhcp = (name) => async (dispatch, getState) => { } } - if (v4.static_ip.static === STATUS_RESPONSE.ERROR) { + if (hasV4Interface && v4.static_ip.static === STATUS_RESPONSE.ERROR) { isError = true; dispatch(addErrorToast({ error: 'dhcp_static_ip_error' })); } @@ -402,10 +408,10 @@ export const findActiveDhcp = (name) => async (dispatch, getState) => { return; } - if (v4.other_server.found === STATUS_RESPONSE.YES - || v6.other_server.found === STATUS_RESPONSE.YES) { + if ((hasV4Interface && v4.other_server.found === STATUS_RESPONSE.YES) + || (hasV6Interface && v6.other_server.found === STATUS_RESPONSE.YES)) { dispatch(addErrorToast({ error: 'dhcp_found' })); - } else if (v4.static_ip.static === STATUS_RESPONSE.NO + } else if (hasV4Interface && v4.static_ip.static === STATUS_RESPONSE.NO && v4.static_ip.ip && interface_name) { const warning = i18next.t('dhcp_dynamic_ip_found', { From 1806816d9c1a655467e6ed3934beec96a6944d4a Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Thu, 20 Aug 2020 14:25:00 +0300 Subject: [PATCH 11/12] Fix DHCP check when there is no V6 interface --- client/src/actions/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 60f58453..b4a51065 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -387,13 +387,15 @@ export const findActiveDhcp = (name) => async (dispatch, getState) => { const hasV4Interface = !!interfaces[selectedInterface]?.ipv4_addresses; const hasV6Interface = !!interfaces[selectedInterface]?.ipv6_addresses; - if ((hasV4Interface && v4.other_server.found === STATUS_RESPONSE.ERROR) - || (hasV6Interface && v6.other_server.found === STATUS_RESPONSE.ERROR)) { + if (hasV4Interface && v4.other_server.found === STATUS_RESPONSE.ERROR) { isError = true; - dispatch(addErrorToast({ error: 'dhcp_error' })); if (v4.other_server.error) { dispatch(addErrorToast({ error: v4.other_server.error })); } + } + + if (hasV6Interface && v6.other_server.found === STATUS_RESPONSE.ERROR) { + isError = true; if (v6.other_server.error) { dispatch(addErrorToast({ error: v6.other_server.error })); } @@ -405,6 +407,7 @@ export const findActiveDhcp = (name) => async (dispatch, getState) => { } if (isError) { + dispatch(addErrorToast({ error: 'dhcp_error' })); return; } From 888c9d571450ca1b934d94446b4eed462207a2d2 Mon Sep 17 00:00:00 2001 From: Andrey Meshkov Date: Thu, 20 Aug 2020 14:37:57 +0300 Subject: [PATCH 12/12] Get rid of extra toast when static IP is not set --- client/src/actions/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/actions/index.js b/client/src/actions/index.js index b4a51065..acb76107 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -383,6 +383,7 @@ export const findActiveDhcp = (name) => async (dispatch, getState) => { const v6 = check?.v6 ?? { other_server: {} }; let isError = false; + let isStaticIPError = false; const hasV4Interface = !!interfaces[selectedInterface]?.ipv4_addresses; const hasV6Interface = !!interfaces[selectedInterface]?.ipv6_addresses; @@ -402,12 +403,17 @@ export const findActiveDhcp = (name) => async (dispatch, getState) => { } if (hasV4Interface && v4.static_ip.static === STATUS_RESPONSE.ERROR) { - isError = true; + isStaticIPError = true; dispatch(addErrorToast({ error: 'dhcp_static_ip_error' })); } + if (isError) { dispatch(addErrorToast({ error: 'dhcp_error' })); + } + + if (isStaticIPError || isError) { + // No need to proceed if there was an error discovering DHCP server return; }