Pull request: create aghnet package

Merge in DNS/adguard-home from mk-aghnet to master

Updates #2829.

Squashed commit of the following:

commit 519806c04b8d0517aacce9c31f2d06ab56631937
Merge: 92376c86 97361234
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Mar 16 19:13:56 2021 +0300

    Merge branch 'master' into mk-aghnet

commit 92376c8665e529191aa482432f9d5e3e2e3afdc8
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Mar 16 18:37:22 2021 +0300

    aghnet: fix linux

commit 7f36d19b0e650d4e4fc5cf9ea4b501a7f636abeb
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Mar 16 18:08:30 2021 +0300

    aghnet: mv network utils from util

commit aa68c70c1146b8c32303c6e037953a41aa7d72f9
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Mar 16 17:30:06 2021 +0300

    aghnet: mv ipDetector here

commit b033657f5c5ee91f869c36508a5eb15976a174a0
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Mar 16 17:24:07 2021 +0300

    all: mk aghnet package, rename sysutil package
This commit is contained in:
Eugene Burkov
2021-03-16 19:42:15 +03:00
parent 9736123483
commit 3a67cc2c45
26 changed files with 135 additions and 148 deletions

View File

@@ -0,0 +1,73 @@
package aghnet
import "net"
// IPDetector describes IP address properties.
type IPDetector struct {
nets []*net.IPNet
}
// NewIPDetector returns a new IP detector.
func NewIPDetector() (ipd *IPDetector, err error) {
specialNetworks := []string{
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.0.0/29",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"240.0.0.0/4",
"255.255.255.255/32",
"::1/128",
"::/128",
"64:ff9b::/96",
// Since this network is used for mapping IPv4 addresses, we
// don't include it.
// "::ffff:0:0/96",
"100::/64",
"2001::/23",
"2001::/32",
"2001:2::/48",
"2001:db8::/32",
"2001:10::/28",
"2002::/16",
"fc00::/7",
"fe80::/10",
}
ipd = &IPDetector{
nets: make([]*net.IPNet, len(specialNetworks)),
}
for i, ipnetStr := range specialNetworks {
var ipnet *net.IPNet
_, ipnet, err = net.ParseCIDR(ipnetStr)
if err != nil {
return nil, err
}
ipd.nets[i] = ipnet
}
return ipd, nil
}
// DetectSpecialNetwork returns true if IP address is contained by any of
// special-purpose IP address registries according to RFC-6890
// (https://tools.ietf.org/html/rfc6890).
func (ipd *IPDetector) DetectSpecialNetwork(ip net.IP) bool {
for _, ipnet := range ipd.nets {
if ipnet.Contains(ip) {
return true
}
}
return false
}

View File

