Merge branch 'master' of github.com:AdguardTeam/AdGuardHome

 Conflicts:
	openapi/openapi.yaml
This commit is contained in:
Jonas Marklén 2020-05-02 10:09:25 +02:00
commit efd2b31104
No known key found for this signature in database
GPG Key ID: F19F3AA368016FEE
54 changed files with 2009 additions and 747 deletions

6
.gitignore vendored
View File

@ -17,3 +17,9 @@ coverage.txt
# Test output
dnsfilter/tests/top-1m.csv
dnsfilter/tests/dnsfilter.TestLotsOfRules*.pprof
# Snapcraft build temporary files
*.snap
launchpad_credentials
snapcraft_login
snapcraft.yaml.bak

View File

@ -13,6 +13,7 @@ run:
skip-files:
- ".*generated.*"
- dnsfilter/rule_to_regexp.go
- util/pprof.go
- ".*_test.go"
@ -65,5 +66,7 @@ issues:
- Error return value of ..*.Shutdown.
# goconst
- string .forcesafesearch.google.com. has 3 occurrences
# gosec: Profiling endpoint is automatically exposed on /debug/pprof
- G108
# gosec: Subprocess launched with function call as argument or cmd arguments
- G204
- G204

View File

@ -86,7 +86,7 @@ matrix:
- node -v
- npm -v
# Prepare releases
- ./release.sh
- ./build_release.sh
- ls -l dist
deploy:
@ -118,4 +118,4 @@ matrix:
- docker login -u="$DOCKER_USER" -p="$DOCKER_PASSWORD"
- ./build_docker.sh
after_script:
- docker images
- docker images

View File

@ -882,6 +882,9 @@ Response:
200 OK
{
"upstream_dns": ["tls://...", ...],
"bootstrap_dns": ["1.2.3.4", ...],
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
@ -890,6 +893,8 @@ Response:
"edns_cs_enabled": true | false,
"dnssec_enabled": true | false
"disable_ipv6": true | false,
"fastest_addr": true | false, // use Fastest Address algorithm
"parallel_requests": true | false, // send DNS requests to all upstream servers at once
}
@ -900,6 +905,9 @@ Request:
POST /control/dns_config
{
"upstream_dns": ["tls://...", ...],
"bootstrap_dns": ["1.2.3.4", ...],
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
@ -908,6 +916,8 @@ Request:
"edns_cs_enabled": true | false,
"dnssec_enabled": true | false
"disable_ipv6": true | false,
"fastest_addr": true | false, // use Fastest Address algorithm
"parallel_requests": true | false, // send DNS requests to all upstream servers at once
}
Response:

View File

@ -5,6 +5,9 @@ GOPATH := $(shell go env GOPATH)
JSFILES = $(shell find client -path client/node_modules -prune -o -type f -name '*.js')
STATIC = build/static/index.html
CHANNEL ?= release
DOCKER_IMAGE_DEV_NAME=adguardhome-dev
DOCKERFILE=packaging/docker/Dockerfile
DOCKERFILE_HUB=packaging/docker/Dockerfile.travis
TARGET=AdGuardHome
@ -26,6 +29,11 @@ $(TARGET): $(STATIC) *.go home/*.go dhcpd/*.go dnsfilter/*.go dnsforward/*.go
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(GIT_VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)"
PATH=$(GOPATH)/bin:$(PATH) packr clean
docker:
docker build -t "$(DOCKER_IMAGE_DEV_NAME)" -f "$(DOCKERFILE)" .
@echo Now you can run the docker image:
@echo docker run --name "$(DOCKER_IMAGE_DEV_NAME)" -p 53:53/tcp -p 53:53/udp -p 3000:3000/tcp $(DOCKER_IMAGE_DEV_NAME)
clean:
$(MAKE) cleanfast
rm -rf build

View File

@ -26,9 +26,19 @@
<a href="https://golangci.com/r/github.com/AdguardTeam/AdGuardHome">
<img src="https://golangci.com/badges/github.com/AdguardTeam/AdGuardHome.svg" alt="GolangCI" />
</a>
<br />
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release" />
</a>
<a href="https://snapcraft.io/adguard-home">
<img alt="adguard-home" src="https://snapcraft.io/adguard-home/badge.svg" />
</a>
<a href="https://hub.docker.com/r/adguard/adguardhome">
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/adguard/adguardhome.svg?maxAge=604800" />
</a>
<a href="https://hub.docker.com/r/adguard/adguardhome">
<img alt="Docker Stars" src="https://img.shields.io/docker/stars/adguard/adguardhome.svg?maxAge=604800" />
</a>
</p>
<br />
@ -59,7 +69,9 @@ It operates as a DNS server that re-routes tracking domains to a "black hole," t
<a id="getting-started"></a>
## Getting Started
Please read the [Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started) article on our Wiki to learn how to install AdGuard Home, and how to configure your devices to use it.
Please read the **[Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started)** article on our Wiki to learn how to install AdGuard Home, and how to configure your devices to use it.
If you're running **Linux**, there's a secure and easy way to install AdGuard Home - you can get it from the [Snap Store](https://snapcraft.io/adguard-home).
Alternatively, you can use our [official Docker image](https://hub.docker.com/r/adguard/adguardhome).
@ -69,6 +81,7 @@ Alternatively, you can use our [official Docker image](https://hub.docker.com/r/
* [AdGuard Home as a DNS-over-HTTPS or DNS-over-TLS server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption)
* [How to install and run AdGuard Home on Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi)
* [How to install and run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS)
* [How to write your own hosts blocklists properly](https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists)
### API
@ -165,11 +178,12 @@ You are welcome to fork this repository, make your changes and submit a pull req
<a id="test-unstable-versions"></a>
### Test unstable versions
There are two options how you can install an unstable version.
You can either install a beta version of AdGuard Home which we update periodically,
or you can use the Docker image from the `edge` tag, which is synced with the repo master branch.
There are three options how you can install an unstable version.
1. You can either install a beta version of AdGuard Home which we update periodically.
2. You can use the Docker image from the `edge` tag, which is synced with the repo master branch.
3. You can install AdGuard Home from `beta` or `edge` channels on the Snap Store.
* [Docker Hub](https://hub.docker.com/r/adguard/adguardhome)
* Beta builds
* [Raspberry Pi (32-bit ARMv6)](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm.tar.gz)
* [MacOS](https://static.adguard.com/adguardhome/beta/AdGuardHome_MacOS.zip)
@ -182,6 +196,8 @@ or you can use the Docker image from the `edge` tag, which is synced with the re
* [Linux 32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz)
* [MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips.tar.gz)
* [MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle.tar.gz)
* [Docker Hub](https://hub.docker.com/r/adguard/adguardhome)
* [Snap Store](https://snapcraft.io/adguard-home)
<a id="reporting-issues"></a>
### Report issues

View File

@ -4,7 +4,7 @@ set -eE
set -o pipefail
set -x
DOCKERFILE="Dockerfile.travis"
DOCKERFILE="packaging/docker/Dockerfile.hub"
IMAGE_NAME="adguard/adguardhome"
if [[ "${TRAVIS_BRANCH}" == "master" ]]

282
build_snap.sh Executable file
View File

@ -0,0 +1,282 @@
#!/usr/bin/env bash
set -eE
set -o pipefail
set -x
BUILDER_IMAGE="adguard/snapcraft:1.0"
SNAPCRAFT_TMPL="packaging/snap/snapcraft.yaml"
SNAP_NAME="adguard-home"
LAUNCHPAD_CREDENTIALS_DIR=".local/share/snapcraft/provider/launchpad"
if [[ -z ${VERSION} ]]; then
VERSION=`git describe --abbrev=4 --dirty --always --tags`
echo "VERSION env variable is not set, getting it from git: ${VERSION}"
fi
# If bash is interactive, set `-it` parameter for docker run
INTERACTIVE=""
if [ -t 0 ] ; then
INTERACTIVE="-it"
fi
function usage() {
cat <<EOF
Usage: ${0##*/} command [options]
Please note that in order for the builds to work properly, you need to setup some env variables.
These are necessary for "remote-build' command.
Read this doc on how to generate them: https://uci.readthedocs.io/en/latest/oauth.html
* LAUNCHPAD_KEY -- launchpad CI key
* LAUNCHPAD_ACCESS_TOKEN -- launchpad access token
* LAUNCHPAD_ACCESS_SECRET -- launchpad access secret
These are necessary for snapcraft publish command to work.
They can be exported using "snapcraft export-login"
* SNAPCRAFT_MACAROON
* SNAPCRAFT_UBUNTU_DISCHARGE
* SNAPCRAFT_EMAIL
Examples:
${0##*/} build-docker - builds snaps using remote-build inside a Docker environment
${0##*/} build - builds snaps using remote-build
${0##*/} publish-docker-beta - publishes snaps to the beta channel using Docker environment
${0##*/} publish-docker-release - publishes snaps to the release channel using Docker environment
${0##*/} publish-beta - publishes snaps to the beta channel
${0##*/} publish-release - publishes snaps to the release channel
${0##*/} cleanup - clean up temporary files that were created by the builds
EOF
exit 1
}
#######################################
# helper functions
#######################################
function prepare() {
if [ -z "${LAUNCHPAD_KEY}" ] || [ -z "${LAUNCHPAD_ACCESS_TOKEN}" ] || [ -z "${LAUNCHPAD_ACCESS_SECRET}" ]; then
echo "Launchpad oauth tokens are not set, exiting"
usage
exit 1
fi
if [ -z "${SNAPCRAFT_MACAROON}" ] || [ -z "${SNAPCRAFT_UBUNTU_DISCHARGE}" ] || [ -z "${SNAPCRAFT_EMAIL}" ]; then
echo "Snapcraft auth params are not set, exiting"
usage
exit 1
fi
# Launchpad oauth tokens data is necessary to run snapcraft remote-build
#
# Here's an instruction on how to generate launchpad OAuth tokens:
# https://uci.readthedocs.io/en/latest/oauth.html
#
# Launchpad credentials are necessary to run snapcraft remote-build command
echo "[1]
consumer_key = ${LAUNCHPAD_KEY}
consumer_secret =
access_token = ${LAUNCHPAD_ACCESS_TOKEN}
access_secret = ${LAUNCHPAD_ACCESS_SECRET}
" > launchpad_credentials
# Snapcraft login data
# It can be exported using snapcraft export-login command
echo "[login.ubuntu.com]
macaroon = ${SNAPCRAFT_MACAROON}
unbound_discharge = ${SNAPCRAFT_UBUNTU_DISCHARGE}
email = ${SNAPCRAFT_EMAIL}" > snapcraft_login
# Prepare the snap configuration
cp ${SNAPCRAFT_TMPL} ./snapcraft.yaml
sed -i.bak 's/dev_version/'"${VERSION}"'/g' ./snapcraft.yaml
rm -f snapcraft.yaml.bak
}
build_snap() {
# prepare credentials
prepare
# copy them to the directory where snapcraft will be able to read them
mkdir -p ~/${LAUNCHPAD_CREDENTIALS_DIR}
cp -f snapcraft_login ~/${LAUNCHPAD_CREDENTIALS_DIR}/credentials
chmod 600 ~/${LAUNCHPAD_CREDENTIALS_DIR}/credentials
# run the build
snapcraft remote-build --build-on=${ARCH} --launchpad-accept-public-upload
# remove the credentials - we don't need them anymore
rm -rf ~/${LAUNCHPAD_CREDENTIALS_DIR}
# remove version from the file name
rename_snap_file
# cleanup credentials
cleanup
}
build_snap_docker() {
# prepare credentials
prepare
docker run ${INTERACTIVE} --rm \
-v $(pwd):/build \
-v $(pwd)/launchpad_credentials:/root/${LAUNCHPAD_CREDENTIALS_DIR}/credentials:ro \
${BUILDER_IMAGE} \
snapcraft remote-build --build-on=${ARCH} --launchpad-accept-public-upload
# remove version from the file name
rename_snap_file
# cleanup credentials
cleanup
}
rename_snap_file() {
# In order to make working with snaps easier later on
# we remove version from the file name
# Check that the snap file exists
snapFile="${SNAP_NAME}_${VERSION}_${ARCH}.snap"
if [ ! -f ${snapFile} ]; then
echo "Snap file ${snapFile} not found!"
exit 1
fi
mv -f ${snapFile} "${SNAP_NAME}_${ARCH}.snap"
}
publish_snap() {
# prepare credentials
prepare
# Check that the snap file exists
snapFile="${SNAP_NAME}_${ARCH}.snap"
if [ ! -f ${snapFile} ]; then
echo "Snap file ${snapFile} not found!"
exit 1
fi
# Login if necessary
snapcraft login --with=snapcraft_login
# Push to the channel
snapcraft push --release=${CHANNEL} ${snapFile}
# cleanup credentials
cleanup
}
publish_snap_docker() {
# prepare credentials
prepare
# Check that the snap file exists
snapFile="${SNAP_NAME}_${ARCH}.snap"
if [ ! -f ${snapFile} ]; then
echo "Snap file ${snapFile} not found!"
exit 1
fi
# Login and publish the snap
docker run ${INTERACTIVE} --rm \
-v $(pwd):/build \
${BUILDER_IMAGE} \
sh -c "snapcraft login --with=/build/snapcraft_login && snapcraft push --release=${CHANNEL} /build/${snapFile}"
# cleanup credentials
cleanup
}
#######################################
# main functions
#######################################
build() {
if [[ -n "$1" ]]; then
echo "ARCH is set to $1"
ARCH=$1 build_snap
else
ARCH=i386 build_snap
ARCH=arm64 build_snap
ARCH=armhf build_snap
ARCH=amd64 build_snap
fi
}
build_docker() {
if [[ -n "$1" ]]; then
echo "ARCH is set to $1"
ARCH=$1 build_snap_docker
else
ARCH=i386 build_snap_docker
ARCH=arm64 build_snap_docker
ARCH=armhf build_snap_docker
ARCH=amd64 build_snap_docker
fi
}
publish_docker() {
if [[ -z $1 ]]; then
echo "No channel specified"
exit 1
fi
CHANNEL="${1}"
if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "beta" ]; then
echo "$CHANNEL is an invalid value for the update channel!"
exit 1
fi
ARCH=i386 publish_snap_docker
ARCH=arm64 publish_snap_docker
ARCH=armhf publish_snap_docker
ARCH=amd64 publish_snap_docker
}
publish() {
if [[ -z $1 ]]; then
echo "No channel specified"
exit 1
fi
CHANNEL="${1}"
if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "beta" ]; then
echo "$CHANNEL is an invalid value for the update channel!"
exit 1
fi
ARCH=i386 publish_snap
ARCH=arm64 publish_snap
ARCH=armhf publish_snap
ARCH=amd64 publish_snap
}
cleanup() {
rm -f launchpad_credentials
rm -f snapcraft.yaml
rm -f snapcraft.yaml.bak
rm -f snapcraft_login
git checkout snapcraft.yaml
}
#######################################
# main
#######################################
if [[ -z $1 || $1 == "--help" || $1 == "-h" ]]; then
usage
fi
case "$1" in
"build-docker") build_docker $2 ;;
"build") build $2 ;;
"publish-docker-beta") publish_docker beta ;;
"publish-docker-release") publish_docker stable ;;
"publish-beta") publish beta ;;
"publish-release") publish stable ;;
"prepare") prepare ;;
"cleanup") cleanup ;;
*) usage ;;
esac
exit 0

