Pull request: 2509 type-safety vol.1

Merge in DNS/adguard-home from 2509-type-safety to master

Updates #2509.

Squashed commit of the following:

commit 535968eb7de3a9e0817ddb57bc2320e5c5a55086
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Jan 20 15:06:16 2021 +0300

    dhcpd: fix comments

commit dc79b80381fe7a8ecec6f9659fd23710c9229f59
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Wed Jan 20 14:08:10 2021 +0300

    all: improve docs

commit 156ebf6c9bad95f82cd121f019f3b59b77b18ba6
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jan 19 17:08:15 2021 +0300

    all: improve JSON encoding and decoding
This commit is contained in:
Eugene Burkov 2021-01-20 15:59:24 +03:00
parent 715df4cd92
commit 5a50efadb2
8 changed files with 202 additions and 180 deletions

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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'