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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"),
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"},

View file

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