Merge: + Clients: use per-client DNS servers

Close #821

* commit '6684a120ac9909140e4ecbe674e09c8f24e87ac4':
  + client: handle upstream DNS servers for clients
  * openapi: add "upstreams" to Client struct
  + use per-client DNS servers
This commit is contained in:
Simon Zolin 2019-12-05 13:22:50 +03:00
commit 9fd27b9add
11 changed files with 110 additions and 15 deletions

View File

@ -669,6 +669,7 @@ Response:
key: "value" key: "value"
... ...
} }
upstreams: ["upstream1", ...]
} }
] ]
auto_clients: [ auto_clients: [
@ -703,6 +704,7 @@ Request:
safesearch_enabled: false safesearch_enabled: false
use_global_blocked_services: true use_global_blocked_services: true
blocked_services: [ "name1", ... ] blocked_services: [ "name1", ... ]
upstreams: ["upstream1", ...]
} }
Response: Response:
@ -732,6 +734,7 @@ Request:
safesearch_enabled: false safesearch_enabled: false
use_global_blocked_services: true use_global_blocked_services: true
blocked_services: [ "name1", ... ] blocked_services: [ "name1", ... ]
upstreams: ["upstream1", ...]
} }
} }

View File

@ -108,6 +108,7 @@
"upstream_dns": "Upstream DNS servers", "upstream_dns": "Upstream DNS servers",
"upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https://www.quad9.net/' target='_blank'>Quad9</a> as an upstream.", "upstream_dns_hint": "If you keep this field empty, AdGuard Home will use <a href='https://www.quad9.net/' target='_blank'>Quad9</a> as an upstream.",
"test_upstream_btn": "Test upstreams", "test_upstream_btn": "Test upstreams",
"upstreams": "Upstreams",
"apply_btn": "Apply", "apply_btn": "Apply",
"disabled_filtering_toast": "Disabled filtering", "disabled_filtering_toast": "Disabled filtering",
"enabled_filtering_toast": "Enabled filtering", "enabled_filtering_toast": "Enabled filtering",

View File