@@ -0,0 +1,145 @@
package aghnet
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIPDetector_detectSpecialNetwork(t *testing.T) {
var ipd *IPDetector
var err error
ipd, err = NewIPDetector()
require.Nil(t, err)
testCases := []struct {
name string
ip net.IP
want bool
}{{
name: "not_specific",
ip: net.ParseIP("8.8.8.8"),
want: false,
}, {
name: "this_host_on_this_network",
ip: net.ParseIP("0.0.0.0"),
want: true,
}, {
name: "private-Use",
ip: net.ParseIP("10.0.0.0"),
want: true,
}, {
name: "shared_address_space",
ip: net.ParseIP("100.64.0.0"),
want: true,
}, {
name: "loopback",
ip: net.ParseIP("127.0.0.0"),
want: true,
}, {
name: "link_local",
ip: net.ParseIP("169.254.0.0"),
want: true,
}, {
name: "private-use",
ip: net.ParseIP("172.16.0.0"),
want: true,
}, {
name: "ietf_protocol_assignments",
ip: net.ParseIP("192.0.0.0"),
want: true,
}, {
name: "ds-lite",
ip: net.ParseIP("192.0.0.0"),
want: true,
}, {
name: "documentation_(test-net-1)",
ip: net.ParseIP("192.0.2.0"),
want: true,
}, {
name: "6to4_relay_anycast",
ip: net.ParseIP("192.88.99.0"),
want: true,
}, {
name: "private-use",
ip: net.ParseIP("192.168.0.0"),
want: true,
}, {
name: "benchmarking",
ip: net.ParseIP("198.18.0.0"),
want: true,
}, {
name: "documentation_(test-net-2)",
ip: net.ParseIP("198.51.100.0"),
want: true,
}, {
name: "documentation_(test-net-3)",
ip: net.ParseIP("203.0.113.0"),
want: true,
}, {
name: "reserved",
ip: net.ParseIP("240.0.0.0"),
want: true,
}, {
name: "limited_broadcast",
ip: net.ParseIP("255.255.255.255"),
want: true,
}, {
name: "loopback_address",
ip: net.ParseIP("::1"),
want: true,
}, {
name: "unspecified_address",
ip: net.ParseIP("::"),
want: true,
}, {
name: "ipv4-ipv6_translation",
ip: net.ParseIP("64:ff9b::"),
want: true,
}, {
name: "discard-only_address_block",
ip: net.ParseIP("100::"),
want: true,
}, {
name: "ietf_protocol_assignments",
ip: net.ParseIP("2001::"),
want: true,
}, {
name: "teredo",
ip: net.ParseIP("2001::"),
want: true,
}, {
name: "benchmarking",
ip: net.ParseIP("2001:2::"),
want: true,
}, {
name: "documentation",
ip: net.ParseIP("2001:db8::"),
want: true,
}, {
name: "orchid",
ip: net.ParseIP("2001:10::"),
want: true,
}, {
name: "6to4",
ip: net.ParseIP("2002::"),
want: true,
}, {
name: "unique-local",
ip: net.ParseIP("fc00::"),
want: true,
}, {
name: "linked-scoped_unicast",
ip: net.ParseIP("fe80::"),
want: true,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, ipd.DetectSpecialNetwork(tc.ip))
})
}
}

253
internal/aghnet/net.go Normal file
View File

