diff --git a/CHANGELOG.md b/CHANGELOG.md index 31854726..97b119be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to ## Added +- The ability to serve DNS on link-local IPv6 addresses ([#2926]). - 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 only if the URL does not contain a ClientID. @@ -33,6 +34,7 @@ and this project adheres to cached now ([#4942]). - 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 [#4942]: https://github.com/AdguardTeam/AdGuardHome/issues/4942 [#4986]: https://github.com/AdguardTeam/AdGuardHome/issues/4986 diff --git a/internal/aghnet/dhcp_unix.go b/internal/aghnet/dhcp_unix.go index 464e5aa9..16c3c87a 100644 --- a/internal/aghnet/dhcp_unix.go +++ b/internal/aghnet/dhcp_unix.go @@ -6,6 +6,7 @@ import ( "bytes" "fmt" "net" + "net/netip" "os" "time" @@ -38,48 +39,44 @@ func checkOtherDHCP(ifaceName string) (ok4, ok6 bool, err4, err6 error) { } // 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 if addrs, err = iface.Addrs(); err != nil { - return nil, err + return netip.Prefix{}, err } for _, a := range addrs { + var ip net.IP + var maskLen int switch a := a.(type) { case *net.IPAddr: - subnet = &net.IPNet{ - IP: a.IP, - Mask: a.IP.DefaultMask(), - } + ip = a.IP + maskLen, _ = ip.DefaultMask().Size() case *net.IPNet: - subnet = a + ip = a.IP + maskLen, _ = a.Mask.Size() default: continue } - if ip4 := subnet.IP.To4(); ip4 != nil { - subnet.IP = ip4 - - return subnet, nil + if ip = ip.To4(); ip != nil { + return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ip)), maskLen), 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 // waits for a response for a period defined by defaultDiscoverTime. func checkOtherDHCPv4(iface *net.Interface) (ok bool, err error) { - var subnet *net.IPNet + var subnet netip.Prefix if subnet, err = ifaceIPv4Subnet(iface); err != nil { return false, err } // Resolve broadcast addr. - dst := netutil.IPPort{ - IP: BroadcastFromIPNet(subnet), - Port: 67, - }.String() + dst := netip.AddrPortFrom(BroadcastFromPref(subnet), 67).String() var dstAddr *net.UDPAddr if dstAddr, err = net.ResolveUDPAddr("udp4", dst); err != nil { return false, fmt.Errorf("couldn't resolve UDP address %s: %w", dst, err) diff --git a/internal/aghnet/hostscontainer.go b/internal/aghnet/hostscontainer.go index 5d750945..9b9124d9 100644 --- a/internal/aghnet/hostscontainer.go +++ b/internal/aghnet/hostscontainer.go @@ -106,9 +106,13 @@ type HostsContainer struct { done chan struct{} // updates is the channel for receiving updated hosts. + // + // TODO(e.burkov): Use map[netip.Addr]struct{} instead. updates chan *netutil.IPMap // 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 // fsys is the working file system to read hosts files from. diff --git a/internal/aghnet/net.go b/internal/aghnet/net.go index 2de9c630..bdcc6e4f 100644 --- a/internal/aghnet/net.go +++ b/internal/aghnet/net.go @@ -7,12 +7,12 @@ import ( "fmt" "io" "net" + "net/netip" "syscall" "github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" - "github.com/AdguardTeam/golibs/netutil" ) // Variables and functions to substitute in tests. @@ -31,6 +31,12 @@ var ( // the IP being static is available. 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. // If it can't give a definitive answer, it returns false and an error for which // 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 // 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) if err != nil { log.Debug("%s", err) - return nil + return netip.Addr{} } else if code != 0 { log.Debug("fetching gateway ip: unexpected exit code: %d", code) - return nil + return netip.Addr{} } fields := bytes.Fields(out) // The meaningful "ip route" command output should contain the word // "default" at first field and default gateway IP address at third field. 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 @@ -78,9 +89,9 @@ func CanBindPrivilegedPorts() (can bool, err error) { // NetInterface represents an entry of network interfaces map. type NetInterface struct { // 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 []*net.IPNet `json:"-"` + Subnets []netip.Prefix `json:"-"` Name string `json:"name"` HardwareAddr net.HardwareAddr `json:"hardware_address"` 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 // WEB only we do not return link-local addresses here. // // 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. -func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) { +func GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) { ifaces, err := net.Interfaces() 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 { - return nil, errors.Error("couldn't find any legible interface") + return nil, errors.Error("no legible interfaces") } - for _, iface := range ifaces { - var addrs []net.Addr - addrs, err = iface.Addrs() + for i := range ifaces { + var niface *NetInterface + niface, err = NetInterfaceFrom(&ifaces[i]) if err != nil { - return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err) - } - - 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. - if len(netIface.Addresses) != 0 { - netIfaces = append(netIfaces, netIface) + return nil, err + } else if len(niface.Addresses) != 0 { + // Discard interfaces with no addresses. + nifaces = append(nifaces, niface) } } - return netIfaces, nil + return nifaces, nil } // 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. // // TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb. -func InterfaceByIP(ip net.IP) (ifaceName string) { +func InterfaceByIP(ip netip.Addr) (ifaceName string) { ifaces, err := GetValidNetInterfacesForWeb() if err != nil { return "" @@ -168,7 +201,7 @@ func InterfaceByIP(ip net.IP) (ifaceName string) { for _, iface := range ifaces { for _, addr := range iface.Addresses { - if ip.Equal(addr) { + if ip == addr { return iface.Name } } @@ -177,15 +210,16 @@ func InterfaceByIP(ip net.IP) (ifaceName string) { 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. // // TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb. -func GetSubnet(ifaceName string) *net.IPNet { +func GetSubnet(ifaceName string) (p netip.Prefix) { netIfaces, err := GetValidNetInterfacesForWeb() if err != nil { log.Error("Could not get network interfaces info: %v", err) - return nil + + return p } 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 // 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 - addr := netutil.IPPort{IP: ip, Port: port}.String() + addr := ipp.String() switch network { case "tcp": c, err = net.Listen(network, addr) @@ -251,18 +285,23 @@ func CollectAllIfacesAddrs() (addrs []string, err error) { return addrs, nil } -// BroadcastFromIPNet calculates the broadcast IP address for n. -func BroadcastFromIPNet(n *net.IPNet) (dc net.IP) { - dc = netutil.CloneIP(n.IP) - - mask := n.Mask - if mask == nil { - mask = dc.DefaultMask() +// BroadcastFromPref calculates the broadcast IP address for p. +func BroadcastFromPref(p netip.Prefix) (bc netip.Addr) { + bc = p.Addr().Unmap() + if !bc.IsValid() { + return netip.Addr{} } - for i, b := range mask { - dc[i] |= ^b + maskLen, addrLen := p.Bits(), bc.BitLen() + 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 } diff --git a/internal/aghnet/net_linux.go b/internal/aghnet/net_linux.go index d0c3f7fd..fc83d56a 100644 --- a/internal/aghnet/net_linux.go +++ b/internal/aghnet/net_linux.go @@ -6,7 +6,7 @@ import ( "bufio" "fmt" "io" - "net" + "net/netip" "os" "strings" @@ -151,7 +151,7 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) { // interface through dhcpcd.conf. func ifaceSetStaticIP(ifaceName string) (err error) { ipNet := GetSubnet(ifaceName) - if ipNet.IP == nil { + if !ipNet.Addr().IsValid() { 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 // 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{} stringutil.WriteToBuilder( b, @@ -183,15 +183,15 @@ func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf stri " added by AdGuard Home.\ninterface ", ifaceName, "\nstatic ip_address=", - ipNet.String(), + subnet.String(), "\n", ) - if gwIP != nil { - stringutil.WriteToBuilder(b, "static routers=", gwIP.String(), "\n") + if gateway != (netip.Addr{}) { + 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() } diff --git a/internal/aghnet/net_test.go b/internal/aghnet/net_test.go index d4ee59ee..730f1b4e 100644 --- a/internal/aghnet/net_test.go +++ b/internal/aghnet/net_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io/fs" "net" + "net/netip" "os" "strings" "testing" @@ -93,34 +94,29 @@ func TestGatewayIP(t *testing.T) { const cmd = "ip route show dev " + ifaceName testCases := []struct { - name string 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), - 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), - want: net.IP{ - 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0xFF, 0xFF, - }, + want: netip.MustParseAddr("::ffff"), + name: "success_v6", }, { - name: "bad_output", 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")), - want: nil, + want: netip.Addr{}, + name: "err_runcmd", }, { - name: "bad_code", shell: theOnlyCmd(cmd, 1, "", nil), - want: nil, + want: netip.Addr{}, + name: "bad_code", }} for _, tc := range testCases { @@ -150,65 +146,64 @@ func TestInterfaceByIP(t *testing.T) { } func TestBroadcastFromIPNet(t *testing.T) { - known6 := net.IP{ - 1, 2, 3, 4, - 5, 6, 7, 8, - 9, 10, 11, 12, - 13, 14, 15, 16, - } + known4 := netip.MustParseAddr("192.168.0.1") + fullBroadcast4 := netip.MustParseAddr("255.255.255.255") + + known6 := netip.MustParseAddr("102:304:506:708:90a:b0c:d0e:f10") testCases := []struct { - name string - subnet *net.IPNet - want net.IP + pref netip.Prefix + want netip.Addr + name string }{{ + pref: netip.PrefixFrom(known4, 0), + want: fullBroadcast4, 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", - subnet: &net.IPNet{ - IP: known6, - }, + pref: netip.PrefixFrom(known4, 20), + want: netip.MustParseAddr("192.168.15.255"), + name: "full", + }, { + pref: netip.PrefixFrom(known6, netutil.IPv6BitLen), want: known6, + name: "ipv6_no_mask", }, { + pref: netip.PrefixFrom(known4, netutil.IPv4BitLen), + want: known4, 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", - subnet: &net.IPNet{ - IP: net.IP{0, 0, 0, 0}, - Mask: net.IPMask{0, 0, 0, 0}, - }, - want: net.IPv4bcast, + }, { + pref: netip.Prefix{}, + want: netip.Addr{}, + name: "invalid", }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - bc := BroadcastFromIPNet(tc.subnet) - assert.True(t, bc.Equal(tc.want), bc) + assert.Equal(t, tc.want, BroadcastFromPref(tc.pref)) }) } } func TestCheckPort(t *testing.T) { + laddr := netip.AddrPortFrom(IPv4Localhost(), 0) + 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) testutil.CleanupAndRequireSuccess(t, l.Close) - ipp := netutil.IPPortFromAddr(l.Addr()) - require.NotNil(t, ipp) - require.NotNil(t, ipp.IP) - require.NotZero(t, ipp.Port) + addr := l.Addr() + require.IsType(t, new(net.TCPAddr), addr) - 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{} require.ErrorAs(t, err, &target) @@ -216,16 +211,18 @@ func TestCheckPort(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) testutil.CleanupAndRequireSuccess(t, conn.Close) - ipp := netutil.IPPortFromAddr(conn.LocalAddr()) - require.NotNil(t, ipp) - require.NotNil(t, ipp.IP) - require.NotZero(t, ipp.Port) + addr := conn.LocalAddr() + require.IsType(t, new(net.UDPAddr), addr) - 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{} require.ErrorAs(t, err, &target) @@ -233,12 +230,12 @@ func TestCheckPort(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) }) 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) }) } @@ -322,18 +319,18 @@ func TestNetInterface_MarshalJSON(t *testing.T) { `"mtu":1500` + `}` + "\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} - mask4, mask6 := net.CIDRMask(24, netutil.IPv4BitLen), net.CIDRMask(8, netutil.IPv6BitLen) + ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4}) + 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{ - Addresses: []net.IP{ip4, ip6}, - Subnets: []*net.IPNet{{ - IP: ip4.Mask(mask4), - Mask: mask4, - }, { - IP: ip6.Mask(mask6), - Mask: mask6, - }}, + Addresses: []netip.Addr{ip4, ip6}, + Subnets: []netip.Prefix{net4, net6}, Name: "iface0", HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, Flags: net.FlagUp | net.FlagMulticast, diff --git a/internal/dhcpd/config.go b/internal/dhcpd/config.go index 9d8ef057..718d567f 100644 --- a/internal/dhcpd/config.go +++ b/internal/dhcpd/config.go @@ -3,12 +3,12 @@ package dhcpd import ( "fmt" "net" + "net/netip" "time" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/golibs/errors" - "github.com/AdguardTeam/golibs/netutil" ) // ServerConfig is the configuration for the DHCP server. The order of YAML @@ -65,16 +65,16 @@ type V4ServerConf struct { Enabled bool `yaml:"-" json:"-"` InterfaceName string `yaml:"-" json:"-"` - GatewayIP net.IP `yaml:"gateway_ip" json:"gateway_ip"` - SubnetMask net.IP `yaml:"subnet_mask" json:"subnet_mask"` + GatewayIP netip.Addr `yaml:"gateway_ip" json:"gateway_ip"` + SubnetMask netip.Addr `yaml:"subnet_mask" json:"subnet_mask"` // broadcastIP is the broadcasting address pre-calculated from the // configured gateway IP and subnet mask. - broadcastIP net.IP + broadcastIP netip.Addr // 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" json:"range_start"` - RangeEnd net.IP `yaml:"range_end" json:"range_end"` + RangeStart netip.Addr `yaml:"range_start" json:"range_start"` + RangeEnd netip.Addr `yaml:"range_end" json:"range_end"` LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds @@ -95,11 +95,11 @@ type V4ServerConf struct { ipRange *ipRange 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 // gateway. - subnet *net.IPNet + subnet netip.Prefix // notify is a way to signal to other components that leases have been // 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. const errNilConfig errors.Error = "nil config" -// ensureV4 returns a 4-byte version of ip. An error is returned if the passed -// ip is not an IPv4. -func ensureV4(ip net.IP) (ip4 net.IP, err error) { - if ip == nil { - return nil, fmt.Errorf("%v is not an IP address", ip) - } - - ip4 = ip.To4() - if ip4 == nil { - return nil, fmt.Errorf("%v is not an IPv4 address", ip) +// ensureV4 returns an unmapped version of ip. An error is returned if the +// passed ip is not an IPv4. +func ensureV4(ip netip.Addr, kind string) (ip4 netip.Addr, err error) { + ip4 = ip.Unmap() + if !ip4.IsValid() || !ip4.Is4() { + return netip.Addr{}, fmt.Errorf("%v is not an IPv4 %s", ip, kind) } return ip4, nil @@ -139,33 +135,45 @@ func (c *V4ServerConf) Validate() (err error) { return errNilConfig } - var gatewayIP net.IP - gatewayIP, err = ensureV4(c.GatewayIP) + gatewayIP, err := ensureV4(c.GatewayIP, "address") 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 } - if c.SubnetMask == nil { - 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) + subnetMask, err := ensureV4(c.SubnetMask, "subnet mask") 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. 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", gatewayIP, 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", c.RangeStart, c.subnet, ) } - if !c.subnet.Contains(c.RangeEnd) { + if !c.subnet.Contains(rangeEnd) { return fmt.Errorf("range end %v is outside network %v", c.RangeEnd, c.subnet, diff --git a/internal/dhcpd/conn_unix.go b/internal/dhcpd/conn_unix.go index ec58afda..efcbc8aa 100644 --- a/internal/dhcpd/conn_unix.go +++ b/internal/dhcpd/conn_unix.go @@ -73,10 +73,10 @@ func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err erro return &dhcpConn{ udpConn: bcast, - bcastIP: s.conf.broadcastIP, + bcastIP: s.conf.broadcastIP.AsSlice(), rawConn: ucast, srcMAC: iface.HardwareAddr, - srcIP: s.conf.dnsIPAddrs[0], + srcIP: s.conf.dnsIPAddrs[0].AsSlice(), }, nil } diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index 875b3d79..a98178ca 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -242,7 +242,7 @@ func Create(conf *ServerConfig) (s *server, err error) { v4conf := conf.Conf4 v4conf.InterfaceName = s.conf.InterfaceName 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) if err != nil { diff --git a/internal/dhcpd/dhcpd_unix_test.go b/internal/dhcpd/dhcpd_unix_test.go index cd1ca39a..f13dbf98 100644 --- a/internal/dhcpd/dhcpd_unix_test.go +++ b/internal/dhcpd/dhcpd_unix_test.go @@ -4,6 +4,7 @@ package dhcpd import ( "net" + "net/netip" "os" "testing" "time" @@ -33,10 +34,10 @@ func TestDB(t *testing.T) { s.srv4, err = v4Create(&V4ServerConf{ Enabled: true, - RangeStart: net.IP{192, 168, 10, 100}, - RangeEnd: net.IP{192, 168, 10, 200}, - GatewayIP: net.IP{192, 168, 10, 1}, - SubnetMask: net.IP{255, 255, 255, 0}, + RangeStart: netip.MustParseAddr("192.168.10.100"), + RangeEnd: netip.MustParseAddr("192.168.10.200"), + GatewayIP: netip.MustParseAddr("192.168.10.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), notify: testNotify, }) require.NoError(t, err) @@ -113,35 +114,35 @@ func TestNormalizeLeases(t *testing.T) { func TestV4Server_badRange(t *testing.T) { testCases := []struct { name string + gatewayIP netip.Addr + subnetMask netip.Addr 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: " + "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 " + "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 " + "192.168.10.1/27", - gatewayIP: net.IP{192, 168, 10, 1}, - subnetMask: net.IP{255, 255, 255, 224}, }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { conf := V4ServerConf{ Enabled: true, - RangeStart: net.IP{192, 168, 10, 20}, - RangeEnd: net.IP{192, 168, 10, 200}, + RangeStart: netip.MustParseAddr("192.168.10.20"), + RangeEnd: netip.MustParseAddr("192.168.10.200"), GatewayIP: tc.gatewayIP, SubnetMask: tc.subnetMask, notify: testNotify, diff --git a/internal/dhcpd/http_unix.go b/internal/dhcpd/http_unix.go index e6b1f8fc..2e7ef57d 100644 --- a/internal/dhcpd/http_unix.go +++ b/internal/dhcpd/http_unix.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + "net/netip" "os" "github.com/AdguardTeam/AdGuardHome/internal/aghalg" @@ -17,11 +18,11 @@ import ( ) type v4ServerConfJSON struct { - GatewayIP net.IP `json:"gateway_ip"` - SubnetMask net.IP `json:"subnet_mask"` - RangeStart net.IP `json:"range_start"` - RangeEnd net.IP `json:"range_end"` - LeaseDuration uint32 `json:"lease_duration"` + GatewayIP netip.Addr `json:"gateway_ip"` + SubnetMask netip.Addr `json:"subnet_mask"` + RangeStart netip.Addr `json:"range_start"` + RangeEnd netip.Addr `json:"range_end"` + LeaseDuration uint32 `json:"lease_duration"` } func (j *v4ServerConfJSON) toServerConf() *V4ServerConf { @@ -39,8 +40,8 @@ func (j *v4ServerConfJSON) toServerConf() *V4ServerConf { } type v6ServerConfJSON struct { - RangeStart net.IP `json:"range_start"` - LeaseDuration uint32 `json:"lease_duration"` + RangeStart netip.Addr `json:"range_start"` + LeaseDuration uint32 `json:"lease_duration"` } func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf { @@ -49,7 +50,7 @@ func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf { } return V6ServerConf{ - RangeStart: j.RangeStart, + RangeStart: j.RangeStart.AsSlice(), LeaseDuration: j.LeaseDuration, } } @@ -144,7 +145,7 @@ func (s *server) handleDHCPSetConfigV4( v4Conf := conf.V4.toServerConf() v4Conf.Enabled = conf.Enabled == aghalg.NBTrue - if len(v4Conf.RangeStart) == 0 { + if !v4Conf.RangeStart.IsValid() { v4Conf.Enabled = false } @@ -275,12 +276,12 @@ func (s *server) setConfFromJSON(conf *dhcpServerConfigJSON, srv4, srv6 DHCPServ } type netInterfaceJSON struct { - Name string `json:"name"` - HardwareAddr string `json:"hardware_address"` - Flags string `json:"flags"` - GatewayIP net.IP `json:"gateway_ip"` - Addrs4 []net.IP `json:"ipv4_addresses"` - Addrs6 []net.IP `json:"ipv6_addresses"` + Name string `json:"name"` + HardwareAddr string `json:"hardware_address"` + Flags string `json:"flags"` + GatewayIP netip.Addr `json:"gateway_ip"` + Addrs4 []netip.Addr `json:"ipv4_addresses"` + Addrs6 []netip.Addr `json:"ipv6_addresses"` } 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 } // ignore link-local + // + // TODO(e.burkov): Try to listen DHCP on LLA as well. if ipnet.IP.IsLinkLocalUnicast() { 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 { - 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 { diff --git a/internal/dhcpd/iprange.go b/internal/dhcpd/iprange.go index 45422957..6cf7d01f 100644 --- a/internal/dhcpd/iprange.go +++ b/internal/dhcpd/iprange.go @@ -27,6 +27,8 @@ const maxRangeLen = math.MaxUint32 // newIPRange creates a new IP address range. start must be less than end. The // resulting range must not be greater than maxRangeLen. +// +// TODO(e.burkov): Use netip.Addr. func newIPRange(start, end net.IP) (r *ipRange, err error) { defer func() { err = errors.Annotate(err, "invalid ip range: %w") }() diff --git a/internal/dhcpd/options_unix.go b/internal/dhcpd/options_unix.go index 6950604d..082826d5 100644 --- a/internal/dhcpd/options_unix.go +++ b/internal/dhcpd/options_unix.go @@ -372,12 +372,9 @@ func (s *v4Server) prepareOptions() { dhcpv4.OptGeneric(dhcpv4.OptionTCPKeepaliveGarbage, []byte{0x01}), // Values From Configuration + dhcpv4.OptRouter(s.conf.GatewayIP.AsSlice()), - // Set the Router Option to working subnet's IP since it's initialized - // with the address of the gateway. - dhcpv4.OptRouter(s.conf.subnet.IP), - - dhcpv4.OptSubnetMask(s.conf.subnet.Mask), + dhcpv4.OptSubnetMask(s.conf.SubnetMask.AsSlice()), ) // Set values for explicitly configured options. diff --git a/internal/dhcpd/options_unix_test.go b/internal/dhcpd/options_unix_test.go index 2b5a5cb0..a4ab7dc8 100644 --- a/internal/dhcpd/options_unix_test.go +++ b/internal/dhcpd/options_unix_test.go @@ -251,8 +251,6 @@ func TestPrepareOptions(t *testing.T) { for _, tc := range testCases { s := &v4Server{ conf: &V4ServerConf{ - // Just to avoid nil pointer dereference. - subnet: &net.IPNet{}, Options: tc.opts, }, } diff --git a/internal/dhcpd/v4_unix.go b/internal/dhcpd/v4_unix.go index 3735ffdc..af389fda 100644 --- a/internal/dhcpd/v4_unix.go +++ b/internal/dhcpd/v4_unix.go @@ -6,6 +6,7 @@ import ( "bytes" "fmt" "net" + "net/netip" "strings" "sync" "time" @@ -295,7 +296,8 @@ func (s *v4Server) addLease(l *Lease) (err error) { if l.IsStatic() { // TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is // 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) } } else if !inOffset { @@ -353,7 +355,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) { ip := l.IP.To4() if ip == nil { 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) } @@ -701,7 +703,8 @@ func (s *v4Server) handleSelecting( // Client inserts the address of the selected server in server identifier, // ciaddr MUST be zero. 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) 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) { 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) return nil, false @@ -747,7 +751,7 @@ func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, 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 // 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) @@ -972,7 +976,7 @@ func (s *v4Server) handle(req, resp *dhcpv4.DHCPv4) int { // Include server's identifier option since any reply should contain it. // // 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. var l *Lease @@ -1188,7 +1192,14 @@ func (s *v4Server) Start() (err error) { 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 if c, err = s.newDHCPConn(iface); err != nil { diff --git a/internal/dhcpd/v4_unix_test.go b/internal/dhcpd/v4_unix_test.go index c73009a2..c8e4dd1f 100644 --- a/internal/dhcpd/v4_unix_test.go +++ b/internal/dhcpd/v4_unix_test.go @@ -5,6 +5,7 @@ package dhcpd import ( "fmt" "net" + "net/netip" "strings" "testing" "time" @@ -22,11 +23,11 @@ import ( ) var ( - DefaultRangeStart = net.IP{192, 168, 10, 100} - DefaultRangeEnd = net.IP{192, 168, 10, 200} - DefaultGatewayIP = net.IP{192, 168, 10, 1} - DefaultSelfIP = net.IP{192, 168, 10, 2} - DefaultSubnetMask = net.IP{255, 255, 255, 0} + DefaultRangeStart = netip.MustParseAddr("192.168.10.100") + DefaultRangeEnd = netip.MustParseAddr("192.168.10.200") + DefaultGatewayIP = netip.MustParseAddr("192.168.10.1") + DefaultSelfIP = netip.MustParseAddr("192.168.10.2") + DefaultSubnetMask = netip.MustParseAddr("255.255.255.0") ) // defaultV4ServerConf returns the default configuration for *v4Server to use in @@ -39,7 +40,7 @@ func defaultV4ServerConf() (conf *V4ServerConf) { GatewayIP: DefaultGatewayIP, SubnetMask: DefaultSubnetMask, 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), Hostname: staticName, HWAddr: anotherMAC, - IP: anotherIP, + IP: anotherIP.AsSlice(), }) assert.ErrorIs(t, err, ErrDupHostname) }) @@ -96,7 +97,7 @@ func TestV4Server_leasing(t *testing.T) { Expiry: time.Unix(leaseExpireStatic, 0), Hostname: anotherName, HWAddr: staticMAC, - IP: anotherIP, + IP: anotherIP.AsSlice(), }) testutil.AssertErrorMsg(t, wantErrMsg, err) }) @@ -135,7 +136,7 @@ func TestV4Server_leasing(t *testing.T) { dhcpv4.WithOption(dhcpv4.OptHostName(name)), dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(ip)), dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte{1, 2, 3, 4, 5, 6, 8})), - dhcpv4.WithGatewayIP(DefaultGatewayIP), + dhcpv4.WithGatewayIP(DefaultGatewayIP.AsSlice()), ) require.NoError(t, err) @@ -150,7 +151,7 @@ func TestV4Server_leasing(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( dhcpv4.OptHostName(staticName), @@ -164,7 +165,7 @@ func TestV4Server_leasing(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( dhcpv4.OptHostName(anotherName), @@ -219,7 +220,7 @@ func TestV4Server_AddRemove_static(t *testing.T) { lease: &Lease{ Hostname: "probably-router.local", HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}, - IP: DefaultGatewayIP, + IP: DefaultGatewayIP.AsSlice(), }, name: "with_gateway_ip", wantErrMsg: "dhcpv4: adding static lease: " + @@ -326,7 +327,7 @@ func TestV4_AddReplace(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} // 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()} } else { - defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP)) }() + defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP.AsSlice())) }() } var err error s, err = v4Create(conf) require.NoError(t, err) - s.conf.dnsIPAddrs = []net.IP{defaultIP} + s.conf.dnsIPAddrs = []netip.Addr{defaultIP} return s } @@ -386,7 +387,7 @@ func TestV4Server_handle_optionsPriority(t *testing.T) { t.Run("default", func(t *testing.T) { 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) { @@ -506,8 +507,9 @@ func TestV4StaticLease_Get(t *testing.T) { s, ok := sIface.(*v4Server) require.True(t, ok) - s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}} - s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...)) + dnsAddr := netip.MustParseAddr("192.168.10.1") + s.conf.dnsIPAddrs = []netip.Addr{dnsAddr} + s.implicitOpts.Update(dhcpv4.OptDNS(dnsAddr.AsSlice())) l := &Lease{ Hostname: "static-1.local", @@ -539,9 +541,12 @@ func TestV4StaticLease_Get(t *testing.T) { assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType()) assert.Equal(t, mac, resp.ClientHWAddr) 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.Equal(t, s.conf.subnet.Mask, resp.SubnetMask()) + + assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice())) + 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()) }) @@ -561,16 +566,19 @@ func TestV4StaticLease_Get(t *testing.T) { assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) assert.Equal(t, mac, resp.ClientHWAddr) 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.Equal(t, s.conf.subnet.Mask, resp.SubnetMask()) + + assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice())) + 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()) }) dnsAddrs := resp.DNS() 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) { ls := s.GetLeases(LeasesStatic) @@ -591,8 +599,8 @@ func TestV4DynamicLease_Get(t *testing.T) { s, err := v4Create(conf) require.NoError(t, err) - s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}} - s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...)) + s.conf.dnsIPAddrs = []netip.Addr{netip.MustParseAddr("192.168.10.1")} + s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs[0].AsSlice())) var req, resp *dhcpv4.DHCPv4 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, mac, resp.ClientHWAddr) - assert.Equal(t, s.conf.RangeStart, resp.YourIPAddr) - assert.Equal(t, s.conf.GatewayIP, resp.ServerIdentifier()) + assert.True(t, resp.YourIPAddr.Equal(s.conf.RangeStart.AsSlice())) + assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice())) router := resp.Router() 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, []byte("012"), resp.Options.Get(dhcpv4.OptionFQDN)) @@ -649,15 +658,17 @@ func TestV4DynamicLease_Get(t *testing.T) { t.Run("ack", func(t *testing.T) { assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType()) 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() 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.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()) }) diff --git a/internal/dnsforward/access.go b/internal/dnsforward/access.go index 23ec1137..ddf3d93f 100644 --- a/internal/dnsforward/access.go +++ b/internal/dnsforward/access.go @@ -19,6 +19,7 @@ import ( // accessCtx controls IP and client blocking that takes place before all other // processing. An accessCtx is safe for concurrent use. type accessCtx struct { + // TODO(e.burkov): Use map[netip.Addr]struct{} instead. allowedIPs *netutil.IPMap blockedIPs *netutil.IPMap diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go index 3806ef5b..2455fff5 100644 --- a/internal/dnsforward/dnsforward.go +++ b/internal/dnsforward/dnsforward.go @@ -81,6 +81,7 @@ type Server struct { tableHostToIP hostToIPTable tableHostToIPLock sync.Mutex + // TODO(e.burkov): Use map[netip.Addr]struct{} instead. tableIPToHost *netutil.IPMap tableIPToHostLock sync.Mutex diff --git a/internal/home/clients.go b/internal/home/clients.go index 631d27ba..ee535ea9 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -119,6 +119,8 @@ type clientsContainer struct { idIndex map[string]*Client // ID -> client // ipToRC is the IP address to *RuntimeClient map. + // + // TODO(e.burkov): Use map[netip.Addr]struct{} instead. ipToRC *netutil.IPMap lock sync.Mutex diff --git a/internal/home/clients_test.go b/internal/home/clients_test.go index 5b4ccdd3..00148c26 100644 --- a/internal/home/clients_test.go +++ b/internal/home/clients_test.go @@ -2,6 +2,7 @@ package home import ( "net" + "net/netip" "os" "runtime" "testing" @@ -287,10 +288,10 @@ func TestClientsAddExisting(t *testing.T) { DBFilePath: "leases.db", Conf4: dhcpd.V4ServerConf{ Enabled: true, - GatewayIP: net.IP{1, 2, 3, 1}, - SubnetMask: net.IP{255, 255, 255, 0}, - RangeStart: net.IP{1, 2, 3, 2}, - RangeEnd: net.IP{1, 2, 3, 10}, + GatewayIP: netip.MustParseAddr("1.2.3.1"), + SubnetMask: netip.MustParseAddr("255.255.255.0"), + RangeStart: netip.MustParseAddr("1.2.3.2"), + RangeEnd: netip.MustParseAddr("1.2.3.10"), }, } diff --git a/internal/home/config.go b/internal/home/config.go index b5aecea2..7df4f853 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -3,7 +3,7 @@ package home import ( "bytes" "fmt" - "net" + "net/netip" "os" "path/filepath" "sync" @@ -85,19 +85,28 @@ type configuration struct { // It's reset after config is parsed fileData []byte - BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to - BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server - BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client - Users []webUser `yaml:"users"` // Users that can access HTTP server + // BindHost is the address for the web interface server to listen on. + BindHost netip.Addr `yaml:"bind_host"` + // BindPort is the port for the web interface server to listen on. + 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 // can do before being blocked. AuthAttempts uint `yaml:"auth_attempts"` // AuthBlockMin is the duration, in minutes, of the block of new login // attempts after AuthAttempts unsuccessful login attempts. - AuthBlockMin uint `yaml:"block_auth_min"` - ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client - Language string `yaml:"language"` // two-letter ISO 639-1 language code - DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060 + AuthBlockMin uint `yaml:"block_auth_min"` + // ProxyURL is the address of proxy server for the internal HTTP client. + ProxyURL string `yaml:"http_proxy"` + // 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) // 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 // only configuration subsection covering the changes with a single - // migration. + // migration. Also keep the blocked services in mind. Filters []filtering.FilterYAML `yaml:"filters"` WhitelistFilters []filtering.FilterYAML `yaml:"whitelist_filters"` UserRules []string `yaml:"user_rules"` @@ -135,18 +144,26 @@ type configuration struct { // field ordering is important -- yaml fields will mirror ordering from here type dnsConfig struct { - BindHosts []net.IP `yaml:"bind_hosts"` - Port int `yaml:"port"` + BindHosts []netip.Addr `yaml:"bind_hosts"` + 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"` - QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled - QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file + // QueryLogEnabled defines if the query log is enabled. + 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 timeutil.Duration `yaml:"querylog_interval"` - QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk - AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats + QueryLogInterval timeutil.Duration `yaml:"querylog_interval"` + // QueryLogMemSize is the number of entries kept in memory before they are + // 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"` @@ -211,12 +228,12 @@ type tlsConfigSettings struct { var config = &configuration{ BindPort: 3000, BetaBindPort: 0, - BindHost: net.IP{0, 0, 0, 0}, + BindHost: netip.IPv4Unspecified(), AuthAttempts: 5, AuthBlockMin: 15, WebSessionTTLHours: 30 * 24, DNS: dnsConfig{ - BindHosts: []net.IP{{0, 0, 0, 0}}, + BindHosts: []netip.Addr{netip.IPv4Unspecified()}, Port: defaultPortDNS, StatsInterval: 1, QueryLogEnabled: true, diff --git a/internal/home/control.go b/internal/home/control.go index 48ac45f4..5e4e6df2 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -2,8 +2,8 @@ package home import ( "fmt" - "net" "net/http" + "net/netip" "net/url" "runtime" "strings" @@ -20,11 +20,11 @@ import ( // appendDNSAddrs is a convenient helper for appending a formatted form of DNS // 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 { var hostport string if config.DNS.Port != defaultPortDNS { - hostport = netutil.JoinHostPort(addr.String(), config.DNS.Port) + hostport = netip.AddrPortFrom(addr, uint16(config.DNS.Port)).String() } else { 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 // dst. It also adds the IP addresses of all network interfaces if src contains // 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 for _, h := range src { 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. func collectDNSAddresses() (addrs []string, err error) { 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 { addrs, err = appendDNSAddrsWithIfaces(addrs, hosts) if err != nil { diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go index 7df8d320..787fb5d1 100644 --- a/internal/home/controlinstall.go +++ b/internal/home/controlinstall.go @@ -5,8 +5,8 @@ import ( "encoding/json" "fmt" "io" - "net" "net/http" + "net/netip" "os" "os/exec" "path/filepath" @@ -64,9 +64,9 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request } type checkConfReqEnt struct { - IP net.IP `json:"ip"` - Port int `json:"port"` - Autofix bool `json:"autofix"` + IP netip.Addr `json:"ip"` + Port int `json:"port"` + Autofix bool `json:"autofix"` } type checkConfReq struct { @@ -117,7 +117,7 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err // 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 @@ -142,13 +142,13 @@ func (req *checkConfReq) validateDNS( 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 { 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) { return false, err } @@ -160,7 +160,7 @@ func (req *checkConfReq) validateDNS( 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 } @@ -196,7 +196,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) // handleStaticIP - handles static IP request // It either checks if we have a static IP // 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{} interfaceName := aghnet.InterfaceByIP(ip) @@ -304,8 +304,8 @@ func disableDNSStubListener() error { } type applyConfigReqEnt struct { - IP net.IP `json:"ip"` - Port int `json:"port"` + IP netip.Addr `json:"ip"` + Port int `json:"port"` } type applyConfigReq struct { @@ -397,14 +397,14 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) { 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 { aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) 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 { 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 config.BindHost = req.Web.IP 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 // 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") } - 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 { - 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 { return nil, false, fmt.Errorf( "checking address %s:%d: %w", @@ -518,9 +518,9 @@ func (web *Web) registerInstallHandlers() { // TODO(e.burkov): This should removed with the API v1 when the appropriate // functionality will appear in default checkConfigReqEnt. type checkConfigReqEntBeta struct { - IP []net.IP `json:"ip"` - Port int `json:"port"` - Autofix bool `json:"autofix"` + IP []netip.Addr `json:"ip"` + Port int `json:"port"` + Autofix bool `json:"autofix"` } // checkConfigReqBeta is a struct representing new client's config check request @@ -590,8 +590,8 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ // TODO(e.burkov): This should removed with the API v1 when the appropriate // functionality will appear in default applyConfigReqEnt. type applyConfigReqEntBeta struct { - IP []net.IP `json:"ip"` - Port int `json:"port"` + IP []netip.Addr `json:"ip"` + Port int `json:"port"` } // applyConfigReqBeta is a struct representing new client's config setting diff --git a/internal/home/dns.go b/internal/home/dns.go index da462876..6c0d6531 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -3,6 +3,7 @@ package home import ( "fmt" "net" + "net/netip" "net/url" "os" "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 { return nil } - tcpAddrs = make([]*net.TCPAddr, len(ips)) - for i, ip := range ips { - tcpAddrs[i] = &net.TCPAddr{ - IP: ip, - Port: port, - } + tcpAddrs = make([]*net.TCPAddr, 0, len(ips)) + for _, ip := range ips { + tcpAddrs = append(tcpAddrs, net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port)))) } 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 { return nil } - udpAddrs = make([]*net.UDPAddr, len(ips)) - for i, ip := range ips { - udpAddrs[i] = &net.UDPAddr{ - IP: ip, - Port: port, - } + udpAddrs = make([]*net.UDPAddr, 0, len(ips)) + for _, ip := range ips { + udpAddrs = append(udpAddrs, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port)))) } return udpAddrs @@ -200,7 +195,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) { dnsConf := config.DNS hosts := dnsConf.BindHosts if len(hosts) == 0 { - hosts = []net.IP{{127, 0, 0, 1}} + hosts = []netip.Addr{aghnet.IPv4Localhost()} } newConf = dnsforward.ServerConfig{ @@ -257,7 +252,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) { 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 == "" { return dnscc, errors.Error("no dnscrypt_config_file") } diff --git a/internal/home/home.go b/internal/home/home.go index 289c1c64..adbf7bfb 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "net/http/pprof" + "net/netip" "net/url" "os" "os/signal" @@ -356,7 +357,7 @@ func setupConfig(opts options) (err error) { } // override bind host/port from the console - if opts.bindHost != nil { + if opts.bindHost.IsValid() { config.BindHost = opts.bindHost } 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 - err := aghnet.CheckPort("tcp", net.IP{127, 0, 0, 1}, defaultPortDNS) + err := aghnet.CheckPort("tcp", netip.AddrPortFrom(aghnet.IPv4Localhost(), defaultPortDNS)) if err != nil { if errors.Is(err, os.ErrPermission) { log.Fatal(`Permission check failed. diff --git a/internal/home/mobileconfig_test.go b/internal/home/mobileconfig_test.go index 5230a2ac..48783d0d 100644 --- a/internal/home/mobileconfig_test.go +++ b/internal/home/mobileconfig_test.go @@ -3,12 +3,11 @@ package home import ( "bytes" "encoding/json" - "net" "net/http" "net/http/httptest" + "net/netip" "testing" - "github.com/AdguardTeam/golibs/netutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "howett.net/plist" @@ -28,7 +27,7 @@ func setupDNSIPs(t testing.TB) { config = &configuration{ DNS: dnsConfig{ - BindHosts: []net.IP{netutil.IPv4Zero()}, + BindHosts: []netip.Addr{netip.IPv4Unspecified()}, Port: defaultPortDNS, }, } diff --git a/internal/home/options.go b/internal/home/options.go index 531a0fd4..e14e26f6 100644 --- a/internal/home/options.go +++ b/internal/home/options.go @@ -2,7 +2,7 @@ package home import ( "fmt" - "net" + "net/netip" "os" "strconv" "strings" @@ -35,7 +35,7 @@ type options struct { serviceControlAction string // 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 int @@ -130,14 +130,15 @@ var cmdLineOpts = []cmdLineOpt{{ longName: "work-dir", shortName: "w", }, { - updateWithValue: func(o options, v string) (options, error) { - o.bindHost = net.ParseIP(v) - return o, nil + updateWithValue: func(o options, v string) (oo options, err error) { + o.bindHost, err = netip.ParseAddr(v) + + return o, err }, updateNoValue: nil, effect: nil, serialize: func(o options) (val string, ok bool) { - if o.bindHost == nil { + if !o.bindHost.IsValid() { return "", false } diff --git a/internal/home/options_test.go b/internal/home/options_test.go index 7954c0e4..32b4243a 100644 --- a/internal/home/options_test.go +++ b/internal/home/options_test.go @@ -2,7 +2,7 @@ package home import ( "fmt" - "net" + "net/netip" "testing" "github.com/stretchr/testify/assert" @@ -56,11 +56,13 @@ func TestParseWorkDir(t *testing.T) { } func TestParseBindHost(t *testing.T) { - assert.Nil(t, testParseOK(t).bindHost, "empty is not host") - assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host") + wantAddr := netip.MustParseAddr("1.2.3.4") + + 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") - 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") } @@ -149,8 +151,8 @@ func TestOptsToArgs(t *testing.T) { opts: options{workDir: "path"}, }, { name: "bind_host", + opts: options{bindHost: netip.MustParseAddr("1.2.3.4")}, args: []string{"-h", "1.2.3.4"}, - opts: options{bindHost: net.IP{1, 2, 3, 4}}, }, { name: "bind_port", args: []string{"-p", "666"}, diff --git a/internal/home/web.go b/internal/home/web.go index 3e248d80..3cd04630 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -4,8 +4,8 @@ import ( "context" "crypto/tls" "io/fs" - "net" "net/http" + "net/netip" "sync" "time" @@ -37,7 +37,7 @@ type webConfig struct { clientFS fs.FS clientBetaFS fs.FS - BindHost net.IP + BindHost netip.Addr BindPort int BetaBindPort int PortHTTPS int @@ -135,8 +135,11 @@ func newWeb(conf *webConfig) (w *Web) { // // TODO(a.garipov): Adapt for HTTP/3. func webCheckPortAvailable(port int) (ok bool) { - return Context.web.httpsServer.server != nil || - aghnet.CheckPort("tcp", config.BindHost, port) == nil + if Context.web.httpsServer.server != nil { + return true + } + + return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil } // TLSConfigChanged updates the TLS configuration and restarts the HTTPS server