From 2313eda12362050fa90e082c718839844a16918b Mon Sep 17 00:00:00 2001 From: Eugene Burkov Date: Mon, 7 Dec 2020 19:48:24 +0300 Subject: [PATCH] Pull request: 2302 static ip Merge in DNS/adguard-home from 2302-static-ip to master Closes #2302. Squashed commit of the following: commit e62b7b033861f1c55f0d06811ca005b3934ddc5b Author: Eugene Burkov Date: Mon Dec 7 19:38:15 2020 +0300 all: format changelog commit 4127aa7630674ddcfe84f712e6c7c8d06b1cab9a Author: Eugene Burkov Date: Mon Dec 7 19:24:53 2020 +0300 all: fix changelog typo commit f8a432056d3682facae0cdec99d7d80a5c2bd9b5 Merge: b809a866e e4383189a Author: Eugene Burkov Date: Mon Dec 7 19:22:27 2020 +0300 Merge branch 'master' into 2302-static-ip commit b809a866e49147354f9c6952b2f958b6b56ad873 Author: Eugene Burkov Date: Mon Dec 7 19:20:05 2020 +0300 all: log changes commit 248c35ba411af731d6eae755a901cdbc77980628 Author: Eugene Burkov Date: Mon Dec 7 18:57:15 2020 +0300 sysutil: use bufio.Scanner commit 0dc19dd5c232fbe9552a2b0d846e048274d73a74 Author: Eugene Burkov Date: Mon Dec 7 17:26:18 2020 +0300 sysutil: fix linux tests commit 91202d6763595cff187040516dd1db10a20762b7 Author: Eugene Burkov Date: Mon Dec 7 17:15:29 2020 +0300 sysutil: fix linux files commit 40fbdbb95876322ebaeef1cbdaa8e3299b83ca7e Merge: d9a43ffb6 9b963fc77 Author: Eugene Burkov Date: Mon Dec 7 16:52:35 2020 +0300 Merge branch 'master' into 2302-static-ip commit d9a43ffb68a2ddbbcf185b69fc75aed139cd6919 Author: Eugene Burkov Date: Mon Dec 7 16:49:22 2020 +0300 sysutil: add main test commit bfef89186035ab0423d23246d46511584c26534c Author: Eugene Burkov Date: Mon Dec 7 16:21:59 2020 +0300 sysutil: improve code quality commit a5f57a373f736971fdeb0c03371da7c8138b3a82 Author: Eugene Burkov Date: Fri Dec 4 14:14:08 2020 +0300 all: move system functionality from dhcpd package to sysutil. commit 020b51864f85a39ca80e2b4e06faeb47cf318e11 Merge: 967e111a6 ab8defdb0 Author: Eugene Burkov Date: Wed Dec 2 14:53:43 2020 +0300 Merge branch 'master' into 2302-static-ip commit 967e111a663c18b5f4a87f3ae38d076f3ab6c200 Author: Eugene Burkov Date: Wed Dec 2 13:52:17 2020 +0300 all: improve code quality --- CHANGELOG.md | 3 + internal/dhcpd/dhcpd.go | 5 - internal/dhcpd/dhcphttp.go | 9 +- internal/dhcpd/netutil.go | 312 ------------------------- internal/dhcpd/netutil_test.go | 63 ----- internal/home/controlinstall.go | 6 +- internal/sysutil/net.go | 44 ++++ internal/sysutil/net_darwin.go | 161 +++++++++++++ internal/sysutil/net_linux.go | 168 +++++++++++++ internal/sysutil/net_linux_test.go | 109 +++++++++ internal/sysutil/net_others.go | 16 ++ internal/sysutil/{sysutil.go => os.go} | 5 - internal/sysutil/os_freebsd.go | 2 +- internal/sysutil/os_linux.go | 2 +- internal/sysutil/os_unix.go | 2 +- internal/sysutil/os_windows.go | 2 +- internal/sysutil/syslog.go | 6 + internal/sysutil/syslog_others.go | 2 +- internal/sysutil/syslog_windows.go | 2 +- internal/sysutil/sysutil_test.go | 11 + internal/util/network_utils.go | 5 +- 21 files changed, 533 insertions(+), 402 deletions(-) delete mode 100644 internal/dhcpd/netutil.go delete mode 100644 internal/dhcpd/netutil_test.go create mode 100644 internal/sysutil/net.go create mode 100644 internal/sysutil/net_darwin.go create mode 100644 internal/sysutil/net_linux.go create mode 100644 internal/sysutil/net_linux_test.go create mode 100644 internal/sysutil/net_others.go rename internal/sysutil/{sysutil.go => os.go} (83%) create mode 100644 internal/sysutil/syslog.go create mode 100644 internal/sysutil/sysutil_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 60167c75..b9b0f7ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to ### Added +- Detecting of network interface configurated to have static IP address via + `/etc/network/interfaces` ([#2302]). - DNSCrypt protocol support [#1361]. - A 5 second wait period until a DHCP server's network interface gets an IP address ([#2304]). @@ -22,6 +24,7 @@ and this project adheres to - HTTP API request body size limit ([#2305]). [#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361 +[#2302]: https://github.com/AdguardTeam/AdGuardHome/issues/2302 [#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304 [#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305 [#2337]: https://github.com/AdguardTeam/AdGuardHome/issues/2337 diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go index 9371a29a..ecb7d48d 100644 --- a/internal/dhcpd/dhcpd.go +++ b/internal/dhcpd/dhcpd.go @@ -81,11 +81,6 @@ type ServerInterface interface { SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) } -// CheckConfig checks the configuration -func (s *Server) CheckConfig(config ServerConfig) error { - return nil -} - // Create - create object func Create(config ServerConfig) *Server { s := &Server{} diff --git a/internal/dhcpd/dhcphttp.go b/internal/dhcpd/dhcphttp.go index c7584f8b..06a4cc79 100644 --- a/internal/dhcpd/dhcphttp.go +++ b/internal/dhcpd/dhcphttp.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/AdguardTeam/AdGuardHome/internal/sysutil" "github.com/AdguardTeam/AdGuardHome/internal/util" "github.com/AdguardTeam/golibs/jsonutil" "github.com/AdguardTeam/golibs/log" @@ -205,9 +206,9 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { s.dbLoad() if s.conf.Enabled { - staticIP, err := HasStaticIP(newconfig.InterfaceName) + staticIP, err := sysutil.IfaceHasStaticIP(newconfig.InterfaceName) if !staticIP && err == nil { - err = SetStaticIP(newconfig.InterfaceName) + err = sysutil.IfaceSetStaticIP(newconfig.InterfaceName) if err != nil { httpError(r, w, http.StatusInternalServerError, "Failed to configure static IP: %s", err) return @@ -282,7 +283,7 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { } } if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 { - jsonIface.GatewayIP = getGatewayIP(iface.Name) + jsonIface.GatewayIP = sysutil.GatewayIP(iface.Name) response[iface.Name] = jsonIface } } @@ -319,7 +320,7 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque found4, err4 := CheckIfOtherDHCPServersPresentV4(interfaceName) staticIP := map[string]interface{}{} - isStaticIP, err := HasStaticIP(interfaceName) + isStaticIP, err := sysutil.IfaceHasStaticIP(interfaceName) staticIPStatus := "yes" if err != nil { staticIPStatus = "error" diff --git a/internal/dhcpd/netutil.go b/internal/dhcpd/netutil.go deleted file mode 100644 index 41a1d7ec..00000000 --- a/internal/dhcpd/netutil.go +++ /dev/null @@ -1,312 +0,0 @@ -package dhcpd - -import ( - "errors" - "fmt" - "io/ioutil" - "net" - "os/exec" - "regexp" - "runtime" - "strings" - - "github.com/AdguardTeam/AdGuardHome/internal/util" - - "github.com/AdguardTeam/golibs/file" - - "github.com/AdguardTeam/golibs/log" -) - -// HasStaticIP check if the network interface has a static IP configured -// -// Supports: Raspbian. -func HasStaticIP(ifaceName string) (bool, error) { - if runtime.GOOS == "linux" { - body, err := ioutil.ReadFile("/etc/dhcpcd.conf") - if err != nil { - return false, err - } - - return hasStaticIPDhcpcdConf(string(body), ifaceName), nil - } - - if runtime.GOOS == "darwin" { - return hasStaticIPDarwin(ifaceName) - } - - return false, fmt.Errorf("cannot check if IP is static: not supported on %s", runtime.GOOS) -} - -// SetStaticIP sets a static IP for the network interface. -func SetStaticIP(ifaceName string) error { - if runtime.GOOS == "linux" { - return setStaticIPDhcpdConf(ifaceName) - } - - if runtime.GOOS == "darwin" { - return setStaticIPDarwin(ifaceName) - } - - return fmt.Errorf("cannot set static IP on %s", runtime.GOOS) -} - -// for dhcpcd.conf -func hasStaticIPDhcpcdConf(dhcpConf, ifaceName string) bool { - lines := strings.Split(dhcpConf, "\n") - nameLine := fmt.Sprintf("interface %s", ifaceName) - withinInterfaceCtx := false - - for _, line := range lines { - line = strings.TrimSpace(line) - - if withinInterfaceCtx && len(line) == 0 { - // an empty line resets our state - withinInterfaceCtx = false - } - - if len(line) == 0 || line[0] == '#' { - continue - } - line = strings.TrimSpace(line) - - if !withinInterfaceCtx { - if line == nameLine { - // we found our interface - withinInterfaceCtx = true - } - } else { - if strings.HasPrefix(line, "interface ") { - // we found another interface - reset our state - withinInterfaceCtx = false - continue - } - if strings.HasPrefix(line, "static ip_address=") { - return true - } - } - } - return false -} - -// Get gateway IP address -func getGatewayIP(ifaceName string) string { - 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 "" - } - - fields := strings.Fields(string(d)) - if len(fields) < 3 || fields[0] != "default" { - return "" - } - - ip := net.ParseIP(fields[2]) - if ip == nil { - return "" - } - - return fields[2] -} - -// setStaticIPDhcpdConf - updates /etc/dhcpd.conf and sets the current IP address to be static -func setStaticIPDhcpdConf(ifaceName string) error { - ip := util.GetSubnet(ifaceName) - if len(ip) == 0 { - return errors.New("can't get IP address") - } - - ip4, _, err := net.ParseCIDR(ip) - if err != nil { - return err - } - gatewayIP := getGatewayIP(ifaceName) - add := updateStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, ip4.String()) - - 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 -} - -// updates dhcpd.conf content -- sets static IP address there -// for dhcpcd.conf -func updateStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string { - var body []byte - - add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n", - ifaceName, ip) - body = append(body, []byte(add)...) - - if len(gatewayIP) != 0 { - 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) -} - -// Check if network interface has a static IP configured -// Supports: MacOS. -func hasStaticIPDarwin(ifaceName string) (bool, error) { - portInfo, err := getCurrentHardwarePortInfo(ifaceName) - if err != nil { - return false, err - } - - return portInfo.static, nil -} - -// setStaticIPDarwin - uses networksetup util to set the current IP address to be static -// Additionally it configures the current DNS servers as well -func setStaticIPDarwin(ifaceName string) 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 -} - -// 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 -} - -// 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 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 -} - -// Gets a list of nameservers currently configured in the /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 -} diff --git a/internal/dhcpd/netutil_test.go b/internal/dhcpd/netutil_test.go deleted file mode 100644 index fb6ef11e..00000000 --- a/internal/dhcpd/netutil_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris - -package dhcpd - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHasStaticIPDhcpcdConf(t *testing.T) { - dhcpdConf := `#comment -# comment - -interface eth0 -static ip_address=192.168.0.1/24 - -# interface wlan0 -static ip_address=192.168.1.1/24 - -# comment -` - assert.True(t, !hasStaticIPDhcpcdConf(dhcpdConf, "wlan0")) - - dhcpdConf = `#comment -# comment - -interface eth0 -static ip_address=192.168.0.1/24 - -# interface wlan0 -static ip_address=192.168.1.1/24 - -# comment - -interface wlan0 -# comment -static ip_address=192.168.2.1/24 -` - assert.True(t, hasStaticIPDhcpcdConf(dhcpdConf, "wlan0")) -} - -func TestSetStaticIPDhcpcdConf(t *testing.T) { - dhcpcdConf := ` -interface wlan0 -static ip_address=192.168.0.2/24 -static routers=192.168.0.1 -static domain_name_servers=192.168.0.2 - -` - s := updateStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "192.168.0.2") - assert.Equal(t, dhcpcdConf, s) - - // without gateway - dhcpcdConf = ` -interface wlan0 -static ip_address=192.168.0.2/24 -static domain_name_servers=192.168.0.2 - -` - s = updateStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2") - assert.Equal(t, dhcpcdConf, s) -} diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go index 3661d300..ed3cdd38 100644 --- a/internal/home/controlinstall.go +++ b/internal/home/controlinstall.go @@ -15,7 +15,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/util" - "github.com/AdguardTeam/AdGuardHome/internal/dhcpd" + "github.com/AdguardTeam/AdGuardHome/internal/sysutil" "github.com/AdguardTeam/golibs/log" ) @@ -167,7 +167,7 @@ func handleStaticIP(ip string, set bool) staticIPJSON { if set { // Try to set static IP for the specified interface - err := dhcpd.SetStaticIP(interfaceName) + err := sysutil.IfaceSetStaticIP(interfaceName) if err != nil { resp.Static = "error" resp.Error = err.Error() @@ -177,7 +177,7 @@ func handleStaticIP(ip string, set bool) staticIPJSON { // Fallthrough here even if we set static IP // Check if we have a static IP and return the details - isStaticIP, err := dhcpd.HasStaticIP(interfaceName) + isStaticIP, err := sysutil.IfaceHasStaticIP(interfaceName) if err != nil { resp.Static = "error" resp.Error = err.Error() diff --git a/internal/sysutil/net.go b/internal/sysutil/net.go new file mode 100644 index 00000000..557dd8d7 --- /dev/null +++ b/internal/sysutil/net.go @@ -0,0 +1,44 @@ +package sysutil + +import ( + "net" + "os/exec" + "strings" + + "github.com/AdguardTeam/golibs/log" +) + +// IfaceHasStaticIP checks if interface is configured to have static IP address. +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) string { + 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 "" + } + + 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 "" + } + + ip := net.ParseIP(fields[2]) + if ip == nil { + return "" + } + + return fields[2] +} diff --git a/internal/sysutil/net_darwin.go b/internal/sysutil/net_darwin.go new file mode 100644 index 00000000..1bc1ac61 --- /dev/null +++ b/internal/sysutil/net_darwin.go @@ -0,0 +1,161 @@ +// +build darwin + +package sysutil + +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 +} diff --git a/internal/sysutil/net_linux.go b/internal/sysutil/net_linux.go new file mode 100644 index 00000000..5206f9fd --- /dev/null +++ b/internal/sysutil/net_linux.go @@ -0,0 +1,168 @@ +// +build linux + +package sysutil + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "strings" + + "github.com/AdguardTeam/AdGuardHome/internal/aghio" + "github.com/AdguardTeam/AdGuardHome/internal/util" + "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) { + var f *os.File + 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", + }} { + f, err = os.Open(check.filePath) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + return false, err + } + defer f.Close() + + fileReadCloser, err := aghio.LimitReadCloser(f, maxConfigFileSize) + if err != nil { + return false, err + } + defer fileReadCloser.Close() + + has, err = check.checker(fileReadCloser, ifaceName) + if has || err != nil { + break + } + } + + return has, err +} + +// 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) { + ip := util.GetSubnet(ifaceName) + if len(ip) == 0 { + return errors.New("can't get IP address") + } + + ip4, _, err := net.ParseCIDR(ip) + if err != nil { + return err + } + gatewayIP := GatewayIP(ifaceName) + add := updateStaticIPdhcpcdConf(ifaceName, ip, gatewayIP, ip4.String()) + + 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, gatewayIP, dnsIP string) string { + var body []byte + + add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n", + ifaceName, ip) + body = append(body, []byte(add)...) + + if len(gatewayIP) != 0 { + 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) +} diff --git a/internal/sysutil/net_linux_test.go b/internal/sysutil/net_linux_test.go new file mode 100644 index 00000000..8cadbbb7 --- /dev/null +++ b/internal/sysutil/net_linux_test.go @@ -0,0 +1,109 @@ +// +build linux + +package sysutil + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +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") + assert.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") + assert.Nil(t, err) + assert.Equal(t, tc.want, has) + }) + } +} + +func TestSetStaticIPdhcpcdConf(t *testing.T) { + 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 + + s := updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "192.168.0.2") + assert.Equal(t, dhcpcdConf, s) + + // 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 + + s = updateStaticIPdhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2") + assert.Equal(t, dhcpcdConf, s) +} diff --git a/internal/sysutil/net_others.go b/internal/sysutil/net_others.go new file mode 100644 index 00000000..fd28c1a1 --- /dev/null +++ b/internal/sysutil/net_others.go @@ -0,0 +1,16 @@ +// +build !linux,!darwin + +package sysutil + +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) +} diff --git a/internal/sysutil/sysutil.go b/internal/sysutil/os.go similarity index 83% rename from internal/sysutil/sysutil.go rename to internal/sysutil/os.go index 47f40600..63d2bf7e 100644 --- a/internal/sysutil/sysutil.go +++ b/internal/sysutil/os.go @@ -24,8 +24,3 @@ func HaveAdminRights() (bool, error) { func SendProcessSignal(pid int, sig syscall.Signal) error { return sendProcessSignal(pid, sig) } - -// ConfigureSyslog reroutes standard logger output to syslog. -func ConfigureSyslog(serviceName string) error { - return configureSyslog(serviceName) -} diff --git a/internal/sysutil/os_freebsd.go b/internal/sysutil/os_freebsd.go index 48033f49..12321aa7 100644 --- a/internal/sysutil/os_freebsd.go +++ b/internal/sysutil/os_freebsd.go @@ -1,4 +1,4 @@ -//+build freebsd +// +build freebsd package sysutil diff --git a/internal/sysutil/os_linux.go b/internal/sysutil/os_linux.go index 7f20e11e..2bab01d4 100644 --- a/internal/sysutil/os_linux.go +++ b/internal/sysutil/os_linux.go @@ -1,4 +1,4 @@ -//+build linux +// +build linux package sysutil diff --git a/internal/sysutil/os_unix.go b/internal/sysutil/os_unix.go index 0a182cdc..a77e9da1 100644 --- a/internal/sysutil/os_unix.go +++ b/internal/sysutil/os_unix.go @@ -1,4 +1,4 @@ -//+build aix darwin dragonfly netbsd openbsd solaris +// +build aix darwin dragonfly netbsd openbsd solaris package sysutil diff --git a/internal/sysutil/os_windows.go b/internal/sysutil/os_windows.go index 58b331b4..3d6745c7 100644 --- a/internal/sysutil/os_windows.go +++ b/internal/sysutil/os_windows.go @@ -1,4 +1,4 @@ -//+build windows +// +build windows package sysutil diff --git a/internal/sysutil/syslog.go b/internal/sysutil/syslog.go new file mode 100644 index 00000000..7bd505fc --- /dev/null +++ b/internal/sysutil/syslog.go @@ -0,0 +1,6 @@ +package sysutil + +// ConfigureSyslog reroutes standard logger output to syslog. +func ConfigureSyslog(serviceName string) error { + return configureSyslog(serviceName) +} diff --git a/internal/sysutil/syslog_others.go b/internal/sysutil/syslog_others.go index f0e15967..12c8a803 100644 --- a/internal/sysutil/syslog_others.go +++ b/internal/sysutil/syslog_others.go @@ -1,4 +1,4 @@ -//+build !windows,!nacl,!plan9 +// +build !windows,!nacl,!plan9 package sysutil diff --git a/internal/sysutil/syslog_windows.go b/internal/sysutil/syslog_windows.go index fbacf44e..f83eab80 100644 --- a/internal/sysutil/syslog_windows.go +++ b/internal/sysutil/syslog_windows.go @@ -1,4 +1,4 @@ -//+build windows nacl plan9 +// +build windows nacl plan9 package sysutil diff --git a/internal/sysutil/sysutil_test.go b/internal/sysutil/sysutil_test.go new file mode 100644 index 00000000..0cddbf42 --- /dev/null +++ b/internal/sysutil/sysutil_test.go @@ -0,0 +1,11 @@ +package sysutil + +import ( + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/testutil" +) + +func TestMain(m *testing.M) { + testutil.DiscardLogOutput(m) +} diff --git a/internal/util/network_utils.go b/internal/util/network_utils.go index 3aafecff..1731ed08 100644 --- a/internal/util/network_utils.go +++ b/internal/util/network_utils.go @@ -33,10 +33,7 @@ func GetValidNetInterfaces() ([]net.Interface, error) { netIfaces := []net.Interface{} - for i := range ifaces { - iface := ifaces[i] - netIfaces = append(netIfaces, iface) - } + netIfaces = append(netIfaces, ifaces...) return netIfaces, nil }