Fix #1069 install: check static ip

Squashed commit of the following:

commit 57466233cb
Merge: 2df5f281 867bf545
Author: Andrey Meshkov <am@adguard.com>
Date:   Thu Feb 13 18:39:15 2020 +0300

    Merge branch 'master' into 1069-install-static-ip

commit 2df5f281c4
Author: Andrey Meshkov <am@adguard.com>
Date:   Thu Feb 13 18:35:54 2020 +0300

    *: lang fix

commit b4649a6b27
Merge: c2785253 f61d5f0f
Author: Andrey Meshkov <am@adguard.com>
Date:   Thu Feb 13 16:47:30 2020 +0300

    *(home): fixed issues with setting static IP on Mac

commit c27852537d
Author: Andrey Meshkov <am@adguard.com>
Date:   Thu Feb 13 14:14:30 2020 +0300

    +(dhcpd): added static IP for MacOS

commit f61d5f0f85
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Thu Feb 13 14:13:35 2020 +0300

    + client: show confirm before setting static IP

commit 7afa16fbe7
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Thu Feb 13 13:51:52 2020 +0300

    - client: fix text

commit 019bff0851
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Thu Feb 13 13:49:16 2020 +0300

    - client: pass all params to the check_config request

commit 194bed72f5
Author: Andrey Meshkov <am@adguard.com>
Date:   Wed Feb 12 17:12:16 2020 +0300

    *: fix home_test

commit 9359f6b55f
Merge: ae299058 c5ca2a77
Author: Andrey Meshkov <am@adguard.com>
Date:   Wed Feb 12 15:54:54 2020 +0300

    Merge with master

commit ae2990582d
Author: Andrey Meshkov <am@adguard.com>
Date:   Wed Feb 12 15:53:36 2020 +0300

    *(global): refactoring - moved runtime properties to Context

commit d8d48c5386
Author: Andrey Meshkov <am@adguard.com>
Date:   Wed Feb 12 15:04:25 2020 +0300

    *(dhcpd): refactoring, use dhcpd/network_utils where possible

commit 8d039c572f
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Fri Feb 7 18:37:39 2020 +0300

    - client: fix button position

commit 26c47e59dd
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Fri Feb 7 18:08:56 2020 +0300

    - client: fix static ip description

commit cb12babc46
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Feb 7 17:08:39 2020 +0300

    *: lower log level for some commands

commit d9001ff848
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Feb 7 16:17:59 2020 +0300

    *(documentation): updated openapi

commit 1d213d53c8
Merge: 8406d7d2 80861860
Author: Andrey Meshkov <am@adguard.com>
Date:   Fri Feb 7 15:16:46 2020 +0300

    *: merge with master

commit 8406d7d288
Author: Ildar Kamalov <i.kamalov@adguard.com>
Date:   Fri Jan 31 16:52:22 2020 +0300

    - client: fix locales

commit fb476b0117
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Jan 31 13:29:03 2020 +0300

    linter

commit 84b5708e71
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Jan 31 13:27:53 2020 +0300

    linter

commit 143a86a28a
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Fri Jan 31 13:26:47 2020 +0300

    linter

... and 7 more commits
This commit is contained in:
Andrey Meshkov 2020-02-13 18:42:07 +03:00
parent 867bf5457f
commit 7a3eda02ce
38 changed files with 1319 additions and 781 deletions

View file

@ -138,10 +138,13 @@ Request:
{ {
"web":{"port":80,"ip":"192.168.11.33"}, "web":{"port":80,"ip":"192.168.11.33"},
"dns":{"port":53,"ip":"127.0.0.1","autofix":false}, "dns":{"port":53,"ip":"127.0.0.1","autofix":false},
"set_static_ip": true | false
} }
Server should check whether a port is available only in case it itself isn't already listening on that port. Server should check whether a port is available only in case it itself isn't already listening on that port.
If `set_static_ip` is `true`, Server attempts to set a static IP for the network interface chosen by `dns.ip` setting. If the operation is successful, `static_ip.static` setting will be `yes`. If it fails, `static_ip.static` setting will be set to `error` and `static_ip.error` will contain the error message.
Server replies on success: Server replies on success:
200 OK 200 OK
@ -149,7 +152,14 @@ Server replies on success:
{ {
"web":{"status":""}, "web":{"status":""},
"dns":{"status":""}, "dns":{"status":""},
"static_ip": {
"static": "yes|no|error",
"ip": "<Current dynamic IP address>", // set if static=no
"error": "..." // set if static=error
} }
}
If `static_ip.static` is `no`, Server has detected that the system uses a dynamic address and it can automatically set a static address if `set_static_ip` in request is `true`. See section `Static IP check/set` for detailed process.
Server replies on error: Server replies on error:
@ -172,7 +182,11 @@ Request:
POST /control/install/check_config POST /control/install/check_config
{ {
"dns":{"port":53,"ip":"127.0.0.1","autofix":false} "dns":{
"port":53,
"ip":"127.0.0.1",
"autofix":false
}
} }
Check if DNSStubListener is enabled: Check if DNSStubListener is enabled:
@ -499,13 +513,7 @@ which will print:
default via 192.168.0.1 proto dhcp metric 100 default via 192.168.0.1 proto dhcp metric 100
#### Phase 2 #### Phase 2 (Raspbian)
This method only works on Raspbian.
On Ubuntu DHCP for a network interface can't be disabled via `dhcpcd.conf`. This must be configured in `/etc/netplan/01-netcfg.yaml`.
Fedora doesn't use `dhcpcd.conf` configuration at all.
Step 1. Step 1.
@ -526,6 +534,44 @@ If we would set a different IP address, we'd need to replace the IP address for
ip addr replace dev eth0 192.168.0.1/24 ip addr replace dev eth0 192.168.0.1/24
#### Phase 2 (Ubuntu)
`/etc/netplan/01-netcfg.yaml` or `/etc/netplan/01-network-manager-all.yaml`
This configuration example has a static IP set for `enp0s3` interface:
network:
version: 2
renderer: networkd
ethernets:
enp0s3:
dhcp4: no
addresses: [192.168.0.2/24]
gateway: 192.168.0.1
nameservers:
addresses: [192.168.0.1,8.8.8.8]
For dynamic configuration `dhcp4: yes` is set.
Make a backup copy to `/etc/netplan/01-netcfg.yaml.backup`.
Apply:
netplan apply
Restart network:
systemctl restart networking
or:
systemctl restart network-manager
or:
systemctl restart system-networkd
### Add a static lease ### Add a static lease
Request: Request:

View file

@ -458,9 +458,16 @@
"check_reason": "Reason: {{reason}}", "check_reason": "Reason: {{reason}}",
"check_rule": "Rule: {{rule}}", "check_rule": "Rule: {{rule}}",
"check_service": "Service name: {{service}}", "check_service": "Service name: {{service}}",
"check_not_found": "Doesn't exist in any filter", "check_not_found": "Not found in your filter lists",
"client_confirm_block": "Are you sure you want to block the client \"{{ip}}\"?", "client_confirm_block": "Are you sure you want to block the client \"{{ip}}\"?",
"client_confirm_unblock": "Are you sure you want to unblock the client \"{{ip}}\"?", "client_confirm_unblock": "Are you sure you want to unblock the client \"{{ip}}\"?",
"client_blocked": "Client \"{{ip}}\" successfully blocked", "client_blocked": "Client \"{{ip}}\" successfully blocked",
"client_unblocked": "Client \"{{ip}}\" successfully unblocked" "client_unblocked": "Client \"{{ip}}\" successfully unblocked",
"static_ip": "Static IP Address",
"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 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?",
"confirm_static_ip": "AdGuard Home will configure {{ip}} to be your static IP address. Do you want to proceed?"
} }

View file