@@ -0,0 +1,253 @@
// Package aghnet contains some utilities for networking.
package aghnet
import (
"encoding/json"
"errors"
"fmt"
"net"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
"github.com/AdguardTeam/golibs/log"
)
// ErrNoStaticIPInfo is returned by IfaceHasStaticIP when no information about
// the IP being static is available.
const ErrNoStaticIPInfo agherr.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)
}
// IfaceSetStaticIP sets static IP address for network interface.
func IfaceSetStaticIP(ifaceName string) (err error) {
return ifaceSetStaticIP(ifaceName)
}
// GatewayIP returns IP address of interface's gateway.
func GatewayIP(ifaceName string) net.IP {
cmd := exec.Command("ip", "route", "show", "dev", ifaceName)
log.Tracef("executing %s %v", cmd.Path, cmd.Args)
d, err := cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 {
return nil
}
fields := strings.Fields(string(d))
// 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 || fields[0] != "default" {
return nil
}
return net.ParseIP(fields[2])
}
// CanBindPort checks if we can bind to the given port.
func CanBindPort(port int) (can bool, err error) {
var addr *net.TCPAddr
addr, err = net.ResolveTCPAddr("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return false, err
}
var listener *net.TCPListener
listener, err = net.ListenTCP("tcp", addr)
if err != nil {
return false, err
}
_ = listener.Close()
return true, nil
}
// NetInterface represents an entry of network interfaces map.
type NetInterface struct {
MTU int `json:"mtu"`
Name string `json:"name"`
HardwareAddr net.HardwareAddr `json:"hardware_address"`
Flags net.Flags `json:"flags"`
// Array with the network interface addresses.
Addresses []net.IP `json:"ip_addresses,omitempty"`
// Array with IP networks for this network interface.
Subnets []*net.IPNet `json:"-"`
}
// MarshalJSON implements the json.Marshaler interface for *NetInterface.
func (iface *NetInterface) MarshalJSON() ([]byte, error) {
type netInterface NetInterface
return json.Marshal(&struct {
HardwareAddr string `json:"hardware_address"`
Flags string `json:"flags"`
*netInterface
}{
HardwareAddr: iface.HardwareAddr.String(),
Flags: iface.Flags.String(),
netInterface: (*netInterface)(iface),
})
}
// GetValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP
// invalid interface is a ppp interface or the one that doesn't allow broadcasts
func GetValidNetInterfaces() ([]net.Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("couldn't get list of interfaces: %w", err)
}
netIfaces := []net.Interface{}
netIfaces = append(netIfaces, ifaces...)
return netIfaces, nil
}
// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and WEB only
// we do not return link-local addresses here
func GetValidNetInterfacesForWeb() ([]*NetInterface, error) {
ifaces, err := GetValidNetInterfaces()
if err != nil {
return nil, fmt.Errorf("couldn't get interfaces: %w", err)
}
if len(ifaces) == 0 {
return nil, errors.New("couldn't find any legible interface")
}
var netInterfaces []*NetInterface
for _, iface := range ifaces {
var addrs []net.Addr
addrs, err = iface.Addrs()
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 iface.Addrs() element %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 {
netInterfaces = append(netInterfaces, netIface)
}
}
return netInterfaces, nil
}
// GetInterfaceByIP returns the name of interface containing provided ip.
func GetInterfaceByIP(ip net.IP) string {
ifaces, err := GetValidNetInterfacesForWeb()
if err != nil {
return ""
}
for _, iface := range ifaces {
for _, addr := range iface.Addresses {
if ip.Equal(addr) {
return iface.Name
}
}
}
return ""
}
// GetSubnet returns pointer to net.IPNet for the specified interface or nil if
// the search fails.
func GetSubnet(ifaceName string) *net.IPNet {
netIfaces, err := GetValidNetInterfacesForWeb()
if err != nil {
log.Error("Could not get network interfaces info: %v", err)
return nil
}
for _, netIface := range netIfaces {
if netIface.Name == ifaceName && len(netIface.Subnets) > 0 {
return netIface.Subnets[0]
}
}
return nil
}
// CheckPortAvailable - check if TCP port is available
func CheckPortAvailable(host net.IP, port int) error {
ln, err := net.Listen("tcp", net.JoinHostPort(host.String(), strconv.Itoa(port)))
if err != nil {
return err
}
_ = ln.Close()
// It seems that net.Listener.Close() doesn't close file descriptors right away.
// We wait for some time and hope that this fd will be closed.
time.Sleep(100 * time.Millisecond)
return nil
}
// CheckPacketPortAvailable - check if UDP port is available
func CheckPacketPortAvailable(host net.IP, port int) error {
ln, err := net.ListenPacket("udp", net.JoinHostPort(host.String(), strconv.Itoa(port)))
if err != nil {
return err
}
_ = ln.Close()
// It seems that net.Listener.Close() doesn't close file descriptors right away.
// We wait for some time and hope that this fd will be closed.
time.Sleep(100 * time.Millisecond)
return err
}
// ErrorIsAddrInUse - check if error is "address already in use"
func ErrorIsAddrInUse(err error) bool {
errOpError, ok := err.(*net.OpError)
if !ok {
return false
}
errSyscallError, ok := errOpError.Err.(*os.SyscallError)
if !ok {
return false
}
errErrno, ok := errSyscallError.Err.(syscall.Errno)
if !ok {
return false
}
if runtime.GOOS == "windows" {
const WSAEADDRINUSE = 10048
return errErrno == WSAEADDRINUSE
}
return errErrno == syscall.EADDRINUSE
}

View File