@ -4,6 +4,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { MODAL_TYPE } from '../../../helpers/constants'; import { MODAL_TYPE } from '../../../helpers/constants';
import { normalizeTextarea } from '../../../helpers/helpers';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import Modal from './Modal'; import Modal from './Modal';
import WrapCell from './WrapCell'; import WrapCell from './WrapCell';
@ -20,13 +21,20 @@ class ClientsTable extends Component {
}; };
handleSubmit = (values) => { handleSubmit = (values) => {
let config = values; const config = values;
if (values && values.blocked_services) { if (values) {
const blocked_services = Object if (values.blocked_services) {
.keys(values.blocked_services) config.blocked_services = Object
.filter(service => values.blocked_services[service]); .keys(values.blocked_services)
config = { ...values, blocked_services }; .filter(service => values.blocked_services[service]);
}
if (values.upstreams && typeof values.upstreams === 'string') {
config.upstreams = normalizeTextarea(values.upstreams);
} else {
config.upstreams = [];
}
} }
if (this.props.modalType === MODAL_TYPE.EDIT) { if (this.props.modalType === MODAL_TYPE.EDIT) {
@ -40,10 +48,10 @@ class ClientsTable extends Component {
const client = clients.find(item => name === item.name); const client = clients.find(item => name === item.name);
if (client) { if (client) {
const { upstreams, whois_info, ...values } = client;
return { return {
use_global_settings: true, upstreams: (upstreams && upstreams.join('\n')) || '',
use_global_blocked_services: true, ...values,
...client,
}; };
} }
@ -143,6 +151,24 @@ class ClientsTable extends Component {
); );
}, },
}, },
{
Header: this.props.t('upstreams'),
accessor: 'upstreams',
minWidth: 120,
Cell: ({ value }) => {
const title = value && value.length > 0 ? (
<Trans>settings_custom</Trans>
) : (
<Trans>settings_global</Trans>
);
return (
<div className="logs__row logs__row--overflow">
<div className="logs__text">{title}</div>
</div>
);
},
},
{ {
Header: this.props.t('whois'), Header: this.props.t('whois'),
accessor: 'whois_info', accessor: 'whois_info',

View File

@ -7,6 +7,7 @@ import flow from 'lodash/flow';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs'; import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples';
import { toggleAllServices } from '../../../helpers/helpers'; import { toggleAllServices } from '../../../helpers/helpers';
import { import {
renderField, renderField,
@ -223,6 +224,17 @@ let Form = (props) => {
</div> </div>
</div> </div>
</div> </div>
<div label="upstream" title={props.t('upstream_dns')}>
<Field
id="upstreams"
name="upstreams"
component="textarea"
type="text"
className="form-control form-control--textarea mb-5"
placeholder={t('upstream_dns')}
/>
<Examples />
</div>
</Tabs> </Tabs>
</div> </div>

View File

@ -94,6 +94,9 @@ type FilteringConfig struct {
// Filtering callback function // Filtering callback function
FilterHandler func(clientAddr string, settings *dnsfilter.RequestFilteringSettings) `yaml:"-"` FilterHandler func(clientAddr string, settings *dnsfilter.RequestFilteringSettings) `yaml:"-"`
// This callback function returns the list of upstream servers for a client specified by IP address
GetUpstreamsByClient func(clientAddr string) []string `yaml:"-"`
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests BlockingMode string `yaml:"blocking_mode"` // mode how to answer filtered requests
@ -393,6 +396,19 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
d.Req.Question[0].Name = dns.Fqdn(res.CanonName) d.Req.Question[0].Name = dns.Fqdn(res.CanonName)
} }
if d.Addr != nil && s.conf.GetUpstreamsByClient != nil {
clientIP, _, _ := net.SplitHostPort(d.Addr.String())
upstreams := s.conf.GetUpstreamsByClient(clientIP)
for _, us := range upstreams {
u, err := upstream.AddressToUpstream(us, upstream.Options{Timeout: 30 * time.Second})
if err != nil {
log.Error("upstream.AddressToUpstream: %s: %s", us, err)
continue
}
d.Upstreams = append(d.Upstreams, u)
}
}
// request was not filtered so let it be processed further // request was not filtered so let it be processed further
err = p.Resolve(d) err = p.Resolve(d)
if err != nil { if err != nil {

View File

@ -44,7 +44,7 @@ func (s *Server) handleSetUpstreamConfig(w http.ResponseWriter, r *http.Request)
return return
} }
err = validateUpstreams(req.Upstreams) err = ValidateUpstreams(req.Upstreams)
if err != nil { if err != nil {
httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err) httpError(r, w, http.StatusBadRequest, "wrong upstreams specification: %s", err)
return return
@ -78,8 +78,8 @@ func (s *Server) handleSetUpstreamConfig(w http.ResponseWriter, r *http.Request)
} }
} }
// validateUpstreams validates each upstream and returns an error if any upstream is invalid or if there are no default upstreams specified // ValidateUpstreams validates each upstream and returns an error if any upstream is invalid or if there are no default upstreams specified
func validateUpstreams(upstreams []string) error { func ValidateUpstreams(upstreams []string) error {
var defaultUpstreamFound bool var defaultUpstreamFound bool
for _, u := range upstreams { for _, u := range upstreams {
d, err := validateUpstream(u) d, err := validateUpstream(u)

View File

@ -762,21 +762,21 @@ func TestValidateUpstreamsSet(t *testing.T) {
"[/host.com/google.com/]8.8.8.8", "[/host.com/google.com/]8.8.8.8",
"[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", "[/host/]sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
} }
err := validateUpstreams(upstreamsSet) err := ValidateUpstreams(upstreamsSet)
if err == nil { if err == nil {
t.Fatalf("there is no default upstream") t.Fatalf("there is no default upstream")
} }
// Let's add default upstream // Let's add default upstream
upstreamsSet = append(upstreamsSet, "8.8.8.8") upstreamsSet = append(upstreamsSet, "8.8.8.8")
err = validateUpstreams(upstreamsSet) err = ValidateUpstreams(upstreamsSet)
if err != nil { if err != nil {
t.Fatalf("upstreams set is valid, but doesn't pass through validation cause: %s", err) t.Fatalf("upstreams set is valid, but doesn't pass through validation cause: %s", err)
} }
// Let's add invalid upstream // Let's add invalid upstream
upstreamsSet = append(upstreamsSet, "dhcp://fake.dns") upstreamsSet = append(upstreamsSet, "dhcp://fake.dns")
err = validateUpstreams(upstreamsSet) err = ValidateUpstreams(upstreamsSet)
if err == nil { if err == nil {
t.Fatalf("there is an invalid upstream in set, but it pass through validation") t.Fatalf("there is an invalid upstream in set, but it pass through validation")
} }

View File

@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/utils" "github.com/AdguardTeam/golibs/utils"
) )
@ -34,6 +35,8 @@ type Client struct {
UseOwnBlockedServices bool // false: use global settings UseOwnBlockedServices bool // false: use global settings
BlockedServices []string BlockedServices []string
Upstreams []string // list of upstream servers to be used for the client's requests
} }
type clientSource uint type clientSource uint
@ -96,6 +99,8 @@ type clientObject struct {
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"` UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
BlockedServices []string `yaml:"blocked_services"` BlockedServices []string `yaml:"blocked_services"`
Upstreams []string `yaml:"upstreams"`
} }
func (clients *clientsContainer) addFromConfig(objects []clientObject) { func (clients *clientsContainer) addFromConfig(objects []clientObject) {
@ -111,6 +116,8 @@ func (clients *clientsContainer) addFromConfig(objects []clientObject) {
UseOwnBlockedServices: !cy.UseGlobalBlockedServices, UseOwnBlockedServices: !cy.UseGlobalBlockedServices,
BlockedServices: cy.BlockedServices, BlockedServices: cy.BlockedServices,
Upstreams: cy.Upstreams,
} }
_, err := clients.Add(cli) _, err := clients.Add(cli)
if err != nil { if err != nil {
@ -134,6 +141,8 @@ func (clients *clientsContainer) WriteDiskConfig(objects *[]clientObject) {
UseGlobalBlockedServices: !cli.UseOwnBlockedServices, UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
BlockedServices: cli.BlockedServices, BlockedServices: cli.BlockedServices,
Upstreams: cli.Upstreams,
} }
*objects = append(*objects, cy) *objects = append(*objects, cy)
} }
@ -268,6 +277,14 @@ func (c *Client) check() error {
return fmt.Errorf("Invalid ID: %s", id) return fmt.Errorf("Invalid ID: %s", id)
} }
if len(c.Upstreams) != 0 {
err := dnsforward.ValidateUpstreams(c.Upstreams)
if err != nil {
return fmt.Errorf("Invalid upstream servers: %s", err)
}
}
return nil return nil
} }