@ -240,6 +240,13 @@ export const port = (value) => {
return undefined; return undefined;
}; };
export const validInstallPort = (value) => {
if (value < 1 || value > 65535) {
return <Trans>form_error_port</Trans>;
}
return undefined;
};
export const portTLS = (value) => { export const portTLS = (value) => {
if (value === 0) { if (value === 0) {
return undefined; return undefined;

View file

@ -7,26 +7,17 @@ import flow from 'lodash/flow';
import Controls from './Controls'; import Controls from './Controls';
import AddressList from './AddressList'; import AddressList from './AddressList';
import { getInterfaceIp } from '../../helpers/helpers'; import { getInterfaceIp } from '../../helpers/helpers';
import { ALL_INTERFACES_IP } from '../../helpers/constants'; import { ALL_INTERFACES_IP } from '../../helpers/constants';
import { renderInputField } from '../../helpers/form'; import { renderInputField, required, validInstallPort, toNumber } from '../../helpers/form';
const required = (value) => { const STATIC_STATUS = {
if (value || value === 0) { ENABLED: 'yes',
return false; DISABLED: 'no',
} ERROR: 'error',
return <Trans>form_error_required</Trans>;
}; };
const port = (value) => {
if (value < 1 || value > 65535) {
return <Trans>form_error_port</Trans>;
}
return false;
};
const toNumber = value => value && parseInt(value, 10);
const renderInterfaces = (interfaces => ( const renderInterfaces = (interfaces => (
Object.keys(interfaces).map((item) => { Object.keys(interfaces).map((item) => {
const option = interfaces[item]; const option = interfaces[item];
@ -79,11 +70,91 @@ class Settings extends Component {
}); });
} }
getStaticIpMessage = (staticIp) => {
const { static: status, ip } = staticIp;
if (!status) {
return '';
}
return (
<Fragment>
{status === STATIC_STATUS.DISABLED && (
<Fragment>
<div className="mb-2">
<Trans values={{ ip }} components={[<strong key="0">text</strong>]}>
install_static_configure
</Trans>
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={() => this.handleStaticIp(ip)}
>
<Trans>set_static_ip</Trans>
</button>
</Fragment>
)}
{status === STATIC_STATUS.ERROR && (
<div className="text-danger">
<Trans>install_static_error</Trans>
</div>
)}
{status === STATIC_STATUS.ENABLED && (
<div className="text-success">
<Trans>
install_static_ok
</Trans>
</div>
)}
</Fragment>
);
};
handleAutofix = (type) => {
const {
webIp,
webPort,
dnsIp,
dnsPort,
handleFix,
} = this.props;
const web = { ip: webIp, port: webPort, autofix: false };
const dns = { ip: dnsIp, port: dnsPort, autofix: false };
const set_static_ip = false;
if (type === 'web') {
web.autofix = true;
} else {
dns.autofix = true;
}
handleFix(web, dns, set_static_ip);
};
handleStaticIp = (ip) => {
const {
webIp,
webPort,
dnsIp,
dnsPort,
handleFix,
} = this.props;
const web = { ip: webIp, port: webPort, autofix: false };
const dns = { ip: dnsIp, port: dnsPort, autofix: false };
const set_static_ip = true;
if (window.confirm(this.props.t('confirm_static_ip', { ip }))) {
handleFix(web, dns, set_static_ip);
}
};
render() { render() {
const { const {
handleSubmit, handleSubmit,
handleChange, handleChange,
handleAutofix,
webIp, webIp,
webPort, webPort,
dnsIp, dnsIp,
@ -100,6 +171,7 @@ class Settings extends Component {
status: dnsStatus, status: dnsStatus,
can_autofix: isDnsFixAvailable, can_autofix: isDnsFixAvailable,
} = config.dns; } = config.dns;
const { staticIp } = config;
return ( return (
<form className="setup__step" onSubmit={handleSubmit}> <form className="setup__step" onSubmit={handleSubmit}>
@ -137,7 +209,7 @@ class Settings extends Component {
type="number" type="number"
className="form-control" className="form-control"
placeholder="80" placeholder="80"
validate={[port, required]} validate={[validInstallPort, required]}
normalize={toNumber} normalize={toNumber}
onChange={handleChange} onChange={handleChange}
/> />
@ -151,11 +223,12 @@ class Settings extends Component {
<button <button
type="button" type="button"
className="btn btn-secondary btn-sm ml-2" className="btn btn-secondary btn-sm ml-2"
onClick={() => handleAutofix('web', webIp, webPort)} onClick={() => this.handleAutofix('web')}
> >
<Trans>fix</Trans> <Trans>fix</Trans>
</button> </button>
} }
<hr className="divider--small" />
</div> </div>
} }
</div> </div>
@ -171,6 +244,7 @@ class Settings extends Component {
</div> </div>
</div> </div>
</div> </div>
<div className="setup__group"> <div className="setup__group">
<div className="setup__subtitle"> <div className="setup__subtitle">
<Trans>install_settings_dns</Trans> <Trans>install_settings_dns</Trans>
@ -205,7 +279,7 @@ class Settings extends Component {
type="number" type="number"
className="form-control" className="form-control"
placeholder="80" placeholder="80"
validate={[port, required]} validate={[validInstallPort, required]}
normalize={toNumber} normalize={toNumber}
onChange={handleChange} onChange={handleChange}
/> />
@ -220,7 +294,7 @@ class Settings extends Component {
<button <button
type="button" type="button"
className="btn btn-secondary btn-sm ml-2" className="btn btn-secondary btn-sm ml-2"
onClick={() => handleAutofix('dns', dnsIp, dnsPort)} onClick={() => this.handleAutofix('dns')}
> >
<Trans>fix</Trans> <Trans>fix</Trans>
</button> </button>
@ -237,6 +311,7 @@ class Settings extends Component {
<Trans>autofix_warning_result</Trans> <Trans>autofix_warning_result</Trans>
</p> </p>
</div> </div>
<hr className="divider--small" />
</Fragment> </Fragment>
} }
</div> </div>
@ -253,6 +328,19 @@ class Settings extends Component {
</div> </div>
</div> </div>
</div> </div>
<div className="setup__group">
<div className="setup__subtitle">
<Trans>static_ip</Trans>
</div>
<div className="mb-2">
<Trans>static_ip_desc</Trans>
</div>
{this.getStaticIpMessage(staticIp)}
</div>
<Controls invalid={invalid} /> <Controls invalid={invalid} />
</form> </form>
); );
@ -262,7 +350,7 @@ class Settings extends Component {
Settings.propTypes = { Settings.propTypes = {
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
handleChange: PropTypes.func, handleChange: PropTypes.func,
handleAutofix: PropTypes.func, handleFix: PropTypes.func.isRequired,
validateForm: PropTypes.func, validateForm: PropTypes.func,
webIp: PropTypes.string.isRequired, webIp: PropTypes.string.isRequired,
dnsIp: PropTypes.string.isRequired, dnsIp: PropTypes.string.isRequired,
@ -278,6 +366,7 @@ Settings.propTypes = {
interfaces: PropTypes.object.isRequired, interfaces: PropTypes.object.isRequired,
invalid: PropTypes.bool.isRequired, invalid: PropTypes.bool.isRequired,
initialValues: PropTypes.object, initialValues: PropTypes.object,
t: PropTypes.func.isRequired,
}; };
const selector = formValueSelector('install'); const selector = formValueSelector('install');

View file

@ -119,3 +119,8 @@
.setup__error { .setup__error {
margin: -5px 0 5px; margin: -5px 0 5px;
} }
.divider--small {
margin-top: 1rem;
margin-bottom: 1rem;
}

View file

@ -33,31 +33,19 @@ class Setup extends Component {
} }
handleFormSubmit = (values) => { handleFormSubmit = (values) => {
this.props.setAllSettings(values); const { staticIp, ...config } = values;
this.props.setAllSettings(config);
}; };
handleFormChange = debounce((values) => { handleFormChange = debounce((values) => {
if (values && values.web.port && values.dns.port) { const { web, dns } = values;
this.props.checkConfig(values); if (values && web.port && dns.port) {
this.props.checkConfig({ web, dns, set_static_ip: false });
} }
}, DEBOUNCE_TIMEOUT); }, DEBOUNCE_TIMEOUT);
handleAutofix = (type, ip, port) => { handleFix = (web, dns, set_static_ip) => {
const data = { this.props.checkConfig({ web, dns, set_static_ip });
ip,
port,
autofix: true,
};
if (type === 'web') {
this.props.checkConfig({
web: { ...data },
});
} else {
this.props.checkConfig({
dns: { ...data },
});
}
}; };
openDashboard = (ip, port) => { openDashboard = (ip, port) => {
@ -95,7 +83,7 @@ class Setup extends Component {
onSubmit={this.nextStep} onSubmit={this.nextStep}
onChange={this.handleFormChange} onChange={this.handleFormChange}
validateForm={this.handleFormChange} validateForm={this.handleFormChange}
handleAutofix={this.handleAutofix} handleFix={this.handleFix}
/> />
); );
case 3: case 3:
@ -117,6 +105,7 @@ class Setup extends Component {
step, step,
web, web,
dns, dns,
staticIp,
interfaces, interfaces,
} = this.props.install; } = this.props.install;
@ -128,7 +117,7 @@ class Setup extends Component {
<div className="setup"> <div className="setup">
<div className="setup__container"> <div className="setup__container">
<img src={logo} className="setup__logo" alt="logo" /> <img src={logo} className="setup__logo" alt="logo" />
{this.renderPage(step, { web, dns }, interfaces)} {this.renderPage(step, { web, dns, staticIp }, interfaces)}
<Progress step={step} /> <Progress step={step} />
</div> </div>
</div> </div>

View file

@ -32,9 +32,10 @@ const install = handleActions({
[actions.checkConfigSuccess]: (state, { payload }) => { [actions.checkConfigSuccess]: (state, { payload }) => {
const web = { ...state.web, ...payload.web }; const web = { ...state.web, ...payload.web };
const dns = { ...state.dns, ...payload.dns }; const dns = { ...state.dns, ...payload.dns };
const staticIp = { ...state.staticIp, ...payload.static_ip };
const newState = { const newState = {
...state, web, dns, processingCheck: false, ...state, web, dns, staticIp, processingCheck: false,
}; };
return newState; return newState;
}, },
@ -55,6 +56,11 @@ const install = handleActions({
status: '', status: '',
can_autofix: false, can_autofix: false,
}, },
staticIp: {
static: '',
ip: '',
error: '',
},
interfaces: {}, interfaces: {},
}); });

View file

@ -2,18 +2,16 @@ package dhcpd
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
"os/exec"
"runtime"
"strings" "strings"
"time" "time"
"github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@ -97,10 +95,9 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
s.conf.ConfigModified() s.conf.ConfigModified()
if newconfig.Enabled { if newconfig.Enabled {
staticIP, err := HasStaticIP(newconfig.InterfaceName)
staticIP, err := hasStaticIP(newconfig.InterfaceName)
if !staticIP && err == nil { if !staticIP && err == nil {
err = setStaticIP(newconfig.InterfaceName) err = SetStaticIP(newconfig.InterfaceName)
if err != nil { if err != nil {
httpError(r, w, http.StatusInternalServerError, "Failed to configure static IP: %s", err) httpError(r, w, http.StatusInternalServerError, "Failed to configure static IP: %s", err)
return return
@ -115,7 +112,7 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
} }
} }
type netInterface struct { type netInterfaceJSON struct {
Name string `json:"name"` Name string `json:"name"`
MTU int `json:"mtu"` MTU int `json:"mtu"`
HardwareAddr string `json:"hardware_address"` HardwareAddr string `json:"hardware_address"`
@ -123,33 +120,10 @@ type netInterface struct {
Flags string `json:"flags"` Flags string `json:"flags"`
} }
// 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
}
func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{} response := map[string]interface{}{}
ifaces, err := getValidNetInterfaces() ifaces, err := util.GetValidNetInterfaces()
if err != nil { if err != nil {
httpError(r, w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err) httpError(r, w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return return
@ -170,7 +144,7 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
return return
} }
jsonIface := netInterface{ jsonIface := netInterfaceJSON{
Name: iface.Name, Name: iface.Name,
MTU: iface.MTU, MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr.String(), HardwareAddr: iface.HardwareAddr.String(),
@ -240,14 +214,14 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
othSrv["found"] = foundVal othSrv["found"] = foundVal
staticIP := map[string]interface{}{} staticIP := map[string]interface{}{}
isStaticIP, err := hasStaticIP(interfaceName) isStaticIP, err := HasStaticIP(interfaceName)
staticIPStatus := "yes" staticIPStatus := "yes"
if err != nil { if err != nil {
staticIPStatus = "error" staticIPStatus = "error"
staticIP["error"] = err.Error() staticIP["error"] = err.Error()
} else if !isStaticIP { } else if !isStaticIP {
staticIPStatus = "no" staticIPStatus = "no"
staticIP["ip"] = getFullIP(interfaceName) staticIP["ip"] = util.GetSubnet(interfaceName)
} }
staticIP["static"] = staticIPStatus staticIP["static"] = staticIPStatus
@ -263,137 +237,6 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
} }
} }
// Check if network interface has a static IP configured
func hasStaticIP(ifaceName string) (bool, error) {
if runtime.GOOS == "windows" {
return false, errors.New("Can't detect static IP: not supported on Windows")
}
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
if err != nil {
return false, err
}
lines := strings.Split(string(body), "\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, nil
}
}
}
return false, nil
}
// 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]
}
// 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]
}
// Set a static IP for network interface
func setStaticIP(ifaceName string) error {
ip := getFullIP(ifaceName)
if len(ip) == 0 {
return errors.New("Can't get IP address")
}
body, err := ioutil.ReadFile("/etc/dhcpcd.conf")
if err != nil {
return err
}
ip4, _, err := net.ParseCIDR(ip)
if err != nil {
return err
}
add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n",
ifaceName, ip)
body = append(body, []byte(add)...)
gatewayIP := getGatewayIP(ifaceName)
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",
ip4)
body = append(body, []byte(add)...)
err = file.SafeWrite("/etc/dhcpcd.conf", body)
if err != nil {
return err
}
return nil
}
func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) { func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
lj := staticLeaseJSON{} lj := staticLeaseJSON{}

312
dhcpd/network_utils.go Normal file
View file

@ -0,0 +1,312 @@
package dhcpd
import (
"errors"
"fmt"
"io/ioutil"
"net"
"os/exec"
"regexp"
"runtime"
"strings"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
)
// Check if 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)
}
// Set a static IP for the specified 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, 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

@ -0,0 +1,61 @@
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)
}

View file

@ -152,7 +152,7 @@ func (a *Auth) addSession(data []byte, s *session) {
a.sessions[name] = s a.sessions[name] = s
a.lock.Unlock() a.lock.Unlock()
if a.storeSession(data, s) { if a.storeSession(data, s) {
log.Info("Auth: created session %s: expire=%d", name, s.expire) log.Debug("Auth: created session %s: expire=%d", name, s.expire)
} }
} }
@ -307,7 +307,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
cookie := config.auth.httpCookie(req) cookie := Context.auth.httpCookie(req)
if len(cookie) == 0 { if len(cookie) == 0 {
log.Info("Auth: invalid user name or password: name='%s'", req.Name) log.Info("Auth: invalid user name or password: name='%s'", req.Name)
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
@ -328,7 +328,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
cookie := r.Header.Get("Cookie") cookie := r.Header.Get("Cookie")
sess := parseCookie(cookie) sess := parseCookie(cookie)
config.auth.RemoveSession(sess) Context.auth.RemoveSession(sess)
w.Header().Set("Location", "/login.html") w.Header().Set("Location", "/login.html")
@ -365,10 +365,10 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
if r.URL.Path == "/login.html" { if r.URL.Path == "/login.html" {
// redirect to dashboard if already authenticated // redirect to dashboard if already authenticated
authRequired := config.auth != nil && config.auth.AuthRequired() authRequired := Context.auth != nil && Context.auth.AuthRequired()
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil { if authRequired && err == nil {
r := config.auth.CheckSession(cookie.Value) r := Context.auth.CheckSession(cookie.Value)
if r == 0 { if r == 0 {
w.Header().Set("Location", "/") w.Header().Set("Location", "/")
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
@ -383,12 +383,12 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
strings.HasPrefix(r.URL.Path, "/__locales/") { strings.HasPrefix(r.URL.Path, "/__locales/") {
// process as usual // process as usual
} else if config.auth != nil && config.auth.AuthRequired() { } else if Context.auth != nil && Context.auth.AuthRequired() {
// redirect to login page if not authenticated // redirect to login page if not authenticated
ok := false ok := false
cookie, err := r.Cookie(sessionCookieName) cookie, err := r.Cookie(sessionCookieName)
if err == nil { if err == nil {
r := config.auth.CheckSession(cookie.Value) r := Context.auth.CheckSession(cookie.Value)
if r == 0 { if r == 0 {
ok = true ok = true
} else if r < 0 { } else if r < 0 {
@ -398,7 +398,7 @@ func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.Re
// there's no Cookie, check Basic authentication // there's no Cookie, check Basic authentication
user, pass, ok2 := r.BasicAuth() user, pass, ok2 := r.BasicAuth()
if ok2 { if ok2 {
u := config.auth.UserFind(user, pass) u := Context.auth.UserFind(user, pass)
if len(u.Name) != 0 { if len(u.Name) != 0 {
ok = true ok = true
} else { } else {
@ -474,7 +474,7 @@ func (a *Auth) GetCurrentUser(r *http.Request) User {
// there's no Cookie, check Basic authentication // there's no Cookie, check Basic authentication
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
if ok { if ok {
u := config.auth.UserFind(user, pass) u := Context.auth.UserFind(user, pass)
return u return u
} }
return User{} return User{}

View file

@ -100,7 +100,7 @@ func TestAuthHTTP(t *testing.T) {
users := []User{ users := []User{
User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"}, User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
} }
config.auth = InitAuth(fn, users, 60) Context.auth = InitAuth(fn, users, 60)
handlerCalled := false handlerCalled := false
handler := func(w http.ResponseWriter, r *http.Request) { handler := func(w http.ResponseWriter, r *http.Request) {
@ -129,7 +129,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
// perform login // perform login
cookie := config.auth.httpCookie(loginJSON{Name: "name", Password: "password"}) cookie := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"})
assert.True(t, cookie != "") assert.True(t, cookie != "")
// get / // get /
@ -173,5 +173,5 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled) assert.True(t, handlerCalled)
r.Header.Del("Cookie") r.Header.Del("Cookie")
config.auth.Close() Context.auth.Close()
} }

View file

@ -44,19 +44,6 @@ type configuration struct {
// It's reset after config is parsed // It's reset after config is parsed
fileData []byte fileData []byte
ourConfigFilename string // Config filename (can be overridden via the command line arguments)
ourWorkingDir string // Location of our directory, used to protect against CWD being somewhere else
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
pidFileName string // PID file name. Empty if no PID file was created.
// runningAsService flag is set to true when options are passed from the service runner
runningAsService bool
disableUpdate bool // If set, don't check for updates
appSignalChannel chan os.Signal
controlLock sync.Mutex
transport *http.Transport
client *http.Client
auth *Auth // HTTP authentication module
// cached version.json to avoid hammering github.io for each page reload // cached version.json to avoid hammering github.io for each page reload
versionCheckJSON []byte versionCheckJSON []byte
versionCheckLastTime time.Time versionCheckLastTime time.Time
@ -152,9 +139,8 @@ type tlsConfig struct {
// initialize to default values, will be changed later when reading config or parsing command line // initialize to default values, will be changed later when reading config or parsing command line
var config = configuration{ var config = configuration{
ourConfigFilename: "AdGuardHome.yaml", BindPort: 3000,
BindPort: 3000, BindHost: "0.0.0.0",
BindHost: "0.0.0.0",
DNS: dnsConfig{ DNS: dnsConfig{
BindHost: "0.0.0.0", BindHost: "0.0.0.0",
Port: 53, Port: 53,
@ -185,14 +171,6 @@ var config = configuration{
// initConfig initializes default configuration for the current OS&ARCH // initConfig initializes default configuration for the current OS&ARCH
func initConfig() { func initConfig() {
config.transport = &http.Transport{
DialContext: customDialContext,
}
config.client = &http.Client{
Timeout: time.Minute * 5,
Transport: config.transport,
}
config.WebSessionTTLHours = 30 * 24 config.WebSessionTTLHours = 30 * 24
config.DNS.QueryLogEnabled = true config.DNS.QueryLogEnabled = true
@ -209,24 +187,19 @@ func initConfig() {
// getConfigFilename returns path to the current config file // getConfigFilename returns path to the current config file
func (c *configuration) getConfigFilename() string { func (c *configuration) getConfigFilename() string {
configFile, err := filepath.EvalSymlinks(config.ourConfigFilename) configFile, err := filepath.EvalSymlinks(Context.configFilename)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
log.Error("unexpected error while config file path evaluation: %s", err) log.Error("unexpected error while config file path evaluation: %s", err)
} }
configFile = config.ourConfigFilename configFile = Context.configFilename
} }
if !filepath.IsAbs(configFile) { if !filepath.IsAbs(configFile) {
configFile = filepath.Join(config.ourWorkingDir, configFile) configFile = filepath.Join(Context.workDir, configFile)
} }
return configFile return configFile
} }
// getDataDir returns path to the directory where we store databases and filters
func (c *configuration) getDataDir() string {
return filepath.Join(c.ourWorkingDir, dataDir)
}
// getLogSettings reads logging settings from the config file. // getLogSettings reads logging settings from the config file.
// we do it in a separate method in order to configure logger before the actual configuration is parsed and applied. // we do it in a separate method in order to configure logger before the actual configuration is parsed and applied.
func getLogSettings() logSettings { func getLogSettings() logSettings {
@ -292,8 +265,8 @@ func (c *configuration) write() error {
Context.clients.WriteDiskConfig(&config.Clients) Context.clients.WriteDiskConfig(&config.Clients)
if config.auth != nil { if Context.auth != nil {
config.Users = config.auth.GetUsers() config.Users = Context.auth.GetUsers()
} }
if Context.stats != nil { if Context.stats != nil {

View file

@ -3,7 +3,13 @@ package home
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/url"
"strconv"
"strings"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
@ -54,8 +60,7 @@ func getDNSAddresses() []string {
dnsAddresses := []string{} dnsAddresses := []string{}
if config.DNS.BindHost == "0.0.0.0" { if config.DNS.BindHost == "0.0.0.0" {
ifaces, e := util.GetValidNetInterfacesForWeb()
ifaces, e := getValidNetInterfacesForWeb()
if e != nil { if e != nil {
log.Error("Couldn't get network interfaces: %v", e) log.Error("Couldn't get network interfaces: %v", e)
return []string{} return []string{}
@ -66,7 +71,6 @@ func getDNSAddresses() []string {
addDNSAddress(&dnsAddresses, addr) addDNSAddress(&dnsAddresses, addr)
} }
} }
} else { } else {
addDNSAddress(&dnsAddresses, config.DNS.BindHost) addDNSAddress(&dnsAddresses, config.DNS.BindHost)
} }
@ -129,7 +133,7 @@ type profileJSON struct {
func handleGetProfile(w http.ResponseWriter, r *http.Request) { func handleGetProfile(w http.ResponseWriter, r *http.Request) {
pj := profileJSON{} pj := profileJSON{}
u := config.auth.GetCurrentUser(r) u := Context.auth.GetCurrentUser(r)
pj.Name = u.Name pj.Name = u.Name
data, err := json.Marshal(pj) data, err := json.Marshal(pj)
@ -180,3 +184,118 @@ func registerControlHandlers() {
func httpRegister(method string, url string, handler func(http.ResponseWriter, *http.Request)) { func httpRegister(method string, url string, handler func(http.ResponseWriter, *http.Request)) {
http.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler))))) 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

@ -210,9 +210,9 @@ func handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
} }
func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) { func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
config.controlLock.Unlock() Context.controlLock.Unlock()
nUpdated, err := refreshFilters() nUpdated, err := refreshFilters()
config.controlLock.Lock() Context.controlLock.Lock()
if err != nil { if err != nil {
httpError(w, http.StatusInternalServerError, "%s", err) httpError(w, http.StatusInternalServerError, "%s", err)
return return

View file

@ -13,6 +13,10 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@ -22,13 +26,21 @@ type firstRunData struct {
Interfaces map[string]interface{} `json:"interfaces"` Interfaces map[string]interface{} `json:"interfaces"`
} }
type netInterfaceJSON struct {
Name string `json:"name"`
MTU int `json:"mtu"`
HardwareAddr string `json:"hardware_address"`
Addresses []string `json:"ip_addresses"`
Flags string `json:"flags"`
}
// Get initial installation settings // Get initial installation settings
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data := firstRunData{} data := firstRunData{}
data.WebPort = 80 data.WebPort = 80
data.DNSPort = 53 data.DNSPort = 53
ifaces, err := getValidNetInterfacesForWeb() ifaces, err := util.GetValidNetInterfacesForWeb()
if err != nil { if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err) httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
return return
@ -36,7 +48,14 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data.Interfaces = make(map[string]interface{}) data.Interfaces = make(map[string]interface{})
for _, iface := range ifaces { for _, iface := range ifaces {
data.Interfaces[iface.Name] = iface ifaceJSON := netInterfaceJSON{
Name: iface.Name,
MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr,
Addresses: iface.Addresses,
Flags: iface.Flags,
}
data.Interfaces[iface.Name] = ifaceJSON
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -53,17 +72,24 @@ type checkConfigReqEnt struct {
Autofix bool `json:"autofix"` Autofix bool `json:"autofix"`
} }
type checkConfigReq struct { type checkConfigReq struct {
Web checkConfigReqEnt `json:"web"` Web checkConfigReqEnt `json:"web"`
DNS checkConfigReqEnt `json:"dns"` DNS checkConfigReqEnt `json:"dns"`
SetStaticIP bool `json:"set_static_ip"`
} }
type checkConfigRespEnt struct { type checkConfigRespEnt struct {
Status string `json:"status"` Status string `json:"status"`
CanAutofix bool `json:"can_autofix"` CanAutofix bool `json:"can_autofix"`
} }
type staticIPJSON struct {
Static string `json:"static"`
IP string `json:"ip"`
Error string `json:"error"`
}
type checkConfigResp struct { type checkConfigResp struct {
Web checkConfigRespEnt `json:"web"` Web checkConfigRespEnt `json:"web"`
DNS checkConfigRespEnt `json:"dns"` DNS checkConfigRespEnt `json:"dns"`
StaticIP staticIPJSON `json:"static_ip"`
} }
// Check if ports are available, respond with results // Check if ports are available, respond with results
@ -77,16 +103,16 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
} }
if reqData.Web.Port != 0 && reqData.Web.Port != config.BindPort { 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 { if err != nil {
respData.Web.Status = fmt.Sprintf("%v", err) respData.Web.Status = fmt.Sprintf("%v", err)
} }
} }
if reqData.DNS.Port != 0 { 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() canAutofix := checkDNSStubListener()
if canAutofix && reqData.DNS.Autofix { if canAutofix && reqData.DNS.Autofix {
@ -95,7 +121,7 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
log.Error("Couldn't disable DNSStubListener: %s", err) 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 canAutofix = false
} }
@ -103,11 +129,13 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
} }
if err == nil { if err == nil {
err = checkPortAvailable(reqData.DNS.IP, reqData.DNS.Port) err = util.CheckPortAvailable(reqData.DNS.IP, reqData.DNS.Port)
} }
if err != nil { if err != nil {
respData.DNS.Status = fmt.Sprintf("%v", err) respData.DNS.Status = fmt.Sprintf("%v", err)
} else {
respData.StaticIP = handleStaticIP(reqData.DNS.IP, reqData.SetStaticIP)
} }
} }
@ -119,6 +147,46 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
} }
} }
// handleStaticIP - handles static IP request
// It either checks if we have a static IP
// Or if set=true, it tries to set it
func handleStaticIP(ip string, set bool) staticIPJSON {
resp := staticIPJSON{}
interfaceName := util.GetInterfaceByIP(ip)
resp.Static = "no"
if len(interfaceName) == 0 {
resp.Static = "error"
resp.Error = fmt.Sprintf("Couldn't find network interface by IP %s", ip)
return resp
}
if set {
// Try to set static IP for the specified interface
err := dhcpd.SetStaticIP(interfaceName)
if err != nil {
resp.Static = "error"
resp.Error = err.Error()
return resp
}
}
// Fallthrough here even if we set static IP
// Check if we have a static IP and return the details
isStaticIP, err := dhcpd.HasStaticIP(interfaceName)
if err != nil {
resp.Static = "error"
resp.Error = err.Error()
} else {
if isStaticIP {
resp.Static = "yes"
}
resp.IP = util.GetSubnet(interfaceName)
}
return resp
}
// Check if DNSStubListener is active // Check if DNSStubListener is active
func checkDNSStubListener() bool { func checkDNSStubListener() bool {
if runtime.GOOS != "linux" { if runtime.GOOS != "linux" {
@ -129,7 +197,7 @@ func checkDNSStubListener() bool {
log.Tracef("executing %s %v", cmd.Path, cmd.Args) log.Tracef("executing %s %v", cmd.Path, cmd.Args)
_, err := cmd.Output() _, err := cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 { if err != nil || cmd.ProcessState.ExitCode() != 0 {
log.Error("command %s has failed: %v code:%d", log.Info("command %s has failed: %v code:%d",
cmd.Path, err, cmd.ProcessState.ExitCode()) cmd.Path, err, cmd.ProcessState.ExitCode())
return false return false
} }
@ -138,7 +206,7 @@ func checkDNSStubListener() bool {
log.Tracef("executing %s %v", cmd.Path, cmd.Args) log.Tracef("executing %s %v", cmd.Path, cmd.Args)
_, err = cmd.Output() _, err = cmd.Output()
if err != nil || cmd.ProcessState.ExitCode() != 0 { if err != nil || cmd.ProcessState.ExitCode() != 0 {
log.Error("command %s has failed: %v code:%d", log.Info("command %s has failed: %v code:%d",
cmd.Path, err, cmd.ProcessState.ExitCode()) cmd.Path, err, cmd.ProcessState.ExitCode())
return false return false
} }
@ -228,7 +296,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
// validate that hosts and ports are bindable // validate that hosts and ports are bindable
if restartHTTP { if restartHTTP {
err = checkPortAvailable(newSettings.Web.IP, newSettings.Web.Port) err = util.CheckPortAvailable(newSettings.Web.IP, newSettings.Web.Port)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "Impossible to listen on IP:port %s due to %s", 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) net.JoinHostPort(newSettings.Web.IP, strconv.Itoa(newSettings.Web.Port)), err)
@ -236,13 +304,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 { if err != nil {
httpError(w, http.StatusBadRequest, "%s", err) httpError(w, http.StatusBadRequest, "%s", err)
return return
} }
err = checkPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port) err = util.CheckPortAvailable(newSettings.DNS.IP, newSettings.DNS.Port)
if err != nil { if err != nil {
httpError(w, http.StatusBadRequest, "%s", err) httpError(w, http.StatusBadRequest, "%s", err)
return return
@ -251,7 +319,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
var curConfig configuration var curConfig configuration
copyInstallSettings(&curConfig, &config) copyInstallSettings(&curConfig, &config)
config.firstRun = false Context.firstRun = false
config.BindHost = newSettings.Web.IP config.BindHost = newSettings.Web.IP
config.BindPort = newSettings.Web.Port config.BindPort = newSettings.Web.Port
config.DNS.BindHost = newSettings.DNS.IP config.DNS.BindHost = newSettings.DNS.IP
@ -266,7 +334,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
} }
} }
if err != nil || err2 != nil { if err != nil || err2 != nil {
config.firstRun = true Context.firstRun = true
copyInstallSettings(&config, &curConfig) copyInstallSettings(&config, &curConfig)
if err != nil { if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't initialize DNS server: %s", err) httpError(w, http.StatusInternalServerError, "Couldn't initialize DNS server: %s", err)
@ -278,11 +346,11 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
u := User{} u := User{}
u.Name = newSettings.Username u.Name = newSettings.Username
config.auth.UserAdd(&u, newSettings.Password) Context.auth.UserAdd(&u, newSettings.Password)
err = config.write() err = config.write()
if err != nil { if err != nil {
config.firstRun = true Context.firstRun = true
copyInstallSettings(&config, &curConfig) copyInstallSettings(&config, &curConfig)
httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err) httpError(w, http.StatusInternalServerError, "Couldn't write config: %s", err)
return return

View file

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

View file

@ -17,6 +17,8 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@ -64,7 +66,7 @@ type getVersionJSONRequest struct {
// Get the latest available version from the Internet // Get the latest available version from the Internet
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
if config.disableUpdate { if Context.disableUpdate {
return return
} }
@ -77,10 +79,10 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
now := time.Now() now := time.Now()
if !req.RecheckNow { if !req.RecheckNow {
config.controlLock.Lock() Context.controlLock.Lock()
cached := now.Sub(config.versionCheckLastTime) <= versionCheckPeriod && len(config.versionCheckJSON) != 0 cached := now.Sub(config.versionCheckLastTime) <= versionCheckPeriod && len(config.versionCheckJSON) != 0
data := config.versionCheckJSON data := config.versionCheckJSON
config.controlLock.Unlock() Context.controlLock.Unlock()
if cached { if cached {
log.Tracef("Returning cached data") log.Tracef("Returning cached data")
@ -93,7 +95,7 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
var resp *http.Response var resp *http.Response
for i := 0; i != 3; i++ { for i := 0; i != 3; i++ {
log.Tracef("Downloading data from %s", versionCheckURL) log.Tracef("Downloading data from %s", versionCheckURL)
resp, err = config.client.Get(versionCheckURL) resp, err = Context.client.Get(versionCheckURL)
if err != nil && strings.HasSuffix(err.Error(), "i/o timeout") { if err != nil && strings.HasSuffix(err.Error(), "i/o timeout") {
// This case may happen while we're restarting DNS server // This case may happen while we're restarting DNS server
// https://github.com/AdguardTeam/AdGuardHome/issues/934 // https://github.com/AdguardTeam/AdGuardHome/issues/934
@ -116,10 +118,10 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
return return
} }
config.controlLock.Lock() Context.controlLock.Lock()
config.versionCheckLastTime = now config.versionCheckLastTime = now
config.versionCheckJSON = body config.versionCheckJSON = body
config.controlLock.Unlock() Context.controlLock.Unlock()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_, err = w.Write(getVersionResp(body)) _, err = w.Write(getVersionResp(body))
@ -158,7 +160,7 @@ type updateInfo struct {
func getUpdateInfo(jsonData []byte) (*updateInfo, error) { func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
var u updateInfo var u updateInfo
workDir := config.ourWorkingDir workDir := Context.workDir
versionJSON := make(map[string]interface{}) versionJSON := make(map[string]interface{})
err := json.Unmarshal(jsonData, &versionJSON) err := json.Unmarshal(jsonData, &versionJSON)
@ -196,7 +198,7 @@ func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
binName = "AdGuardHome.exe" binName = "AdGuardHome.exe"
} }
u.curBinName = filepath.Join(workDir, binName) 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) return nil, fmt.Errorf("Executable file %s doesn't exist", u.curBinName)
} }
u.bkpBinName = filepath.Join(u.backupDir, binName) u.bkpBinName = filepath.Join(u.backupDir, binName)
@ -365,7 +367,7 @@ func copySupportingFiles(files []string, srcdir, dstdir string, useSrcNameOnly,
// Download package file and save it to disk // Download package file and save it to disk
func getPackageFile(u *updateInfo) error { func getPackageFile(u *updateInfo) error {
resp, err := config.client.Get(u.pkgURL) resp, err := Context.client.Get(u.pkgURL)
if err != nil { if err != nil {
return fmt.Errorf("HTTP request failed: %s", err) return fmt.Errorf("HTTP request failed: %s", err)
} }
@ -436,17 +438,17 @@ func doUpdate(u *updateInfo) error {
} }
// ./README.md -> backup/README.md // ./README.md -> backup/README.md
err = copySupportingFiles(files, config.ourWorkingDir, u.backupDir, true, true) err = copySupportingFiles(files, Context.workDir, u.backupDir, true, true)
if err != nil { if err != nil {
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
config.ourWorkingDir, u.backupDir, err) Context.workDir, u.backupDir, err)
} }
// update/[AdGuardHome/]README.md -> ./README.md // update/[AdGuardHome/]README.md -> ./README.md
err = copySupportingFiles(files, u.updateDir, config.ourWorkingDir, false, true) err = copySupportingFiles(files, u.updateDir, Context.workDir, false, true)
if err != nil { if err != nil {
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
u.updateDir, config.ourWorkingDir, err) u.updateDir, Context.workDir, err)
} }
log.Tracef("Renaming: %s -> %s", u.curBinName, u.bkpBinName) log.Tracef("Renaming: %s -> %s", u.curBinName, u.bkpBinName)
@ -478,8 +480,7 @@ func finishUpdate(u *updateInfo) {
cleanupAlways() cleanupAlways()
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if Context.runningAsService {
if config.runningAsService {
// Note: // Note:
// we can't restart the service via "kardianos/service" package - it kills the process first // we can't restart the service via "kardianos/service" package - it kills the process first
// we can't start a new instance - Windows doesn't allow it // we can't start a new instance - Windows doesn't allow it

View file

@ -8,9 +8,8 @@ import (
) )
func TestDoUpdate(t *testing.T) { func TestDoUpdate(t *testing.T) {
config.DNS.Port = 0 config.DNS.Port = 0
config.ourWorkingDir = "..." // set absolute path Context.workDir = "..." // set absolute path
newver := "v0.96" newver := "v0.96"
data := `{ data := `{
@ -35,15 +34,15 @@ func TestDoUpdate(t *testing.T) {
u := updateInfo{ u := updateInfo{
pkgURL: "https://github.com/AdguardTeam/AdGuardHome/releases/download/" + newver + "/AdGuardHome_linux_amd64.tar.gz", pkgURL: "https://github.com/AdguardTeam/AdGuardHome/releases/download/" + newver + "/AdGuardHome_linux_amd64.tar.gz",
pkgName: config.ourWorkingDir + "/agh-update-" + newver + "/AdGuardHome_linux_amd64.tar.gz", pkgName: Context.workDir + "/agh-update-" + newver + "/AdGuardHome_linux_amd64.tar.gz",
newVer: newver, newVer: newver,
updateDir: config.ourWorkingDir + "/agh-update-" + newver, updateDir: Context.workDir + "/agh-update-" + newver,
backupDir: config.ourWorkingDir + "/agh-backup", backupDir: Context.workDir + "/agh-backup",
configName: config.ourWorkingDir + "/AdGuardHome.yaml", configName: Context.workDir + "/AdGuardHome.yaml",
updateConfigName: config.ourWorkingDir + "/agh-update-" + newver + "/AdGuardHome/AdGuardHome.yaml", updateConfigName: Context.workDir + "/agh-update-" + newver + "/AdGuardHome/AdGuardHome.yaml",
curBinName: config.ourWorkingDir + "/AdGuardHome", curBinName: Context.workDir + "/AdGuardHome",
bkpBinName: config.ourWorkingDir + "/agh-backup/AdGuardHome", bkpBinName: Context.workDir + "/agh-backup/AdGuardHome",
newBinName: config.ourWorkingDir + "/agh-update-" + newver + "/AdGuardHome/AdGuardHome", newBinName: Context.workDir + "/agh-update-" + newver + "/AdGuardHome/AdGuardHome",
} }
if uu.pkgURL != u.pkgURL || if uu.pkgURL != u.pkgURL ||

View file

@ -25,7 +25,7 @@ func onConfigModified() {
// Please note that we must do it even if we don't start it // Please note that we must do it even if we don't start it
// so that we had access to the query log and the stats // so that we had access to the query log and the stats
func initDNSServer() error { func initDNSServer() error {
baseDir := config.getDataDir() baseDir := Context.getDataDir()
err := os.MkdirAll(baseDir, 0755) err := os.MkdirAll(baseDir, 0755)
if err != nil { if err != nil {
@ -71,8 +71,8 @@ func initDNSServer() error {
} }
sessFilename := filepath.Join(baseDir, "sessions.db") sessFilename := filepath.Join(baseDir, "sessions.db")
config.auth = InitAuth(sessFilename, config.Users, config.WebSessionTTLHours*60*60) Context.auth = InitAuth(sessFilename, config.Users, config.WebSessionTTLHours*60*60)
if config.auth == nil { if Context.auth == nil {
closeDNSServer() closeDNSServer()
return fmt.Errorf("Couldn't initialize Auth module") return fmt.Errorf("Couldn't initialize Auth module")
} }
@ -294,9 +294,9 @@ func closeDNSServer() {
Context.queryLog = nil Context.queryLog = nil
} }
if config.auth != nil { if Context.auth != nil {
config.auth.Close() Context.auth.Close()
config.auth = nil Context.auth = nil
} }
log.Debug("Closed all DNS modules") log.Debug("Closed all DNS modules")

View file

@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
@ -401,7 +402,7 @@ func parseFilterContents(contents []byte) (int, string) {
// Count lines in the filter // Count lines in the filter
for len(data) != 0 { for len(data) != 0 {
line := SplitNext(&data, '\n') line := util.SplitNext(&data, '\n')
if len(line) == 0 { if len(line) == 0 {
continue continue
} }
@ -424,7 +425,7 @@ func parseFilterContents(contents []byte) (int, string) {
func (filter *filter) update() (bool, error) { func (filter *filter) update() (bool, error) {
log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL) log.Tracef("Downloading update for filter %d from %s", filter.ID, filter.URL)
resp, err := config.client.Get(filter.URL) resp, err := Context.client.Get(filter.URL)
if resp != nil && resp.Body != nil { if resp != nil && resp.Body != nil {
defer resp.Body.Close() defer resp.Body.Close()
} }
@ -538,7 +539,7 @@ func (filter *filter) unload() {
// Path to the filter contents // Path to the filter contents
func (filter *filter) Path() string { func (filter *filter) Path() string {
return filepath.Join(config.getDataDir(), filterDir, strconv.FormatInt(filter.ID, 10)+".txt") return filepath.Join(Context.getDataDir(), filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
} }
// LastTimeUpdated returns the time when the filter was last time updated // LastTimeUpdated returns the time when the filter was last time updated

View file

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

View file

@ -1,380 +0,0 @@
package home
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"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" {
config.controlLock.Lock()
defer config.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 := config.ourConfigFilename
if !filepath.IsAbs(configfile) {
configfile = filepath.Join(config.ourWorkingDir, config.ourConfigFilename)
}
_, 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 !config.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 config.firstRun &&
!(strings.HasPrefix(r.URL.Path, "/install.") ||
strings.HasPrefix(r.URL.Path, "/__locales/") ||
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}
}
// ------------------
// network interfaces
// ------------------
type netInterface struct {
Name string `json:"name"`
MTU int `json:"mtu"`
HardwareAddr string `json:"hardware_address"`
Addresses []string `json:"ip_addresses"`
Flags string `json:"flags"`
}
// 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 := getValidNetInterfaces()
if err != nil {
return nil, errorx.Decorate(err, "Couldn't get interfaces")
}
if len(ifaces) == 0 {
return nil, errors.New("couldn't find any legible interface")
}
var netInterfaces []netInterface
for _, iface := range ifaces {
addrs, e := iface.Addrs()
if e != nil {
return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name)
}
netIface := netInterface{
Name: iface.Name,
MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr.String(),
}
if iface.Flags != 0 {
netIface.Flags = iface.Flags.String()
}
// we don't want link-local addresses in json, so skip them
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
return nil, fmt.Errorf("SHOULD NOT HAPPEN: 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.String())
}
if len(netIface.Addresses) != 0 {
netInterfaces = append(netInterfaces, netIface)
}
}
return netInterfaces, nil
}
// 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 {
ln, err := net.Listen("tcp", net.JoinHostPort(host, 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
}
func checkPacketPortAvailable(host string, port int) error {
ln, err := net.ListenPacket("udp", net.JoinHostPort(host, 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
}
// 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...)
}
// 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
}
// ---------------------
// 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" "syscall"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/joomcode/errorx"
"github.com/AdguardTeam/AdGuardHome/isdelve" "github.com/AdguardTeam/AdGuardHome/isdelve"
"github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dhcpd"
@ -49,6 +53,9 @@ const versionCheckPeriod = time.Hour * 8
// Global context // Global context
type homeContext struct { type homeContext struct {
// Modules
// --
clients clientsContainer // per-client-settings module clients clientsContainer // per-client-settings module
stats stats.Stats // statistics module stats stats.Stats // statistics module
queryLog querylog.QueryLog // query log module queryLog querylog.QueryLog // query log module
@ -57,8 +64,29 @@ type homeContext struct {
whois *Whois // WHOIS module whois *Whois // WHOIS module
dnsFilter *dnsfilter.Dnsfilter // DNS filtering module dnsFilter *dnsfilter.Dnsfilter // DNS filtering module
dhcpServer *dhcpd.Server // DHCP module dhcpServer *dhcpd.Server // DHCP module
auth *Auth // HTTP authentication module
httpServer *http.Server // HTTP module httpServer *http.Server // HTTP module
httpsServer HTTPSServer // HTTPS module httpsServer HTTPSServer // HTTPS module
// Runtime properties
// --
configFilename string // Config filename (can be overridden via the command line arguments)
workDir string // Location of our directory, used to protect against CWD being somewhere else
firstRun bool // if set to true, don't run any services except HTTP web inteface, and serve only first-run html
pidFileName string // PID file name. Empty if no PID file was created.
disableUpdate bool // If set, don't check for updates
controlLock sync.Mutex
transport *http.Transport
client *http.Client
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
// runningAsService flag is set to true when options are passed from the service runner
runningAsService bool
}
// getDataDir returns path to the directory where we store databases and filters
func (c *homeContext) getDataDir() string {
return filepath.Join(c.workDir, dataDir)
} }
// Context - a global context object // Context - a global context object
@ -81,17 +109,38 @@ func Main(version string, channel string, armVer string) {
return return
} }
Context.appSignalChannel = make(chan os.Signal)
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
<-Context.appSignalChannel
cleanup()
cleanupAlways()
os.Exit(0)
}()
// run the protection // run the protection
run(args) run(args)
} }
// run initializes configuration and runs the AdGuard Home // run initializes configuration and runs the AdGuard Home
// run is a blocking method and it won't exit until the service is stopped! // run is a blocking method!
// nolint // nolint
func run(args options) { func run(args options) {
// config file path can be overridden by command-line arguments: // config file path can be overridden by command-line arguments:
if args.configFilename != "" { if args.configFilename != "" {
config.ourConfigFilename = args.configFilename Context.configFilename = args.configFilename
} else {
// Default config file name
Context.configFilename = "AdGuardHome.yaml"
}
// Init some of the Context fields right away
Context.transport = &http.Transport{
DialContext: customDialContext,
}
Context.client = &http.Client{
Timeout: time.Minute * 5,
Transport: Context.transport,
} }
// configure working dir and config path // configure working dir and config path
@ -106,31 +155,22 @@ func run(args options) {
msg = msg + " v" + ARMVersion msg = msg + " v" + ARMVersion
} }
log.Printf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH, ARMVersion) log.Printf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH, ARMVersion)
log.Debug("Current working directory is %s", config.ourWorkingDir) log.Debug("Current working directory is %s", Context.workDir)
if args.runningAsService { if args.runningAsService {
log.Info("AdGuard Home is running as a service") log.Info("AdGuard Home is running as a service")
} }
config.runningAsService = args.runningAsService Context.runningAsService = args.runningAsService
config.disableUpdate = args.disableUpdate Context.disableUpdate = args.disableUpdate
config.firstRun = detectFirstRun() Context.firstRun = detectFirstRun()
if config.firstRun { if Context.firstRun {
requireAdminRights() requireAdminRights()
} }
config.appSignalChannel = make(chan os.Signal)
signal.Notify(config.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
<-config.appSignalChannel
cleanup()
cleanupAlways()
os.Exit(0)
}()
initConfig() initConfig()
initServices() initServices()
if !config.firstRun { if !Context.firstRun {
// Do the upgrade if necessary // Do the upgrade if necessary
err := upgradeConfig() err := upgradeConfig()
if err != nil { if err != nil {
@ -148,7 +188,7 @@ func run(args options) {
} }
} }
config.DHCP.WorkDir = config.ourWorkingDir config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified config.DHCP.ConfigModified = onConfigModified
Context.dhcpServer = dhcpd.Create(config.DHCP) Context.dhcpServer = dhcpd.Create(config.DHCP)
@ -157,7 +197,7 @@ func run(args options) {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
config.RlimitNoFile != 0 { config.RlimitNoFile != 0 {
setRlimit(config.RlimitNoFile) util.SetRlimit(config.RlimitNoFile)
} }
// override bind host/port from the console // override bind host/port from the console
@ -168,7 +208,7 @@ func run(args options) {
config.BindPort = args.bindPort config.BindPort = args.bindPort
} }
if !config.firstRun { if !Context.firstRun {
// Save the updated config // Save the updated config
err := config.write() err := config.write()
if err != nil { if err != nil {
@ -193,7 +233,7 @@ func run(args options) {
} }
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) { if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
config.pidFileName = args.pidFile Context.pidFileName = args.pidFile
} }
// Initialize and run the admin Web interface // Initialize and run the admin Web interface
@ -204,7 +244,7 @@ func run(args options) {
registerControlHandlers() registerControlHandlers()
// add handlers for /install paths, we only need them when we're not configured yet // add handlers for /install paths, we only need them when we're not configured yet
if config.firstRun { if Context.firstRun {
log.Info("This is the first launch of AdGuard Home, redirecting everything to /install.html ") log.Info("This is the first launch of AdGuard Home, redirecting everything to /install.html ")
http.Handle("/install.html", preInstallHandler(http.FileServer(box))) http.Handle("/install.html", preInstallHandler(http.FileServer(box)))
registerInstallHandlers() registerInstallHandlers()
@ -291,7 +331,7 @@ func httpServerLoop() {
// Check if the current user has root (administrator) rights // Check if the current user has root (administrator) rights
// and if not, ask and try to run as root // and if not, ask and try to run as root
func requireAdminRights() { func requireAdminRights() {
admin, _ := haveAdminRights() admin, _ := util.HaveAdminRights()
if //noinspection ALL if //noinspection ALL
admin || isdelve.Enabled { admin || isdelve.Enabled {
return return
@ -331,7 +371,7 @@ func writePIDFile(fn string) bool {
return true return true
} }
// initWorkingDir initializes the ourWorkingDir // initWorkingDir initializes the workDir
// if no command-line arguments specified, we use the directory where our binary file is located // if no command-line arguments specified, we use the directory where our binary file is located
func initWorkingDir(args options) { func initWorkingDir(args options) {
execPath, err := os.Executable() execPath, err := os.Executable()
@ -341,9 +381,9 @@ func initWorkingDir(args options) {
if args.workDir != "" { if args.workDir != "" {
// If there is a custom config file, use it's directory as our working dir // If there is a custom config file, use it's directory as our working dir
config.ourWorkingDir = args.workDir Context.workDir = args.workDir
} else { } else {
config.ourWorkingDir = filepath.Dir(execPath) Context.workDir = filepath.Dir(execPath)
} }
} }
@ -376,12 +416,12 @@ func configureLogger(args options) {
if ls.LogFile == configSyslog { if ls.LogFile == configSyslog {
// Use syslog where it is possible and eventlog on Windows // Use syslog where it is possible and eventlog on Windows
err := configureSyslog() err := util.ConfigureSyslog(serviceName)
if err != nil { if err != nil {
log.Fatalf("cannot initialize syslog: %s", err) log.Fatalf("cannot initialize syslog: %s", err)
} }
} else { } else {
logFilePath := filepath.Join(config.ourWorkingDir, ls.LogFile) logFilePath := filepath.Join(Context.workDir, ls.LogFile)
if filepath.IsAbs(ls.LogFile) { if filepath.IsAbs(ls.LogFile) {
logFilePath = ls.LogFile logFilePath = ls.LogFile
} }
@ -420,8 +460,8 @@ func stopHTTPServer() {
// This function is called before application exits // This function is called before application exits
func cleanupAlways() { func cleanupAlways() {
if len(config.pidFileName) != 0 { if len(Context.pidFileName) != 0 {
_ = os.Remove(config.pidFileName) _ = os.Remove(Context.pidFileName)
} }
log.Info("Stopped") log.Info("Stopped")
} }
@ -544,7 +584,7 @@ func printHTTPAddresses(proto string) {
} }
} else if config.BindHost == "0.0.0.0" { } else if config.BindHost == "0.0.0.0" {
log.Println("AdGuard Home is available on the following addresses:") log.Println("AdGuard Home is available on the following addresses:")
ifaces, err := getValidNetInterfacesForWeb() ifaces, err := util.GetValidNetInterfacesForWeb()
if err != nil { if err != nil {
// That's weird, but we'll ignore it // That's weird, but we'll ignore it
address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort)) address = net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
@ -561,3 +601,60 @@ func printHTTPAddresses(proto string) {
log.Printf("Go to %s://%s", proto, address) 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

@ -107,6 +107,9 @@ schema_version: 5
// . Wait until the filters are downloaded // . Wait until the filters are downloaded
// . Stop and cleanup // . Stop and cleanup
func TestHome(t *testing.T) { func TestHome(t *testing.T) {
// Reinit context
Context = homeContext{}
dir := prepareTestDir() dir := prepareTestDir()
defer func() { _ = os.RemoveAll(dir) }() defer func() { _ = os.RemoveAll(dir) }()
fn := filepath.Join(dir, "AdGuardHome.yaml") fn := filepath.Join(dir, "AdGuardHome.yaml")
@ -123,12 +126,12 @@ func TestHome(t *testing.T) {
var err error var err error
var resp *http.Response var resp *http.Response
h := http.Client{} h := http.Client{}
for i := 0; i != 5; i++ { for i := 0; i != 50; i++ {
resp, err = h.Get("http://127.0.0.1:3000/") resp, err = h.Get("http://127.0.0.1:3000/")
if err == nil && resp.StatusCode != 404 { if err == nil && resp.StatusCode != 404 {
break break
} }
time.Sleep(1 * time.Second) time.Sleep(100 * time.Millisecond)
} }
assert.Truef(t, err == nil, "%s", err) assert.Truef(t, err == nil, "%s", err)
assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 200, resp.StatusCode)
@ -140,7 +143,7 @@ func TestHome(t *testing.T) {
// test DNS over UDP // test DNS over UDP
r := upstream.NewResolver("127.0.0.1:5354", 3*time.Second) r := upstream.NewResolver("127.0.0.1:5354", 3*time.Second)
addrs, err := r.LookupIPAddr(context.TODO(), "static.adguard.com") addrs, err := r.LookupIPAddr(context.TODO(), "static.adguard.com")
assert.Truef(t, err == nil, "%s", err) assert.Nil(t, err)
haveIP := len(addrs) != 0 haveIP := len(addrs) != 0
assert.True(t, haveIP) assert.True(t, haveIP)

View file

@ -7,6 +7,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/AdguardTeam/AdGuardHome/util"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/kardianos/service" "github.com/kardianos/service"
) )
@ -34,10 +35,10 @@ func (p *program) Start(s service.Service) error {
// Stop stops the program // Stop stops the program
func (p *program) Stop(s service.Service) error { func (p *program) Stop(s service.Service) error {
// Stop should not block. Return with a few seconds. // Stop should not block. Return with a few seconds.
if config.appSignalChannel == nil { if Context.appSignalChannel == nil {
os.Exit(0) os.Exit(0)
} }
config.appSignalChannel <- syscall.SIGINT Context.appSignalChannel <- syscall.SIGINT
return nil return nil
} }
@ -229,7 +230,7 @@ func configureService(c *service.Config) {
// returns command code or error if any // returns command code or error if any
func runInitdCommand(action string) (int, error) { func runInitdCommand(action string) (int, error) {
confPath := "/etc/init.d/" + serviceName confPath := "/etc/init.d/" + serviceName
code, _, err := runCommand("sh", "-c", confPath+" "+action) code, _, err := util.RunCommand("sh", "-c", confPath+" "+action)
return code, err return code, err
} }

View file

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

View file

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

View file

@ -1831,6 +1831,9 @@ definitions:
$ref: "#/definitions/CheckConfigRequestInfo" $ref: "#/definitions/CheckConfigRequestInfo"
web: web:
$ref: "#/definitions/CheckConfigRequestInfo" $ref: "#/definitions/CheckConfigRequestInfo"
set_static_ip:
type: "boolean"
example: false
CheckConfigRequestInfo: CheckConfigRequestInfo:
type: "object" type: "object"
properties: properties:
@ -1851,6 +1854,8 @@ definitions:
$ref: "#/definitions/CheckConfigResponseInfo" $ref: "#/definitions/CheckConfigResponseInfo"
web: web:
$ref: "#/definitions/CheckConfigResponseInfo" $ref: "#/definitions/CheckConfigResponseInfo"
static_ip:
$ref: "#/definitions/CheckConfigStaticIpInfo"
CheckConfigResponseInfo: CheckConfigResponseInfo:
type: "object" type: "object"
properties: properties:
@ -1860,6 +1865,23 @@ definitions:
can_autofix: can_autofix:
type: "boolean" type: "boolean"
example: false example: false
CheckConfigStaticIpInfo:
type: "object"
properties:
static:
type: "string"
example: "no"
description: "Can be: yes, no, error"
ip:
type: "string"
example: "192.168.1.1"
description: "Current dynamic IP address. Set if static=no"
error:
type: "string"
example: ""
description: "Error text. Set if static=error"
InitialConfiguration: InitialConfiguration:
type: "object" type: "object"
description: "AdGuard Home initial configuration (for the first-install wizard)" description: "AdGuard Home initial configuration (for the first-install wizard)"

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: %v: %s", command, err, string(out))
}
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)
}

194
util/network_utils.go Normal file
View file

@ -0,0 +1,194 @@
package util
import (
"errors"
"fmt"
"net"
"os"
"runtime"
"strconv"
"syscall"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/joomcode/errorx"
)
// 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 := GetValidNetInterfaces()
if err != nil {
return nil, errorx.Decorate(err, "Couldn't get interfaces")
}
if len(ifaces) == 0 {
return nil, errors.New("couldn't find any legible interface")
}
var netInterfaces []NetInterface
for _, iface := range ifaces {
addrs, e := iface.Addrs()
if e != nil {
return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name)
}
netIface := NetInterface{
Name: iface.Name,
MTU: iface.MTU,
HardwareAddr: iface.HardwareAddr.String(),
}
if iface.Flags != 0 {
netIface.Flags = iface.Flags.String()
}
// Collect network interface addresses
for _, addr := range addrs {
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() {
continue
}
// 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)
}
}
return netInterfaces, nil
}
// Get interface name by its IP address.
func GetInterfaceByIP(ip string) string {
ifaces, err := GetValidNetInterfacesForWeb()
if err != nil {
return ""
}
for _, iface := range ifaces {
for _, addr := range iface.Addresses {
if ip == addr {
return iface.Name
}
}
}
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 {
ln, err := net.Listen("tcp", net.JoinHostPort(host, 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
}
func CheckPacketPortAvailable(host string, port int) error {
ln, err := net.ListenPacket("udp", net.JoinHostPort(host, 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
}
// 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

@ -1,14 +1,12 @@
package home package util
import ( import (
"log"
"testing" "testing"
"github.com/AdguardTeam/golibs/log"
"github.com/stretchr/testify/assert"
) )
func TestGetValidNetInterfacesForWeb(t *testing.T) { func TestGetValidNetInterfacesForWeb(t *testing.T) {
ifaces, err := getValidNetInterfacesForWeb() ifaces, err := GetValidNetInterfacesForWeb()
if err != nil { if err != nil {
t.Fatalf("Cannot get net interfaces: %s", err) t.Fatalf("Cannot get net interfaces: %s", err)
} }
@ -24,10 +22,3 @@ func TestGetValidNetInterfacesForWeb(t *testing.T) {
log.Printf("%v", iface) 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 // +build freebsd
package home package util
import ( import (
"os" "os"
@ -11,7 +11,7 @@ import (
// Set user-specified limit of how many fd's we can use // Set user-specified limit of how many fd's we can use
// https://github.com/AdguardTeam/AdGuardHome/issues/659 // https://github.com/AdguardTeam/AdGuardHome/issues/659
func setRlimit(val uint) { func SetRlimit(val uint) {
var rlim syscall.Rlimit var rlim syscall.Rlimit
rlim.Max = int64(val) rlim.Max = int64(val)
rlim.Cur = int64(val) rlim.Cur = int64(val)
@ -22,6 +22,6 @@ func setRlimit(val uint) {
} }
// Check if the current user has root (administrator) rights // Check if the current user has root (administrator) rights
func haveAdminRights() (bool, error) { func HaveAdminRights() (bool, error) {
return os.Getuid() == 0, nil return os.Getuid() == 0, nil
} }

View file

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

View file

@ -1,12 +1,12 @@
package home package util
import "golang.org/x/sys/windows" import "golang.org/x/sys/windows"
// Set user-specified limit of how many fd's we can use // 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 var token windows.Token
h, _ := windows.GetCurrentProcess() h, _ := windows.GetCurrentProcess()
err := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token) err := windows.OpenProcessToken(h, windows.TOKEN_QUERY, &token)

View file

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

View file

@ -1,4 +1,4 @@
package home package util
import ( import (
"log" "log"
@ -17,7 +17,7 @@ func (w *eventLogWriter) Write(b []byte) (int, error) {
return len(b), w.el.Info(1, string(b)) 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 // 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 // Otherwise, we will get "the description for event id cannot be found" warning in every log record