@@ -0,0 +1,161 @@
// +build darwin
package aghnet
import (
"errors"
"fmt"
"io/ioutil"
"regexp"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/util"
)
// hardwarePortInfo - information obtained using MacOS networksetup
// about the current state of the internet connection
type hardwarePortInfo struct {
name string
ip string
subnet string
gatewayIP string
static bool
}
func ifaceHasStaticIP(ifaceName string) (bool, error) {
portInfo, err := getCurrentHardwarePortInfo(ifaceName)
if err != nil {
return false, err
}
return portInfo.static, nil
}
// getCurrentHardwarePortInfo gets information the specified network interface.
func getCurrentHardwarePortInfo(ifaceName string) (hardwarePortInfo, error) {
// First of all we should find hardware port name
m := getNetworkSetupHardwareReports()
hardwarePort, ok := m[ifaceName]
if !ok {
return hardwarePortInfo{}, fmt.Errorf("could not find hardware port for %s", ifaceName)
}
return getHardwarePortInfo(hardwarePort)
}
// getNetworkSetupHardwareReports parses the output of the `networksetup -listallhardwareports` command
// it returns a map where the key is the interface name, and the value is the "hardware port"
// returns nil if it fails to parse the output
func getNetworkSetupHardwareReports() map[string]string {
_, out, err := util.RunCommand("networksetup", "-listallhardwareports")
if err != nil {
return nil
}
re, err := regexp.Compile("Hardware Port: (.*?)\nDevice: (.*?)\n")
if err != nil {
return nil
}
m := make(map[string]string)
matches := re.FindAllStringSubmatch(out, -1)
for i := range matches {
port := matches[i][1]
device := matches[i][2]
m[device] = port
}
return m
}
func getHardwarePortInfo(hardwarePort string) (hardwarePortInfo, error) {
h := hardwarePortInfo{}
_, out, err := util.RunCommand("networksetup", "-getinfo", hardwarePort)
if err != nil {
return h, err
}
re := regexp.MustCompile("IP address: (.*?)\nSubnet mask: (.*?)\nRouter: (.*?)\n")
match := re.FindStringSubmatch(out)
if len(match) == 0 {
return h, errors.New("could not find hardware port info")
}
h.name = hardwarePort
h.ip = match[1]
h.subnet = match[2]
h.gatewayIP = match[3]
if strings.Index(out, "Manual Configuration") == 0 {
h.static = true
}
return h, nil
}
func ifaceSetStaticIP(ifaceName string) (err error) {
portInfo, err := getCurrentHardwarePortInfo(ifaceName)
if err != nil {
return err
}
if portInfo.static {
return errors.New("IP address is already static")
}
dnsAddrs, err := getEtcResolvConfServers()
if err != nil {
return err
}
args := make([]string, 0)
args = append(args, "-setdnsservers", portInfo.name)
args = append(args, dnsAddrs...)
// Setting DNS servers is necessary when configuring a static IP
code, _, err := util.RunCommand("networksetup", args...)
if err != nil {
return err
}
if code != 0 {
return fmt.Errorf("failed to set DNS servers, code=%d", code)
}
// Actually configures hardware port to have static IP
code, _, err = util.RunCommand("networksetup", "-setmanual",
portInfo.name, portInfo.ip, portInfo.subnet, portInfo.gatewayIP)
if err != nil {
return err
}
if code != 0 {
return fmt.Errorf("failed to set DNS servers, code=%d", code)
}
return nil
}
// getEtcResolvConfServers returns a list of nameservers configured in
// /etc/resolv.conf.
func getEtcResolvConfServers() ([]string, error) {
body, err := ioutil.ReadFile("/etc/resolv.conf")
if err != nil {
return nil, err
}
re := regexp.MustCompile("nameserver ([a-zA-Z0-9.:]+)")
matches := re.FindAllStringSubmatch(string(body), -1)
if len(matches) == 0 {
return nil, errors.New("found no DNS servers in /etc/resolv.conf")
}
addrs := make([]string, 0)
for i := range matches {
addrs = append(addrs, matches[i][1])
}
return addrs, nil
}

View File

