+(dhcpd): added static IP for MacOS

This commit is contained in:
Andrey Meshkov 2020-02-13 14:14:30 +03:00
parent 7afa16fbe7
commit c27852537d
26 changed files with 589 additions and 406 deletions

View file

@ -467,6 +467,6 @@
"static_ip_desc": "AdGuard Home is a server so it needs a static IP address to function properly. Otherwise, at some point, your router may assign a different IP address to this device.",
"set_static_ip": "Set a static IP address",
"install_static_ok": "Good news! The static IP address is already configured",
"install_static_error": "AdGuard Home cannot configure it automatically for your OS. Please look for an instruction on how to do this manually",
"install_static_error": "AdGuard Home cannot configure it automatically for this network interface. Please look for an instruction on how to do this manually.",
"install_static_configure": "We have detected that a dynamic IP address is used — <0>{{ip}}</0>. Do you want to use it as your static address?"
}

View file

@ -10,6 +10,8 @@ import (
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/log"
)
@ -121,7 +123,7 @@ type netInterfaceJSON struct {
func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{}
ifaces, err := GetValidNetInterfaces()
ifaces, err := util.GetValidNetInterfaces()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return
@ -219,7 +221,7 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
staticIP["error"] = err.Error()
} else if !isStaticIP {
staticIPStatus = "no"
staticIP["ip"] = GetFullIP(interfaceName)
staticIP["ip"] = util.GetSubnet(interfaceName)
}
staticIP["static"] = staticIPStatus

View file

