all: aghnet use executil

This commit is contained in:
Stanislav Chzhen 2025-09-03 16:31:55 +03:00
parent 2bffd664f0
commit 3fed265d8f
16 changed files with 318 additions and 268 deletions

View File

@ -13,7 +13,6 @@ import (
"strings"
"syscall"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
@ -27,19 +26,8 @@ type DialContextFunc = func(ctx context.Context, network, addr string) (conn net
// Variables and functions to substitute in tests.
var (
// aghosRunCommand is the function to run shell commands.
//
// TODO(s.chzhen): Use [aghos.RunCommand] directly.
aghosRunCommand = (func() func(string, ...string) (int, []byte, error) {
ctx := context.TODO()
cmdCons := executil.SystemCommandConstructor{}
return func(command string, arguments ...string) (int, []byte, error) {
return aghos.RunCommand(ctx, cmdCons, command, arguments...)
}
})()
// netInterfaces is the function to get the available network interfaces.
// netInterfaceAddrs is the function to get the available network
// interfaces.
netInterfaceAddrs = net.InterfaceAddrs
// rootDirFS is the filesystem pointing to the root directory.
@ -53,32 +41,53 @@ const ErrNoStaticIPInfo errors.Error = "no information about static ip"
// 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.
func IfaceHasStaticIP(ifaceName string) (has bool, err error) {
return ifaceHasStaticIP(ifaceName)
func IfaceHasStaticIP(
ctx context.Context,
cmdCons executil.CommandConstructor,
ifaceName string,
) (has bool, err error) {
return ifaceHasStaticIP(ctx, cmdCons, ifaceName)
}
// IfaceSetStaticIP sets static IP address for network interface.
func IfaceSetStaticIP(ifaceName string) (err error) {
return ifaceSetStaticIP(ifaceName)
func IfaceSetStaticIP(
ctx context.Context,
cmdCons executil.CommandConstructor,
ifaceName string,
) (err error) {
return ifaceSetStaticIP(ctx, cmdCons, ifaceName)
}
// GatewayIP returns IP address of interface's gateway.
//
// 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 netip.Addr) {
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
func GatewayIP(
ctx context.Context,
cmdCons executil.CommandConstructor,
ifaceName string,
) (ip netip.Addr) {
stdout := bytes.Buffer{}
err := executil.Run(
ctx,
cmdCons,
&executil.CommandConfig{
Path: "ip",
Args: []string{"route", "show", "dev", ifaceName},
Stdout: &stdout,
},
)
if err != nil {
log.Debug("%s", err)
return netip.Addr{}
} else if code != 0 {
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
if code, ok := executil.ExitCodeFromError(err); ok {
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
} else {
log.Debug("%s", err)
}
return netip.Addr{}
}
fields := bytes.Fields(out)
fields := bytes.Fields(stdout.Bytes())
// 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" {

View File

@ -5,12 +5,14 @@ package aghnet
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"regexp"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/osutil/executil"
)
// hardwarePortInfo contains information about the current state of the internet
@ -23,8 +25,12 @@ type hardwarePortInfo struct {
static bool
}
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
portInfo, err := getCurrentHardwarePortInfo(ifaceName)
func ifaceHasStaticIP(
ctx context.Context,
cmdCons executil.CommandConstructor,
ifaceName string,
) (ok bool, err error) {
portInfo, err := getCurrentHardwarePortInfo(ctx, cmdCons, ifaceName)
if err != nil {
return false, err
}
@ -34,15 +40,19 @@ func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
// getCurrentHardwarePortInfo gets information for the specified network
// interface.
func getCurrentHardwarePortInfo(ifaceName string) (hardwarePortInfo, error) {
func getCurrentHardwarePortInfo(
ctx context.Context,
cmdCons executil.CommandConstructor,
ifaceName string,
) (hardwarePortInfo, error) {
// First of all we should find hardware port name.
m := getNetworkSetupHardwareReports()
m := getNetworkSetupHardwareReports(ctx, cmdCons)
hardwarePort, ok := m[ifaceName]
if !ok {
return hardwarePortInfo{}, fmt.Errorf("could not find hardware port for %s", ifaceName)
}
return getHardwarePortInfo(hardwarePort)
return getHardwarePortInfo(ctx, cmdCons, hardwarePort)
}
// hardwareReportsReg is the regular expression matching the lines of
@ -57,8 +67,11 @@ var hardwareReportsReg = regexp.MustCompile("Hardware Port: (.*?)\nDevice: (.*?)
// TODO(e.burkov): There should be more proper approach than parsing the
// command output. For example, see
// https://developer.apple.com/documentation/systemconfiguration.
func getNetworkSetupHardwareReports() (reports map[string]string) {
_, out, err := aghosRunCommand("networksetup", "-listallhardwareports")
func getNetworkSetupHardwareReports(
ctx context.Context,
cmdCons executil.CommandConstructor,
) (reports map[string]string) {
_, out, err := aghos.RunCommand(ctx, cmdCons, "networksetup", "-listallhardwareports")
if err != nil {
return nil
}
@ -77,8 +90,12 @@ func getNetworkSetupHardwareReports() (reports map[string]string) {
// command output lines containing the port information.
var hardwarePortReg = regexp.MustCompile("IP address: (.*?)\nSubnet mask: (.*?)\nRouter: (.*?)\n")
func getHardwarePortInfo(hardwarePort string) (h hardwarePortInfo, err error) {
_, out, err := aghosRunCommand("networksetup", "-getinfo", hardwarePort)
func getHardwarePortInfo(
ctx context.Context,
cmdCons executil.CommandConstructor,
hardwarePort string,
) (h hardwarePortInfo, err error) {
_, out, err := aghos.RunCommand(ctx, cmdCons, "networksetup", "-getinfo", hardwarePort)
if err != nil {
return h, err
}
@ -97,8 +114,12 @@ func getHardwarePortInfo(hardwarePort string) (h hardwarePortInfo, err error) {
}, nil
}
func ifaceSetStaticIP(ifaceName string) (err error) {
portInfo, err := getCurrentHardwarePortInfo(ifaceName)
func ifaceSetStaticIP(
ctx context.Context,
cmdCons executil.CommandConstructor,
ifaceName string,
) (err error) {
portInfo, err := getCurrentHardwarePortInfo(ctx, cmdCons, ifaceName)
if err != nil {
return err
}
@ -115,7 +136,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
args := append([]string{"-setdnsservers", portInfo.name}, dnsAddrs...)
// Setting DNS servers is necessary when configuring a static IP
code, _, err := aghosRunCommand("networksetup", args...)
code, _, err := aghos.RunCommand(ctx, cmdCons, "networksetup", args...)
if err != nil {
return err
} else if code != 0 {
@ -123,7 +144,9 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
}
// Actually configures hardware port to have static IP
code, _, err = aghosRunCommand(
code, _, err = aghos.RunCommand(
ctx,
cmdCons,
"networksetup",
"-setmanual",
portInfo.name,

View File

@ -7,7 +7,9 @@ import (
"testing"
"testing/fstest"
"github.com/AdguardTeam/AdGuardHome/internal/agh"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/osutil/executil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/testutil/fakeio/fakefs"
"github.com/stretchr/testify/assert"
@ -16,48 +18,46 @@ import (
func TestIfaceHasStaticIP(t *testing.T) {
testCases := []struct {
name string
shell mapShell
cmdCons executil.CommandConstructor
ifaceName string
wantHas assert.BoolAssertionFunc
wantErrMsg string
}{{
name: "success",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: nil,
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
code: 0,
},
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: nil,
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
Code: 0,
}),
ifaceName: "en0",
wantHas: assert.False,
wantErrMsg: ``,
}, {
name: "success_static",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: nil,
out: "Manual Configuration\nIP address: 1.2.3.4\n" +
"Subnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
code: 0,
},
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: nil,
Out: "Manual Configuration\nIP address: 1.2.3.4\n" +
"Subnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
Code: 0,
}),
ifaceName: "en0",
wantHas: assert.True,
wantErrMsg: ``,
}, {
name: "reports_error",
shell: theOnlyCmd(
cmdCons: agh.NewCommandConstructor(
"networksetup -listallhardwareports",
0,
"",
@ -68,35 +68,33 @@ func TestIfaceHasStaticIP(t *testing.T) {
wantErrMsg: `could not find hardware port for en0`,
}, {
name: "port_error",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: errors.Error("can't get"),
out: ``,
code: 0,
},
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: errors.Error("can't get"),
Out: ``,
Code: 0,
}),
ifaceName: "en0",
wantHas: assert.False,
wantErrMsg: `can't get`,
}, {
name: "port_bad_output",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: nil,
out: "nothing meaningful",
code: 0,
},
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: nil,
Out: "nothing meaningful",
Code: 0,
}),
ifaceName: "en0",
wantHas: assert.False,
wantErrMsg: `could not find hardware port info`,
@ -104,9 +102,8 @@ func TestIfaceHasStaticIP(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
substShell(t, tc.shell.RunCmd)
has, err := IfaceHasStaticIP(tc.ifaceName)
ctx := testutil.ContextWithTimeout(t, testTimeout)
has, err := IfaceHasStaticIP(ctx, tc.cmdCons, tc.ifaceName)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
tc.wantHas(t, has)
@ -126,55 +123,53 @@ func TestIfaceSetStaticIP(t *testing.T) {
testCases := []struct {
name string
shell mapShell
cmdCons executil.CommandConstructor
fsys fs.FS
wantErrMsg string
}{{
name: "success",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: nil,
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
code: 0,
},
"networksetup -setdnsservers hwport 1.1.1.1": {
err: nil,
out: "",
code: 0,
},
"networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1": {
err: nil,
out: "",
code: 0,
},
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: nil,
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -setdnsservers hwport 1.1.1.1",
Err: nil,
Out: "",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1",
Err: nil,
Out: "",
Code: 0,
}),
fsys: succFsys,
wantErrMsg: ``,
}, {
name: "static_already",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: nil,
out: "Manual Configuration\nIP address: 1.2.3.4\n" +
"Subnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
code: 0,
},
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: nil,
Out: "Manual Configuration\nIP address: 1.2.3.4\n" +
"Subnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
Code: 0,
}),
fsys: panicFsys,
wantErrMsg: `ip address is already static`,
}, {
name: "reports_error",
shell: theOnlyCmd(
cmdCons: agh.NewCommandConstructor(
"networksetup -listallhardwareports",
0,
"",
@ -184,18 +179,18 @@ func TestIfaceSetStaticIP(t *testing.T) {
wantErrMsg: `could not find hardware port for en0`,
}, {
name: "resolv_conf_error",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: nil,
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
code: 0,
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: nil,
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
Code: 0,
},
),
fsys: fstest.MapFS{
"etc/resolv.conf": &fstest.MapFile{
Data: []byte("this resolv.conf is invalid"),
@ -204,59 +199,57 @@ func TestIfaceSetStaticIP(t *testing.T) {
wantErrMsg: `found no dns servers in etc/resolv.conf`,
}, {
name: "set_dns_error",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: nil,
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
code: 0,
},
"networksetup -setdnsservers hwport 1.1.1.1": {
err: errors.Error("can't set"),
out: "",
code: 0,
},
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: nil,
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -setdnsservers hwport 1.1.1.1",
Err: errors.Error("can't set"),
Out: "",
Code: 0,
}),
fsys: succFsys,
wantErrMsg: `can't set`,
}, {
name: "set_manual_error",
shell: mapShell{
"networksetup -listallhardwareports": {
err: nil,
out: "Hardware Port: hwport\nDevice: en0\n",
code: 0,
},
"networksetup -getinfo hwport": {
err: nil,
out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
code: 0,
},
"networksetup -setdnsservers hwport 1.1.1.1": {
err: nil,
out: "",
code: 0,
},
"networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1": {
err: errors.Error("can't set"),
out: "",
code: 0,
},
},
cmdCons: agh.NewMultipleCommandConstructor(agh.ExternalCommand{
Cmd: "networksetup -listallhardwareports",
Err: nil,
Out: "Hardware Port: hwport\nDevice: en0\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -getinfo hwport",
Err: nil,
Out: "IP address: 1.2.3.4\nSubnet mask: 255.255.255.0\nRouter: 1.2.3.1\n",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -setdnsservers hwport 1.1.1.1",
Err: nil,
Out: "",
Code: 0,
}, agh.ExternalCommand{
Cmd: "networksetup -setmanual hwport 1.2.3.4 255.255.255.0 1.2.3.1",
Err: errors.Error("can't set"),
Out: "",
Code: 0,
}),
fsys: succFsys,
wantErrMsg: `can't set`,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
substShell(t, tc.shell.RunCmd)
substRootDirFS(t, tc.fsys)
err := IfaceSetStaticIP("en0")
ctx := testutil.ContextWithTimeout(t, testTimeout)
err := IfaceSetStaticIP(ctx, tc.cmdCons, "en0")
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}

View File

@ -4,15 +4,21 @@ package aghnet
import (
"bufio"
"context"
"fmt"
"io"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/osutil/executil"
)
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
func ifaceHasStaticIP(
_ context.Context,
_ executil.CommandConstructor,
ifaceName string,
) (ok bool, err error) {
const rcConfFilename = "etc/rc.conf"
walker := aghos.FileWalker(interfaceName(ifaceName).rcConfStaticConfig)
@ -52,6 +58,6 @@ func (n interfaceName) rcConfStaticConfig(r io.Reader) (_ []string, cont bool, e
return nil, true, s.Err()
}
func ifaceSetStaticIP(string) (err error) {
func ifaceSetStaticIP(_ context.Context, _ executil.CommandConstructor, _ string) (err error) {
return aghos.Unsupported("setting static ip")
}

View File

@ -7,6 +7,8 @@ import (
"testing"
"testing/fstest"
"github.com/AdguardTeam/golibs/osutil/executil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -67,7 +69,8 @@ func TestIfaceHasStaticIP(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
substRootDirFS(t, tc.rootFsys)
has, err := IfaceHasStaticIP(ifaceName)
ctx := testutil.ContextWithTimeout(t, testTimeout)
has, err := IfaceHasStaticIP(ctx, executil.EmptyCommandConstructor{}, ifaceName)
require.NoError(t, err)
tc.wantHas(t, has)

View File

@ -3,20 +3,24 @@ package aghnet
import (
"bytes"
"encoding/json"
"fmt"
"io/fs"
"net"
"net/netip"
"strings"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/agh"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/osutil/executil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second
// substRootDirFS replaces the aghos.RootDirFS function used throughout the
// package with fsys for tests ran under t.
func substRootDirFS(tb testing.TB, fsys fs.FS) {
@ -30,43 +34,6 @@ func substRootDirFS(tb testing.TB, fsys fs.FS) {
// RunCmdFunc is the signature of aghos.RunCommand function.
type RunCmdFunc func(cmd string, args ...string) (code int, out []byte, err error)
// substShell replaces the the aghos.RunCommand function used throughout the
// package with rc for tests ran under t.
func substShell(tb testing.TB, rc RunCmdFunc) {
tb.Helper()
prev := aghosRunCommand
tb.Cleanup(func() { aghosRunCommand = prev })
aghosRunCommand = rc
}
// mapShell is a substitution of aghos.RunCommand that maps the command to it's
// execution result. It's only needed to simplify testing.
//
// TODO(e.burkov): Perhaps put all the shell interactions behind an interface.
type mapShell map[string]struct {
err error
out string
code int
}
// theOnlyCmd returns mapShell that only handles a single command and arguments
// combination from cmd.
func theOnlyCmd(cmd string, code int, out string, err error) (s mapShell) {
return mapShell{cmd: {code: code, out: out, err: err}}
}
// RunCmd is a RunCmdFunc handled by s.
func (s mapShell) RunCmd(cmd string, args ...string) (code int, out []byte, err error) {
key := strings.Join(append([]string{cmd}, args...), " ")
ret, ok := s[key]
if !ok {
return 0, nil, fmt.Errorf("unexpected shell command %q", key)
}
return ret.code, []byte(ret.out), ret.err
}
// ifaceAddrsFunc is the signature of net.InterfaceAddrs function.
type ifaceAddrsFunc func() (ifaces []net.Addr, err error)
@ -85,36 +52,35 @@ func TestGatewayIP(t *testing.T) {
const cmd = "ip route show dev " + ifaceName
testCases := []struct {
shell mapShell
want netip.Addr
name string
cmdCons executil.CommandConstructor
want netip.Addr
name string
}{{
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
want: netip.MustParseAddr("1.2.3.4"),
name: "success_v4",
cmdCons: agh.NewCommandConstructor(cmd, 0, `default via 1.2.3.4 onlink`, nil),
want: netip.MustParseAddr("1.2.3.4"),
name: "success_v4",
}, {
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
want: netip.MustParseAddr("::ffff"),
name: "success_v6",
cmdCons: agh.NewCommandConstructor(cmd, 0, `default via ::ffff onlink`, nil),
want: netip.MustParseAddr("::ffff"),
name: "success_v6",
}, {
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
want: netip.Addr{},
name: "bad_output",
cmdCons: agh.NewCommandConstructor(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
want: netip.Addr{},
name: "bad_output",
}, {
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
want: netip.Addr{},
name: "err_runcmd",
cmdCons: agh.NewCommandConstructor(cmd, 0, "", errors.Error("can't run command")),
want: netip.Addr{},
name: "err_runcmd",
}, {
shell: theOnlyCmd(cmd, 1, "", nil),
want: netip.Addr{},
name: "bad_code",
cmdCons: agh.NewCommandConstructor(cmd, 1, "", nil),
want: netip.Addr{},
name: "bad_code",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
substShell(t, tc.shell.RunCmd)
assert.Equal(t, tc.want, GatewayIP(ifaceName))
ctx := testutil.ContextWithTimeout(t, testTimeout)
assert.Equal(t, tc.want, GatewayIP(ctx, tc.cmdCons, ifaceName))
})
}
}

View File

@ -4,6 +4,7 @@ package aghnet
import (
"bufio"
"context"
"fmt"
"io"
"net/netip"
@ -13,6 +14,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/osutil/executil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/google/renameio/v2/maybe"
"golang.org/x/sys/unix"
@ -104,7 +106,11 @@ func (n interfaceName) ifacesStaticConfig(r io.Reader) (sub []string, cont bool,
return sub, true, s.Err()
}
func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
func ifaceHasStaticIP(
_ context.Context,
_ executil.CommandConstructor,
ifaceName string,
) (has bool, err error) {
// TODO(a.garipov): Currently, this function returns the first definitive
// result. So if /etc/dhcpcd.conf has and /etc/network/interfaces has no
// static IP configuration, it will return true. Perhaps this is not the
@ -149,7 +155,11 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
// ifaceSetStaticIP configures the system to retain its current IP on the
// interface through dhcpcd.conf.
func ifaceSetStaticIP(ifaceName string) (err error) {
func ifaceSetStaticIP(
ctx context.Context,
cmdCons executil.CommandConstructor,
ifaceName string,
) (err error) {
ipNet := GetSubnet(ifaceName)
if !ipNet.Addr().IsValid() {
return errors.Error("can't get IP address")
@ -160,7 +170,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
return err
}
gatewayIP := GatewayIP(ifaceName)
gatewayIP := GatewayIP(ctx, cmdCons, ifaceName)
add := dhcpcdConfIface(ifaceName, ipNet, gatewayIP)
body = append(body, []byte(add)...)

View File

@ -7,6 +7,7 @@ import (
"testing"
"testing/fstest"
"github.com/AdguardTeam/golibs/osutil/executil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
)
@ -117,7 +118,8 @@ func TestHasStaticIP(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
substRootDirFS(t, tc.rootFsys)
has, err := IfaceHasStaticIP(ifaceName)
ctx := testutil.ContextWithTimeout(t, testTimeout)
has, err := IfaceHasStaticIP(ctx, executil.EmptyCommandConstructor{}, ifaceName)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
tc.wantHas(t, has)

View File

@ -4,15 +4,21 @@ package aghnet
import (
"bufio"
"context"
"fmt"
"io"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/osutil/executil"
)
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
func ifaceHasStaticIP(
_ context.Context,
_ executil.CommandConstructor,
ifaceName string,
) (ok bool, err error) {
filename := fmt.Sprintf("etc/hostname.%s", ifaceName)
return aghos.FileWalker(hostnameIfStaticConfig).Walk(rootDirFS, filename)
@ -39,6 +45,6 @@ func hostnameIfStaticConfig(r io.Reader) (_ []string, ok bool, err error) {
return nil, true, s.Err()
}
func ifaceSetStaticIP(string) (err error) {
func ifaceSetStaticIP(_ context.Context, _ executil.CommandConstructor, _ string) (err error) {
return aghos.Unsupported("setting static ip")
}

View File

@ -8,6 +8,8 @@ import (
"testing"
"testing/fstest"
"github.com/AdguardTeam/golibs/osutil/executil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -62,7 +64,8 @@ func TestIfaceHasStaticIP(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
substRootDirFS(t, tc.rootFsys)
has, err := IfaceHasStaticIP(ifaceName)
ctx := testutil.ContextWithTimeout(t, testTimeout)
has, err := IfaceHasStaticIP(ctx, executil.EmptyCommandConstructor{}, ifaceName)
require.NoError(t, err)
tc.wantHas(t, has)

View File

@ -3,12 +3,14 @@
package aghnet
import (
"context"
"io"
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/osutil/executil"
"golang.org/x/sys/windows"
)
@ -16,11 +18,15 @@ func canBindPrivilegedPorts() (can bool, err error) {
return true, nil
}
func ifaceHasStaticIP(string) (ok bool, err error) {
func ifaceHasStaticIP(
_ context.Context,
_ executil.CommandConstructor,
_ string,
) (ok bool, err error) {
return false, aghos.Unsupported("checking static ip")
}
func ifaceSetStaticIP(string) (err error) {
func ifaceSetStaticIP(_ context.Context, _ executil.CommandConstructor, _ string) (err error) {
return aghos.Unsupported("setting static ip")
}

View File

@ -11,11 +11,15 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/osutil/executil"
)
// ServerConfig is the configuration for the DHCP server. The order of YAML
// fields is important, since the YAML configuration file follows it.
type ServerConfig struct {
// CommandConstructor is used to run external commands. It must not be nil.
CommandConstructor executil.CommandConstructor `yaml:"-"`
// ConfModifier is used to update the global configuration. It must not be
// nil.
ConfModifier agh.ConfigModifier `yaml:"-"`

View File

@ -107,7 +107,8 @@ var _ Interface = (*server)(nil)
func Create(conf *ServerConfig) (s *server, err error) {
s = &server{
conf: &ServerConfig{
ConfModifier: conf.ConfModifier,
CommandConstructor: conf.CommandConstructor,
ConfModifier: conf.ConfModifier,
HTTPRegister: conf.HTTPRegister,

View File

@ -3,6 +3,7 @@
package dhcpd
import (
"context"
"encoding/json"
"fmt"
"io"
@ -20,6 +21,7 @@ import (
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/osutil/executil"
)
type v4ServerConfJSON struct {
@ -171,9 +173,10 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
aghhttp.WriteJSONResponseOK(w, r, status)
}
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
func (s *server) enableDHCP(ctx context.Context, ifaceName string) (code int, err error) {
var hasStaticIP bool
hasStaticIP, err = aghnet.IfaceHasStaticIP(ifaceName)
cmdCons := s.conf.CommandConstructor
hasStaticIP, err = aghnet.IfaceHasStaticIP(ctx, cmdCons, ifaceName)
if err != nil {
if errors.Is(err, os.ErrPermission) {
// ErrPermission may happen here on Linux systems where AdGuard Home
@ -202,7 +205,7 @@ func (s *server) enableDHCP(ifaceName string) (code int, err error) {
}
if !hasStaticIP {
err = aghnet.IfaceSetStaticIP(ifaceName)
err = aghnet.IfaceSetStaticIP(ctx, cmdCons, ifaceName)
if err != nil {
err = fmt.Errorf("setting static ip: %w", err)
@ -309,6 +312,8 @@ func (s *server) createServers(conf *dhcpServerConfigJSON) (srv4, srv6 DHCPServe
// handleDHCPSetConfig is the handler for the POST /control/dhcp/set_config
// HTTP API.
func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
conf := &dhcpServerConfigJSON{}
conf.Enabled = aghalg.BoolToNullBool(s.conf.Enabled)
conf.InterfaceName = s.conf.InterfaceName
@ -346,7 +351,7 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
if s.conf.Enabled {
var code int
code, err = s.enableDHCP(conf.InterfaceName)
code, err = s.enableDHCP(ctx, conf.InterfaceName)
if err != nil {
aghhttp.Error(r, w, code, "enabling dhcp: %s", err)
}
@ -405,7 +410,7 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
continue
}
jsonIface, iErr := newNetInterfaceJSON(iface)
jsonIface, iErr := newNetInterfaceJSON(r.Context(), iface, s.conf.CommandConstructor)
if iErr != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", iErr)
@ -421,7 +426,11 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
}
// newNetInterfaceJSON creates a JSON object from a [net.Interface] iface.
func newNetInterfaceJSON(iface net.Interface) (out *netInterfaceJSON, err error) {
func newNetInterfaceJSON(
ctx context.Context,
iface net.Interface,
cmdCons executil.CommandConstructor,
) (out *netInterfaceJSON, err error) {
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf(
@ -473,7 +482,7 @@ func newNetInterfaceJSON(iface net.Interface) (out *netInterfaceJSON, err error)
return nil, nil
}
out.GatewayIP = aghnet.GatewayIP(iface.Name)
out.GatewayIP = aghnet.GatewayIP(ctx, cmdCons, iface.Name)
return out, nil
}
@ -558,7 +567,8 @@ func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
},
}
if isStaticIP, serr := aghnet.IfaceHasStaticIP(ifaceName); serr != nil {
cmdCons := s.conf.CommandConstructor
if isStaticIP, serr := aghnet.IfaceHasStaticIP(r.Context(), cmdCons, ifaceName); serr != nil {
result.V4.StaticIP.Static = "error"
result.V4.StaticIP.Error = serr.Error()
} else if !isStaticIP {

View File

@ -175,6 +175,8 @@ func (req *checkConfReq) validateDNS(
// handleInstallCheckConfig handles the /check_config endpoint.
func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req := &checkConfReq{}
err := json.NewDecoder(r.Body).Decode(req)
@ -190,11 +192,11 @@ func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Reque
resp.Web.Status = err.Error()
}
resp.DNS.CanAutofix, err = req.validateDNS(r.Context(), web.logger, tcpPorts, web.cmdCons)
resp.DNS.CanAutofix, err = req.validateDNS(ctx, web.logger, tcpPorts, web.cmdCons)
if err != nil {
resp.DNS.Status = err.Error()
} else if !req.DNS.IP.IsUnspecified() {
resp.StaticIP = handleStaticIP(req.DNS.IP, req.SetStaticIP)
resp.StaticIP = handleStaticIP(ctx, req.DNS.IP, req.SetStaticIP, web.cmdCons)
}
aghhttp.WriteJSONResponseOK(w, r, resp)
@ -203,7 +205,12 @@ func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Reque
// 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 netip.Addr, set bool) staticIPJSON {
func handleStaticIP(
ctx context.Context,
ip netip.Addr,
set bool,
cmdCons executil.CommandConstructor,
) staticIPJSON {
resp := staticIPJSON{}
interfaceName := aghnet.InterfaceByIP(ip)
@ -217,7 +224,7 @@ func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
if set {
// Try to set static IP for the specified interface
err := aghnet.IfaceSetStaticIP(interfaceName)
err := aghnet.IfaceSetStaticIP(ctx, cmdCons, interfaceName)
if err != nil {
resp.Static = "error"
resp.Error = err.Error()
@ -227,7 +234,7 @@ func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
// Fallthrough here even if we set static IP
// Check if we have a static IP and return the details
isStaticIP, err := aghnet.IfaceHasStaticIP(interfaceName)
isStaticIP, err := aghnet.IfaceHasStaticIP(ctx, cmdCons, interfaceName)
if err != nil {
resp.Static = "error"
resp.Error = err.Error()

View File

@ -306,6 +306,7 @@ func initContextClients(
config.DHCP.WorkDir = globalContext.workDir
config.DHCP.DataDir = globalContext.getDataDir()
config.DHCP.HTTPRegister = httpRegister
config.DHCP.CommandConstructor = executil.SystemCommandConstructor{}
config.DHCP.ConfModifier = confModifier
globalContext.dhcpServer, err = dhcpd.Create(config.DHCP)