@@ -0,0 +1,176 @@
// +build linux
package aghnet
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/golibs/file"
)
// maxConfigFileSize is the maximum length of interfaces configuration file.
const maxConfigFileSize = 1024 * 1024
func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
// TODO(a.garipov): Currently, this function returns the first
// definitive result. So if /etc/dhcpcd.conf has a static IP while
// /etc/network/interfaces doesn't, it will return true. Perhaps this
// is not the most desirable behavior.
for _, check := range []struct {
checker func(io.Reader, string) (bool, error)
filePath string
}{{
checker: dhcpcdStaticConfig,
filePath: "/etc/dhcpcd.conf",
}, {
checker: ifacesStaticConfig,
filePath: "/etc/network/interfaces",
}} {
var f *os.File
f, err = os.Open(check.filePath)
if err != nil {
// ErrNotExist can happen here if there is no such file.
// This is normal, as not every system uses those files.
if errors.Is(err, os.ErrNotExist) {
err = nil
continue
}
return false, err
}
defer f.Close()
var fileReadCloser io.ReadCloser
fileReadCloser, err = aghio.LimitReadCloser(f, maxConfigFileSize)
if err != nil {
return false, err
}
defer fileReadCloser.Close()
has, err = check.checker(fileReadCloser, ifaceName)
if err != nil {
return false, err
}
return has, nil
}
return false, ErrNoStaticIPInfo
}
// dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to
// have a static IP.
func dhcpcdStaticConfig(r io.Reader, ifaceName string) (has bool, err error) {
s := bufio.NewScanner(r)
var withinInterfaceCtx bool
for s.Scan() {
line := strings.TrimSpace(s.Text())
if withinInterfaceCtx && len(line) == 0 {
// An empty line resets our state.
withinInterfaceCtx = false
}
if len(line) == 0 || line[0] == '#' {
continue
}
fields := strings.Fields(line)
if withinInterfaceCtx {
if len(fields) >= 2 && fields[0] == "static" && strings.HasPrefix(fields[1], "ip_address=") {
return true, nil
}
if len(fields) > 0 && fields[0] == "interface" {
// Another interface found.
withinInterfaceCtx = false
}
continue
}
if len(fields) == 2 && fields[0] == "interface" && fields[1] == ifaceName {
// The interface found.
withinInterfaceCtx = true
}
}
return false, s.Err()
}
// ifacesStaticConfig checks if interface is configured by
// /etc/network/interfaces to have a static IP.
func ifacesStaticConfig(r io.Reader, ifaceName string) (has bool, err error) {
s := bufio.NewScanner(r)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if len(line) == 0 || line[0] == '#' {
continue
}
fields := strings.Fields(line)
// Man page interfaces(5) declares that interface definition
// should consist of the key word "iface" followed by interface
// name, and method at fourth field.
if len(fields) >= 4 && fields[0] == "iface" && fields[1] == ifaceName && fields[3] == "static" {
return true, nil
}
}
return false, s.Err()
}
func ifaceSetStaticIP(ifaceName string) (err error) {
ipNet := GetSubnet(ifaceName)
if ipNet.IP == nil {
return errors.New("can't get IP address")
}
gatewayIP := GatewayIP(ifaceName)
add := updateStaticIPdhcpcdConf(ifaceName, ipNet.String(), gatewayIP, ipNet.IP)
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
if err != nil {
return err
}
body = append(body, []byte(add)...)
err = file.SafeWrite("/etc/dhcpcd.conf", body)
if err != nil {
return err
}
return nil
}
// updateStaticIPdhcpcdConf sets static IP address for the interface by writing
// into dhcpd.conf.
func updateStaticIPdhcpcdConf(ifaceName, ip string, gatewayIP, dnsIP net.IP) string {
var body []byte
add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
ifaceName, ip)
body = append(body, []byte(add)...)
if gatewayIP != nil {
add = fmt.Sprintf("static routers=%s\n",
gatewayIP)
body = append(body, []byte(add)...)
}
add = fmt.Sprintf("static domain_name_servers=%s\n\n",
dnsIP)
body = append(body, []byte(add)...)
return string(body)
}

