diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index ecb7d48d..17d817cf 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -3,6 +3,8 @@ package dhcpd import ( "encoding/hex" + "encoding/json" + "fmt" "net" "net/http" "path/filepath" @@ -33,6 +35,39 @@ type Lease struct { Expiry time.Time `json:"expires"` } +// MarshalJSON implements the json.Marshaler interface for *Lease. +func (l *Lease) MarshalJSON() ([]byte, error) { + type lease Lease + return json.Marshal(&struct { + HWAddr string `json:"mac"` + *lease + }{ + HWAddr: l.HWAddr.String(), + lease: (*lease)(l), + }) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for *Lease. +func (l *Lease) UnmarshalJSON(data []byte) (err error) { + type lease Lease + aux := struct { + HWAddr string `json:"mac"` + *lease + }{ + lease: (*lease)(l), + } + if err = json.Unmarshal(data, &aux); err != nil { + return err + } + + l.HWAddr, err = net.ParseMAC(aux.HWAddr) + if err != nil { + return fmt.Errorf("couldn't parse MAC address: %w", err) + } + + return nil +} + // ServerConfig - DHCP server configuration // field ordering is important -- yaml fields will mirror ordering from here type ServerConfig struct { diff --git a/internal/dhcpd/dhcphttp.go b/internal/dhcpd/dhcphttp.go index e35322f8..36bc64ef 100644 --- a/internal/dhcpd/dhcphttp.go +++ b/internal/dhcpd/dhcphttp.go @@ -8,7 +8,6 @@ import ( "net/http" "os" "strings" - "time" "github.com/AdguardTeam/AdGuardHome/internal/sysutil" "github.com/AdguardTeam/AdGuardHome/internal/util" @@ -22,25 +21,6 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string, http.Error(w, text, code) } -// []Lease -> JSON -func convertLeases(inputLeases []Lease, includeExpires bool) []map[string]string { - leases := []map[string]string{} - for _, l := range inputLeases { - lease := map[string]string{ - "mac": l.HWAddr.String(), - "ip": l.IP.String(), - "hostname": l.Hostname, - } - - if includeExpires { - lease["expires"] = l.Expiry.Format(time.RFC3339) - } - - leases = append(leases, lease) - } - return leases -} - type v4ServerConfJSON struct { GatewayIP net.IP `json:"gateway_ip"` SubnetMask net.IP `json:"subnet_mask"` @@ -49,22 +29,12 @@ type v4ServerConfJSON struct { 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.To4(), - SubnetMask: j.SubnetMask.To4(), - RangeStart: j.RangeStart.To4(), - RangeEnd: j.RangeEnd.To4(), + GatewayIP: j.GatewayIP, + SubnetMask: j.SubnetMask, + RangeStart: j.RangeStart, + RangeEnd: j.RangeEnd, LeaseDuration: j.LeaseDuration, } } @@ -74,13 +44,6 @@ type v6ServerConfJSON struct { 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, @@ -88,25 +51,30 @@ func v6JSONToServerConf(j v6ServerConfJSON) V6ServerConf { } } +// dhcpStatusResponse is the response for /control/dhcp/status endpoint. +type dhcpStatusResponse struct { + Enabled bool `json:"enabled"` + IfaceName string `json:"interface_name"` + V4 V4ServerConf `json:"v4"` + V6 V6ServerConf `json:"v6"` + Leases []Lease `json:"leases"` + StaticLeases []Lease `json:"static_leases"` +} + 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{}{ - "enabled": s.conf.Enabled, - "interface_name": s.conf.InterfaceName, - "v4": v4ServerConfToJSON(v4conf), - "v6": v6ServerConfToJSON(v6conf), - "leases": leases, - "static_leases": staticLeases, + status := &dhcpStatusResponse{ + Enabled: s.conf.Enabled, + IfaceName: s.conf.InterfaceName, + V4: V4ServerConf{}, + V6: V6ServerConf{}, } + s.srv4.WriteDiskConfig4(&status.V4) + s.srv6.WriteDiskConfig6(&status.V6) + + status.Leases = s.Leases(LeasesDynamic) + status.StaticLeases = s.Leases(LeasesStatic) + w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(status) if err != nil { @@ -115,12 +83,6 @@ func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) { } } -type staticLeaseJSON struct { - HWAddr string `json:"mac"` - IP net.IP `json:"ip"` - Hostname string `json:"hostname"` -} - type dhcpServerConfigJSON struct { Enabled bool `json:"enabled"` InterfaceName string `json:"interface_name"` @@ -233,7 +195,7 @@ type netInterfaceJSON struct { } func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { - response := map[string]interface{}{} + response := map[string]netInterfaceJSON{} ifaces, err := util.GetValidNetInterfaces() if err != nil { @@ -295,6 +257,40 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { } } +// dhcpSearchOtherResult contains information about other DHCP server for +// specific network interface. +type dhcpSearchOtherResult struct { + Found string `json:"found,omitempty"` + Error string `json:"error,omitempty"` +} + +// dhcpStaticIPStatus contains information about static IP address for DHCP +// server. +type dhcpStaticIPStatus struct { + Static string `json:"static"` + IP string `json:"ip,omitempty"` + Error string `json:"error,omitempty"` +} + +// dhcpSearchV4Result contains information about DHCPv4 server for specific +// network interface. +type dhcpSearchV4Result struct { + OtherServer dhcpSearchOtherResult `json:"other_server"` + StaticIP dhcpStaticIPStatus `json:"static_ip"` +} + +// dhcpSearchV6Result contains information about DHCPv6 server for specific +// network interface. +type dhcpSearchV6Result struct { + OtherServer dhcpSearchOtherResult `json:"other_server"` +} + +// dhcpSearchResult is a response for /control/dhcp/find_active_dhcp endpoint. +type dhcpSearchResult struct { + V4 dhcpSearchV4Result `json:"v4"` + V6 dhcpSearchV6Result `json:"v6"` +} + // Perform the following tasks: // . Search for another DHCP server running // . Check if a static IP is configured for the network interface @@ -317,50 +313,42 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque return } + result := dhcpSearchResult{ + V4: dhcpSearchV4Result{ + OtherServer: dhcpSearchOtherResult{}, + StaticIP: dhcpStaticIPStatus{}, + }, + V6: dhcpSearchV6Result{ + OtherServer: dhcpSearchOtherResult{}, + }, + } + found4, err4 := CheckIfOtherDHCPServersPresentV4(interfaceName) - staticIP := map[string]interface{}{} isStaticIP, err := sysutil.IfaceHasStaticIP(interfaceName) - staticIPStatus := "yes" if err != nil { - staticIPStatus = "error" - staticIP["error"] = err.Error() + result.V4.StaticIP.Static = "error" + result.V4.StaticIP.Error = err.Error() } else if !isStaticIP { - staticIPStatus = "no" - staticIP["ip"] = util.GetSubnet(interfaceName) + result.V4.StaticIP.Static = "no" + result.V4.StaticIP.IP = util.GetSubnet(interfaceName) } - staticIP["static"] = staticIPStatus - v4 := map[string]interface{}{} - othSrv := map[string]interface{}{} - foundVal := "no" if found4 { - foundVal = "yes" + result.V4.OtherServer.Found = "yes" } else if err4 != nil { - foundVal = "error" - othSrv["error"] = err4.Error() + result.V4.OtherServer.Found = "error" + result.V4.OtherServer.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" + result.V6.OtherServer.Found = "yes" } else if err6 != nil { - foundVal = "error" - othSrv["error"] = err6.Error() + result.V6.OtherServer.Found = "error" + result.V6.OtherServer.Error = err6.Error() } - othSrv["found"] = foundVal - v6["other_server"] = othSrv - - result := map[string]interface{}{} - result["v4"] = v4 - result["v6"] = v6 w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(result) @@ -371,7 +359,7 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque } func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) { - lj := staticLeaseJSON{} + lj := Lease{} err := json.NewDecoder(r.Body).Decode(&lj) if err != nil { httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err) @@ -387,21 +375,10 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request ip4 := lj.IP.To4() - mac, err := net.ParseMAC(lj.HWAddr) - lease := Lease{ - HWAddr: mac, - } - if ip4 == nil { - lease.IP = lj.IP.To16() + lj.IP = lj.IP.To16() - if err != nil { - httpError(r, w, http.StatusBadRequest, "invalid MAC") - - return - } - - err = s.srv6.AddStaticLease(lease) + err = s.srv6.AddStaticLease(lj) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) } @@ -409,9 +386,8 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request return } - lease.IP = ip4 - lease.Hostname = lj.Hostname - err = s.srv4.AddStaticLease(lease) + lj.IP = ip4 + err = s.srv4.AddStaticLease(lj) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) @@ -420,7 +396,7 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request } func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) { - lj := staticLeaseJSON{} + lj := Lease{} err := json.NewDecoder(r.Body).Decode(&lj) if err != nil { httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err) @@ -436,21 +412,10 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ ip4 := lj.IP.To4() - mac, err := net.ParseMAC(lj.HWAddr) - lease := Lease{ - HWAddr: mac, - } - if ip4 == nil { - lease.IP = lj.IP.To16() + lj.IP = lj.IP.To16() - if err != nil { - httpError(r, w, http.StatusBadRequest, "invalid MAC") - - return - } - - err = s.srv6.RemoveStaticLease(lease) + err = s.srv6.RemoveStaticLease(lj) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) } @@ -458,9 +423,8 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ return } - lease.IP = ip4 - lease.Hostname = lj.Hostname - err = s.srv4.RemoveStaticLease(lease) + lj.IP = ip4 + err = s.srv4.RemoveStaticLease(lj) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) diff --git a/internal/dhcpd/server.go b/internal/dhcpd/server.go index 20f6cad3..261ad4db 100644 --- a/internal/dhcpd/server.go +++ b/internal/dhcpd/server.go @@ -33,22 +33,22 @@ type DHCPServer interface { // V4ServerConf - server configuration type V4ServerConf struct { - Enabled bool `yaml:"-"` - InterfaceName string `yaml:"-"` + Enabled bool `yaml:"-" json:"-"` + InterfaceName string `yaml:"-" json:"-"` - GatewayIP net.IP `yaml:"gateway_ip"` - SubnetMask net.IP `yaml:"subnet_mask"` + GatewayIP net.IP `yaml:"gateway_ip" json:"gateway_ip"` + SubnetMask net.IP `yaml:"subnet_mask" json:"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 net.IP `yaml:"range_start"` - RangeEnd net.IP `yaml:"range_end"` + RangeStart net.IP `yaml:"range_start" json:"range_start"` + RangeEnd net.IP `yaml:"range_end" json:"range_end"` - LeaseDuration uint32 `yaml:"lease_duration"` // in seconds + LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds // IP conflict detector: time (ms) to wait for ICMP reply // 0: disable - ICMPTimeout uint32 `yaml:"icmp_timeout_msec"` + ICMPTimeout uint32 `yaml:"icmp_timeout_msec" json:"-"` // Custom Options. // @@ -58,7 +58,7 @@ type V4ServerConf struct { // // Option with IP data (only 1 IP is supported): // DEC_CODE ip IP_ADDR - Options []string `yaml:"options"` + Options []string `yaml:"options" json:"-"` ipStart net.IP // starting IP address for dynamic leases ipEnd net.IP // ending IP address for dynamic leases @@ -74,17 +74,17 @@ type V4ServerConf struct { // V6ServerConf - server configuration type V6ServerConf struct { - Enabled bool `yaml:"-"` - InterfaceName string `yaml:"-"` + Enabled bool `yaml:"-" json:"-"` + InterfaceName string `yaml:"-" json:"-"` // The first IP address for dynamic leases // The last allowed IP address ends with 0xff byte - RangeStart string `yaml:"range_start"` + RangeStart string `yaml:"range_start" json:"range_start"` - LeaseDuration uint32 `yaml:"lease_duration"` // in seconds + LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds - RaSlaacOnly bool `yaml:"ra_slaac_only"` // send ICMPv6.RA packets without MO flags - RaAllowSlaac bool `yaml:"ra_allow_slaac"` // send ICMPv6.RA packets with MO flags + RaSlaacOnly bool `yaml:"ra_slaac_only" json:"-"` // send ICMPv6.RA packets without MO flags + RaAllowSlaac bool `yaml:"ra_allow_slaac" json:"-"` // send ICMPv6.RA packets with MO flags ipStart net.IP // starting IP address for dynamic leases leaseTime time.Duration // the time during which a dynamic lease is considered valid diff --git a/internal/dhcpd/v4.go b/internal/dhcpd/v4.go index 038d8f9c..2f5484a2 100644 --- a/internal/dhcpd/v4.go +++ b/internal/dhcpd/v4.go @@ -77,7 +77,10 @@ func (s *v4Server) blacklisted(l *Lease) bool { // GetLeases returns the list of current DHCP leases (thread-safe) func (s *v4Server) GetLeases(flags int) []Lease { - var result []Lease + // The function shouldn't return nil value because zero-length slice + // behaves differently in cases like marshalling. Our front-end also + // requires non-nil value in the response. + result := []Lease{} now := time.Now().Unix() s.leasesLock.Lock() diff --git a/internal/dhcpd/v6.go b/internal/dhcpd/v6.go index 2dd41b5c..0f8cddfd 100644 --- a/internal/dhcpd/v6.go +++ b/internal/dhcpd/v6.go @@ -72,7 +72,10 @@ func (s *v6Server) ResetLeases(ll []*Lease) { // GetLeases - get current leases func (s *v6Server) GetLeases(flags int) []Lease { - var result []Lease + // The function shouldn't return nil value because zero-length slice + // behaves differently in cases like marshalling. Our front-end also + // requires non-nil value in the response. + result := []Lease{} s.leasesLock.Lock() for _, lease := range s.leases { if lease.Expiry.Unix() == leaseExpireStatic { diff --git a/internal/dnsfilter/safebrowsing.go b/internal/dnsfilter/safebrowsing.go index 87cd2607..cec3081b 100644 --- a/internal/dnsfilter/safebrowsing.go +++ b/internal/dnsfilter/safebrowsing.go @@ -346,16 +346,12 @@ func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Req } func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "enabled": d.Config.SafeBrowsingEnabled, - } - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) + err := json.NewEncoder(w).Encode(&struct { + Enabled bool `json:"enabled"` + }{ + Enabled: d.Config.SafeBrowsingEnabled, + }) if err != nil { httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err) return @@ -373,17 +369,12 @@ func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request } func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "enabled": d.Config.ParentalEnabled, - } - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - return - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) + err := json.NewEncoder(w).Encode(&struct { + Enabled bool `json:"enabled"` + }{ + Enabled: d.Config.ParentalEnabled, + }) if err != nil { httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err) return diff --git a/internal/dnsfilter/safesearch.go b/internal/dnsfilter/safesearch.go index 4aefa5e1..958962db 100644 --- a/internal/dnsfilter/safesearch.go +++ b/internal/dnsfilter/safesearch.go @@ -133,17 +133,12 @@ func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Reque } func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "enabled": d.Config.SafeSearchEnabled, - } - jsonVal, err := json.Marshal(data) - if err != nil { - httpError(r, w, http.StatusInternalServerError, "Unable to marshal status json: %s", err) - return - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(jsonVal) + err := json.NewEncoder(w).Encode(&struct { + Enabled bool `json:"enabled"` + }{ + Enabled: d.Config.SafeSearchEnabled, + }) if err != nil { httpError(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err) return diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index dbad4e50..e41eac24 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -350,7 +350,26 @@ 'application/json': 'schema': '$ref': '#/components/schemas/DhcpStatus' - '501': + '500': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/Error' + 'description': 'Not implemented (for example, on Windows).' + '/dhcp/interfaces': + 'get': + 'tags': + - 'dhcp' + 'operationId': 'dhcpInterfaces' + 'summary': 'Gets the available interfaces' + 'responses': + '200': + 'description': 'OK.' + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/NetInterfaces' + '500': 'content': 'application/json': 'schema': @@ -1620,6 +1639,12 @@ 'type': 'array' 'items': '$ref': '#/components/schemas/DhcpStaticLease' + 'NetInterfaces': + 'type': 'object' + 'description': > + Network interfaces dictionary, keys are interface names. + 'additionalProperties': + '$ref': '#/components/schemas/NetInterface' 'DhcpSearchResult': 'type': 'object' @@ -1650,7 +1675,12 @@ 'properties': 'found': 'type': 'string' - 'description': 'yes|no|error' + 'enum': + - 'yes' + - 'no' + - 'error' + 'description': > + The result of searching the other DHCP server. 'example': 'no' 'error': 'type': 'string' @@ -1662,7 +1692,12 @@ 'properties': 'static': 'type': 'string' - 'description': 'yes|no|error' + 'enum': + - 'yes' + - 'no' + - 'error' + 'description': > + The result of determining static IP address. 'example': 'yes' 'ip': 'type': 'string' @@ -2015,11 +2050,7 @@ 'format': 'uint16' 'example': 80 'interfaces': - 'type': 'object' - 'description': > - Network interfaces dictionary, keys are interface names. - 'additionalProperties': - '$ref': '#/components/schemas/NetInterface' + '$ref': '#/components/schemas/NetInterfaces' 'AddressesInfoBeta': 'type': 'object' 'description': 'AdGuard Home addresses configuration'