View File

@ -20,6 +20,8 @@ type clientJSON struct {
UseGlobalBlockedServices bool `json:"use_global_blocked_services"` UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
BlockedServices []string `json:"blocked_services"` BlockedServices []string `json:"blocked_services"`
Upstreams []string `json:"upstreams"`
} }
type clientHostJSON struct { type clientHostJSON struct {
@ -92,6 +94,8 @@ func jsonToClient(cj clientJSON) (*Client, error) {
UseOwnBlockedServices: !cj.UseGlobalBlockedServices, UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
BlockedServices: cj.BlockedServices, BlockedServices: cj.BlockedServices,
Upstreams: cj.Upstreams,
} }
return &c, nil return &c, nil
} }
@ -109,6 +113,8 @@ func clientToJSON(c *Client) clientJSON {
UseGlobalBlockedServices: !c.UseOwnBlockedServices, UseGlobalBlockedServices: !c.UseOwnBlockedServices,
BlockedServices: c.BlockedServices, BlockedServices: c.BlockedServices,
Upstreams: c.Upstreams,
} }
cj.WhoisInfo = make(map[string]interface{}) cj.WhoisInfo = make(map[string]interface{})

View File

@ -170,9 +170,19 @@ func generateServerConfig() (dnsforward.ServerConfig, error) {
} }
newconfig.FilterHandler = applyAdditionalFiltering newconfig.FilterHandler = applyAdditionalFiltering
newconfig.GetUpstreamsByClient = getUpstreamsByClient
return newconfig, nil return newconfig, nil
} }
func getUpstreamsByClient(clientAddr string) []string {
c, ok := config.clients.Find(clientAddr)
if !ok {
return []string{}
}
log.Debug("Using upstreams %v for client %s (IP: %s)", c.Upstreams, c.Name, clientAddr)
return c.Upstreams
}
// If a client has his own settings, apply them // If a client has his own settings, apply them
func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) { func applyAdditionalFiltering(clientAddr string, setts *dnsfilter.RequestFilteringSettings) {

View File

@ -1649,6 +1649,10 @@ definitions:
type: "array" type: "array"
items: items:
type: "string" type: "string"
upstreams:
type: "array"
items:
type: "string"
ClientAuto: ClientAuto:
type: "object" type: "object"
description: "Auto-Client information" description: "Auto-Client information"