AdGuardHome/internal/dhcpd/network_utils.go
Ainar Garipov bf4c256c72 Pull request: return 501 when we don't support features
Merge in DNS/adguard-home from 2295-dhcp-windows to master

Updates #2295.

Squashed commit of the following:

commit 3b00a90c3d9bc33e9af478e4062c0f938d4f327d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Nov 16 16:45:43 2020 +0300

    all: use the 501 handlers instead of the real ones, revert other changes

commit 0a3b37736a21abd6181e0d28c32069e8d7a576d0
Merge: 45feba755 6358240e9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Nov 16 15:59:15 2020 +0300

    Merge branch 'master' into 2295-dhcp-windows and update

commit 45feba755dde37e43cc8075b896e1576157341e6
Merge: cd987d8bc a19523b25
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Nov 16 15:51:16 2020 +0300

    Merge branch 'master' into 2295-dhcp-windows

commit cd987d8bc2cd524b7454d9037b595069714645f9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Nov 13 15:55:23 2020 +0300

    all: improve tests and refactor dhcp checking code even more

commit 3aad675443f325b5909523bcc1c987aa04ac61d9
Merge: 70c477e61 09196118e
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Nov 13 14:44:43 2020 +0300

    Merge branch 'master' into 2295-dhcp-windows

commit 70c477e61cdc1237603918f1c44470c1549f1136
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Nov 13 14:34:06 2020 +0300

    home: fix dhcpd test on windows

commit e59597d783fb9304e63f94eee2b5a5d67a5b2169
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Nov 13 13:38:25 2020 +0300

    all: mention the feature in the changelog

commit 5555c8d881b1c20b5b0a0cb096a17cf56e209c06
Merge: c3b6a5a93 e802e6645
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Nov 13 13:35:35 2020 +0300

    Merge branch 'master' into 2295-dhcp-windows

commit c3b6a5a930693090838eb1ef9f75a09b5b223ba6
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Nov 12 20:37:09 2020 +0300

    util: fix comment

commit ed92dfdb5d3a6c4ba5d032cbe781e7fd87882813
Author: ArtemBaskal <asbaskal@miem.hse.ru>
Date:   Thu Nov 12 20:24:14 2020 +0300

    Adapt client

commit e6f0494c20a4ad5388492af9091568eea5c6e2d6
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Nov 12 13:35:25 2020 +0300

    return 501 when we don't support features
2020-11-16 19:01:12 +03:00

313 lines
7.3 KiB
Go

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
}