Pull request: Migrate to netip.Addr vol.1

Merge in DNS/adguard-home from 2926-lla-v6 to master

Updates #2926.
Updates #5035.

Squashed commit of the following:

commit 2e770d4b6d4e1ec3f7762f2f2466662983bf146c
Merge: 25c1afc5 893358ea
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Oct 14 15:14:56 2022 +0300

    Merge branch 'master' into 2926-lla-v6

commit 25c1afc5f0a5027fafac9dee77618886aefee29c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Oct 13 18:24:20 2022 +0300

    all: imp code, docs

commit 59549c4f74ee17b10eae542d1f1828d4e59894c9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Oct 11 18:49:09 2022 +0300

    dhcpd: use netip initially

commit 1af623096b0517d07752385540f2f750f7f5b3bb
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Sep 30 18:03:52 2022 +0300

    all: imp docs, code

commit e9faeb71dbc0e887b25a7f3d5b33a401805f2ae7
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Sep 29 14:56:37 2022 +0300

    all: use netip for web

commit 38305e555a6884c3bd1b0839330b942ce0e59093
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Sep 28 19:13:58 2022 +0300

    add basic lla
This commit is contained in:
Eugene Burkov 2022-10-14 15:29:44 +03:00
parent 893358ea71
commit 4582b1c919
29 changed files with 465 additions and 367 deletions

View File