@ -6,37 +6,17 @@ import (
"io/ioutil"
"net"
"os/exec"
"regexp"
"runtime"
"strings"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
)
// 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: %s", err)
}
netIfaces := []net.Interface{}
for i := range ifaces {
if ifaces[i].Flags&net.FlagPointToPoint != 0 {
// this interface is ppp, we're not interested in this one
continue
}
iface := ifaces[i]
netIfaces = append(netIfaces, iface)
}
return netIfaces, nil
}
// Check if network interface has a static IP configured
// Supports: Raspbian.
func HasStaticIP(ifaceName string) (bool, error) {
@ -56,54 +36,18 @@ func HasStaticIP(ifaceName string) (bool, error) {
return false, fmt.Errorf("Cannot check if IP is static: not supported on %s", runtime.GOOS)
}
// Get IP address with netmask
func GetFullIP(ifaceName string) string {
cmd := exec.Command("ip", "-oneline", "-family", "inet", "address", "show", 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) < 4 {
return ""
}
_, _, err = net.ParseCIDR(fields[3])
if err != nil {
return ""
}
return fields[3]
}
// Set a static IP for network interface
// Supports: Raspbian.
// Set a static IP for the specified network interface
func SetStaticIP(ifaceName string) error {
ip := GetFullIP(ifaceName)
if len(ip) == 0 {
return errors.New("Can't get IP address")
if runtime.GOOS == "linux" {
return setStaticIPDhcpdConf(ifaceName)
}
ip4, _, err := net.ParseCIDR(ip)
if err != nil {
return err
}
gatewayIP := getGatewayIP(ifaceName)
add := setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, ip4.String())
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
if err != nil {
return err
if runtime.GOOS == "darwin" {
return fmt.Errorf("cannot do that")
// return setStaticIPDarwin(ifaceName)
}
body = append(body, []byte(add)...)
err = file.SafeWrite("/etc/dhcpcd.conf", body)
if err != nil {
return err
}
return nil
return fmt.Errorf("Cannot set static IP on %s", runtime.GOOS)
}
// for dhcpcd.conf
@ -167,8 +111,37 @@ func getGatewayIP(ifaceName string) string {
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 setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string {
func updateStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string {
var body []byte
add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
@ -187,3 +160,154 @@ func setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string {
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")
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, 0)
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
}

View file

@ -1,8 +0,0 @@
package dhcpd
// Check if network interface has a static IP configured
// Supports: Raspbian.
func hasStaticIPDarwin(ifaceName string) (bool, error) {
return false, nil
}

View file

@ -46,7 +46,7 @@ static routers=192.168.0.1
static domain_name_servers=192.168.0.2
`
s := setStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "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
@ -56,6 +56,6 @@ static ip_address=192.168.0.2/24
static domain_name_servers=192.168.0.2
`
s = setStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2")
s = updateStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2")
assert.Equal(t, dhcpcdConf, s)
}

View file

@ -3,7 +3,13 @@ package home
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/golibs/log"
@ -54,8 +60,7 @@ func getDNSAddresses() []string {
dnsAddresses := []string{}
if config.DNS.BindHost == "0.0.0.0" {
ifaces, e := getValidNetInterfacesForWeb()
ifaces, e := util.GetValidNetInterfacesForWeb()
if e != nil {
log.Error("Couldn't get network interfaces: %v", e)
return []string{}
@ -66,7 +71,6 @@ func getDNSAddresses() []string {
addDNSAddress(&dnsAddresses, addr)
}
}
} else {
addDNSAddress(&dnsAddresses, config.DNS.BindHost)
}
@ -180,3 +184,118 @@ func registerControlHandlers() {
func httpRegister(method string, url string, handler func(http.ResponseWriter, *http.Request)) {
http.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler)))))
}
// ----------------------------------
// helper functions for HTTP handlers
// ----------------------------------
func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Debug("%s %v", r.Method, r.URL)
if r.Method != method {
http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
return
}
if method == "POST" || method == "PUT" || method == "DELETE" {
Context.controlLock.Lock()
defer Context.controlLock.Unlock()
}
handler(w, r)
}
}
func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return ensure("POST", handler)
}
func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return ensure("GET", handler)
}
// Bridge between http.Handler object and Go function
type httpHandler struct {
handler func(http.ResponseWriter, *http.Request)
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handler(w, r)
}
func ensureHandler(method string, handler func(http.ResponseWriter, *http.Request)) http.Handler {
h := httpHandler{}
h.handler = ensure(method, handler)
return &h
}
// preInstall lets the handler run only if firstRun is true, no redirects
func preInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if !Context.firstRun {
// if it's not first run, don't let users access it (for example /install.html when configuration is done)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
handler(w, r)
}
}
// preInstallStruct wraps preInstall into a struct that can be returned as an interface where necessary
type preInstallHandlerStruct struct {
handler http.Handler
}
func (p *preInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
preInstall(p.handler.ServeHTTP)(w, r)
}
// preInstallHandler returns http.Handler interface for preInstall wrapper
func preInstallHandler(handler http.Handler) http.Handler {
return &preInstallHandlerStruct{handler}
}
// postInstall lets the handler run only if firstRun is false, and redirects to /install.html otherwise
// it also enforces HTTPS if it is enabled and configured
func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if Context.firstRun &&
!strings.HasPrefix(r.URL.Path, "/install.") &&
r.URL.Path != "/favicon.png" {
http.Redirect(w, r, "/install.html", http.StatusSeeOther) // should not be cacheable
return
}
// enforce https?
if config.TLS.ForceHTTPS && r.TLS == nil && config.TLS.Enabled && config.TLS.PortHTTPS != 0 && Context.httpsServer.server != nil {
// yes, and we want host from host:port
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
// no port in host
host = r.Host
}
// construct new URL to redirect to
newURL := url.URL{
Scheme: "https",
Host: net.JoinHostPort(host, strconv.Itoa(config.TLS.PortHTTPS)),
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
}
http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
handler(w, r)
}
}
type postInstallHandlerStruct struct {
handler http.Handler
}
func (p *postInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
postInstall(p.handler.ServeHTTP)(w, r)
}
func postInstallHandler(handler http.Handler) http.Handler {
return &postInstallHandlerStruct{handler}
}

View file

