diff --git a/client/src/components/ui/Update.css b/client/src/components/ui/Topline.css
similarity index 89%
rename from client/src/components/ui/Update.css
rename to client/src/components/ui/Topline.css
index ec7ec532..33c4e8fd 100644
--- a/client/src/components/ui/Update.css
+++ b/client/src/components/ui/Topline.css
@@ -1,4 +1,4 @@
-.update {
+.topline {
position: relative;
z-index: 102;
margin-bottom: 0;
diff --git a/client/src/components/ui/Topline.js b/client/src/components/ui/Topline.js
new file mode 100644
index 00000000..13bfd827
--- /dev/null
+++ b/client/src/components/ui/Topline.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import './Topline.css';
+
+const Topline = props => (
+
+);
+
+Topline.propTypes = {
+ children: PropTypes.node.isRequired,
+ type: PropTypes.string.isRequired,
+};
+
+export default Topline;
diff --git a/client/src/components/ui/Update.js b/client/src/components/ui/Update.js
deleted file mode 100644
index 5df9df65..00000000
--- a/client/src/components/ui/Update.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import './Update.css';
-
-const Update = props => (
-
-
- {props.announcement}
Click here for more info.
-
-
-);
-
-Update.propTypes = {
- announcement: PropTypes.string.isRequired,
- announcementUrl: PropTypes.string.isRequired,
-};
-
-export default Update;
diff --git a/client/src/components/ui/UpdateTopline.js b/client/src/components/ui/UpdateTopline.js
new file mode 100644
index 00000000..a9124666
--- /dev/null
+++ b/client/src/components/ui/UpdateTopline.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Trans, withNamespaces } from 'react-i18next';
+
+import Topline from './Topline';
+
+const UpdateTopline = props => (
+
+
+ Click here
+ ,
+ ]}
+ >
+ update_announcement
+
+
+);
+
+UpdateTopline.propTypes = {
+ version: PropTypes.string.isRequired,
+ url: PropTypes.string.isRequired,
+};
+
+export default withNamespaces()(UpdateTopline);
diff --git a/client/src/containers/App.js b/client/src/containers/App.js
index 905596c5..b6ce2cde 100644
--- a/client/src/containers/App.js
+++ b/client/src/containers/App.js
@@ -3,8 +3,8 @@ import * as actionCreators from '../actions';
import App from '../components/App';
const mapStateToProps = (state) => {
- const { dashboard } = state;
- const props = { dashboard };
+ const { dashboard, encryption } = state;
+ const props = { dashboard, encryption };
return props;
};
diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js
index 7d46b751..d593761a 100644
--- a/client/src/containers/Settings.js
+++ b/client/src/containers/Settings.js
@@ -12,11 +12,26 @@ import {
setDhcpConfig,
findActiveDhcp,
} from '../actions';
+import {
+ getTlsStatus,
+ setTlsConfig,
+ validateTlsConfig,
+} from '../actions/encryption';
import Settings from '../components/Settings';
const mapStateToProps = (state) => {
- const { settings, dashboard, dhcp } = state;
- const props = { settings, dashboard, dhcp };
+ const {
+ settings,
+ dashboard,
+ dhcp,
+ encryption,
+ } = state;
+ const props = {
+ settings,
+ dashboard,
+ dhcp,
+ encryption,
+ };
return props;
};
@@ -32,6 +47,9 @@ const mapDispatchToProps = {
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
+ getTlsStatus,
+ setTlsConfig,
+ validateTlsConfig,
};
export default connect(
diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js
index 3f3172b9..295bae58 100644
--- a/client/src/helpers/constants.js
+++ b/client/src/helpers/constants.js
@@ -73,3 +73,77 @@ export const SETTINGS_NAMES = {
export const STANDARD_DNS_PORT = 53;
export const STANDARD_WEB_PORT = 80;
+export const STANDARD_HTTPS_PORT = 443;
+
+export const EMPTY_DATE = '0001-01-01T00:00:00Z';
+
+export const DEBOUNCE_TIMEOUT = 300;
+export const CHECK_TIMEOUT = 1000;
+export const STOP_TIMEOUT = 10000;
+
+export const UNSAFE_PORTS = [
+ 1,
+ 7,
+ 9,
+ 11,
+ 13,
+ 15,
+ 17,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 25,
+ 37,
+ 42,
+ 43,
+ 53,
+ 77,
+ 79,
+ 87,
+ 95,
+ 101,
+ 102,
+ 103,
+ 104,
+ 109,
+ 110,
+ 111,
+ 113,
+ 115,
+ 117,
+ 119,
+ 123,
+ 135,
+ 139,
+ 143,
+ 179,
+ 389,
+ 465,
+ 512,
+ 513,
+ 514,
+ 515,
+ 526,
+ 530,
+ 531,
+ 532,
+ 540,
+ 556,
+ 563,
+ 587,
+ 601,
+ 636,
+ 993,
+ 995,
+ 2049,
+ 3659,
+ 4045,
+ 6000,
+ 6665,
+ 6666,
+ 6667,
+ 6668,
+ 6669,
+];
diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js
new file mode 100644
index 00000000..1f0a339a
--- /dev/null
+++ b/client/src/helpers/form.js
@@ -0,0 +1,79 @@
+import React, { Fragment } from 'react';
+import { Trans } from 'react-i18next';
+
+import { R_IPV4, UNSAFE_PORTS } from '../helpers/constants';
+
+export const renderField = ({
+ input, id, className, placeholder, type, disabled, meta: { touched, error },
+}) => (
+
+
+ {!disabled && touched && (error && {error})}
+
+);
+
+export const renderSelectField = ({
+ input, placeholder, disabled, meta: { touched, error },
+}) => (
+
+
+ {!disabled && touched && (error && {error})}
+
+);
+
+export const required = (value) => {
+ if (value || value === 0) {
+ return false;
+ }
+ return
form_error_required;
+};
+
+export const ipv4 = (value) => {
+ if (value && !new RegExp(R_IPV4).test(value)) {
+ return
form_error_ip_format;
+ }
+ return false;
+};
+
+export const isPositive = (value) => {
+ if ((value || value === 0) && (value <= 0)) {
+ return
form_error_positive;
+ }
+ return false;
+};
+
+export const port = (value) => {
+ if ((value || value === 0) && (value < 80 || value > 65535)) {
+ return
form_error_port_range;
+ }
+ return false;
+};
+
+export const isSafePort = (value) => {
+ if (UNSAFE_PORTS.includes(value)) {
+ return
form_error_port_unsafe;
+ }
+ return false;
+};
+
+export const toNumber = value => value && parseInt(value, 10);
diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js
index 0630416d..eb7c7db2 100644
--- a/client/src/helpers/helpers.js
+++ b/client/src/helpers/helpers.js
@@ -3,8 +3,15 @@ import dateFormat from 'date-fns/format';
import subHours from 'date-fns/sub_hours';
import addHours from 'date-fns/add_hours';
import round from 'lodash/round';
+import axios from 'axios';
-import { STATS_NAMES, STANDARD_DNS_PORT, STANDARD_WEB_PORT } from './constants';
+import {
+ STATS_NAMES,
+ STANDARD_DNS_PORT,
+ STANDARD_WEB_PORT,
+ STANDARD_HTTPS_PORT,
+ CHECK_TIMEOUT,
+} from './constants';
export const formatTime = (time) => {
const parsedTime = dateParse(time);
@@ -140,3 +147,57 @@ export const getWebAddress = (ip, port = '') => {
return address;
};
+
+export const checkRedirect = (url, attempts) => {
+ let count = attempts || 1;
+
+ if (count > 10) {
+ window.location.replace(url);
+ return false;
+ }
+
+ const rmTimeout = t => t && clearTimeout(t);
+ const setRecursiveTimeout = (time, ...args) => setTimeout(
+ checkRedirect,
+ time,
+ ...args,
+ );
+
+ let timeout;
+
+ axios.get(url)
+ .then((response) => {
+ rmTimeout(timeout);
+ if (response) {
+ window.location.replace(url);
+ return;
+ }
+ timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, count += 1);
+ })
+ .catch((error) => {
+ rmTimeout(timeout);
+ if (error.response) {
+ window.location.replace(url);
+ return;
+ }
+ timeout = setRecursiveTimeout(CHECK_TIMEOUT, url, count += 1);
+ });
+
+ return false;
+};
+
+export const redirectToCurrentProtocol = (values, httpPort = 80) => {
+ const {
+ protocol, hostname, hash, port,
+ } = window.location;
+ const { enabled, port_https } = values;
+ const httpsPort = port_https !== STANDARD_HTTPS_PORT ? `:${port_https}` : '';
+
+ if (protocol !== 'https:' && enabled && port_https) {
+ checkRedirect(`https://${hostname}${httpsPort}/${hash}`);
+ } else if (protocol === 'https:' && enabled && port_https && port_https !== parseInt(port, 10)) {
+ checkRedirect(`https://${hostname}${httpsPort}/${hash}`);
+ } else if (protocol === 'https:' && (!enabled || !port_https)) {
+ window.location.replace(`http://${hostname}:${httpPort}/${hash}`);
+ }
+};
diff --git a/client/src/install/Setup/Greeting.js b/client/src/install/Setup/Greeting.js
index 914d89c8..88c43770 100644
--- a/client/src/install/Setup/Greeting.js
+++ b/client/src/install/Setup/Greeting.js
@@ -1,23 +1,19 @@
-import React, { Component } from 'react';
+import React from 'react';
import { Trans, withNamespaces } from 'react-i18next';
import Controls from './Controls';
-class Greeting extends Component {
- render() {
- return (
-
-
-
- install_welcome_title
-
-
- install_welcome_desc
-
-
-
-
- );
- }
-}
+const Greeting = () => (
+
+
+
+ install_welcome_title
+
+
+ install_welcome_desc
+
+
+
+
+);
export default withNamespaces()(Greeting);
diff --git a/client/src/install/Setup/Setup.css b/client/src/install/Setup/Setup.css
index c88e8b82..cf58bc3c 100644
--- a/client/src/install/Setup/Setup.css
+++ b/client/src/install/Setup/Setup.css
@@ -15,7 +15,7 @@
padding: 30px 20px;
line-height: 1.6;
background-color: #fff;
- box-shadow: 0 1px 4px rgba(74, 74, 74, .36);
+ box-shadow: 0 1px 4px rgba(74, 74, 74, 0.36);
border-radius: 3px;
}
@@ -92,7 +92,7 @@
line-height: 20px;
color: #fff;
text-align: center;
- box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
transition: width 0.6s ease;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
}
diff --git a/client/src/reducers/encryption.js b/client/src/reducers/encryption.js
new file mode 100644
index 00000000..3f51b217
--- /dev/null
+++ b/client/src/reducers/encryption.js
@@ -0,0 +1,81 @@
+import { handleActions } from 'redux-actions';
+
+import * as actions from '../actions/encryption';
+
+const encryption = handleActions({
+ [actions.getTlsStatusRequest]: state => ({ ...state, processing: true }),
+ [actions.getTlsStatusFailure]: state => ({ ...state, processing: false }),
+ [actions.getTlsStatusSuccess]: (state, { payload }) => {
+ const newState = {
+ ...state,
+ ...payload,
+ processing: false,
+ };
+ return newState;
+ },
+
+ [actions.setTlsConfigRequest]: state => ({ ...state, processingConfig: true }),
+ [actions.setTlsConfigFailure]: state => ({ ...state, processingConfig: false }),
+ [actions.setTlsConfigSuccess]: (state, { payload }) => {
+ const newState = {
+ ...state,
+ ...payload,
+ processingConfig: false,
+ };
+ return newState;
+ },
+
+ [actions.validateTlsConfigRequest]: state => ({ ...state, processingValidate: true }),
+ [actions.validateTlsConfigFailure]: state => ({ ...state, processingValidate: false }),
+ [actions.validateTlsConfigSuccess]: (state, { payload }) => {
+ const {
+ issuer = '',
+ key_type = '',
+ not_after = '',
+ not_before = '',
+ subject = '',
+ warning_validation = '',
+ dns_names = '',
+ ...values
+ } = payload;
+
+ const newState = {
+ ...state,
+ ...values,
+ issuer,
+ key_type,
+ not_after,
+ not_before,
+ subject,
+ warning_validation,
+ dns_names,
+ processingValidate: false,
+ };
+ return newState;
+ },
+}, {
+ processing: true,
+ processingConfig: false,
+ processingValidate: false,
+ enabled: false,
+ dns_names: null,
+ force_https: false,
+ issuer: '',
+ key_type: '',
+ not_after: '',
+ not_before: '',
+ port_dns_over_tls: '',
+ port_https: '',
+ subject: '',
+ valid_chain: false,
+ valid_key: false,
+ valid_cert: false,
+ status_cert: '',
+ status_key: '',
+ certificate_chain: '',
+ private_key: '',
+ server_name: '',
+ warning_validation: '',
+});
+
+export default encryption;
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 19bbbf63..e83f48a1 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -6,6 +6,7 @@ import versionCompare from '../helpers/versionCompare';
import * as actions from '../actions';
import toasts from './toasts';
+import encryption from './encryption';
const settings = handleActions({
[actions.initSettingsRequest]: state => ({ ...state, processing: true }),
@@ -52,6 +53,7 @@ const dashboard = handleActions({
upstream_dns: upstreamDns,
protection_enabled: protectionEnabled,
language,
+ http_port: httpPort,
} = payload;
const newState = {
...state,
@@ -64,6 +66,7 @@ const dashboard = handleActions({
upstreamDns: upstreamDns.join('\n'),
protectionEnabled,
language,
+ httpPort,
};
return newState;
},
@@ -117,13 +120,13 @@ const dashboard = handleActions({
if (versionCompare(currentVersion, payload.version) === -1) {
const {
- announcement,
+ version,
announcement_url: announcementUrl,
} = payload;
const newState = {
...state,
- announcement,
+ version,
announcementUrl,
isUpdateAvailable: true,
};
@@ -171,6 +174,7 @@ const dashboard = handleActions({
upstreamDns: [],
protectionEnabled: false,
processingProtection: false,
+ httpPort: 80,
});
const queryLogs = handleActions({
@@ -309,6 +313,7 @@ export default combineReducers({
filtering,
toasts,
dhcp,
+ encryption,
loadingBar: loadingBarReducer,
form: formReducer,
});
diff --git a/config.go b/config.go
index 0e680a1b..87e5c6a8 100644
--- a/config.go
+++ b/config.go
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"sync"
+ "time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
@@ -31,12 +32,13 @@ type configuration struct {
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
- BindHost string `yaml:"bind_host"`
- BindPort int `yaml:"bind_port"`
- AuthName string `yaml:"auth_name"`
- AuthPass string `yaml:"auth_pass"`
- Language string `yaml:"language"` // two-letter ISO 639-1 language code
+ BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
+ BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
+ AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
+ AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
+ Language string `yaml:"language"` // two-letter ISO 639-1 language code
DNS dnsConfig `yaml:"dns"`
+ TLS tlsConfig `yaml:"tls"`
Filters []filter `yaml:"filters"`
UserRules []string `yaml:"user_rules"`
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
@@ -60,6 +62,43 @@ type dnsConfig struct {
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
+type tlsConfigSettings struct {
+ Enabled bool `yaml:"enabled" json:"enabled"` // Enabled is the encryption (DOT/DOH/HTTPS) status
+ ServerName string `yaml:"server_name" json:"server_name,omitempty"` // ServerName is the hostname of your HTTPS/TLS server
+ ForceHTTPS bool `yaml:"force_https" json:"force_https,omitempty"` // ForceHTTPS: if true, forces HTTP->HTTPS redirect
+ PortHTTPS int `yaml:"port_https" json:"port_https,omitempty"` // HTTPS port. If 0, HTTPS will be disabled
+ PortDNSOverTLS int `yaml:"port_dns_over_tls" json:"port_dns_over_tls,omitempty"` // DNS-over-TLS port. If 0, DOT will be disabled
+
+ dnsforward.TLSConfig `yaml:",inline" json:",inline"`
+}
+
+// field ordering is not important -- these are for API and are recalculated on each run
+type tlsConfigStatus struct {
+ ValidCert bool `yaml:"-" json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
+ ValidChain bool `yaml:"-" json:"valid_chain"` // ValidChain is true if the specified certificates chain is verified and issued by a known CA
+ Subject string `yaml:"-" json:"subject,omitempty"` // Subject is the subject of the first certificate in the chain
+ Issuer string `yaml:"-" json:"issuer,omitempty"` // Issuer is the issuer of the first certificate in the chain
+ NotBefore time.Time `yaml:"-" json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
+ NotAfter time.Time `yaml:"-" json:"not_after,omitempty"` // NotAfter is the NotAfter field of the first certificate in the chain
+ DNSNames []string `yaml:"-" json:"dns_names"` // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
+
+ // key status
+ ValidKey bool `yaml:"-" json:"valid_key"` // ValidKey is true if the key is a valid private key
+ KeyType string `yaml:"-" json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
+
+ // is usable? set by validator
+ usable bool
+
+ // warnings
+ WarningValidation string `yaml:"-" json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
+}
+
+// field ordering is important -- yaml fields will mirror ordering from here
+type tlsConfig struct {
+ tlsConfigSettings `yaml:",inline" json:",inline"`
+ tlsConfigStatus `yaml:"-" json:",inline"`
+}
+
// initialize to default values, will be changed later when reading config or parsing command line
var config = configuration{
ourConfigFilename: "AdGuardHome.yaml",
@@ -79,6 +118,12 @@ var config = configuration{
},
UpstreamDNS: defaultDNS,
},
+ TLS: tlsConfig{
+ tlsConfigSettings: tlsConfigSettings{
+ PortHTTPS: 443,
+ PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
+ },
+ },
Filters: []filter{
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
diff --git a/control.go b/control.go
index 35373c2c..fd1759e3 100644
--- a/control.go
+++ b/control.go
@@ -3,12 +3,21 @@ package main
import (
"bytes"
"context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/base64"
"encoding/json"
+ "encoding/pem"
+ "errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
+ "reflect"
"sort"
"strconv"
"strings"
@@ -17,6 +26,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/hmage/golibs/log"
+ "github.com/joomcode/errorx"
"github.com/miekg/dns"
govalidator "gopkg.in/asaskevich/govalidator.v4"
)
@@ -68,9 +78,7 @@ func writeAllConfigsAndReloadDNS() error {
func httpUpdateConfigReloadDNSReturnOK(w http.ResponseWriter, r *http.Request) {
err := writeAllConfigsAndReloadDNS()
if err != nil {
- errorText := fmt.Sprintf("Couldn't write config file: %s", err)
- log.Println(errorText)
- http.Error(w, errorText, http.StatusInternalServerError)
+ httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err)
return
}
returnOK(w)
@@ -78,7 +86,8 @@ func httpUpdateConfigReloadDNSReturnOK(w http.ResponseWriter, r *http.Request) {
func handleStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
- "dns_address": config.BindHost,
+ "dns_address": config.DNS.BindHost,
+ "http_port": config.BindPort,
"dns_port": config.DNS.Port,
"protection_enabled": config.DNS.ProtectionEnabled,
"querylog_enabled": config.DNS.QueryLogEnabled,
@@ -400,7 +409,7 @@ func handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
func checkDNS(input string) error {
log.Printf("Checking if DNS %s works...", input)
- u, err := upstream.AddressToUpstream(input, "", dnsforward.DefaultTimeout)
+ u, err := upstream.AddressToUpstream(input, upstream.Options{Timeout: dnsforward.DefaultTimeout})
if err != nil {
return fmt.Errorf("failed to choose upstream for %s: %s", input, err)
}
@@ -900,17 +909,6 @@ type firstRunData struct {
func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data := firstRunData{}
- ifaces, err := getValidNetInterfaces()
- if err != nil {
- httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
- return
- }
- if len(ifaces) == 0 {
- httpError(w, http.StatusServiceUnavailable, "Couldn't find any legible interface, plase try again later")
- return
- }
-
- // fill out the fields
// find out if port 80 is available -- if not, fall back to 3000
if checkPortAvailable("", 80) == nil {
@@ -925,41 +923,15 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
data.DNS.Warning = "Port 53 is not available for binding -- this will make DNS clients unable to contact AdGuard Home."
}
+ ifaces, err := getValidNetInterfacesForWeb()
+ if err != nil {
+ httpError(w, http.StatusInternalServerError, "Couldn't get interfaces: %s", err)
+ return
+ }
+
data.Interfaces = make(map[string]interface{})
for _, iface := range ifaces {
- addrs, e := iface.Addrs()
- if e != nil {
- httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %s: %s", iface.Name, err)
- return
- }
-
- jsonIface := netInterface{
- Name: iface.Name,
- MTU: iface.MTU,
- HardwareAddr: iface.HardwareAddr.String(),
- }
-
- if iface.Flags != 0 {
- jsonIface.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
- httpError(w, http.StatusInternalServerError, "SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr)
- return
- }
- // ignore link-local
- if ipnet.IP.IsLinkLocalUnicast() {
- continue
- }
- jsonIface.Addresses = append(jsonIface.Addresses, ipnet.IP.String())
- }
- if len(jsonIface.Addresses) != 0 {
- data.Interfaces[iface.Name] = jsonIface
- }
+ data.Interfaces[iface.Name] = iface
}
w.Header().Set("Content-Type", "application/json")
@@ -974,7 +946,7 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
newSettings := firstRunData{}
err := json.NewDecoder(r.Body).Decode(&newSettings)
if err != nil {
- httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
+ httpError(w, http.StatusBadRequest, "Failed to parse new config json: %s", err)
return
}
@@ -1025,6 +997,312 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
}
}
+// ---
+// TLS
+// ---
+func handleTLSStatus(w http.ResponseWriter, r *http.Request) {
+ marshalTLS(w, config.TLS)
+}
+
+func handleTLSValidate(w http.ResponseWriter, r *http.Request) {
+ data, err := unmarshalTLS(r)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
+ return
+ }
+
+ // check if port is available
+ // BUT: if we are already using this port, no need
+ alreadyRunning := false
+ if httpsServer.server != nil {
+ alreadyRunning = true
+ }
+ if !alreadyRunning {
+ err = checkPortAvailable(config.BindHost, data.PortHTTPS)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
+ return
+ }
+ }
+
+ data = validateCertificates(data)
+ marshalTLS(w, data)
+}
+
+func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
+ data, err := unmarshalTLS(r)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
+ return
+ }
+
+ // check if port is available
+ // BUT: if we are already using this port, no need
+ alreadyRunning := false
+ if httpsServer.server != nil {
+ alreadyRunning = true
+ }
+ if !alreadyRunning {
+ err = checkPortAvailable(config.BindHost, data.PortHTTPS)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "port %d is not available, cannot enable HTTPS on it", data.PortHTTPS)
+ return
+ }
+ }
+
+ restartHTTPS := false
+ data = validateCertificates(data)
+ if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) {
+ log.Printf("tls config settings have changed, will restart HTTPS server")
+ restartHTTPS = true
+ }
+ config.TLS = data
+ err = writeAllConfigsAndReloadDNS()
+ if err != nil {
+ httpError(w, http.StatusInternalServerError, "Couldn't write config file: %s", err)
+ return
+ }
+ marshalTLS(w, data)
+ // this needs to be done in a goroutine because Shutdown() is a blocking call, and it will block
+ // until all requests are finished, and _we_ are inside a request right now, so it will block indefinitely
+ if restartHTTPS {
+ go func() {
+ time.Sleep(time.Second) // TODO: could not find a way to reliably know that data was fully sent to client by https server, so we wait a bit to let response through before closing the server
+ httpsServer.cond.L.Lock()
+ httpsServer.cond.Broadcast()
+ if httpsServer.server != nil {
+ httpsServer.server.Shutdown(context.TODO())
+ }
+ httpsServer.cond.L.Unlock()
+ }()
+ }
+}
+
+func validateCertificates(data tlsConfig) tlsConfig {
+ var err error
+
+ // clear out status for certificates
+ data.tlsConfigStatus = tlsConfigStatus{}
+
+ // check only public certificate separately from the key
+ if data.CertificateChain != "" {
+ log.Tracef("got certificate: %s", data.CertificateChain)
+
+ // now do a more extended validation
+ var certs []*pem.Block // PEM-encoded certificates
+ var skippedBytes []string // skipped bytes
+
+ pemblock := []byte(data.CertificateChain)
+ for {
+ var decoded *pem.Block
+ decoded, pemblock = pem.Decode(pemblock)
+ if decoded == nil {
+ break
+ }
+ if decoded.Type == "CERTIFICATE" {
+ certs = append(certs, decoded)
+ } else {
+ skippedBytes = append(skippedBytes, decoded.Type)
+ }
+ }
+
+ var parsedCerts []*x509.Certificate
+
+ for _, cert := range certs {
+ parsed, err := x509.ParseCertificate(cert.Bytes)
+ if err != nil {
+ data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err)
+ return data
+ }
+ parsedCerts = append(parsedCerts, parsed)
+ }
+
+ if len(parsedCerts) == 0 {
+ data.WarningValidation = fmt.Sprintf("You have specified an empty certificate")
+ return data
+ }
+
+ data.ValidCert = true
+
+ // spew.Dump(parsedCerts)
+
+ opts := x509.VerifyOptions{
+ DNSName: data.ServerName,
+ }
+
+ log.Printf("number of certs - %d", len(parsedCerts))
+ if len(parsedCerts) > 1 {
+ // set up an intermediate
+ pool := x509.NewCertPool()
+ for _, cert := range parsedCerts[1:] {
+ log.Printf("got an intermediate cert")
+ pool.AddCert(cert)
+ }
+ opts.Intermediates = pool
+ }
+
+ // TODO: save it as a warning rather than error it out -- shouldn't be a big problem
+ mainCert := parsedCerts[0]
+ _, err := mainCert.Verify(opts)
+ if err != nil {
+ // let self-signed certs through
+ data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err)
+ } else {
+ data.ValidChain = true
+ }
+ // spew.Dump(chains)
+
+ // update status
+ if mainCert != nil {
+ notAfter := mainCert.NotAfter
+ data.Subject = mainCert.Subject.String()
+ data.Issuer = mainCert.Issuer.String()
+ data.NotAfter = notAfter
+ data.NotBefore = mainCert.NotBefore
+ data.DNSNames = mainCert.DNSNames
+ }
+ }
+
+ // validate private key (right now the only validation possible is just parsing it)
+ if data.PrivateKey != "" {
+ // now do a more extended validation
+ var key *pem.Block // PEM-encoded certificates
+ var skippedBytes []string // skipped bytes
+
+ // go through all pem blocks, but take first valid pem block and drop the rest
+ pemblock := []byte(data.PrivateKey)
+ for {
+ var decoded *pem.Block
+ decoded, pemblock = pem.Decode(pemblock)
+ if decoded == nil {
+ break
+ }
+ if decoded.Type == "PRIVATE KEY" || strings.HasSuffix(decoded.Type, " PRIVATE KEY") {
+ key = decoded
+ break
+ } else {
+ skippedBytes = append(skippedBytes, decoded.Type)
+ }
+ }
+
+ if key == nil {
+ data.WarningValidation = "No valid keys were found"
+ return data
+ }
+
+ // parse the decoded key
+ _, keytype, err := parsePrivateKey(key.Bytes)
+ if err != nil {
+ data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err)
+ return data
+ }
+
+ data.ValidKey = true
+ data.KeyType = keytype
+ }
+
+ // if both are set, validate both in unison
+ if data.PrivateKey != "" && data.CertificateChain != "" {
+ _, err = tls.X509KeyPair([]byte(data.CertificateChain), []byte(data.PrivateKey))
+ if err != nil {
+ data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err)
+ return data
+ }
+ data.usable = true
+ }
+
+ return data
+}
+
+// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
+// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
+// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
+func parsePrivateKey(der []byte) (crypto.PrivateKey, string, error) {
+ if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
+ return key, "RSA", nil
+ }
+ if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
+ switch key := key.(type) {
+ case *rsa.PrivateKey:
+ return key, "RSA", nil
+ case *ecdsa.PrivateKey:
+ return key, "ECDSA", nil
+ default:
+ return nil, "", errors.New("tls: found unknown private key type in PKCS#8 wrapping")
+ }
+ }
+ if key, err := x509.ParseECPrivateKey(der); err == nil {
+ return key, "ECDSA", nil
+ }
+
+ return nil, "", errors.New("tls: failed to parse private key")
+}
+
+// unmarshalTLS handles base64-encoded certificates transparently
+func unmarshalTLS(r *http.Request) (tlsConfig, error) {
+ data := tlsConfig{}
+ err := json.NewDecoder(r.Body).Decode(&data)
+ if err != nil {
+ return data, errorx.Decorate(err, "Failed to parse new TLS config json")
+ }
+
+ if data.CertificateChain != "" {
+ certPEM, err := base64.StdEncoding.DecodeString(data.CertificateChain)
+ if err != nil {
+ return data, errorx.Decorate(err, "Failed to base64-decode certificate chain")
+ }
+ data.CertificateChain = string(certPEM)
+ }
+
+ if data.PrivateKey != "" {
+ keyPEM, err := base64.StdEncoding.DecodeString(data.PrivateKey)
+ if err != nil {
+ return data, errorx.Decorate(err, "Failed to base64-decode private key")
+ }
+
+ data.PrivateKey = string(keyPEM)
+ }
+
+ return data, nil
+}
+
+func marshalTLS(w http.ResponseWriter, data tlsConfig) {
+ w.Header().Set("Content-Type", "application/json")
+ if data.CertificateChain != "" {
+ encoded := base64.StdEncoding.EncodeToString([]byte(data.CertificateChain))
+ data.CertificateChain = encoded
+ }
+ if data.PrivateKey != "" {
+ encoded := base64.StdEncoding.EncodeToString([]byte(data.PrivateKey))
+ data.PrivateKey = encoded
+ }
+ err := json.NewEncoder(w).Encode(data)
+ if err != nil {
+ httpError(w, http.StatusInternalServerError, "Failed to marshal json with TLS status: %s", err)
+ return
+ }
+}
+
+// --------------
+// DNS-over-HTTPS
+// --------------
+func handleDOH(w http.ResponseWriter, r *http.Request) {
+ if r.TLS == nil {
+ httpError(w, http.StatusNotFound, "Not Found")
+ return
+ }
+
+ if !isRunning() {
+ httpError(w, http.StatusInternalServerError, "DNS server is not running")
+ return
+ }
+
+ dnsServer.ServeHTTP(w, r)
+}
+
+// ------------------------
+// registration of handlers
+// ------------------------
func registerInstallHandlers() {
http.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(handleInstallGetAddresses)))
http.HandleFunc("/control/install/configure", preInstall(ensurePOST(handleInstallConfigure)))
@@ -1068,4 +1346,10 @@ func registerControlHandlers() {
http.HandleFunc("/control/dhcp/interfaces", postInstall(optionalAuth(ensureGET(handleDHCPInterfaces))))
http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig))))
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
+
+ http.HandleFunc("/control/tls/status", postInstall(optionalAuth(ensureGET(handleTLSStatus))))
+ http.HandleFunc("/control/tls/configure", postInstall(optionalAuth(ensurePOST(handleTLSConfigure))))
+ http.HandleFunc("/control/tls/validate", postInstall(optionalAuth(ensurePOST(handleTLSValidate))))
+
+ http.HandleFunc("/dns-query", postInstall(handleDOH))
}
diff --git a/dns.go b/dns.go
index 3e800892..b7f0d130 100644
--- a/dns.go
+++ b/dns.go
@@ -51,8 +51,19 @@ func generateServerConfig() dnsforward.ServerConfig {
Filters: filters,
}
+ if config.TLS.Enabled {
+ newconfig.TLSConfig = config.TLS.TLSConfig
+ if config.TLS.PortDNSOverTLS != 0 {
+ newconfig.TLSListenAddr = &net.TCPAddr{IP: net.ParseIP(config.DNS.BindHost), Port: config.TLS.PortDNSOverTLS}
+ }
+ }
+
for _, u := range config.DNS.UpstreamDNS {
- dnsUpstream, err := upstream.AddressToUpstream(u, config.DNS.BootstrapDNS, dnsforward.DefaultTimeout)
+ opts := upstream.Options{
+ Timeout: dnsforward.DefaultTimeout,
+ Bootstrap: []string{config.DNS.BootstrapDNS},
+ }
+ dnsUpstream, err := upstream.AddressToUpstream(u, opts)
if err != nil {
log.Printf("Couldn't get upstream: %s", err)
// continue, just ignore the upstream
diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go
index e1006f83..99f09e6d 100644
--- a/dnsforward/dnsforward.go
+++ b/dnsforward/dnsforward.go
@@ -1,9 +1,11 @@
package dnsforward
import (
+ "crypto/tls"
"errors"
"fmt"
"net"
+ "net/http"
"strings"
"sync"
"time"
@@ -55,6 +57,7 @@ func NewServer(baseDir string) *Server {
}
// FilteringConfig represents the DNS filtering configuration of AdGuard Home
+// The zero FilteringConfig is empty and ready for use.
type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
@@ -68,6 +71,13 @@ type FilteringConfig struct {
dnsfilter.Config `yaml:",inline"`
}
+// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
+type TLSConfig struct {
+ TLSListenAddr *net.TCPAddr `yaml:"-" json:"-"`
+ CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` // PEM-encoded certificates chain
+ PrivateKey string `yaml:"private_key" json:"private_key"` // PEM-encoded private key
+}
+
// ServerConfig represents server configuration.
// The zero ServerConfig is empty and ready for use.
type ServerConfig struct {
@@ -77,6 +87,7 @@ type ServerConfig struct {
Filters []dnsfilter.Filter // A list of filters to use
FilteringConfig
+ TLSConfig
}
// if any of ServerConfig values are zero, then default values from below are used
@@ -91,7 +102,7 @@ func init() {
defaultUpstreams := make([]upstream.Upstream, 0)
for _, addr := range defaultDNS {
- u, err := upstream.AddressToUpstream(addr, "", DefaultTimeout)
+ u, err := upstream.AddressToUpstream(addr, upstream.Options{Timeout: DefaultTimeout})
if err == nil {
defaultUpstreams = append(defaultUpstreams, u)
}
@@ -154,6 +165,15 @@ func (s *Server) startInternal(config *ServerConfig) error {
Handler: s.handleDNSRequest,
}
+ if s.TLSListenAddr != nil && s.CertificateChain != "" && s.PrivateKey != "" {
+ proxyConfig.TLSListenAddr = s.TLSListenAddr
+ keypair, err := tls.X509KeyPair([]byte(s.CertificateChain), []byte(s.PrivateKey))
+ if err != nil {
+ return errorx.Decorate(err, "Failed to parse TLS keypair")
+ }
+ proxyConfig.TLSConfig = &tls.Config{Certificates: []tls.Certificate{keypair}}
+ }
+
if proxyConfig.UDPListenAddr == nil {
proxyConfig.UDPListenAddr = defaultValues.UDPListenAddr
}
@@ -240,24 +260,38 @@ func (s *Server) Reconfigure(config *ServerConfig) error {
return nil
}
+// ServeHTTP is a HTTP handler method we use to provide DNS-over-HTTPS
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ s.RLock()
+ s.dnsProxy.ServeHTTP(w, r)
+ s.RUnlock()
+}
+
// GetQueryLog returns a map with the current query log ready to be converted to a JSON
func (s *Server) GetQueryLog() []map[string]interface{} {
+ s.RLock()
+ defer s.RUnlock()
return s.queryLog.getQueryLog()
}
// GetStatsTop returns the current stop stats
func (s *Server) GetStatsTop() *StatsTop {
+ s.RLock()
+ defer s.RUnlock()
return s.queryLog.runningTop.getStatsTop()
}
// PurgeStats purges current server stats
func (s *Server) PurgeStats() {
- // TODO: Locks?
+ s.Lock()
+ defer s.Unlock()
s.stats.purgeStats()
}
// GetAggregatedStats returns aggregated stats data for the 24 hours
func (s *Server) GetAggregatedStats() map[string]interface{} {
+ s.RLock()
+ defer s.RUnlock()
return s.stats.getAggregatedStats()
}
@@ -267,6 +301,8 @@ func (s *Server) GetAggregatedStats() map[string]interface{} {
// end is end of the time range
// returns nil if time unit is not supported
func (s *Server) GetStatsHistory(timeUnit time.Duration, startTime time.Time, endTime time.Time) (map[string]interface{}, error) {
+ s.RLock()
+ defer s.RUnlock()
return s.stats.getStatsHistory(timeUnit, startTime, endTime)
}
@@ -350,9 +386,9 @@ func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Resu
switch result.Reason {
case dnsfilter.FilteredSafeBrowsing:
- return s.genBlockedHost(m, safeBrowsingBlockHost, d.Upstream)
+ return s.genBlockedHost(m, safeBrowsingBlockHost, d)
case dnsfilter.FilteredParental:
- return s.genBlockedHost(m, parentalBlockHost, d.Upstream)
+ return s.genBlockedHost(m, parentalBlockHost, d)
default:
if result.IP != nil {
return s.genARecord(m, result.IP)
@@ -381,22 +417,30 @@ func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
return &resp
}
-func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, upstream upstream.Upstream) *dns.Msg {
+func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSContext) *dns.Msg {
// look up the hostname, TODO: cache
replReq := dns.Msg{}
replReq.SetQuestion(dns.Fqdn(newAddr), request.Question[0].Qtype)
replReq.RecursionDesired = true
- reply, err := upstream.Exchange(&replReq)
+
+ newContext := &proxy.DNSContext{
+ Proto: d.Proto,
+ Addr: d.Addr,
+ StartTime: time.Now(),
+ Req: &replReq,
+ }
+
+ err := s.dnsProxy.Resolve(newContext)
if err != nil {
- log.Printf("Couldn't look up replacement host '%s' on upstream %s: %s", newAddr, upstream.Address(), err)
+ log.Printf("Couldn't look up replacement host '%s': %s", newAddr, err)
return s.genServerFailure(request)
}
resp := dns.Msg{}
resp.SetReply(request)
resp.Authoritative, resp.RecursionAvailable = true, true
- if reply != nil {
- for _, answer := range reply.Answer {
+ if newContext.Res != nil {
+ for _, answer := range newContext.Res.Answer {
answer.Header().Name = request.Question[0].Name
resp.Answer = append(resp.Answer, answer)
}
diff --git a/dnsforward/dnsforward_test.go b/dnsforward/dnsforward_test.go
index 990b646d..79543523 100644
--- a/dnsforward/dnsforward_test.go
+++ b/dnsforward/dnsforward_test.go
@@ -1,17 +1,34 @@
package dnsforward
import (
+ "crypto/ecdsa"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "math/big"
"net"
"os"
+ "sync"
"testing"
"time"
+ "github.com/AdguardTeam/dnsproxy/proxy"
+
"github.com/stretchr/testify/assert"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/miekg/dns"
)
+const (
+ tlsServerName = "testdns.adguard.com"
+ dataDir = "testData"
+ testMessagesCount = 10
+)
+
func TestServer(t *testing.T) {
s := createTestServer(t)
defer removeDataDir(t)
@@ -22,7 +39,7 @@ func TestServer(t *testing.T) {
// message over UDP
req := createGoogleATestMessage()
- addr := s.dnsProxy.Addr("udp")
+ addr := s.dnsProxy.Addr(proxy.ProtoUDP)
client := dns.Client{Net: "udp"}
reply, _, err := client.Exchange(req, addr.String())
if err != nil {
@@ -63,6 +80,69 @@ func TestServer(t *testing.T) {
}
}
+func TestDotServer(t *testing.T) {
+ // Prepare the proxy server
+ _, certPem, keyPem := createServerTLSConfig(t)
+ s := createTestServer(t)
+ defer removeDataDir(t)
+
+ s.TLSConfig = TLSConfig{
+ TLSListenAddr: &net.TCPAddr{Port: 0},
+ CertificateChain: string(certPem),
+ PrivateKey: string(keyPem),
+ }
+
+ // Starting the server
+ err := s.Start(nil)
+ if err != nil {
+ t.Fatalf("Failed to start server: %s", err)
+ }
+
+ // Add our self-signed generated config to roots
+ roots := x509.NewCertPool()
+ roots.AppendCertsFromPEM(certPem)
+ tlsConfig := &tls.Config{ServerName: tlsServerName, RootCAs: roots}
+
+ // Create a DNS-over-TLS client connection
+ addr := s.dnsProxy.Addr(proxy.ProtoTLS)
+ conn, err := dns.DialWithTLS("tcp-tls", addr.String(), tlsConfig)
+ if err != nil {
+ t.Fatalf("cannot connect to the proxy: %s", err)
+ }
+
+ sendTestMessages(t, conn)
+
+ // Stop the proxy
+ err = s.Stop()
+ if err != nil {
+ t.Fatalf("DNS server failed to stop: %s", err)
+ }
+}
+
+func TestServerRace(t *testing.T) {
+ s := createTestServer(t)
+ defer removeDataDir(t)
+ err := s.Start(nil)
+ if err != nil {
+ t.Fatalf("Failed to start server: %s", err)
+ }
+
+ // message over UDP
+ addr := s.dnsProxy.Addr(proxy.ProtoUDP)
+ conn, err := dns.Dial("udp", addr.String())
+ if err != nil {
+ t.Fatalf("cannot connect to the proxy: %s", err)
+ }
+
+ sendTestMessagesAsync(t, conn)
+
+ // Stop the proxy
+ err = s.Stop()
+ if err != nil {
+ t.Fatalf("DNS server failed to stop: %s", err)
+ }
+}
+
func TestSafeSearch(t *testing.T) {
s := createTestServer(t)
s.SafeSearchEnabled = true
@@ -141,7 +221,7 @@ func TestInvalidRequest(t *testing.T) {
}
// server is running, send a message
- addr := s.dnsProxy.Addr("udp")
+ addr := s.dnsProxy.Addr(proxy.ProtoUDP)
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
@@ -175,7 +255,7 @@ func TestBlockedRequest(t *testing.T) {
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
- addr := s.dnsProxy.Addr("udp")
+ addr := s.dnsProxy.Addr(proxy.ProtoUDP)
//
// NXDomain blocking
@@ -216,7 +296,7 @@ func TestBlockedByHosts(t *testing.T) {
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
- addr := s.dnsProxy.Addr("udp")
+ addr := s.dnsProxy.Addr(proxy.ProtoUDP)
//
// Hosts blocking
@@ -264,7 +344,7 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
- addr := s.dnsProxy.Addr("udp")
+ addr := s.dnsProxy.Addr(proxy.ProtoUDP)
//
// Safebrowsing blocking
@@ -320,6 +400,7 @@ func createTestServer(t *testing.T) *Server {
s := NewServer(createDataDir(t))
s.UDPListenAddr = &net.UDPAddr{Port: 0}
s.TCPListenAddr = &net.TCPAddr{Port: 0}
+
s.QueryLogEnabled = true
s.FilteringConfig.FilteringEnabled = true
s.FilteringConfig.ProtectionEnabled = true
@@ -335,20 +416,111 @@ func createTestServer(t *testing.T) *Server {
return s
}
-func createDataDir(t *testing.T) string {
- dir := "testData"
- err := os.MkdirAll(dir, 0755)
+func createServerTLSConfig(t *testing.T) (*tls.Config, []byte, []byte) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
- t.Fatalf("Cannot create %s: %s", dir, err)
+ t.Fatalf("cannot generate RSA key: %s", err)
}
- return dir
+
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ t.Fatalf("failed to generate serial number: %s", err)
+ }
+
+ notBefore := time.Now()
+ notAfter := notBefore.Add(5 * 365 * time.Hour * 24)
+
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{
+ Organization: []string{"AdGuard Tests"},
+ },
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+ template.DNSNames = append(template.DNSNames, tlsServerName)
+
+ derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(privateKey), privateKey)
+ if err != nil {
+ t.Fatalf("failed to create certificate: %s", err)
+ }
+
+ certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
+ keyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
+
+ cert, err := tls.X509KeyPair(certPem, keyPem)
+ if err != nil {
+ t.Fatalf("failed to create certificate: %s", err)
+ }
+
+ return &tls.Config{Certificates: []tls.Certificate{cert}, ServerName: tlsServerName}, certPem, keyPem
+}
+
+func createDataDir(t *testing.T) string {
+ err := os.MkdirAll(dataDir, 0755)
+ if err != nil {
+ t.Fatalf("Cannot create %s: %s", dataDir, err)
+ }
+ return dataDir
}
func removeDataDir(t *testing.T) {
- dir := "testData"
- err := os.RemoveAll(dir)
+ err := os.RemoveAll(dataDir)
if err != nil {
- t.Fatalf("Cannot remove %s: %s", dir, err)
+ t.Fatalf("Cannot remove %s: %s", dataDir, err)
+ }
+}
+
+func sendTestMessageAsync(t *testing.T, conn *dns.Conn, g *sync.WaitGroup) {
+ defer func() {
+ g.Done()
+ }()
+
+ req := createTestMessage()
+ err := conn.WriteMsg(req)
+ if err != nil {
+ t.Fatalf("cannot write message: %s", err)
+ }
+
+ res, err := conn.ReadMsg()
+ if err != nil {
+ t.Fatalf("cannot read response to message: %s", err)
+ }
+ assertResponse(t, res)
+}
+
+// sendTestMessagesAsync sends messages in parallel
+// so that we could find race issues
+func sendTestMessagesAsync(t *testing.T, conn *dns.Conn) {
+ g := &sync.WaitGroup{}
+ g.Add(testMessagesCount)
+
+ for i := 0; i < testMessagesCount; i++ {
+ go sendTestMessageAsync(t, conn, g)
+ }
+
+ g.Wait()
+}
+
+func sendTestMessages(t *testing.T, conn *dns.Conn) {
+ for i := 0; i < 10; i++ {
+ req := createTestMessage()
+ err := conn.WriteMsg(req)
+ if err != nil {
+ t.Fatalf("cannot write message #%d: %s", i, err)
+ }
+
+ res, err := conn.ReadMsg()
+ if err != nil {
+ t.Fatalf("cannot read response to message #%d: %s", i, err)
+ }
+ assertResponse(t, res)
}
}
@@ -391,3 +563,14 @@ func assertResponse(t *testing.T, reply *dns.Msg, ip string) {
t.Fatalf("DNS server returned wrong answer type instead of A: %v", reply.Answer[0])
}
}
+
+func publicKey(priv interface{}) interface{} {
+ switch k := priv.(type) {
+ case *rsa.PrivateKey:
+ return &k.PublicKey
+ case *ecdsa.PrivateKey:
+ return &k.PublicKey
+ default:
+ return nil
+ }
+}
diff --git a/filter.go b/filter.go
index dcdd40be..84db3a47 100644
--- a/filter.go
+++ b/filter.go
@@ -166,7 +166,7 @@ func (filter *filter) update(force bool) (bool, error) {
return false, nil
}
- log.Printf("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 := client.Get(filter.URL)
if resp != nil && resp.Body != nil {
@@ -203,7 +203,7 @@ func (filter *filter) update(force bool) (bool, error) {
// Check if the filter has been really changed
if reflect.DeepEqual(filter.Rules, rules) {
- log.Printf("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL)
+ log.Tracef("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL)
return false, nil
}
diff --git a/go.mod b/go.mod
index 68a55326..cba63387 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,13 @@
module github.com/AdguardTeam/AdGuardHome
require (
- github.com/AdguardTeam/dnsproxy v0.9.10
+ github.com/AdguardTeam/dnsproxy v0.11.1
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-test/deep v1.0.1
github.com/gobuffalo/packr v1.19.0
- github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4
+ github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24
github.com/joomcode/errorx v0.1.0
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b
@@ -17,8 +17,8 @@ require (
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
github.com/stretchr/testify v1.2.2
go.uber.org/goleak v0.10.0
- golang.org/x/net v0.0.0-20181220203305-927f97764cc3
- golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb
+ golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
+ golang.org/x/sys v0.0.0-20190122071731-054c452bb702
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477
gopkg.in/yaml.v2 v2.2.1
)
diff --git a/go.sum b/go.sum
index 29de0319..5ba6426d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,13 @@
-github.com/AdguardTeam/dnsproxy v0.9.10 h1:q364WlTvC+CS8kJbMy7TCyt4Niqixxw584MQJtCGhJU=
-github.com/AdguardTeam/dnsproxy v0.9.10/go.mod h1:IqBhopgNpzB168kMurbjXf86dn50geasBIuGVxY63j0=
+github.com/AdguardTeam/dnsproxy v0.11.1 h1:qO5VH0GYF9vdksQRG8frEfJ+CJjsPBwuct8FH6Mij7o=
+github.com/AdguardTeam/dnsproxy v0.11.1/go.mod h1:lEi2srAWwfSQWoy8GeZR6lwS+FSMoiZid8bQPreOLb0=
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY=
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
-github.com/ameshkov/dnscrypt v1.0.4 h1:vtwHm5m4R2dhcCx23wiI+gNBoy7qm4h7+kZ4Pucw/vE=
-github.com/ameshkov/dnscrypt v1.0.4/go.mod h1:hVW52S6r0QvUpIwsyfZ1ifYYpfGu5pewD3pl7afMJcQ=
+github.com/ameshkov/dnscrypt v1.0.6 h1:55wfnNF8c4E3JXDNlwPl2Pbs7UPPIh+kI6KK3THqYS0=
+github.com/ameshkov/dnscrypt v1.0.6/go.mod h1:ZvT9LaNaJfDNXKIbkYFf24HUgHuQR6MNT6nwVvN4jMQ=
github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
@@ -26,13 +26,11 @@ github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264 h1:roWyi0eEdiFreSq
github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
-github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4 h1:FMAReGTEDNr4AdbScv/PqzjMQUpkkVHiF/t8sDHQQVQ=
-github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4/go.mod h1:H6Ev6svFxUVPFThxLtdnFfcE9e3GWufpfmcVFpqV6HM=
+github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24 h1:yyDtaSMcAZdm1I6uL8YLghpWiJljfBHs8NC/P86PYQk=
+github.com/hmage/golibs v0.0.0-20190121112702-20153bd03c24/go.mod h1:H6Ev6svFxUVPFThxLtdnFfcE9e3GWufpfmcVFpqV6HM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff h1:6NvhExg4omUC9NfA+l4Oq3ibNNeJUdiAF3iBVB0PlDk=
-github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff/go.mod h1:ddfPX8Z28YMjiqoaJhNBzWHapTHXejnB5cDCUWDwriw=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joomcode/errorx v0.1.0 h1:QmJMiI1DE1UFje2aI1ZWO/VMT5a32qBoXUclGOt8vsc=
@@ -67,21 +65,21 @@ go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190122013713-64072686203f h1:u1CmMhe3a44hy8VIgpInORnI01UVaUYheqR7x9BxT3c=
+golang.org/x/crypto v0.0.0-20190122013713-64072686203f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6 h1:gT0Y6H7hbVPUtvtk0YGxMXPgN+p8fYlqWkgJeUCZcaQ=
-golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q=
+golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM=
-golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb h1:pf3XwC90UUdNPYWZdFjhGBE7DUFuK3Ct1zWmZ65QN30=
-golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190122071731-054c452bb702 h1:Lk4tbZFnlyPgV+sLgTw5yGfzrlOn9kx4vSombi2FFlY=
+golang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477 h1:5xUJw+lg4zao9W4HIDzlFbMYgSgtvNVHh00MEHvbGpQ=
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477/go.mod h1:QDV1vrFSrowdoOba0UM8VJPUZONT7dnfdLsM+GG53Z8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/helpers.go b/helpers.go
index a0cf1fd7..8e884d36 100644
--- a/helpers.go
+++ b/helpers.go
@@ -8,12 +8,15 @@ import (
"io/ioutil"
"net"
"net/http"
+ "net/url"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
+
+ "github.com/joomcode/errorx"
)
// ----------------------------------
@@ -137,12 +140,32 @@ func preInstallHandler(handler http.Handler) http.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.") {
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 && 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)
}
}
@@ -216,6 +239,56 @@ func getValidNetInterfaces() ([]net.Interface, error) {
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)))
diff --git a/helpers_test.go b/helpers_test.go
new file mode 100644
index 00000000..66fc3d27
--- /dev/null
+++ b/helpers_test.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ "testing"
+
+ "github.com/hmage/golibs/log"
+)
+
+func TestGetValidNetInterfacesForWeb(t *testing.T) {
+ ifaces, err := getValidNetInterfacesForWeb()
+ if err != nil {
+ t.Fatalf("Cannot get net interfaces: %s", err)
+ }
+ if len(ifaces) == 0 {
+ t.Fatalf("No net interfaces found")
+ }
+
+ for _, iface := range ifaces {
+ if len(iface.Addresses) == 0 {
+ t.Fatalf("No addresses found for %s", iface.Name)
+ }
+
+ log.Printf("%v", iface)
+ }
+}
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index c9801b4b..f1e23f86 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -2,7 +2,7 @@ swagger: '2.0'
info:
title: 'AdGuard Home'
description: 'AdGuard Home REST API. Admin web interface is built on top of this REST API.'
- version: 0.92.0
+ version: 0.93.0
schemes:
- http
basePath: /control
@@ -12,6 +12,9 @@ tags:
-
name: global
description: 'AdGuard Home server general settings and controls'
+ -
+ name: tls
+ description: 'AdGuard Home HTTPS/DOH/DOT settings'
-
name: log
description: 'AdGuard Home query log'
@@ -36,6 +39,9 @@ tags:
-
name: dhcp
description: 'Built-in DHCP server controls'
+ -
+ name: install
+ description: 'First-time install configuration handlers'
paths:
# API TO-DO LIST
@@ -267,6 +273,70 @@ paths:
200:
description: OK
+ # --------------------------------------------------
+ # TLS server methods
+ # --------------------------------------------------
+
+ /tls/status:
+ get:
+ tags:
+ - tls
+ operationId: tlsStatus
+ summary: "Returns TLS configuration and its status"
+ responses:
+ 200:
+ description: OK
+ schema:
+ $ref: "#/definitions/TlsConfig"
+
+ /tls/configure:
+ post:
+ tags:
+ - tls
+ operationId: tlsConfigure
+ summary: "Updates current TLS configuration"
+ consumes:
+ - application/json
+ parameters:
+ - in: "body"
+ name: "body"
+ description: "TLS configuration JSON"
+ required: true
+ schema:
+ $ref: "#/definitions/TlsConfig"
+ responses:
+ 200:
+ description: "TLS configuration and its status"
+ schema:
+ $ref: "#/definitions/TlsConfig"
+ 400:
+ description: "Invalid configuration or unavailable port"
+ 500:
+ description: "Error occurred while applying configuration"
+
+ /tls/validate:
+ post:
+ tags:
+ - tls
+ operationId: tlsValidate
+ summary: "Checks if the current TLS configuration is valid"
+ consumes:
+ - application/json
+ parameters:
+ - in: "body"
+ name: "body"
+ description: "TLS configuration JSON"
+ required: true
+ schema:
+ $ref: "#/definitions/TlsConfig"
+ responses:
+ 200:
+ description: "TLS configuration and its status"
+ schema:
+ $ref: "#/definitions/TlsConfig"
+ 400:
+ description: "Invalid configuration or unavailable port"
+
# --------------------------------------------------
# DHCP server methods
# --------------------------------------------------
@@ -646,6 +716,42 @@ paths:
text/plain:
en
+ # --------------------------------------------------
+ # First-time install configuration methods
+ # --------------------------------------------------
+
+ /install/get_addresses:
+ get:
+ tags:
+ - install
+ operationId: installGetAddresses
+ summary: "Gets the network interfaces information."
+ responses:
+ 200:
+ description: OK
+ schema:
+ $ref: "#/definitions/AddressesInfo"
+ /install/configure:
+ post:
+ tags:
+ - install
+ operationId: installConfigure
+ summary: "Applies the initial configuration."
+ parameters:
+ - in: "body"
+ name: "body"
+ description: "Initial configuration JSON"
+ required: true
+ schema:
+ $ref: "#/definitions/InitialConfiguration"
+ responses:
+ 200:
+ description: OK
+ 400:
+ description: "Failed to parse initial configuration or cannot listen to the specified addresses"
+ 500:
+ description: "Cannot start the DNS server"
+
definitions:
ServerStatus:
type: "object"
@@ -1063,4 +1169,147 @@ definitions:
type: "array"
description: "Query log"
items:
- $ref: "#/definitions/QueryLogItem"
\ No newline at end of file
+ $ref: "#/definitions/QueryLogItem"
+ TlsConfig:
+ type: "object"
+ description: "TLS configuration settings and status"
+ properties:
+ # TLS configuration
+ enabled:
+ type: "boolean"
+ example: "true"
+ description: "enabled is the encryption (DOT/DOH/HTTPS) status"
+ server_name:
+ type: "string"
+ example: "example.org"
+ description: "server_name is the hostname of your HTTPS/TLS server"
+ force_https:
+ type: "boolean"
+ example: "true"
+ description: "if true, forces HTTP->HTTPS redirect"
+ port_https:
+ type: "integer"
+ format: "int32"
+ example: 443
+ description: "HTTPS port. If 0, HTTPS will be disabled."
+ port_dns_over_tls:
+ type: "integer"
+ format: "int32"
+ example: 853
+ description: "DNS-over-TLS port. If 0, DOT will be disabled."
+ certificate_chain:
+ type: "string"
+ description: "Base64 string with PEM-encoded certificates chain"
+ private_key:
+ type: "string"
+ description: "Base64 string with PEM-encoded private key"
+ # Below goes validation fields
+ valid_cert:
+ type: "boolean"
+ example: "true"
+ description: "valid_cert is true if the specified certificates chain is a valid chain of X509 certificates"
+ valid_chain:
+ type: "boolean"
+ example: "true"
+ description: "valid_chain is true if the specified certificates chain is verified and issued by a known CA"
+ subject:
+ type: "string"
+ example: "CN=example.org"
+ description: "subject is the subject of the first certificate in the chain"
+ issuer:
+ type: "string"
+ example: "CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US"
+ description: "issuer is the issuer of the first certificate in the chain"
+ not_before:
+ type: "string"
+ example: "2019-01-31T10:47:32Z"
+ description: "not_before is the NotBefore field of the first certificate in the chain"
+ not_after:
+ type: "string"
+ example: "2019-05-01T10:47:32Z"
+ description: "not_after is the NotAfter field of the first certificate in the chain"
+ dns_names:
+ type: "array"
+ items:
+ type: "string"
+ description: "dns_names is the value of SubjectAltNames field of the first certificate in the chain"
+ example:
+ - "*.example.org"
+ valid_key:
+ type: "boolean"
+ example: "true"
+ description: "valid_key is true if the key is a valid private key"
+ key_type:
+ type: "string"
+ example: "RSA"
+ description: "key_type is either RSA or ECDSA"
+ warning_validation:
+ type: "string"
+ example: "You have specified an empty certificate"
+ description: "warning_validation is a validation warning message with the issue description"
+ NetInterface:
+ type: "object"
+ description: "Network interface info"
+ properties:
+ flags:
+ type: "string"
+ example: "up|broadcast|multicast"
+ hardware_address:
+ type: "string"
+ example: "52:54:00:11:09:ba"
+ mtu:
+ type: "integer"
+ format: "int32"
+ example: 1500
+ name:
+ type: "string"
+ example: "eth0"
+ ip_addresses:
+ type: "array"
+ items:
+ type: "string"
+ example:
+ - "127.0.0.1"
+ AddressInfo:
+ type: "object"
+ description: "Port information"
+ properties:
+ ip:
+ type: "string"
+ example: "127.0.01"
+ port:
+ type: "integer"
+ format: "int32"
+ example: 53
+ warning:
+ type: "string"
+ example: "Cannot bind to this port"
+ AddressesInfo:
+ type: "object"
+ description: "AdGuard Home addresses configuration"
+ properties:
+ dns:
+ $ref: "#/definitions/AddressInfo"
+ web:
+ $ref: "#/definitions/AddressInfo"
+ interfaces:
+ type: "object"
+ description: "Network interfaces dictionary (key is the interface name)"
+ additionalProperties:
+ $ref: "#/definitions/NetInterface"
+ InitialConfiguration:
+ type: "object"
+ description: "AdGuard Home initial configuration (for the first-install wizard)"
+ properties:
+ dns:
+ $ref: "#/definitions/AddressInfo"
+ web:
+ $ref: "#/definitions/AddressInfo"
+ username:
+ type: "string"
+ description: "Basic auth username"
+ example: "admin"
+ password:
+ type: "string"
+ description: "Basic auth password"
+ example: "password"
\ No newline at end of file
diff --git a/release.sh b/release.sh
index 94cda10b..3ee8974a 100755
--- a/release.sh
+++ b/release.sh
@@ -9,20 +9,23 @@ version=`git describe --abbrev=4 --dirty --always --tags`
f() {
make cleanfast; CGO_DISABLED=1 make
if [[ $GOOS == darwin ]]; then
+ rm -f dist/AdGuardHome_"$version"_MacOS.zip
zip dist/AdGuardHome_"$version"_MacOS.zip AdGuardHome README.md LICENSE.txt
elif [[ $GOOS == windows ]]; then
+ rm -f dist/AdGuardHome_"$version"_Windows.zip
zip dist/AdGuardHome_"$version"_Windows.zip AdGuardHome.exe README.md LICENSE.txt
else
- tar zcvf dist/AdGuardHome_"$version"_"$GOOS"_"$GOARCH".tar.gz AdGuardHome README.md LICENSE.txt
+ rm -rf dist/AdguardHome
+ mkdir -p dist/AdGuardHome
+ cp -pv {AdGuardHome,LICENSE.txt,README.md} dist/AdGuardHome/
+ pushd dist
+ tar zcvf AdGuardHome_"$version"_"$GOOS"_"$GOARCH".tar.gz AdGuardHome/{AdGuardHome,LICENSE.txt,README.md}
+ popd
+ rm -rf dist/AdguardHome
fi
}
-# Clean and rebuild both static and binary
-make clean
-make
-
# Prepare the dist folder
-rm -rf dist
mkdir -p dist
# Prepare releases