View File

@@ -0,0 +1,121 @@
// +build linux
package aghnet
import (
"bytes"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const nl = "\n"
func TestDHCPCDStaticConfig(t *testing.T) {
testCases := []struct {
name string
data []byte
want bool
}{{
name: "has_not",
data: []byte(`#comment` + nl +
`# comment` + nl +
`interface eth0` + nl +
`static ip_address=192.168.0.1/24` + nl +
`# interface wlan0` + nl +
`static ip_address=192.168.1.1/24` + nl +
`# comment` + nl,
),
want: false,
}, {
name: "has",
data: []byte(`#comment` + nl +
`# comment` + nl +
`interface eth0` + nl +
`static ip_address=192.168.0.1/24` + nl +
`# interface wlan0` + nl +
`static ip_address=192.168.1.1/24` + nl +
`# comment` + nl +
`interface wlan0` + nl +
`# comment` + nl +
`static ip_address=192.168.2.1/24` + nl,
),
want: true,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := bytes.NewReader(tc.data)
has, err := dhcpcdStaticConfig(r, "wlan0")
require.Nil(t, err)
assert.Equal(t, tc.want, has)
})
}
}
func TestIfacesStaticConfig(t *testing.T) {
testCases := []struct {
name string
data []byte
want bool
}{{
name: "has_not",
data: []byte(`allow-hotplug enp0s3` + nl +
`#iface enp0s3 inet static` + nl +
`# address 192.168.0.200` + nl +
`# netmask 255.255.255.0` + nl +
`# gateway 192.168.0.1` + nl +
`iface enp0s3 inet dhcp` + nl,
),
want: false,
}, {
name: "has",
data: []byte(`allow-hotplug enp0s3` + nl +
`iface enp0s3 inet static` + nl +
` address 192.168.0.200` + nl +
` netmask 255.255.255.0` + nl +
` gateway 192.168.0.1` + nl +
`#iface enp0s3 inet dhcp` + nl,
),
want: true,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := bytes.NewReader(tc.data)
has, err := ifacesStaticConfig(r, "enp0s3")
require.Nil(t, err)
assert.Equal(t, tc.want, has)
})
}
}
func TestSetStaticIPdhcpcdConf(t *testing.T) {
testCases := []struct {
name string
dhcpcdConf string
routers net.IP
}{{
name: "with_gateway",
dhcpcdConf: nl + `interface wlan0` + nl +
`static ip_address=192.168.0.2/24` + nl +
`static routers=192.168.0.1` + nl +
`static domain_name_servers=192.168.0.2` + nl + nl,
routers: net.IP{192, 168, 0, 1},
}, {
name: "without_gateway",
dhcpcdConf: nl + `interface wlan0` + nl +
`static ip_address=192.168.0.2/24` + nl +
`static domain_name_servers=192.168.0.2` + nl + nl,
routers: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", tc.routers, net.IP{192, 168, 0, 2})
assert.Equal(t, tc.dhcpcdConf, s)
})
}
}

View File

@@ -0,0 +1,16 @@
// +build !linux,!darwin
package aghnet
import (
"fmt"
"runtime"
)
func ifaceHasStaticIP(string) (bool, error) {
return false, fmt.Errorf("cannot check if IP is static: not supported on %s", runtime.GOOS)
}
func ifaceSetStaticIP(string) error {
return fmt.Errorf("cannot set static IP on %s", runtime.GOOS)
}

View File

@@ -0,0 +1,16 @@
package aghnet
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetValidNetInterfacesForWeb(t *testing.T) {
ifaces, err := GetValidNetInterfacesForWeb()
require.Nilf(t, err, "Cannot get net interfaces: %s", err)
require.NotEmpty(t, ifaces, "No net interfaces found")
for _, iface := range ifaces {
require.NotEmptyf(t, iface.Addresses, "No addresses found for %s", iface.Name)
}
}