@ -13,6 +13,8 @@ import (
"runtime"
"strconv"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/golibs/log"
@ -38,7 +40,7 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data.WebPort = 80
data.DNSPort = 53
ifaces, err := getValidNetInterfacesForWeb()
ifaces, err := util.GetValidNetInterfacesForWeb()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return
@ -101,16 +103,16 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
}
if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort {
err = checkPortAvailable(reqData.Web.IP, reqData.Web.Port)
err = util.CheckPortAvailable(reqData.Web.IP, reqData.Web.Port)
if err != nil {
respData.Web.Status = fmt.Sprintf("%v", err)
}
}
if reqData.DNS.Port != 0 {
err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
err = util.CheckPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
if errorIsAddrInUse(err) {
if util.ErrorIsAddrInUse(err) {
canAutofix := checkDNSStubListener()
if canAutofix && reqData.DNS.Autofix {
@ -119,7 +121,7 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
log.Error("Couldn't disable DNSStubListener: %s", err)
}
err = checkPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
err = util.CheckPacketPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
canAutofix = false
}
@ -127,26 +129,22 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
}
if err == nil {
err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
err = util.CheckPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
}
if err != nil {
respData.DNS.Status = fmt.Sprintf("%v", err)
} else {
interfaceName := getInterfaceByIP(reqData.DNS.IP)
interfaceName := util.GetInterfaceByIP(reqData.DNS.IP)
staticIPStatus := "yes"
if len(interfaceName) == 0 {
staticIPStatus = "error"
respData.StaticIP.Error = fmt.Sprintf("Couldn't find network interface by IP %s", reqData.DNS.IP)
} else if reqData.DNS.SetStaticIP {
err = dhcpd.SetStaticIP(interfaceName)
staticIPStatus = "error"
respData.StaticIP.Error = err.Error()
} else {
// check if we have a static IP
isStaticIP, err := dhcpd.HasStaticIP(interfaceName)
@ -155,7 +153,7 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
respData.StaticIP.Error = err.Error()
} else if !isStaticIP {
staticIPStatus = "no"
respData.StaticIP.IP = dhcpd.GetFullIP(interfaceName)
respData.StaticIP.IP = util.GetSubnet(interfaceName)
}
}
respData.StaticIP.Static = staticIPStatus
@ -279,7 +277,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
// validate that hosts and ports are bindable
if restartHTTP {
err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
err = util.CheckPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
if err != nil {
httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s",
net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err)
@ -287,13 +285,13 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
}
}
err = checkPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
err = util.CheckPacketPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return
}
err = checkPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
err = util.CheckPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
if err != nil {
httpError(w, http.StatusBadRequest, "%s", err)
return

View file

@ -20,6 +20,8 @@ import (
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
@ -84,7 +86,7 @@ func handleTLSValidate(w http.ResponseWriter, r *http.Request) {
alreadyRunning = true
}
if !alreadyRunning {
err = checkPortAvailable(config.BindHost, data.PortHTTPS)
err = util.CheckPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return
@ -114,7 +116,7 @@ func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
alreadyRunning = true
}
if !alreadyRunning {
err = checkPortAvailable(config.BindHost, data.PortHTTPS)
err = util.CheckPortAvailable(config.BindHost, data.PortHTTPS)
if err != nil {
httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
return

View file

@ -17,6 +17,8 @@ import (
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/log"
)
@ -196,7 +198,7 @@ func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
binName = "AdGuardHome.exe"
}
u.curBinName = filepath.Join(workDir, binName)
if !fileExists(u.curBinName) {
if !util.FileExists(u.curBinName) {
return nil, fmt.Errorf("Executable file %s doesn't exist", u.curBinName)
}
u.bkpBinName = filepath.Join(u.backupDir, binName)

View file

@ -8,7 +8,6 @@ import (
)
func TestDoUpdate(t *testing.T) {
config.DNS.Port = 0
Context.workDir = "..." // set absolute path
newver := "v0.96"

View file

@ -13,6 +13,7 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
)
@ -401,7 +402,7 @@ func parseFilterContents(contents []byte) (int, string) {
// Count lines in the filter
for len(data) != 0 {
line := SplitNext(&data, '\n')
line := util.SplitNext(&data, '\n')
if len(line) == 0 {
continue
}

View file

@ -10,6 +10,11 @@ import (
)
func TestFilters(t *testing.T) {
dir := prepareTestDir()
defer func() { _ = os.RemoveAll(dir) }()
Context = homeContext{}
Context.workDir = dir
Context.client = &http.Client{
Timeout: time.Minute * 5,
}
@ -33,5 +38,5 @@ func TestFilters(t *testing.T) {
assert.True(t, err == nil)
f.unload()
os.Remove(f.Path())
_ = os.Remove(f.Path())
}

View file

@ -1,241 +0,0 @@
package home
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
// ----------------------------------
// helper functions for HTTP handlers
// ----------------------------------
func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Debug("%s %v", r.Method, r.URL)
if r.Method != method {
http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed)
return
}
if method == "POST" || method == "PUT" || method == "DELETE" {
Context.controlLock.Lock()
defer Context.controlLock.Unlock()
}
handler(w, r)
}
}
func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return ensure("POST", handler)
}
func ensureGET(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return ensure("GET", handler)
}
// Bridge between http.Handler object and Go function
type httpHandler struct {
handler func(http.ResponseWriter, *http.Request)
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handler(w, r)
}
func ensureHandler(method string, handler func(http.ResponseWriter, *http.Request)) http.Handler {
h := httpHandler{}
h.handler = ensure(method, handler)
return &h
}
// -------------------
// first run / install
// -------------------
func detectFirstRun() bool {
configfile := Context.configFilename
if !filepath.IsAbs(configfile) {
configfile = filepath.Join(Context.workDir, Context.configFilename)
}
_, err := os.Stat(configfile)
if !os.IsNotExist(err) {
// do nothing, file exists
return false
}
return true
}
// preInstall lets the handler run only if firstRun is true, no redirects
func preInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if !Context.firstRun {
// if it's not first run, don't let users access it (for example /install.html when configuration is done)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
handler(w, r)
}
}
// preInstallStruct wraps preInstall into a struct that can be returned as an interface where necessary
type preInstallHandlerStruct struct {
handler http.Handler
}
func (p *preInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
preInstall(p.handler.ServeHTTP)(w, r)
}
// preInstallHandler returns http.Handler interface for preInstall wrapper
func preInstallHandler(handler http.Handler) http.Handler {
return &preInstallHandlerStruct{handler}
}
// postInstall lets the handler run only if firstRun is false, and redirects to /install.html otherwise
// it also enforces HTTPS if it is enabled and configured
func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if Context.firstRun &&
!strings.HasPrefix(r.URL.Path, "/install.") &&
r.URL.Path != "/favicon.png" {
http.Redirect(w, r, "/install.html", http.StatusSeeOther) // should not be cacheable
return
}
// enforce https?
if config.TLS.ForceHTTPS && r.TLS == nil && config.TLS.Enabled && config.TLS.PortHTTPS != 0 && Context.httpsServer.server != nil {
// yes, and we want host from host:port
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
// no port in host
host = r.Host
}
// construct new URL to redirect to
newURL := url.URL{
Scheme: "https",
Host: net.JoinHostPort(host, strconv.Itoa(config.TLS.PortHTTPS)),
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
}
http.Redirect(w, r, newURL.String(), http.StatusTemporaryRedirect)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
handler(w, r)
}
}
type postInstallHandlerStruct struct {
handler http.Handler
}
func (p *postInstallHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
postInstall(p.handler.ServeHTTP)(w, r)
}
func postInstallHandler(handler http.Handler) http.Handler {
return &postInstallHandlerStruct{handler}
}
// Connect to a remote server resolving hostname using our own DNS server
func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
log.Tracef("network:%v addr:%v", network, addr)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: time.Minute * 5,
}
if net.ParseIP(host) != nil || config.DNS.Port == 0 {
con, err := dialer.DialContext(ctx, network, addr)
return con, err
}
addrs, e := Context.dnsServer.Resolve(host)
log.Debug("dnsServer.Resolve: %s: %v", host, addrs)
if e != nil {
return nil, e
}
if len(addrs) == 0 {
return nil, fmt.Errorf("couldn't lookup host: %s", host)
}
var dialErrs []error
for _, a := range addrs {
addr = net.JoinHostPort(a.String(), port)
con, err := dialer.DialContext(ctx, network, addr)
if err != nil {
dialErrs = append(dialErrs, err)
continue
}
return con, err
}
return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
}
// ---------------------
// general helpers
// ---------------------
// fileExists returns TRUE if file exists
func fileExists(fn string) bool {
_, err := os.Stat(fn)
if err != nil {
return false
}
return true
}
// runCommand runs shell command
func runCommand(command string, arguments ...string) (int, string, error) {
cmd := exec.Command(command, arguments...)
out, err := cmd.Output()
if err != nil {
return 1, "", fmt.Errorf("exec.Command(%s) failed: %s", command, err)
}
return cmd.ProcessState.ExitCode(), string(out), nil
}
// ---------------------
// debug logging helpers
// ---------------------
func _Func() string {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return path.Base(f.Name())
}
// SplitNext - split string by a byte and return the first chunk
// Whitespace is trimmed
func SplitNext(str *string, splitBy byte) string {
i := strings.IndexByte(*str, splitBy)
s := ""
if i != -1 {
s = (*str)[0:i]
*str = (*str)[i+1:]
} else {
s = *str
*str = ""
}
return strings.TrimSpace(s)
}