828
client/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
{
"client_settings": "Client settings",
"example_upstream_reserved": "you can specify DNS upstream <0>for a specific domain(s)</0>",
"upstream_parallel": "Use parallel queries to speed up resolving by simultaneously querying all upstream servers",
"example_upstream_reserved": "You can specify DNS upstream <0>for the specific domain(s)</0>",
"upstream_parallel": "Use parallel requests to speed up resolving by simultaneously querying all upstream servers",
"parallel_requests": "Parallel requests",
"bootstrap_dns": "Bootstrap DNS servers",
"bootstrap_dns_desc": "Bootstrap DNS servers are used to resolve IP addresses of the DoH/DoT resolvers you specify as upstreams.",
"check_dhcp_servers": "Check for DHCP servers",
@ -256,6 +257,7 @@
"install_devices_router_list_1": "Open the preferences for your router. Usually, you can access it from your browser via a URL (like http://192.168.0.1/ or http://192.168.1.1/). You may be asked to enter the password. If you don't remember it, you can often reset the password by pressing a button on the router itself. Some routers require a specific application, which in that case should be already installed on your computer/phone.",
"install_devices_router_list_2": "Find the DHCP/DNS settings. Look for the DNS letters next to a field which allows two or three sets of numbers, each broken into four groups of one to three digits.",
"install_devices_router_list_3": "Enter your AdGuard Home server addresses there.",
"install_devices_router_list_4": "You can't set a custom DNS server on some types of routers. In this case it may help if you set up AdGuard Home as a DHCP server. Otherwise, you should search for the manual on how to customize DNS servers for your particular router model.",
"install_devices_windows_list_1": "Open Control Panel through Start menu or Windows search.",
"install_devices_windows_list_2": "Go to Network and Internet category and then to Network and Sharing Center.",
"install_devices_windows_list_3": "On the left side of the screen find Change adapter settings and click on it.",
@ -453,6 +455,8 @@
"example_rewrite_wildcard": "rewrite responses for all <0>example.org</0> subdomains.",
"disable_ipv6": "Disable IPv6",
"disable_ipv6_desc": "If this feature is enabled, all DNS queries for IPv6 addresses (type AAAA) will be dropped.",
"fastest_addr": "Fastest IP address",
"fastest_addr_desc": "Query all DNS servers and return the fastest IP address among all responses",
"autofix_warning_text": "If you click \"Fix\", AdGuard Home will configure your system to use AdGuard Home DNS server.",
"autofix_warning_list": "It will perform these tasks: <0>Deactivate system DNSStubListener</0> <0>Set DNS server address to 127.0.0.1</0> <0>Replace symbolic link target of /etc/resolv.conf with /run/systemd/resolve/resolv.conf</0> <0>Stop DNSStubListener (reload systemd-resolved service)</0>",
"autofix_warning_result": "As a result all DNS requests from your system will be processed by AdGuard Home by default.",

View File

@ -2,6 +2,7 @@ import { createAction } from 'redux-actions';
import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './index';
import { normalizeTextarea } from '../helpers/helpers';
export const getDnsConfigRequest = createAction('GET_DNS_CONFIG_REQUEST');
export const getDnsConfigFailure = createAction('GET_DNS_CONFIG_FAILURE');
@ -25,8 +26,26 @@ export const setDnsConfigSuccess = createAction('SET_DNS_CONFIG_SUCCESS');
export const setDnsConfig = config => async (dispatch) => {
dispatch(setDnsConfigRequest());
try {
await apiClient.setDnsConfig(config);
dispatch(addSuccessToast('config_successfully_saved'));
const data = { ...config };
let hasDnsSettings = false;
if (Object.prototype.hasOwnProperty.call(data, 'bootstrap_dns')) {
data.bootstrap_dns = normalizeTextarea(config.bootstrap_dns);
hasDnsSettings = true;
}
if (Object.prototype.hasOwnProperty.call(data, 'upstream_dns')) {
data.upstream_dns = normalizeTextarea(config.upstream_dns);
hasDnsSettings = true;
}
await apiClient.setDnsConfig(data);
if (hasDnsSettings) {
dispatch(addSuccessToast('updated_upstream_dns_toast'));
} else {
dispatch(addSuccessToast('config_successfully_saved'));
}
dispatch(setDnsConfigSuccess(config));
} catch (error) {
dispatch(addErrorToast({ error }));

View File

@ -244,6 +244,7 @@ export const getDnsStatus = () => async (dispatch) => {
dispatch(dnsStatusFailure());
window.location.reload(true);
};
const handleRequestSuccess = (response) => {
const dnsStatus = response.data;
const { running } = dnsStatus;
@ -265,42 +266,6 @@ export const getDnsStatus = () => async (dispatch) => {
}
};
export const getDnsSettingsRequest = createAction('GET_DNS_SETTINGS_REQUEST');
export const getDnsSettingsFailure = createAction('GET_DNS_SETTINGS_FAILURE');
export const getDnsSettingsSuccess = createAction('GET_DNS_SETTINGS_SUCCESS');
export const getDnsSettings = () => async (dispatch) => {
dispatch(getDnsSettingsRequest());
try {
const dnsStatus = await apiClient.getGlobalStatus();
dispatch(getDnsSettingsSuccess(dnsStatus));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getDnsSettingsFailure());
}
};
export const handleUpstreamChange = createAction('HANDLE_UPSTREAM_CHANGE');
export const setUpstreamRequest = createAction('SET_UPSTREAM_REQUEST');
export const setUpstreamFailure = createAction('SET_UPSTREAM_FAILURE');
export const setUpstreamSuccess = createAction('SET_UPSTREAM_SUCCESS');
export const setUpstream = config => async (dispatch) => {
dispatch(setUpstreamRequest());
try {
const values = { ...config };
values.bootstrap_dns = normalizeTextarea(values.bootstrap_dns);
values.upstream_dns = normalizeTextarea(values.upstream_dns);
await apiClient.setUpstream(values);
dispatch(addSuccessToast('updated_upstream_dns_toast'));
dispatch(setUpstreamSuccess(config));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setUpstreamFailure());
}
};
export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');

View File

@ -32,7 +32,6 @@ class Api {
// Global methods
GLOBAL_STATUS = { path: 'status', method: 'GET' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstreams_config', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'POST' };
GLOBAL_UPDATE = { path: 'update', method: 'POST' };
@ -42,15 +41,6 @@ class Api {
return this.makeRequest(path, method);
}
setUpstream(url) {
const { path, method } = this.GLOBAL_SET_UPSTREAM_DNS;
const config = {
data: url,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, config);
}
testUpstream(servers) {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = {

View File

@ -58,7 +58,7 @@ const BlockedDomains = ({
noDataText={t('no_domains_found')}
minRows={6}
defaultPageSize={100}
className="-highlight card-table-overflow stats__table"
className="-highlight card-table-overflow--limited stats__table"
/>
</Card>
);

View File

@ -119,7 +119,7 @@ const Clients = ({
noDataText={t('no_clients_found')}
minRows={6}
defaultPageSize={100}
className="-highlight card-table-overflow clients__table"
className="-highlight card-table-overflow--limited clients__table"
getTrProps={(_state, rowInfo) => {
if (!rowInfo) {
return {};

View File

@ -59,7 +59,7 @@ const QueriedDomains = ({
noDataText={t('no_domains_found')}
minRows={6}
defaultPageSize={100}
className="-highlight card-table-overflow stats__table"
className="-highlight card-table-overflow--limited stats__table"
/>
</Card>
);

View File

@ -17,26 +17,55 @@ import {
} from '../../../../helpers/form';
import { BLOCKING_MODES } from '../../../../helpers/constants';
const getFields = (processing, t) => Object.values(BLOCKING_MODES).map(mode => (
<Field
key={mode}
name="blocking_mode"
type="radio"
component={renderRadioField}
value={mode}
placeholder={t(mode)}
disabled={processing}
/>
));
const checkboxes = [{
name: 'edns_cs_enabled',
placeholder: 'edns_enable',
subtitle: 'edns_cs_desc',
},
{
name: 'dnssec_enabled',
placeholder: 'dnssec_enable',
subtitle: 'dnssec_enable_desc',
},
{
name: 'disable_ipv6',
placeholder: 'disable_ipv6',
subtitle: 'disable_ipv6_desc',
}];
const customIps = [{
description: 'blocking_ipv4_desc',
name: 'blocking_ipv4',
validateIp: ipv4,
},
{
description: 'blocking_ipv6_desc',
name: 'blocking_ipv6',
validateIp: ipv6,
}];
const getFields = (processing, t) => Object.values(BLOCKING_MODES)
.map(mode => (
<Field
key={mode}
name="blocking_mode"
type="radio"
component={renderRadioField}
value={mode}
placeholder={t(mode)}
disabled={processing}
/>
));
let Form = ({
handleSubmit, submitting, invalid, processing, blockingMode, t,
}) => (
}) =>
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="ratelimit" className="form__label form__label--with-desc">
<label htmlFor="ratelimit"
className="form__label form__label--with-desc">
<Trans>rate_limit</Trans>
</label>
<div className="form__desc form__desc--top">
@ -53,53 +82,31 @@ let Form = ({
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="edns_cs_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('edns_enable')}
disabled={processing}
subtitle={t('edns_cs_desc')}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="dnssec_enabled"
type="checkbox"
component={renderSelectField}
placeholder={t('dnssec_enable')}
disabled={processing}
subtitle={t('dnssec_enable_desc')}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="disable_ipv6"
type="checkbox"
component={renderSelectField}
placeholder={t('disable_ipv6')}
disabled={processing}
subtitle={t('disable_ipv6_desc')}
/>
</div>
</div>
{checkboxes.map(({ name, placeholder, subtitle }) =>
<div className="col-12" key={name}>
<div className="form__group form__group--settings">
<Field
name={name}
type="checkbox"
component={renderSelectField}
placeholder={t(placeholder)}
disabled={processing}
subtitle={t(subtitle)}
/>
</div>
</div>)}
<div className="col-12">
<div className="form__group form__group--settings mb-4">
<label className="form__label form__label--with-desc">
<Trans>blocking_mode</Trans>
</label>
<div className="form__desc form__desc--top">
{Object.values(BLOCKING_MODES).map(mode => (
<li key={mode}>
<Trans >{`blocking_mode_${mode}`}</Trans>
</li>
))}
{Object.values(BLOCKING_MODES)
.map(mode => (
<li key={mode}>
<Trans>{`blocking_mode_${mode}`}</Trans>
</li>
))}
</div>
<div className="custom-controls-stacked">
{getFields(processing, t)}
@ -108,40 +115,27 @@ let Form = ({
</div>
{blockingMode === BLOCKING_MODES.custom_ip && (
<Fragment>
<div className="col-12 col-sm-6">
{customIps.map(({
description,
name,
validateIp,
}) => <div className="col-12 col-sm-6" key={name}>
<div className="form__group form__group--settings">
<label htmlFor="blocking_ipv4" className="form__label form__label--with-desc">
<Trans>blocking_ipv4</Trans>
<label className="form__label form__label--with-desc"
htmlFor={name}><Trans>{name}</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>blocking_ipv4_desc</Trans>
<Trans>{description}</Trans>
</div>
<Field
name="blocking_ipv4"
name={name}
component={renderInputField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv4, required]}
validate={[validateIp, required]}
/>
</div>
</div>
<div className="col-12 col-sm-6">
<div className="form__group form__group--settings">
<label htmlFor="ip_address" className="form__label form__label--with-desc">
<Trans>blocking_ipv6</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>blocking_ipv6_desc</Trans>
</div>
<Field
name="blocking_ipv6"
component={renderInputField}
className="form-control"
placeholder={t('form_enter_ip')}
validate={[ipv6, required]}
/>
</div>
</div>
</div>)}
</Fragment>
)}
</div>
@ -152,8 +146,7 @@ let Form = ({
>
<Trans>save_btn</Trans>
</button>
</form>
);
</form>;
Form.propTypes = {
blockingMode: PropTypes.string.isRequired,

View File

@ -6,21 +6,50 @@ import { Trans, withNamespaces } from 'react-i18next';
import flow from 'lodash/flow';
import classnames from 'classnames';
import { renderSelectField } from '../../../../helpers/form';
import Examples from './Examples';
import { renderSelectField } from '../../../../helpers/form';
const getInputFields = (parallel_requests_selected, fastest_addr_selected) => [{
// eslint-disable-next-line react/display-name
getTitle: () => <label className="form__label" htmlFor="upstream_dns">
<Trans>upstream_dns</Trans>
</label>,
name: 'upstream_dns',
type: 'text',
component: 'textarea',
className: 'form-control form-control--textarea',
placeholder: 'upstream_dns',
},
{
name: 'parallel_requests',
placeholder: 'parallel_requests',
component: renderSelectField,
type: 'checkbox',
subtitle: 'upstream_parallel',
disabled: fastest_addr_selected,
},
{
name: 'fastest_addr',
placeholder: 'fastest_addr',
component: renderSelectField,
type: 'checkbox',
subtitle: 'fastest_addr_desc',
disabled: parallel_requests_selected,
}];
let Form = (props) => {
const {
t,
handleSubmit,
testUpstream,
upstreamDns,
bootstrapDns,
allServers,
submitting,
invalid,
processingSetUpstream,
processingSetConfig,
processingTestUpstream,
fastest_addr,
parallel_requests,
upstream_dns,
bootstrap_dns,
} = props;
const testButtonClass = classnames({
@ -28,61 +57,49 @@ let Form = (props) => {
'btn btn-primary btn-standard mr-2 btn-loading': processingTestUpstream,
});
const INPUT_FIELDS = getInputFields(parallel_requests, fastest_addr);
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-12">
<div className="form__group form__group--settings">
<label className="form__label" htmlFor="upstream_dns">
<Trans>upstream_dns</Trans>
</label>
<Field
id="upstream_dns"
name="upstream_dns"
component="textarea"
type="text"
className="form-control form-control--textarea"
placeholder={t('upstream_dns')}
disabled={processingSetUpstream || processingTestUpstream}
/>
</div>
</div>
<div className="col-12">
<div className="form__group form__group--settings">
<Field
name="all_servers"
type="checkbox"
component={renderSelectField}
placeholder={t('upstream_parallel')}
disabled={processingSetUpstream}
/>
</div>
</div>
{INPUT_FIELDS.map(({
name, component, type, className, placeholder, getTitle, subtitle, disabled,
}) => <div className="col-12 mb-4" key={name}>
{typeof getTitle === 'function' && getTitle()}
<Field
id={name}
name={name}
component={component}
type={type}
className={className}
placeholder={t(placeholder)}
subtitle={t(subtitle)}
disabled={processingSetConfig || processingTestUpstream || disabled}
/>
</div>)}
<div className="col-12">
<Examples />
<hr />
</div>
<div className="col-12">
<div className="form__group">
<label
className="form__label form__label--with-desc"
htmlFor="bootstrap_dns"
>
<Trans>bootstrap_dns</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>bootstrap_dns_desc</Trans>
</div>
<Field
id="bootstrap_dns"
name="bootstrap_dns"
component="textarea"
type="text"
className="form-control form-control--textarea form-control--textarea-small"
placeholder={t('bootstrap_dns')}
disabled={processingSetUpstream}
/>
<div className="col-12 mb-4">
<label
className="form__label form__label--with-desc"
htmlFor="bootstrap_dns"
>
<Trans>bootstrap_dns</Trans>
</label>
<div className="form__desc form__desc--top">
<Trans>bootstrap_dns_desc</Trans>
</div>
<Field
id="bootstrap_dns"
name="bootstrap_dns"
component="textarea"
type="text"
className="form-control form-control--textarea form-control--textarea-small"
placeholder={t('bootstrap_dns')}
disabled={processingSetConfig}
/>
</div>
</div>
<div className="card-actions">
@ -92,12 +109,11 @@ let Form = (props) => {
className={testButtonClass}
onClick={() =>
testUpstream({
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
upstream_dns,
bootstrap_dns,
})
}
disabled={!upstreamDns || processingTestUpstream}
disabled={!upstream_dns || processingTestUpstream}
>
<Trans>test_upstream_btn</Trans>
</button>
@ -105,7 +121,7 @@ let Form = (props) => {
type="submit"
className="btn btn-success btn-standard"
disabled={
submitting || invalid || processingSetUpstream || processingTestUpstream
submitting || invalid || processingSetConfig || processingTestUpstream
}
>
<Trans>apply_btn</Trans>
@ -122,24 +138,28 @@ Form.propTypes = {
submitting: PropTypes.bool,
invalid: PropTypes.bool,
initialValues: PropTypes.object,
upstreamDns: PropTypes.string,
bootstrapDns: PropTypes.string,
allServers: PropTypes.bool,
upstream_dns: PropTypes.string,
bootstrap_dns: PropTypes.string,
fastest_addr: PropTypes.bool,
parallel_requests: PropTypes.bool,
processingTestUpstream: PropTypes.bool,
processingSetUpstream: PropTypes.bool,
processingSetConfig: PropTypes.bool,
t: PropTypes.func,
};
const selector = formValueSelector('upstreamForm');
Form = connect((state) => {
const upstreamDns = selector(state, 'upstream_dns');
const bootstrapDns = selector(state, 'bootstrap_dns');
const allServers = selector(state, 'all_servers');
const upstream_dns = selector(state, 'upstream_dns');
const bootstrap_dns = selector(state, 'bootstrap_dns');
const fastest_addr = selector(state, 'fastest_addr');
const parallel_requests = selector(state, 'parallel_requests');
return {
upstreamDns,
bootstrapDns,
allServers,
upstream_dns,
bootstrap_dns,
fastest_addr,
parallel_requests,
};
})(Form);

View File

@ -7,7 +7,7 @@ import Card from '../../../ui/Card';
class Upstream extends Component {
handleSubmit = (values) => {
this.props.setUpstream(values);
this.props.setDnsConfig(values);
};
handleTest = (values) => {
@ -17,11 +17,14 @@ class Upstream extends Component {
render() {
const {
t,
upstreamDns: upstream_dns,
bootstrapDns: bootstrap_dns,
allServers: all_servers,
processingSetUpstream,
processingTestUpstream,
dnsConfig: {
upstream_dns,
bootstrap_dns,
fastest_addr,
parallel_requests,
processingSetConfig,
},
} = this.props;
return (
@ -36,12 +39,13 @@ class Upstream extends Component {
initialValues={{
upstream_dns,
bootstrap_dns,
all_servers,
fastest_addr,
parallel_requests,
}}
testUpstream={this.handleTest}
onSubmit={this.handleSubmit}
processingTestUpstream={processingTestUpstream}
processingSetUpstream={processingSetUpstream}
processingSetConfig={processingSetConfig}
/>
</div>
</div>
@ -51,14 +55,11 @@ class Upstream extends Component {
}
Upstream.propTypes = {
upstreamDns: PropTypes.string,
bootstrapDns: PropTypes.string,
allServers: PropTypes.bool,
setUpstream: PropTypes.func.isRequired,
testUpstream: PropTypes.func.isRequired,
processingSetUpstream: PropTypes.bool.isRequired,
processingTestUpstream: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,
};
export default withNamespaces()(Upstream);

View File

@ -10,7 +10,6 @@ import Loading from '../../ui/Loading';
class Dns extends Component {
componentDidMount() {
this.props.getDnsSettings();
this.props.getAccessList();
this.props.getDnsConfig();
}
@ -18,59 +17,45 @@ class Dns extends Component {
render() {
const {
t,
dashboard,
settings,
access,
setAccessList,
testUpstream,
setUpstream,
dnsConfig,
setDnsConfig,
} = this.props;
const isDataLoading = dashboard.processingDnsSettings
|| access.processing
|| dnsConfig.processingGetConfig;
const isDataReady = !dashboard.processingDnsSettings
&& !access.processing
&& !dnsConfig.processingGetConfig;
const isDataLoading = access.processing || dnsConfig.processingGetConfig;
return (
<Fragment>
<PageTitle title={t('dns_settings')} />
{isDataLoading && <Loading />}
{isDataReady && (
{isDataLoading ?
<Loading /> :
<Fragment>
<Upstream
processingTestUpstream={settings.processingTestUpstream}
testUpstream={testUpstream}
dnsConfig={dnsConfig}
setDnsConfig={setDnsConfig}
/>
<Config
dnsConfig={dnsConfig}
setDnsConfig={setDnsConfig}
/>
<Upstream
upstreamDns={dashboard.upstreamDns}
bootstrapDns={dashboard.bootstrapDns}
allServers={dashboard.allServers}
processingTestUpstream={settings.processingTestUpstream}
processingSetUpstream={settings.processingSetUpstream}
setUpstream={setUpstream}
testUpstream={testUpstream}
/>
<Access access={access} setAccessList={setAccessList} />
</Fragment>
)}
</Fragment>}
</Fragment>
);
}
}
Dns.propTypes = {
dashboard: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
setUpstream: PropTypes.func.isRequired,
testUpstream: PropTypes.func.isRequired,
getAccessList: PropTypes.func.isRequired,
setAccessList: PropTypes.func.isRequired,
access: PropTypes.object.isRequired,
getDnsSettings: PropTypes.func.isRequired,
dnsConfig: PropTypes.object.isRequired,
setDnsConfig: PropTypes.func.isRequired,
getDnsConfig: PropTypes.func.isRequired,

View File

@ -10,6 +10,11 @@
}
.card-table-overflow {
overflow-y: auto;
max-height: 100%;
}
.card-table-overflow--limited {
overflow-y: auto;
max-height: 280px;
}

View File

@ -83,6 +83,7 @@
.checkbox__input:disabled + .checkbox__label {
cursor: default;
color: var(--gray);
}
.checkbox__input:disabled + .checkbox__label:before {

View File

@ -34,6 +34,9 @@ const Guide = (props) => {
<li>
<Trans>install_devices_router_list_3</Trans>
</li>
<li>
<Trans>install_devices_router_list_4</Trans>
</li>
</ol>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { handleUpstreamChange, setUpstream, testUpstream, getDnsSettings } from '../actions';
import { testUpstream } from '../actions';
import { getAccessList, setAccessList } from '../actions/access';
import {
getRewritesList,
@ -25,8 +25,6 @@ const mapStateToProps = (state) => {
};
const mapDispatchToProps = {
handleUpstreamChange,
setUpstream,
testUpstream,
getAccessList,
setAccessList,
@ -34,7 +32,6 @@ const mapDispatchToProps = {
addRewrite,
deleteRewrite,
toggleRewritesModal,
getDnsSettings,
getDnsConfig,
setDnsConfig,
};

View File

@ -117,29 +117,30 @@ export const renderSelectField = ({
placeholder,
subtitle,
disabled,
onClick,
modifier = 'checkbox--form',
meta: { touched, error },
}) => (
<Fragment>
<label className={`checkbox ${modifier}`}>
<span className="checkbox__marker" />
<input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
<span className="checkbox__label">
<span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{placeholder}</span>
{subtitle && (
<span
className="checkbox__label-subtitle"
dangerouslySetInnerHTML={{ __html: subtitle }}
/>
)}
<Fragment>
<label className={`checkbox ${modifier}`} onClick={onClick}>
<span className="checkbox__marker" />
<input {...input} type="checkbox" className="checkbox__input" disabled={disabled} />
<span className="checkbox__label">
<span className="checkbox__label-text checkbox__label-text--long">
<span className="checkbox__label-title">{placeholder}</span>
{subtitle && (
<span
className="checkbox__label-subtitle"
dangerouslySetInnerHTML={{ __html: subtitle }}
/>
)}
</span>
</span>
</span>
</label>
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
</label>
{!disabled &&
touched &&
(error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
export const renderServiceField = ({

View File

@ -217,19 +217,19 @@ class Settings extends Component {
</div>
<div className="col-12">
{webStatus &&
<div className="setup__error text-danger">
{webStatus}
{isWebFixAvailable &&
<button
type="button"
className="btn btn-secondary btn-sm ml-2"
onClick={() => this.handleAutofix('web')}
>
<Trans>fix</Trans>
</button>
}
<hr className="divider--small" />
</div>
<div className="setup__error text-danger">
{webStatus}
{isWebFixAvailable &&
<button
type="button"
className="btn btn-secondary btn-sm ml-2"
onClick={() => this.handleAutofix('web')}
>
<Trans>fix</Trans>
</button>
}
<hr className="divider--small" />
</div>
}
</div>
</div>
@ -287,32 +287,33 @@ class Settings extends Component {
</div>
<div className="col-12">
{dnsStatus &&
<Fragment>
<div className="setup__error text-danger">
{dnsStatus}
{isDnsFixAvailable &&
<button
type="button"
className="btn btn-secondary btn-sm ml-2"
onClick={() => this.handleAutofix('dns')}
>
<Trans>fix</Trans>
</button>
}
</div>
<div className="text-muted mb-2">
<p className="mb-1">
<Trans>autofix_warning_text</Trans>
</p>
<Trans components={[<li key="0">text</li>]}>
autofix_warning_list
</Trans>
<p className="mb-1">
<Trans>autofix_warning_result</Trans>
</p>
</div>
<hr className="divider--small" />
</Fragment>
<Fragment>
<div className="setup__error text-danger">
{dnsStatus}
{isDnsFixAvailable &&
<button
type="button"
className="btn btn-secondary btn-sm ml-2"
onClick={() => this.handleAutofix('dns')}
>
<Trans>fix</Trans>
</button>
}
</div>
{isDnsFixAvailable &&
<div className="text-muted mb-2">
<p className="mb-1">
<Trans>autofix_warning_text</Trans>
</p>
<Trans components={[<li key="0">text</li>]}>
autofix_warning_list
</Trans>
<p className="mb-1">
<Trans>autofix_warning_result</Trans>
</p>
</div>}
<hr className="divider--small" />
</Fragment>
}
</div>
</div>

View File

@ -15,6 +15,8 @@ const dnsConfig = handleActions(
const {
blocking_ipv4,
blocking_ipv6,
upstream_dns,
bootstrap_dns,
...values
} = payload;
@ -23,6 +25,8 @@ const dnsConfig = handleActions(
...values,
blocking_ipv4: blocking_ipv4 || DEFAULT_BLOCKING_IPV4,
blocking_ipv6: blocking_ipv6 || DEFAULT_BLOCKING_IPV6,
upstream_dns: (upstream_dns && upstream_dns.join('\n')) || '',
bootstrap_dns: (bootstrap_dns && bootstrap_dns.join('\n')) || '',
processingGetConfig: false,
};
},

View File

@ -35,14 +35,6 @@ const settings = handleActions(
const newSettingsList = { ...settingsList, [settingKey]: newSetting };
return { ...state, settingsList: newSettingsList };
},
[actions.setUpstreamRequest]: state => ({ ...state, processingSetUpstream: true }),
[actions.setUpstreamFailure]: state => ({ ...state, processingSetUpstream: false }),
[actions.setUpstreamSuccess]: (state, { payload }) => ({
...state,
...payload,
processingSetUpstream: false,
}),
[actions.testUpstreamRequest]: state => ({ ...state, processingTestUpstream: true }),
[actions.testUpstreamFailure]: state => ({ ...state, processingTestUpstream: false }),
[actions.testUpstreamSuccess]: state => ({ ...state, processingTestUpstream: false }),
@ -50,7 +42,6 @@ const settings = handleActions(
{
processing: true,
processingTestUpstream: false,
processingSetUpstream: false,
processingDhcpStatus: false,
settingsList: {},
},
@ -67,12 +58,9 @@ const dashboard = handleActions(
version,
dns_port: dnsPort,
dns_addresses: dnsAddresses,
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
protection_enabled: protectionEnabled,
language,
http_port: httpPort,
language,
} = payload;
const newState = {
...state,
@ -81,9 +69,6 @@ const dashboard = handleActions(
dnsVersion: version,
dnsPort,
dnsAddresses,
upstreamDns: (upstreamDns && upstreamDns.join('\n')) || '',
bootstrapDns: (bootstrapDns && bootstrapDns.join('\n')) || '',
allServers,
protectionEnabled,
language,
httpPort,
@ -138,11 +123,6 @@ const dashboard = handleActions(
return newState;
},
[actions.handleUpstreamChange]: (state, { payload }) => {
const { upstreamDns } = payload;
return { ...state, upstreamDns };
},
[actions.getLanguageSuccess]: (state, { payload }) => {
const newState = { ...state, language: payload };
return newState;
@ -159,24 +139,6 @@ const dashboard = handleActions(
return newState;
},
[actions.getDnsSettingsRequest]: state => ({ ...state, processingDnsSettings: true }),
[actions.getDnsSettingsFailure]: state => ({ ...state, processingDnsSettings: false }),
[actions.getDnsSettingsSuccess]: (state, { payload }) => {
const {
upstream_dns: upstreamDns,
bootstrap_dns: bootstrapDns,
all_servers: allServers,
} = payload;
return {
...state,
allServers,
upstreamDns: (upstreamDns && upstreamDns.join('\n')) || '',
bootstrapDns: (bootstrapDns && bootstrapDns.join('\n')) || '',
processingDnsSettings: false,
};
},
[actions.getProfileRequest]: state => ({ ...state, processingProfile: true }),
[actions.getProfileFailure]: state => ({ ...state, processingProfile: false }),
[actions.getProfileSuccess]: (state, { payload }) => ({
@ -191,11 +153,7 @@ const dashboard = handleActions(
processingVersion: true,
processingClients: true,
processingUpdate: false,
processingDnsSettings: true,
processingProfile: true,
upstreamDns: '',
bootstrapDns: '',
allServers: false,
protectionEnabled: false,
processingProtection: false,
httpPort: 80,

View File

@ -24,6 +24,7 @@ var serviceRulesArray = []svc{
"||facebook.com^",
"||facebook.net^",
"||fbcdn.net^",
"||accountkit.com^",
"||fb.me^",
"||fb.com^",
"||fbsbx.com^",
@ -31,7 +32,7 @@ var serviceRulesArray = []svc{
"||facebookcorewwwi.onion^",
"||fbcdn.com^",
}},
{"twitter", []string{"||twitter.com^", "||t.co^", "||twimg.com^"}},
{"twitter", []string{"||twitter.com^", "||twttr.com^", "||t.co^", "||twimg.com^"}},
{"youtube", []string{
"||youtube.com^",
"||ytimg.com^",
@ -40,17 +41,31 @@ var serviceRulesArray = []svc{
"||youtubei.googleapis.com^",
"||youtube-nocookie.com^",
}},
{"twitch", []string{"||twitch.tv^", "||ttvnw.net^"}},
{"netflix", []string{"||nflxext.com^", "||netflix.com^"}},
{"twitch", []string{"||twitch.tv^", "||ttvnw.net^", "||jtvnw.net^", "||twitchcdn.net^"}},
{"netflix", []string{"||nflxext.com^", "||netflix.com^", "||nflximg.net^", "||nflxvideo.net^"}},
{"instagram", []string{"||instagram.com^", "||cdninstagram.com^"}},
{"snapchat", []string{"||snapchat.com^", "||sc-cdn.net^", "||impala-media-production.s3.amazonaws.com^"}},
{"snapchat", []string{
"||snapchat.com^",
"||sc-cdn.net^",
"||snap-dev.net^",
"||snapkit.co",
"||snapads.com^",
"||impala-media-production.s3.amazonaws.com^",
}},
{"discord", []string{"||discord.gg^", "||discordapp.net^", "||discordapp.com^", "||discord.media^"}},
{"ok", []string{"||ok.ru^"}},
{"skype", []string{"||skype.com^"}},
{"vk", []string{"||vk.com^"}},
{"skype", []string{"||skype.com^", "||skypeassets.com^"}},
{"vk", []string{"||vk.com^", "||userapi.com^", "||vk-cdn.net^", "||vkuservideo.net^"}},
{"origin", []string{"||origin.com^", "||signin.ea.com^", "||accounts.ea.com^"}},
{"steam", []string{"||steam.com^", "||steampowered.com^"}},
{"epic_games", []string{"||epicgames.com^"}},
{"steam", []string{
"||steam.com^",
"||steampowered.com^",
"||steamcommunity.com^",
"||steamstatic.com^",
"||steamstore-a.akamaihd.net^",
"||steamcdn-a.akamaihd.net^",
}},
{"epic_games", []string{"||epicgames.com^", "||easyanticheat.net^", "||easy.ac^", "||eac-cdn.com^"}},
{"reddit", []string{"||reddit.com^", "||redditstatic.com^", "||redditmedia.com^", "||redd.it^"}},
{"mail_ru", []string{"||mail.ru^"}},
{"cloudflare", []string{
@ -72,7 +87,13 @@ var serviceRulesArray = []svc{
{"amazon", []string{
"||amazon.com^",
"||media-amazon.com^",
"||primevideo.com^",
"||amazontrust.com^",
"||images-amazon.com^",
"||ssl-images-amazon.com^",
"||amazonpay.com^",
"||amazonpay.in^",
"||amazon-adsystem.com^",
"||a2z.com^",
"||amazon.ae^",
"||amazon.ca^",
@ -88,6 +109,7 @@ var serviceRulesArray = []svc{
"||amazon.co.jp^",
"||amazon.com.mx^",
"||amazon.co.uk^",
"||createspace.com^",
}},
{"ebay", []string{
"||ebay.com^",
@ -119,6 +141,7 @@ var serviceRulesArray = []svc{
{"tiktok", []string{
"||tiktok.com^",
"||tiktokcdn.com^",
"||musical.ly^",
"||snssdk.com^",
"||amemv.com^",
"||toutiao.com^",

View File

@ -628,6 +628,11 @@ func makeResult(rule rules.Rule, reason Reason) Result {
return res
}
// InitModule() - manually initialize blocked services map
func InitModule() {
initBlockedServices()
}
// New creates properly initialized DNS Filter that is ready to be used
func New(c *Config, blockFilters []Filter) *Dnsfilter {
@ -677,8 +682,6 @@ func New(c *Config, blockFilters []Filter) *Dnsfilter {
}
d.BlockedServices = bsvcs
initBlockedServices()
if blockFilters != nil {
err := d.initFiltering(nil, blockFilters)
if err != nil {

View File

@ -532,96 +532,6 @@ func TestClientSettings(t *testing.T) {
assert.True(t, r.IsFiltered && r.Reason == FilteredBlockedService)
}
func TestRewrites(t *testing.T) {
d := Dnsfilter{}
// CNAME, A, AAAA
d.Rewrites = []RewriteEntry{
RewriteEntry{"somecname", "somehost.com", 0, nil},
RewriteEntry{"somehost.com", "0.0.0.0", 0, nil},
RewriteEntry{"host.com", "1.2.3.4", 0, nil},
RewriteEntry{"host.com", "1.2.3.5", 0, nil},
RewriteEntry{"host.com", "1:2:3::4", 0, nil},
RewriteEntry{"www.host.com", "host.com", 0, nil},
}
d.prepareRewrites()
r := d.processRewrites("host2.com")
assert.Equal(t, NotFilteredNotFound, r.Reason)
r = d.processRewrites("www.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, "host.com", r.CanonName)
assert.True(t, len(r.IPList) == 3)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
assert.True(t, r.IPList[1].Equal(net.ParseIP("1.2.3.5")))
assert.True(t, r.IPList[2].Equal(net.ParseIP("1:2:3::4")))
// wildcard
d.Rewrites = []RewriteEntry{
RewriteEntry{"host.com", "1.2.3.4", 0, nil},
RewriteEntry{"*.host.com", "1.2.3.5", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
r = d.processRewrites("www.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.5")))
r = d.processRewrites("www.host2.com")
assert.Equal(t, NotFilteredNotFound, r.Reason)
// override a wildcard
d.Rewrites = []RewriteEntry{
RewriteEntry{"a.host.com", "1.2.3.4", 0, nil},
RewriteEntry{"*.host.com", "1.2.3.5", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("a.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.True(t, len(r.IPList) == 1)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
// wildcard + CNAME
d.Rewrites = []RewriteEntry{
RewriteEntry{"host.com", "1.2.3.4", 0, nil},
RewriteEntry{"*.host.com", "host.com", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("www.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, "host.com", r.CanonName)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
// 2 CNAMEs
d.Rewrites = []RewriteEntry{
RewriteEntry{"b.host.com", "a.host.com", 0, nil},
RewriteEntry{"a.host.com", "host.com", 0, nil},
RewriteEntry{"host.com", "1.2.3.4", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("b.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, "host.com", r.CanonName)
assert.True(t, len(r.IPList) == 1)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
// 2 CNAMEs + wildcard
d.Rewrites = []RewriteEntry{
RewriteEntry{"b.host.com", "a.host.com", 0, nil},
RewriteEntry{"a.host.com", "x.somehost.com", 0, nil},
RewriteEntry{"*.somehost.com", "1.2.3.4", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("b.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, "x.somehost.com", r.CanonName)
assert.True(t, len(r.IPList) == 1)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
}
func prepareTestDir() string {
const dir = "./agh-test"
_ = os.RemoveAll(dir)

View File

@ -42,7 +42,10 @@ func (a rewritesArray) Len() int { return len(a) }
func (a rewritesArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// Priority: CNAME, A/AAAA; exact, wildcard.
// Priority:
// . CNAME > A/AAAA;
// . exact > wildcard;
// . higher level wildcard > lower level wildcard
func (a rewritesArray) Less(i, j int) bool {
if a[i].Type == dns.TypeCNAME && a[j].Type != dns.TypeCNAME {
return false
@ -50,13 +53,18 @@ func (a rewritesArray) Less(i, j int) bool {
return true
}
if isWildcard(a[i].Domain) && !isWildcard(a[j].Domain) {
return false
} else if !isWildcard(a[i].Domain) && isWildcard(a[j].Domain) {
return true
if isWildcard(a[i].Domain) {
if !isWildcard(a[j].Domain) {
return false
}
} else {
if isWildcard(a[j].Domain) {
return true
}
}
return i < j
// both are wildcards
return len(a[i].Domain) > len(a[j].Domain)
}
// Prepare entry for use
@ -86,6 +94,7 @@ func (d *Dnsfilter) prepareRewrites() {
// Get the list of matched rewrite entries.
// Priority: CNAME, A/AAAA; exact, wildcard.
// If matched exactly, don't return wildcard entries.
// If matched by several wildcards, select the more specific one
func findRewrites(a []RewriteEntry, host string) []RewriteEntry {
rr := rewritesArray{}
for _, r := range a {
@ -111,7 +120,10 @@ func findRewrites(a []RewriteEntry, host string) []RewriteEntry {
break
}
}
} else {
rr = rr[:1]
}
return rr
}

127
dnsfilter/rewrites_test.go Normal file
View File

@ -0,0 +1,127 @@
package dnsfilter
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRewrites(t *testing.T) {
d := Dnsfilter{}
// CNAME, A, AAAA
d.Rewrites = []RewriteEntry{
RewriteEntry{"somecname", "somehost.com", 0, nil},
RewriteEntry{"somehost.com", "0.0.0.0", 0, nil},
RewriteEntry{"host.com", "1.2.3.4", 0, nil},
RewriteEntry{"host.com", "1.2.3.5", 0, nil},
RewriteEntry{"host.com", "1:2:3::4", 0, nil},
RewriteEntry{"www.host.com", "host.com", 0, nil},
}
d.prepareRewrites()
r := d.processRewrites("host2.com")
assert.Equal(t, NotFilteredNotFound, r.Reason)
r = d.processRewrites("www.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, "host.com", r.CanonName)
assert.True(t, len(r.IPList) == 3)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
assert.True(t, r.IPList[1].Equal(net.ParseIP("1.2.3.5")))
assert.True(t, r.IPList[2].Equal(net.ParseIP("1:2:3::4")))
// wildcard
d.Rewrites = []RewriteEntry{
RewriteEntry{"host.com", "1.2.3.4", 0, nil},
RewriteEntry{"*.host.com", "1.2.3.5", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
r = d.processRewrites("www.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.5")))
r = d.processRewrites("www.host2.com")
assert.Equal(t, NotFilteredNotFound, r.Reason)
// override a wildcard
d.Rewrites = []RewriteEntry{
RewriteEntry{"a.host.com", "1.2.3.4", 0, nil},
RewriteEntry{"*.host.com", "1.2.3.5", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("a.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.True(t, len(r.IPList) == 1)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
// wildcard + CNAME
d.Rewrites = []RewriteEntry{
RewriteEntry{"host.com", "1.2.3.4", 0, nil},
RewriteEntry{"*.host.com", "host.com", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("www.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, "host.com", r.CanonName)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
// 2 CNAMEs
d.Rewrites = []RewriteEntry{
RewriteEntry{"b.host.com", "a.host.com", 0, nil},
RewriteEntry{"a.host.com", "host.com", 0, nil},
RewriteEntry{"host.com", "1.2.3.4", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("b.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, "host.com", r.CanonName)
assert.True(t, len(r.IPList) == 1)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
// 2 CNAMEs + wildcard
d.Rewrites = []RewriteEntry{
RewriteEntry{"b.host.com", "a.host.com", 0, nil},
RewriteEntry{"a.host.com", "x.somehost.com", 0, nil},
RewriteEntry{"*.somehost.com", "1.2.3.4", 0, nil},
}
d.prepareRewrites()
r = d.processRewrites("b.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, "x.somehost.com", r.CanonName)
assert.True(t, len(r.IPList) == 1)
assert.True(t, r.IPList[0].Equal(net.ParseIP("1.2.3.4")))
}
func TestRewritesLevels(t *testing.T) {
d := Dnsfilter{}
// exact host, wildcard L2, wildcard L3
d.Rewrites = []RewriteEntry{
RewriteEntry{"host.com", "1.1.1.1", 0, nil},
RewriteEntry{"*.host.com", "2.2.2.2", 0, nil},
RewriteEntry{"*.sub.host.com", "3.3.3.3", 0, nil},
}
d.prepareRewrites()
// match exact
r := d.processRewrites("host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, 1, len(r.IPList))
assert.Equal(t, "1.1.1.1", r.IPList[0].String())
// match L2
r = d.processRewrites("sub.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, 1, len(r.IPList))
assert.Equal(t, "2.2.2.2", r.IPList[0].String())
// match L3
r = d.processRewrites("my.sub.host.com")
assert.Equal(t, ReasonRewrite, r.Reason)
assert.Equal(t, 1, len(r.IPList))
assert.Equal(t, "3.3.3.3", r.IPList[0].String())
}

View File

@ -141,6 +141,8 @@ type FilteringConfig struct {
// Respond with an empty answer to all AAAA requests
AAAADisabled bool `yaml:"aaaa_disabled"`
FastestAddrAlgo bool `yaml:"fastest_addr"` // use Fastest Address algorithm
AllowedClients []string `yaml:"allowed_clients"` // IP addresses of whitelist clients
DisallowedClients []string `yaml:"disallowed_clients"` // IP addresses of clients that should be blocked
BlockedHosts []string `yaml:"blocked_hosts"` // hosts that should be blocked
@ -305,6 +307,7 @@ func (s *Server) Prepare(config *ServerConfig) error {
RequestHandler: s.handleDNSRequest,
AllServers: s.conf.AllServers,
EnableEDNSClientSubnet: s.conf.EnableEDNSClientSubnet,
FindFastestAddr: s.conf.FastestAddrAlgo,
}
intlProxyConfig := proxy.Config{

View File

@ -22,6 +22,9 @@ func httpError(r *http.Request, w http.ResponseWriter, code int, format string,
}
type dnsConfigJSON struct {
Upstreams []string `json:"upstream_dns"`
Bootstraps []string `json:"bootstrap_dns"`
ProtectionEnabled bool `json:"protection_enabled"`
RateLimit uint32 `json:"ratelimit"`
BlockingMode string `json:"blocking_mode"`
@ -30,11 +33,16 @@ type dnsConfigJSON struct {
EDNSCSEnabled bool `json:"edns_cs_enabled"`
DNSSECEnabled bool `json:"dnssec_enabled"`
DisableIPv6 bool `json:"disable_ipv6"`
FastestAddr bool `json:"fastest_addr"`
ParallelRequests bool `json:"parallel_requests"`
}
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
resp := dnsConfigJSON{}
s.RLock()
resp.Upstreams = stringArrayDup(s.conf.UpstreamDNS)
resp.Bootstraps = stringArrayDup(s.conf.BootstrapDNS)
resp.ProtectionEnabled = s.conf.ProtectionEnabled
resp.BlockingMode = s.conf.BlockingMode
resp.BlockingIPv4 = s.conf.BlockingIPv4
@ -43,6 +51,8 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
resp.EDNSCSEnabled = s.conf.EnableEDNSClientSubnet
resp.DNSSECEnabled = s.conf.EnableDNSSEC
resp.DisableIPv6 = s.conf.AAAADisabled
resp.FastestAddr = s.conf.FastestAddrAlgo
resp.ParallelRequests = s.conf.AllServers
s.RUnlock()
js, err := json.Marshal(resp)
@ -75,6 +85,7 @@ func checkBlockingMode(req dnsConfigJSON) bool {
return true
}
// nolint(gocyclo) - we need to check each JSON field separately
func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
req := dnsConfigJSON{}
js, err := jsonutil.DecodeObject(&req, r.Body)
@ -83,6 +94,25 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
return
}
if js.Exists("upstream_dns") {
if len(req.Upstreams) != 0 {
err = ValidateUpstreams(req.Upstreams)
if err != nil {
httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
return
}
}
}
if js.Exists("bootstrap_dns") {
for _, host := range req.Bootstraps {
if err := checkPlainDNS(host); err != nil {
httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", host, err)
return
}
}
}
if js.Exists("blocking_mode") && !checkBlockingMode(req) {
httpError(r, w, http.StatusBadRequest, "blocking_mode: incorrect value")
return
@ -91,6 +121,16 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
restart := false
s.Lock()
if js.Exists("upstream_dns") {
s.conf.UpstreamDNS = req.Upstreams
restart = true
}
if js.Exists("bootstrap_dns") {
s.conf.BootstrapDNS = req.Bootstraps
restart = true
}
if js.Exists("protection_enabled") {
s.conf.ProtectionEnabled = req.ProtectionEnabled
}
@ -129,6 +169,14 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
s.conf.AAAADisabled = req.DisableIPv6
}
if js.Exists("fastest_addr") {
s.conf.FastestAddrAlgo = req.FastestAddr
}
if js.Exists("parallel_requests") {
s.conf.AllServers = req.ParallelRequests
}
s.Unlock()
s.conf.ConfigModified()
@ -144,51 +192,6 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
type upstreamJSON struct {
Upstreams []string `json:"upstream_dns"` // Upstreams
BootstrapDNS []string `json:"bootstrap_dns"` // Bootstrap DNS
AllServers bool `json:"all_servers"` // --all-servers param for dnsproxy
}
func (s *Server) handleSetUpstreamConfig(w http.ResponseWriter, r *http.Request) {
req := upstreamJSON{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
httpError(r, w, http.StatusBadRequest, "Failed to parse new upstreams config json: %s", err)
return
}
if len(req.Upstreams) != 0 {
err = ValidateUpstreams(req.Upstreams)
if err != nil {
httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
return
}
}
newconf := FilteringConfig{}
newconf.UpstreamDNS = req.Upstreams
// bootstrap servers are plain DNS only
for _, host := range req.BootstrapDNS {
if err := checkPlainDNS(host); err != nil {
httpError(r, w, http.StatusBadRequest, "%s can not be used as bootstrap dns cause: %s", host, err)
return
}
}
newconf.BootstrapDNS = req.BootstrapDNS
newconf.AllServers = req.AllServers
s.Lock()
s.conf.UpstreamDNS = newconf.UpstreamDNS
s.conf.BootstrapDNS = newconf.BootstrapDNS
s.conf.AllServers = newconf.AllServers
s.Unlock()
s.conf.ConfigModified()
err = s.Reconfigure(nil)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "%s", err)
return
}
}
// ValidateUpstreams validates each upstream and returns an error if any upstream is invalid or if there are no default upstreams specified
@ -399,7 +402,6 @@ func (s *Server) handleDOH(w http.ResponseWriter, r *http.Request) {
func (s *Server) registerHandlers() {
s.conf.HTTPRegister("GET", "/control/dns_info", s.handleGetConfig)
s.conf.HTTPRegister("POST", "/control/dns_config", s.handleSetConfig)
s.conf.HTTPRegister("POST", "/control/set_upstreams_config", s.handleSetUpstreamConfig)
s.conf.HTTPRegister("POST", "/control/test_upstream_dns", s.handleTestUpstreamDNS)
s.conf.HTTPRegister("GET", "/control/access/list", s.handleAccessList)

2
go.mod
View File

@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.14
require (
github.com/AdguardTeam/dnsproxy v0.26.1
github.com/AdguardTeam/dnsproxy v0.27.1
github.com/AdguardTeam/golibs v0.4.2
github.com/AdguardTeam/urlfilter v0.10.0
github.com/NYTimes/gziphandler v1.1.1

4
go.sum
View File

@ -1,5 +1,5 @@
github.com/AdguardTeam/dnsproxy v0.26.1 h1:ZdrrEuNlMqiBlJpcDStitO3m6VBGCb1AP7ndaor2I6E=
github.com/AdguardTeam/dnsproxy v0.26.1/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
github.com/AdguardTeam/dnsproxy v0.27.1 h1:CQ3vtGSNbHNeYkxC6pALwugTSssP2MnsjdxkvVMzEp4=
github.com/AdguardTeam/dnsproxy v0.27.1/go.mod h1:hOYFV9TW+pd5XKYz7KZf2FFD8SvSPqjyGTxUae86s58=
github.com/AdguardTeam/golibs v0.4.0 h1:4VX6LoOqFe9p9Gf55BeD8BvJD6M6RDYmgEiHrENE9KU=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=

View File

@ -47,7 +47,7 @@ func TestAuth(t *testing.T) {
// add session with TTL = 2 sec
s = session{}
s.expire = uint32(now + 2)
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.addSession(sess, &s)
assert.True(t, a.CheckSession(sessStr) == 0)
@ -59,7 +59,7 @@ func TestAuth(t *testing.T) {
// the session is still alive
assert.True(t, a.CheckSession(sessStr) == 0)
// reset our expiration time because CheckSession() has just updated it
s.expire = uint32(now + 2)
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.storeSession(sess, &s)
a.Close()

View File

@ -45,6 +45,7 @@ type configuration struct {
ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client
Language string `yaml:"language"` // two-letter ISO 639-1 language code
RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060
// TTL for a web session (in hours)
// An active session is automatically refreshed once a day.

View File

@ -55,9 +55,6 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
"language": config.Language,
"protection_enabled": c.ProtectionEnabled,
"bootstrap_dns": c.BootstrapDNS,
"upstream_dns": c.UpstreamDNS,
"all_servers": c.AllServers,
}
jsonVal, err := json.Marshal(data)

View File

@ -351,6 +351,11 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
registerControlHandlers()
returnOK(w)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
// 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 restartHTTP {
@ -358,8 +363,6 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
_ = Context.web.httpServer.Shutdown(context.TODO())
}()
}
returnOK(w)
}
func (web *Web) registerInstallHandlers() {

View File

@ -548,7 +548,9 @@ func handleUpdate(w http.ResponseWriter, r *http.Request) {
}
returnOK(w)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
time.Sleep(time.Second) // wait (hopefully) until response is sent (not sure whether it's really necessary)
go finishUpdate(u)
}

View File

@ -208,6 +208,11 @@ func run(args options) {
}
}
// 'clients' module uses 'dnsfilter' module's static data (dnsfilter.BlockedSvcKnown()),
// so we have to initialize dnsfilter's static data first,
// but also avoid relying on automatic Go init() function
dnsfilter.InitModule()
config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified
@ -242,6 +247,16 @@ func run(args options) {
if err != nil {
log.Fatal(err)
}
if config.DebugPProf {
mux := http.NewServeMux()
util.PProfRegisterWebHandlers(mux)
go func() {
log.Info("pprof: listening on localhost:6060")
err := http.ListenAndServe("localhost:6060", mux)
log.Error("Error while running the pprof server: %s", err)
}()
}
}
err := os.MkdirAll(Context.getDataDir(), 0755)

View File

@ -279,11 +279,14 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
tlsConfigStatus: t.status,
}
marshalTLS(w, data2)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
// 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
Context.web.TLSConfigChanged(data)
}()
}

View File

@ -1,6 +1,70 @@
# AdGuard Home API Change Log
## v0.102: API changes
### API: Get general status: GET /control/status
* Removed "upstream_dns", "bootstrap_dns", "all_servers" parameters
### API: Get DNS general settings: GET /control/dns_info
* Added "parallel_requests", "upstream_dns", "bootstrap_dns" parameters
Request:
GET /control/dns_info
Response:
200 OK
{
"upstream_dns": ["tls://...", ...],
"bootstrap_dns": ["1.2.3.4", ...],
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
"blocking_ipv4": "1.2.3.4",
"blocking_ipv6": "1:2:3::4",
"edns_cs_enabled": true | false,
"dnssec_enabled": true | false
"disable_ipv6": true | false,
"fastest_addr": true | false, // use Fastest Address algorithm
"parallel_requests": true | false, // send DNS requests to all upstream servers at once
}
### API: Set DNS general settings: POST /control/dns_config
* Added "parallel_requests", "upstream_dns", "bootstrap_dns" parameters
* removed /control/set_upstreams_config method
Request:
POST /control/dns_config
{
"upstream_dns": ["tls://...", ...],
"bootstrap_dns": ["1.2.3.4", ...],
"protection_enabled": true | false,
"ratelimit": 1234,
"blocking_mode": "default" | "nxdomain" | "null_ip" | "custom_ip",
"blocking_ipv4": "1.2.3.4",
"blocking_ipv6": "1:2:3::4",
"edns_cs_enabled": true | false,
"dnssec_enabled": true | false
"disable_ipv6": true | false,
"fastest_addr": true | false, // use Fastest Address algorithm
"parallel_requests": true | false, // send DNS requests to all upstream servers at once
}
Response:
200 OK
## v0.101: API changes
### API: Refresh filters: POST /control/filtering/refresh

View File

@ -0,0 +1,23 @@
FROM alpine:latest
LABEL maintainer="AdGuard Team <devteam@adguard.com>"
# Update CA certs
RUN apk --no-cache --update add ca-certificates libcap && \
rm -rf /var/cache/apk/* && \
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
chown -R nobody: /opt/adguardhome
COPY --chown=nobody:nogroup ./AdGuardHome /opt/adguardhome/AdGuardHome
RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 853/tcp 3000/tcp
VOLUME ["/opt/adguardhome/conf", "/opt/adguardhome/work"]
WORKDIR /opt/adguardhome/work
#USER nobody
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
CMD ["-h", "0.0.0.0", "-c", "/opt/adguardhome/conf/AdGuardHome.yaml", "-w", "/opt/adguardhome/work", "--no-check-update"]

View File

@ -0,0 +1,6 @@
## Docker images
* `Dockerfile` is used for local development. Build it using `make docker` command.
* `Dockerfile.hub` is used to publish AdGuard images to Docker Hub: https://hub.docker.com/r/adguard/adguardhome
Check out `build_docker.sh` for the details.

5
packaging/snap/README.md Normal file
View File

@ -0,0 +1,5 @@
## Snapcraft
Configuration for our snap.
Check out `build_snap.sh` for more details.

View File

@ -0,0 +1,32 @@
name: adguard-home
base: core18
version: 'dev_version'
summary: Network-wide ads & trackers blocking DNS server
description: |
AdGuard Home is a network-wide software for blocking ads & tracking. After
you set it up, it'll cover ALL your home devices, and you don't need any
client-side software for that.
It operates as a DNS server that re-routes tracking domains to a "black hole,"
thus preventing your devices from connecting to those servers. It's based
on software we use for our public AdGuard DNS servers -- both share a lot
of common code.
grade: stable
confinement: strict
parts:
adguard-home:
plugin: make
source: .
build-snaps: [ node/13/stable, go ]
build-packages: [ git, build-essential ]
override-build: |
make clean
make
cp AdGuardHome ${SNAPCRAFT_PART_INSTALL}/
apps:
adguard-home:
command: AdGuardHome -w ${SNAP_DATA} --no-check-update
plugs: [ network-bind ]
daemon: simple
restart-condition: always

View File

@ -238,7 +238,7 @@ func (l *queryLog) getData(params getDataParams) map[string]interface{} {
entries := append(memoryEntries, fileEntries...)
if len(entries) > getDataLimit {
// remove extra records
entries = entries[(len(entries) - getDataLimit):]
entries = entries[:getDataLimit]
}
if len(entries) == getDataLimit {
// change the "oldest" value here.

41
snapcraft.yaml Normal file
View File

@ -0,0 +1,41 @@
# Note that this snapcraft.yaml file is used for automatic Edge channel builds ONLY!
# We use packaging/snap/snapcraft.yaml for beta and release builds
# Check out build_snap.sh for more details
name: adguard-home
base: core18
version: 'edge'
summary: Network-wide ads & trackers blocking DNS server
description: |
AdGuard Home is a network-wide software for blocking ads & tracking. After
you set it up, it'll cover ALL your home devices, and you don't need any
client-side software for that.
It operates as a DNS server that re-routes tracking domains to a "black hole,"
thus preventing your devices from connecting to those servers. It's based
on software we use for our public AdGuard DNS servers -- both share a lot
of common code.
grade: stable
confinement: strict
architectures:
- build-on: amd64
- build-on: armhf
- build-on: i386
- build-on: arm64
parts:
adguard-home:
plugin: make
source: .
build-snaps: [ node/13/stable, go ]
build-packages: [ git, build-essential ]
override-build: |
make clean
make
cp AdGuardHome ${SNAPCRAFT_PART_INSTALL}/
apps:
adguard-home:
command: AdGuardHome -w ${SNAP_DATA} --no-check-update
plugs: [ network-bind ]
daemon: simple
restart-condition: always

352
util/pprof.go Normal file
View File

@ -0,0 +1,352 @@
// Modified pprof package
// The problem with the mainstream package is that it registers HTTP handlers
// inside init() function.
// This behaviour makes it impossible to enable pprof on demand in runtime.
// But this package has a separate function for this -
// PProfRegisterWebHandlers().
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package pprof serves via its HTTP server runtime profiling data
// in the format expected by the pprof visualization tool.
//
// The package is typically only imported for the side effect of
// registering its HTTP handlers.
// The handled paths all begin with /debug/pprof/.
//
// To use pprof, link this package into your program:
// import _ "net/http/pprof"
//
// If your application is not already running an http server, you
// need to start one. Add "net/http" and "log" to your imports and
// the following code to your main function:
//
// go func() {
// log.Println(http.ListenAndServe("localhost:6060", nil))
// }()
//
// If you are not using DefaultServeMux, you will have to register handlers
// with the mux you are using.
//
// Then use the pprof tool to look at the heap profile:
//
// go tool pprof http://localhost:6060/debug/pprof/heap
//
// Or to look at a 30-second CPU profile:
//
// go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
//
// Or to look at the goroutine blocking profile, after calling
// runtime.SetBlockProfileRate in your program:
//
// go tool pprof http://localhost:6060/debug/pprof/block
//
// Or to collect a 5-second execution trace:
//
// wget http://localhost:6060/debug/pprof/trace?seconds=5
//
// Or to look at the holders of contended mutexes, after calling
// runtime.SetMutexProfileFraction in your program:
//
// go tool pprof http://localhost:6060/debug/pprof/mutex
//
// To view all available profiles, open http://localhost:6060/debug/pprof/
// in your browser.
//
// For a study of the facility in action, visit
//
// https://blog.golang.org/2011/06/profiling-go-programs.html
//
package util
import (
"bufio"
"bytes"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"runtime"
"runtime/pprof"
"runtime/trace"
"sort"
"strconv"
"strings"
"time"
)
// PProfRegisterWebHandlers - register HTTP handlers for pprof
func PProfRegisterWebHandlers(mux *http.ServeMux) {
mux.HandleFunc("/debug/pprof/", Index)
mux.HandleFunc("/debug/pprof/cmdline", Cmdline)
mux.HandleFunc("/debug/pprof/profile", Profile)
mux.HandleFunc("/debug/pprof/symbol", Symbol)
mux.HandleFunc("/debug/pprof/trace", Trace)
}
// Cmdline responds with the running program's
// command line, with arguments separated by NUL bytes.
// The package initialization registers it as /debug/pprof/cmdline.
func Cmdline(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, strings.Join(os.Args, "\x00"))
}
func sleep(w http.ResponseWriter, d time.Duration) {
var clientGone <-chan bool
if cn, ok := w.(http.CloseNotifier); ok {
clientGone = cn.CloseNotify()
}
select {
case <-time.After(d):
case <-clientGone:
}
}
func durationExceedsWriteTimeout(r *http.Request, seconds float64) bool {
srv, ok := r.Context().Value(http.ServerContextKey).(*http.Server)
return ok && srv.WriteTimeout != 0 && seconds >= srv.WriteTimeout.Seconds()
}
func serveError(w http.ResponseWriter, status int, txt string) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Go-Pprof", "1")
w.Header().Del("Content-Disposition")
w.WriteHeader(status)
fmt.Fprintln(w, txt)
}
// Profile responds with the pprof-formatted cpu profile.
// Profiling lasts for duration specified in seconds GET parameter, or for 30 seconds if not specified.
// The package initialization registers it as /debug/pprof/profile.
func Profile(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
sec, err := strconv.ParseInt(r.FormValue("seconds"), 10, 64)
if sec <= 0 || err != nil {
sec = 30
}
if durationExceedsWriteTimeout(r, float64(sec)) {
serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout")
return
}
// Set Content Type assuming StartCPUProfile will work,
// because if it does it starts writing.
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", `attachment; filename="profile"`)
if err := pprof.StartCPUProfile(w); err != nil {
// StartCPUProfile failed, so no writes yet.
serveError(w, http.StatusInternalServerError,
fmt.Sprintf("Could not enable CPU profiling: %s", err))
return
}
sleep(w, time.Duration(sec)*time.Second)
pprof.StopCPUProfile()
}
// Trace responds with the execution trace in binary form.
// Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified.
// The package initialization registers it as /debug/pprof/trace.
func Trace(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
sec, err := strconv.ParseFloat(r.FormValue("seconds"), 64)
if sec <= 0 || err != nil {
sec = 1
}
if durationExceedsWriteTimeout(r, sec) {
serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout")
return
}
// Set Content Type assuming trace.Start will work,
// because if it does it starts writing.
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", `attachment; filename="trace"`)
if err := trace.Start(w); err != nil {
// trace.Start failed, so no writes yet.
serveError(w, http.StatusInternalServerError,
fmt.Sprintf("Could not enable tracing: %s", err))
return
}
sleep(w, time.Duration(sec*float64(time.Second)))
trace.Stop()
}
// Symbol looks up the program counters listed in the request,
// responding with a table mapping program counters to function names.
// The package initialization registers it as /debug/pprof/symbol.
func Symbol(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
// We have to read the whole POST body before
// writing any output. Buffer the output here.
var buf bytes.Buffer
// We don't know how many symbols we have, but we
// do have symbol information. Pprof only cares whether
// this number is 0 (no symbols available) or > 0.
fmt.Fprintf(&buf, "num_symbols: 1\n")
var b *bufio.Reader
if r.Method == "POST" {
b = bufio.NewReader(r.Body)
} else {
b = bufio.NewReader(strings.NewReader(r.URL.RawQuery))
}
for {
word, err := b.ReadSlice('+')
if err == nil {
word = word[0 : len(word)-1] // trim +
}
pc, _ := strconv.ParseUint(string(word), 0, 64)
if pc != 0 {
f := runtime.FuncForPC(uintptr(pc))
if f != nil {
fmt.Fprintf(&buf, "%#x %s\n", pc, f.Name())
}
}
// Wait until here to check for err; the last
// symbol will have an err because it doesn't end in +.
if err != nil {
if err != io.EOF {
fmt.Fprintf(&buf, "reading request: %v\n", err)
}
break
}
}
w.Write(buf.Bytes())
}
// Handler returns an HTTP handler that serves the named profile.
func Handler(name string) http.Handler {
return handler(name)
}
type handler string
func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
p := pprof.Lookup(string(name))
if p == nil {
serveError(w, http.StatusNotFound, "Unknown profile")
return
}
gc, _ := strconv.Atoi(r.FormValue("gc"))
if name == "heap" && gc > 0 {
runtime.GC()
}
debug, _ := strconv.Atoi(r.FormValue("debug"))
if debug != 0 {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
} else {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
}
p.WriteTo(w, debug)
}
var profileDescriptions = map[string]string{
"allocs": "A sampling of all past memory allocations",
"block": "Stack traces that led to blocking on synchronization primitives",
"cmdline": "The command line invocation of the current program",
"goroutine": "Stack traces of all current goroutines",
"heap": "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.",
"mutex": "Stack traces of holders of contended mutexes",
"profile": "CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.",
"threadcreate": "Stack traces that led to the creation of new OS threads",
"trace": "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.",
}
// Index responds with the pprof-formatted profile named by the request.
// For example, "/debug/pprof/heap" serves the "heap" profile.
// Index responds to a request for "/debug/pprof/" with an HTML page
// listing the available profiles.
func Index(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/debug/pprof/") {
name := strings.TrimPrefix(r.URL.Path, "/debug/pprof/")
if name != "" {
handler(name).ServeHTTP(w, r)
return
}
}
type profile struct {
Name string
Href string
Desc string
Count int
}
var profiles []profile
for _, p := range pprof.Profiles() {
profiles = append(profiles, profile{
Name: p.Name(),
Href: p.Name() + "?debug=1",
Desc: profileDescriptions[p.Name()],
Count: p.Count(),
})
}
// Adding other profiles exposed from within this package
for _, p := range []string{"cmdline", "profile", "trace"} {
profiles = append(profiles, profile{
Name: p,
Href: p,
Desc: profileDescriptions[p],
})
}
sort.Slice(profiles, func(i, j int) bool {
return profiles[i].Name < profiles[j].Name
})
if err := indexTmpl.Execute(w, profiles); err != nil {
log.Print(err)
}
}
var indexTmpl = template.Must(template.New("index").Parse(`<html>
<head>
<title>/debug/pprof/</title>
<style>
.profile-name{
display:inline-block;
width:6rem;
}
</style>
</head>
<body>
/debug/pprof/<br>
<br>
Types of profiles available:
<table>
<thead><td>Count</td><td>Profile</td></thead>
{{range .}}
<tr>
<td>{{.Count}}</td><td><a href={{.Href}}>{{.Name}}</a></td>
</tr>
{{end}}
</table>
<a href="goroutine?debug=2">full goroutine stack dump</a>
<br/>
<p>
Profile Descriptions:
<ul>
{{range .}}
<li><div class=profile-name>{{.Name}}:</div> {{.Desc}}</li>
{{end}}
</ul>
</p>
</body>
</html>
`))