@ -17,6 +17,7 @@ and this project adheres to
## Added ## Added
- The ability to serve DNS on link-local IPv6 addresses ([#2926]).
- The ability to put [ClientIDs][clientid] into DNS-over-HTTPS hostnames as - The ability to put [ClientIDs][clientid] into DNS-over-HTTPS hostnames as
opposed to URL paths ([#3418]). Note that AdGuard Home checks the server name opposed to URL paths ([#3418]). Note that AdGuard Home checks the server name
only if the URL does not contain a ClientID. only if the URL does not contain a ClientID.
@ -33,6 +34,7 @@ and this project adheres to
cached now ([#4942]). cached now ([#4942]).
- Web UI not switching to HTTP/3 ([#4986], [#4993]). - Web UI not switching to HTTP/3 ([#4986], [#4993]).
[#2926]: https://github.com/AdguardTeam/AdGuardHome/issues/2926
[#3418]: https://github.com/AdguardTeam/AdGuardHome/issues/3418 [#3418]: https://github.com/AdguardTeam/AdGuardHome/issues/3418
[#4942]: https://github.com/AdguardTeam/AdGuardHome/issues/4942 [#4942]: https://github.com/AdguardTeam/AdGuardHome/issues/4942
[#4986]: https://github.com/AdguardTeam/AdGuardHome/issues/4986 [#4986]: https://github.com/AdguardTeam/AdGuardHome/issues/4986

View File

@ -6,6 +6,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net"
"net/netip"
"os" "os"
"time" "time"
@ -38,48 +39,44 @@ func checkOtherDHCP(ifaceName string) (ok4, ok6 bool, err4, err6 error) {
} }
// ifaceIPv4Subnet returns the first suitable IPv4 subnetwork iface has. // ifaceIPv4Subnet returns the first suitable IPv4 subnetwork iface has.
func ifaceIPv4Subnet(iface *net.Interface) (subnet *net.IPNet, err error) { func ifaceIPv4Subnet(iface *net.Interface) (subnet netip.Prefix, err error) {
var addrs []net.Addr var addrs []net.Addr
if addrs, err = iface.Addrs(); err != nil { if addrs, err = iface.Addrs(); err != nil {
return nil, err return netip.Prefix{}, err
} }
for _, a := range addrs { for _, a := range addrs {
var ip net.IP
var maskLen int
switch a := a.(type) { switch a := a.(type) {
case *net.IPAddr: case *net.IPAddr:
subnet = &net.IPNet{ ip = a.IP
IP: a.IP, maskLen, _ = ip.DefaultMask().Size()
Mask: a.IP.DefaultMask(),
}
case *net.IPNet: case *net.IPNet:
subnet = a ip = a.IP
maskLen, _ = a.Mask.Size()
default: default:
continue continue
} }
if ip4 := subnet.IP.To4(); ip4 != nil { if ip = ip.To4(); ip != nil {
subnet.IP = ip4 return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ip)), maskLen), nil
return subnet, nil
} }
} }
return nil, fmt.Errorf("interface %s has no ipv4 addresses", iface.Name) return netip.Prefix{}, fmt.Errorf("interface %s has no ipv4 addresses", iface.Name)
} }
// checkOtherDHCPv4 sends a DHCP request to the specified network interface, and // checkOtherDHCPv4 sends a DHCP request to the specified network interface, and
// waits for a response for a period defined by defaultDiscoverTime. // waits for a response for a period defined by defaultDiscoverTime.
func checkOtherDHCPv4(iface *net.Interface) (ok bool, err error) { func checkOtherDHCPv4(iface *net.Interface) (ok bool, err error) {
var subnet *net.IPNet var subnet netip.Prefix
if subnet, err = ifaceIPv4Subnet(iface); err != nil { if subnet, err = ifaceIPv4Subnet(iface); err != nil {
return false, err return false, err
} }
// Resolve broadcast addr. // Resolve broadcast addr.
dst := netutil.IPPort{ dst := netip.AddrPortFrom(BroadcastFromPref(subnet), 67).String()
IP: BroadcastFromIPNet(subnet),
Port: 67,
}.String()
var dstAddr *net.UDPAddr var dstAddr *net.UDPAddr
if dstAddr, err = net.ResolveUDPAddr("udp4", dst); err != nil { if dstAddr, err = net.ResolveUDPAddr("udp4", dst); err != nil {
return false, fmt.Errorf("couldn't resolve UDP address %s: %w", dst, err) return false, fmt.Errorf("couldn't resolve UDP address %s: %w", dst, err)

View File

@ -106,9 +106,13 @@ type HostsContainer struct {
done chan struct{} done chan struct{}
// updates is the channel for receiving updated hosts. // updates is the channel for receiving updated hosts.
//
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
updates chan *netutil.IPMap updates chan *netutil.IPMap
// last is the set of hosts that was cached within last detected change. // last is the set of hosts that was cached within last detected change.
//
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
last *netutil.IPMap last *netutil.IPMap
// fsys is the working file system to read hosts files from. // fsys is the working file system to read hosts files from.

View File

@ -7,12 +7,12 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"syscall" "syscall"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
) )
// Variables and functions to substitute in tests. // Variables and functions to substitute in tests.
@ -31,6 +31,12 @@ var (
// the IP being static is available. // the IP being static is available.
const ErrNoStaticIPInfo errors.Error = "no information about static ip" const ErrNoStaticIPInfo errors.Error = "no information about static ip"
// IPv4Localhost returns 127.0.0.1, which returns true for [netip.Addr.Is4].
func IPv4Localhost() (ip netip.Addr) { return netip.AddrFrom4([4]byte{127, 0, 0, 1}) }
// IPv6Localhost returns ::1, which returns true for [netip.Addr.Is6].
func IPv6Localhost() (ip netip.Addr) { return netip.AddrFrom16([16]byte{15: 1}) }
// IfaceHasStaticIP checks if interface is configured to have static IP address. // IfaceHasStaticIP checks if interface is configured to have static IP address.
// If it can't give a definitive answer, it returns false and an error for which // If it can't give a definitive answer, it returns false and an error for which
// errors.Is(err, ErrNoStaticIPInfo) is true. // errors.Is(err, ErrNoStaticIPInfo) is true.
@ -47,26 +53,31 @@ func IfaceSetStaticIP(ifaceName string) (err error) {
// //
// TODO(e.burkov): Investigate if the gateway address may be fetched in another // TODO(e.burkov): Investigate if the gateway address may be fetched in another
// way since not every machine has the software installed. // way since not every machine has the software installed.
func GatewayIP(ifaceName string) (ip net.IP) { func GatewayIP(ifaceName string) (ip netip.Addr) {
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName) code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
if err != nil { if err != nil {
log.Debug("%s", err) log.Debug("%s", err)
return nil return netip.Addr{}
} else if code != 0 { } else if code != 0 {
log.Debug("fetching gateway ip: unexpected exit code: %d", code) log.Debug("fetching gateway ip: unexpected exit code: %d", code)
return nil return netip.Addr{}
} }
fields := bytes.Fields(out) fields := bytes.Fields(out)
// The meaningful "ip route" command output should contain the word // The meaningful "ip route" command output should contain the word
// "default" at first field and default gateway IP address at third field. // "default" at first field and default gateway IP address at third field.
if len(fields) < 3 || string(fields[0]) != "default" { if len(fields) < 3 || string(fields[0]) != "default" {
return nil return netip.Addr{}
} }
return net.ParseIP(string(fields[2])) ip, err = netip.ParseAddr(string(fields[2]))
if err != nil {
return netip.Addr{}
}
return ip
} }
// CanBindPrivilegedPorts checks if current process can bind to privileged // CanBindPrivilegedPorts checks if current process can bind to privileged
@ -78,9 +89,9 @@ func CanBindPrivilegedPorts() (can bool, err error) {
// NetInterface represents an entry of network interfaces map. // NetInterface represents an entry of network interfaces map.
type NetInterface struct { type NetInterface struct {
// Addresses are the network interface addresses. // Addresses are the network interface addresses.
Addresses []net.IP `json:"ip_addresses,omitempty"` Addresses []netip.Addr `json:"ip_addresses,omitempty"`
// Subnets are the IP networks for this network interface. // Subnets are the IP networks for this network interface.
Subnets []*net.IPNet `json:"-"` Subnets []netip.Prefix `json:"-"`
Name string `json:"name"` Name string `json:"name"`
HardwareAddr net.HardwareAddr `json:"hardware_address"` HardwareAddr net.HardwareAddr `json:"hardware_address"`
Flags net.Flags `json:"flags"` Flags net.Flags `json:"flags"`
@ -101,57 +112,79 @@ func (iface NetInterface) MarshalJSON() ([]byte, error) {
}) })
} }
func NetInterfaceFrom(iface *net.Interface) (niface *NetInterface, err error) {
niface = &NetInterface{
Name: iface.Name,
HardwareAddr: iface.HardwareAddr,
Flags: iface.Flags,
MTU: iface.MTU,
}
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
}
// Collect network interface addresses.
for _, addr := range addrs {
n, ok := addr.(*net.IPNet)
if !ok {
// Should be *net.IPNet, this is weird.
return nil, fmt.Errorf("expected %[2]s to be %[1]T, got %[2]T", n, addr)
} else if ip4 := n.IP.To4(); ip4 != nil {
n.IP = ip4
}
ip, ok := netip.AddrFromSlice(n.IP)
if !ok {
return nil, fmt.Errorf("bad address %s", n.IP)
}
ip = ip.Unmap()
if ip.IsLinkLocalUnicast() {
// Ignore link-local IPv4.
if ip.Is4() {
continue
}
ip = ip.WithZone(iface.Name)
}
ones, _ := n.Mask.Size()
p := netip.PrefixFrom(ip, ones)
niface.Addresses = append(niface.Addresses, ip)
niface.Subnets = append(niface.Subnets, p)
}
return niface, nil
}
// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and // GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and
// WEB only we do not return link-local addresses here. // WEB only we do not return link-local addresses here.
// //
// TODO(e.burkov): Can't properly test the function since it's nontrivial to // TODO(e.burkov): Can't properly test the function since it's nontrivial to
// substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used. // substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used.
func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) { func GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) {
ifaces, err := net.Interfaces() ifaces, err := net.Interfaces()
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't get interfaces: %w", err) return nil, fmt.Errorf("getting interfaces: %w", err)
} else if len(ifaces) == 0 { } else if len(ifaces) == 0 {
return nil, errors.Error("couldn't find any legible interface") return nil, errors.Error("no legible interfaces")
} }
for _, iface := range ifaces { for i := range ifaces {
var addrs []net.Addr var niface *NetInterface
addrs, err = iface.Addrs() niface, err = NetInterfaceFrom(&ifaces[i])
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err) return nil, err
} } else if len(niface.Addresses) != 0 {
netIface := &NetInterface{
MTU: iface.MTU,
Name: iface.Name,
HardwareAddr: iface.HardwareAddr,
Flags: iface.Flags,
}
// Collect network interface addresses.
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
// Should be net.IPNet, this is weird.
return nil, fmt.Errorf("got %s that is not net.IPNet, it is %T", addr, addr)
}
// Ignore link-local.
if ipNet.IP.IsLinkLocalUnicast() {
continue
}
netIface.Addresses = append(netIface.Addresses, ipNet.IP)
netIface.Subnets = append(netIface.Subnets, ipNet)
}
// Discard interfaces with no addresses. // Discard interfaces with no addresses.
if len(netIface.Addresses) != 0 { nifaces = append(nifaces, niface)
netIfaces = append(netIfaces, netIface)
} }
} }
return netIfaces, nil return nifaces, nil
} }
// InterfaceByIP returns the name of the interface bound to ip. // InterfaceByIP returns the name of the interface bound to ip.
@ -160,7 +193,7 @@ func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
// IP address can be shared by multiple interfaces in some configurations. // IP address can be shared by multiple interfaces in some configurations.
// //
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb. // TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
func InterfaceByIP(ip net.IP) (ifaceName string) { func InterfaceByIP(ip netip.Addr) (ifaceName string) {
ifaces, err := GetValidNetInterfacesForWeb() ifaces, err := GetValidNetInterfacesForWeb()
if err != nil { if err != nil {
return "" return ""
@ -168,7 +201,7 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
for _, iface := range ifaces { for _, iface := range ifaces {
for _, addr := range iface.Addresses { for _, addr := range iface.Addresses {
if ip.Equal(addr) { if ip == addr {
return iface.Name return iface.Name
} }
} }
@ -177,15 +210,16 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
return "" return ""
} }
// GetSubnet returns pointer to net.IPNet for the specified interface or nil if // GetSubnet returns the subnet corresponding to the interface of zero prefix if
// the search fails. // the search fails.
// //
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb. // TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
func GetSubnet(ifaceName string) *net.IPNet { func GetSubnet(ifaceName string) (p netip.Prefix) {
netIfaces, err := GetValidNetInterfacesForWeb() netIfaces, err := GetValidNetInterfacesForWeb()
if err != nil { if err != nil {
log.Error("Could not get network interfaces info: %v", err) log.Error("Could not get network interfaces info: %v", err)
return nil
return p
} }
for _, netIface := range netIfaces { for _, netIface := range netIfaces {
@ -194,14 +228,14 @@ func GetSubnet(ifaceName string) *net.IPNet {
} }
} }
return nil return p
} }
// CheckPort checks if the port is available for binding. network is expected // CheckPort checks if the port is available for binding. network is expected
// to be one of "udp" and "tcp". // to be one of "udp" and "tcp".
func CheckPort(network string, ip net.IP, port int) (err error) { func CheckPort(network string, ipp netip.AddrPort) (err error) {
var c io.Closer var c io.Closer
addr := netutil.IPPort{IP: ip, Port: port}.String() addr := ipp.String()
switch network { switch network {
case "tcp": case "tcp":
c, err = net.Listen(network, addr) c, err = net.Listen(network, addr)
@ -251,18 +285,23 @@ func CollectAllIfacesAddrs() (addrs []string, err error) {
return addrs, nil return addrs, nil
} }
// BroadcastFromIPNet calculates the broadcast IP address for n. // BroadcastFromPref calculates the broadcast IP address for p.
func BroadcastFromIPNet(n *net.IPNet) (dc net.IP) { func BroadcastFromPref(p netip.Prefix) (bc netip.Addr) {
dc = netutil.CloneIP(n.IP) bc = p.Addr().Unmap()
if !bc.IsValid() {
mask := n.Mask return netip.Addr{}
if mask == nil {
mask = dc.DefaultMask()
} }
for i, b := range mask { maskLen, addrLen := p.Bits(), bc.BitLen()
dc[i] |= ^b if maskLen == addrLen {
return bc
} }
return dc ipBytes := bc.AsSlice()
for i := maskLen; i < addrLen; i++ {
ipBytes[i/8] |= 1 << (7 - (i % 8))
}
bc, _ = netip.AddrFromSlice(ipBytes)
return bc
} }

View File

@ -6,7 +6,7 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io" "io"
"net" "net/netip"
"os" "os"
"strings" "strings"
@ -151,7 +151,7 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
// interface through dhcpcd.conf. // interface through dhcpcd.conf.
func ifaceSetStaticIP(ifaceName string) (err error) { func ifaceSetStaticIP(ifaceName string) (err error) {
ipNet := GetSubnet(ifaceName) ipNet := GetSubnet(ifaceName)
if ipNet.IP == nil { if !ipNet.Addr().IsValid() {
return errors.Error("can't get IP address") return errors.Error("can't get IP address")
} }
@ -174,7 +174,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
// dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that // dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that
// configure the interface to have a static IP. // configure the interface to have a static IP.
func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf string) { func dhcpcdConfIface(ifaceName string, subnet netip.Prefix, gateway netip.Addr) (conf string) {
b := &strings.Builder{} b := &strings.Builder{}
stringutil.WriteToBuilder( stringutil.WriteToBuilder(
b, b,
@ -183,15 +183,15 @@ func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf stri
" added by AdGuard Home.\ninterface ", " added by AdGuard Home.\ninterface ",
ifaceName, ifaceName,
"\nstatic ip_address=", "\nstatic ip_address=",
ipNet.String(), subnet.String(),
"\n", "\n",
) )
if gwIP != nil { if gateway != (netip.Addr{}) {
stringutil.WriteToBuilder(b, "static routers=", gwIP.String(), "\n") stringutil.WriteToBuilder(b, "static routers=", gateway.String(), "\n")
} }
stringutil.WriteToBuilder(b, "static domain_name_servers=", ipNet.IP.String(), "\n\n") stringutil.WriteToBuilder(b, "static domain_name_servers=", subnet.Addr().String(), "\n\n")
return b.String() return b.String()
} }

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"net" "net"
"net/netip"
"os" "os"
"strings" "strings"
"testing" "testing"
@ -93,34 +94,29 @@ func TestGatewayIP(t *testing.T) {
const cmd = "ip route show dev " + ifaceName const cmd = "ip route show dev " + ifaceName
testCases := []struct { testCases := []struct {
name string
shell mapShell shell mapShell
want net.IP want netip.Addr
name string
}{{ }{{
name: "success_v4",
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil), shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
want: net.IP{1, 2, 3, 4}.To16(), want: netip.MustParseAddr("1.2.3.4"),
name: "success_v4",
}, { }, {
name: "success_v6",
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil), shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
want: net.IP{ want: netip.MustParseAddr("::ffff"),
0x0, 0x0, 0x0, 0x0, name: "success_v6",
0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0xFF, 0xFF,
},
}, { }, {
name: "bad_output",
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil), shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
want: nil, want: netip.Addr{},
name: "bad_output",
}, { }, {
name: "err_runcmd",
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")), shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
want: nil, want: netip.Addr{},
name: "err_runcmd",
}, { }, {
name: "bad_code",
shell: theOnlyCmd(cmd, 1, "", nil), shell: theOnlyCmd(cmd, 1, "", nil),
want: nil, want: netip.Addr{},
name: "bad_code",
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@ -150,65 +146,64 @@ func TestInterfaceByIP(t *testing.T) {
} }
func TestBroadcastFromIPNet(t *testing.T) { func TestBroadcastFromIPNet(t *testing.T) {
known6 := net.IP{ known4 := netip.MustParseAddr("192.168.0.1")
1, 2, 3, 4, fullBroadcast4 := netip.MustParseAddr("255.255.255.255")
5, 6, 7, 8,
9, 10, 11, 12, known6 := netip.MustParseAddr("102:304:506:708:90a:b0c:d0e:f10")
13, 14, 15, 16,
}
testCases := []struct { testCases := []struct {
pref netip.Prefix
want netip.Addr
name string name string
subnet *net.IPNet
want net.IP
}{{ }{{
pref: netip.PrefixFrom(known4, 0),
want: fullBroadcast4,
name: "full", name: "full",
subnet: &net.IPNet{
IP: net.IP{192, 168, 0, 1},
Mask: net.IPMask{255, 255, 15, 0},
},
want: net.IP{192, 168, 240, 255},
}, { }, {
name: "ipv6_no_mask", pref: netip.PrefixFrom(known4, 20),
subnet: &net.IPNet{ want: netip.MustParseAddr("192.168.15.255"),
IP: known6, name: "full",
}, }, {
pref: netip.PrefixFrom(known6, netutil.IPv6BitLen),
want: known6, want: known6,
name: "ipv6_no_mask",
}, { }, {
pref: netip.PrefixFrom(known4, netutil.IPv4BitLen),
want: known4,
name: "ipv4_no_mask", name: "ipv4_no_mask",
subnet: &net.IPNet{
IP: net.IP{192, 168, 1, 2},
},
want: net.IP{192, 168, 1, 255},
}, { }, {
pref: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
want: fullBroadcast4,
name: "unspecified", name: "unspecified",
subnet: &net.IPNet{ }, {
IP: net.IP{0, 0, 0, 0}, pref: netip.Prefix{},
Mask: net.IPMask{0, 0, 0, 0}, want: netip.Addr{},
}, name: "invalid",
want: net.IPv4bcast,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
bc := BroadcastFromIPNet(tc.subnet) assert.Equal(t, tc.want, BroadcastFromPref(tc.pref))
assert.True(t, bc.Equal(tc.want), bc)
}) })
} }
} }
func TestCheckPort(t *testing.T) { func TestCheckPort(t *testing.T) {
laddr := netip.AddrPortFrom(IPv4Localhost(), 0)
t.Run("tcp_bound", func(t *testing.T) { t.Run("tcp_bound", func(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:") l, err := net.Listen("tcp", laddr.String())
require.NoError(t, err) require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, l.Close) testutil.CleanupAndRequireSuccess(t, l.Close)
ipp := netutil.IPPortFromAddr(l.Addr()) addr := l.Addr()
require.NotNil(t, ipp) require.IsType(t, new(net.TCPAddr), addr)
require.NotNil(t, ipp.IP)
require.NotZero(t, ipp.Port)
err = CheckPort("tcp", ipp.IP, ipp.Port) ipp := addr.(*net.TCPAddr).AddrPort()
require.Equal(t, laddr.Addr(), ipp.Addr())
require.NotZero(t, ipp.Port())
err = CheckPort("tcp", ipp)
target := &net.OpError{} target := &net.OpError{}
require.ErrorAs(t, err, &target) require.ErrorAs(t, err, &target)
@ -216,16 +211,18 @@ func TestCheckPort(t *testing.T) {
}) })
t.Run("udp_bound", func(t *testing.T) { t.Run("udp_bound", func(t *testing.T) {
conn, err := net.ListenPacket("udp", "127.0.0.1:") conn, err := net.ListenPacket("udp", laddr.String())
require.NoError(t, err) require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, conn.Close) testutil.CleanupAndRequireSuccess(t, conn.Close)
ipp := netutil.IPPortFromAddr(conn.LocalAddr()) addr := conn.LocalAddr()
require.NotNil(t, ipp) require.IsType(t, new(net.UDPAddr), addr)
require.NotNil(t, ipp.IP)
require.NotZero(t, ipp.Port)
err = CheckPort("udp", ipp.IP, ipp.Port) ipp := addr.(*net.UDPAddr).AddrPort()
require.Equal(t, laddr.Addr(), ipp.Addr())
require.NotZero(t, ipp.Port())
err = CheckPort("udp", ipp)
target := &net.OpError{} target := &net.OpError{}
require.ErrorAs(t, err, &target) require.ErrorAs(t, err, &target)
@ -233,12 +230,12 @@ func TestCheckPort(t *testing.T) {
}) })
t.Run("bad_network", func(t *testing.T) { t.Run("bad_network", func(t *testing.T) {
err := CheckPort("bad_network", nil, 0) err := CheckPort("bad_network", netip.AddrPortFrom(netip.Addr{}, 0))
assert.NoError(t, err) assert.NoError(t, err)
}) })
t.Run("can_bind", func(t *testing.T) { t.Run("can_bind", func(t *testing.T) {
err := CheckPort("udp", net.IP{0, 0, 0, 0}, 0) err := CheckPort("udp", netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
assert.NoError(t, err) assert.NoError(t, err)
}) })
} }
@ -322,18 +319,18 @@ func TestNetInterface_MarshalJSON(t *testing.T) {
`"mtu":1500` + `"mtu":1500` +
`}` + "\n" `}` + "\n"
ip4, ip6 := net.IP{1, 2, 3, 4}, net.IP{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
mask4, mask6 := net.CIDRMask(24, netutil.IPv4BitLen), net.CIDRMask(8, netutil.IPv6BitLen) require.True(t, ok)
ip6, ok := netip.AddrFromSlice([]byte{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
require.True(t, ok)
net4 := netip.PrefixFrom(ip4, 24)
net6 := netip.PrefixFrom(ip6, 8)
iface := &NetInterface{ iface := &NetInterface{
Addresses: []net.IP{ip4, ip6}, Addresses: []netip.Addr{ip4, ip6},
Subnets: []*net.IPNet{{ Subnets: []netip.Prefix{net4, net6},
IP: ip4.Mask(mask4),
Mask: mask4,
}, {
IP: ip6.Mask(mask6),
Mask: mask6,
}},
Name: "iface0", Name: "iface0",
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
Flags: net.FlagUp | net.FlagMulticast, Flags: net.FlagUp | net.FlagMulticast,

View File

@ -3,12 +3,12 @@ package dhcpd
import ( import (
"fmt" "fmt"
"net" "net"
"net/netip"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
) )
// ServerConfig is the configuration for the DHCP server. The order of YAML // ServerConfig is the configuration for the DHCP server. The order of YAML
@ -65,16 +65,16 @@ type V4ServerConf struct {
Enabled bool `yaml:"-" json:"-"` Enabled bool `yaml:"-" json:"-"`
InterfaceName string `yaml:"-" json:"-"` InterfaceName string `yaml:"-" json:"-"`
GatewayIP net.IP `yaml:"gateway_ip" json:"gateway_ip"` GatewayIP netip.Addr `yaml:"gateway_ip" json:"gateway_ip"`
SubnetMask net.IP `yaml:"subnet_mask" json:"subnet_mask"` SubnetMask netip.Addr `yaml:"subnet_mask" json:"subnet_mask"`
// broadcastIP is the broadcasting address pre-calculated from the // broadcastIP is the broadcasting address pre-calculated from the
// configured gateway IP and subnet mask. // configured gateway IP and subnet mask.
broadcastIP net.IP broadcastIP netip.Addr
// The first & the last IP address for dynamic leases // The first & the last IP address for dynamic leases
// Bytes [0..2] of the last allowed IP address must match the first IP // Bytes [0..2] of the last allowed IP address must match the first IP
RangeStart net.IP `yaml:"range_start" json:"range_start"` RangeStart netip.Addr `yaml:"range_start" json:"range_start"`
RangeEnd net.IP `yaml:"range_end" json:"range_end"` RangeEnd netip.Addr `yaml:"range_end" json:"range_end"`
LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds
@ -95,11 +95,11 @@ type V4ServerConf struct {
ipRange *ipRange ipRange *ipRange
leaseTime time.Duration // the time during which a dynamic lease is considered valid 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 dnsIPAddrs []netip.Addr // IPv4 addresses to return to DHCP clients as DNS server addresses
// subnet contains the DHCP server's subnet. The IP is the IP of the // subnet contains the DHCP server's subnet. The IP is the IP of the
// gateway. // gateway.
subnet *net.IPNet subnet netip.Prefix
// notify is a way to signal to other components that leases have been // notify is a way to signal to other components that leases have been
// changed. notify must be called outside of locked sections, since the // changed. notify must be called outside of locked sections, since the
@ -113,16 +113,12 @@ type V4ServerConf struct {
// errNilConfig is an error returned by validation method if the config is nil. // errNilConfig is an error returned by validation method if the config is nil.
const errNilConfig errors.Error = "nil config" const errNilConfig errors.Error = "nil config"
// ensureV4 returns a 4-byte version of ip. An error is returned if the passed // ensureV4 returns an unmapped version of ip. An error is returned if the
// ip is not an IPv4. // passed ip is not an IPv4.
func ensureV4(ip net.IP) (ip4 net.IP, err error) { func ensureV4(ip netip.Addr, kind string) (ip4 netip.Addr, err error) {
if ip == nil { ip4 = ip.Unmap()
return nil, fmt.Errorf("%v is not an IP address", ip) if !ip4.IsValid() || !ip4.Is4() {
} return netip.Addr{}, fmt.Errorf("%v is not an IPv4 %s", ip, kind)
ip4 = ip.To4()
if ip4 == nil {
return nil, fmt.Errorf("%v is not an IPv4 address", ip)
} }
return ip4, nil return ip4, nil
@ -139,33 +135,45 @@ func (c *V4ServerConf) Validate() (err error) {
return errNilConfig return errNilConfig
} }
var gatewayIP net.IP gatewayIP, err := ensureV4(c.GatewayIP, "address")
gatewayIP, err = ensureV4(c.GatewayIP)
if err != nil { if err != nil {
// Don't wrap an errors since it's inforative enough as is and there is // Don't wrap an errors since it's informative enough as is and there is
// an annotation deferred already. // an annotation deferred already.
return err return err
} }
if c.SubnetMask == nil { subnetMask, err := ensureV4(c.SubnetMask, "subnet mask")
return fmt.Errorf("invalid subnet mask: %v", c.SubnetMask)
}
subnetMask := net.IPMask(netutil.CloneIP(c.SubnetMask.To4()))
c.subnet = &net.IPNet{
IP: gatewayIP,
Mask: subnetMask,
}
c.broadcastIP = aghnet.BroadcastFromIPNet(c.subnet)
c.ipRange, err = newIPRange(c.RangeStart, c.RangeEnd)
if err != nil { if err != nil {
// Don't wrap an errors since it's inforative enough as is and there is // Don't wrap an errors since it's informative enough as is and there is
// an annotation deferred already.
return err
}
maskLen, _ := net.IPMask(subnetMask.AsSlice()).Size()
c.subnet = netip.PrefixFrom(gatewayIP, maskLen)
c.broadcastIP = aghnet.BroadcastFromPref(c.subnet)
rangeStart, err := ensureV4(c.RangeStart, "address")
if err != nil {
// Don't wrap an errors since it's informative enough as is and there is
// an annotation deferred already.
return err
}
rangeEnd, err := ensureV4(c.RangeEnd, "address")
if err != nil {
// Don't wrap an errors since it's informative enough as is and there is
// an annotation deferred already. // an annotation deferred already.
return err return err
} }
if c.ipRange.contains(gatewayIP) { c.ipRange, err = newIPRange(rangeStart.AsSlice(), rangeEnd.AsSlice())
if err != nil {
// Don't wrap an errors since it's informative enough as is and there is
// an annotation deferred already.
return err
}
if c.ipRange.contains(gatewayIP.AsSlice()) {
return fmt.Errorf("gateway ip %v in the ip range: %v-%v", return fmt.Errorf("gateway ip %v in the ip range: %v-%v",
gatewayIP, gatewayIP,
c.RangeStart, c.RangeStart,
@ -173,14 +181,14 @@ func (c *V4ServerConf) Validate() (err error) {
) )
} }
if !c.subnet.Contains(c.RangeStart) { if !c.subnet.Contains(rangeStart) {
return fmt.Errorf("range start %v is outside network %v", return fmt.Errorf("range start %v is outside network %v",
c.RangeStart, c.RangeStart,
c.subnet, c.subnet,
) )
} }
if !c.subnet.Contains(c.RangeEnd) { if !c.subnet.Contains(rangeEnd) {
return fmt.Errorf("range end %v is outside network %v", return fmt.Errorf("range end %v is outside network %v",
c.RangeEnd, c.RangeEnd,
c.subnet, c.subnet,

View File

@ -73,10 +73,10 @@ func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err erro
return &dhcpConn{ return &dhcpConn{
udpConn: bcast, udpConn: bcast,
bcastIP: s.conf.broadcastIP, bcastIP: s.conf.broadcastIP.AsSlice(),
rawConn: ucast, rawConn: ucast,
srcMAC: iface.HardwareAddr, srcMAC: iface.HardwareAddr,
srcIP: s.conf.dnsIPAddrs[0], srcIP: s.conf.dnsIPAddrs[0].AsSlice(),
}, nil }, nil
} }

View File

@ -242,7 +242,7 @@ func Create(conf *ServerConfig) (s *server, err error) {
v4conf := conf.Conf4 v4conf := conf.Conf4
v4conf.InterfaceName = s.conf.InterfaceName v4conf.InterfaceName = s.conf.InterfaceName
v4conf.notify = s.onNotify v4conf.notify = s.onNotify
v4conf.Enabled = s.conf.Enabled && len(v4conf.RangeStart) != 0 v4conf.Enabled = s.conf.Enabled && v4conf.RangeStart.IsValid()
s.srv4, err = v4Create(&v4conf) s.srv4, err = v4Create(&v4conf)
if err != nil { if err != nil {

View File

@ -4,6 +4,7 @@ package dhcpd
import ( import (
"net" "net"
"net/netip"
"os" "os"
"testing" "testing"
"time" "time"
@ -33,10 +34,10 @@ func TestDB(t *testing.T) {
s.srv4, err = v4Create(&V4ServerConf{ s.srv4, err = v4Create(&V4ServerConf{
Enabled: true, Enabled: true,
RangeStart: net.IP{192, 168, 10, 100}, RangeStart: netip.MustParseAddr("192.168.10.100"),
RangeEnd: net.IP{192, 168, 10, 200}, RangeEnd: netip.MustParseAddr("192.168.10.200"),
GatewayIP: net.IP{192, 168, 10, 1}, GatewayIP: netip.MustParseAddr("192.168.10.1"),
SubnetMask: net.IP{255, 255, 255, 0}, SubnetMask: netip.MustParseAddr("255.255.255.0"),
notify: testNotify, notify: testNotify,
}) })
require.NoError(t, err) require.NoError(t, err)
@ -113,35 +114,35 @@ func TestNormalizeLeases(t *testing.T) {
func TestV4Server_badRange(t *testing.T) { func TestV4Server_badRange(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
gatewayIP netip.Addr
subnetMask netip.Addr
wantErrMsg string wantErrMsg string
gatewayIP net.IP
subnetMask net.IP
}{{ }{{
name: "gateway_in_range", name: "gateway_in_range",
gatewayIP: netip.MustParseAddr("192.168.10.120"),
subnetMask: netip.MustParseAddr("255.255.255.0"),
wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " + wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " +
"192.168.10.20-192.168.10.200", "192.168.10.20-192.168.10.200",
gatewayIP: net.IP{192, 168, 10, 120},
subnetMask: net.IP{255, 255, 255, 0},
}, { }, {
name: "outside_range_start", name: "outside_range_start",
gatewayIP: netip.MustParseAddr("192.168.10.1"),
subnetMask: netip.MustParseAddr("255.255.255.240"),
wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " + wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " +
"192.168.10.1/28", "192.168.10.1/28",
gatewayIP: net.IP{192, 168, 10, 1},
subnetMask: net.IP{255, 255, 255, 240},
}, { }, {
name: "outside_range_end", name: "outside_range_end",
gatewayIP: netip.MustParseAddr("192.168.10.1"),
subnetMask: netip.MustParseAddr("255.255.255.224"),
wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " + wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " +
"192.168.10.1/27", "192.168.10.1/27",
gatewayIP: net.IP{192, 168, 10, 1},
subnetMask: net.IP{255, 255, 255, 224},
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
conf := V4ServerConf{ conf := V4ServerConf{
Enabled: true, Enabled: true,
RangeStart: net.IP{192, 168, 10, 20}, RangeStart: netip.MustParseAddr("192.168.10.20"),
RangeEnd: net.IP{192, 168, 10, 200}, RangeEnd: netip.MustParseAddr("192.168.10.200"),
GatewayIP: tc.gatewayIP, GatewayIP: tc.gatewayIP,
SubnetMask: tc.subnetMask, SubnetMask: tc.subnetMask,
notify: testNotify, notify: testNotify,

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/netip"
"os" "os"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
@ -17,10 +18,10 @@ import (
) )
type v4ServerConfJSON struct { type v4ServerConfJSON struct {
GatewayIP net.IP `json:"gateway_ip"` GatewayIP netip.Addr `json:"gateway_ip"`
SubnetMask net.IP `json:"subnet_mask"` SubnetMask netip.Addr `json:"subnet_mask"`
RangeStart net.IP `json:"range_start"` RangeStart netip.Addr `json:"range_start"`
RangeEnd net.IP `json:"range_end"` RangeEnd netip.Addr `json:"range_end"`
LeaseDuration uint32 `json:"lease_duration"` LeaseDuration uint32 `json:"lease_duration"`
} }
@ -39,7 +40,7 @@ func (j *v4ServerConfJSON) toServerConf() *V4ServerConf {
} }
type v6ServerConfJSON struct { type v6ServerConfJSON struct {
RangeStart net.IP `json:"range_start"` RangeStart netip.Addr `json:"range_start"`
LeaseDuration uint32 `json:"lease_duration"` LeaseDuration uint32 `json:"lease_duration"`
} }
@ -49,7 +50,7 @@ func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
} }
return V6ServerConf{ return V6ServerConf{
RangeStart: j.RangeStart, RangeStart: j.RangeStart.AsSlice(),
LeaseDuration: j.LeaseDuration, LeaseDuration: j.LeaseDuration,
} }
} }
@ -144,7 +145,7 @@ func (s *server) handleDHCPSetConfigV4(
v4Conf := conf.V4.toServerConf() v4Conf := conf.V4.toServerConf()
v4Conf.Enabled = conf.Enabled == aghalg.NBTrue v4Conf.Enabled = conf.Enabled == aghalg.NBTrue
if len(v4Conf.RangeStart) == 0 { if !v4Conf.RangeStart.IsValid() {
v4Conf.Enabled = false v4Conf.Enabled = false
} }
@ -278,9 +279,9 @@ type netInterfaceJSON struct {
Name string `json:"name"` Name string `json:"name"`
HardwareAddr string `json:"hardware_address"` HardwareAddr string `json:"hardware_address"`
Flags string `json:"flags"` Flags string `json:"flags"`
GatewayIP net.IP `json:"gateway_ip"` GatewayIP netip.Addr `json:"gateway_ip"`
Addrs4 []net.IP `json:"ipv4_addresses"` Addrs4 []netip.Addr `json:"ipv4_addresses"`
Addrs6 []net.IP `json:"ipv6_addresses"` Addrs6 []netip.Addr `json:"ipv6_addresses"`
} }
func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
@ -341,13 +342,18 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
return return
} }
// ignore link-local // ignore link-local
//
// TODO(e.burkov): Try to listen DHCP on LLA as well.
if ipnet.IP.IsLinkLocalUnicast() { if ipnet.IP.IsLinkLocalUnicast() {
continue continue
} }
if ipnet.IP.To4() != nil {
jsonIface.Addrs4 = append(jsonIface.Addrs4, ipnet.IP) if ip4 := ipnet.IP.To4(); ip4 != nil {
addr := netip.AddrFrom4(*(*[4]byte)(ip4))
jsonIface.Addrs4 = append(jsonIface.Addrs4, addr)
} else { } else {
jsonIface.Addrs6 = append(jsonIface.Addrs6, ipnet.IP) addr := netip.AddrFrom16(*(*[16]byte)(ipnet.IP))
jsonIface.Addrs6 = append(jsonIface.Addrs6, addr)
} }
} }
if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 { if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 {

View File

@ -27,6 +27,8 @@ const maxRangeLen = math.MaxUint32
// newIPRange creates a new IP address range. start must be less than end. The // newIPRange creates a new IP address range. start must be less than end. The
// resulting range must not be greater than maxRangeLen. // resulting range must not be greater than maxRangeLen.
//
// TODO(e.burkov): Use netip.Addr.
func newIPRange(start, end net.IP) (r *ipRange, err error) { func newIPRange(start, end net.IP) (r *ipRange, err error) {
defer func() { err = errors.Annotate(err, "invalid ip range: %w") }() defer func() { err = errors.Annotate(err, "invalid ip range: %w") }()

View File

@ -372,12 +372,9 @@ func (s *v4Server) prepareOptions() {
dhcpv4.OptGeneric(dhcpv4.OptionTCPKeepaliveGarbage, []byte{0x01}), dhcpv4.OptGeneric(dhcpv4.OptionTCPKeepaliveGarbage, []byte{0x01}),
// Values From Configuration // Values From Configuration
dhcpv4.OptRouter(s.conf.GatewayIP.AsSlice()),
// Set the Router Option to working subnet's IP since it's initialized dhcpv4.OptSubnetMask(s.conf.SubnetMask.AsSlice()),
// with the address of the gateway.
dhcpv4.OptRouter(s.conf.subnet.IP),
dhcpv4.OptSubnetMask(s.conf.subnet.Mask),
) )
// Set values for explicitly configured options. // Set values for explicitly configured options.

View File

@ -251,8 +251,6 @@ func TestPrepareOptions(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
s := &v4Server{ s := &v4Server{
conf: &V4ServerConf{ conf: &V4ServerConf{
// Just to avoid nil pointer dereference.
subnet: &net.IPNet{},
Options: tc.opts, Options: tc.opts,
}, },
} }

View File

@ -6,6 +6,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net"
"net/netip"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -295,7 +296,8 @@ func (s *v4Server) addLease(l *Lease) (err error) {
if l.IsStatic() { if l.IsStatic() {
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is // TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
// disabled. // disabled.
if sn := s.conf.subnet; !sn.Contains(l.IP) { addr := netip.AddrFrom4(*(*[4]byte)(l.IP.To4()))
if sn := s.conf.subnet; !sn.Contains(addr) {
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP) return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
} }
} else if !inOffset { } else if !inOffset {
@ -353,7 +355,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
ip := l.IP.To4() ip := l.IP.To4()
if ip == nil { if ip == nil {
return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP) return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP)
} else if gwIP := s.conf.GatewayIP; gwIP.Equal(ip) { } else if gwIP := s.conf.GatewayIP; gwIP == netip.AddrFrom4(*(*[4]byte)(ip)) {
return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP) return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP)
} }
@ -701,7 +703,8 @@ func (s *v4Server) handleSelecting(
// Client inserts the address of the selected server in server identifier, // Client inserts the address of the selected server in server identifier,
// ciaddr MUST be zero. // ciaddr MUST be zero.
mac := req.ClientHWAddr mac := req.ClientHWAddr
if !sid.Equal(s.conf.dnsIPAddrs[0]) {
if !sid.Equal(s.conf.dnsIPAddrs[0].AsSlice()) {
log.Debug("dhcpv4: bad server identifier in req msg for %s: %s", mac, sid) log.Debug("dhcpv4: bad server identifier in req msg for %s: %s", mac, sid)
return nil, false return nil, false
@ -733,7 +736,8 @@ func (s *v4Server) handleSelecting(
func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, needsReply bool) { func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, needsReply bool) {
mac := req.ClientHWAddr mac := req.ClientHWAddr
if ip4 := reqIP.To4(); ip4 == nil { ip4 := reqIP.To4()
if ip4 == nil {
log.Debug("dhcpv4: bad requested address in req msg for %s: %s", mac, reqIP) log.Debug("dhcpv4: bad requested address in req msg for %s: %s", mac, reqIP)
return nil, false return nil, false
@ -747,7 +751,7 @@ func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease,
return nil, false return nil, false
} }
if !s.conf.subnet.Contains(reqIP) { if !s.conf.subnet.Contains(netip.AddrFrom4(*(*[4]byte)(ip4))) {
// If the DHCP server detects that the client is on the wrong net then // If the DHCP server detects that the client is on the wrong net then
// the server SHOULD send a DHCPNAK message to the client. // the server SHOULD send a DHCPNAK message to the client.
log.Debug("dhcpv4: wrong subnet in init-reboot req msg for %s: %s", mac, reqIP) log.Debug("dhcpv4: wrong subnet in init-reboot req msg for %s: %s", mac, reqIP)
@ -972,7 +976,7 @@ func (s *v4Server) handle(req, resp *dhcpv4.DHCPv4) int {
// Include server's identifier option since any reply should contain it. // Include server's identifier option since any reply should contain it.
// //
// See https://datatracker.ietf.org/doc/html/rfc2131#page-29. // See https://datatracker.ietf.org/doc/html/rfc2131#page-29.
resp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0])) resp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0].AsSlice()))
// TODO(a.garipov): Refactor this into handlers. // TODO(a.garipov): Refactor this into handlers.
var l *Lease var l *Lease
@ -1188,7 +1192,14 @@ func (s *v4Server) Start() (err error) {
s.implicitOpts.Update(dhcpv4.OptDNS(dnsIPAddrs...)) s.implicitOpts.Update(dhcpv4.OptDNS(dnsIPAddrs...))
} }
s.conf.dnsIPAddrs = dnsIPAddrs for _, ip := range dnsIPAddrs {
ip = ip.To4()
if ip == nil {
continue
}
s.conf.dnsIPAddrs = append(s.conf.dnsIPAddrs, netip.AddrFrom4(*(*[4]byte)(ip)))
}
var c net.PacketConn var c net.PacketConn
if c, err = s.newDHCPConn(iface); err != nil { if c, err = s.newDHCPConn(iface); err != nil {

View File

@ -5,6 +5,7 @@ package dhcpd
import ( import (
"fmt" "fmt"
"net" "net"
"net/netip"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -22,11 +23,11 @@ import (
) )
var ( var (
DefaultRangeStart = net.IP{192, 168, 10, 100} DefaultRangeStart = netip.MustParseAddr("192.168.10.100")
DefaultRangeEnd = net.IP{192, 168, 10, 200} DefaultRangeEnd = netip.MustParseAddr("192.168.10.200")
DefaultGatewayIP = net.IP{192, 168, 10, 1} DefaultGatewayIP = netip.MustParseAddr("192.168.10.1")
DefaultSelfIP = net.IP{192, 168, 10, 2} DefaultSelfIP = netip.MustParseAddr("192.168.10.2")
DefaultSubnetMask = net.IP{255, 255, 255, 0} DefaultSubnetMask = netip.MustParseAddr("255.255.255.0")
) )
// defaultV4ServerConf returns the default configuration for *v4Server to use in // defaultV4ServerConf returns the default configuration for *v4Server to use in
@ -39,7 +40,7 @@ func defaultV4ServerConf() (conf *V4ServerConf) {
GatewayIP: DefaultGatewayIP, GatewayIP: DefaultGatewayIP,
SubnetMask: DefaultSubnetMask, SubnetMask: DefaultSubnetMask,
notify: testNotify, notify: testNotify,
dnsIPAddrs: []net.IP{DefaultSelfIP}, dnsIPAddrs: []netip.Addr{DefaultSelfIP},
} }
} }
@ -82,7 +83,7 @@ func TestV4Server_leasing(t *testing.T) {
Expiry: time.Unix(leaseExpireStatic, 0), Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName, Hostname: staticName,
HWAddr: anotherMAC, HWAddr: anotherMAC,
IP: anotherIP, IP: anotherIP.AsSlice(),
}) })
assert.ErrorIs(t, err, ErrDupHostname) assert.ErrorIs(t, err, ErrDupHostname)
}) })
@ -96,7 +97,7 @@ func TestV4Server_leasing(t *testing.T) {
Expiry: time.Unix(leaseExpireStatic, 0), Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: anotherName, Hostname: anotherName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: anotherIP, IP: anotherIP.AsSlice(),
}) })
testutil.AssertErrorMsg(t, wantErrMsg, err) testutil.AssertErrorMsg(t, wantErrMsg, err)
}) })
@ -135,7 +136,7 @@ func TestV4Server_leasing(t *testing.T) {
dhcpv4.WithOption(dhcpv4.OptHostName(name)), dhcpv4.WithOption(dhcpv4.OptHostName(name)),
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(ip)), dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(ip)),
dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte{1, 2, 3, 4, 5, 6, 8})), dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte{1, 2, 3, 4, 5, 6, 8})),
dhcpv4.WithGatewayIP(DefaultGatewayIP), dhcpv4.WithGatewayIP(DefaultGatewayIP.AsSlice()),
) )
require.NoError(t, err) require.NoError(t, err)
@ -150,7 +151,7 @@ func TestV4Server_leasing(t *testing.T) {
} }
t.Run("same_name", func(t *testing.T) { t.Run("same_name", func(t *testing.T) {
resp := discoverAnOffer(t, staticName, anotherIP, anotherMAC) resp := discoverAnOffer(t, staticName, anotherIP.AsSlice(), anotherMAC)
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption( req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
dhcpv4.OptHostName(staticName), dhcpv4.OptHostName(staticName),
@ -164,7 +165,7 @@ func TestV4Server_leasing(t *testing.T) {
}) })
t.Run("same_mac", func(t *testing.T) { t.Run("same_mac", func(t *testing.T) {
resp := discoverAnOffer(t, anotherName, anotherIP, staticMAC) resp := discoverAnOffer(t, anotherName, anotherIP.AsSlice(), staticMAC)
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption( req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
dhcpv4.OptHostName(anotherName), dhcpv4.OptHostName(anotherName),
@ -219,7 +220,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
lease: &Lease{ lease: &Lease{
Hostname: "probably-router.local", Hostname: "probably-router.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
IP: DefaultGatewayIP, IP: DefaultGatewayIP.AsSlice(),
}, },
name: "with_gateway_ip", name: "with_gateway_ip",
wantErrMsg: "dhcpv4: adding static lease: " + wantErrMsg: "dhcpv4: adding static lease: " +
@ -326,7 +327,7 @@ func TestV4_AddReplace(t *testing.T) {
} }
func TestV4Server_handle_optionsPriority(t *testing.T) { func TestV4Server_handle_optionsPriority(t *testing.T) {
defaultIP := net.IP{192, 168, 1, 1} defaultIP := netip.MustParseAddr("192.168.1.1")
knownIP := net.IP{1, 2, 3, 4} knownIP := net.IP{1, 2, 3, 4}
// prepareSrv creates a *v4Server and sets the opt6IPs in the initial // prepareSrv creates a *v4Server and sets the opt6IPs in the initial
@ -343,14 +344,14 @@ func TestV4Server_handle_optionsPriority(t *testing.T) {
} }
conf.Options = []string{b.String()} conf.Options = []string{b.String()}
} else { } else {
defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP)) }() defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP.AsSlice())) }()
} }
var err error var err error
s, err = v4Create(conf) s, err = v4Create(conf)
require.NoError(t, err) require.NoError(t, err)
s.conf.dnsIPAddrs = []net.IP{defaultIP} s.conf.dnsIPAddrs = []netip.Addr{defaultIP}
return s return s
} }
@ -386,7 +387,7 @@ func TestV4Server_handle_optionsPriority(t *testing.T) {
t.Run("default", func(t *testing.T) { t.Run("default", func(t *testing.T) {
s := prepareSrv(t, nil) s := prepareSrv(t, nil)
checkResp(t, s, []net.IP{defaultIP}) checkResp(t, s, []net.IP{defaultIP.AsSlice()})
}) })
t.Run("explicitly_configured", func(t *testing.T) { t.Run("explicitly_configured", func(t *testing.T) {
@ -506,8 +507,9 @@ func TestV4StaticLease_Get(t *testing.T) {
s, ok := sIface.(*v4Server) s, ok := sIface.(*v4Server)
require.True(t, ok) require.True(t, ok)
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}} dnsAddr := netip.MustParseAddr("192.168.10.1")
s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...)) s.conf.dnsIPAddrs = []netip.Addr{dnsAddr}
s.implicitOpts.Update(dhcpv4.OptDNS(dnsAddr.AsSlice()))
l := &Lease{ l := &Lease{
Hostname: "static-1.local", Hostname: "static-1.local",
@ -539,9 +541,12 @@ func TestV4StaticLease_Get(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
assert.Equal(t, mac, resp.ClientHWAddr) assert.Equal(t, mac, resp.ClientHWAddr)
assert.True(t, l.IP.Equal(resp.YourIPAddr)) assert.True(t, l.IP.Equal(resp.YourIPAddr))
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier())) assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
assert.Equal(t, s.conf.subnet.Mask, resp.SubnetMask()) assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
ones, _ := resp.SubnetMask().Size()
assert.Equal(t, s.conf.subnet.Bits(), ones)
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
}) })
@ -561,16 +566,19 @@ func TestV4StaticLease_Get(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
assert.Equal(t, mac, resp.ClientHWAddr) assert.Equal(t, mac, resp.ClientHWAddr)
assert.True(t, l.IP.Equal(resp.YourIPAddr)) assert.True(t, l.IP.Equal(resp.YourIPAddr))
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier())) assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
assert.Equal(t, s.conf.subnet.Mask, resp.SubnetMask()) assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
ones, _ := resp.SubnetMask().Size()
assert.Equal(t, s.conf.subnet.Bits(), ones)
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
}) })
dnsAddrs := resp.DNS() dnsAddrs := resp.DNS()
require.Len(t, dnsAddrs, 1) require.Len(t, dnsAddrs, 1)
assert.True(t, s.conf.GatewayIP.Equal(dnsAddrs[0])) assert.True(t, dnsAddrs[0].Equal(s.conf.GatewayIP.AsSlice()))
t.Run("check_lease", func(t *testing.T) { t.Run("check_lease", func(t *testing.T) {
ls := s.GetLeases(LeasesStatic) ls := s.GetLeases(LeasesStatic)
@ -591,8 +599,8 @@ func TestV4DynamicLease_Get(t *testing.T) {
s, err := v4Create(conf) s, err := v4Create(conf)
require.NoError(t, err) require.NoError(t, err)
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}} s.conf.dnsIPAddrs = []netip.Addr{netip.MustParseAddr("192.168.10.1")}
s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...)) s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs[0].AsSlice()))
var req, resp *dhcpv4.DHCPv4 var req, resp *dhcpv4.DHCPv4
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
@ -617,15 +625,16 @@ func TestV4DynamicLease_Get(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
assert.Equal(t, mac, resp.ClientHWAddr) assert.Equal(t, mac, resp.ClientHWAddr)
assert.Equal(t, s.conf.RangeStart, resp.YourIPAddr) assert.True(t, resp.YourIPAddr.Equal(s.conf.RangeStart.AsSlice()))
assert.Equal(t, s.conf.GatewayIP, resp.ServerIdentifier()) assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
router := resp.Router() router := resp.Router()
require.Len(t, router, 1) require.Len(t, router, 1)
assert.Equal(t, s.conf.GatewayIP, router[0]) assert.True(t, router[0].Equal(s.conf.GatewayIP.AsSlice()))
assert.Equal(t, s.conf.subnet.Mask, resp.SubnetMask()) ones, _ := resp.SubnetMask().Size()
assert.Equal(t, s.conf.subnet.Bits(), ones)
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
assert.Equal(t, []byte("012"), resp.Options.Get(dhcpv4.OptionFQDN)) assert.Equal(t, []byte("012"), resp.Options.Get(dhcpv4.OptionFQDN))
@ -649,15 +658,17 @@ func TestV4DynamicLease_Get(t *testing.T) {
t.Run("ack", func(t *testing.T) { t.Run("ack", func(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
assert.Equal(t, mac, resp.ClientHWAddr) assert.Equal(t, mac, resp.ClientHWAddr)
assert.True(t, s.conf.RangeStart.Equal(resp.YourIPAddr)) assert.True(t, resp.YourIPAddr.Equal(s.conf.RangeStart.AsSlice()))
router := resp.Router() router := resp.Router()
require.Len(t, router, 1) require.Len(t, router, 1)
assert.Equal(t, s.conf.GatewayIP, router[0]) assert.True(t, router[0].Equal(s.conf.GatewayIP.AsSlice()))
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier())) assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
assert.Equal(t, s.conf.subnet.Mask, resp.SubnetMask())
ones, _ := resp.SubnetMask().Size()
assert.Equal(t, s.conf.subnet.Bits(), ones)
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
}) })