View file

@ -20,6 +20,10 @@ import (
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/joomcode/errorx"
"github.com/AdguardTeam/AdGuardHome/isdelve"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
@ -193,7 +197,7 @@ func run(args options) {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
config.RlimitNoFile != 0 {
setRlimit(config.RlimitNoFile)
util.SetRlimit(config.RlimitNoFile)
}
// override bind host/port from the console
@ -327,7 +331,7 @@ func httpServerLoop() {
// Check if the current user has root (administrator) rights
// and if not, ask and try to run as root
func requireAdminRights() {
admin, _ := haveAdminRights()
admin, _ := util.HaveAdminRights()
if //noinspection ALL
admin || isdelve.Enabled {
return
@ -412,7 +416,7 @@ func configureLogger(args options) {
if ls.LogFile == configSyslog {
// Use syslog where it is possible and eventlog on Windows
err := configureSyslog()
err := util.ConfigureSyslog(serviceName)
if err != nil {
log.Fatalf("cannot initialize syslog: %s", err)
}
@ -448,9 +452,9 @@ func stopHTTPServer() {
log.Info("Stopping HTTP server...")
Context.httpsServer.shutdown = true
if Context.httpsServer.server != nil {
Context.httpsServer.server.Shutdown(context.TODO())
_ = Context.httpsServer.server.Shutdown(context.TODO())
}
Context.httpServer.Shutdown(context.TODO())
_ = Context.httpServer.Shutdown(context.TODO())
log.Info("Stopped HTTP server")
}
@ -580,7 +584,7 @@ func printHTTPAddresses(proto string) {
}
} else if config.BindHost == "0.0.0.0" {
log.Println("AdGuard Home is available on the following addresses:")
ifaces, err := getValidNetInterfacesForWeb()
ifaces, err := util.GetValidNetInterfacesForWeb()
if err != nil {
// That's weird, but we'll ignore it
address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
@ -597,3 +601,60 @@ func printHTTPAddresses(proto string) {
log.Printf("Go to %s://%s", proto, address)
}
}
// -------------------
// first run / install
// -------------------
func detectFirstRun() bool {
configfile := Context.configFilename
if !filepath.IsAbs(configfile) {
configfile = filepath.Join(Context.workDir, Context.configFilename)
}
_, err := os.Stat(configfile)
if !os.IsNotExist(err) {
// do nothing, file exists
return false
}
return true
}
// Connect to a remote server resolving hostname using our own DNS server
func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
log.Tracef("network:%v addr:%v", network, addr)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: time.Minute * 5,
}
if net.ParseIP(host) != nil || config.DNS.Port == 0 {
con, err := dialer.DialContext(ctx, network, addr)
return con, err
}
addrs, e := Context.dnsServer.Resolve(host)
log.Debug("dnsServer.Resolve: %s: %v", host, addrs)
if e != nil {
return nil, e
}
if len(addrs) == 0 {
return nil, fmt.Errorf("couldn't lookup host: %s", host)
}
var dialErrs []error
for _, a := range addrs {
addr = net.JoinHostPort(a.String(), port)
con, err := dialer.DialContext(ctx, network, addr)
if err != nil {
dialErrs = append(dialErrs, err)
continue
}
return con, err
}
return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...)
}

View file

@ -7,6 +7,7 @@ import (
"strings"
"syscall"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/log"
"github.com/kardianos/service"
)
@ -229,7 +230,7 @@ func configureService(c *service.Config) {
// returns command code or error if any
func runInitdCommand(action string) (int, error) {
confPath := "/etc/init.d/" + serviceName
code, _, err := runCommand("sh", "-c", confPath+" "+action)
code, _, err := util.RunCommand("sh", "-c", confPath+" "+action)
return code, err
}

View file

@ -5,6 +5,8 @@ import (
"os"
"path/filepath"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"golang.org/x/crypto/bcrypt"
@ -114,7 +116,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err
// The first schema upgrade:
// No more "dnsfilter.txt", filters are now kept in data/filters/
func upgradeSchema0to1(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
log.Printf("%s(): called", util.FuncName())
dnsFilterPath := filepath.Join(Context.workDir, "dnsfilter.txt")
if _, err := os.Stat(dnsFilterPath); !os.IsNotExist(err) {
@ -135,7 +137,7 @@ func upgradeSchema0to1(diskConfig *map[string]interface{}) error {
// coredns is now dns in config
// delete 'Corefile', since we don't use that anymore
func upgradeSchema1to2(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
log.Printf("%s(): called", util.FuncName())
coreFilePath := filepath.Join(Context.workDir, "Corefile")
if _, err := os.Stat(coreFilePath); !os.IsNotExist(err) {
@ -159,7 +161,7 @@ func upgradeSchema1to2(diskConfig *map[string]interface{}) error {
// Third schema upgrade:
// Bootstrap DNS becomes an array
func upgradeSchema2to3(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
log.Printf("%s(): called", util.FuncName())
// Let's read dns configuration from diskConfig
dnsConfig, ok := (*diskConfig)["dns"]
@ -196,7 +198,7 @@ func upgradeSchema2to3(diskConfig *map[string]interface{}) error {
// Add use_global_blocked_services=true setting for existing "clients" array
func upgradeSchema3to4(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
log.Printf("%s(): called", util.FuncName())
(*diskConfig)["schema_version"] = 4
@ -233,7 +235,7 @@ func upgradeSchema3to4(diskConfig *map[string]interface{}) error {
// password: "..."
// ...
func upgradeSchema4to5(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
log.Printf("%s(): called", util.FuncName())
(*diskConfig)["schema_version"] = 5
@ -288,7 +290,7 @@ func upgradeSchema4to5(diskConfig *map[string]interface{}) error {
// - 127.0.0.1
// - ...
func upgradeSchema5to6(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
log.Printf("%s(): called", util.FuncName())
(*diskConfig)["schema_version"] = 6

View file

@ -8,6 +8,8 @@ import (
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
)
@ -61,7 +63,7 @@ func whoisParse(data string) map[string]string {
descr := ""
netname := ""
for len(data) != 0 {
ln := SplitNext(&data, '\n')
ln := util.SplitNext(&data, '\n')
if len(ln) == 0 || ln[0] == '#' || ln[0] == '%' {
continue
}

59
util/helpers.go Normal file
View file

@ -0,0 +1,59 @@
package util
import (
"fmt"
"os"
"os/exec"
"path"
"runtime"
"strings"
)
// ---------------------
// general helpers
// ---------------------
// fileExists returns TRUE if file exists
func FileExists(fn string) bool {
_, err := os.Stat(fn)
if err != nil {
return false
}
return true
}
// runCommand runs shell command
func RunCommand(command string, arguments ...string) (int, string, error) {
cmd := exec.Command(command, arguments...)
out, err := cmd.Output()
if err != nil {
return 1, "", fmt.Errorf("exec.Command(%s) failed: %s", command, err)
}
return cmd.ProcessState.ExitCode(), string(out), nil
}
// ---------------------
// debug logging helpers
// ---------------------
func FuncName() string {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return path.Base(f.Name())
}
// SplitNext - split string by a byte and return the first chunk
// Whitespace is trimmed
func SplitNext(str *string, splitBy byte) string {
i := strings.IndexByte(*str, splitBy)
s := ""
if i != -1 {
s = (*str)[0:i]
*str = (*str)[i+1:]
} else {
s = *str
*str = ""
}
return strings.TrimSpace(s)
}

14
util/helpers_test.go Normal file
View file

@ -0,0 +1,14 @@
package util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSplitNext(t *testing.T) {
s := " a,b , c "
assert.True(t, SplitNext(&s, ',') == "a")
assert.True(t, SplitNext(&s, ',') == "b")
assert.True(t, SplitNext(&s, ',') == "c" && len(s) == 0)
}

View file

@ -1,4 +1,4 @@
package home
package util
import (
"errors"
@ -10,23 +10,48 @@ import (
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
type netInterface struct {
Name string
MTU int
HardwareAddr string
Addresses []string
Flags string
// NetInterface represents a list of network interfaces
type NetInterface struct {
Name string // Network interface name
MTU int // MTU
HardwareAddr string // Hardware address
Addresses []string // Array with the network interface addresses
Subnets []string // Array with CIDR addresses of this network interface
Flags string // Network interface flags (up, broadcast, etc)
}
// 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: %s", err)
}
netIfaces := []net.Interface{}
for i := range ifaces {
if ifaces[i].Flags&net.FlagPointToPoint != 0 {
// this interface is ppp, we're not interested in this one
continue
}
iface := ifaces[i]
netIfaces = append(netIfaces, iface)
}
return netIfaces, nil
}
// getValidNetInterfacesMap returns interfaces that are eligible for DNS and WEB only
// we do not return link-local addresses here
func getValidNetInterfacesForWeb() ([]netInterface, error) {
ifaces, err := dhcpd.GetValidNetInterfaces()
func GetValidNetInterfacesForWeb() ([]NetInterface, error) {
ifaces, err := GetValidNetInterfaces()
if err != nil {
return nil, errorx.Decorate(err, "Couldn't get interfaces")
}
@ -34,7 +59,7 @@ func getValidNetInterfacesForWeb() ([]netInterface, error) {
return nil, errors.New("couldn't find any legible interface")
}
var netInterfaces []netInterface
var netInterfaces []NetInterface
for _, iface := range ifaces {
addrs, e := iface.Addrs()
@ -42,7 +67,7 @@ func getValidNetInterfacesForWeb() ([]netInterface, error) {
return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name)
}
netIface := netInterface{
netIface := NetInterface{
Name: iface.Name,
MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr.String(),
@ -52,19 +77,26 @@ func getValidNetInterfacesForWeb() ([]netInterface, error) {
netIface.Flags = iface.Flags.String()
}
// we don't want link-local addresses in json, so skip them
// Collect network interface addresses
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
ipNet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
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() {
if ipNet.IP.IsLinkLocalUnicast() {
continue
}
netIface.Addresses = append(netIface.Addresses, ipnet.IP.String())
// ignore IPv6
if ipNet.IP.To4() == nil {
continue
}
netIface.Addresses = append(netIface.Addresses, ipNet.IP.String())
netIface.Subnets = append(netIface.Subnets, ipNet.String())
}
// Discard interfaces with no addresses
if len(netIface.Addresses) != 0 {
netInterfaces = append(netInterfaces, netIface)
}
@ -74,8 +106,8 @@ func getValidNetInterfacesForWeb() ([]netInterface, error) {
}
// Get interface name by its IP address.
func getInterfaceByIP(ip string) string {
ifaces, err := getValidNetInterfacesForWeb()
func GetInterfaceByIP(ip string) string {
ifaces, err := GetValidNetInterfacesForWeb()
if err != nil {
return ""
}
@ -91,8 +123,26 @@ func getInterfaceByIP(ip string) string {
return ""
}
// Get IP address with netmask for the specified interface
// Returns an empty string if it fails to find it
func GetSubnet(ifaceName string) string {
netIfaces, err := GetValidNetInterfacesForWeb()
if err != nil {
log.Error("Could not get network interfaces info: %v", err)
return ""
}
for _, netIface := range netIfaces {
if netIface.Name == ifaceName && len(netIface.Subnets) > 0 {
return netIface.Subnets[0]
}
}
return ""
}
// checkPortAvailable is not a cheap test to see if the port is bindable, because it's actually doing the bind momentarily
func checkPortAvailable(host string, port int) error {
func CheckPortAvailable(host string, port int) error {
ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port)))
if err != nil {
return err
@ -105,7 +155,7 @@ func checkPortAvailable(host string, port int) error {
return nil
}
func checkPacketPortAvailable(host string, port int) error {
func CheckPacketPortAvailable(host string, port int) error {
ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port)))
if err != nil {
return err
@ -119,7 +169,7 @@ func checkPacketPortAvailable(host string, port int) error {
}
// check if error is "address already in use"
func errorIsAddrInUse(err error) bool {
func ErrorIsAddrInUse(err error) bool {
errOpError, ok := err.(*net.OpError)
if !ok {
return false

View file

@ -1,14 +1,12 @@
package home
package util
import (
"log"
"testing"
"github.com/AdguardTeam/golibs/log"
"github.com/stretchr/testify/assert"
)
func TestGetValidNetInterfacesForWeb(t *testing.T) {
ifaces, err := getValidNetInterfacesForWeb()
ifaces, err := GetValidNetInterfacesForWeb()
if err != nil {
t.Fatalf("Cannot get net interfaces: %s", err)
}
@ -24,10 +22,3 @@ func TestGetValidNetInterfacesForWeb(t *testing.T) {
log.Printf("%v", iface)
}
}
func TestSplitNext(t *testing.T) {
s := " a,b , c "
assert.True(t, SplitNext(&s, ',') == "a")
assert.True(t, SplitNext(&s, ',') == "b")
assert.True(t, SplitNext(&s, ',') == "c" && len(s) == 0)
}

View file

@ -1,6 +1,6 @@
// +build freebsd
package home
package util
import (
"os"
@ -11,7 +11,7 @@ import (
// Set user-specified limit of how many fd's we can use
// https://github.com/AdguardTeam/AdGuardHome/issues/659
func setRlimit(val uint) {
func SetRlimit(val uint) {
var rlim syscall.Rlimit
rlim.Max = int64(val)
rlim.Cur = int64(val)
@ -22,6 +22,6 @@ func setRlimit(val uint) {
}
// Check if the current user has root (administrator) rights
func haveAdminRights() (bool, error) {
func HaveAdminRights() (bool, error) {
return os.Getuid() == 0, nil
}

View file

@ -1,6 +1,6 @@
// +build aix darwin dragonfly linux netbsd openbsd solaris
package home
package util
import (
"os"
@ -11,7 +11,7 @@ import (
// Set user-specified limit of how many fd's we can use
// https://github.com/AdguardTeam/AdGuardHome/issues/659
func setRlimit(val uint) {
func SetRlimit(val uint) {
var rlim syscall.Rlimit
rlim.Max = uint64(val)
rlim.Cur = uint64(val)
@ -22,6 +22,6 @@ func setRlimit(val uint) {
}
// Check if the current user has root (administrator) rights
func haveAdminRights() (bool, error) {
func HaveAdminRights() (bool, error) {
return os.Getuid() == 0, nil
}

View file

@ -1,12 +1,12 @@
package home
package util
import "golang.org/x/sys/windows"
// Set user-specified limit of how many fd's we can use
func setRlimit(val uint) {
func SetRlimit(val uint) {
}
func haveAdminRights() (bool, error) {
func HaveAdminRights() (bool, error) {
var token windows.Token
h, _ := windows.GetCurrentProcess()
err := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token)

View file

@ -1,14 +1,14 @@
// +build !windows,!nacl,!plan9
package home
package util
import (
"log"
"log/syslog"
)
// configureSyslog reroutes standard logger output to syslog
func configureSyslog() error {
// ConfigureSyslog reroutes standard logger output to syslog
func ConfigureSyslog(serviceName string) error {
w, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_USER, serviceName)
if err != nil {
return err

View file

@ -1,4 +1,4 @@
package home
package util
import (
"log"
@ -17,7 +17,7 @@ func (w *eventLogWriter) Write(b []byte) (int, error) {
return len(b), w.el.Info(1, string(b))
}
func configureSyslog() error {
func ConfigureSyslog(serviceName string) error {
// Note that the eventlog src is the same as the service name
// Otherwise, we will get "the description for event id cannot be found" warning in every log record