SafeDNS Shield
- Overview
- Architecture and deployment
- Setup and requirements
- Miscellaneous
- REST API overview
- REST API reference
Overview
SafeDNS Shield is an on-premises web filtering and security solution that operates as a DNS proxy. It processes every DNS query to identify the requesting user, evaluates the queried domain against that user’s filtering policy, and then allows or blocks the request.
When a domain is blocked, the proxy resolves it to the IP address of a block page instead of returning the actual IP. The block page can be a custom corporate page hosted outside the solution, or the default block page built into SafeDNS Shield, which can also be customized.
If a domain is allowed, the proxy forwards the DNS request to the next server in the resolution chain. This server can be a local corporate DNS server, an ISP’s caching DNS resolver, or any public DNS service (for example, Google Public DNS or Cloudflare).
Because SafeDNS Shield inspects all DNS traffic, it enables comprehensive, per‑user traffic analysis. Every request is logged, providing access to detailed statistical information.
Architecture and deployment
Components
SafeDNS Shield is composed of the following components:
-
DNS Proxy Module
The core filtering engine. It receives DNS requests from end users, identifies the requesting user, applies the configured filtering policy, and returns either the resolved IP address or the IP address of a block page. -
Internal Database
Stores all configuration and policy data: user identifiers (subnet, IP, port), filtering profiles, block pages, user groups, and their assignments. - Block pages
HTTP/S pages served to the users instead of blocked websites. Can be hosted alongside Shield or on an external server. -
REST API
Provides a management interface for administrators to update the Internal Database. It supports creation and modification of:-
User identifiers (subnet, IP, port)
-
Filtering profiles (categories to block)
-
Block pages
-
-
Binary Log Parsing Module (StatsLoader)
Processes the binary log files generated by the DNS Proxy. It parses the DNS query logs, extracts statistics, and sends them to the ClickHouse cluster for storage and analysis. -
ClickHouse Cluster
A distributed database for storing and analyzing DNS request statistics. The cluster is divided into shards, each containing multiple mirrored nodes for fault tolerance and high‑performance parallel reads and writes.-
Load Balancer
Receives statistics data from StatsLoader and distributes it evenly across the ClickHouse cluster nodes. -
ZooKeeper
Manages coordination and configuration of the ClickHouse cluster, ensuring data consistency and system reliability.
-
The DNS Proxy writes its binary query logs to a designated host path (HostPATH), from which StatsLoader reads them.
External dependencies
The following elements are not part of SafeDNS Shield but are required for operation:
-
User – The end‑user device that sends DNS requests to SafeDNS Shield.
-
Caching DNS Server – A recursive resolver deployed on the organization’s network that performs upstream DNS resolution for allowed queries.
Deployment options
SafeDNS Shield supports multiple deployment options to accommodate different network topologies. This section describes the most common scenarios.
For ISPs
This deployment option is used in ISP networks, where NAT separates end users from the on-premises DNS infrastructure, making it impossible to identify them solely by their individual IP addresses.
For corporate clients
or
This deployment option is used in corporate networks where end users can be identified by their individual IP addresses at the point where SafeDNS Shield is deployed. Depending on whether the organization has its own caching DNS server, requests are forwarded to that server or to an external resolver, such as an ISP’s DNS or a public DNS service (e.g., 1.1.1.1 or 8.8.8.8).
User Identification
User Identification
To apply filtering policies and to separate statistics on a per‑user basis, SafeDNS Shield must identify each end user. Identification is based on the source address of the DNS request and can be configured using one of the following methods:
-
IP address – Use when each user has a unique IP address.
-
IP subnet – Use when per‑user granularity is not required and all users in a subnet can share the same policy.
-
IP:port – Use when multiple users share a single IP address (for example, behind NAT44 or CGNAT). The source port distinguishes individual users.
-
IP:port range – Use when users can be identified by a range of source ports on a shared IP address.
The appropriate method depends on the network topology and the level of user separation required.
Setup and requirements
Product setup and support
Custom Local Deployment
Setup, maintenance, and support for an on-premises deployment are managed entirely by SafeDNS specialists, establishing a clear division of responsibilities. The client provides the required hardware and full remote access, after which SafeDNS performs the complete turnkey installation.
Following deployment, SafeDNS specialists work with the client to configure the initial filtering rules and provide training to enable the client’s staff to make future adjustments independently. A dedicated support line is available for clients using on-premises solutions.
VM Deployment via ISO Image
As an alternative to a physical server installation, SafeDNS Shield can be deployed as a virtual appliance by mounting the provided ISO image in a virtual machine. This method streamlines deployment in environments that rely on virtualization infrastructure.
System requirements
The following specifications apply to a server running the SafeDNS Shield components. All deployments require Debian 12 (x86‑64).
DNS Proxy Module
Choose a configuration based on the expected peak query load.
CPU: Intel 12th‑generation or later, Intel Xeon Silver/Gold, or equivalent AMD Ryzen/Epyc.
| Queries per second (QPS) | CPU Cores | RAM | Storage | Network |
|---|---|---|---|---|
| Up to 1,000 | 4 | 8 GB | 200 GB NVMe | 1 Gbps |
| Up to 15,000 | 12 | 16 GB | 512 GB SSD (RAID 1) | 1 Gbps |
| Up to 310,000 | 64 | 128 GB | 2 TB SSD/NVMe (RAID 1) | 1 Gbps |
| Up to 2,000,000 | 128 | 2 TB | 16 TB SSD/NVMe (RAID 1) | 25 Gbps |
ClickHouse Cluster (Statistics Storage)
The ClickHouse cluster stores and analyzes DNS request logs. A single node meets the following minimum:
- CPU: 6 cores (x86‑64)
- RAM: 16 GB
- Storage: 500 GB NVMe
- Expandable up to 6 TB depending on traffic volume and retention needs
Example configuration for 75,000 QPS and one year of log retention:
- 4 ClickHouse data nodes
- CPU: 6 cores
- RAM: 16 GB
- Storage: 6 TB NVMe
- 3 ClickHouse Keeper nodes (coordination service)
- CPU: 2 cores
- RAM: 4 GB
- Storage: 60 GB SSD
We recommend a minimum cluster of 4 data servers arranged as 2 shards × 2 replicas. This provides parallel read/write operations and redundancy. A load balancer distributes incoming statistics across the nodes.
For lower traffic volumes, a standalone ClickHouse server can be deployed without ClickHouse Keeper, eliminating the need for the coordination layer.
Miscellaneous
Working with statistics
The DNS Proxy generates binary logs, which are stored directly on the SafeDNS Shield server. A dedicated parsing module processes these logs and exports the resulting data to an external DBMS for analysis and report generation. The module includes a connector for ClickHouse, which provides the best performance for this data type, but logs can also be exported to other databases if required.
The local binary log storage acts as a buffer: if the connection to ClickHouse is temporarily lost, logs generated during the outage are retained on disk and automatically exported once connectivity is restored. No data is lost during short interruptions, provided the local storage on the Shield server does not become completely full.
Although the statistics module is not essential for the core filtering functionality, it is indispensable for assessing system performance and investigating incidents effectively.
Note on HTTPS block pages
To display the built‑in block page for HTTPS requests, the SafeDNS root certificate must be added to the trusted certificate store on every end user device. Without the certificate, web browsers cannot validate the block page’s TLS certificate; instead of the block page, users will see a TLS/SSL error. Access to the requested resource will still be denied.
Displaying the block page over HTTP does not require the certificate.
If an externally hosted block page is used, certificate requirements depend on your own hosting configuration.
REST API overview
All administration and configuration of SafeDNS Shield is performed through the REST API. The API listens on port 8080 of the same IP address that handles DNS queries. By default, access is restricted to a set of whitelisted IP addresses specified during deployment.
Initial Configuration Example
The following example demonstrates the initial configuration workflow. Send the following request:
- Type: POST
- URL:
<domain>/init/ - Data:
{
"profiles": [
{
"profile": {
"id": 1,
"page_id": 1
},
"cat_ids": [3, 4, 12],
"app_ids": [1, 12, 93]
}
],
"blockpages": [
{
"id": 1,
"type": 0
},
{
"id": 2,
"type": 1
}
],
"bw_lists": [
{
"profile_id": 1,
"type": "deny",
"domains": [
"example1.com",
"example2.com",
"example3.com"
]
}
],
"nets": [
{
"ip": "100.100.100.100",
"profile_id": 1,
"prefix_len": 32
},
{
"ip": "100.110.110.0",
"profile_id": 1,
"prefix_len": 24
},
{
"ip": "100.120.0.0",
"profile_id": 1,
"prefix_len": 16
}
],
"nets6": [],
"napts": []
}
The initialization query defines the full configuration applied to SafeDNS Shield. The following settings are processed in sequence:
-
Filtering profile and block page – A filtering profile (ID=1) and a block page (ID=1) are created.
-
Blocked categories – Three DNS categories are blocked for the profile: malware, phishing, and botnets.
-
Blocked applications – Three AppBlocker categories are blocked: Slack, AnyDesk, and Netflix.
-
Block pages – Two block pages of different types are created:
-
Type 0 – displays the blocked domain name.
-
Type 1 – does not display the blocked domain name.
-
-
Blacklist – Three domains are added to the blacklist for filtering profile 1.
-
IP assignment – The profile is assigned one specific IP address and two subnets (/16 and /24). DNS requests from these sources are filtered according to the profile’s policy.
-
IPv6 and NAT‑port identification – The
nets6(IPv6) andnapts(NAT port‑based identification) sections are included but left empty. These fields must be present in the request even if not used, to trigger the creation of the initial configuration.
Once the request is sent and a successful response (HTTP status 204) is received, the initial configuration is complete. All subsequent DNS traffic processed by the server is handled according to the specified rules.
Modifying Filtering Categories
To modify the filtering categories for an existing profile, you must update the entire list of categories assigned to that profile. Adding new categories requires sending the full set (both current and new categories) in a single request.
For example, the initial configuration created profile id=1 with three blocked categories: malware, phishing, and botnets. To add Cryptojacking, DGA, and Ransomware, the request must include all six categories:
- Type: PATCH
- URL:
<domain>/profiles/1/ - Data:
{
"cat_ids": [3, 4, 12, 66, 70, 71],
"app_ids": [1, 12, 93],
"profile": {
"page_id": 1
}
}
In this request, the profile ID is specified directly in the URL. The request body contains the updated list of categories (original three and three new) while the AppBlocker categories and block page remain unchanged.
Adding a Domain to the Allowlist
To exempt a specific domain from category-based blocking, add it to the allowlist of the relevant filtering profile. For profile id=1, send the following request:
- Type: POST
- URL:
<domain>/profile/1/bw_list - Data:
{
"type": "allow",
"domain": "example4.com"
}
This adds the domain to the allowlist, giving it priority over category-based blocking.
Adding Domains to the Denylist
To block specific domains that are not covered by the configured categories, add them directly to the denylist for the filtering profile. The following request adds domains in a batch operation for profile id=1:
- Type: POST
- URL:
<domain>/profile/1/bw_list/batch - Data:
{
"type": "deny",
"domains": ["example5.com", "example6.com", "example7.com", "example8.com", "example9.com"]
}
Adding a Network or IP Address for Filtering
To assign a new IP address or subnet to a filtering profile, send the following request. The example adds a single IP address by specifying a /32 network prefix for profile id=1:
- Type: POST
- URL:
<domain>/net/ - Data:
{
"ip": "100.100.100.101",
"profile_id": 1,
"prefix_len": 32
}
To add an entire subnet, adjust the prefix length accordingly. For instance, a /24 subnet would be specified as:
{
"ip": "100.100.101.0",
"profile_id": 1,
"prefix_len": 24
}
Removing an IP Address from Filtering
To remove a previously assigned IP address or subnet from a filtering profile, send the following request:
- Type: DELETE
- URL:
<domain>/net/1684300901 - Data: None
This request has no body. The IP address must be provided in decimal format in the URL. Use the exact address originally submitted, without the mask - for example, 100.100.100.101 or 100.100.101.0 from the earlier examples. You can convert an IP address to decimal format using a tool like this one.
Modifying the Filtering Profile for a Specific IP
To change the filtering profile assigned to an existing IP address or subnet, send the following request. The IP address must be provided in decimal format in the URL. This example reassigns the address 100.100.101.0 (originally a /24 subnet) from profile id=1 to profile id=2:
- Type: PATCH
- URL:
<domain>/net/1684301056 - Data:
{
"ip": "100.100.101.0",
"profile_id": 2,
"prefix_len": 24
}
To change the network prefix (for example, from /24 to /16), you must delete the existing record and add a new one with the correct network address. In this case, you would delete 100.100.101.0 and add 100.100.0.0 with the /16 prefix.
Creating a New Filtering Profile
Before an IP can be reassigned to a different profile, that profile must exist. The following request creates a new filtering profile:
- Type: POST
- URL:
<domain>/profiles/ - Data:
{ "profile": { "id": 2, "page_id": 1 }, "cat_ids": [3, 4, 12, 66, 70, 71, 13], "app_ids": [1, 12, 93] }
This creates profile id=2, configured with the same block page type and the same categories and AppBlocker settings as profile id=1, with the addition of the "Adult Related" category.
REST API reference
User data methods
Initialize user data database (batch data creation)
- Type: POST
- URL:
<domain>/init/ - Data:
{ 'profiles': [ { 'cat_ids': [<cat_id>,], 'app_ids': [<app_id>,], 'profile': { 'page_id': <page_id>, 'id': <id> } }, ... ], 'nets': [{'ip': <ipv4>, 'profile_id': <profile_id>, 'prefix_len': <prefix_len4>}, ...], 'nets6': [{'ip': <ipv6>, 'profile_id': <profile_id>, 'prefix_len': <prefix_len6>}, ...], 'blockpages': [{'id': <page_id>, 'type': <blockpage_type>}, ...], 'napts': [ { 'ip': <ipv4>, "profile_id": <profile_id>, "lower_port_bound": <lower_port_bound>, "upper_port_bound": <upper_port_bound> }, ] } - Result: 204 No body
Profile methods
Fetch all profiles
- Type: GET
- URL:
<domain>/profiles - Result: 200 OK
Add a profile
- Type: POST
- URL:
<domain>/profiles - Data:
{ "profile": { "id": int, "page_id": int }, "cat_ids": [int, ], "app_ids": [int, ], } - Result: 201 Created
Fetch a specific profile
- Type: GET
- URL:
<domain>/profiles/<id> - Result: 200 OK
Update a specific profile
- Type: PATCH
- URL:
<domain>/profiles - Data:
{ "app_ids": [int, ], "cat_ids": [int, ], "profile": { "plan_id": int, "provider_id": int, "white_list_only": bool, "hide_block_reason": bool, "empty_dns_answer": bool, "page_id": int, "tls": bool } } - Result: 200 OK
Blockpage methods
Get all blockpages
- Type: GET
- URL:
<domain>/blockpage - Result: 200 OK
Create a blockpage
- Type: POST
- URL:
<domain>/blockpage - Data:
{ "type": int, "id": int } - Result: 200 OK
Get a specific blockpage
- Type: GET
- URL:
<domain>/blockpage/<page_id> - Result: 200 OK
Update a blockpage
- Type: PATCH
- URL:
<domain>/blockpage/<page_id> - Data:
{ "type": int } - Result: 200 OK
Delete a blockpage
- Type: DELETE
- URL:
<domain>/blockpage/<page_id> - Result: 204 No Content, no body
Lists methods
Create an allow- or denylist
- Type: POST
- URL:
<domain>/profile/<profile_id>/bw_list - Data:
{ "type": "string", //"allow" or "deny" only "domains": ["string", "string", ] } - Result: 201 Created
Create an allow- or denylist with multiple entries
- Type: POST
- URL:
<domain>/profile/<profile_id>/bw_list/batch - Data:
{ "type": "string", //"allow" or "deny" only "domains": ["string", "string", ] } - Result: 201 Created
Update a domain in an allow- or denylist
- Type: PATCH
- URL:
<domain>/profile/<profile_id>/bw_list/<domain> - Data:
{ "type": "string", //"allow" or "deny" only "domain": "string", "profile_id": int } - Result: 200 OK
Delete a domain from an allow- or denylist:
- Type: DELETE
- URL:
<domain>/profile/<profile_id>/bw_list/<domain> - Result: 204 No Content, no body
IPv4 network methods
Create an IPv4 address entry
- Type: POST
- URL:
<domain>/net - Data:
{ "ip": "string", //IPv4 address in canonical form "profile_id": int, "prefix_len": int } - Result: 201 Created
Get an IPv4 address entry
- Type: GET
- URL:
<domain>/net/<int_ip> - Result: 200 OK
Update an IPv4 address entry
- Type: PATCH
- URL:
<domain>/net/<int_ip> - Data:
{ "ip": "string", //IPv4 address in canonical form "profile_id": int, "prefix_len": int } - Result: 200 OK
Delete an IPv4 address
- Type: DELETE
- URL:
<domain>/net/<int_ip> - Result: 204 No Content, no body
IPv6 network methods
Create an IPv6 address entry
- Type: POST
- URL:
<domain>/net6 - Data:
{ "ip": "string", //IPv6 address in canonical form "prefix_len": int, "profile_id": int } - Result: 201 Created
Get an IPv6 address entry
- Type: GET
- URL:
<domain>/net6/<ipv6> - Result: Dictionary with IPv6 and profile_id data
Update an IPv6 address entry
- Type: PATCH
- URL:
<domain>/net6/<ipv6> - Data:
{ "ip": "string", //IPv6 address in canonical form "prefix_len": int, "profile_id": int } - Result: 200 OK
Delete an IPv6 address
- Type: DELETE
- URL:
<domain>/net6/<ipv6> - Result: 204 No Content, no body
IPv4 NAT methods
Create an IPv4 NAT entry
- Type: POST
- URL:
<domain>/napt/ - Data:
{ "ip": "string", //IPv4 address in canonical form "lower_port_bound": int, "upper_port_bound": int, "profile_id": int } - Result: 201 Created
Create multiple IPv4 NAT entries
- Type: POST
- URL:
<domain>/napt/batch - Data:
[ { "ip": "string", //IPv4 address in canonical form "lower_port_bound": int, "upper_port_bound": int, "profile_id": int }, ] - Result: 201 Created
Get all IPv4 NAT entries by IP
- Type: GET
- URL:
<domain>/napt/<int_ip> - Result: 200 OK
Get a specific IPv4 NAT entry
- Type: GET
- URL:
<domain>/napt/<int_ip>/<lower_port_bound>/<upper_port_bound> - Result: 200 OK
Update an IPv4 NAT entry
- Type: PATCH
- URL:
<domain>/napt/<int_ip>/<lower_port_bound>/<upper_port_bound> - Data:
{ "ip": "string", //IPv4 address in canonical form "lower_port_bound": int, "upper_port_bound": int, "profile_id": int } - Result: 200 OK
Delete an IPv4 NAT entry
- Type: DELETE
- URL:
<domain>/napt/<int_ip>/<lower_port_bound>/<upper_port_bound> - Result: 204 No Content, no body
Delete multiple IPv4 NAT entries
- Type: DELETE
- URL:
<domain>/napt/batch - Data:
[ { "ip": "string", //IPv4 address in canonical form "lower_port_bound": int, "upper_port_bound": int, "profile_id": int }, ] - Result: 204 No Content, no body
AppBlocker methods
Get a list of all available AppBlocker categories
- Type: GET
- URL:
<domain>/app_aware/application/ - Result: 200 OK