View File

@ -19,6 +19,7 @@ import (
// accessCtx controls IP and client blocking that takes place before all other // accessCtx controls IP and client blocking that takes place before all other
// processing. An accessCtx is safe for concurrent use. // processing. An accessCtx is safe for concurrent use.
type accessCtx struct { type accessCtx struct {
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
allowedIPs *netutil.IPMap allowedIPs *netutil.IPMap
blockedIPs *netutil.IPMap blockedIPs *netutil.IPMap

View File

@ -81,6 +81,7 @@ type Server struct {
tableHostToIP hostToIPTable tableHostToIP hostToIPTable
tableHostToIPLock sync.Mutex tableHostToIPLock sync.Mutex
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
tableIPToHost *netutil.IPMap tableIPToHost *netutil.IPMap
tableIPToHostLock sync.Mutex tableIPToHostLock sync.Mutex

View File

@ -119,6 +119,8 @@ type clientsContainer struct {
idIndex map[string]*Client // ID -> client idIndex map[string]*Client // ID -> client
// ipToRC is the IP address to *RuntimeClient map. // ipToRC is the IP address to *RuntimeClient map.
//
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
ipToRC *netutil.IPMap ipToRC *netutil.IPMap
lock sync.Mutex lock sync.Mutex

View File

@ -2,6 +2,7 @@ package home
import ( import (
"net" "net"
"net/netip"
"os" "os"
"runtime" "runtime"
"testing" "testing"
@ -287,10 +288,10 @@ func TestClientsAddExisting(t *testing.T) {
DBFilePath: "leases.db", DBFilePath: "leases.db",
Conf4: dhcpd.V4ServerConf{ Conf4: dhcpd.V4ServerConf{
Enabled: true, Enabled: true,
GatewayIP: net.IP{1, 2, 3, 1}, GatewayIP: netip.MustParseAddr("1.2.3.1"),
SubnetMask: net.IP{255, 255, 255, 0}, SubnetMask: netip.MustParseAddr("255.255.255.0"),
RangeStart: net.IP{1, 2, 3, 2}, RangeStart: netip.MustParseAddr("1.2.3.2"),
RangeEnd: net.IP{1, 2, 3, 10}, RangeEnd: netip.MustParseAddr("1.2.3.10"),
}, },
} }

View File

@ -3,7 +3,7 @@ package home
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -85,19 +85,28 @@ type configuration struct {
// It's reset after config is parsed // It's reset after config is parsed
fileData []byte fileData []byte
BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to // BindHost is the address for the web interface server to listen on.
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server BindHost netip.Addr `yaml:"bind_host"`
BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client // BindPort is the port for the web interface server to listen on.
Users []webUser `yaml:"users"` // Users that can access HTTP server BindPort int `yaml:"bind_port"`
// BetaBindPort is the port for the new client's web interface server to
// listen on.
BetaBindPort int `yaml:"beta_bind_port"`
// Users are the clients capable for accessing the web interface.
Users []webUser `yaml:"users"`
// AuthAttempts is the maximum number of failed login attempts a user // AuthAttempts is the maximum number of failed login attempts a user
// can do before being blocked. // can do before being blocked.
AuthAttempts uint `yaml:"auth_attempts"` AuthAttempts uint `yaml:"auth_attempts"`
// AuthBlockMin is the duration, in minutes, of the block of new login // AuthBlockMin is the duration, in minutes, of the block of new login
// attempts after AuthAttempts unsuccessful login attempts. // attempts after AuthAttempts unsuccessful login attempts.
AuthBlockMin uint `yaml:"block_auth_min"` AuthBlockMin uint `yaml:"block_auth_min"`
ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client // ProxyURL is the address of proxy server for the internal HTTP client.
Language string `yaml:"language"` // two-letter ISO 639-1 language code ProxyURL string `yaml:"http_proxy"`
DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060 // Language is a two-letter ISO 639-1 language code.
Language string `yaml:"language"`
// DebugPProf defines if the profiling HTTP handler will listen on :6060.
DebugPProf bool `yaml:"debug_pprof"`
// TTL for a web session (in hours) // TTL for a web session (in hours)
// An active session is automatically refreshed once a day. // An active session is automatically refreshed once a day.
@ -112,7 +121,7 @@ type configuration struct {
// //
// TODO(e.burkov): Move all the filtering configuration fields into the // TODO(e.burkov): Move all the filtering configuration fields into the
// only configuration subsection covering the changes with a single // only configuration subsection covering the changes with a single
// migration. // migration. Also keep the blocked services in mind.
Filters []filtering.FilterYAML `yaml:"filters"` Filters []filtering.FilterYAML `yaml:"filters"`
WhitelistFilters []filtering.FilterYAML `yaml:"whitelist_filters"` WhitelistFilters []filtering.FilterYAML `yaml:"whitelist_filters"`
UserRules []string `yaml:"user_rules"` UserRules []string `yaml:"user_rules"`
@ -135,18 +144,26 @@ type configuration struct {
// field ordering is important -- yaml fields will mirror ordering from here // field ordering is important -- yaml fields will mirror ordering from here
type dnsConfig struct { type dnsConfig struct {
BindHosts []net.IP `yaml:"bind_hosts"` BindHosts []netip.Addr `yaml:"bind_hosts"`
Port int `yaml:"port"` Port int `yaml:"port"`
// time interval for statistics (in days) // StatsInterval is the time interval for flushing statistics to the disk in
// days.
StatsInterval uint32 `yaml:"statistics_interval"` StatsInterval uint32 `yaml:"statistics_interval"`
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled // QueryLogEnabled defines if the query log is enabled.
QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file QueryLogEnabled bool `yaml:"querylog_enabled"`
// QueryLogFileEnabled defines, if the query log is written to the file.
QueryLogFileEnabled bool `yaml:"querylog_file_enabled"`
// QueryLogInterval is the interval for query log's files rotation. // QueryLogInterval is the interval for query log's files rotation.
QueryLogInterval timeutil.Duration `yaml:"querylog_interval"` QueryLogInterval timeutil.Duration `yaml:"querylog_interval"`
QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk // QueryLogMemSize is the number of entries kept in memory before they are
AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats // flushed to disk.
QueryLogMemSize uint32 `yaml:"querylog_size_memory"`
// AnonymizeClientIP defines if clients' IP addresses should be anonymized
// in query log and statistics.
AnonymizeClientIP bool `yaml:"anonymize_client_ip"`
dnsforward.FilteringConfig `yaml:",inline"` dnsforward.FilteringConfig `yaml:",inline"`
@ -211,12 +228,12 @@ type tlsConfigSettings struct {
var config = &configuration{ var config = &configuration{
BindPort: 3000, BindPort: 3000,
BetaBindPort: 0, BetaBindPort: 0,
BindHost: net.IP{0, 0, 0, 0}, BindHost: netip.IPv4Unspecified(),
AuthAttempts: 5, AuthAttempts: 5,
AuthBlockMin: 15, AuthBlockMin: 15,
WebSessionTTLHours: 30 * 24, WebSessionTTLHours: 30 * 24,
DNS: dnsConfig{ DNS: dnsConfig{
BindHosts: []net.IP{{0, 0, 0, 0}}, BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS, Port: defaultPortDNS,
StatsInterval: 1, StatsInterval: 1,
QueryLogEnabled: true, QueryLogEnabled: true,

View File

@ -2,8 +2,8 @@ package home
import ( import (
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"runtime" "runtime"
"strings" "strings"
@ -20,11 +20,11 @@ import (
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS // appendDNSAddrs is a convenient helper for appending a formatted form of DNS
// addresses to a slice of strings. // addresses to a slice of strings.
func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) { func appendDNSAddrs(dst []string, addrs ...netip.Addr) (res []string) {
for _, addr := range addrs { for _, addr := range addrs {
var hostport string var hostport string
if config.DNS.Port != defaultPortDNS { if config.DNS.Port != defaultPortDNS {
hostport = netutil.JoinHostPort(addr.String(), config.DNS.Port) hostport = netip.AddrPortFrom(addr, uint16(config.DNS.Port)).String()
} else { } else {
hostport = addr.String() hostport = addr.String()
} }
@ -38,7 +38,7 @@ func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) {
// appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to // appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to
// dst. It also adds the IP addresses of all network interfaces if src contains // dst. It also adds the IP addresses of all network interfaces if src contains
// an unspecified IP address. // an unspecified IP address.
func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (res []string, err error) { func appendDNSAddrsWithIfaces(dst []string, src []netip.Addr) (res []string, err error) {
ifacesAdded := false ifacesAdded := false
for _, h := range src { for _, h := range src {
if !h.IsUnspecified() { if !h.IsUnspecified() {
@ -71,7 +71,9 @@ func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (res []string, err err
// on, including the addresses on all interfaces in cases of unspecified IPs. // on, including the addresses on all interfaces in cases of unspecified IPs.
func collectDNSAddresses() (addrs []string, err error) { func collectDNSAddresses() (addrs []string, err error) {
if hosts := config.DNS.BindHosts; len(hosts) == 0 { if hosts := config.DNS.BindHosts; len(hosts) == 0 {
addrs = appendDNSAddrs(addrs, net.IP{127, 0, 0, 1}) addr := aghnet.IPv4Localhost()
addrs = appendDNSAddrs(addrs, addr)
} else { } else {
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts) addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
if err != nil { if err != nil {

View File

@ -5,8 +5,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"net/netip"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -64,7 +64,7 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request
} }
type checkConfReqEnt struct { type checkConfReqEnt struct {
IP net.IP `json:"ip"` IP netip.Addr `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
Autofix bool `json:"autofix"` Autofix bool `json:"autofix"`
} }
@ -117,7 +117,7 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err
// unbound after install. // unbound after install.
} }
return aghnet.CheckPort("tcp", req.Web.IP, portInt) return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(portInt)))
} }
// validateDNS returns error if the DNS part of the initial configuration can't // validateDNS returns error if the DNS part of the initial configuration can't
@ -142,13 +142,13 @@ func (req *checkConfReq) validateDNS(
return false, err return false, err
} }
err = aghnet.CheckPort("tcp", req.DNS.IP, port) err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
if err != nil { if err != nil {
return false, err return false, err
} }
} }
err = aghnet.CheckPort("udp", req.DNS.IP, port) err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
if !aghnet.IsAddrInUse(err) { if !aghnet.IsAddrInUse(err) {
return false, err return false, err
} }
@ -160,7 +160,7 @@ func (req *checkConfReq) validateDNS(
log.Error("disabling DNSStubListener: %s", err) log.Error("disabling DNSStubListener: %s", err)
} }
err = aghnet.CheckPort("udp", req.DNS.IP, port) err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
canAutofix = false canAutofix = false
} }
@ -196,7 +196,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
// handleStaticIP - handles static IP request // handleStaticIP - handles static IP request
// It either checks if we have a static IP // It either checks if we have a static IP
// Or if set=true, it tries to set it // Or if set=true, it tries to set it
func handleStaticIP(ip net.IP, set bool) staticIPJSON { func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
resp := staticIPJSON{} resp := staticIPJSON{}
interfaceName := aghnet.InterfaceByIP(ip) interfaceName := aghnet.InterfaceByIP(ip)
@ -304,7 +304,7 @@ func disableDNSStubListener() error {
} }
type applyConfigReqEnt struct { type applyConfigReqEnt struct {
IP net.IP `json:"ip"` IP netip.Addr `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
} }
@ -397,14 +397,14 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
return return
} }
err = aghnet.CheckPort("udp", req.DNS.IP, req.DNS.Port) err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(req.DNS.Port)))
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
return return
} }
err = aghnet.CheckPort("tcp", req.DNS.IP, req.DNS.Port) err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.DNS.IP, uint16(req.DNS.Port)))
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@ -417,7 +417,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
Context.firstRun = false Context.firstRun = false
config.BindHost = req.Web.IP config.BindHost = req.Web.IP
config.BindPort = req.Web.Port config.BindPort = req.Web.Port
config.DNS.BindHosts = []net.IP{req.DNS.IP} config.DNS.BindHosts = []netip.Addr{req.DNS.IP}
config.DNS.Port = req.DNS.Port config.DNS.Port = req.DNS.Port
// TODO(e.burkov): StartMods() should be put in a separate goroutine at the // TODO(e.burkov): StartMods() should be put in a separate goroutine at the
@ -490,9 +490,9 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
return nil, false, errors.Error("ports cannot be 0") return nil, false, errors.Error("ports cannot be 0")
} }
restartHTTP = !config.BindHost.Equal(req.Web.IP) || config.BindPort != req.Web.Port restartHTTP = config.BindHost != req.Web.IP || config.BindPort != req.Web.Port
if restartHTTP { if restartHTTP {
err = aghnet.CheckPort("tcp", req.Web.IP, req.Web.Port) err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port)))
if err != nil { if err != nil {
return nil, false, fmt.Errorf( return nil, false, fmt.Errorf(
"checking address %s:%d: %w", "checking address %s:%d: %w",
@ -518,7 +518,7 @@ func (web *Web) registerInstallHandlers() {
// TODO(e.burkov): This should removed with the API v1 when the appropriate // TODO(e.burkov): This should removed with the API v1 when the appropriate
// functionality will appear in default checkConfigReqEnt. // functionality will appear in default checkConfigReqEnt.
type checkConfigReqEntBeta struct { type checkConfigReqEntBeta struct {
IP []net.IP `json:"ip"` IP []netip.Addr `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
Autofix bool `json:"autofix"` Autofix bool `json:"autofix"`
} }
@ -590,7 +590,7 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
// TODO(e.burkov): This should removed with the API v1 when the appropriate // TODO(e.burkov): This should removed with the API v1 when the appropriate
// functionality will appear in default applyConfigReqEnt. // functionality will appear in default applyConfigReqEnt.
type applyConfigReqEntBeta struct { type applyConfigReqEntBeta struct {
IP []net.IP `json:"ip"` IP []netip.Addr `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
} }

View File

@ -3,6 +3,7 @@ package home
import ( import (
"fmt" "fmt"
"net" "net"
"net/netip"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -164,33 +165,27 @@ func onDNSRequest(pctx *proxy.DNSContext) {
} }
} }
func ipsToTCPAddrs(ips []net.IP, port int) (tcpAddrs []*net.TCPAddr) { func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) {
if ips == nil { if ips == nil {
return nil return nil
} }
tcpAddrs = make([]*net.TCPAddr, len(ips)) tcpAddrs = make([]*net.TCPAddr, 0, len(ips))
for i, ip := range ips { for _, ip := range ips {
tcpAddrs[i] = &net.TCPAddr{ tcpAddrs = append(tcpAddrs, net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
IP: ip,
Port: port,
}
} }
return tcpAddrs return tcpAddrs
} }
func ipsToUDPAddrs(ips []net.IP, port int) (udpAddrs []*net.UDPAddr) { func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
if ips == nil { if ips == nil {
return nil return nil
} }
udpAddrs = make([]*net.UDPAddr, len(ips)) udpAddrs = make([]*net.UDPAddr, 0, len(ips))
for i, ip := range ips { for _, ip := range ips {
udpAddrs[i] = &net.UDPAddr{ udpAddrs = append(udpAddrs, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
IP: ip,
Port: port,
}
} }
return udpAddrs return udpAddrs
@ -200,7 +195,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
dnsConf := config.DNS dnsConf := config.DNS
hosts := dnsConf.BindHosts hosts := dnsConf.BindHosts
if len(hosts) == 0 { if len(hosts) == 0 {
hosts = []net.IP{{127, 0, 0, 1}} hosts = []netip.Addr{aghnet.IPv4Localhost()}
} }
newConf = dnsforward.ServerConfig{ newConf = dnsforward.ServerConfig{
@ -257,7 +252,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
return newConf, nil return newConf, nil
} }
func newDNSCrypt(hosts []net.IP, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) { func newDNSCrypt(hosts []netip.Addr, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) {
if tlsConf.DNSCryptConfigFile == "" { if tlsConf.DNSCryptConfigFile == "" {
return dnscc, errors.Error("no dnscrypt_config_file") return dnscc, errors.Error("no dnscrypt_config_file")
} }

View File

@ -10,6 +10,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
"net/netip"
"net/url" "net/url"
"os" "os"
"os/signal" "os/signal"
@ -356,7 +357,7 @@ func setupConfig(opts options) (err error) {
} }
// override bind host/port from the console // override bind host/port from the console
if opts.bindHost != nil { if opts.bindHost.IsValid() {
config.BindHost = opts.bindHost config.BindHost = opts.bindHost
} }
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) { if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
@ -556,7 +557,7 @@ func checkPermissions() {
} }
// We should check if AdGuard Home is able to bind to port 53 // We should check if AdGuard Home is able to bind to port 53
err := aghnet.CheckPort("tcp", net.IP{127, 0, 0, 1}, defaultPortDNS) err := aghnet.CheckPort("tcp", netip.AddrPortFrom(aghnet.IPv4Localhost(), defaultPortDNS))
if err != nil { if err != nil {
if errors.Is(err, os.ErrPermission) { if errors.Is(err, os.ErrPermission) {
log.Fatal(`Permission check failed. log.Fatal(`Permission check failed.

View File

@ -3,12 +3,11 @@ package home
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip"
"testing" "testing"
"github.com/AdguardTeam/golibs/netutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"howett.net/plist" "howett.net/plist"
@ -28,7 +27,7 @@ func setupDNSIPs(t testing.TB) {
config = &configuration{ config = &configuration{
DNS: dnsConfig{ DNS: dnsConfig{
BindHosts: []net.IP{netutil.IPv4Zero()}, BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS, Port: defaultPortDNS,
}, },
} }

View File

@ -2,7 +2,7 @@ package home
import ( import (
"fmt" "fmt"
"net" "net/netip"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -35,7 +35,7 @@ type options struct {
serviceControlAction string serviceControlAction string
// bindHost is the address on which to serve the HTTP UI. // bindHost is the address on which to serve the HTTP UI.
bindHost net.IP bindHost netip.Addr
// bindPort is the port on which to serve the HTTP UI. // bindPort is the port on which to serve the HTTP UI.
bindPort int bindPort int
@ -130,14 +130,15 @@ var cmdLineOpts = []cmdLineOpt{{
longName: "work-dir", longName: "work-dir",
shortName: "w", shortName: "w",
}, { }, {
updateWithValue: func(o options, v string) (options, error) { updateWithValue: func(o options, v string) (oo options, err error) {
o.bindHost = net.ParseIP(v) o.bindHost, err = netip.ParseAddr(v)
return o, nil
return o, err
}, },
updateNoValue: nil, updateNoValue: nil,
effect: nil, effect: nil,
serialize: func(o options) (val string, ok bool) { serialize: func(o options) (val string, ok bool) {
if o.bindHost == nil { if !o.bindHost.IsValid() {
return "", false return "", false
} }

View File

@ -2,7 +2,7 @@ package home
import ( import (
"fmt" "fmt"
"net" "net/netip"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -56,11 +56,13 @@ func TestParseWorkDir(t *testing.T) {
} }
func TestParseBindHost(t *testing.T) { func TestParseBindHost(t *testing.T) {
assert.Nil(t, testParseOK(t).bindHost, "empty is not host") wantAddr := netip.MustParseAddr("1.2.3.4")
assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
assert.Zero(t, testParseOK(t).bindHost, "empty is not host")
assert.Equal(t, wantAddr, testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
testParseParamMissing(t, "-h") testParseParamMissing(t, "-h")
assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "--host", "1.2.3.4").bindHost, "--host is host") assert.Equal(t, wantAddr, testParseOK(t, "--host", "1.2.3.4").bindHost, "--host is host")
testParseParamMissing(t, "--host") testParseParamMissing(t, "--host")
} }
@ -149,8 +151,8 @@ func TestOptsToArgs(t *testing.T) {
opts: options{workDir: "path"}, opts: options{workDir: "path"},
}, { }, {
name: "bind_host", name: "bind_host",
opts: options{bindHost: netip.MustParseAddr("1.2.3.4")},
args: []string{"-h", "1.2.3.4"}, args: []string{"-h", "1.2.3.4"},
opts: options{bindHost: net.IP{1, 2, 3, 4}},
}, { }, {
name: "bind_port", name: "bind_port",
args: []string{"-p", "666"}, args: []string{"-p", "666"},

View File

@ -4,8 +4,8 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"io/fs" "io/fs"
"net"
"net/http" "net/http"
"net/netip"
"sync" "sync"
"time" "time"
@ -37,7 +37,7 @@ type webConfig struct {
clientFS fs.FS clientFS fs.FS
clientBetaFS fs.FS clientBetaFS fs.FS
BindHost net.IP BindHost netip.Addr
BindPort int BindPort int
BetaBindPort int BetaBindPort int
PortHTTPS int PortHTTPS int
@ -135,8 +135,11 @@ func newWeb(conf *webConfig) (w *Web) {
// //
// TODO(a.garipov): Adapt for HTTP/3. // TODO(a.garipov): Adapt for HTTP/3.
func webCheckPortAvailable(port int) (ok bool) { func webCheckPortAvailable(port int) (ok bool) {
return Context.web.httpsServer.server != nil || if Context.web.httpsServer.server != nil {
aghnet.CheckPort("tcp", config.BindHost, port) == nil return true
}
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
} }
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server // TLSConfigChanged updates the TLS configuration and restarts the HTTPS server