diff --git a/CHANGELOG.md b/CHANGELOG.md index cb71b998..8bccfd0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to ### Added +- Static IP address detection on FreeBSD ([#3289]). - Optimistic cache ([#2145]). - New possible value of `6h` for `querylog_interval` setting ([#2504]). - Blocking access using client IDs ([#2624], [#3162]). @@ -62,6 +63,7 @@ and this project adheres to ### Fixed +- Incomplete HTTP response for static IP address. - DNSCrypt queries weren't appearing in query log ([#3372]). - Wrong IP address for proxied DNS-over-HTTPS queries ([#2799]). - Domain name letter case mismatches in DNS rewrites ([#3351]). @@ -108,6 +110,7 @@ and this project adheres to [#3217]: https://github.com/AdguardTeam/AdGuardHome/issues/3217 [#3256]: https://github.com/AdguardTeam/AdGuardHome/issues/3256 [#3257]: https://github.com/AdguardTeam/AdGuardHome/issues/3257 +[#3289]: https://github.com/AdguardTeam/AdGuardHome/issues/3289 [#3335]: https://github.com/AdguardTeam/AdGuardHome/issues/3335 [#3343]: https://github.com/AdguardTeam/AdGuardHome/issues/3343 [#3351]: https://github.com/AdguardTeam/AdGuardHome/issues/3351 diff --git a/internal/aghnet/net_freebsd.go b/internal/aghnet/net_freebsd.go new file mode 100644 index 00000000..df6e8970 --- /dev/null +++ b/internal/aghnet/net_freebsd.go @@ -0,0 +1,78 @@ +//go:build freebsd +// +build freebsd + +package aghnet + +import ( + "bufio" + "fmt" + "io" + "net" + "os" + "strings" + + "github.com/AdguardTeam/AdGuardHome/internal/aghio" + "github.com/AdguardTeam/AdGuardHome/internal/aghos" + "github.com/AdguardTeam/golibs/errors" +) + +func canBindPrivilegedPorts() (can bool, err error) { + return aghos.HaveAdminRights() +} + +// maxCheckedFileSize is the maximum acceptable length of the /etc/rc.conf file. +const maxCheckedFileSize = 1024 * 1024 + +func ifaceHasStaticIP(ifaceName string) (ok bool, err error) { + const filename = "/etc/rc.conf" + + var f *os.File + f, err = os.Open(filename) + if err != nil { + return false, err + } + defer func() { err = errors.WithDeferred(err, f.Close()) }() + + var r io.Reader + r, err = aghio.LimitReader(f, maxCheckedFileSize) + if err != nil { + return false, err + } + + return rcConfStaticConfig(r, ifaceName) +} + +// rcConfStaticConfig checks if the interface is configured by /etc/rc.conf to +// have a static IP. +func rcConfStaticConfig(r io.Reader, ifaceName string) (has bool, err error) { + s := bufio.NewScanner(r) + for ifaceLinePref := fmt.Sprintf("ifconfig_%s", ifaceName); s.Scan(); { + line := strings.TrimSpace(s.Text()) + if !strings.HasPrefix(line, ifaceLinePref) { + continue + } + + eqIdx := len(ifaceLinePref) + if line[eqIdx] != '=' { + continue + } + + fieldsStart, fieldsEnd := eqIdx+2, len(line)-1 + if fieldsStart >= fieldsEnd { + continue + } + + fields := strings.Fields(line[fieldsStart:fieldsEnd]) + if len(fields) >= 2 && + strings.ToLower(fields[0]) == "inet" && + net.ParseIP(fields[1]) != nil { + return true, s.Err() + } + } + + return false, s.Err() +} + +func ifaceSetStaticIP(string) (err error) { + return aghos.Unsupported("setting static ip") +} diff --git a/internal/aghnet/net_freebsd_test.go b/internal/aghnet/net_freebsd_test.go new file mode 100644 index 00000000..bf8c7ec3 --- /dev/null +++ b/internal/aghnet/net_freebsd_test.go @@ -0,0 +1,60 @@ +//go:build freebsd +// +build freebsd + +package aghnet + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRcConfStaticConfig(t *testing.T) { + const ifaceName = `em0` + const nl = "\n" + + testCases := []struct { + name string + rcconfData string + wantHas bool + }{{ + name: "simple", + rcconfData: `ifconfig_em0="inet 127.0.0.253 netmask 0xffffffff"` + nl, + wantHas: true, + }, { + name: "case_insensitiveness", + rcconfData: `ifconfig_em0="InEt 127.0.0.253 NeTmAsK 0xffffffff"` + nl, + wantHas: true, + }, { + name: "comments_and_trash", + rcconfData: `# comment 1` + nl + + `` + nl + + `# comment 2` + nl + + `ifconfig_em0="inet 127.0.0.253 netmask 0xffffffff"` + nl, + wantHas: true, + }, { + name: "aliases", + rcconfData: `ifconfig_em0_alias="inet 127.0.0.1/24"` + nl + + `ifconfig_em0="inet 127.0.0.253 netmask 0xffffffff"` + nl, + wantHas: true, + }, { + name: "incorrect_config", + rcconfData: `ifconfig_em0="inet6 127.0.0.253 netmask 0xffffffff"` + nl + + `ifconfig_em0="inet 127.0.0.253 net-mask 0xffffffff"` + nl + + `ifconfig_em0="inet 256.256.256.256 netmask 0xffffffff"` + nl + + `ifconfig_em0=""` + nl, + wantHas: false, + }} + + for _, tc := range testCases { + r := strings.NewReader(tc.rcconfData) + t.Run(tc.name, func(t *testing.T) { + has, err := rcConfStaticConfig(r, ifaceName) + require.NoError(t, err) + + assert.Equal(t, tc.wantHas, has) + }) + } +} diff --git a/internal/aghnet/net_others.go b/internal/aghnet/net_others.go index 07add540..033a40cc 100644 --- a/internal/aghnet/net_others.go +++ b/internal/aghnet/net_others.go @@ -1,12 +1,9 @@ -//go:build !(linux || darwin) -// +build !linux,!darwin +//go:build !(linux || darwin || freebsd) +// +build !linux,!darwin,!freebsd package aghnet import ( - "fmt" - "runtime" - "github.com/AdguardTeam/AdGuardHome/internal/aghos" ) @@ -14,10 +11,10 @@ func canBindPrivilegedPorts() (can bool, err error) { return aghos.HaveAdminRights() } -func ifaceHasStaticIP(string) (bool, error) { - return false, fmt.Errorf("cannot check if IP is static: not supported on %s", runtime.GOOS) +func ifaceHasStaticIP(string) (ok bool, err error) { + return false, aghos.Unsupported("checking static ip") } -func ifaceSetStaticIP(string) error { - return fmt.Errorf("cannot set static IP on %s", runtime.GOOS) +func ifaceSetStaticIP(string) (err error) { + return aghos.Unsupported("setting static ip") } diff --git a/internal/dhcpd/http.go b/internal/dhcpd/http.go index aec55c94..4c10bd96 100644 --- a/internal/dhcpd/http.go +++ b/internal/dhcpd/http.go @@ -412,7 +412,9 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque result := dhcpSearchResult{ V4: dhcpSearchV4Result{ OtherServer: dhcpSearchOtherResult{}, - StaticIP: dhcpStaticIPStatus{}, + StaticIP: dhcpStaticIPStatus{ + Static: "yes", + }, }, V6: dhcpSearchV6Result{ OtherServer: dhcpSearchOtherResult{}, diff --git a/internal/dhcpd/server.go b/internal/dhcpd/server.go index 13c3b91b..d326e812 100644 --- a/internal/dhcpd/server.go +++ b/internal/dhcpd/server.go @@ -38,6 +38,9 @@ type V4ServerConf struct { GatewayIP net.IP `yaml:"gateway_ip" json:"gateway_ip"` SubnetMask net.IP `yaml:"subnet_mask" json:"subnet_mask"` + // broadcastIP is the broadcasting address pre-calculated from the + // configured gateway IP and subnet mask. + broadcastIP net.IP // The first & the last IP address for dynamic leases // Bytes [0..2] of the last allowed IP address must match the first IP diff --git a/internal/dhcpd/v4.go b/internal/dhcpd/v4.go index 654e0c0e..2b7c9b25 100644 --- a/internal/dhcpd/v4.go +++ b/internal/dhcpd/v4.go @@ -927,12 +927,30 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4 resp.Options.Update(dhcpv4.OptMessageType(dhcpv4.MessageTypeNak)) } + // peer is expected to be of type *net.UDPConn as the server4.NewServer + // initializes it. + udpPeer, ok := peer.(*net.UDPAddr) + if !ok { + log.Error("dhcpv4: peer is of unexpected type %T", peer) + + return + } + + // Despite the fact that server4.NewIPv4UDPConn explicitly sets socket + // options to allow broadcasting, it also binds the connection to a + // specific interface. On FreeBSD conn.WriteTo causes errors while + // writing to the addresses that belong to another interface. So, use + // the broadcast address specific for the binded interface in case + // server4.Server.Serve sets it to net.IPv4Bcast. + if udpPeer.IP.Equal(net.IPv4bcast) { + udpPeer.IP = s.conf.broadcastIP + } + log.Debug("dhcpv4: sending: %s", resp.Summary()) _, err = conn.WriteTo(resp.ToBytes(), peer) if err != nil { log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err) - return } } @@ -1043,6 +1061,12 @@ func v4Create(conf V4ServerConf) (srv DHCPServer, err error) { Mask: subnetMask, } + bcastIP := aghnet.CloneIP(routerIP) + for i, b := range subnetMask { + bcastIP[i] |= ^b + } + s.conf.broadcastIP = bcastIP + s.conf.ipRange, err = newIPRange(conf.RangeStart, conf.RangeEnd) if err != nil { return s, fmt.Errorf("dhcpv4: %w", err)