Merge branch 'master' into 4990-custom-ciphers
This commit is contained in:
33
internal/aghchan/aghchan.go
Normal file
33
internal/aghchan/aghchan.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Package aghchan contains channel utilities.
|
||||
package aghchan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Receive returns an error if it cannot receive a value form c before timeout
|
||||
// runs out.
|
||||
func Receive[T any](c <-chan T, timeout time.Duration) (v T, ok bool, err error) {
|
||||
var zero T
|
||||
timeoutCh := time.After(timeout)
|
||||
select {
|
||||
case <-timeoutCh:
|
||||
// TODO(a.garipov): Consider implementing [errors.Aser] for
|
||||
// os.ErrTimeout.
|
||||
return zero, false, fmt.Errorf("did not receive after %s", timeout)
|
||||
case v, ok = <-c:
|
||||
return v, ok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// MustReceive panics if it cannot receive a value form c before timeout runs
|
||||
// out.
|
||||
func MustReceive[T any](c <-chan T, timeout time.Duration) (v T, ok bool) {
|
||||
v, ok, err := Receive(c, timeout)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return v, ok
|
||||
}
|
||||
@@ -62,9 +62,16 @@ func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainTe
|
||||
}
|
||||
|
||||
// WriteJSONResponse sets the content-type header in w.Header() to
|
||||
// "application/json", encodes resp to w, calls Error on any returned error, and
|
||||
// returns it as well.
|
||||
// "application/json", writes a header with a "200 OK" status, encodes resp to
|
||||
// w, calls [Error] on any returned error, and returns it as well.
|
||||
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err error) {
|
||||
return WriteJSONResponseCode(w, r, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// WriteJSONResponseCode is like [WriteJSONResponse] but adds the ability to
|
||||
// redefine the status code.
|
||||
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
|
||||
w.WriteHeader(code)
|
||||
w.Header().Set(HdrNameContentType, HdrValApplicationJSON)
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,11 +8,14 @@ package aghhttp
|
||||
const (
|
||||
HdrNameAcceptEncoding = "Accept-Encoding"
|
||||
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||
HdrNameAltSvc = "Alt-Svc"
|
||||
HdrNameContentEncoding = "Content-Encoding"
|
||||
HdrNameContentType = "Content-Type"
|
||||
HdrNameOrigin = "Origin"
|
||||
HdrNameServer = "Server"
|
||||
HdrNameTrailer = "Trailer"
|
||||
HdrNameUserAgent = "User-Agent"
|
||||
HdrNameVary = "Vary"
|
||||
)
|
||||
|
||||
// HTTP header value constants.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
@@ -163,15 +163,9 @@ func TestHostsContainer_refresh(t *testing.T) {
|
||||
checkRefresh := func(t *testing.T, want *HostsRecord) {
|
||||
t.Helper()
|
||||
|
||||
var ok bool
|
||||
var upd *netutil.IPMap
|
||||
select {
|
||||
case upd, ok = <-hc.Upd():
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, upd)
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("did not receive after 1s")
|
||||
}
|
||||
upd, ok := aghchan.MustReceive(hc.Upd(), 1*time.Second)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, upd)
|
||||
|
||||
assert.Equal(t, 1, upd.Len())
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
// errFSOpen.
|
||||
type errFS struct{}
|
||||
|
||||
// errFSOpen is returned from errGlobFS.Open.
|
||||
// errFSOpen is returned from errFS.Open.
|
||||
const errFSOpen errors.Error = "test open error"
|
||||
|
||||
// Open implements the fs.FS interface for *errGlobFS. fsys is always nil and
|
||||
// err is always errFSOpen.
|
||||
// Open implements the fs.FS interface for *errFS. fsys is always nil and err
|
||||
// is always errFSOpen.
|
||||
func (efs *errFS) Open(name string) (fsys fs.File, err error) {
|
||||
return nil, errFSOpen
|
||||
}
|
||||
|
||||
@@ -175,11 +175,21 @@ func RootDirFS() (fsys fs.FS) {
|
||||
return os.DirFS("")
|
||||
}
|
||||
|
||||
// NotifyReconfigureSignal notifies c on receiving reconfigure signals.
|
||||
func NotifyReconfigureSignal(c chan<- os.Signal) {
|
||||
notifyReconfigureSignal(c)
|
||||
}
|
||||
|
||||
// NotifyShutdownSignal notifies c on receiving shutdown signals.
|
||||
func NotifyShutdownSignal(c chan<- os.Signal) {
|
||||
notifyShutdownSignal(c)
|
||||
}
|
||||
|
||||
// IsReconfigureSignal returns true if sig is a reconfigure signal.
|
||||
func IsReconfigureSignal(sig os.Signal) (ok bool) {
|
||||
return isReconfigureSignal(sig)
|
||||
}
|
||||
|
||||
// IsShutdownSignal returns true if sig is a shutdown signal.
|
||||
func IsShutdownSignal(sig os.Signal) (ok bool) {
|
||||
return isShutdownSignal(sig)
|
||||
|
||||
@@ -9,10 +9,18 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func notifyReconfigureSignal(c chan<- os.Signal) {
|
||||
signal.Notify(c, unix.SIGHUP)
|
||||
}
|
||||
|
||||
func notifyShutdownSignal(c chan<- os.Signal) {
|
||||
signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM)
|
||||
}
|
||||
|
||||
func isReconfigureSignal(sig os.Signal) (ok bool) {
|
||||
return sig == unix.SIGHUP
|
||||
}
|
||||
|
||||
func isShutdownSignal(sig os.Signal) (ok bool) {
|
||||
switch sig {
|
||||
case
|
||||
|
||||
@@ -39,12 +39,20 @@ func isOpenWrt() (ok bool) {
|
||||
return false
|
||||
}
|
||||
|
||||
func notifyReconfigureSignal(c chan<- os.Signal) {
|
||||
signal.Notify(c, windows.SIGHUP)
|
||||
}
|
||||
|
||||
func notifyShutdownSignal(c chan<- os.Signal) {
|
||||
// syscall.SIGTERM is processed automatically. See go doc os/signal,
|
||||
// section Windows.
|
||||
signal.Notify(c, os.Interrupt)
|
||||
}
|
||||
|
||||
func isReconfigureSignal(sig os.Signal) (ok bool) {
|
||||
return sig == windows.SIGHUP
|
||||
}
|
||||
|
||||
func isShutdownSignal(sig os.Signal) (ok bool) {
|
||||
switch sig {
|
||||
case
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package aghtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -15,6 +17,8 @@ import (
|
||||
|
||||
// Standard Library
|
||||
|
||||
// Package fs
|
||||
|
||||
// type check
|
||||
var _ fs.FS = &FS{}
|
||||
|
||||
@@ -58,6 +62,8 @@ func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
|
||||
return fsys.OnStat(name)
|
||||
}
|
||||
|
||||
// Package net
|
||||
|
||||
// type check
|
||||
var _ net.Listener = (*Listener)(nil)
|
||||
|
||||
@@ -83,31 +89,9 @@ func (l *Listener) Close() (err error) {
|
||||
return l.OnClose()
|
||||
}
|
||||
|
||||
// Module dnsproxy
|
||||
// Module adguard-home
|
||||
|
||||
// type check
|
||||
var _ upstream.Upstream = (*UpstreamMock)(nil)
|
||||
|
||||
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
|
||||
//
|
||||
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
|
||||
// rename it to just Upstream.
|
||||
type UpstreamMock struct {
|
||||
OnAddress func() (addr string)
|
||||
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
|
||||
}
|
||||
|
||||
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||
func (u *UpstreamMock) Address() (addr string) {
|
||||
return u.OnAddress()
|
||||
}
|
||||
|
||||
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
return u.OnExchange(req)
|
||||
}
|
||||
|
||||
// Module AdGuardHome
|
||||
// Package aghos
|
||||
|
||||
// type check
|
||||
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
||||
@@ -133,3 +117,59 @@ func (w *FSWatcher) Add(name string) (err error) {
|
||||
func (w *FSWatcher) Close() (err error) {
|
||||
return w.OnClose()
|
||||
}
|
||||
|
||||
// Package agh
|
||||
|
||||
// type check
|
||||
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
|
||||
|
||||
// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
|
||||
type ServiceWithConfig[ConfigType any] struct {
|
||||
OnStart func() (err error)
|
||||
OnShutdown func(ctx context.Context) (err error)
|
||||
OnConfig func() (c ConfigType)
|
||||
}
|
||||
|
||||
// Start implements the [agh.ServiceWithConfig] interface for
|
||||
// *ServiceWithConfig.
|
||||
func (s *ServiceWithConfig[_]) Start() (err error) {
|
||||
return s.OnStart()
|
||||
}
|
||||
|
||||
// Shutdown implements the [agh.ServiceWithConfig] interface for
|
||||
// *ServiceWithConfig.
|
||||
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
|
||||
return s.OnShutdown(ctx)
|
||||
}
|
||||
|
||||
// Config implements the [agh.ServiceWithConfig] interface for
|
||||
// *ServiceWithConfig.
|
||||
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||
return s.OnConfig()
|
||||
}
|
||||
|
||||
// Module dnsproxy
|
||||
|
||||
// Package upstream
|
||||
|
||||
// type check
|
||||
var _ upstream.Upstream = (*UpstreamMock)(nil)
|
||||
|
||||
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
|
||||
//
|
||||
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
|
||||
// rename it to just Upstream.
|
||||
type UpstreamMock struct {
|
||||
OnAddress func() (addr string)
|
||||
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
|
||||
}
|
||||
|
||||
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||
func (u *UpstreamMock) Address() (addr string) {
|
||||
return u.OnAddress()
|
||||
}
|
||||
|
||||
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
return u.OnExchange(req)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
package aghtest_test
|
||||
|
||||
import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
)
|
||||
|
||||
// type check
|
||||
var _ aghos.FSWatcher = (*aghtest.FSWatcher)(nil)
|
||||
// Put interface checks that cause import cycles here.
|
||||
|
||||
@@ -4,8 +4,46 @@ package aghtls
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// init makes sure that the cipher name map is filled.
|
||||
//
|
||||
// TODO(a.garipov): Propose a similar API to crypto/tls.
|
||||
func init() {
|
||||
suites := tls.CipherSuites()
|
||||
cipherSuites = make(map[string]uint16, len(suites))
|
||||
for _, s := range suites {
|
||||
cipherSuites[s.Name] = s.ID
|
||||
}
|
||||
|
||||
log.Debug("tls: known ciphers: %q", cipherSuites)
|
||||
}
|
||||
|
||||
// cipherSuites are a name-to-ID mapping of cipher suites from crypto/tls. It
|
||||
// is filled by init. It must not be modified.
|
||||
var cipherSuites map[string]uint16
|
||||
|
||||
// ParseCiphers parses a slice of cipher suites from cipher names.
|
||||
func ParseCiphers(cipherNames []string) (cipherIDs []uint16, err error) {
|
||||
if cipherNames == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cipherIDs = make([]uint16, 0, len(cipherNames))
|
||||
for _, name := range cipherNames {
|
||||
id, ok := cipherSuites[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown cipher %q", name)
|
||||
}
|
||||
|
||||
cipherIDs = append(cipherIDs, id)
|
||||
}
|
||||
|
||||
return cipherIDs, nil
|
||||
}
|
||||
|
||||
// SaferCipherSuites returns a set of default cipher suites with vulnerable and
|
||||
// weak cipher suites removed.
|
||||
func SaferCipherSuites() (safe []uint16) {
|
||||
@@ -31,28 +69,3 @@ func SaferCipherSuites() (safe []uint16) {
|
||||
|
||||
return safe
|
||||
}
|
||||
|
||||
// ParseCipherIDs returns a set of cipher suites with the cipher names provided
|
||||
func ParseCipherIDs(ciphers []string) (userCiphers []uint16, err error) {
|
||||
for _, cipher := range ciphers {
|
||||
exists, cipherID := CipherExists(cipher)
|
||||
if exists {
|
||||
userCiphers = append(userCiphers, cipherID)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown cipher : %s ", cipher)
|
||||
}
|
||||
}
|
||||
|
||||
return userCiphers, nil
|
||||
}
|
||||
|
||||
// CipherExists returns cipherid if exists, else return false in boolean
|
||||
func CipherExists(cipher string) (exists bool, cipherID uint16) {
|
||||
for _, s := range tls.CipherSuites() {
|
||||
if s.Name == cipher {
|
||||
return true, s.ID
|
||||
}
|
||||
}
|
||||
|
||||
return false, 0
|
||||
}
|
||||
|
||||
57
internal/aghtls/aghtls_test.go
Normal file
57
internal/aghtls/aghtls_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package aghtls_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
func TestParseCiphers(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
want []uint16
|
||||
in []string
|
||||
}{{
|
||||
name: "nil",
|
||||
wantErrMsg: "",
|
||||
want: nil,
|
||||
in: nil,
|
||||
}, {
|
||||
name: "empty",
|
||||
wantErrMsg: "",
|
||||
want: []uint16{},
|
||||
in: []string{},
|
||||
}, {}, {
|
||||
name: "one",
|
||||
wantErrMsg: "",
|
||||
want: []uint16{tls.TLS_AES_128_GCM_SHA256},
|
||||
in: []string{"TLS_AES_128_GCM_SHA256"},
|
||||
}, {
|
||||
name: "several",
|
||||
wantErrMsg: "",
|
||||
want: []uint16{tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384},
|
||||
in: []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"},
|
||||
}, {
|
||||
name: "bad",
|
||||
wantErrMsg: `unknown cipher "bad_cipher"`,
|
||||
want: nil,
|
||||
in: []string{"bad_cipher"},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := aghtls.ParseCiphers(tc.in)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
14
internal/aghtls/root.go
Normal file
14
internal/aghtls/root.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package aghtls
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
// SystemRootCAs tries to load root certificates from the operating system. It
|
||||
// returns nil in case nothing is found so that Go' crypto/x509 can use its
|
||||
// default algorithm to find system root CA list.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/1311.
|
||||
func SystemRootCAs() (roots *x509.CertPool) {
|
||||
return rootCAs()
|
||||
}
|
||||
56
internal/aghtls/root_linux.go
Normal file
56
internal/aghtls/root_linux.go
Normal file
@@ -0,0 +1,56 @@
|
||||
//go:build linux
|
||||
|
||||
package aghtls
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
func rootCAs() (roots *x509.CertPool) {
|
||||
// Directories with the system root certificates, which aren't supported by
|
||||
// Go's crypto/x509.
|
||||
dirs := []string{
|
||||
// Entware.
|
||||
"/opt/etc/ssl/certs",
|
||||
}
|
||||
|
||||
roots = x509.NewCertPool()
|
||||
for _, dir := range dirs {
|
||||
dirEnts, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Improve error handling here and in other places.
|
||||
log.Error("aghtls: opening directory %q: %s", dir, err)
|
||||
}
|
||||
|
||||
var rootsAdded bool
|
||||
for _, de := range dirEnts {
|
||||
var certData []byte
|
||||
rootFile := filepath.Join(dir, de.Name())
|
||||
certData, err = os.ReadFile(rootFile)
|
||||
if err != nil {
|
||||
log.Error("aghtls: reading root cert: %s", err)
|
||||
} else {
|
||||
if roots.AppendCertsFromPEM(certData) {
|
||||
rootsAdded = true
|
||||
} else {
|
||||
log.Error("aghtls: could not add root from %q", rootFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rootsAdded {
|
||||
return roots
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
9
internal/aghtls/root_others.go
Normal file
9
internal/aghtls/root_others.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !linux
|
||||
|
||||
package aghtls
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
func rootCAs() (roots *x509.CertPool) {
|
||||
return nil
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -78,18 +79,7 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status.Leases = s.Leases(LeasesDynamic)
|
||||
status.StaticLeases = s.Leases(LeasesStatic)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := json.NewEncoder(w).Encode(status)
|
||||
if err != nil {
|
||||
aghhttp.Error(
|
||||
r,
|
||||
w,
|
||||
http.StatusInternalServerError,
|
||||
"Unable to marshal DHCP status json: %s",
|
||||
err,
|
||||
)
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, status)
|
||||
}
|
||||
|
||||
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
||||
@@ -155,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
|
||||
}
|
||||
|
||||
@@ -246,22 +236,7 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if conf.Enabled != aghalg.NBNull {
|
||||
s.conf.Enabled = conf.Enabled == aghalg.NBTrue
|
||||
}
|
||||
|
||||
if conf.InterfaceName != "" {
|
||||
s.conf.InterfaceName = conf.InterfaceName
|
||||
}
|
||||
|
||||
if srv4 != nil {
|
||||
s.srv4 = srv4
|
||||
}
|
||||
|
||||
if srv6 != nil {
|
||||
s.srv6 = srv6
|
||||
}
|
||||
|
||||
s.setConfFromJSON(conf, srv4, srv6)
|
||||
s.conf.ConfigModified()
|
||||
|
||||
err = s.dbLoad()
|
||||
@@ -280,13 +255,33 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// setConfFromJSON sets configuration parameters in s from the new configuration
|
||||
// decoded from JSON.
|
||||
func (s *server) setConfFromJSON(conf *dhcpServerConfigJSON, srv4, srv6 DHCPServer) {
|
||||
if conf.Enabled != aghalg.NBNull {
|
||||
s.conf.Enabled = conf.Enabled == aghalg.NBTrue
|
||||
}
|
||||
|
||||
if conf.InterfaceName != "" {
|
||||
s.conf.InterfaceName = conf.InterfaceName
|
||||
}
|
||||
|
||||
if srv4 != nil {
|
||||
s.srv4 = srv4
|
||||
}
|
||||
|
||||
if srv6 != nil {
|
||||
s.srv6 = srv6
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -347,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 {
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// jsonError is a generic JSON error response.
|
||||
@@ -25,15 +24,9 @@ type jsonError struct {
|
||||
// TODO(a.garipov): Either take the logger from the server after we've
|
||||
// refactored logging or make this not a method of *Server.
|
||||
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
|
||||
err := json.NewEncoder(w).Encode(&jsonError{
|
||||
_ = aghhttp.WriteJSONResponseCode(w, r, http.StatusNotImplemented, &jsonError{
|
||||
Message: aghos.Unsupported("dhcp").Error(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Debug("writing 501 json response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// registerHandlers sets the handlers for DHCP HTTP API that always respond with
|
||||
|
||||
@@ -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") }()
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -123,7 +123,14 @@ type quicConnection interface {
|
||||
func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
|
||||
proto := pctx.Proto
|
||||
if proto == proxy.ProtoHTTPS {
|
||||
return clientIDFromDNSContextHTTPS(pctx)
|
||||
clientID, err = clientIDFromDNSContextHTTPS(pctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("checking url: %w", err)
|
||||
} else if clientID != "" {
|
||||
return clientID, nil
|
||||
}
|
||||
|
||||
// Go on and check the domain name as well.
|
||||
} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
|
||||
return "", nil
|
||||
}
|
||||
@@ -133,31 +140,9 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
||||
return "", nil
|
||||
}
|
||||
|
||||
cliSrvName := ""
|
||||
switch proto {
|
||||
case proxy.ProtoTLS:
|
||||
conn := pctx.Conn
|
||||
tc, ok := conn.(tlsConn)
|
||||
if !ok {
|
||||
return "", fmt.Errorf(
|
||||
"proxy ctx conn of proto %s is %T, want *tls.Conn",
|
||||
proto,
|
||||
conn,
|
||||
)
|
||||
}
|
||||
|
||||
cliSrvName = tc.ConnectionState().ServerName
|
||||
case proxy.ProtoQUIC:
|
||||
conn, ok := pctx.QUICConnection.(quicConnection)
|
||||
if !ok {
|
||||
return "", fmt.Errorf(
|
||||
"proxy ctx quic conn of proto %s is %T, want quic.Connection",
|
||||
proto,
|
||||
pctx.QUICConnection,
|
||||
)
|
||||
}
|
||||
|
||||
cliSrvName = conn.ConnectionState().TLS.ServerName
|
||||
cliSrvName, err := clientServerName(pctx, proto)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
clientID, err = clientIDFromClientServerName(
|
||||
@@ -171,3 +156,35 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
||||
|
||||
return clientID, nil
|
||||
}
|
||||
|
||||
// clientServerName returns the TLS server name based on the protocol.
|
||||
func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string, err error) {
|
||||
switch proto {
|
||||
case proxy.ProtoHTTPS:
|
||||
if connState := pctx.HTTPRequest.TLS; connState != nil {
|
||||
srvName = pctx.HTTPRequest.TLS.ServerName
|
||||
}
|
||||
case proxy.ProtoQUIC:
|
||||
qConn := pctx.QUICConnection
|
||||
conn, ok := qConn.(quicConnection)
|
||||
if !ok {
|
||||
return "", fmt.Errorf(
|
||||
"proxy ctx quic conn of proto %s is %T, want quic.Connection",
|
||||
proto,
|
||||
qConn,
|
||||
)
|
||||
}
|
||||
|
||||
srvName = conn.ConnectionState().TLS.ServerName
|
||||
case proxy.ProtoTLS:
|
||||
conn := pctx.Conn
|
||||
tc, ok := conn.(tlsConn)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("proxy ctx conn of proto %s is %T, want *tls.Conn", proto, conn)
|
||||
}
|
||||
|
||||
srvName = tc.ConnectionState().ServerName
|
||||
}
|
||||
|
||||
return srvName, nil
|
||||
}
|
||||
|
||||
@@ -160,6 +160,22 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
wantClientID: "insensitive",
|
||||
wantErrMsg: ``,
|
||||
strictSNI: true,
|
||||
}, {
|
||||
name: "https_no_clientid",
|
||||
proto: proxy.ProtoHTTPS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
strictSNI: true,
|
||||
}, {
|
||||
name: "https_clientid",
|
||||
proto: proxy.ProtoHTTPS,
|
||||
hostSrvName: "example.com",
|
||||
cliSrvName: "cli.example.com",
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
strictSNI: true,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -173,16 +189,32 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
conf: ServerConfig{TLSConfig: tlsConf},
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
if tc.proto == proxy.ProtoTLS {
|
||||
conn = testTLSConn{
|
||||
var (
|
||||
conn net.Conn
|
||||
qconn quic.Connection
|
||||
httpReq *http.Request
|
||||
)
|
||||
|
||||
switch tc.proto {
|
||||
case proxy.ProtoHTTPS:
|
||||
u := &url.URL{
|
||||
Path: "/dns-query",
|
||||
}
|
||||
|
||||
connState := &tls.ConnectionState{
|
||||
ServerName: tc.cliSrvName,
|
||||
}
|
||||
|
||||
httpReq = &http.Request{
|
||||
URL: u,
|
||||
TLS: connState,
|
||||
}
|
||||
case proxy.ProtoQUIC:
|
||||
qconn = testQUICConnection{
|
||||
serverName: tc.cliSrvName,
|
||||
}
|
||||
}
|
||||
|
||||
var qconn quic.Connection
|
||||
if tc.proto == proxy.ProtoQUIC {
|
||||
qconn = testQUICConnection{
|
||||
case proxy.ProtoTLS:
|
||||
conn = testTLSConn{
|
||||
serverName: tc.cliSrvName,
|
||||
}
|
||||
}
|
||||
@@ -190,6 +222,7 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
pctx := &proxy.DNSContext{
|
||||
Proto: tc.proto,
|
||||
Conn: conn,
|
||||
HTTPRequest: httpReq,
|
||||
QUICConnection: qconn,
|
||||
}
|
||||
|
||||
@@ -205,56 +238,76 @@ func TestClientIDFromDNSContextHTTPS(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
cliSrvName string
|
||||
wantClientID string
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "no_clientid",
|
||||
path: "/dns-query",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "no_clientid_slash",
|
||||
path: "/dns-query/",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "clientid",
|
||||
path: "/dns-query/cli",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "clientid_slash",
|
||||
path: "/dns-query/cli/",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "cli",
|
||||
wantErrMsg: "",
|
||||
}, {
|
||||
name: "clientid_case",
|
||||
path: "/dns-query/InSeNsItIvE",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "insensitive",
|
||||
wantErrMsg: ``,
|
||||
}, {
|
||||
name: "bad_url",
|
||||
path: "/foo",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "",
|
||||
wantErrMsg: `clientid check: invalid path "/foo"`,
|
||||
}, {
|
||||
name: "extra",
|
||||
path: "/dns-query/cli/foo",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "",
|
||||
wantErrMsg: `clientid check: invalid path "/dns-query/cli/foo": extra parts`,
|
||||
}, {
|
||||
name: "invalid_clientid",
|
||||
path: "/dns-query/!!!",
|
||||
cliSrvName: "example.com",
|
||||
wantClientID: "",
|
||||
wantErrMsg: `clientid check: invalid clientid "!!!": bad domain name label rune '!'`,
|
||||
}, {
|
||||
name: "both_ids",
|
||||
path: "/dns-query/right",
|
||||
cliSrvName: "wrong.example.com",
|
||||
wantClientID: "right",
|
||||
wantErrMsg: "",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
connState := &tls.ConnectionState{
|
||||
ServerName: tc.cliSrvName,
|
||||
}
|
||||
|
||||
r := &http.Request{
|
||||
URL: &url.URL{
|
||||
Path: tc.path,
|
||||
},
|
||||
TLS: connState,
|
||||
}
|
||||
|
||||
pctx := &proxy.DNSContext{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -453,13 +453,7 @@ func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleBlockedServicesAvailableServices(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(serviceIDs)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding available services: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, serviceIDs)
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -467,13 +461,7 @@ func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Req
|
||||
list := d.Config.BlockedServices
|
||||
d.confLock.RUnlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(list)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding services: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, list)
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -301,14 +301,7 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
type filterJSON struct {
|
||||
@@ -361,17 +354,7 @@ func (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request
|
||||
resp.UserRules = d.UserRules
|
||||
d.filtersMu.RUnlock()
|
||||
|
||||
jsonVal, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(jsonVal)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "http write: %s", err)
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
// Set filtering configuration
|
||||
@@ -473,11 +456,7 @@ func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding response: %s", err)
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
// RegisterFilteringHandlers - register handlers
|
||||
|
||||
@@ -240,13 +240,7 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
d.confLock.Unlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(arr)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Encode: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, arr)
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -381,17 +380,13 @@ func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(&struct {
|
||||
resp := &struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}{
|
||||
Enabled: d.Config.SafeBrowsingEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -405,13 +400,11 @@ func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(&struct {
|
||||
resp := &struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}{
|
||||
Enabled: d.Config.ParentalEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||
}
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -146,21 +145,13 @@ func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(&struct {
|
||||
resp := &struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}{
|
||||
Enabled: d.Config.SafeSearchEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
aghhttp.Error(
|
||||
r,
|
||||
w,
|
||||
http.StatusInternalServerError,
|
||||
"Unable to write response json: %s",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
var safeSearchDomains = map[string]string{
|
||||
|
||||
@@ -12,16 +12,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
func TestNewSessionToken(t *testing.T) {
|
||||
// Successful case.
|
||||
token, err := newSessionToken()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -236,6 +253,7 @@ var config = &configuration{
|
||||
},
|
||||
|
||||
TrustedProxies: []string{"127.0.0.0/8", "::1/128"},
|
||||
CacheSize: 4 * 1024 * 1024,
|
||||
|
||||
// set default maximum concurrent queries to 300
|
||||
// we introduced a default limit due to this:
|
||||
|
||||
@@ -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 {
|
||||
@@ -320,6 +322,28 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||
return false
|
||||
}
|
||||
|
||||
var serveHTTP3 bool
|
||||
var portHTTPS int
|
||||
func() {
|
||||
config.RLock()
|
||||
defer config.RUnlock()
|
||||
|
||||
serveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS
|
||||
}()
|
||||
|
||||
respHdr := w.Header()
|
||||
|
||||
// Let the browser know that server supports HTTP/3.
|
||||
//
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Alt-Svc.
|
||||
//
|
||||
// TODO(a.garipov): Consider adding a configurable max-age. Currently, the
|
||||
// default is 24 hours.
|
||||
if serveHTTP3 {
|
||||
altSvc := fmt.Sprintf(`h3=":%d"`, portHTTPS)
|
||||
respHdr.Set(aghhttp.HdrNameAltSvc, altSvc)
|
||||
}
|
||||
|
||||
if r.TLS == nil && web.forceHTTPS {
|
||||
hostPort := host
|
||||
if port := web.conf.PortHTTPS; port != defaultPortHTTPS {
|
||||
@@ -346,8 +370,9 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||
Scheme: aghhttp.SchemeHTTP,
|
||||
Host: r.Host,
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", originURL.String())
|
||||
w.Header().Set("Vary", "Origin")
|
||||
|
||||
respHdr.Set(aghhttp.HdrNameAccessControlAllowOrigin, originURL.String())
|
||||
respHdr.Set(aghhttp.HdrNameVary, aghhttp.HdrNameOrigin)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -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,14 +417,14 @@ 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
|
||||
// moment we'll allow setting up TLS in the initial configuration or the
|
||||
// configuration itself will use HTTPS protocol, because the underlying
|
||||
// functions potentially restart the HTTPS server.
|
||||
err = StartMods()
|
||||
err = startMods()
|
||||
if err != nil {
|
||||
Context.firstRun = true
|
||||
copyInstallSettings(config, curConfig)
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -58,7 +59,7 @@ type homeContext struct {
|
||||
auth *Auth // HTTP authentication module
|
||||
filters *filtering.DNSFilter // DNS filtering module
|
||||
web *Web // Web (HTTP, HTTPS) module
|
||||
tls *TLSMod // TLS module
|
||||
tls *tlsManager // TLS module
|
||||
// etcHosts is an IP-hostname pairs set taken from system configuration
|
||||
// (e.g. /etc/hosts) files.
|
||||
etcHosts *aghnet.HostsContainer
|
||||
@@ -97,9 +98,15 @@ var Context homeContext
|
||||
|
||||
// Main is the entry point
|
||||
func Main(clientBuildFS fs.FS) {
|
||||
// config can be specified, which reads options from there, but other command line flags have to override config values
|
||||
// therefore, we must do it manually instead of using a lib
|
||||
args := loadOptions()
|
||||
initCmdLineOpts()
|
||||
|
||||
// The configuration file path can be overridden, but other command-line
|
||||
// options have to override config values. Therefore, do it manually
|
||||
// instead of using package flag.
|
||||
//
|
||||
// TODO(a.garipov): The comment above is most likely false. Replace with
|
||||
// package flag.
|
||||
opts := loadCmdLineOpts()
|
||||
|
||||
Context.appSignalChannel = make(chan os.Signal)
|
||||
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||
@@ -110,7 +117,7 @@ func Main(clientBuildFS fs.FS) {
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
Context.clients.Reload()
|
||||
Context.tls.Reload()
|
||||
Context.tls.reload()
|
||||
|
||||
default:
|
||||
cleanup(context.Background())
|
||||
@@ -120,26 +127,18 @@ func Main(clientBuildFS fs.FS) {
|
||||
}
|
||||
}()
|
||||
|
||||
if args.serviceControlAction != "" {
|
||||
handleServiceControlAction(args, clientBuildFS)
|
||||
if opts.serviceControlAction != "" {
|
||||
handleServiceControlAction(opts, clientBuildFS)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// run the protection
|
||||
run(args, clientBuildFS)
|
||||
run(opts, clientBuildFS)
|
||||
}
|
||||
|
||||
func setupContext(args options) {
|
||||
Context.runningAsService = args.runningAsService
|
||||
Context.disableUpdate = args.disableUpdate ||
|
||||
version.Channel() == version.ChannelDevelopment
|
||||
|
||||
Context.firstRun = detectFirstRun()
|
||||
if Context.firstRun {
|
||||
log.Info("This is the first time AdGuard Home is launched")
|
||||
checkPermissions()
|
||||
}
|
||||
func setupContext(opts options) {
|
||||
setupContextFlags(opts)
|
||||
|
||||
switch version.Channel() {
|
||||
case version.ChannelEdge, version.ChannelDevelopment:
|
||||
@@ -148,7 +147,7 @@ func setupContext(args options) {
|
||||
// Go on.
|
||||
}
|
||||
|
||||
Context.tlsRoots = LoadSystemRootCAs()
|
||||
Context.tlsRoots = aghtls.SystemRootCAs()
|
||||
Context.transport = &http.Transport{
|
||||
DialContext: customDialContext,
|
||||
Proxy: getHTTPProxy,
|
||||
@@ -174,13 +173,13 @@ func setupContext(args options) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if args.checkConfig {
|
||||
if opts.checkConfig {
|
||||
log.Info("configuration file is ok")
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if !args.noEtcHosts && config.Clients.Sources.HostsFile {
|
||||
if !opts.noEtcHosts && config.Clients.Sources.HostsFile {
|
||||
err = setupHostsContainer()
|
||||
fatalOnError(err)
|
||||
}
|
||||
@@ -189,6 +188,24 @@ func setupContext(args options) {
|
||||
Context.mux = http.NewServeMux()
|
||||
}
|
||||
|
||||
// setupContextFlags sets global flags and prints their status to the log.
|
||||
func setupContextFlags(opts options) {
|
||||
Context.firstRun = detectFirstRun()
|
||||
if Context.firstRun {
|
||||
log.Info("This is the first time AdGuard Home is launched")
|
||||
checkPermissions()
|
||||
}
|
||||
|
||||
Context.runningAsService = opts.runningAsService
|
||||
// Don't print the runningAsService flag, since that has already been done
|
||||
// in [run].
|
||||
|
||||
Context.disableUpdate = opts.disableUpdate || version.Channel() == version.ChannelDevelopment
|
||||
if Context.disableUpdate {
|
||||
log.Info("AdGuard Home updates are disabled")
|
||||
}
|
||||
}
|
||||
|
||||
// logIfUnsupported logs a formatted warning if the error is one of the
|
||||
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
|
||||
// nil. Otherwise, it returns err.
|
||||
@@ -270,7 +287,7 @@ func setupHostsContainer() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupConfig(args options) (err error) {
|
||||
func setupConfig(opts options) (err error) {
|
||||
config.DNS.DnsfilterConf.EtcHosts = Context.etcHosts
|
||||
config.DNS.DnsfilterConf.ConfigModified = onConfigModified
|
||||
config.DNS.DnsfilterConf.HTTPRegister = httpRegister
|
||||
@@ -312,9 +329,9 @@ func setupConfig(args options) (err error) {
|
||||
|
||||
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
|
||||
|
||||
if args.bindPort != 0 {
|
||||
if opts.bindPort != 0 {
|
||||
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
||||
addPorts(tcpPorts, tcpPort(args.bindPort), tcpPort(config.BetaBindPort))
|
||||
addPorts(tcpPorts, tcpPort(opts.bindPort), tcpPort(config.BetaBindPort))
|
||||
|
||||
udpPorts := aghalg.UniqChecker[udpPort]{}
|
||||
addPorts(udpPorts, udpPort(config.DNS.Port))
|
||||
@@ -336,23 +353,23 @@ func setupConfig(args options) (err error) {
|
||||
return fmt.Errorf("validating udp ports: %w", err)
|
||||
}
|
||||
|
||||
config.BindPort = args.bindPort
|
||||
config.BindPort = opts.bindPort
|
||||
}
|
||||
|
||||
// override bind host/port from the console
|
||||
if args.bindHost != nil {
|
||||
config.BindHost = args.bindHost
|
||||
if opts.bindHost.IsValid() {
|
||||
config.BindHost = opts.bindHost
|
||||
}
|
||||
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
|
||||
Context.pidFileName = args.pidFile
|
||||
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
|
||||
Context.pidFileName = opts.pidFile
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
|
||||
func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
|
||||
var clientFS, clientBetaFS fs.FS
|
||||
if args.localFrontend {
|
||||
if opts.localFrontend {
|
||||
log.Info("warning: using local frontend files")
|
||||
|
||||
clientFS = os.DirFS("build/static")
|
||||
@@ -406,24 +423,24 @@ func fatalOnError(err error) {
|
||||
}
|
||||
|
||||
// run configures and starts AdGuard Home.
|
||||
func run(args options, clientBuildFS fs.FS) {
|
||||
func run(opts options, clientBuildFS fs.FS) {
|
||||
// configure config filename
|
||||
initConfigFilename(args)
|
||||
initConfigFilename(opts)
|
||||
|
||||
// configure working dir and config path
|
||||
initWorkingDir(args)
|
||||
initWorkingDir(opts)
|
||||
|
||||
// configure log level and output
|
||||
configureLogger(args)
|
||||
configureLogger(opts)
|
||||
|
||||
// Print the first message after logger is configured.
|
||||
log.Info(version.Full())
|
||||
log.Debug("current working directory is %s", Context.workDir)
|
||||
if args.runningAsService {
|
||||
if opts.runningAsService {
|
||||
log.Info("AdGuard Home is running as a service")
|
||||
}
|
||||
|
||||
setupContext(args)
|
||||
setupContext(opts)
|
||||
|
||||
err := configureOS(config)
|
||||
fatalOnError(err)
|
||||
@@ -433,7 +450,7 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
// but also avoid relying on automatic Go init() function
|
||||
filtering.InitModule()
|
||||
|
||||
err = setupConfig(args)
|
||||
err = setupConfig(opts)
|
||||
fatalOnError(err)
|
||||
|
||||
if !Context.firstRun {
|
||||
@@ -462,7 +479,7 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
}
|
||||
|
||||
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
||||
GLMode = args.glinetMode
|
||||
GLMode = opts.glinetMode
|
||||
var rateLimiter *authRateLimiter
|
||||
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
|
||||
rateLimiter = newAuthRateLimiter(
|
||||
@@ -484,19 +501,19 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
}
|
||||
config.Users = nil
|
||||
|
||||
Context.tls = tlsCreate(config.TLS)
|
||||
if Context.tls == nil {
|
||||
log.Fatalf("Can't initialize TLS module")
|
||||
Context.tls, err = newTLSManager(config.TLS)
|
||||
if err != nil {
|
||||
log.Fatalf("initializing tls: %s", err)
|
||||
}
|
||||
|
||||
Context.web, err = initWeb(args, clientBuildFS)
|
||||
Context.web, err = initWeb(opts, clientBuildFS)
|
||||
fatalOnError(err)
|
||||
|
||||
if !Context.firstRun {
|
||||
err = initDNSServer()
|
||||
fatalOnError(err)
|
||||
|
||||
Context.tls.Start()
|
||||
Context.tls.start()
|
||||
|
||||
go func() {
|
||||
serr := startDNSServer()
|
||||
@@ -520,20 +537,22 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
select {}
|
||||
}
|
||||
|
||||
// StartMods initializes and starts the DNS server after installation.
|
||||
func StartMods() error {
|
||||
// startMods initializes and starts the DNS server after installation.
|
||||
func startMods() error {
|
||||
err := initDNSServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Context.tls.Start()
|
||||
Context.tls.start()
|
||||
|
||||
err = startDNSServer()
|
||||
if err != nil {
|
||||
closeDNSServer()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -546,7 +565,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.
|
||||
@@ -581,10 +600,10 @@ func writePIDFile(fn string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func initConfigFilename(args options) {
|
||||
func initConfigFilename(opts options) {
|
||||
// config file path can be overridden by command-line arguments:
|
||||
if args.configFilename != "" {
|
||||
Context.configFilename = args.configFilename
|
||||
if opts.confFilename != "" {
|
||||
Context.configFilename = opts.confFilename
|
||||
} else {
|
||||
// Default config file name
|
||||
Context.configFilename = "AdGuardHome.yaml"
|
||||
@@ -593,15 +612,15 @@ func initConfigFilename(args options) {
|
||||
|
||||
// initWorkingDir initializes the workDir
|
||||
// if no command-line arguments specified, we use the directory where our binary file is located
|
||||
func initWorkingDir(args options) {
|
||||
func initWorkingDir(opts options) {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if args.workDir != "" {
|
||||
if opts.workDir != "" {
|
||||
// If there is a custom config file, use it's directory as our working dir
|
||||
Context.workDir = args.workDir
|
||||
Context.workDir = opts.workDir
|
||||
} else {
|
||||
Context.workDir = filepath.Dir(execPath)
|
||||
}
|
||||
@@ -615,15 +634,15 @@ func initWorkingDir(args options) {
|
||||
}
|
||||
|
||||
// configureLogger configures logger level and output
|
||||
func configureLogger(args options) {
|
||||
func configureLogger(opts options) {
|
||||
ls := getLogSettings()
|
||||
|
||||
// command-line arguments can override config settings
|
||||
if args.verbose || config.Verbose {
|
||||
if opts.verbose || config.Verbose {
|
||||
ls.Verbose = true
|
||||
}
|
||||
if args.logFile != "" {
|
||||
ls.File = args.logFile
|
||||
if opts.logFile != "" {
|
||||
ls.File = opts.logFile
|
||||
} else if config.File != "" {
|
||||
ls.File = config.File
|
||||
}
|
||||
@@ -644,7 +663,7 @@ func configureLogger(args options) {
|
||||
// happen pretty quickly.
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
|
||||
if args.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
|
||||
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
|
||||
// When running as a Windows service, use eventlog by default if nothing
|
||||
// else is configured. Otherwise, we'll simply lose the log output.
|
||||
ls.File = configSyslog
|
||||
@@ -717,7 +736,6 @@ func cleanup(ctx context.Context) {
|
||||
}
|
||||
|
||||
if Context.tls != nil {
|
||||
Context.tls.Close()
|
||||
Context.tls = nil
|
||||
}
|
||||
}
|
||||
@@ -727,32 +745,37 @@ func cleanupAlways() {
|
||||
if len(Context.pidFileName) != 0 {
|
||||
_ = os.Remove(Context.pidFileName)
|
||||
}
|
||||
log.Info("Stopped")
|
||||
|
||||
log.Info("stopped")
|
||||
}
|
||||
|
||||
func exitWithError() {
|
||||
os.Exit(64)
|
||||
}
|
||||
|
||||
// loadOptions reads command line arguments and initializes configuration
|
||||
func loadOptions() options {
|
||||
o, f, err := parse(os.Args[0], os.Args[1:])
|
||||
|
||||
// loadCmdLineOpts reads command line arguments and initializes configuration
|
||||
// from them. If there is an error or an effect, loadCmdLineOpts processes them
|
||||
// and exits.
|
||||
func loadCmdLineOpts() (opts options) {
|
||||
opts, eff, err := parseCmdOpts(os.Args[0], os.Args[1:])
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
_ = printHelp(os.Args[0])
|
||||
printHelp(os.Args[0])
|
||||
|
||||
exitWithError()
|
||||
} else if f != nil {
|
||||
err = f()
|
||||
}
|
||||
|
||||
if eff != nil {
|
||||
err = eff()
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
exitWithError()
|
||||
} else {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return o
|
||||
return opts
|
||||
}
|
||||
|
||||
// printWebAddrs prints addresses built from proto, addr, and an appropriate
|
||||
@@ -901,6 +924,6 @@ func getTLSCiphers() (cipherIds []uint16, err error) {
|
||||
return aghtls.SaferCipherSuites(), nil
|
||||
} else {
|
||||
log.Info("Overriding TLS Ciphers : %s", config.TLS.OverrideTLSCiphers)
|
||||
return aghtls.ParseCipherIDs(config.TLS.OverrideTLSCiphers)
|
||||
return aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
|
||||
}
|
||||
}
|
||||
|
||||
12
internal/home/home_test.go
Normal file
12
internal/home/home_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
initCmdLineOpts()
|
||||
}
|
||||
@@ -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,12 +27,12 @@ func setupDNSIPs(t testing.TB) {
|
||||
|
||||
config = &configuration{
|
||||
DNS: dnsConfig{
|
||||
BindHosts: []net.IP{netutil.IPv4Zero()},
|
||||
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
|
||||
Port: defaultPortDNS,
|
||||
},
|
||||
}
|
||||
|
||||
Context.tls = &TLSMod{}
|
||||
Context.tls = &tlsManager{}
|
||||
}
|
||||
|
||||
func TestHandleMobileConfigDoH(t *testing.T) {
|
||||
@@ -66,7 +65,7 @@ func TestHandleMobileConfigDoH(t *testing.T) {
|
||||
oldTLSConf := Context.tls
|
||||
t.Cleanup(func() { Context.tls = oldTLSConf })
|
||||
|
||||
Context.tls = &TLSMod{conf: tlsConfigSettings{}}
|
||||
Context.tls = &tlsManager{conf: tlsConfigSettings{}}
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil)
|
||||
require.NoError(t, err)
|
||||
@@ -138,7 +137,7 @@ func TestHandleMobileConfigDoT(t *testing.T) {
|
||||
oldTLSConf := Context.tls
|
||||
t.Cleanup(func() { Context.tls = oldTLSConf })
|
||||
|
||||
Context.tls = &TLSMod{conf: tlsConfigSettings{}}
|
||||
Context.tls = &tlsManager{conf: tlsConfigSettings{}}
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -2,33 +2,63 @@ package home
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
)
|
||||
|
||||
// options passed from command-line arguments
|
||||
type options struct {
|
||||
verbose bool // is verbose logging enabled
|
||||
configFilename string // path to the config file
|
||||
workDir string // path to the working directory where we will store the filters data and the querylog
|
||||
bindHost net.IP // host address to bind HTTP server on
|
||||
bindPort int // port to serve HTTP pages on
|
||||
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
||||
pidFile string // File name to save PID to
|
||||
checkConfig bool // Check configuration and exit
|
||||
disableUpdate bool // If set, don't check for updates
|
||||
// TODO(a.garipov): Replace with package flag.
|
||||
|
||||
// service control action (see service.ControlAction array + "status" command)
|
||||
// options represents the command-line options.
|
||||
type options struct {
|
||||
// confFilename is the path to the configuration file.
|
||||
confFilename string
|
||||
|
||||
// workDir is the path to the working directory where AdGuard Home stores
|
||||
// filter data, the query log, and other data.
|
||||
workDir string
|
||||
|
||||
// logFile is the path to the log file. If empty, AdGuard Home writes to
|
||||
// stdout; if "syslog", to syslog.
|
||||
logFile string
|
||||
|
||||
// pidFile is the file name for the file to which the PID is saved.
|
||||
pidFile string
|
||||
|
||||
// serviceControlAction is the service action to perform. See
|
||||
// [service.ControlAction] and [handleServiceControlAction].
|
||||
serviceControlAction string
|
||||
|
||||
// runningAsService flag is set to true when options are passed from the service runner
|
||||
// bindHost is the address on which to serve the HTTP UI.
|
||||
bindHost netip.Addr
|
||||
|
||||
// bindPort is the port on which to serve the HTTP UI.
|
||||
bindPort int
|
||||
|
||||
// checkConfig is true if the current invocation is only required to check
|
||||
// the configuration file and exit.
|
||||
checkConfig bool
|
||||
|
||||
// disableUpdate, if set, makes AdGuard Home not check for updates.
|
||||
disableUpdate bool
|
||||
|
||||
// verbose shows if verbose logging is enabled.
|
||||
verbose bool
|
||||
|
||||
// runningAsService flag is set to true when options are passed from the
|
||||
// service runner
|
||||
//
|
||||
// TODO(a.garipov): Perhaps this could be determined by a non-empty
|
||||
// serviceControlAction?
|
||||
runningAsService bool
|
||||
|
||||
glinetMode bool // Activate GL-Inet compatibility mode
|
||||
// glinetMode shows if the GL-Inet compatibility mode is enabled.
|
||||
glinetMode bool
|
||||
|
||||
// noEtcHosts flag should be provided when /etc/hosts file shouldn't be
|
||||
// used.
|
||||
@@ -39,88 +69,86 @@ type options struct {
|
||||
localFrontend bool
|
||||
}
|
||||
|
||||
// functions used for their side-effects
|
||||
type effect func() error
|
||||
|
||||
type arg struct {
|
||||
description string // a short, English description of the argument
|
||||
longName string // the name of the argument used after '--'
|
||||
shortName string // the name of the argument used after '-'
|
||||
|
||||
// only one of updateWithValue, updateNoValue, and effect should be present
|
||||
|
||||
updateWithValue func(o options, v string) (options, error) // the mutator for arguments with parameters
|
||||
updateNoValue func(o options) (options, error) // the mutator for arguments without parameters
|
||||
effect func(o options, exec string) (f effect, err error) // the side-effect closure generator
|
||||
|
||||
serialize func(o options) []string // the re-serialization function back to arguments (return nil for omit)
|
||||
// initCmdLineOpts completes initialization of the global command-line option
|
||||
// slice. It must only be called once.
|
||||
func initCmdLineOpts() {
|
||||
// The --help option cannot be put directly into cmdLineOpts, because that
|
||||
// causes initialization cycle due to printHelp referencing cmdLineOpts.
|
||||
cmdLineOpts = append(cmdLineOpts, cmdLineOpt{
|
||||
updateWithValue: nil,
|
||||
updateNoValue: nil,
|
||||
effect: func(o options, exec string) (effect, error) {
|
||||
return func() error { printHelp(exec); exitWithError(); return nil }, nil
|
||||
},
|
||||
serialize: func(o options) (val string, ok bool) { return "", false },
|
||||
description: "Print this help.",
|
||||
longName: "help",
|
||||
shortName: "",
|
||||
})
|
||||
}
|
||||
|
||||
// {type}SliceOrNil functions check their parameter of type {type}
|
||||
// against its zero value and return nil if the parameter value is
|
||||
// zero otherwise they return a string slice of the parameter
|
||||
// effect is the type for functions used for their side-effects.
|
||||
type effect func() (err error)
|
||||
|
||||
func ipSliceOrNil(ip net.IP) []string {
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
// cmdLineOpt contains the data for a single command-line option. Only one of
|
||||
// updateWithValue, updateNoValue, and effect must be present.
|
||||
type cmdLineOpt struct {
|
||||
updateWithValue func(o options, v string) (updated options, err error)
|
||||
updateNoValue func(o options) (updated options, err error)
|
||||
effect func(o options, exec string) (eff effect, err error)
|
||||
|
||||
return []string{ip.String()}
|
||||
// serialize is a function that encodes the option into a slice of
|
||||
// command-line arguments, if necessary. If ok is false, this option should
|
||||
// be skipped.
|
||||
serialize func(o options) (val string, ok bool)
|
||||
|
||||
description string
|
||||
longName string
|
||||
shortName string
|
||||
}
|
||||
|
||||
func stringSliceOrNil(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
// cmdLineOpts are all command-line options of AdGuard Home.
|
||||
var cmdLineOpts = []cmdLineOpt{{
|
||||
updateWithValue: func(o options, v string) (options, error) {
|
||||
o.confFilename = v
|
||||
return o, nil
|
||||
},
|
||||
updateNoValue: nil,
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) {
|
||||
return o.confFilename, o.confFilename != ""
|
||||
},
|
||||
description: "Path to the config file.",
|
||||
longName: "config",
|
||||
shortName: "c",
|
||||
}, {
|
||||
updateWithValue: func(o options, v string) (options, error) { o.workDir = v; return o, nil },
|
||||
updateNoValue: nil,
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) { return o.workDir, o.workDir != "" },
|
||||
description: "Path to the working directory.",
|
||||
longName: "work-dir",
|
||||
shortName: "w",
|
||||
}, {
|
||||
updateWithValue: func(o options, v string) (oo options, err error) {
|
||||
o.bindHost, err = netip.ParseAddr(v)
|
||||
|
||||
return []string{s}
|
||||
}
|
||||
return o, err
|
||||
},
|
||||
updateNoValue: nil,
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) {
|
||||
if !o.bindHost.IsValid() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func intSliceOrNil(i int) []string {
|
||||
if i == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{strconv.Itoa(i)}
|
||||
}
|
||||
|
||||
func boolSliceOrNil(b bool) []string {
|
||||
if b {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var args []arg
|
||||
|
||||
var configArg = arg{
|
||||
"Path to the config file.",
|
||||
"config", "c",
|
||||
func(o options, v string) (options, error) { o.configFilename = v; return o, nil },
|
||||
nil,
|
||||
nil,
|
||||
func(o options) []string { return stringSliceOrNil(o.configFilename) },
|
||||
}
|
||||
|
||||
var workDirArg = arg{
|
||||
"Path to the working directory.",
|
||||
"work-dir", "w",
|
||||
func(o options, v string) (options, error) { o.workDir = v; return o, nil }, nil, nil,
|
||||
func(o options) []string { return stringSliceOrNil(o.workDir) },
|
||||
}
|
||||
|
||||
var hostArg = arg{
|
||||
"Host address to bind HTTP server on.",
|
||||
"host", "h",
|
||||
func(o options, v string) (options, error) { o.bindHost = net.ParseIP(v); return o, nil }, nil, nil,
|
||||
func(o options) []string { return ipSliceOrNil(o.bindHost) },
|
||||
}
|
||||
|
||||
var portArg = arg{
|
||||
"Port to serve HTTP pages on.",
|
||||
"port", "p",
|
||||
func(o options, v string) (options, error) {
|
||||
return o.bindHost.String(), true
|
||||
},
|
||||
description: "Host address to bind HTTP server on.",
|
||||
longName: "host",
|
||||
shortName: "h",
|
||||
}, {
|
||||
updateWithValue: func(o options, v string) (options, error) {
|
||||
var err error
|
||||
var p int
|
||||
minPort, maxPort := 0, 1<<16-1
|
||||
@@ -131,108 +159,81 @@ var portArg = arg{
|
||||
} else {
|
||||
o.bindPort = p
|
||||
}
|
||||
return o, err
|
||||
}, nil, nil,
|
||||
func(o options) []string { return intSliceOrNil(o.bindPort) },
|
||||
}
|
||||
|
||||
var serviceArg = arg{
|
||||
"Service control action: status, install, uninstall, start, stop, restart, reload (configuration).",
|
||||
"service", "s",
|
||||
func(o options, v string) (options, error) {
|
||||
return o, err
|
||||
},
|
||||
updateNoValue: nil,
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) {
|
||||
if o.bindPort == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return strconv.Itoa(o.bindPort), true
|
||||
},
|
||||
description: "Port to serve HTTP pages on.",
|
||||
longName: "port",
|
||||
shortName: "p",
|
||||
}, {
|
||||
updateWithValue: func(o options, v string) (options, error) {
|
||||
o.serviceControlAction = v
|
||||
return o, nil
|
||||
}, nil, nil,
|
||||
func(o options) []string { return stringSliceOrNil(o.serviceControlAction) },
|
||||
}
|
||||
|
||||
var logfileArg = arg{
|
||||
"Path to log file. If empty: write to stdout; if 'syslog': write to system log.",
|
||||
"logfile", "l",
|
||||
func(o options, v string) (options, error) { o.logFile = v; return o, nil }, nil, nil,
|
||||
func(o options) []string { return stringSliceOrNil(o.logFile) },
|
||||
}
|
||||
|
||||
var pidfileArg = arg{
|
||||
"Path to a file where PID is stored.",
|
||||
"pidfile", "",
|
||||
func(o options, v string) (options, error) { o.pidFile = v; return o, nil }, nil, nil,
|
||||
func(o options) []string { return stringSliceOrNil(o.pidFile) },
|
||||
}
|
||||
|
||||
var checkConfigArg = arg{
|
||||
"Check configuration and exit.",
|
||||
"check-config", "",
|
||||
nil, func(o options) (options, error) { o.checkConfig = true; return o, nil }, nil,
|
||||
func(o options) []string { return boolSliceOrNil(o.checkConfig) },
|
||||
}
|
||||
|
||||
var noCheckUpdateArg = arg{
|
||||
"Don't check for updates.",
|
||||
"no-check-update", "",
|
||||
nil, func(o options) (options, error) { o.disableUpdate = true; return o, nil }, nil,
|
||||
func(o options) []string { return boolSliceOrNil(o.disableUpdate) },
|
||||
}
|
||||
|
||||
var disableMemoryOptimizationArg = arg{
|
||||
"Deprecated. Disable memory optimization.",
|
||||
"no-mem-optimization", "",
|
||||
nil, nil, func(_ options, _ string) (f effect, err error) {
|
||||
},
|
||||
updateNoValue: nil,
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) {
|
||||
return o.serviceControlAction, o.serviceControlAction != ""
|
||||
},
|
||||
description: `Service control action: status, install (as a service), ` +
|
||||
`uninstall (as a service), start, stop, restart, reload (configuration).`,
|
||||
longName: "service",
|
||||
shortName: "s",
|
||||
}, {
|
||||
updateWithValue: func(o options, v string) (options, error) { o.logFile = v; return o, nil },
|
||||
updateNoValue: nil,
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) { return o.logFile, o.logFile != "" },
|
||||
description: `Path to log file. If empty, write to stdout; ` +
|
||||
`if "syslog", write to system log.`,
|
||||
longName: "logfile",
|
||||
shortName: "l",
|
||||
}, {
|
||||
updateWithValue: func(o options, v string) (options, error) { o.pidFile = v; return o, nil },
|
||||
updateNoValue: nil,
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) { return o.pidFile, o.pidFile != "" },
|
||||
description: "Path to a file where PID is stored.",
|
||||
longName: "pidfile",
|
||||
shortName: "",
|
||||
}, {
|
||||
updateWithValue: nil,
|
||||
updateNoValue: func(o options) (options, error) { o.checkConfig = true; return o, nil },
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) { return "", o.checkConfig },
|
||||
description: "Check configuration and exit.",
|
||||
longName: "check-config",
|
||||
shortName: "",
|
||||
}, {
|
||||
updateWithValue: nil,
|
||||
updateNoValue: func(o options) (options, error) { o.disableUpdate = true; return o, nil },
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) { return "", o.disableUpdate },
|
||||
description: "Don't check for updates.",
|
||||
longName: "no-check-update",
|
||||
shortName: "",
|
||||
}, {
|
||||
updateWithValue: nil,
|
||||
updateNoValue: nil,
|
||||
effect: func(_ options, _ string) (f effect, err error) {
|
||||
log.Info("warning: using --no-mem-optimization flag has no effect and is deprecated")
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
func(o options) []string { return nil },
|
||||
}
|
||||
|
||||
var verboseArg = arg{
|
||||
"Enable verbose output.",
|
||||
"verbose", "v",
|
||||
nil, func(o options) (options, error) { o.verbose = true; return o, nil }, nil,
|
||||
func(o options) []string { return boolSliceOrNil(o.verbose) },
|
||||
}
|
||||
|
||||
var glinetArg = arg{
|
||||
"Run in GL-Inet compatibility mode.",
|
||||
"glinet", "",
|
||||
nil, func(o options) (options, error) { o.glinetMode = true; return o, nil }, nil,
|
||||
func(o options) []string { return boolSliceOrNil(o.glinetMode) },
|
||||
}
|
||||
|
||||
var versionArg = arg{
|
||||
description: "Show the version and exit. Show more detailed version description with -v.",
|
||||
longName: "version",
|
||||
shortName: "",
|
||||
updateWithValue: nil,
|
||||
updateNoValue: nil,
|
||||
effect: func(o options, exec string) (effect, error) {
|
||||
return func() error {
|
||||
if o.verbose {
|
||||
fmt.Println(version.Verbose())
|
||||
} else {
|
||||
fmt.Println(version.Full())
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
return nil
|
||||
}, nil
|
||||
},
|
||||
serialize: func(o options) []string { return nil },
|
||||
}
|
||||
|
||||
var helpArg = arg{
|
||||
"Print this help.",
|
||||
"help", "",
|
||||
nil, nil, func(o options, exec string) (effect, error) {
|
||||
return func() error { _ = printHelp(exec); os.Exit(64); return nil }, nil
|
||||
},
|
||||
func(o options) []string { return nil },
|
||||
}
|
||||
|
||||
var noEtcHostsArg = arg{
|
||||
description: "Deprecated. Do not use the OS-provided hosts.",
|
||||
longName: "no-etc-hosts",
|
||||
shortName: "",
|
||||
serialize: func(o options) (val string, ok bool) { return "", false },
|
||||
description: "Deprecated. Disable memory optimization.",
|
||||
longName: "no-mem-optimization",
|
||||
shortName: "",
|
||||
}, {
|
||||
updateWithValue: nil,
|
||||
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
|
||||
effect: func(_ options, _ string) (f effect, err error) {
|
||||
@@ -242,146 +243,216 @@ var noEtcHostsArg = arg{
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
serialize: func(o options) []string { return boolSliceOrNil(o.noEtcHosts) },
|
||||
}
|
||||
|
||||
var localFrontendArg = arg{
|
||||
description: "Use local frontend directories.",
|
||||
longName: "local-frontend",
|
||||
shortName: "",
|
||||
serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
|
||||
description: "Deprecated. Do not use the OS-provided hosts.",
|
||||
longName: "no-etc-hosts",
|
||||
shortName: "",
|
||||
}, {
|
||||
updateWithValue: nil,
|
||||
updateNoValue: func(o options) (options, error) { o.localFrontend = true; return o, nil },
|
||||
effect: nil,
|
||||
serialize: func(o options) []string { return boolSliceOrNil(o.localFrontend) },
|
||||
}
|
||||
serialize: func(o options) (val string, ok bool) { return "", o.localFrontend },
|
||||
description: "Use local frontend directories.",
|
||||
longName: "local-frontend",
|
||||
shortName: "",
|
||||
}, {
|
||||
updateWithValue: nil,
|
||||
updateNoValue: func(o options) (options, error) { o.verbose = true; return o, nil },
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) { return "", o.verbose },
|
||||
description: "Enable verbose output.",
|
||||
longName: "verbose",
|
||||
shortName: "v",
|
||||
}, {
|
||||
updateWithValue: nil,
|
||||
updateNoValue: func(o options) (options, error) { o.glinetMode = true; return o, nil },
|
||||
effect: nil,
|
||||
serialize: func(o options) (val string, ok bool) { return "", o.glinetMode },
|
||||
description: "Run in GL-Inet compatibility mode.",
|
||||
longName: "glinet",
|
||||
shortName: "",
|
||||
}, {
|
||||
updateWithValue: nil,
|
||||
updateNoValue: nil,
|
||||
effect: func(o options, exec string) (effect, error) {
|
||||
return func() error {
|
||||
if o.verbose {
|
||||
fmt.Println(version.Verbose())
|
||||
} else {
|
||||
fmt.Println(version.Full())
|
||||
}
|
||||
|
||||
func init() {
|
||||
args = []arg{
|
||||
configArg,
|
||||
workDirArg,
|
||||
hostArg,
|
||||
portArg,
|
||||
serviceArg,
|
||||
logfileArg,
|
||||
pidfileArg,
|
||||
checkConfigArg,
|
||||
noCheckUpdateArg,
|
||||
disableMemoryOptimizationArg,
|
||||
noEtcHostsArg,
|
||||
localFrontendArg,
|
||||
verboseArg,
|
||||
glinetArg,
|
||||
versionArg,
|
||||
helpArg,
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
func getUsageLines(exec string, args []arg) []string {
|
||||
usage := []string{
|
||||
"Usage:",
|
||||
"",
|
||||
fmt.Sprintf("%s [options]", exec),
|
||||
"",
|
||||
"Options:",
|
||||
}
|
||||
for _, arg := range args {
|
||||
return nil
|
||||
}, nil
|
||||
},
|
||||
serialize: func(o options) (val string, ok bool) { return "", false },
|
||||
description: "Show the version and exit. Show more detailed version description with -v.",
|
||||
longName: "version",
|
||||
shortName: "",
|
||||
}}
|
||||
|
||||
// printHelp prints the entire help message. It exits with an error code if
|
||||
// there are any I/O errors.
|
||||
func printHelp(exec string) {
|
||||
b := &strings.Builder{}
|
||||
|
||||
stringutil.WriteToBuilder(
|
||||
b,
|
||||
"Usage:\n\n",
|
||||
fmt.Sprintf("%s [options]\n\n", exec),
|
||||
"Options:\n",
|
||||
)
|
||||
|
||||
var err error
|
||||
for _, opt := range cmdLineOpts {
|
||||
val := ""
|
||||
if arg.updateWithValue != nil {
|
||||
if opt.updateWithValue != nil {
|
||||
val = " VALUE"
|
||||
}
|
||||
if arg.shortName != "" {
|
||||
usage = append(usage, fmt.Sprintf(" -%s, %-30s %s",
|
||||
arg.shortName,
|
||||
"--"+arg.longName+val,
|
||||
arg.description))
|
||||
|
||||
longDesc := opt.longName + val
|
||||
if opt.shortName != "" {
|
||||
_, err = fmt.Fprintf(b, " -%s, --%-28s %s\n", opt.shortName, longDesc, opt.description)
|
||||
} else {
|
||||
usage = append(usage, fmt.Sprintf(" %-34s %s",
|
||||
"--"+arg.longName+val,
|
||||
arg.description))
|
||||
_, err = fmt.Fprintf(b, " --%-32s %s\n", longDesc, opt.description)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// The only error here can be from incorrect Fprintf usage, which is
|
||||
// a programmer error.
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return usage
|
||||
|
||||
_, err = fmt.Print(b)
|
||||
if err != nil {
|
||||
// Exit immediately, since not being able to print out a help message
|
||||
// essentially means that the I/O is very broken at the moment.
|
||||
exitWithError()
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp(exec string) error {
|
||||
for _, line := range getUsageLines(exec, args) {
|
||||
_, err := fmt.Println(line)
|
||||
// parseCmdOpts parses the command-line arguments into options and effects.
|
||||
func parseCmdOpts(cmdName string, args []string) (o options, eff effect, err error) {
|
||||
// Don't use range since the loop changes the loop variable.
|
||||
argsLen := len(args)
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
isKnown := false
|
||||
for _, opt := range cmdLineOpts {
|
||||
isKnown = argMatches(opt, arg)
|
||||
if !isKnown {
|
||||
continue
|
||||
}
|
||||
|
||||
if opt.updateWithValue != nil {
|
||||
i++
|
||||
if i >= argsLen {
|
||||
return o, eff, fmt.Errorf("got %s without argument", arg)
|
||||
}
|
||||
|
||||
o, err = opt.updateWithValue(o, args[i])
|
||||
} else {
|
||||
o, eff, err = updateOptsNoValue(o, eff, opt, cmdName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return o, eff, fmt.Errorf("applying option %s: %w", arg, err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if !isKnown {
|
||||
return o, eff, fmt.Errorf("unknown option %s", arg)
|
||||
}
|
||||
}
|
||||
|
||||
return o, eff, err
|
||||
}
|
||||
|
||||
// argMatches returns true if arg matches command-line option opt.
|
||||
func argMatches(opt cmdLineOpt, arg string) (ok bool) {
|
||||
if arg == "" || arg[0] != '-' {
|
||||
return false
|
||||
}
|
||||
|
||||
arg = arg[1:]
|
||||
if arg == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return (opt.shortName != "" && arg == opt.shortName) ||
|
||||
(arg[0] == '-' && arg[1:] == opt.longName)
|
||||
}
|
||||
|
||||
// updateOptsNoValue sets values or effects from opt into o or prev.
|
||||
func updateOptsNoValue(
|
||||
o options,
|
||||
prev effect,
|
||||
opt cmdLineOpt,
|
||||
cmdName string,
|
||||
) (updated options, chained effect, err error) {
|
||||
if opt.updateNoValue != nil {
|
||||
o, err = opt.updateNoValue(o)
|
||||
if err != nil {
|
||||
return o, prev, err
|
||||
}
|
||||
|
||||
return o, prev, nil
|
||||
}
|
||||
|
||||
next, err := opt.effect(o, cmdName)
|
||||
if err != nil {
|
||||
return o, prev, err
|
||||
}
|
||||
|
||||
chained = chainEffect(prev, next)
|
||||
|
||||
return o, chained, nil
|
||||
}
|
||||
|
||||
// chainEffect chans the next effect after the prev one. If prev is nil, eff
|
||||
// only calls next. If next is nil, eff is prev; if prev is nil, eff is next.
|
||||
func chainEffect(prev, next effect) (eff effect) {
|
||||
if prev == nil {
|
||||
return next
|
||||
} else if next == nil {
|
||||
return prev
|
||||
}
|
||||
|
||||
eff = func() (err error) {
|
||||
err = prev()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
return nil
|
||||
|
||||
return eff
|
||||
}
|
||||
|
||||
func argMatches(a arg, v string) bool {
|
||||
return v == "--"+a.longName || (a.shortName != "" && v == "-"+a.shortName)
|
||||
}
|
||||
|
||||
func parse(exec string, ss []string) (o options, f effect, err error) {
|
||||
for i := 0; i < len(ss); i++ {
|
||||
v := ss[i]
|
||||
knownParam := false
|
||||
for _, arg := range args {
|
||||
if argMatches(arg, v) {
|
||||
if arg.updateWithValue != nil {
|
||||
if i+1 >= len(ss) {
|
||||
return o, f, fmt.Errorf("got %s without argument", v)
|
||||
}
|
||||
i++
|
||||
o, err = arg.updateWithValue(o, ss[i])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if arg.updateNoValue != nil {
|
||||
o, err = arg.updateNoValue(o)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if arg.effect != nil {
|
||||
var eff effect
|
||||
eff, err = arg.effect(o, exec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if eff != nil {
|
||||
prevf := f
|
||||
f = func() (ferr error) {
|
||||
if prevf != nil {
|
||||
ferr = prevf()
|
||||
}
|
||||
if ferr == nil {
|
||||
ferr = eff()
|
||||
}
|
||||
return ferr
|
||||
}
|
||||
}
|
||||
}
|
||||
knownParam = true
|
||||
break
|
||||
}
|
||||
// optsToArgs converts command line options into a list of arguments.
|
||||
func optsToArgs(o options) (args []string) {
|
||||
for _, opt := range cmdLineOpts {
|
||||
val, ok := opt.serialize(o)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !knownParam {
|
||||
return o, f, fmt.Errorf("unknown option %v", v)
|
||||
|
||||
if opt.shortName != "" {
|
||||
args = append(args, "-"+opt.shortName)
|
||||
} else {
|
||||
args = append(args, "--"+opt.longName)
|
||||
}
|
||||
|
||||
if val != "" {
|
||||
args = append(args, val)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func shortestFlag(a arg) string {
|
||||
if a.shortName != "" {
|
||||
return "-" + a.shortName
|
||||
}
|
||||
return "--" + a.longName
|
||||
}
|
||||
|
||||
func serialize(o options) []string {
|
||||
ss := []string{}
|
||||
for _, arg := range args {
|
||||
s := arg.serialize(o)
|
||||
if s != nil {
|
||||
ss = append(ss, append([]string{shortestFlag(arg)}, s...)...)
|
||||
}
|
||||
}
|
||||
return ss
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package home
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func testParseOK(t *testing.T, ss ...string) options {
|
||||
t.Helper()
|
||||
|
||||
o, _, err := parse("", ss)
|
||||
o, _, err := parseCmdOpts("", ss)
|
||||
require.NoError(t, err)
|
||||
|
||||
return o
|
||||
@@ -21,7 +21,7 @@ func testParseOK(t *testing.T, ss ...string) options {
|
||||
func testParseErr(t *testing.T, descr string, ss ...string) {
|
||||
t.Helper()
|
||||
|
||||
_, _, err := parse("", ss)
|
||||
_, _, err := parseCmdOpts("", ss)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ func TestParseVerbose(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseConfigFilename(t *testing.T) {
|
||||
assert.Equal(t, "", testParseOK(t).configFilename, "empty is no config filename")
|
||||
assert.Equal(t, "path", testParseOK(t, "-c", "path").configFilename, "-c is config filename")
|
||||
assert.Equal(t, "", testParseOK(t).confFilename, "empty is no config filename")
|
||||
assert.Equal(t, "path", testParseOK(t, "-c", "path").confFilename, "-c is config filename")
|
||||
testParseParamMissing(t, "-c")
|
||||
|
||||
assert.Equal(t, "path", testParseOK(t, "--config", "path").configFilename, "--config is config filename")
|
||||
assert.Equal(t, "path", testParseOK(t, "--config", "path").confFilename, "--config is config filename")
|
||||
testParseParamMissing(t, "--config")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -103,7 +105,7 @@ func TestParseDisableUpdate(t *testing.T) {
|
||||
|
||||
// TODO(e.burkov): Remove after v0.108.0.
|
||||
func TestParseDisableMemoryOptimization(t *testing.T) {
|
||||
o, eff, err := parse("", []string{"--no-mem-optimization"})
|
||||
o, eff, err := parseCmdOpts("", []string{"--no-mem-optimization"})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Nil(t, eff)
|
||||
@@ -130,73 +132,73 @@ func TestParseUnknown(t *testing.T) {
|
||||
testParseErr(t, "unknown dash", "-")
|
||||
}
|
||||
|
||||
func TestSerialize(t *testing.T) {
|
||||
func TestOptsToArgs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
opts options
|
||||
ss []string
|
||||
}{{
|
||||
name: "empty",
|
||||
args: []string{},
|
||||
opts: options{},
|
||||
ss: []string{},
|
||||
}, {
|
||||
name: "config_filename",
|
||||
opts: options{configFilename: "path"},
|
||||
ss: []string{"-c", "path"},
|
||||
args: []string{"-c", "path"},
|
||||
opts: options{confFilename: "path"},
|
||||
}, {
|
||||
name: "work_dir",
|
||||
args: []string{"-w", "path"},
|
||||
opts: options{workDir: "path"},
|
||||
ss: []string{"-w", "path"},
|
||||
}, {
|
||||
name: "bind_host",
|
||||
opts: options{bindHost: net.IP{1, 2, 3, 4}},
|
||||
ss: []string{"-h", "1.2.3.4"},
|
||||
opts: options{bindHost: netip.MustParseAddr("1.2.3.4")},
|
||||
args: []string{"-h", "1.2.3.4"},
|
||||
}, {
|
||||
name: "bind_port",
|
||||
args: []string{"-p", "666"},
|
||||
opts: options{bindPort: 666},
|
||||
ss: []string{"-p", "666"},
|
||||
}, {
|
||||
name: "log_file",
|
||||
args: []string{"-l", "path"},
|
||||
opts: options{logFile: "path"},
|
||||
ss: []string{"-l", "path"},
|
||||
}, {
|
||||
name: "pid_file",
|
||||
args: []string{"--pidfile", "path"},
|
||||
opts: options{pidFile: "path"},
|
||||
ss: []string{"--pidfile", "path"},
|
||||
}, {
|
||||
name: "disable_update",
|
||||
args: []string{"--no-check-update"},
|
||||
opts: options{disableUpdate: true},
|
||||
ss: []string{"--no-check-update"},
|
||||
}, {
|
||||
name: "control_action",
|
||||
args: []string{"-s", "run"},
|
||||
opts: options{serviceControlAction: "run"},
|
||||
ss: []string{"-s", "run"},
|
||||
}, {
|
||||
name: "glinet_mode",
|
||||
args: []string{"--glinet"},
|
||||
opts: options{glinetMode: true},
|
||||
ss: []string{"--glinet"},
|
||||
}, {
|
||||
name: "multiple",
|
||||
opts: options{
|
||||
serviceControlAction: "run",
|
||||
configFilename: "config",
|
||||
workDir: "work",
|
||||
pidFile: "pid",
|
||||
disableUpdate: true,
|
||||
},
|
||||
ss: []string{
|
||||
args: []string{
|
||||
"-c", "config",
|
||||
"-w", "work",
|
||||
"-s", "run",
|
||||
"--pidfile", "pid",
|
||||
"--no-check-update",
|
||||
},
|
||||
opts: options{
|
||||
serviceControlAction: "run",
|
||||
confFilename: "config",
|
||||
workDir: "work",
|
||||
pidFile: "pid",
|
||||
disableUpdate: true,
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := serialize(tc.opts)
|
||||
assert.ElementsMatch(t, tc.ss, result)
|
||||
result := optsToArgs(tc.opts)
|
||||
assert.ElementsMatch(t, tc.args, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
|
||||
DisplayName: serviceDisplayName,
|
||||
Description: serviceDescription,
|
||||
WorkingDirectory: pwd,
|
||||
Arguments: serialize(runOpts),
|
||||
Arguments: optsToArgs(runOpts),
|
||||
}
|
||||
configureService(svcConfig)
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -28,216 +26,256 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
var tlsWebHandlersRegistered = false
|
||||
// tlsManager contains the current configuration and state of AdGuard Home TLS
|
||||
// encryption.
|
||||
type tlsManager struct {
|
||||
// status is the current status of the configuration. It is never nil.
|
||||
status *tlsConfigStatus
|
||||
|
||||
// TLSMod - TLS module object
|
||||
type TLSMod struct {
|
||||
certLastMod time.Time // last modification time of the certificate file
|
||||
status tlsConfigStatus
|
||||
confLock sync.Mutex
|
||||
conf tlsConfigSettings
|
||||
// certLastMod is the last modification time of the certificate file.
|
||||
certLastMod time.Time
|
||||
|
||||
confLock sync.Mutex
|
||||
conf tlsConfigSettings
|
||||
}
|
||||
|
||||
// Create TLS module
|
||||
func tlsCreate(conf tlsConfigSettings) *TLSMod {
|
||||
t := &TLSMod{}
|
||||
t.conf = conf
|
||||
if t.conf.Enabled {
|
||||
if !t.load() {
|
||||
// Something is not valid - return an empty TLS config
|
||||
return &TLSMod{conf: tlsConfigSettings{
|
||||
Enabled: conf.Enabled,
|
||||
ServerName: conf.ServerName,
|
||||
PortHTTPS: conf.PortHTTPS,
|
||||
PortDNSOverTLS: conf.PortDNSOverTLS,
|
||||
PortDNSOverQUIC: conf.PortDNSOverQUIC,
|
||||
AllowUnencryptedDoH: conf.AllowUnencryptedDoH,
|
||||
}}
|
||||
// newTLSManager initializes the TLS configuration.
|
||||
func newTLSManager(conf tlsConfigSettings) (m *tlsManager, err error) {
|
||||
m = &tlsManager{
|
||||
status: &tlsConfigStatus{},
|
||||
conf: conf,
|
||||
}
|
||||
|
||||
if m.conf.Enabled {
|
||||
err = m.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.setCertFileTime()
|
||||
|
||||
m.setCertFileTime()
|
||||
}
|
||||
return t
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (t *TLSMod) load() bool {
|
||||
if !tlsLoadConfig(&t.conf, &t.status) {
|
||||
log.Error("failed to load TLS config: %s", t.status.WarningValidation)
|
||||
return false
|
||||
// load reloads the TLS configuration from files or data from the config file.
|
||||
func (m *tlsManager) load() (err error) {
|
||||
err = loadTLSConf(&m.conf, m.status)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
}
|
||||
|
||||
// validate current TLS config and update warnings (it could have been loaded from file)
|
||||
data := validateCertificates(string(t.conf.CertificateChainData), string(t.conf.PrivateKeyData), t.conf.ServerName)
|
||||
if !data.ValidPair {
|
||||
log.Error("failed to validate certificate: %s", data.WarningValidation)
|
||||
return false
|
||||
}
|
||||
t.status = data
|
||||
return true
|
||||
}
|
||||
|
||||
// Close - close module
|
||||
func (t *TLSMod) Close() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteDiskConfig - write config
|
||||
func (t *TLSMod) WriteDiskConfig(conf *tlsConfigSettings) {
|
||||
t.confLock.Lock()
|
||||
*conf = t.conf
|
||||
t.confLock.Unlock()
|
||||
func (m *tlsManager) WriteDiskConfig(conf *tlsConfigSettings) {
|
||||
m.confLock.Lock()
|
||||
*conf = m.conf
|
||||
m.confLock.Unlock()
|
||||
}
|
||||
|
||||
func (t *TLSMod) setCertFileTime() {
|
||||
if len(t.conf.CertificatePath) == 0 {
|
||||
// setCertFileTime sets t.certLastMod from the certificate. If there are
|
||||
// errors, setCertFileTime logs them.
|
||||
func (m *tlsManager) setCertFileTime() {
|
||||
if len(m.conf.CertificatePath) == 0 {
|
||||
return
|
||||
}
|
||||
fi, err := os.Stat(t.conf.CertificatePath)
|
||||
|
||||
fi, err := os.Stat(m.conf.CertificatePath)
|
||||
if err != nil {
|
||||
log.Error("TLS: %s", err)
|
||||
log.Error("tls: looking up certificate path: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
t.certLastMod = fi.ModTime().UTC()
|
||||
|
||||
m.certLastMod = fi.ModTime().UTC()
|
||||
}
|
||||
|
||||
// Start updates the configuration of TLSMod and starts it.
|
||||
func (t *TLSMod) Start() {
|
||||
if !tlsWebHandlersRegistered {
|
||||
tlsWebHandlersRegistered = true
|
||||
t.registerWebHandlers()
|
||||
}
|
||||
// start updates the configuration of t and starts it.
|
||||
func (m *tlsManager) start() {
|
||||
m.registerWebHandlers()
|
||||
|
||||
t.confLock.Lock()
|
||||
tlsConf := t.conf
|
||||
t.confLock.Unlock()
|
||||
m.confLock.Lock()
|
||||
tlsConf := m.conf
|
||||
m.confLock.Unlock()
|
||||
|
||||
// The background context is used because the TLSConfigChanged wraps
|
||||
// context with timeout on its own and shuts down the server, which
|
||||
// handles current request.
|
||||
// The background context is used because the TLSConfigChanged wraps context
|
||||
// with timeout on its own and shuts down the server, which handles current
|
||||
// request.
|
||||
Context.web.TLSConfigChanged(context.Background(), tlsConf)
|
||||
}
|
||||
|
||||
// Reload updates the configuration of TLSMod and restarts it.
|
||||
func (t *TLSMod) Reload() {
|
||||
t.confLock.Lock()
|
||||
tlsConf := t.conf
|
||||
t.confLock.Unlock()
|
||||
// reload updates the configuration and restarts t.
|
||||
func (m *tlsManager) reload() {
|
||||
m.confLock.Lock()
|
||||
tlsConf := m.conf
|
||||
m.confLock.Unlock()
|
||||
|
||||
if !tlsConf.Enabled || len(tlsConf.CertificatePath) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fi, err := os.Stat(tlsConf.CertificatePath)
|
||||
if err != nil {
|
||||
log.Error("TLS: %s", err)
|
||||
return
|
||||
}
|
||||
if fi.ModTime().UTC().Equal(t.certLastMod) {
|
||||
log.Debug("TLS: certificate file isn't modified")
|
||||
return
|
||||
}
|
||||
log.Debug("TLS: certificate file is modified")
|
||||
log.Error("tls: %s", err)
|
||||
|
||||
t.confLock.Lock()
|
||||
r := t.load()
|
||||
t.confLock.Unlock()
|
||||
if !r {
|
||||
return
|
||||
}
|
||||
|
||||
t.certLastMod = fi.ModTime().UTC()
|
||||
if fi.ModTime().UTC().Equal(m.certLastMod) {
|
||||
log.Debug("tls: certificate file isn't modified")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("tls: certificate file is modified")
|
||||
|
||||
m.confLock.Lock()
|
||||
err = m.load()
|
||||
m.confLock.Unlock()
|
||||
if err != nil {
|
||||
log.Error("tls: reloading: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
m.certLastMod = fi.ModTime().UTC()
|
||||
|
||||
_ = reconfigureDNSServer()
|
||||
|
||||
t.confLock.Lock()
|
||||
tlsConf = t.conf
|
||||
t.confLock.Unlock()
|
||||
// The background context is used because the TLSConfigChanged wraps
|
||||
// context with timeout on its own and shuts down the server, which
|
||||
// handles current request.
|
||||
m.confLock.Lock()
|
||||
tlsConf = m.conf
|
||||
m.confLock.Unlock()
|
||||
|
||||
// The background context is used because the TLSConfigChanged wraps context
|
||||
// with timeout on its own and shuts down the server, which handles current
|
||||
// request.
|
||||
Context.web.TLSConfigChanged(context.Background(), tlsConf)
|
||||
}
|
||||
|
||||
// Set certificate and private key data
|
||||
func tlsLoadConfig(tls *tlsConfigSettings, status *tlsConfigStatus) bool {
|
||||
tls.CertificateChainData = []byte(tls.CertificateChain)
|
||||
tls.PrivateKeyData = []byte(tls.PrivateKey)
|
||||
|
||||
var err error
|
||||
if tls.CertificatePath != "" {
|
||||
if tls.CertificateChain != "" {
|
||||
status.WarningValidation = "certificate data and file can't be set together"
|
||||
return false
|
||||
}
|
||||
tls.CertificateChainData, err = os.ReadFile(tls.CertificatePath)
|
||||
// loadTLSConf loads and validates the TLS configuration. The returned error is
|
||||
// also set in status.WarningValidation.
|
||||
func loadTLSConf(tlsConf *tlsConfigSettings, status *tlsConfigStatus) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
status.WarningValidation = err.Error()
|
||||
return false
|
||||
}
|
||||
}()
|
||||
|
||||
tlsConf.CertificateChainData = []byte(tlsConf.CertificateChain)
|
||||
tlsConf.PrivateKeyData = []byte(tlsConf.PrivateKey)
|
||||
|
||||
if tlsConf.CertificatePath != "" {
|
||||
if tlsConf.CertificateChain != "" {
|
||||
return errors.Error("certificate data and file can't be set together")
|
||||
}
|
||||
|
||||
tlsConf.CertificateChainData, err = os.ReadFile(tlsConf.CertificatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading cert file: %w", err)
|
||||
}
|
||||
|
||||
status.ValidCert = true
|
||||
}
|
||||
|
||||
if tls.PrivateKeyPath != "" {
|
||||
if tls.PrivateKey != "" {
|
||||
status.WarningValidation = "private key data and file can't be set together"
|
||||
return false
|
||||
if tlsConf.PrivateKeyPath != "" {
|
||||
if tlsConf.PrivateKey != "" {
|
||||
return errors.Error("private key data and file can't be set together")
|
||||
}
|
||||
tls.PrivateKeyData, err = os.ReadFile(tls.PrivateKeyPath)
|
||||
|
||||
tlsConf.PrivateKeyData, err = os.ReadFile(tlsConf.PrivateKeyPath)
|
||||
if err != nil {
|
||||
status.WarningValidation = err.Error()
|
||||
return false
|
||||
return fmt.Errorf("reading key file: %w", err)
|
||||
}
|
||||
|
||||
status.ValidKey = true
|
||||
}
|
||||
|
||||
return true
|
||||
err = validateCertificates(
|
||||
status,
|
||||
tlsConf.CertificateChainData,
|
||||
tlsConf.PrivateKeyData,
|
||||
tlsConf.ServerName,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating certificate pair: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tlsConfigStatus contains the status of a certificate chain and key pair.
|
||||
type tlsConfigStatus struct {
|
||||
ValidCert bool `json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
|
||||
ValidChain bool `json:"valid_chain"` // ValidChain is true if the specified certificates chain is verified and issued by a known CA
|
||||
Subject string `json:"subject,omitempty"` // Subject is the subject of the first certificate in the chain
|
||||
Issuer string `json:"issuer,omitempty"` // Issuer is the issuer of the first certificate in the chain
|
||||
NotBefore time.Time `json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
|
||||
NotAfter time.Time `json:"not_after,omitempty"` // NotAfter is the NotAfter field of the first certificate in the chain
|
||||
DNSNames []string `json:"dns_names"` // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
|
||||
// Subject is the subject of the first certificate in the chain.
|
||||
Subject string `json:"subject,omitempty"`
|
||||
|
||||
// key status
|
||||
ValidKey bool `json:"valid_key"` // ValidKey is true if the key is a valid private key
|
||||
KeyType string `json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
|
||||
// Issuer is the issuer of the first certificate in the chain.
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
|
||||
// is usable? set by validator
|
||||
ValidPair bool `json:"valid_pair"` // ValidPair is true if both certificate and private key are correct
|
||||
// KeyType is the type of the private key.
|
||||
KeyType string `json:"key_type,omitempty"`
|
||||
|
||||
// warnings
|
||||
WarningValidation string `json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
|
||||
// NotBefore is the NotBefore field of the first certificate in the chain.
|
||||
NotBefore time.Time `json:"not_before,omitempty"`
|
||||
|
||||
// NotAfter is the NotAfter field of the first certificate in the chain.
|
||||
NotAfter time.Time `json:"not_after,omitempty"`
|
||||
|
||||
// WarningValidation is a validation warning message with the issue
|
||||
// description.
|
||||
WarningValidation string `json:"warning_validation,omitempty"`
|
||||
|
||||
// DNSNames is the value of SubjectAltNames field of the first certificate
|
||||
// in the chain.
|
||||
DNSNames []string `json:"dns_names"`
|
||||
|
||||
// ValidCert is true if the specified certificate chain is a valid chain of
|
||||
// X509 certificates.
|
||||
ValidCert bool `json:"valid_cert"`
|
||||
|
||||
// ValidChain is true if the specified certificate chain is verified and
|
||||
// issued by a known CA.
|
||||
ValidChain bool `json:"valid_chain"`
|
||||
|
||||
// ValidKey is true if the key is a valid private key.
|
||||
ValidKey bool `json:"valid_key"`
|
||||
|
||||
// ValidPair is true if both certificate and private key are correct for
|
||||
// each other.
|
||||
ValidPair bool `json:"valid_pair"`
|
||||
}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
// tlsConfig is the TLS configuration and status response.
|
||||
type tlsConfig struct {
|
||||
tlsConfigStatus `json:",inline"`
|
||||
*tlsConfigStatus `json:",inline"`
|
||||
tlsConfigSettingsExt `json:",inline"`
|
||||
}
|
||||
|
||||
// tlsConfigSettingsExt is used to (un)marshal PrivateKeySaved to ensure that
|
||||
// clients don't send and receive previously saved private keys.
|
||||
// tlsConfigSettingsExt is used to (un)marshal the PrivateKeySaved field to
|
||||
// ensure that clients don't send and receive previously saved private keys.
|
||||
type tlsConfigSettingsExt struct {
|
||||
tlsConfigSettings `json:",inline"`
|
||||
// If private key saved as a string, we set this flag to true
|
||||
// and omit key from answer.
|
||||
|
||||
// PrivateKeySaved is true if the private key is saved as a string and omit
|
||||
// key from answer.
|
||||
PrivateKeySaved bool `yaml:"-" json:"private_key_saved,inline"`
|
||||
}
|
||||
|
||||
func (t *TLSMod) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
|
||||
t.confLock.Lock()
|
||||
func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
|
||||
m.confLock.Lock()
|
||||
data := tlsConfig{
|
||||
tlsConfigSettingsExt: tlsConfigSettingsExt{
|
||||
tlsConfigSettings: t.conf,
|
||||
tlsConfigSettings: m.conf,
|
||||
},
|
||||
tlsConfigStatus: t.status,
|
||||
tlsConfigStatus: m.status,
|
||||
}
|
||||
t.confLock.Unlock()
|
||||
m.confLock.Unlock()
|
||||
|
||||
marshalTLS(w, r, data)
|
||||
}
|
||||
|
||||
func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
setts, err := unmarshalTLS(r)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
||||
@@ -246,7 +284,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if setts.PrivateKeySaved {
|
||||
setts.PrivateKey = t.conf.PrivateKey
|
||||
setts.PrivateKey = m.conf.PrivateKey
|
||||
}
|
||||
|
||||
if setts.Enabled {
|
||||
@@ -278,75 +316,74 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
status := tlsConfigStatus{}
|
||||
if tlsLoadConfig(&setts.tlsConfigSettings, &status) {
|
||||
status = validateCertificates(string(setts.CertificateChainData), string(setts.PrivateKeyData), setts.ServerName)
|
||||
}
|
||||
|
||||
data := tlsConfig{
|
||||
// Skip the error check, since we are only interested in the value of
|
||||
// status.WarningValidation.
|
||||
status := &tlsConfigStatus{}
|
||||
_ = loadTLSConf(&setts.tlsConfigSettings, status)
|
||||
resp := tlsConfig{
|
||||
tlsConfigSettingsExt: setts,
|
||||
tlsConfigStatus: status,
|
||||
}
|
||||
|
||||
marshalTLS(w, r, data)
|
||||
marshalTLS(w, r, resp)
|
||||
}
|
||||
|
||||
func (t *TLSMod) setConfig(newConf tlsConfigSettings, status tlsConfigStatus) (restartHTTPS bool) {
|
||||
t.confLock.Lock()
|
||||
defer t.confLock.Unlock()
|
||||
func (m *tlsManager) setConfig(newConf tlsConfigSettings, status *tlsConfigStatus) (restartHTTPS bool) {
|
||||
m.confLock.Lock()
|
||||
defer m.confLock.Unlock()
|
||||
|
||||
// Reset the DNSCrypt data before comparing, since we currently do not
|
||||
// accept these from the frontend.
|
||||
//
|
||||
// TODO(a.garipov): Define a custom comparer for dnsforward.TLSConfig.
|
||||
newConf.DNSCryptConfigFile = t.conf.DNSCryptConfigFile
|
||||
newConf.PortDNSCrypt = t.conf.PortDNSCrypt
|
||||
if !cmp.Equal(t.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
|
||||
newConf.DNSCryptConfigFile = m.conf.DNSCryptConfigFile
|
||||
newConf.PortDNSCrypt = m.conf.PortDNSCrypt
|
||||
if !cmp.Equal(m.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
|
||||
log.Info("tls config has changed, restarting https server")
|
||||
restartHTTPS = true
|
||||
} else {
|
||||
log.Info("tls config has not changed")
|
||||
log.Info("tls: config has not changed")
|
||||
}
|
||||
|
||||
// Note: don't do just `t.conf = data` because we must preserve all other members of t.conf
|
||||
t.conf.Enabled = newConf.Enabled
|
||||
t.conf.ServerName = newConf.ServerName
|
||||
t.conf.ForceHTTPS = newConf.ForceHTTPS
|
||||
t.conf.PortHTTPS = newConf.PortHTTPS
|
||||
t.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
|
||||
t.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
|
||||
t.conf.CertificateChain = newConf.CertificateChain
|
||||
t.conf.CertificatePath = newConf.CertificatePath
|
||||
t.conf.CertificateChainData = newConf.CertificateChainData
|
||||
t.conf.PrivateKey = newConf.PrivateKey
|
||||
t.conf.PrivateKeyPath = newConf.PrivateKeyPath
|
||||
t.conf.PrivateKeyData = newConf.PrivateKeyData
|
||||
t.status = status
|
||||
m.conf.Enabled = newConf.Enabled
|
||||
m.conf.ServerName = newConf.ServerName
|
||||
m.conf.ForceHTTPS = newConf.ForceHTTPS
|
||||
m.conf.PortHTTPS = newConf.PortHTTPS
|
||||
m.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
|
||||
m.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
|
||||
m.conf.CertificateChain = newConf.CertificateChain
|
||||
m.conf.CertificatePath = newConf.CertificatePath
|
||||
m.conf.CertificateChainData = newConf.CertificateChainData
|
||||
m.conf.PrivateKey = newConf.PrivateKey
|
||||
m.conf.PrivateKeyPath = newConf.PrivateKeyPath
|
||||
m.conf.PrivateKeyData = newConf.PrivateKeyData
|
||||
m.status = status
|
||||
|
||||
return restartHTTPS
|
||||
}
|
||||
|
||||
func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := unmarshalTLS(r)
|
||||
func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := unmarshalTLS(r)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if data.PrivateKeySaved {
|
||||
data.PrivateKey = t.conf.PrivateKey
|
||||
if req.PrivateKeySaved {
|
||||
req.PrivateKey = m.conf.PrivateKey
|
||||
}
|
||||
|
||||
if data.Enabled {
|
||||
if req.Enabled {
|
||||
err = validatePorts(
|
||||
tcpPort(config.BindPort),
|
||||
tcpPort(config.BetaBindPort),
|
||||
tcpPort(data.PortHTTPS),
|
||||
tcpPort(data.PortDNSOverTLS),
|
||||
tcpPort(data.PortDNSCrypt),
|
||||
tcpPort(req.PortHTTPS),
|
||||
tcpPort(req.PortDNSOverTLS),
|
||||
tcpPort(req.PortDNSCrypt),
|
||||
udpPort(config.DNS.Port),
|
||||
udpPort(data.PortDNSOverQUIC),
|
||||
udpPort(req.PortDNSOverQUIC),
|
||||
)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
@@ -356,33 +393,33 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Investigate and perhaps check other ports.
|
||||
if !webCheckPortAvailable(data.PortHTTPS) {
|
||||
if !webCheckPortAvailable(req.PortHTTPS) {
|
||||
aghhttp.Error(
|
||||
r,
|
||||
w,
|
||||
http.StatusBadRequest,
|
||||
"port %d is not available, cannot enable HTTPS on it",
|
||||
data.PortHTTPS,
|
||||
"port %d is not available, cannot enable https on it",
|
||||
req.PortHTTPS,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
status := tlsConfigStatus{}
|
||||
if !tlsLoadConfig(&data.tlsConfigSettings, &status) {
|
||||
data2 := tlsConfig{
|
||||
tlsConfigSettingsExt: data,
|
||||
tlsConfigStatus: t.status,
|
||||
status := &tlsConfigStatus{}
|
||||
err = loadTLSConf(&req.tlsConfigSettings, status)
|
||||
if err != nil {
|
||||
resp := tlsConfig{
|
||||
tlsConfigSettingsExt: req,
|
||||
tlsConfigStatus: status,
|
||||
}
|
||||
marshalTLS(w, r, data2)
|
||||
|
||||
marshalTLS(w, r, resp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
status = validateCertificates(string(data.CertificateChainData), string(data.PrivateKeyData), data.ServerName)
|
||||
|
||||
restartHTTPS := t.setConfig(data.tlsConfigSettings, status)
|
||||
t.setCertFileTime()
|
||||
restartHTTPS := m.setConfig(req.tlsConfigSettings, status)
|
||||
m.setCertFileTime()
|
||||
onConfigModified()
|
||||
|
||||
err = reconfigureDNSServer()
|
||||
@@ -392,12 +429,12 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
data2 := tlsConfig{
|
||||
tlsConfigSettingsExt: data,
|
||||
tlsConfigStatus: t.status,
|
||||
resp := tlsConfig{
|
||||
tlsConfigSettingsExt: req,
|
||||
tlsConfigStatus: m.status,
|
||||
}
|
||||
|
||||
marshalTLS(w, r, data2)
|
||||
marshalTLS(w, r, resp)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
@@ -408,7 +445,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
// same reason.
|
||||
if restartHTTPS {
|
||||
go func() {
|
||||
Context.web.TLSConfigChanged(context.Background(), data.tlsConfigSettings)
|
||||
Context.web.TLSConfigChanged(context.Background(), req.tlsConfigSettings)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -445,89 +482,105 @@ func validatePorts(
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyCertChain(data *tlsConfigStatus, certChain, serverName string) error {
|
||||
log.Tracef("TLS: got certificate: %d bytes", len(certChain))
|
||||
// validateCertChain validates the certificate chain and sets data in status.
|
||||
// The returned error is also set in status.WarningValidation.
|
||||
func validateCertChain(status *tlsConfigStatus, certChain []byte, serverName string) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
status.WarningValidation = err.Error()
|
||||
}
|
||||
}()
|
||||
|
||||
// now do a more extended validation
|
||||
var certs []*pem.Block // PEM-encoded certificates
|
||||
log.Debug("tls: got certificate chain: %d bytes", len(certChain))
|
||||
|
||||
pemblock := []byte(certChain)
|
||||
var certs []*pem.Block
|
||||
pemblock := certChain
|
||||
for {
|
||||
var decoded *pem.Block
|
||||
decoded, pemblock = pem.Decode(pemblock)
|
||||
if decoded == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if decoded.Type == "CERTIFICATE" {
|
||||
certs = append(certs, decoded)
|
||||
}
|
||||
}
|
||||
|
||||
var parsedCerts []*x509.Certificate
|
||||
|
||||
for _, cert := range certs {
|
||||
parsed, err := x509.ParseCertificate(cert.Bytes)
|
||||
if err != nil {
|
||||
data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err)
|
||||
return errors.Error(data.WarningValidation)
|
||||
}
|
||||
parsedCerts = append(parsedCerts, parsed)
|
||||
parsedCerts, err := parsePEMCerts(certs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(parsedCerts) == 0 {
|
||||
data.WarningValidation = "You have specified an empty certificate"
|
||||
return errors.Error(data.WarningValidation)
|
||||
}
|
||||
|
||||
data.ValidCert = true
|
||||
|
||||
// spew.Dump(parsedCerts)
|
||||
status.ValidCert = true
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
DNSName: serverName,
|
||||
Roots: Context.tlsRoots,
|
||||
}
|
||||
|
||||
log.Printf("number of certs - %d", len(parsedCerts))
|
||||
if len(parsedCerts) > 1 {
|
||||
// set up an intermediate
|
||||
pool := x509.NewCertPool()
|
||||
for _, cert := range parsedCerts[1:] {
|
||||
log.Printf("got an intermediate cert")
|
||||
pool.AddCert(cert)
|
||||
}
|
||||
opts.Intermediates = pool
|
||||
log.Info("tls: number of certs: %d", len(parsedCerts))
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
for _, cert := range parsedCerts[1:] {
|
||||
log.Info("tls: got an intermediate cert")
|
||||
pool.AddCert(cert)
|
||||
}
|
||||
|
||||
// TODO: save it as a warning rather than error it out -- shouldn't be a big problem
|
||||
opts.Intermediates = pool
|
||||
|
||||
mainCert := parsedCerts[0]
|
||||
_, err := mainCert.Verify(opts)
|
||||
_, err = mainCert.Verify(opts)
|
||||
if err != nil {
|
||||
// let self-signed certs through
|
||||
data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err)
|
||||
// Let self-signed certs through and don't return this error.
|
||||
status.WarningValidation = fmt.Sprintf("certificate does not verify: %s", err)
|
||||
} else {
|
||||
data.ValidChain = true
|
||||
status.ValidChain = true
|
||||
}
|
||||
// spew.Dump(chains)
|
||||
|
||||
// update status
|
||||
if mainCert != nil {
|
||||
notAfter := mainCert.NotAfter
|
||||
data.Subject = mainCert.Subject.String()
|
||||
data.Issuer = mainCert.Issuer.String()
|
||||
data.NotAfter = notAfter
|
||||
data.NotBefore = mainCert.NotBefore
|
||||
data.DNSNames = mainCert.DNSNames
|
||||
status.Subject = mainCert.Subject.String()
|
||||
status.Issuer = mainCert.Issuer.String()
|
||||
status.NotAfter = mainCert.NotAfter
|
||||
status.NotBefore = mainCert.NotBefore
|
||||
status.DNSNames = mainCert.DNSNames
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePkey(data *tlsConfigStatus, pkey string) error {
|
||||
// now do a more extended validation
|
||||
var key *pem.Block // PEM-encoded certificates
|
||||
// parsePEMCerts parses multiple PEM-encoded certificates.
|
||||
func parsePEMCerts(certs []*pem.Block) (parsedCerts []*x509.Certificate, err error) {
|
||||
for i, cert := range certs {
|
||||
var parsed *x509.Certificate
|
||||
parsed, err = x509.ParseCertificate(cert.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing certificate at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
// go through all pem blocks, but take first valid pem block and drop the rest
|
||||
parsedCerts = append(parsedCerts, parsed)
|
||||
}
|
||||
|
||||
if len(parsedCerts) == 0 {
|
||||
return nil, errors.Error("empty certificate")
|
||||
}
|
||||
|
||||
return parsedCerts, nil
|
||||
}
|
||||
|
||||
// validatePKey validates the private key and sets data in status. The returned
|
||||
// error is also set in status.WarningValidation.
|
||||
func validatePKey(status *tlsConfigStatus, pkey []byte) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
status.WarningValidation = err.Error()
|
||||
}
|
||||
}()
|
||||
|
||||
var key *pem.Block
|
||||
|
||||
// Go through all pem blocks, but take first valid pem block and drop the
|
||||
// rest.
|
||||
pemblock := []byte(pkey)
|
||||
for {
|
||||
var decoded *pem.Block
|
||||
@@ -544,61 +597,77 @@ func validatePkey(data *tlsConfigStatus, pkey string) error {
|
||||
}
|
||||
|
||||
if key == nil {
|
||||
data.WarningValidation = "No valid keys were found"
|
||||
|
||||
return errors.Error(data.WarningValidation)
|
||||
return errors.Error("no valid keys were found")
|
||||
}
|
||||
|
||||
// parse the decoded key
|
||||
_, keyType, err := parsePrivateKey(key.Bytes)
|
||||
if err != nil {
|
||||
data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err)
|
||||
|
||||
return errors.Error(data.WarningValidation)
|
||||
} else if keyType == keyTypeED25519 {
|
||||
data.WarningValidation = "ED25519 keys are not supported by browsers; " +
|
||||
"did you mean to use X25519 for key exchange?"
|
||||
|
||||
return errors.Error(data.WarningValidation)
|
||||
return fmt.Errorf("parsing private key: %w", err)
|
||||
}
|
||||
|
||||
data.ValidKey = true
|
||||
data.KeyType = keyType
|
||||
if keyType == keyTypeED25519 {
|
||||
return errors.Error(
|
||||
"ED25519 keys are not supported by browsers; " +
|
||||
"did you mean to use X25519 for key exchange?",
|
||||
)
|
||||
}
|
||||
|
||||
status.ValidKey = true
|
||||
status.KeyType = keyType
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCertificates processes certificate data and its private key. All
|
||||
// parameters are optional. On error, validateCertificates returns a partially
|
||||
// set object with field WarningValidation containing error description.
|
||||
func validateCertificates(certChain, pkey, serverName string) tlsConfigStatus {
|
||||
var data tlsConfigStatus
|
||||
|
||||
// check only public certificate separately from the key
|
||||
if certChain != "" {
|
||||
if verifyCertChain(&data, certChain, serverName) != nil {
|
||||
return data
|
||||
// parameters are optional. status must not be nil. The returned error is also
|
||||
// set in status.WarningValidation.
|
||||
func validateCertificates(
|
||||
status *tlsConfigStatus,
|
||||
certChain []byte,
|
||||
pkey []byte,
|
||||
serverName string,
|
||||
) (err error) {
|
||||
defer func() {
|
||||
// Capitalize the warning for the UI. Assume that warnings are all
|
||||
// ASCII-only.
|
||||
//
|
||||
// TODO(a.garipov): Figure out a better way to do this. Perhaps a
|
||||
// custom string or error type.
|
||||
if w := status.WarningValidation; w != "" {
|
||||
status.WarningValidation = strings.ToUpper(w[:1]) + w[1:]
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// validate private key (right now the only validation possible is just parsing it)
|
||||
if pkey != "" {
|
||||
if validatePkey(&data, pkey) != nil {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
// if both are set, validate both in unison
|
||||
if pkey != "" && certChain != "" {
|
||||
_, err := tls.X509KeyPair([]byte(certChain), []byte(pkey))
|
||||
// Check only the public certificate separately from the key.
|
||||
if len(certChain) > 0 {
|
||||
err = validateCertChain(status, certChain, serverName)
|
||||
if err != nil {
|
||||
data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err)
|
||||
return data
|
||||
return err
|
||||
}
|
||||
data.ValidPair = true
|
||||
}
|
||||
|
||||
return data
|
||||
// Validate the private key by parsing it.
|
||||
if len(pkey) > 0 {
|
||||
err = validatePKey(status, pkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If both are set, validate together.
|
||||
if len(certChain) > 0 && len(pkey) > 0 {
|
||||
_, err = tls.X509KeyPair(certChain, pkey)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("certificate-key pair: %w", err)
|
||||
status.WarningValidation = err.Error()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
status.ValidPair = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Key types.
|
||||
@@ -693,52 +762,9 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
|
||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
||||
}
|
||||
|
||||
// registerWebHandlers registers HTTP handlers for TLS configuration
|
||||
func (t *TLSMod) registerWebHandlers() {
|
||||
httpRegister(http.MethodGet, "/control/tls/status", t.handleTLSStatus)
|
||||
httpRegister(http.MethodPost, "/control/tls/configure", t.handleTLSConfigure)
|
||||
httpRegister(http.MethodPost, "/control/tls/validate", t.handleTLSValidate)
|
||||
}
|
||||
|
||||
// LoadSystemRootCAs tries to load root certificates from the operating system.
|
||||
// It returns nil in case nothing is found so that that Go.crypto will use it's
|
||||
// default algorithm to find system root CA list.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/internal/issues/1311.
|
||||
func LoadSystemRootCAs() (roots *x509.CertPool) {
|
||||
// TODO(e.burkov): Use build tags instead.
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Directories with the system root certificates, which aren't supported
|
||||
// by Go.crypto.
|
||||
dirs := []string{
|
||||
// Entware.
|
||||
"/opt/etc/ssl/certs",
|
||||
}
|
||||
roots = x509.NewCertPool()
|
||||
for _, dir := range dirs {
|
||||
dirEnts, err := os.ReadDir(dir)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.Error("opening directory: %q: %s", dir, err)
|
||||
}
|
||||
|
||||
var rootsAdded bool
|
||||
for _, de := range dirEnts {
|
||||
var certData []byte
|
||||
certData, err = os.ReadFile(filepath.Join(dir, de.Name()))
|
||||
if err == nil && roots.AppendCertsFromPEM(certData) {
|
||||
rootsAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
if rootsAdded {
|
||||
return roots
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
// registerWebHandlers registers HTTP handlers for TLS configuration.
|
||||
func (m *tlsManager) registerWebHandlers() {
|
||||
httpRegister(http.MethodGet, "/control/tls/status", m.handleTLSStatus)
|
||||
httpRegister(http.MethodPost, "/control/tls/configure", m.handleTLSConfigure)
|
||||
httpRegister(http.MethodPost, "/control/tls/validate", m.handleTLSValidate)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
CertificateChain = `-----BEGIN CERTIFICATE-----
|
||||
var testCertChainData = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV
|
||||
BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3
|
||||
MDkyNDIzWhcNNDYwNzE0MDkyNDIzWjAtMRQwEgYDVQQKDAtBZEd1YXJkIEx0ZDEV
|
||||
@@ -21,8 +20,9 @@ eKO029jYd2AAZEQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQB8
|
||||
LwlXfbakf7qkVTlCNXgoY7RaJ8rJdPgOZPoCTVToEhT6u/cb1c2qp8QB0dNExDna
|
||||
b0Z+dnODTZqQOJo6z/wIXlcUrnR4cQVvytXt8lFn+26l6Y6EMI26twC/xWr+1swq
|
||||
Muj4FeWHVDerquH4yMr1jsYLD3ci+kc5sbIX6TfVxQ==
|
||||
-----END CERTIFICATE-----`
|
||||
PrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
var testPrivateKeyData = []byte(`-----BEGIN PRIVATE KEY-----
|
||||
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALC/BSc8mI68tw5p
|
||||
aYa7pjrySwWvXeetcFywOWHGVfLw9qiFWLdfESa3Y6tWMpZAXD9t1Xh9n211YUBV
|
||||
FGSB4ZshnM/tgEPU6t787lJD4NsIIRp++MkJxdAitN4oUTqL0bdpIwezQ/CrYuBX
|
||||
@@ -37,36 +37,43 @@ An/jMjZSMCxNl6UyFcqt5Et1EGVhuFECQQCZLXxaT+qcyHjlHJTMzuMgkz1QFbEp
|
||||
O5EX70gpeGQMPDK0QSWpaazg956njJSDbNCFM4BccrdQbJu1cW4qOsfBAkAMgZuG
|
||||
O88slmgTRHX4JGFmy3rrLiHNI2BbJSuJ++Yllz8beVzh6NfvuY+HKRCmPqoBPATU
|
||||
kXS9jgARhhiWXJrk
|
||||
-----END PRIVATE KEY-----`
|
||||
)
|
||||
-----END PRIVATE KEY-----`)
|
||||
|
||||
func TestValidateCertificates(t *testing.T) {
|
||||
t.Run("bad_certificate", func(t *testing.T) {
|
||||
data := validateCertificates("bad cert", "", "")
|
||||
assert.NotEmpty(t, data.WarningValidation)
|
||||
assert.False(t, data.ValidCert)
|
||||
assert.False(t, data.ValidChain)
|
||||
status := &tlsConfigStatus{}
|
||||
err := validateCertificates(status, []byte("bad cert"), nil, "")
|
||||
assert.Error(t, err)
|
||||
assert.NotEmpty(t, status.WarningValidation)
|
||||
assert.False(t, status.ValidCert)
|
||||
assert.False(t, status.ValidChain)
|
||||
})
|
||||
|
||||
t.Run("bad_private_key", func(t *testing.T) {
|
||||
data := validateCertificates("", "bad priv key", "")
|
||||
assert.NotEmpty(t, data.WarningValidation)
|
||||
assert.False(t, data.ValidKey)
|
||||
status := &tlsConfigStatus{}
|
||||
err := validateCertificates(status, nil, []byte("bad priv key"), "")
|
||||
assert.Error(t, err)
|
||||
assert.NotEmpty(t, status.WarningValidation)
|
||||
assert.False(t, status.ValidKey)
|
||||
})
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
data := validateCertificates(CertificateChain, PrivateKey, "")
|
||||
notBefore, _ := time.Parse(time.RFC3339, "2019-02-27T09:24:23Z")
|
||||
notAfter, _ := time.Parse(time.RFC3339, "2046-07-14T09:24:23Z")
|
||||
assert.NotEmpty(t, data.WarningValidation)
|
||||
assert.True(t, data.ValidCert)
|
||||
assert.False(t, data.ValidChain)
|
||||
assert.True(t, data.ValidKey)
|
||||
assert.Equal(t, "RSA", data.KeyType)
|
||||
assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", data.Subject)
|
||||
assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", data.Issuer)
|
||||
assert.Equal(t, notBefore, data.NotBefore)
|
||||
assert.Equal(t, notAfter, data.NotAfter)
|
||||
assert.True(t, data.ValidPair)
|
||||
status := &tlsConfigStatus{}
|
||||
err := validateCertificates(status, testCertChainData, testPrivateKeyData, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
notBefore := time.Date(2019, 2, 27, 9, 24, 23, 0, time.UTC)
|
||||
notAfter := time.Date(2046, 7, 14, 9, 24, 23, 0, time.UTC)
|
||||
|
||||
assert.NotEmpty(t, status.WarningValidation)
|
||||
assert.True(t, status.ValidCert)
|
||||
assert.False(t, status.ValidChain)
|
||||
assert.True(t, status.ValidKey)
|
||||
assert.Equal(t, "RSA", status.KeyType)
|
||||
assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", status.Subject)
|
||||
assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", status.Issuer)
|
||||
assert.Equal(t, notBefore, status.NotBefore)
|
||||
assert.Equal(t, notAfter, status.NotAfter)
|
||||
assert.True(t, status.ValidPair)
|
||||
})
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -39,7 +39,7 @@ type webConfig struct {
|
||||
clientFS fs.FS
|
||||
clientBetaFS fs.FS
|
||||
|
||||
BindHost net.IP
|
||||
BindHost netip.Addr
|
||||
BindPort int
|
||||
BetaBindPort int
|
||||
PortHTTPS int
|
||||
@@ -137,8 +137,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
|
||||
|
||||
63
internal/next/agh/agh.go
Normal file
63
internal/next/agh/agh.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Package agh contains common entities and interfaces of AdGuard Home.
|
||||
package agh
|
||||
|
||||
import "context"
|
||||
|
||||
// Service is the interface for API servers.
|
||||
//
|
||||
// TODO(a.garipov): Consider adding a context to Start.
|
||||
//
|
||||
// TODO(a.garipov): Consider adding a Wait method or making an extension
|
||||
// interface for that.
|
||||
type Service interface {
|
||||
// Start starts the service. It does not block.
|
||||
Start() (err error)
|
||||
|
||||
// Shutdown gracefully stops the service. ctx is used to determine
|
||||
// a timeout before trying to stop the service less gracefully.
|
||||
Shutdown(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Service = EmptyService{}
|
||||
|
||||
// EmptyService is a [Service] that does nothing.
|
||||
//
|
||||
// TODO(a.garipov): Remove if unnecessary.
|
||||
type EmptyService struct{}
|
||||
|
||||
// Start implements the [Service] interface for EmptyService.
|
||||
func (EmptyService) Start() (err error) { return nil }
|
||||
|
||||
// Shutdown implements the [Service] interface for EmptyService.
|
||||
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
||||
|
||||
// ServiceWithConfig is an extension of the [Service] interface for services
|
||||
// that can return their configuration.
|
||||
//
|
||||
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
||||
// how to make it testable in a better way.
|
||||
type ServiceWithConfig[ConfigType any] interface {
|
||||
Service
|
||||
|
||||
Config() (c ConfigType)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)
|
||||
|
||||
// EmptyServiceWithConfig is a ServiceWithConfig that does nothing. Its Config
|
||||
// method returns Conf.
|
||||
//
|
||||
// TODO(a.garipov): Remove if unnecessary.
|
||||
type EmptyServiceWithConfig[ConfigType any] struct {
|
||||
EmptyService
|
||||
|
||||
Conf ConfigType
|
||||
}
|
||||
|
||||
// Config implements the [ServiceWithConfig] interface for
|
||||
// *EmptyServiceWithConfig.
|
||||
func (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {
|
||||
return s.Conf
|
||||
}
|
||||
@@ -8,39 +8,49 @@ import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Main is the entry point of application.
|
||||
func Main(clientBuildFS fs.FS) {
|
||||
// # Initial Configuration
|
||||
// Initial Configuration
|
||||
|
||||
start := time.Now()
|
||||
rand.Seed(start.UnixNano())
|
||||
|
||||
// TODO(a.garipov): Set up logging.
|
||||
|
||||
// # Web Service
|
||||
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
|
||||
|
||||
// Web Service
|
||||
|
||||
// TODO(a.garipov): Use in the Web service.
|
||||
_ = clientBuildFS
|
||||
|
||||
// TODO(a.garipov): Make configurable.
|
||||
web := websvc.New(&websvc.Config{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
|
||||
Start: start,
|
||||
Timeout: 60 * time.Second,
|
||||
})
|
||||
// TODO(a.garipov): Set up configuration file name.
|
||||
const confFile = "AdGuardHome.1.yaml"
|
||||
|
||||
err := web.Start()
|
||||
confMgr, err := configmgr.New(confFile, start)
|
||||
fatalOnError(err)
|
||||
|
||||
web := confMgr.Web()
|
||||
err = web.Start()
|
||||
fatalOnError(err)
|
||||
|
||||
dns := confMgr.DNS()
|
||||
err = dns.Start()
|
||||
fatalOnError(err)
|
||||
|
||||
sigHdlr := newSignalHandler(
|
||||
confFile,
|
||||
start,
|
||||
web,
|
||||
dns,
|
||||
)
|
||||
|
||||
go sigHdlr.handle()
|
||||
118
internal/next/cmd/signal.go
Normal file
118
internal/next/cmd/signal.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// signalHandler processes incoming signals and shuts services down.
|
||||
type signalHandler struct {
|
||||
// signal is the channel to which OS signals are sent.
|
||||
signal chan os.Signal
|
||||
|
||||
// confFile is the path to the configuration file.
|
||||
confFile string
|
||||
|
||||
// start is the time at which AdGuard Home has been started.
|
||||
start time.Time
|
||||
|
||||
// services are the services that are shut down before application exiting.
|
||||
services []agh.Service
|
||||
}
|
||||
|
||||
// handle processes OS signals.
|
||||
func (h *signalHandler) handle() {
|
||||
defer log.OnPanic("signalHandler.handle")
|
||||
|
||||
for sig := range h.signal {
|
||||
log.Info("sighdlr: received signal %q", sig)
|
||||
|
||||
if aghos.IsReconfigureSignal(sig) {
|
||||
h.reconfigure()
|
||||
} else if aghos.IsShutdownSignal(sig) {
|
||||
status := h.shutdown()
|
||||
log.Info("sighdlr: exiting with status %d", status)
|
||||
|
||||
os.Exit(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reconfigure rereads the configuration file and updates and restarts services.
|
||||
func (h *signalHandler) reconfigure() {
|
||||
log.Info("sighdlr: reconfiguring adguard home")
|
||||
|
||||
status := h.shutdown()
|
||||
if status != statusSuccess {
|
||||
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
|
||||
|
||||
os.Exit(status)
|
||||
}
|
||||
|
||||
// TODO(a.garipov): This is a very rough way to do it. Some services can be
|
||||
// reconfigured without the full shutdown, and the error handling is
|
||||
// currently not the best.
|
||||
|
||||
confMgr, err := configmgr.New(h.confFile, h.start)
|
||||
fatalOnError(err)
|
||||
|
||||
web := confMgr.Web()
|
||||
err = web.Start()
|
||||
fatalOnError(err)
|
||||
|
||||
dns := confMgr.DNS()
|
||||
err = dns.Start()
|
||||
fatalOnError(err)
|
||||
|
||||
h.services = []agh.Service{
|
||||
dns,
|
||||
web,
|
||||
}
|
||||
|
||||
log.Info("sighdlr: successfully reconfigured adguard home")
|
||||
}
|
||||
|
||||
// Exit status constants.
|
||||
const (
|
||||
statusSuccess = 0
|
||||
statusError = 1
|
||||
)
|
||||
|
||||
// shutdown gracefully shuts down all services.
|
||||
func (h *signalHandler) shutdown() (status int) {
|
||||
ctx, cancel := ctxWithDefaultTimeout()
|
||||
defer cancel()
|
||||
|
||||
status = statusSuccess
|
||||
|
||||
log.Info("sighdlr: shutting down services")
|
||||
for i, service := range h.services {
|
||||
err := service.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Error("sighdlr: shutting down service at index %d: %s", i, err)
|
||||
status = statusError
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
||||
func newSignalHandler(confFile string, start time.Time, svcs ...agh.Service) (h *signalHandler) {
|
||||
h = &signalHandler{
|
||||
signal: make(chan os.Signal, 1),
|
||||
confFile: confFile,
|
||||
start: start,
|
||||
services: svcs,
|
||||
}
|
||||
|
||||
aghos.NotifyShutdownSignal(h.signal)
|
||||
aghos.NotifyReconfigureSignal(h.signal)
|
||||
|
||||
return h
|
||||
}
|
||||
40
internal/next/configmgr/config.go
Normal file
40
internal/next/configmgr/config.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package configmgr
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
)
|
||||
|
||||
// Configuration Structures
|
||||
|
||||
// config is the top-level on-disk configuration structure.
|
||||
type config struct {
|
||||
DNS *dnsConfig `yaml:"dns"`
|
||||
HTTP *httpConfig `yaml:"http"`
|
||||
// TODO(a.garipov): Use.
|
||||
SchemaVersion int `yaml:"schema_version"`
|
||||
// TODO(a.garipov): Use.
|
||||
DebugPprof bool `yaml:"debug_pprof"`
|
||||
Verbose bool `yaml:"verbose"`
|
||||
}
|
||||
|
||||
// dnsConfig is the on-disk DNS configuration.
|
||||
//
|
||||
// TODO(a.garipov): Validate.
|
||||
type dnsConfig struct {
|
||||
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||
BootstrapDNS []string `yaml:"bootstrap_dns"`
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
|
||||
}
|
||||
|
||||
// httpConfig is the on-disk web API configuration.
|
||||
//
|
||||
// TODO(a.garipov): Validate.
|
||||
type httpConfig struct {
|
||||
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
|
||||
Timeout timeutil.Duration `yaml:"timeout"`
|
||||
ForceHTTPS bool `yaml:"force_https"`
|
||||
}
|
||||
205
internal/next/configmgr/configmgr.go
Normal file
205
internal/next/configmgr/configmgr.go
Normal file
@@ -0,0 +1,205 @@
|
||||
// Package configmgr defines the AdGuard Home on-disk configuration entities and
|
||||
// configuration manager.
|
||||
package configmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Configuration Manager
|
||||
|
||||
// Manager handles full and partial changes in the configuration, persisting
|
||||
// them to disk if necessary.
|
||||
type Manager struct {
|
||||
// updMu makes sure that at most one reconfiguration is performed at a time.
|
||||
// updMu protects all fields below.
|
||||
updMu *sync.RWMutex
|
||||
|
||||
// dns is the DNS service.
|
||||
dns *dnssvc.Service
|
||||
|
||||
// Web is the Web API service.
|
||||
web *websvc.Service
|
||||
|
||||
// current is the current configuration.
|
||||
current *config
|
||||
|
||||
// fileName is the name of the configuration file.
|
||||
fileName string
|
||||
}
|
||||
|
||||
// New creates a new *Manager that persists changes to the file pointed to by
|
||||
// fileName. It reads the configuration file and populates the service fields.
|
||||
// start is the startup time of AdGuard Home.
|
||||
func New(fileName string, start time.Time) (m *Manager, err error) {
|
||||
defer func() { err = errors.Annotate(err, "reading config") }()
|
||||
|
||||
conf := &config{}
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, f.Close()) }()
|
||||
|
||||
err = yaml.NewDecoder(f).Decode(conf)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Move into a separate function and add other logging
|
||||
// settings.
|
||||
if conf.Verbose {
|
||||
log.SetLevel(log.DEBUG)
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Validate the configuration structure. Return an error
|
||||
// if it's incorrect.
|
||||
|
||||
m = &Manager{
|
||||
updMu: &sync.RWMutex{},
|
||||
current: conf,
|
||||
fileName: fileName,
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Get the context with the timeout from the arguments?
|
||||
const assemblyTimeout = 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = m.assemble(ctx, conf, start)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// assemble creates all services and puts them into the corresponding fields.
|
||||
// The fields of conf must not be modified after calling assemble.
|
||||
func (m *Manager) assemble(ctx context.Context, conf *config, start time.Time) (err error) {
|
||||
dnsConf := &dnssvc.Config{
|
||||
Addresses: conf.DNS.Addresses,
|
||||
BootstrapServers: conf.DNS.BootstrapDNS,
|
||||
UpstreamServers: conf.DNS.UpstreamDNS,
|
||||
UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration,
|
||||
}
|
||||
err = m.updateDNS(ctx, dnsConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("assembling dnssvc: %w", err)
|
||||
}
|
||||
|
||||
webSvcConf := &websvc.Config{
|
||||
ConfigManager: m,
|
||||
// TODO(a.garipov): Fill from config file.
|
||||
TLS: nil,
|
||||
Start: start,
|
||||
Addresses: conf.HTTP.Addresses,
|
||||
SecureAddresses: conf.HTTP.SecureAddresses,
|
||||
Timeout: conf.HTTP.Timeout.Duration,
|
||||
ForceHTTPS: conf.HTTP.ForceHTTPS,
|
||||
}
|
||||
|
||||
err = m.updateWeb(ctx, webSvcConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("assembling websvc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DNS returns the current DNS service. It is safe for concurrent use.
|
||||
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
m.updMu.RLock()
|
||||
defer m.updMu.RUnlock()
|
||||
|
||||
return m.dns
|
||||
}
|
||||
|
||||
// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager. The
|
||||
// fields of c must not be modified after calling UpdateDNS.
|
||||
func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
m.updMu.Lock()
|
||||
defer m.updMu.Unlock()
|
||||
|
||||
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||
// error if something went wrong.
|
||||
|
||||
err = m.updateDNS(ctx, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reassembling dnssvc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
|
||||
func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
if prev := m.dns; prev != nil {
|
||||
err = prev.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down dns svc: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
svc, err := dnssvc.New(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating dns svc: %w", err)
|
||||
}
|
||||
|
||||
m.dns = svc
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Web returns the current web service. It is safe for concurrent use.
|
||||
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
|
||||
m.updMu.RLock()
|
||||
defer m.updMu.RUnlock()
|
||||
|
||||
return m.web
|
||||
}
|
||||
|
||||
// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager. The
|
||||
// fields of c must not be modified after calling UpdateWeb.
|
||||
func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
m.updMu.Lock()
|
||||
defer m.updMu.Unlock()
|
||||
|
||||
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||
// error if something went wrong.
|
||||
|
||||
err = m.updateWeb(ctx, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reassembling websvc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateWeb recreates the web service. m.upd is expected to be locked.
|
||||
func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
if prev := m.web; prev != nil {
|
||||
err = prev.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down web svc: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.web = websvc.New(c)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
|
||||
// and replacement of module dnsproxy.
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
@@ -47,6 +48,14 @@ type Config struct {
|
||||
// Service is the AdGuard Home DNS service. A nil *Service is a valid
|
||||
// [agh.Service] that does nothing.
|
||||
type Service struct {
|
||||
// running is an atomic boolean value. Keep it the first value in the
|
||||
// struct to ensure atomic alignment. 0 means that the service is not
|
||||
// running, 1 means that it is running.
|
||||
//
|
||||
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19 or get rid of it
|
||||
// completely.
|
||||
running uint64
|
||||
|
||||
proxy *proxy.Proxy
|
||||
bootstraps []string
|
||||
upstreams []string
|
||||
@@ -160,6 +169,17 @@ func (svc *Service) Start() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to
|
||||
// tell when all servers are actually up, so at best this is merely an
|
||||
// assumption.
|
||||
if err != nil {
|
||||
atomic.StoreUint64(&svc.running, 0)
|
||||
} else {
|
||||
atomic.StoreUint64(&svc.running, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
return svc.proxy.Start()
|
||||
}
|
||||
|
||||
@@ -173,13 +193,27 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||
return svc.proxy.Stop()
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the web service.
|
||||
// Config returns the current configuration of the web service. Config must not
|
||||
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||
// addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
func (svc *Service) Config() (c *Config) {
|
||||
// TODO(a.garipov): Do we need to get the TCP addresses separately?
|
||||
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
||||
addrs := make([]netip.AddrPort, len(udpAddrs))
|
||||
for i, a := range udpAddrs {
|
||||
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
||||
|
||||
var addrs []netip.AddrPort
|
||||
if atomic.LoadUint64(&svc.running) == 1 {
|
||||
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
||||
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||
for i, a := range udpAddrs {
|
||||
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
||||
}
|
||||
} else {
|
||||
conf := svc.proxy.Config
|
||||
udpAddrs := conf.UDPListenAddr
|
||||
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||
for i, a := range udpAddrs {
|
||||
addrs[i] = a.AddrPort()
|
||||
}
|
||||
}
|
||||
|
||||
c = &Config{
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
84
internal/next/websvc/dns.go
Normal file
84
internal/next/websvc/dns.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
)
|
||||
|
||||
// DNS Settings Handlers
|
||||
|
||||
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
||||
// HTTP API.
|
||||
type ReqPatchSettingsDNS struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
BootstrapServers []string `json:"bootstrap_servers"`
|
||||
UpstreamServers []string `json:"upstream_servers"`
|
||||
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||
}
|
||||
|
||||
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
|
||||
// DnsSettings object in the OpenAPI specification.
|
||||
type HTTPAPIDNSSettings struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
BootstrapServers []string `json:"bootstrap_servers"`
|
||||
UpstreamServers []string `json:"upstream_servers"`
|
||||
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||
}
|
||||
|
||||
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
|
||||
// API.
|
||||
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
|
||||
req := &ReqPatchSettingsDNS{
|
||||
Addresses: []netip.AddrPort{},
|
||||
BootstrapServers: []string{},
|
||||
UpstreamServers: []string{},
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newConf := &dnssvc.Config{
|
||||
Addresses: req.Addresses,
|
||||
BootstrapServers: req.BootstrapServers,
|
||||
UpstreamServers: req.UpstreamServers,
|
||||
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newSvc := svc.confMgr.DNS()
|
||||
err = newSvc.Start()
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
|
||||
Addresses: newConf.Addresses,
|
||||
BootstrapServers: newConf.BootstrapServers,
|
||||
UpstreamServers: newConf.UpstreamServers,
|
||||
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
|
||||
})
|
||||
}
|
||||
69
internal/next/websvc/dns_test.go
Normal file
69
internal/next/websvc/dns_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")},
|
||||
BootstrapServers: []string{"1.0.0.1"},
|
||||
UpstreamServers: []string{"1.1.1.1"},
|
||||
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
||||
var numStarted uint64
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||
OnStart: func() (err error) {
|
||||
atomic.AddUint64(&numStarted, 1)
|
||||
|
||||
return nil
|
||||
},
|
||||
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
|
||||
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
|
||||
}
|
||||
}
|
||||
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsDNS,
|
||||
}
|
||||
|
||||
req := jobj{
|
||||
"addresses": wantDNS.Addresses,
|
||||
"bootstrap_servers": wantDNS.BootstrapServers,
|
||||
"upstream_servers": wantDNS.UpstreamServers,
|
||||
"upstream_timeout": wantDNS.UpstreamTimeout,
|
||||
}
|
||||
|
||||
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||
resp := &websvc.HTTPAPIDNSSettings{}
|
||||
err := json.Unmarshal(respBody, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, uint64(1), numStarted)
|
||||
assert.Equal(t, wantDNS, resp)
|
||||
assert.Equal(t, wantDNS, resp)
|
||||
}
|
||||
110
internal/next/websvc/http.go
Normal file
110
internal/next/websvc/http.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// HTTP Settings Handlers
|
||||
|
||||
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
||||
// HTTP API.
|
||||
type ReqPatchSettingsHTTP struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
//
|
||||
// TODO(a.garipov): Add wait time.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||
Timeout JSONDuration `json:"timeout"`
|
||||
}
|
||||
|
||||
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
||||
// HttpSettings object in the OpenAPI specification.
|
||||
type HTTPAPIHTTPSettings struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||
Timeout JSONDuration `json:"timeout"`
|
||||
ForceHTTPS bool `json:"force_https"`
|
||||
}
|
||||
|
||||
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
|
||||
// HTTP API.
|
||||
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
req := &ReqPatchSettingsHTTP{}
|
||||
|
||||
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newConf := &Config{
|
||||
ConfigManager: svc.confMgr,
|
||||
TLS: svc.tls,
|
||||
Addresses: req.Addresses,
|
||||
SecureAddresses: req.SecureAddresses,
|
||||
Timeout: time.Duration(req.Timeout),
|
||||
ForceHTTPS: svc.forceHTTPS,
|
||||
}
|
||||
|
||||
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
|
||||
Addresses: newConf.Addresses,
|
||||
SecureAddresses: newConf.SecureAddresses,
|
||||
Timeout: JSONDuration(newConf.Timeout),
|
||||
ForceHTTPS: newConf.ForceHTTPS,
|
||||
})
|
||||
|
||||
cancelUpd := func() {}
|
||||
updCtx := context.Background()
|
||||
|
||||
ctx := r.Context()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
updCtx, cancelUpd = context.WithDeadline(updCtx, deadline)
|
||||
}
|
||||
|
||||
// Launch the new HTTP service in a separate goroutine to let this handler
|
||||
// finish and thus, this server to shutdown.
|
||||
go func() {
|
||||
defer cancelUpd()
|
||||
|
||||
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
|
||||
if updErr != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Consider better ways to do this.
|
||||
const maxUpdDur = 10 * time.Second
|
||||
updStart := time.Now()
|
||||
var newSvc agh.ServiceWithConfig[*Config]
|
||||
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||
if time.Since(updStart) >= maxUpdDur {
|
||||
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("websvc: waiting for new websvc to be configured")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
updErr = newSvc.Start()
|
||||
if updErr != nil {
|
||||
log.Error("websvc: new svc failed to start with error: %s", updErr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
63
internal/next/websvc/http_test.go
Normal file
63
internal/next/websvc/http_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
||||
Timeout: websvc.JSONDuration(10 * time.Second),
|
||||
ForceHTTPS: false,
|
||||
}
|
||||
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||
return websvc.New(&websvc.Config{
|
||||
TLS: &tls.Config{
|
||||
Certificates: []tls.Certificate{{}},
|
||||
},
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||
Timeout: 5 * time.Second,
|
||||
ForceHTTPS: true,
|
||||
})
|
||||
}
|
||||
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsHTTP,
|
||||
}
|
||||
|
||||
req := jobj{
|
||||
"addresses": wantWeb.Addresses,
|
||||
"secure_addresses": wantWeb.SecureAddresses,
|
||||
"timeout": wantWeb.Timeout,
|
||||
"force_https": wantWeb.ForceHTTPS,
|
||||
}
|
||||
|
||||
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||
resp := &websvc.HTTPAPIHTTPSettings{}
|
||||
err := json.Unmarshal(respBody, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, wantWeb, resp)
|
||||
}
|
||||
143
internal/next/websvc/json.go
Normal file
143
internal/next/websvc/json.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// JSON Utilities
|
||||
|
||||
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||
|
||||
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
|
||||
// into JSON according to our API conventions.
|
||||
type JSONDuration time.Duration
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = JSONDuration(0)
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
|
||||
// always nil.
|
||||
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Duration(d)) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*JSONDuration)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
|
||||
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
|
||||
if d == nil {
|
||||
return fmt.Errorf("json duration is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*d = JSONDuration(int64(msec * nsecPerMsec))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||
// according to our API conventions.
|
||||
type JSONTime time.Time
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = JSONTime{}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
|
||||
// always nil.
|
||||
func (t JSONTime) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*JSONTime)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
|
||||
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
||||
if t == nil {
|
||||
return fmt.Errorf("json time is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
|
||||
// and logs any errors it encounters. r is used to get additional information
|
||||
// from the request.
|
||||
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
||||
writeJSONResponse(w, r, v, http.StatusOK)
|
||||
}
|
||||
|
||||
// writeJSONResponse writes headers with code, encodes v into w, and logs any
|
||||
// errors it encounters. r is used to get additional information from the
|
||||
// request.
|
||||
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
||||
// TODO(a.garipov): Put some of these to a middleware.
|
||||
h := w.Header()
|
||||
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
|
||||
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
|
||||
|
||||
w.WriteHeader(code)
|
||||
|
||||
err := json.NewEncoder(w).Encode(v)
|
||||
if err != nil {
|
||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
||||
// definition in the OpenAPI specification.
|
||||
type ErrorCode string
|
||||
|
||||
// ErrorCode constants.
|
||||
//
|
||||
// TODO(a.garipov): Expand and document codes.
|
||||
const (
|
||||
// ErrorCodeTMP000 is the temporary error code used for all errors.
|
||||
ErrorCodeTMP000 = ""
|
||||
)
|
||||
|
||||
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
|
||||
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
|
||||
// specification.
|
||||
type HTTPAPIErrorResp struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
|
||||
// errors it encounters. r is used to get additional information from the
|
||||
// request.
|
||||
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
||||
|
||||
writeJSONResponse(w, r, &HTTPAPIErrorResp{
|
||||
Code: ErrorCodeTMP000,
|
||||
Msg: err.Error(),
|
||||
}, http.StatusUnprocessableEntity)
|
||||
}
|
||||
114
internal/next/websvc/json_test.go
Normal file
114
internal/next/websvc/json_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testJSONTime is the JSON time for tests.
|
||||
var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
||||
|
||||
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
||||
const testJSONTimeStr = "1234567890123.456"
|
||||
|
||||
func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
in websvc.JSONTime
|
||||
want []byte
|
||||
}{{
|
||||
name: "unix_zero",
|
||||
wantErrMsg: "",
|
||||
in: websvc.JSONTime(time.Unix(0, 0)),
|
||||
want: []byte("0"),
|
||||
}, {
|
||||
name: "empty",
|
||||
wantErrMsg: "",
|
||||
in: websvc.JSONTime{},
|
||||
want: []byte("-6795364578871.345"),
|
||||
}, {
|
||||
name: "time",
|
||||
wantErrMsg: "",
|
||||
in: testJSONTime,
|
||||
want: []byte(testJSONTimeStr),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.in.MarshalJSON()
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
in := &struct {
|
||||
A websvc.JSONTime
|
||||
}{
|
||||
A: testJSONTime,
|
||||
}
|
||||
|
||||
got, err := json.Marshal(in)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErrMsg string
|
||||
want websvc.JSONTime
|
||||
data []byte
|
||||
}{{
|
||||
name: "time",
|
||||
wantErrMsg: "",
|
||||
want: testJSONTime,
|
||||
data: []byte(testJSONTimeStr),
|
||||
}, {
|
||||
name: "bad",
|
||||
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||
`invalid syntax`,
|
||||
want: websvc.JSONTime{},
|
||||
data: []byte(`{}`),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got websvc.JSONTime
|
||||
err := got.UnmarshalJSON(tc.data)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
||||
require.Error(t, err)
|
||||
|
||||
msg := err.Error()
|
||||
assert.Equal(t, "json time is nil", msg)
|
||||
})
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
want := testJSONTime
|
||||
var got struct {
|
||||
A websvc.JSONTime
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, want, got.A)
|
||||
})
|
||||
}
|
||||
11
internal/next/websvc/path.go
Normal file
11
internal/next/websvc/path.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package websvc
|
||||
|
||||
// Path constants
|
||||
const (
|
||||
PathHealthCheck = "/health-check"
|
||||
|
||||
PathV1SettingsAll = "/api/v1/settings/all"
|
||||
PathV1SettingsDNS = "/api/v1/settings/dns"
|
||||
PathV1SettingsHTTP = "/api/v1/settings/http"
|
||||
PathV1SystemInfo = "/api/v1/system/info"
|
||||
)
|
||||
42
internal/next/websvc/settings.go
Normal file
42
internal/next/websvc/settings.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// All Settings Handlers
|
||||
|
||||
// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all
|
||||
// HTTP API.
|
||||
type RespGetV1SettingsAll struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
DNS *HTTPAPIDNSSettings `json:"dns"`
|
||||
HTTP *HTTPAPIHTTPSettings `json:"http"`
|
||||
}
|
||||
|
||||
// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP
|
||||
// API.
|
||||
func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {
|
||||
dnsSvc := svc.confMgr.DNS()
|
||||
dnsConf := dnsSvc.Config()
|
||||
|
||||
webSvc := svc.confMgr.Web()
|
||||
httpConf := webSvc.Config()
|
||||
|
||||
// TODO(a.garipov): Add all currently supported parameters.
|
||||
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
|
||||
DNS: &HTTPAPIDNSSettings{
|
||||
Addresses: dnsConf.Addresses,
|
||||
BootstrapServers: dnsConf.BootstrapServers,
|
||||
UpstreamServers: dnsConf.UpstreamServers,
|
||||
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
|
||||
},
|
||||
HTTP: &HTTPAPIHTTPSettings{
|
||||
Addresses: httpConf.Addresses,
|
||||
SecureAddresses: httpConf.SecureAddresses,
|
||||
Timeout: JSONDuration(httpConf.Timeout),
|
||||
ForceHTTPS: httpConf.ForceHTTPS,
|
||||
},
|
||||
})
|
||||
}
|
||||
75
internal/next/websvc/settings_test.go
Normal file
75
internal/next/websvc/settings_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||
// TODO(a.garipov): Add all currently supported parameters.
|
||||
|
||||
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
||||
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
||||
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
||||
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
|
||||
}
|
||||
|
||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||
Timeout: websvc.JSONDuration(5 * time.Second),
|
||||
ForceHTTPS: true,
|
||||
}
|
||||
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
c, err := dnssvc.New(&dnssvc.Config{
|
||||
Addresses: wantDNS.Addresses,
|
||||
UpstreamServers: wantDNS.UpstreamServers,
|
||||
BootstrapServers: wantDNS.BootstrapServers,
|
||||
UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||
return websvc.New(&websvc.Config{
|
||||
TLS: &tls.Config{
|
||||
Certificates: []tls.Certificate{{}},
|
||||
},
|
||||
Addresses: wantWeb.Addresses,
|
||||
SecureAddresses: wantWeb.SecureAddresses,
|
||||
Timeout: time.Duration(wantWeb.Timeout),
|
||||
ForceHTTPS: true,
|
||||
})
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsAll,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
resp := &websvc.RespGetV1SettingsAll{}
|
||||
err := json.Unmarshal(body, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, wantDNS, resp.DNS)
|
||||
assert.Equal(t, wantWeb, resp.HTTP)
|
||||
}
|
||||
@@ -16,20 +16,20 @@ type RespGetV1SystemInfo struct {
|
||||
Channel string `json:"channel"`
|
||||
OS string `json:"os"`
|
||||
NewVersion string `json:"new_version,omitempty"`
|
||||
Start jsonTime `json:"start"`
|
||||
Start JSONTime `json:"start"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
||||
// API.
|
||||
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSONResponse(w, r, &RespGetV1SystemInfo{
|
||||
writeJSONOKResponse(w, r, &RespGetV1SystemInfo{
|
||||
Arch: runtime.GOARCH,
|
||||
Channel: version.Channel(),
|
||||
OS: runtime.GOOS,
|
||||
// TODO(a.garipov): Fill this when we have an updater.
|
||||
NewVersion: "",
|
||||
Start: jsonTime(svc.start),
|
||||
Start: JSONTime(svc.start),
|
||||
Version: version.Version(),
|
||||
})
|
||||
}
|
||||
@@ -8,16 +8,17 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
||||
_, addr := newTestServer(t)
|
||||
confMgr := newConfigManager()
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr,
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SystemInfo,
|
||||
}
|
||||
|
||||
31
internal/next/websvc/waitlistener.go
Normal file
31
internal/next/websvc/waitlistener.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Wait Listener
|
||||
|
||||
// waitListener is a wrapper around a listener that also calls wg.Done() on the
|
||||
// first call to Accept. It is useful in situations where it is important to
|
||||
// catch the precise moment of the first call to Accept, for example when
|
||||
// starting an HTTP server.
|
||||
//
|
||||
// TODO(a.garipov): Move to aghnet?
|
||||
type waitListener struct {
|
||||
net.Listener
|
||||
|
||||
firstAcceptWG *sync.WaitGroup
|
||||
firstAcceptOnce sync.Once
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ net.Listener = (*waitListener)(nil)
|
||||
|
||||
// Accept implements the [net.Listener] interface for *waitListener.
|
||||
func (l *waitListener) Accept() (conn net.Conn, err error) {
|
||||
l.firstAcceptOnce.Do(l.firstAcceptWG.Done)
|
||||
|
||||
return l.Listener.Accept()
|
||||
}
|
||||
46
internal/next/websvc/waitlistener_internal_test.go
Normal file
46
internal/next/websvc/waitlistener_internal_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWaitListener_Accept(t *testing.T) {
|
||||
// TODO(a.garipov): use atomic.Bool in Go 1.19.
|
||||
var numAcceptCalls uint32
|
||||
var l net.Listener = &aghtest.Listener{
|
||||
OnAccept: func() (conn net.Conn, err error) {
|
||||
atomic.AddUint32(&numAcceptCalls, 1)
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
OnAddr: func() (addr net.Addr) { panic("not implemented") },
|
||||
OnClose: func() (err error) { panic("not implemented") },
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
done := make(chan struct{})
|
||||
go aghchan.MustReceive(done, testTimeout)
|
||||
|
||||
go func() {
|
||||
var wrapper net.Listener = &waitListener{
|
||||
Listener: l,
|
||||
firstAcceptWG: wg,
|
||||
}
|
||||
|
||||
_, _ = wrapper.Accept()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(done)
|
||||
|
||||
assert.Equal(t, uint32(1), atomic.LoadUint32(&numAcceptCalls))
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
// Package websvc contains the AdGuard Home web service.
|
||||
// Package websvc contains the AdGuard Home HTTP API service.
|
||||
//
|
||||
// NOTE: Packages other than cmd must not import this package, as it imports
|
||||
// most other packages.
|
||||
//
|
||||
// TODO(a.garipov): Add tests.
|
||||
package websvc
|
||||
@@ -14,18 +17,35 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
||||
)
|
||||
|
||||
// ConfigManager is the configuration manager interface.
|
||||
type ConfigManager interface {
|
||||
DNS() (svc agh.ServiceWithConfig[*dnssvc.Config])
|
||||
Web() (svc agh.ServiceWithConfig[*Config])
|
||||
|
||||
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
|
||||
UpdateWeb(ctx context.Context, c *Config) (err error)
|
||||
}
|
||||
|
||||
// Config is the AdGuard Home web service configuration structure.
|
||||
type Config struct {
|
||||
// ConfigManager is used to show information about services as well as
|
||||
// dynamically reconfigure them.
|
||||
ConfigManager ConfigManager
|
||||
|
||||
// TLS is the optional TLS configuration. If TLS is not nil,
|
||||
// SecureAddresses must not be empty.
|
||||
TLS *tls.Config
|
||||
|
||||
// Start is the time of start of AdGuard Home.
|
||||
Start time.Time
|
||||
|
||||
// Addresses are the addresses on which to serve the plain HTTP API.
|
||||
Addresses []netip.AddrPort
|
||||
|
||||
@@ -33,40 +53,48 @@ type Config struct {
|
||||
// SecureAddresses is not empty, TLS must not be nil.
|
||||
SecureAddresses []netip.AddrPort
|
||||
|
||||
// Start is the time of start of AdGuard Home.
|
||||
Start time.Time
|
||||
|
||||
// Timeout is the timeout for all server operations.
|
||||
Timeout time.Duration
|
||||
|
||||
// ForceHTTPS tells if all requests to Addresses should be redirected to a
|
||||
// secure address instead.
|
||||
//
|
||||
// TODO(a.garipov): Use; define rules, which address to redirect to.
|
||||
ForceHTTPS bool
|
||||
}
|
||||
|
||||
// Service is the AdGuard Home web service. A nil *Service is a valid
|
||||
// [agh.Service] that does nothing.
|
||||
type Service struct {
|
||||
tls *tls.Config
|
||||
servers []*http.Server
|
||||
start time.Time
|
||||
timeout time.Duration
|
||||
confMgr ConfigManager
|
||||
tls *tls.Config
|
||||
start time.Time
|
||||
servers []*http.Server
|
||||
timeout time.Duration
|
||||
forceHTTPS bool
|
||||
}
|
||||
|
||||
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
||||
// *Service that does nothing.
|
||||
// *Service that does nothing. The fields of c must not be modified after
|
||||
// calling New.
|
||||
func New(c *Config) (svc *Service) {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
svc = &Service{
|
||||
tls: c.TLS,
|
||||
start: c.Start,
|
||||
timeout: c.Timeout,
|
||||
confMgr: c.ConfigManager,
|
||||
tls: c.TLS,
|
||||
start: c.Start,
|
||||
timeout: c.Timeout,
|
||||
forceHTTPS: c.ForceHTTPS,
|
||||
}
|
||||
|
||||
mux := newMux(svc)
|
||||
|
||||
for _, a := range c.Addresses {
|
||||
addr := a.String()
|
||||
errLog := log.StdLog("websvc: http: "+addr, log.ERROR)
|
||||
errLog := log.StdLog("websvc: plain http: "+addr, log.ERROR)
|
||||
svc.servers = append(svc.servers, &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
@@ -111,6 +139,21 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||
method: http.MethodGet,
|
||||
path: PathHealthCheck,
|
||||
isJSON: false,
|
||||
}, {
|
||||
handler: svc.handleGetSettingsAll,
|
||||
method: http.MethodGet,
|
||||
path: PathV1SettingsAll,
|
||||
isJSON: true,
|
||||
}, {
|
||||
handler: svc.handlePatchSettingsDNS,
|
||||
method: http.MethodPatch,
|
||||
path: PathV1SettingsDNS,
|
||||
isJSON: true,
|
||||
}, {
|
||||
handler: svc.handlePatchSettingsHTTP,
|
||||
method: http.MethodPatch,
|
||||
path: PathV1SettingsHTTP,
|
||||
isJSON: true,
|
||||
}, {
|
||||
handler: svc.handleGetV1SystemInfo,
|
||||
method: http.MethodGet,
|
||||
@@ -119,29 +162,41 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||
}}
|
||||
|
||||
for _, r := range routes {
|
||||
var h http.HandlerFunc
|
||||
if r.isJSON {
|
||||
// TODO(a.garipov): Consider using httptreemux's MiddlewareFunc.
|
||||
h = jsonMw(r.handler)
|
||||
mux.Handle(r.method, r.path, jsonMw(r.handler))
|
||||
} else {
|
||||
h = r.handler
|
||||
mux.Handle(r.method, r.path, r.handler)
|
||||
}
|
||||
|
||||
mux.Handle(r.method, r.path, h)
|
||||
}
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// Addrs returns all addresses on which this server serves the HTTP API. Addrs
|
||||
// must not be called until Start returns.
|
||||
func (svc *Service) Addrs() (addrs []string) {
|
||||
addrs = make([]string, 0, len(svc.servers))
|
||||
// addrs returns all addresses on which this server serves the HTTP API. addrs
|
||||
// must not be called simultaneously with Start. If svc was initialized with
|
||||
// ":0" addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||
for _, srv := range svc.servers {
|
||||
addrs = append(addrs, srv.Addr)
|
||||
addrPort, err := netip.ParseAddrPort(srv.Addr)
|
||||
if err != nil {
|
||||
// Technically shouldn't happen, since all servers must have a valid
|
||||
// address.
|
||||
panic(fmt.Errorf("websvc: server %q: bad address: %w", srv.Addr, err))
|
||||
}
|
||||
|
||||
// srv.Serve will set TLSConfig to an almost empty value, so, instead of
|
||||
// relying only on the nilness of TLSConfig, check the length of the
|
||||
// certificates field as well.
|
||||
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
|
||||
addrs = append(addrs, addrPort)
|
||||
} else {
|
||||
secureAddrs = append(secureAddrs, addrPort)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return addrs
|
||||
return addrs, secureAddrs
|
||||
}
|
||||
|
||||
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
||||
@@ -149,9 +204,6 @@ func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request)
|
||||
_, _ = io.WriteString(w, "OK")
|
||||
}
|
||||
|
||||
// unit is a convenient alias for struct{}.
|
||||
type unit = struct{}
|
||||
|
||||
// type check
|
||||
var _ agh.Service = (*Service)(nil)
|
||||
|
||||
@@ -163,11 +215,9 @@ func (svc *Service) Start() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
srvs := svc.servers
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(srvs))
|
||||
for _, srv := range srvs {
|
||||
wg.Add(len(svc.servers))
|
||||
for _, srv := range svc.servers {
|
||||
go serve(srv, wg)
|
||||
}
|
||||
|
||||
@@ -181,11 +231,14 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
||||
addr := srv.Addr
|
||||
defer log.OnPanic(addr)
|
||||
|
||||
var proto string
|
||||
var l net.Listener
|
||||
var err error
|
||||
if srv.TLSConfig == nil {
|
||||
proto = "http"
|
||||
l, err = net.Listen("tcp", addr)
|
||||
} else {
|
||||
proto = "https"
|
||||
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -196,8 +249,12 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
||||
// would mean that a random available port was automatically chosen.
|
||||
srv.Addr = l.Addr().String()
|
||||
|
||||
log.Info("websvc: starting srv http://%s", srv.Addr)
|
||||
wg.Done()
|
||||
log.Info("websvc: starting srv %s://%s", proto, srv.Addr)
|
||||
|
||||
l = &waitListener{
|
||||
Listener: l,
|
||||
firstAcceptWG: wg,
|
||||
}
|
||||
|
||||
err = srv.Serve(l)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
@@ -221,8 +278,28 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.List("shutting down")
|
||||
return errors.List("shutting down", errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the web service. Config must not
|
||||
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||
// addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
func (svc *Service) Config() (c *Config) {
|
||||
c = &Config{
|
||||
ConfigManager: svc.confMgr,
|
||||
TLS: svc.tls,
|
||||
// Leave Addresses and SecureAddresses empty and get the actual
|
||||
// addresses that include the :0 ones later.
|
||||
Start: svc.start,
|
||||
Timeout: svc.timeout,
|
||||
ForceHTTPS: svc.forceHTTPS,
|
||||
}
|
||||
|
||||
c.Addresses, c.SecureAddresses = svc.addrs()
|
||||
|
||||
return c
|
||||
}
|
||||
6
internal/next/websvc/websvc_internal_test.go
Normal file
6
internal/next/websvc/websvc_internal_test.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package websvc
|
||||
|
||||
import "time"
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
188
internal/next/websvc/websvc_test.go
Normal file
188
internal/next/websvc/websvc_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// testStart is the server start value for tests.
|
||||
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// type check
|
||||
var _ websvc.ConfigManager = (*configManager)(nil)
|
||||
|
||||
// configManager is a [websvc.ConfigManager] for tests.
|
||||
type configManager struct {
|
||||
onDNS func() (svc agh.ServiceWithConfig[*dnssvc.Config])
|
||||
onWeb func() (svc agh.ServiceWithConfig[*websvc.Config])
|
||||
|
||||
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
|
||||
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
|
||||
}
|
||||
|
||||
// DNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
return m.onDNS()
|
||||
}
|
||||
|
||||
// Web implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) Web() (svc agh.ServiceWithConfig[*websvc.Config]) {
|
||||
return m.onWeb()
|
||||
}
|
||||
|
||||
// UpdateDNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
return m.onUpdateDNS(ctx, c)
|
||||
}
|
||||
|
||||
// UpdateWeb implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
return m.onUpdateWeb(ctx, c)
|
||||
}
|
||||
|
||||
// newConfigManager returns a *configManager all methods of which panic.
|
||||
func newConfigManager() (m *configManager) {
|
||||
return &configManager{
|
||||
onDNS: func() (svc agh.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
|
||||
onWeb: func() (svc agh.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
|
||||
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
onUpdateWeb: func(_ context.Context, _ *websvc.Config) (err error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newTestServer creates and starts a new web service instance as well as its
|
||||
// sole address. It also registers a cleanup procedure, which shuts the
|
||||
// instance down.
|
||||
//
|
||||
// TODO(a.garipov): Use svc or remove it.
|
||||
func newTestServer(
|
||||
t testing.TB,
|
||||
confMgr websvc.ConfigManager,
|
||||
) (svc *websvc.Service, addr netip.AddrPort) {
|
||||
t.Helper()
|
||||
|
||||
c := &websvc.Config{
|
||||
ConfigManager: confMgr,
|
||||
TLS: nil,
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||
SecureAddresses: nil,
|
||||
Timeout: testTimeout,
|
||||
Start: testStart,
|
||||
ForceHTTPS: false,
|
||||
}
|
||||
|
||||
svc = websvc.New(c)
|
||||
|
||||
err := svc.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err = svc.Shutdown(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
c = svc.Config()
|
||||
require.NotNil(t, c)
|
||||
require.Len(t, c.Addresses, 1)
|
||||
|
||||
return svc, c.Addresses[0]
|
||||
}
|
||||
|
||||
// jobj is a utility alias for JSON objects.
|
||||
type jobj map[string]any
|
||||
|
||||
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
||||
// the response as well as checks that the status code is correct.
|
||||
//
|
||||
// TODO(a.garipov): Add helpers for other methods.
|
||||
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
||||
t.Helper()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoErrorf(t, err, "creating req")
|
||||
|
||||
httpCli := &http.Client{
|
||||
Timeout: testTimeout,
|
||||
}
|
||||
resp, err := httpCli.Do(req)
|
||||
require.NoErrorf(t, err, "performing req")
|
||||
require.Equal(t, wantCode, resp.StatusCode)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoErrorf(t, err, "reading body")
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
// httpPatch is a helper that performs an HTTP PATCH request with JSON-encoded
|
||||
// reqBody as the request body and returns the body of the response as well as
|
||||
// checks that the status code is correct.
|
||||
//
|
||||
// TODO(a.garipov): Add helpers for other methods.
|
||||
func httpPatch(t testing.TB, u *url.URL, reqBody any, wantCode int) (body []byte) {
|
||||
t.Helper()
|
||||
|
||||
b, err := json.Marshal(reqBody)
|
||||
require.NoErrorf(t, err, "marshaling reqBody")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(b))
|
||||
require.NoErrorf(t, err, "creating req")
|
||||
|
||||
httpCli := &http.Client{
|
||||
Timeout: testTimeout,
|
||||
}
|
||||
resp, err := httpCli.Do(req)
|
||||
require.NoErrorf(t, err, "performing req")
|
||||
require.Equal(t, wantCode, resp.StatusCode)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoErrorf(t, err, "reading body")
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
func TestService_Start_getHealthCheck(t *testing.T) {
|
||||
confMgr := newConfigManager()
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathHealthCheck,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package querylog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -48,24 +47,7 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
||||
// convert log entries to JSON
|
||||
data := l.entriesToJSON(entries, oldest)
|
||||
|
||||
jsonVal, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
aghhttp.Error(
|
||||
r,
|
||||
w,
|
||||
http.StatusInternalServerError,
|
||||
"Couldn't marshal data into json: %s",
|
||||
err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(jsonVal)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
||||
}
|
||||
|
||||
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
||||
@@ -74,23 +56,13 @@ func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
// Get configuration
|
||||
func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
|
||||
resp := qlogConfig{}
|
||||
resp.Enabled = l.conf.Enabled
|
||||
resp.Interval = l.conf.RotationIvl.Hours() / 24
|
||||
resp.AnonymizeClientIP = l.conf.AnonymizeClientIP
|
||||
|
||||
jsonVal, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
|
||||
return
|
||||
resp := qlogConfig{
|
||||
Enabled: l.conf.Enabled,
|
||||
Interval: l.conf.RotationIvl.Hours() / 24,
|
||||
AnonymizeClientIP: l.conf.AnonymizeClientIP,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err = w.Write(jsonVal)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "http write: %s", err)
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.
|
||||
|
||||
@@ -55,12 +55,7 @@ func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
// configResp is the response to the GET /control/stats_info.
|
||||
@@ -71,13 +66,7 @@ type configResp struct {
|
||||
// handleStatsInfo handles requests to the GET /control/stats_info endpoint.
|
||||
func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
|
||||
resp := configResp{IntervalDays: atomic.LoadUint32(&s.limitHours) / 24}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
}
|
||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||
}
|
||||
|
||||
// handleStatsConfig handles requests to the POST /control/stats_config
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Package agh contains common entities and interfaces of AdGuard Home.
|
||||
//
|
||||
// TODO(a.garipov): Move to the upper-level internal/.
|
||||
package agh
|
||||
|
||||
import "context"
|
||||
|
||||
// Service is the interface for API servers.
|
||||
//
|
||||
// TODO(a.garipov): Consider adding a context to Start.
|
||||
//
|
||||
// TODO(a.garipov): Consider adding a Wait method or making an extension
|
||||
// interface for that.
|
||||
type Service interface {
|
||||
// Start starts the service. It does not block.
|
||||
Start() (err error)
|
||||
|
||||
// Shutdown gracefully stops the service. ctx is used to determine
|
||||
// a timeout before trying to stop the service less gracefully.
|
||||
Shutdown(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Service = EmptyService{}
|
||||
|
||||
// EmptyService is a Service that does nothing.
|
||||
type EmptyService struct{}
|
||||
|
||||
// Start implements the Service interface for EmptyService.
|
||||
func (EmptyService) Start() (err error) { return nil }
|
||||
|
||||
// Shutdown implements the Service interface for EmptyService.
|
||||
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
||||
@@ -1,70 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// signalHandler processes incoming signals and shuts services down.
|
||||
type signalHandler struct {
|
||||
signal chan os.Signal
|
||||
|
||||
// services are the services that are shut down before application
|
||||
// exiting.
|
||||
services []agh.Service
|
||||
}
|
||||
|
||||
// handle processes OS signals.
|
||||
func (h *signalHandler) handle() {
|
||||
defer log.OnPanic("signalHandler.handle")
|
||||
|
||||
for sig := range h.signal {
|
||||
log.Info("sighdlr: received signal %q", sig)
|
||||
|
||||
if aghos.IsShutdownSignal(sig) {
|
||||
h.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exit status constants.
|
||||
const (
|
||||
statusSuccess = 0
|
||||
statusError = 1
|
||||
)
|
||||
|
||||
// shutdown gracefully shuts down all services.
|
||||
func (h *signalHandler) shutdown() {
|
||||
ctx, cancel := ctxWithDefaultTimeout()
|
||||
defer cancel()
|
||||
|
||||
status := statusSuccess
|
||||
|
||||
log.Info("sighdlr: shutting down services")
|
||||
for i, service := range h.services {
|
||||
err := service.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Error("sighdlr: shutting down service at index %d: %s", i, err)
|
||||
status = statusError
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("sighdlr: shutting down adguard home")
|
||||
|
||||
os.Exit(status)
|
||||
}
|
||||
|
||||
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
||||
func newSignalHandler(svcs ...agh.Service) (h *signalHandler) {
|
||||
h = &signalHandler{
|
||||
signal: make(chan os.Signal, 1),
|
||||
services: svcs,
|
||||
}
|
||||
|
||||
aghos.NotifyShutdownSignal(h.signal)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// JSON Utilities
|
||||
|
||||
// jsonTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||
// according to our API conventions.
|
||||
type jsonTime time.Time
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = jsonTime{}
|
||||
|
||||
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for jsonTime. err is
|
||||
// always nil.
|
||||
func (t jsonTime) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', 3, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*jsonTime)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *jsonTime.
|
||||
func (t *jsonTime) UnmarshalJSON(b []byte) (err error) {
|
||||
if t == nil {
|
||||
return fmt.Errorf("json time is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*t = jsonTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeJSONResponse encodes v into w and logs any errors it encounters. r is
|
||||
// used to get additional information from the request.
|
||||
func writeJSONResponse(w io.Writer, r *http.Request, v any) {
|
||||
err := json.NewEncoder(w).Encode(v)
|
||||
if err != nil {
|
||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package websvc
|
||||
|
||||
// Path constants
|
||||
const (
|
||||
PathHealthCheck = "/health-check"
|
||||
|
||||
PathV1SystemInfo = "/api/v1/system/info"
|
||||
)
|
||||
@@ -1,93 +0,0 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// testStart is the server start value for tests.
|
||||
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// newTestServer creates and starts a new web service instance as well as its
|
||||
// sole address. It also registers a cleanup procedure, which shuts the
|
||||
// instance down.
|
||||
//
|
||||
// TODO(a.garipov): Use svc or remove it.
|
||||
func newTestServer(t testing.TB) (svc *websvc.Service, addr string) {
|
||||
t.Helper()
|
||||
|
||||
c := &websvc.Config{
|
||||
TLS: nil,
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||
SecureAddresses: nil,
|
||||
Timeout: testTimeout,
|
||||
Start: testStart,
|
||||
}
|
||||
|
||||
svc = websvc.New(c)
|
||||
|
||||
err := svc.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err = svc.Shutdown(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
addrs := svc.Addrs()
|
||||
require.Len(t, addrs, 1)
|
||||
|
||||
return svc, addrs[0]
|
||||
}
|
||||
|
||||
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
||||
// the response as well as checks that the status code is correct.
|
||||
//
|
||||
// TODO(a.garipov): Add helpers for other methods.
|
||||
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
||||
t.Helper()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoErrorf(t, err, "creating req")
|
||||
|
||||
httpCli := &http.Client{
|
||||
Timeout: testTimeout,
|
||||
}
|
||||
resp, err := httpCli.Do(req)
|
||||
require.NoErrorf(t, err, "performing req")
|
||||
require.Equal(t, wantCode, resp.StatusCode)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoErrorf(t, err, "reading body")
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
func TestService_Start_getHealthCheck(t *testing.T) {
|
||||
_, addr := newTestServer(t)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr,
|
||||
Path: websvc.PathHealthCheck,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
}
|
||||
@@ -63,14 +63,6 @@ func Version() (v string) {
|
||||
return version
|
||||
}
|
||||
|
||||
// Constants defining the format of module information string.
|
||||
const (
|
||||
modInfoAtSep = "@"
|
||||
modInfoDevSep = " "
|
||||
modInfoSumLeft = " (sum: "
|
||||
modInfoSumRight = ")"
|
||||
)
|
||||
|
||||
// fmtModule returns formatted information about module. The result looks like:
|
||||
//
|
||||
// github.com/Username/module@v1.2.3 (sum: someHASHSUM=)
|
||||
@@ -87,14 +79,16 @@ func fmtModule(m *debug.Module) (formatted string) {
|
||||
|
||||
stringutil.WriteToBuilder(b, m.Path)
|
||||
if ver := m.Version; ver != "" {
|
||||
sep := modInfoAtSep
|
||||
sep := "@"
|
||||
if ver == "(devel)" {
|
||||
sep = modInfoDevSep
|
||||
sep = " "
|
||||
}
|
||||
|
||||
stringutil.WriteToBuilder(b, sep, ver)
|
||||
}
|
||||
|
||||
if sum := m.Sum; sum != "" {
|
||||
stringutil.WriteToBuilder(b, modInfoSumLeft, sum, modInfoSumRight)
|
||||
stringutil.WriteToBuilder(b, "(sum: ", sum, ")")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
|
||||
Reference in New Issue
Block a user