Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import i18next from 'i18next';
import { captitalizeWords } from '../../../../../helpers/helpers';
import { ClientForm } from '../types';
import { Checkbox } from '../../../../ui/Controls/Checkbox';
import { Radio } from '../../../../ui/Controls/Radio';
import { Input } from '../../../../ui/Controls/Input';
import { BLOCKING_MODES } from '../../../../../helpers/constants';
import { validateIpv4, validateIpv6, validateRequiredValue } from '../../../../../helpers/validators';


type ProtectionSettings = 'use_global_settings' | 'filtering_enabled' | 'safebrowsing_enabled' | 'parental_enabled';

Expand Down Expand Up @@ -43,14 +48,60 @@ const logAndStatsCheckboxes: { name: LogsStatsSettings; placeholder: string }[]
},
];

const customIps: {
name: 'blocking_ipv4' | 'blocking_ipv6';
label: string;
description: string;
validateIp: (value: string) => string;
}[] = [
{
name: 'blocking_ipv4',
label: i18next.t('blocking_ipv4'),
description: i18next.t('blocking_ipv4_desc'),
validateIp: validateIpv4,
},
{
name: 'blocking_ipv6',
label: i18next.t('blocking_ipv6'),
description: i18next.t('blocking_ipv6_desc'),
validateIp: validateIpv6,
},
];

const blockingModeOptions = [
{
value: BLOCKING_MODES.default,
label: i18next.t('default'),
},
{
value: BLOCKING_MODES.refused,
label: i18next.t('refused'),
},
{
value: BLOCKING_MODES.nxdomain,
label: i18next.t('nxdomain'),
},
{
value: BLOCKING_MODES.null_ip,
label: i18next.t('null_ip'),
},
{
value: BLOCKING_MODES.custom_ip,
label: i18next.t('custom_ip'),
},
];

type Props = {
safeSearchServices: Record<string, boolean>;
processingAdding: boolean;
processingUpdating: boolean;
};

export const MainSettings = ({ safeSearchServices }: Props) => {
export const MainSettings = ({ processingAdding, processingUpdating, safeSearchServices }: Props) => {
const { t } = useTranslation();
const { watch, control } = useFormContext<ClientForm>();

const blockingMode = watch('blocking_mode');
const useGlobalSettings = watch('use_global_settings');

return (
Expand Down Expand Up @@ -107,6 +158,53 @@ export const MainSettings = ({ safeSearchServices }: Props) => {
))}
</div>

<div className="form__group">
<label className="form__label--bold form__label--top form__label--with-desc">{t('blocking_mode')}</label>

<div className="custom-controls-stacked">
<Controller
name="blocking_mode"
control={control}
render={({ field }) => (
<Radio {...field}
options={blockingModeOptions}
disabled={processingAdding || processingUpdating} />
)}
/>
</div>
</div>
{blockingMode === BLOCKING_MODES.custom_ip && (
<>
{customIps.map(({ label, description, name, validateIp }) => (
<div className="col-12 col-sm-6" key={name}>
<div className="form__group">
<Controller
name={name}
control={control}
rules={{
validate: {
required: validateRequiredValue,
ip: validateIp,
},
}}
render={({ field, fieldState }) => (
<Input
{...field}
data-testid="dns_config_blocked_response_ttl"
type="text"
label={label}
desc={description}
error={fieldState.error?.message}
disabled={processingAdding || processingUpdating}
/>
)}
/>
</div>
</div>
))}
</>
)}

<div className="form__label--bold form__label--top form__label--bot">
{t('log_and_stats_section_label')}
</div>
Expand Down
10 changes: 8 additions & 2 deletions client/src/components/Settings/Clients/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Controller, FormProvider, useForm } from 'react-hook-form';
import Select from 'react-select';

import Tabs from '../../../ui/Tabs';
import { CLIENT_ID_LINK, LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
import { CLIENT_ID_LINK, LOCAL_TIMEZONE_VALUE, BLOCKING_MODES } from '../../../../helpers/constants';
import { RootState } from '../../../../initialState';
import { Input } from '../../../ui/Controls/Input';
import { validateRequiredValue } from '../../../../helpers/validators';
Expand Down Expand Up @@ -33,6 +33,9 @@ const defaultFormValues: ClientForm = {
blocked_services_schedule: {
time_zone: LOCAL_TIMEZONE_VALUE,
},
blocking_mode: BLOCKING_MODES.default,
blocking_ipv4: '',
blocking_ipv6: '',
};

type Props = {
Expand Down Expand Up @@ -83,7 +86,10 @@ export const Form = ({
const tabs = {
settings: {
title: 'settings',
component: <MainSettings safeSearchServices={safeSearchServices} />,
component: <MainSettings
processingAdding={processingAdding}
processingUpdating={processingUpdating}
safeSearchServices={safeSearchServices} />,
},
block_services: {
title: 'block_services',
Expand Down
3 changes: 3 additions & 0 deletions client/src/components/Settings/Clients/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export type ClientForm = {
parental_enabled: boolean;
ignore_querylog: boolean;
ignore_statistics: boolean;
blocking_mode: string;
blocking_ipv4: string;
blocking_ipv6: string;
};

export type SubmitClientForm = Omit<ClientForm, 'ids' | 'tags'> & {
Expand Down
38 changes: 38 additions & 0 deletions internal/client/persistent.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ type Persistent struct {
// must not be nil after initialization.
BlockedServices *filtering.BlockedServices

// BlockingMode is the blocking mode override for a client
BlockingMode filtering.BlockingMode

// BlockingIPv4 is the IP address to be returned for a blocked A request.
BlockingIPv4 netip.Addr

// BlockingIPv6 is the IP address to be returned for a blocked AAAA request.
BlockingIPv6 netip.Addr

// Name of the persistent client. Must not be empty.
Name string

Expand Down Expand Up @@ -140,6 +149,10 @@ func (c *Persistent) validate(ctx context.Context, l *slog.Logger, allTags []str
return errors.Error("uid required")
}

if e := c.validateBlockingMode(); e != nil {
return e
}

conf, err := proxy.ParseUpstreamsConfig(c.Upstreams, &upstream.Options{})
if err != nil {
return fmt.Errorf("invalid upstream servers: %w", err)
Expand All @@ -163,6 +176,27 @@ func (c *Persistent) validate(ctx context.Context, l *slog.Logger, allTags []str
return nil
}

func (c *Persistent) validateBlockingMode() error {
switch c.BlockingMode {
case
filtering.BlockingModeDefault,
filtering.BlockingModeNXDOMAIN,
filtering.BlockingModeREFUSED,
filtering.BlockingModeNullIP:
return nil
case filtering.BlockingModeCustomIP:
if !c.BlockingIPv4.Is4() {
return fmt.Errorf("blocking_ipv4 must be valid ipv4 on custom_ip blocking_mode")
}
if !c.BlockingIPv6.Is6() {
return fmt.Errorf("blocking_ipv6 must be valid ipv6 on custom_ip blocking_mode")
}
}

// Assumed to default to the default blocking mode
return nil
}

// SetIDs parses a list of strings into typed fields and returns an error if
// there is one.
func (c *Persistent) SetIDs(ids []string) (err error) {
Expand Down Expand Up @@ -295,6 +329,10 @@ func (c *Persistent) ShallowClone() (clone *Persistent) {
clone = &Persistent{}
*clone = *c

clone.BlockingMode = c.BlockingMode
clone.BlockingIPv4 = c.BlockingIPv4
clone.BlockingIPv6 = c.BlockingIPv6

clone.BlockedServices = c.BlockedServices.Clone()
clone.Tags = slices.Clone(c.Tags)
clone.Upstreams = slices.Clone(c.Upstreams)
Expand Down
3 changes: 3 additions & 0 deletions internal/client/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,4 +705,7 @@ func (s *Storage) ApplyClientFiltering(id string, addr netip.Addr, setts *filter
setts.ClientSafeSearch = c.SafeSearch
setts.SafeBrowsingEnabled = c.SafeBrowsingEnabled
setts.ParentalEnabled = c.ParentalEnabled
setts.BlockingMode = c.BlockingMode
setts.BlockingIPv4 = c.BlockingIPv4
setts.BlockingIPv6 = c.BlockingIPv6
}
4 changes: 2 additions & 2 deletions internal/dnsforward/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (s *Server) filterDNSRequest(dctx *dnsContext) (res *filtering.Result, err
req.Question[0].Name = dns.Fqdn(res.CanonName)
case res.IsFiltered:
log.Debug("dnsforward: host %q is filtered, reason: %q", host, res.Reason)
pctx.Res = s.genDNSFilterMessage(pctx, res)
pctx.Res = s.genDNSFilterMessage(dctx, pctx, res)
case res.Reason.In(filtering.Rewritten, filtering.FilteredSafeSearch):
pctx.Res = s.getCNAMEWithIPs(req, res.IPList, res.CanonName)
case res.Reason.In(filtering.RewrittenRule, filtering.RewrittenAutoHosts):
Expand Down Expand Up @@ -130,7 +130,7 @@ func (s *Server) filterDNSResponse(dctx *dnsContext) (err error) {
} else if res != nil && res.IsFiltered {
dctx.result = res
dctx.origResp = pctx.Res
pctx.Res = s.genDNSFilterMessage(pctx, res)
pctx.Res = s.genDNSFilterMessage(dctx, pctx, res)

log.Debug("dnsforward: matched %q by response: %q", pctx.Req.Question[0].Name, host)

Expand Down
17 changes: 9 additions & 8 deletions internal/dnsforward/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ func ipsFromRules(resRules []*filtering.ResultRule) (ips []netip.Addr) {
// genDNSFilterMessage generates a filtered response to req for the filtering
// result res.
func (s *Server) genDNSFilterMessage(
dctx *proxy.DNSContext,
dctx *dnsContext,
pctx *proxy.DNSContext,
res *filtering.Result,
) (resp *dns.Msg) {
req := dctx.Req
req := pctx.Req
qt := req.Question[0].Qtype
if qt != dns.TypeA && qt != dns.TypeAAAA && qt != dns.TypeHTTPS {
m, _, _ := s.dnsFilter.BlockingMode()
m, _, _ := s.blockingMode(dctx)
if m == filtering.BlockingModeNullIP {
return s.replyCompressed(req)
}
Expand All @@ -63,16 +64,16 @@ func (s *Server) genDNSFilterMessage(

switch res.Reason {
case filtering.FilteredSafeBrowsing:
return s.genBlockedHost(req, s.dnsFilter.SafeBrowsingBlockHost(), dctx)
return s.genBlockedHost(req, s.dnsFilter.SafeBrowsingBlockHost(), pctx)
case filtering.FilteredParental:
return s.genBlockedHost(req, s.dnsFilter.ParentalBlockHost(), dctx)
return s.genBlockedHost(req, s.dnsFilter.ParentalBlockHost(), pctx)
case filtering.FilteredSafeSearch:
// If Safe Search generated the necessary IP addresses, use them.
// Otherwise, if there were no errors, there are no addresses for the
// requested IP version, so produce a NODATA response.
return s.getCNAMEWithIPs(req, ipsFromRules(res.Rules), res.CanonName)
default:
return s.genForBlockingMode(req, ipsFromRules(res.Rules))
return s.genForBlockingMode(dctx, req, ipsFromRules(res.Rules))
}
}

Expand Down Expand Up @@ -112,8 +113,8 @@ func (s *Server) getCNAMEWithIPs(req *dns.Msg, ips []netip.Addr, cname string) (

// genForBlockingMode generates a filtered response to req based on the server's
// blocking mode.
func (s *Server) genForBlockingMode(req *dns.Msg, ips []netip.Addr) (resp *dns.Msg) {
switch mode, bIPv4, bIPv6 := s.dnsFilter.BlockingMode(); mode {
func (s *Server) genForBlockingMode(dctx *dnsContext, req *dns.Msg, ips []netip.Addr) (resp *dns.Msg) {
switch mode, bIPv4, bIPv6 := s.blockingMode(dctx); mode {
case filtering.BlockingModeCustomIP:
return s.makeResponseCustomIP(req, bIPv4, bIPv6)
case filtering.BlockingModeDefault:
Expand Down
5 changes: 5 additions & 0 deletions internal/dnsforward/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,11 @@ func (s *Server) processFilteringAfterResponse(dctx *dnsContext) (rc resultCode)
}
}

// blockingMode fetches the blocking mode for the specific client from the context
func (s *Server) blockingMode(dctx *dnsContext) (filtering.BlockingMode, netip.Addr, netip.Addr) {
return dctx.setts.BlockingMode, dctx.setts.BlockingIPv4, dctx.setts.BlockingIPv6
}

// filterAfterResponse returns the result of filtering the response that wasn't
// explicitly allowed or rewritten.
func (s *Server) filterAfterResponse(dctx *dnsContext) (res resultCode) {
Expand Down
1 change: 1 addition & 0 deletions internal/filtering/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ func (d *DNSFilter) ApplyAdditionalFiltering(cliAddr netip.Addr, clientID string

d.ApplyBlockedServices(setts)
d.applyClientFiltering(clientID, cliAddr, setts)

if setts.BlockedServices != nil {
// TODO(e.burkov): Get rid of this crutch.
setts.ServicesRules = nil
Expand Down
7 changes: 7 additions & 0 deletions internal/filtering/filtering.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ type Settings struct {
// is nil if the client does not have any blocked services.
BlockedServices *BlockedServices

BlockingMode BlockingMode
BlockingIPv4 netip.Addr
BlockingIPv6 netip.Addr

ProtectionEnabled bool
FilteringEnabled bool
SafeSearchEnabled bool
Expand Down Expand Up @@ -387,6 +391,9 @@ func (d *DNSFilter) Settings() (s *Settings) {
SafeSearchEnabled: d.conf.SafeSearchConf.Enabled,
SafeBrowsingEnabled: d.conf.SafeBrowsingEnabled,
ParentalEnabled: d.conf.ParentalEnabled,
BlockingMode: d.conf.BlockingMode,
BlockingIPv4: d.conf.BlockingIPv4,
BlockingIPv6: d.conf.BlockingIPv6,
}
}

Expand Down
12 changes: 12 additions & 0 deletions internal/home/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ type clientObject struct {
Tags []string `yaml:"tags"`
Upstreams []string `yaml:"upstreams"`

BlockingMode filtering.BlockingMode `yaml:"blocking_mode"`
BlockingIPv4 netip.Addr `yaml:"blocking_ipv4"`
BlockingIPv6 netip.Addr `yaml:"blocking_ipv6"`

// UID is the unique identifier of the persistent client.
UID client.UID `yaml:"uid"`

Expand Down Expand Up @@ -194,6 +198,10 @@ func (o *clientObject) toPersistent(

UID: o.UID,

BlockingMode: o.BlockingMode,
BlockingIPv4: o.BlockingIPv4,
BlockingIPv6: o.BlockingIPv6,

UseOwnSettings: !o.UseGlobalSettings,
FilteringEnabled: o.FilteringEnabled,
ParentalEnabled: o.ParentalEnabled,
Expand Down Expand Up @@ -273,6 +281,10 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
Tags: slices.Clone(cli.Tags),
Upstreams: slices.Clone(cli.Upstreams),

BlockingMode: cli.BlockingMode,
BlockingIPv4: cli.BlockingIPv4,
BlockingIPv6: cli.BlockingIPv6,

UID: cli.UID,

UseGlobalSettings: !cli.UseOwnSettings,
Expand Down
Loading
Loading