# SafeDNS Shield

# Overview

<span class="">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.</span>

<span class="">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.</span>

<span class="">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).</span>

<span class="">Because SafeDNS Shield inspects all DNS traffic, it enables comprehensive, per‑user traffic analysis. Every request is logged, providing access to detailed statistical information.</span>

# Architecture and deployment

### Components

[![image-1772786479925.png](https://docs.safedns.com/uploads/images/gallery/2026-03/scaled-1680-/MuD0Wr8fD6cCi4bA-image-1772786479925.png)](https://docs.safedns.com/uploads/images/gallery/2026-03/MuD0Wr8fD6cCi4bA-image-1772786479925.png)

<span class="">SafeDNS Shield is composed of the following components:</span>

- **<span class="">DNS Proxy Module</span>**  
    <span class="">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.</span>
- **<span class="">Internal Database</span>**  
    <span class="">Stores all configuration and policy data: user identifiers (subnet, IP, port), filtering profiles, block pages, user groups, and their assignments.</span>
- <span class="">**Block pages**  
    HTTP/S pages served to the users instead of blocked websites. Can be hosted alongside Shield or on an external server.  
    </span>
- **<span class="">REST API</span>**  
    <span class="">Provides a management interface for administrators to update the Internal Database. It supports creation and modification of:</span>
    
    
    - <span class="">User identifiers (subnet, IP, port)</span>
    - <span class="">Filtering profiles (categories to block)</span>
    - <span class="">Block pages</span>
- **<span class="">Binary Log Parsing Module (StatsLoader)</span>**  
    <span class="">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.</span>
- **<span class="">ClickHouse Cluster</span>**  
    <span class="">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.</span>
    
    
    - **<span class="">Load Balancer</span>**  
        <span class="">Receives statistics data from StatsLoader and distributes it evenly across the ClickHouse cluster nodes.</span>
    - **<span class="">ZooKeeper</span>**  
        <span class="">Manages coordination and configuration of the ClickHouse cluster, ensuring data consistency and system reliability.</span>

<span class="">The DNS Proxy writes its binary query logs to a designated host path (</span>`HostPATH`<span class="">), from which StatsLoader reads them.</span>

**<span class="">External dependencies</span>**  
<span class="">The following elements are not part of SafeDNS Shield but are required for operation:</span>

- **<span class="">User</span>**<span class=""> – The end‑user device that sends DNS requests to SafeDNS Shield.</span>
- **<span class="">Caching DNS Server</span>**<span class=""> – A recursive resolver deployed on the organization’s network that performs upstream DNS resolution for allowed queries.</span>

### Deployment options

SafeDNS Shield supports multiple deployment options to accommodate different network topologies. This section describes the most common scenarios.

##### For ISPs

##### **![](https://docs.safedns.com/uploads/images/gallery/2024-10/qNQmeYymeTxr15Xl-image-1728560145793.png)**

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.

<div class="pointer-container" id="bkmrk-%C2%A0"><div class="pointer anim is-page-editable"><svg class="svg-icon" data-icon="link" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg><div class="input-group inline block"> <button class="button outline icon" data-clipboard-target="#pointer-url" title="Copy Link" type="button"><svg class="svg-icon" data-icon="copy" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg></button></div><svg class="svg-icon" data-icon="edit" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg></div></div>##### For corporate clients  


[![image-1772786346085.png](https://docs.safedns.com/uploads/images/gallery/2026-03/scaled-1680-/eNEcaAFBnndiMMt5-image-1772786346085.png)](https://docs.safedns.com/uploads/images/gallery/2026-03/eNEcaAFBnndiMMt5-image-1772786346085.png)

or

[![image-1772786395390.png](https://docs.safedns.com/uploads/images/gallery/2026-03/scaled-1680-/wUXsLWAApQ8PeBDb-image-1772786395390.png)](https://docs.safedns.com/uploads/images/gallery/2026-03/wUXsLWAApQ8PeBDb-image-1772786395390.png)

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).

### <span class="">User Identification</span>

**<span class="">User Identification</span>**

<span class="">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:</span>

- **<span class="">IP address</span>**<span class=""> – Use when each user has a unique IP address.</span>
- **<span class="">IP subnet</span>**<span class=""> – Use when per‑user granularity is not required and all users in a subnet can share the same policy.</span>
- **<span class="">IP:port</span>**<span class=""> – Use when multiple users share a single IP address (for example, behind NAT44 or CGNAT). The source port distinguishes individual users.</span>
- **<span class="">IP:port range</span>**<span class=""> – Use when users can be identified by a range of source ports on a shared IP address.</span>

<span class="">The appropriate method depends on the network topology and the level of user separation required.</span>

# Setup and requirements

### <span class="">Product setup and support</span>

##### <span class="">Custom Local Deployment</span>

<span class="">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.</span>

<span class="">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.</span>

##### <span class="">VM Deployment via ISO Image</span>

<span class="">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.</span>

### <span class="">System requirements</span>

<span class="">The following specifications apply to a server running the SafeDNS Shield components. All deployments require Debian 12 (x86‑64).</span>

##### <span class="">DNS Proxy Module</span>

<span class="">Choose a configuration based on the expected peak query load.</span>

<span class="">CPU: Intel 12th‑generation or later, Intel Xeon Silver/Gold, or equivalent AMD Ryzen/Epyc.</span>

<div class="ds-scroll-area ds-scroll-area--show-on-focus-within _1210dd7 c03cafe9" id="bkmrk-queries-per-second-%28"><table><thead><tr><th><span class="">Queries per second (QPS)</span></th><th><span class="">CPU Cores</span></th><th><span class="">RAM</span></th><th><span class="">Storage</span></th><th><span class="">Network</span></th></tr></thead><tbody><tr><td><span class="">Up to 1,000</span></td><td><span class="">4</span></td><td><span class="">8 GB</span></td><td><span class="">200 GB NVMe</span></td><td><span class="">1 Gbps</span></td></tr><tr><td><span class="">Up to 15,000</span></td><td><span class="">12</span></td><td><span class="">16 GB</span></td><td><span class="">512 GB SSD (RAID 1)</span></td><td><span class="">1 Gbps</span></td></tr><tr><td><span class="">Up to 310,000</span></td><td><span class="">64</span></td><td><span class="">128 GB</span></td><td><span class="">2 TB SSD/NVMe (RAID 1)</span></td><td><span class="">1 Gbps</span></td></tr><tr><td><span class="">Up to 2,000,000</span></td><td><span class="">128</span></td><td><span class="">2 TB</span></td><td><span class="">16 TB SSD/NVMe (RAID 1)</span></td><td><span class="">25 Gbps</span></td></tr></tbody></table>

</div>##### <span class="">ClickHouse Cluster (Statistics Storage)</span>

<span class="">The ClickHouse cluster stores and analyzes DNS request logs. A single node meets the following minimum:</span>

- <span class="">CPU: **6** cores (x86‑64)</span>
- <span class="">RAM: **16** GB</span>
- <span class="">Storage: **500** GB NVMe</span>
    - <span class="">Expandable up to **6** TB depending on traffic volume and retention needs</span>

*<span class="">Example configuration for **75,000** QPS and one year of log retention:</span>*

- <span class="">**4** ClickHouse data nodes</span>
    - <span class="">CPU: **6** cores</span>
    - <span class="">RAM: **16** GB</span>
    - <span class="">Storage: **6** TB NVMe</span>
- <span class="">**3** ClickHouse Keeper nodes</span><span class=""> (coordination service)</span>
    - <span class="">CPU: **2** cores</span>
    - <span class="">RAM: **4** GB</span>
    - <span class="">Storage: **60** GB SSD</span>

<span class="">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.</span>

<span class="">For lower traffic volumes, a standalone ClickHouse server can be deployed without ClickHouse Keeper, eliminating the need for the coordination layer.</span>

# Miscellaneous

### Working with statistics

<span class="">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.</span>

<span class="">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.</span>

<span class="">Although the statistics module is not essential for the core filtering functionality, it is indispensable for assessing system performance and investigating incidents effectively.</span>

### Note on HTTPS block pages

<span class="">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.</span>  
<span class="">Displaying the block page over HTTP does not require the certificate.</span>  
<span class="">If an externally hosted block page is used, certificate requirements depend on your own hosting configuration.</span>

# REST API overview

SafeDNS Shield filtering configuration is managed through its REST API. The API address is deployment-specific. The default management API port is `8080`, but the host, protocol, and port may be changed during deployment.

Use the following placeholder throughout this guide:

```text
<base-url> = http[s]://<shield-management-host>:<management-api-port>

```

For example:

```text
http://192.0.2.10:8080

```

Access is normally restricted to source addresses approved during deployment.

## Using the API

**Authentication**

All client-facing API requests require an HTTP bearer token.

```http
Authorization: Bearer <token>
Content-Type: application/json

```

Example:

```bash
curl \
  --header "Authorization: Bearer <token>" \
  --header "Content-Type: application/json" \
  "<base-url>/profiles/"

```

Use the token and management API address assigned to the Shield deployment.

**General behavior**

- Successfully completed configuration changes take effect immediately. No service restart or separate activation request is required.
- Successful create operations normally return `201 Created`.
- Successful update and retrieval operations normally return `200 OK`.
- Successful deletion and initialization operations return `204 No Content` with no response body.
- Invalid request data may return `422 Unprocessable Entity` with validation details.
- IPv4 addresses are supplied as dotted-decimal strings in request bodies, but some resource paths identify an IPv4 address by its unsigned integer representation.
- The examples use trailing slashes where they appear in the API specification.

## Profiles and filtering

A filtering profile combines category rules, application rules, block-page behavior, and several profile-level options.

**Profile fields**

<table id="bkmrk-field-type-descripti"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`id`</td><td>integer</td><td>Client-supplied profile identifier. Required when creating a profile.</td></tr><tr><td>`page_id`</td><td>integer or `null`</td><td>Identifier of the block page assigned to the profile.</td></tr><tr><td>`white_list_only`</td><td>boolean</td><td>When enabled, only domains on the allowlist and explicitly allowed applications are permitted. Default: `false`.</td></tr><tr><td>`empty_dns_answer`</td><td>boolean</td><td>When enabled, Shield returns an empty DNS answer for a blocked domain instead of directing the client to a block page. Default: `false`.</td></tr><tr><td>`appaware_priority_over_bwlists`</td><td>boolean</td><td>When enabled, application rules take priority over explicit domain allow/deny-list rules. Default: `false`.</td></tr></tbody></table>

A profile request also supports:

<table id="bkmrk-field-type-descripti-0"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`cat_ids`</td><td>array of integers</td><td>Complete collection of filtering category IDs assigned to the profile.</td></tr><tr><td>`apps`</td><td>array of objects</td><td>Complete collection of application rules assigned to the profile.</td></tr></tbody></table>

Each application rule contains:

```json
{
  "app_id": 12,
  "status": "deny"
}

```

Supported statuses are:

- `deny` — blocks domains associated with the application.
- `allow` — explicitly permits resolution of domains associated with the application, including when those domains would otherwise be blocked by the profile's category rules.

Application IDs can be obtained from:

```text
GET /app_aware/application/

```

Category IDs are listed in the [SafeDNS category reference](https://docs.safedns.com/books/installation-guides/page/list-of-safedns-categories).

**Replacement semantics**

When `cat_ids` or `apps` is included in:

```text
PATCH /profiles/{profile_id}

```

it replaces the complete existing collection. It is not merged with the current configuration.

- Omitting `cat_ids` leaves the category collection unchanged.
- Sending `"cat_ids": []` removes all category assignments.
- Omitting `apps` leaves the application-rule collection unchanged.
- Sending `"apps": []` removes all application rules.

**Initial configuration example**

The `/init/` operation creates the initial Shield configuration in one request.

- **Method:** `POST`
- **URL:** `<base-url>/init/`

```json
{
  "blockpages": [
    {
      "id": 1,
      "type": 0
    },
    {
      "id": 2,
      "type": 1
    }
  ],
  "profiles": [
    {
      "profile": {
        "id": 1,
        "page_id": 1,
        "white_list_only": false,
        "empty_dns_answer": false,
        "appaware_priority_over_bwlists": false
      },
      "cat_ids": [3, 4, 12],
      "apps": [
        {
          "app_id": 1,
          "status": "deny"
        },
        {
          "app_id": 12,
          "status": "deny"
        },
        {
          "app_id": 93,
          "status": "deny"
        }
      ]
    }
  ],
  "bw_lists": [
    {
      "profile_id": 1,
      "type": "deny",
      "domains": [
        "example1.com",
        "example2.com",
        "example3.com"
      ]
    }
  ],
  "napts": [],
  "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": []
}

```

All six top-level arrays are required. Include an empty array when a resource type is not used:

- `blockpages`
- `profiles`
- `bw_lists`
- `napts`
- `nets`
- `nets6`

The example creates:

1. Two block-page records: 
    - Type `0`: default block page.
    - Type `1`: custom block page.
2. Profile `1`, assigned to block page `1`.
3. Three blocked content/security categories.
4. Three denied applications.
5. Three explicitly denied domains.
6. One IPv4 host assignment and two IPv4 subnet assignments.

Custom block-page content is configured outside the Shield REST API. The API only creates and assigns the block-page record.

A successful initialization returns:

```text
204 No Content

```

## Profile configuration examples

**Modifying filtering categories**

To change categories assigned to a profile, send the complete desired category collection.

The following request replaces the existing categories of profile `1` with six categories:

- **Method:** `PATCH`
- **URL:** `<base-url>/profiles/1`

```json
{
  "cat_ids": [3, 4, 12, 66, 70, 71]
}

```

Because `apps` and `profile` are omitted, only the category collection is changed.

To remove all category assignments:

```json
{
  "cat_ids": []
}

```

**Replacing application rules for a profile**

Application rules can be replaced together through the profile endpoint.

- **Method:** `PATCH`
- **URL:** `<base-url>/profiles/1`

```json
{
  "apps": [
    {
      "app_id": 1,
      "status": "deny"
    },
    {
      "app_id": 12,
      "status": "allow"
    }
  ]
}

```

This request removes any other application rules previously assigned to profile `1`. It denies application `1` and explicitly permits domains associated with application `12`, even if category rules would otherwise block them.

To remove all application rules:

```json
{
  "apps": []
}

```

**Managing individual application rules**

**Retrieve the application catalogue**

- **Method:** `GET`
- **URL:** `<base-url>/app_aware/application/`

Example response:

```json
[
  {
    "id": 1,
    "name": "Application name"
  }
]

```

**Retrieve all application rules for a profile**

- **Method:** `GET`
- **URL:** `<base-url>/profile/1/app_aware`

Example response:

```json
[
  {
    "profile_id": 1,
    "app_id": 12,
    "status": "allow"
  }
]

```

**Add one application rule**

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/app_aware`

```json
{
  "app_id": 12,
  "status": "allow"
}

```

**Add several application rules with the same status**

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/app_aware/batch`

```json
{
  "app_ids": [1, 12, 93],
  "status": "deny"
}

```

**Delete an application rule**

The final path value is the application ID. The current OpenAPI parameter name is `app_aware_id`, but it identifies the same value as `app_id`.

- **Method:** `DELETE`
- **URL:** `<base-url>/profile/1/app_aware/12`

No request body is required.

**Adding a domain to the allowlist**

To exempt a domain from category-based blocking, add it to the allowlist of the relevant profile.

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/bw_list`

```json
{
  "type": "allow",
  "domain": "example4.com"
}

```

**Adding domains to the denylist**

To block several domains directly, use the batch operation.

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/bw_list/batch`

```json
{
  "type": "deny",
  "domains": [
    "example5.com",
    "example6.com",
    "example7.com",
    "example8.com",
    "example9.com"
  ]
}

```

**Allow/deny-list storage limitation**

Shield stores allowlist and denylist domains as hashes. The API therefore cannot retrieve or enumerate the original domain names after they have been submitted.

API clients should retain their own copy of configured allowlists and denylists when later inspection, reconciliation, or synchronization is required.

Update and delete operations still accept the original domain name. Shield calculates its hash internally to locate the stored rule.

**Creating a new filtering profile**

- **Method:** `POST`
- **URL:** `<base-url>/profiles/`

```json
{
  "profile": {
    "id": 2,
    "page_id": 1,
    "white_list_only": false,
    "empty_dns_answer": false,
    "appaware_priority_over_bwlists": false
  },
  "cat_ids": [3, 4, 12, 13, 66, 70, 71],
  "apps": [
    {
      "app_id": 1,
      "status": "deny"
    },
    {
      "app_id": 12,
      "status": "deny"
    },
    {
      "app_id": 93,
      "status": "deny"
    }
  ]
}

```

This creates profile `2`, assigns block page `1`, enables the listed filtering categories, and adds three denied application rules.

## Subscriber assignments

**Assigning an IPv4 address or subnet**

To assign an IPv4 host to profile `1`, create a `/32` network record.

- **Method:** `POST`
- **URL:** `<base-url>/net/`

```json
{
  "ip": "100.100.100.101",
  "profile_id": 1,
  "prefix_len": 32
}

```

To assign a `/24` subnet:

```json
{
  "ip": "100.100.101.0",
  "profile_id": 1,
  "prefix_len": 24
}

```

Supported IPv4 prefix lengths are `/10` through `/32`.

**Retrieve all IPv4 assignments**

- **Method:** `GET`
- **URL:** `<base-url>/net/`

Example response:

```json
[
  {
    "ip": "100.100.100.101",
    "profile_id": 1,
    "prefix_len": 32
  },
  {
    "ip": "100.100.101.0",
    "profile_id": 1,
    "prefix_len": 24
  }
]

```

**IPv4 integer representation**

Individual IPv4 resource paths identify an address by its unsigned 32-bit integer representation.

For example:

```text
100.100.100.101 = 1684300901
100.100.101.0   = 1684301056

```

Python conversion example:

```python
from ipaddress import IPv4Address

int_ip = int(IPv4Address("100.100.100.101"))
print(int_ip)  # 1684300901

```

**Reassign an IPv4 address or subnet**

- **Method:** `PATCH`
- **URL:** `<base-url>/net/1684301056`

```json
{
  "ip": "100.100.101.0",
  "profile_id": 2,
  "prefix_len": 24
}

```

**Remove an IPv4 address or subnet**

- **Method:** `DELETE`
- **URL:** `<base-url>/net/1684301056`

No request body is required.

**Assigning an IPv6 address or subnet**

- **Method:** `POST`
- **URL:** `<base-url>/net6/`

```json
{
  "ip": "2001:db8:100::",
  "profile_id": 1,
  "prefix_len": 64
}

```

Supported IPv6 prefix lengths are `/16` through `/128`.

**Retrieve all IPv6 assignments**

- **Method:** `GET`
- **URL:** `<base-url>/net6/`

Example response:

```json
[
  {
    "ip": "2001:db8:100::",
    "profile_id": 1,
    "prefix_len": 64
  }
]

```

**Identifying subscribers through NAPT port ranges**

NAPT records assign different source-port ranges on a shared IPv4 address to different filtering profiles.

Port boundaries are inclusive. A range from `10000` to `19999` includes both port `10000` and port `19999`.

**Create a NAPT assignment**

- **Method:** `POST`
- **URL:** `<base-url>/napt/`

```json
{
  "ip": "203.0.113.10",
  "lower_port_bound": 10000,
  "upper_port_bound": 19999,
  "profile_id": 1
}

```

Port values must be between `0` and `65535`. The complete port range is `0` through `65535`.

**Create several NAPT assignments**

- **Method:** `POST`
- **URL:** `<base-url>/napt/batch`

```json
[
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 10000,
    "upper_port_bound": 19999,
    "profile_id": 1
  },
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 20000,
    "upper_port_bound": 29999,
    "profile_id": 2
  }
]

```

**Retrieve all NAPT assignments for an IPv4 address**

The path uses the integer representation of the IPv4 address.

- **Method:** `GET`
- **URL:** `<base-url>/napt/3405803786`

`3405803786` is the integer representation of `203.0.113.10`.

**Delete several NAPT assignments**

Unlike individual NAPT resource paths, the batch-deletion body uses dotted-decimal IPv4 strings.

- **Method:** `DELETE`
- **URL:** `<base-url>/napt/batch`

```json
[
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 10000,
    "upper_port_bound": 19999
  },
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 20000,
    "upper_port_bound": 29999
  }
]

```

`profile_id` is not included in the batch-deletion request.

## Profile schedules

A schedule temporarily switches one profile to another during specified periods.

The profile identified in the URL is the source profile. `target_profile_id` identifies the profile used during scheduled periods. A profile cannot target itself.

**Supported day formats**

Both weekday names and integers are accepted:

<table id="bkmrk-integer-weekday-0-mo"><thead><tr><th align="right">Integer</th><th>Weekday</th></tr></thead><tbody><tr><td align="right">`0`</td><td>Monday</td></tr><tr><td align="right">`1`</td><td>Tuesday</td></tr><tr><td align="right">`2`</td><td>Wednesday</td></tr><tr><td align="right">`3`</td><td>Thursday</td></tr><tr><td align="right">`4`</td><td>Friday</td></tr><tr><td align="right">`5`</td><td>Saturday</td></tr><tr><td align="right">`6`</td><td>Sunday</td></tr></tbody></table>

Weekday names must be lowercase:

```text
monday, tuesday, wednesday, thursday, friday, saturday, sunday

```

Examples in this guide use weekday names for readability.

Times use `HH:MM` format and must fall on 30-minute boundaries, such as `08:00`, `12:30`, or `18:00`. `00:00` represents the start of a day and `24:00` represents the end of a day.

**Create a schedule**

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/schedule`

```json
{
  "target_profile_id": 2,
  "periods": [
    {
      "start": {
        "day": "monday",
        "time": "18:00"
      },
      "end": {
        "day": "tuesday",
        "time": "08:00"
      }
    },
    {
      "start": {
        "day": 5,
        "time": "00:00"
      },
      "end": {
        "day": 5,
        "time": "24:00"
      }
    }
  ]
}

```

This example switches profile `1` to profile `2` from Monday evening through Tuesday morning and for the whole of Saturday.

**Update a schedule**

- **Method:** `PATCH`
- **URL:** `<base-url>/profile/1/schedule`

To change the target profile for all existing periods:

```json
{
  "target_profile_id": 3
}

```

To replace all existing periods:

```json
{
  "periods": [
    {
      "start": {
        "day": "friday",
        "time": "18:00"
      },
      "end": {
        "day": "monday",
        "time": "08:00"
      }
    }
  ]
}

```

When `periods` is supplied, all previously configured periods are deleted and replaced by the submitted collection.

**Delete a schedule**

- **Method:** `DELETE`
- **URL:** `<base-url>/profile/1/schedule`

No request body is required.

## Block pages

Block-page records use two types:

<table id="bkmrk-type-meaning-0-defau"><thead><tr><th align="right">Type</th><th>Meaning</th></tr></thead><tbody><tr><td align="right">`0`</td><td>Default block page</td></tr><tr><td align="right">`1`</td><td>Custom block page</td></tr></tbody></table>

Example creation request:

- **Method:** `POST`
- **URL:** `<base-url>/blockpage/`

```json
{
  "id": 2,
  "type": 1
}

```

Custom block-page content is configured outside the Shield REST API.

# REST API reference

This reference describes the client-facing operations exposed by the current SafeDNS Shield REST API.

## Using the API

**Base URL**

SafeDNS Shield does not have a universal API hostname. The management API address is assigned during deployment.

```text
<base-url> = http[s]://<shield-management-host>:<management-api-port>

```

The default management API port is `8080`, but it may be changed for a particular deployment.

**Authentication**

All operations in this reference require HTTP bearer authentication.

```http
Authorization: Bearer <token>
Content-Type: application/json

```

Example:

```bash
curl \
  --header "Authorization: Bearer <token>" \
  --header "Content-Type: application/json" \
  "<base-url>/profiles/"

```

Access may additionally be restricted by source IP address according to the deployment configuration.

**General conventions**

**Immediate application**

Successfully completed changes take effect immediately. No service restart, reload, or separate activation operation is required.

**Success status codes**

<table id="bkmrk-status-typical-use-2"><thead><tr><th align="right">Status</th><th>Typical use</th></tr></thead><tbody><tr><td align="right">`200 OK`</td><td>Successful retrieval or update</td></tr><tr><td align="right">`201 Created`</td><td>Successful resource creation</td></tr><tr><td align="right">`204 No Content`</td><td>Successful deletion or initialization; no response body</td></tr></tbody></table>

**Validation errors**

Invalid request data may return `422 Unprocessable Entity`.

Example response:

```json
{
  "detail": [
    {
      "loc": [
        "body",
        "field_name"
      ],
      "msg": "Validation error message",
      "type": "validation_error"
    }
  ]
}

```

The current OpenAPI specification formally describes validation errors but does not provide complete response schemas for every possible authentication, authorization, conflict, or not-found condition.

**IPv4 representations**

IPv4 addresses appear in two forms:

- **Dotted-decimal string**, for example `"100.100.100.101"`, in request and response bodies.
- **Unsigned 32-bit integer**, for example `1684300901`, in individual IPv4 and NAPT resource paths.

Python conversion example:

```python
from ipaddress import IPv4Address

int_ip = int(IPv4Address("100.100.100.101"))
ip = str(IPv4Address(1684300901))

```

**Category IDs**

Filtering profiles refer to categories by numeric `cat_ids`. See the [SafeDNS category reference](https://docs.safedns.com/books/installation-guides/page/list-of-safedns-categories).

**Application IDs**

Application IDs can be retrieved through:

```text
GET /app_aware/application/

```

**Allow/deny-list storage**

Shield stores allowlist and denylist domains as hashes. The original domain names cannot be retrieved or enumerated through the API after submission.

Clients that need to inspect or synchronize these lists must retain their own copy of the original domains. Update and delete requests still use the original domain name; Shield calculates the corresponding hash internally.

## Endpoint summary

<table id="bkmrk-group-method-path-pu"><thead><tr><th>Group</th><th>Method</th><th>Path</th><th>Purpose</th></tr></thead><tbody><tr><td>Initialization</td><td>`POST`</td><td>`/init/`</td><td>Create the initial configuration in one request</td></tr><tr><td>Profiles</td><td>`GET`</td><td>`/profiles/`</td><td>Retrieve all profiles</td></tr><tr><td>Profiles</td><td>`POST`</td><td>`/profiles/`</td><td>Create a profile</td></tr><tr><td>Profiles</td><td>`GET`</td><td>`/profiles/{profile_id}`</td><td>Retrieve one profile</td></tr><tr><td>Profiles</td><td>`PATCH`</td><td>`/profiles/{profile_id}`</td><td>Update a profile and replace selected collections</td></tr><tr><td>Profile applications</td><td>`GET`</td><td>`/profile/{profile_id}/app_aware`</td><td>Retrieve application rules for a profile</td></tr><tr><td>Profile applications</td><td>`POST`</td><td>`/profile/{profile_id}/app_aware`</td><td>Add one application rule</td></tr><tr><td>Profile applications</td><td>`POST`</td><td>`/profile/{profile_id}/app_aware/batch`</td><td>Add several application rules with one status</td></tr><tr><td>Profile applications</td><td>`DELETE`</td><td>`/profile/{profile_id}/app_aware/{app_id}`</td><td>Delete an application rule</td></tr><tr><td>Application catalogue</td><td>`GET`</td><td>`/app_aware/application/`</td><td>Retrieve all available applications</td></tr><tr><td>Block pages</td><td>`GET`</td><td>`/blockpage/`</td><td>Retrieve all block-page records</td></tr><tr><td>Block pages</td><td>`POST`</td><td>`/blockpage/`</td><td>Create a block-page record</td></tr><tr><td>Block pages</td><td>`GET`</td><td>`/blockpage/{page_id}`</td><td>Retrieve one block-page record</td></tr><tr><td>Block pages</td><td>`PATCH`</td><td>`/blockpage/{page_id}`</td><td>Update a block-page record</td></tr><tr><td>Block pages</td><td>`DELETE`</td><td>`/blockpage/{page_id}`</td><td>Delete a block-page record</td></tr><tr><td>Domain lists</td><td>`POST`</td><td>`/profile/{profile_id}/bw_list`</td><td>Add one allowlist or denylist domain</td></tr><tr><td>Domain lists</td><td>`POST`</td><td>`/profile/{profile_id}/bw_list/batch`</td><td>Add several allowlist or denylist domains</td></tr><tr><td>Domain lists</td><td>`PATCH`</td><td>`/profile/{profile_id}/bw_list/{domain}`</td><td>Change a stored domain rule</td></tr><tr><td>Domain lists</td><td>`DELETE`</td><td>`/profile/{profile_id}/bw_list/{domain}`</td><td>Delete a stored domain rule</td></tr><tr><td>IPv4 networks</td><td>`GET`</td><td>`/net/`</td><td>Retrieve all IPv4 assignments</td></tr><tr><td>IPv4 networks</td><td>`POST`</td><td>`/net/`</td><td>Create an IPv4 assignment</td></tr><tr><td>IPv4 networks</td><td>`GET`</td><td>`/net/{int_ip}`</td><td>Retrieve an IPv4 assignment</td></tr><tr><td>IPv4 networks</td><td>`PATCH`</td><td>`/net/{int_ip}`</td><td>Update an IPv4 assignment</td></tr><tr><td>IPv4 networks</td><td>`DELETE`</td><td>`/net/{int_ip}`</td><td>Delete an IPv4 assignment</td></tr><tr><td>IPv6 networks</td><td>`GET`</td><td>`/net6/`</td><td>Retrieve all IPv6 assignments</td></tr><tr><td>IPv6 networks</td><td>`POST`</td><td>`/net6/`</td><td>Create an IPv6 assignment</td></tr><tr><td>IPv6 networks</td><td>`GET`</td><td>`/net6/{ipv6}`</td><td>Retrieve an IPv6 assignment</td></tr><tr><td>IPv6 networks</td><td>`PATCH`</td><td>`/net6/{ipv6}`</td><td>Update an IPv6 assignment</td></tr><tr><td>IPv6 networks</td><td>`DELETE`</td><td>`/net6/{ipv6}`</td><td>Delete an IPv6 assignment</td></tr><tr><td>NAPT</td><td>`POST`</td><td>`/napt/`</td><td>Create a NAPT assignment</td></tr><tr><td>NAPT</td><td>`POST`</td><td>`/napt/batch`</td><td>Create several NAPT assignments</td></tr><tr><td>NAPT</td><td>`GET`</td><td>`/napt/{int_ip}`</td><td>Retrieve all NAPT assignments for an IPv4 address</td></tr><tr><td>NAPT</td><td>`GET`</td><td>`/napt/{int_ip}/{lower_port_bound}/{upper_port_bound}`</td><td>Retrieve one NAPT assignment</td></tr><tr><td>NAPT</td><td>`PATCH`</td><td>`/napt/{int_ip}/{lower_port_bound}/{upper_port_bound}`</td><td>Update one NAPT assignment</td></tr><tr><td>NAPT</td><td>`DELETE`</td><td>`/napt/{int_ip}/{lower_port_bound}/{upper_port_bound}`</td><td>Delete one NAPT assignment</td></tr><tr><td>NAPT</td><td>`DELETE`</td><td>`/napt/batch`</td><td>Delete several NAPT assignments</td></tr><tr><td>Schedules</td><td>`GET`</td><td>`/profile/{profile_id}/schedule`</td><td>Retrieve a profile schedule</td></tr><tr><td>Schedules</td><td>`POST`</td><td>`/profile/{profile_id}/schedule`</td><td>Create a profile schedule</td></tr><tr><td>Schedules</td><td>`PATCH`</td><td>`/profile/{profile_id}/schedule`</td><td>Update a profile schedule</td></tr><tr><td>Schedules</td><td>`DELETE`</td><td>`/profile/{profile_id}/schedule`</td><td>Delete a profile schedule</td></tr></tbody></table>

## Data models

**Profile object**

```json
{
  "id": 1,
  "page_id": 1,
  "white_list_only": false,
  "empty_dns_answer": false,
  "appaware_priority_over_bwlists": false
}

```

<table id="bkmrk-field-type-descripti"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`id`</td><td>integer</td><td>Profile identifier. Required on creation.</td></tr><tr><td>`page_id`</td><td>integer or `null`</td><td>Assigned block-page identifier.</td></tr><tr><td>`white_list_only`</td><td>boolean</td><td>Allows only allowlisted domains and explicitly allowed applications. Default: `false`.</td></tr><tr><td>`empty_dns_answer`</td><td>boolean</td><td>Returns an empty DNS answer instead of a block-page response for blocked domains. Default: `false`.</td></tr><tr><td>`appaware_priority_over_bwlists`</td><td>boolean</td><td>Gives application rules priority over domain allow/deny lists. Default: `false`.</td></tr></tbody></table>

**Profile with categories and applications**

```json
{
  "profile": {
    "id": 1,
    "page_id": 1,
    "white_list_only": false,
    "empty_dns_answer": false,
    "appaware_priority_over_bwlists": false
  },
  "cat_ids": [3, 4, 12],
  "apps": [
    {
      "app_id": 12,
      "status": "allow"
    }
  ]
}

```

`cat_ids` contains filtering category IDs. `apps` contains application rules.

**Application rule**

```json
{
  "app_id": 12,
  "status": "allow"
}

```

<table id="bkmrk-field-type-descripti-0"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`app_id`</td><td>integer</td><td>Application identifier.</td></tr><tr><td>`status`</td><td>string</td><td>`allow` or `deny`. Default in the schema: `deny`.</td></tr></tbody></table>

Behavior:

- `deny` blocks domains associated with the application.
- `allow` permits resolution of application-associated domains even when category rules would otherwise block them.

**Application-rule response**

```json
{
  "profile_id": 1,
  "app_id": 12,
  "status": "allow"
}

```

**Block-page object**

```json
{
  "id": 1,
  "type": 0
}

```

<table id="bkmrk-type-meaning-0-defau"><thead><tr><th align="right">Type</th><th>Meaning</th></tr></thead><tbody><tr><td align="right">`0`</td><td>Default block page</td></tr><tr><td align="right">`1`</td><td>Custom block page</td></tr></tbody></table>

Custom block-page content is configured outside the REST API.

**IPv4 network object**

```json
{
  "ip": "100.100.100.101",
  "profile_id": 1,
  "prefix_len": 32
}

```

<table id="bkmrk-field-type-constrain"><thead><tr><th>Field</th><th>Type</th><th>Constraints</th></tr></thead><tbody><tr><td>`ip`</td><td>IPv4 string</td><td>Dotted-decimal IPv4 address or network address</td></tr><tr><td>`profile_id`</td><td>integer</td><td>Assigned filtering profile</td></tr><tr><td>`prefix_len`</td><td>integer</td><td>`10` through `32`; default `32`</td></tr></tbody></table>

**IPv6 network object**

```json
{
  "ip": "2001:db8:100::",
  "profile_id": 1,
  "prefix_len": 64
}

```

<table id="bkmrk-field-type-constrain-0"><thead><tr><th>Field</th><th>Type</th><th>Constraints</th></tr></thead><tbody><tr><td>`ip`</td><td>IPv6 string</td><td>IPv6 address or network address</td></tr><tr><td>`profile_id`</td><td>integer</td><td>Assigned filtering profile</td></tr><tr><td>`prefix_len`</td><td>integer</td><td>`16` through `128`; default `128`</td></tr></tbody></table>

**NAPT object**

```json
{
  "ip": "203.0.113.10",
  "lower_port_bound": 10000,
  "upper_port_bound": 19999,
  "profile_id": 1
}

```

<table id="bkmrk-field-type-constrain-1"><thead><tr><th>Field</th><th>Type</th><th>Constraints</th></tr></thead><tbody><tr><td>`ip`</td><td>IPv4 string</td><td>Dotted-decimal IPv4 address</td></tr><tr><td>`lower_port_bound`</td><td>integer</td><td>`0` through `65535`; default `0`</td></tr><tr><td>`upper_port_bound`</td><td>integer</td><td>`0` through `65535`; default `65535`</td></tr><tr><td>`profile_id`</td><td>integer</td><td>Assigned filtering profile</td></tr></tbody></table>

Port boundaries are inclusive.

**Schedule object**

```json
{
  "target_profile_id": 2,
  "periods": [
    {
      "start": {
        "day": "monday",
        "time": "18:00"
      },
      "end": {
        "day": "tuesday",
        "time": "08:00"
      }
    }
  ]
}

```

`day` accepts either a lowercase weekday name or an integer:

<table id="bkmrk-integer-weekday-0-mo"><thead><tr><th align="right">Integer</th><th>Weekday</th></tr></thead><tbody><tr><td align="right">`0`</td><td>Monday</td></tr><tr><td align="right">`1`</td><td>Tuesday</td></tr><tr><td align="right">`2`</td><td>Wednesday</td></tr><tr><td align="right">`3`</td><td>Thursday</td></tr><tr><td align="right">`4`</td><td>Friday</td></tr><tr><td align="right">`5`</td><td>Saturday</td></tr><tr><td align="right">`6`</td><td>Sunday</td></tr></tbody></table>

Times use `HH:MM` and must be divisible into 30-minute intervals. `00:00` and `24:00` are supported day boundaries.

## Initialization

**Initialize configuration**

```text
POST /init/

```

Creates the initial Shield configuration from a single request containing block pages, profiles, domain rules, NAPT assignments, IPv4 networks, and IPv6 networks.

**Request body**

All six top-level arrays are required, including arrays that are empty.

```json
{
  "blockpages": [
    {
      "id": 1,
      "type": 0
    }
  ],
  "profiles": [
    {
      "profile": {
        "id": 1,
        "page_id": 1,
        "white_list_only": false,
        "empty_dns_answer": false,
        "appaware_priority_over_bwlists": false
      },
      "cat_ids": [3, 4, 12],
      "apps": [
        {
          "app_id": 12,
          "status": "deny"
        }
      ]
    }
  ],
  "bw_lists": [
    {
      "profile_id": 1,
      "type": "deny",
      "domains": [
        "example1.com",
        "example2.com"
      ]
    }
  ],
  "napts": [],
  "nets": [
    {
      "ip": "100.100.100.101",
      "profile_id": 1,
      "prefix_len": 32
    }
  ],
  "nets6": []
}

```

**Success response**

```text
204 No Content

```

## Profiles

**Retrieve all profiles**

```text
GET /profiles/

```

Returns all profiles, including profile settings, category IDs, and application rules.

**Success response**

```text
200 OK

```

```json
[
  {
    "profile": {
      "id": 1,
      "page_id": 1,
      "white_list_only": false,
      "empty_dns_answer": false,
      "appaware_priority_over_bwlists": false
    },
    "cat_ids": [3, 4, 12],
    "apps": [
      {
        "app_id": 12,
        "status": "deny"
      }
    ]
  }
]

```

**Create a profile**

```text
POST /profiles/

```

**Request body**

```json
{
  "profile": {
    "id": 2,
    "page_id": 1,
    "white_list_only": false,
    "empty_dns_answer": false,
    "appaware_priority_over_bwlists": false
  },
  "cat_ids": [3, 4, 12, 13],
  "apps": [
    {
      "app_id": 12,
      "status": "allow"
    },
    {
      "app_id": 93,
      "status": "deny"
    }
  ]
}

```

**Success response**

```text
201 Created

```

The response body contains the created profile with its categories and application rules.

**Retrieve a profile**

```text
GET /profiles/{profile_id}

```

**Path parameters**

<table id="bkmrk-parameter-type-descr"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`profile_id`</td><td>integer</td><td>Profile identifier</td></tr></tbody></table>

**Success response**

```text
200 OK

```

The response body uses the **Profile with categories and applications** model.

**Update a profile**

```text
PATCH /profiles/{profile_id}

```

Updates profile-level settings and optionally replaces the profile's complete category or application collections.

**Path parameters**

<table id="bkmrk-parameter-type-descr-0"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`profile_id`</td><td>integer</td><td>Profile identifier</td></tr></tbody></table>

**Request body**

All top-level fields are optional. Include only the areas to be changed.

```json
{
  "profile": {
    "page_id": 2,
    "white_list_only": false,
    "empty_dns_answer": true,
    "appaware_priority_over_bwlists": true
  },
  "cat_ids": [3, 4, 12, 66],
  "apps": [
    {
      "app_id": 12,
      "status": "allow"
    }
  ]
}

```

**Collection replacement rules**

- When `cat_ids` is present, it replaces the complete existing category collection.
- `"cat_ids": []` removes all category assignments.
- When `apps` is present, it replaces the complete existing application-rule collection.
- `"apps": []` removes all application rules.
- Omitted collections remain unchanged.

**Success response**

```text
200 OK

```

The response body contains the updated profile with its current categories and application rules.

## Application catalogue and profile application rules

**Retrieve all available applications**

```text
GET /app_aware/application/

```

Returns the application catalogue used to obtain valid `app_id` values.

**Success response**

```text
200 OK

```

```json
[
  {
    "id": 12,
    "name": "Application name"
  }
]

```

**Retrieve all application rules for a profile**

```text
GET /profile/{profile_id}/app_aware

```

**Path parameters**

<table id="bkmrk-parameter-type-descr-1"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`profile_id`</td><td>integer</td><td>Profile identifier</td></tr></tbody></table>

**Success response**

```text
200 OK

```

```json
[
  {
    "profile_id": 1,
    "app_id": 12,
    "status": "allow"
  },
  {
    "profile_id": 1,
    "app_id": 93,
    "status": "deny"
  }
]

```

**Add one application rule**

```text
POST /profile/{profile_id}/app_aware

```

**Path parameters**

<table id="bkmrk-parameter-type-descr-2"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`profile_id`</td><td>integer</td><td>Profile identifier</td></tr></tbody></table>

**Request body**

```json
{
  "app_id": 12,
  "status": "allow"
}

```

`status` may be `allow` or `deny`. The schema default is `deny`, but clients should set the value explicitly.

**Success response**

```text
201 Created

```

```json
{
  "profile_id": 1,
  "app_id": 12,
  "status": "allow"
}

```

**Add several application rules**

```text
POST /profile/{profile_id}/app_aware/batch

```

All submitted applications receive the same status.

**Request body**

```json
{
  "app_ids": [1, 12, 93],
  "status": "deny"
}

```

**Success response**

```text
201 Created

```

```json
[
  {
    "profile_id": 1,
    "app_id": 1,
    "status": "deny"
  },
  {
    "profile_id": 1,
    "app_id": 12,
    "status": "deny"
  },
  {
    "profile_id": 1,
    "app_id": 93,
    "status": "deny"
  }
]

```

**Delete an application rule**

```text
DELETE /profile/{profile_id}/app_aware/{app_id}

```

The current OpenAPI document names the final parameter `app_aware_id`, but the API uses the application ID itself.

**Path parameters**

<table id="bkmrk-parameter-type-descr-3"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`profile_id`</td><td>integer</td><td>Profile identifier</td></tr><tr><td>`app_id`</td><td>integer</td><td>Application identifier</td></tr></tbody></table>

**Success response**

```text
204 No Content

```

There is no request body.

To replace or change the complete application configuration predictably, use `PATCH /profiles/{profile_id}` with the full desired `apps` collection.

## Block pages

**Retrieve all block pages**

```text
GET /blockpage/

```

**Success response**

```text
200 OK

```

```json
[
  {
    "id": 1,
    "type": 0
  },
  {
    "id": 2,
    "type": 1
  }
]

```

**Create a block page**

```text
POST /blockpage/

```

**Request body**

```json
{
  "id": 2,
  "type": 1
}

```

`type` must be `0` or `1`.

**Success response**

```text
201 Created

```

```json
{
  "id": 2,
  "type": 1
}

```

**Retrieve a block page**

```text
GET /blockpage/{page_id}

```

**Path parameters**

<table id="bkmrk-parameter-type-descr-4"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`page_id`</td><td>integer</td><td>Block-page identifier</td></tr></tbody></table>

**Success response**

```text
200 OK

```

The response body contains a block-page object.

**Update a block page**

```text
PATCH /blockpage/{page_id}

```

**Request body**

```json
{
  "type": 0
}

```

**Success response**

```text
200 OK

```

The response body contains the updated block-page object.

**Delete a block page**

```text
DELETE /blockpage/{page_id}

```

**Success response**

```text
204 No Content

```

There is no request body.

## Domain allowlists and denylists

Shield stores submitted domains as hashes. There is no GET operation for listing the original domains in a profile's allowlist or denylist.

Supported rule types are:

- `allow`
- `deny`

Clients should always send `type` explicitly.

**Add one domain**

```text
POST /profile/{profile_id}/bw_list

```

**Path parameters**

<table id="bkmrk-parameter-type-descr-5"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`profile_id`</td><td>integer</td><td>Profile identifier</td></tr></tbody></table>

**Request body**

```json
{
  "type": "allow",
  "domain": "example.com"
}

```

**Success response**

```text
201 Created

```

The API returns the stored database representation:

```json
{
  "profile_id": 1,
  "domain_hash": 123456789,
  "cat_id": 0
}

```

**Add several domains**

```text
POST /profile/{profile_id}/bw_list/batch

```

**Request body**

```json
{
  "type": "deny",
  "domains": [
    "example1.com",
    "example2.com",
    "example3.com"
  ]
}

```

**Success response**

```text
201 Created

```

The response is an array of stored database representations.

**Update a domain rule**

```text
PATCH /profile/{profile_id}/bw_list/{domain}

```

Changes the allow/deny state of a stored domain rule.

**Path parameters**

<table id="bkmrk-parameter-type-descr-6"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`profile_id`</td><td>integer</td><td>Profile identifier</td></tr><tr><td>`domain`</td><td>string</td><td>Original submitted domain</td></tr></tbody></table>

**Request body**

The current API schema includes the domain and profile ID in the body as well as the path:

```json
{
  "type": "allow",
  "domain": "example.com",
  "profile_id": 1
}

```

**Success response**

```text
200 OK

```

The response uses the stored domain-rule representation containing `profile_id`, `domain_hash`, and `cat_id`.

**Delete a domain rule**

```text
DELETE /profile/{profile_id}/bw_list/{domain}

```

**Path parameters**

<table id="bkmrk-parameter-type-descr-7"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`profile_id`</td><td>integer</td><td>Profile identifier</td></tr><tr><td>`domain`</td><td>string</td><td>Original submitted domain</td></tr></tbody></table>

**Success response**

```text
204 No Content

```

There is no request body.

## IPv4 networks

Supported prefix lengths are `/10` through `/32`.

**Retrieve all IPv4 assignments**

```text
GET /net/

```

**Success response**

```text
200 OK

```

```json
[
  {
    "ip": "100.100.100.101",
    "profile_id": 1,
    "prefix_len": 32
  },
  {
    "ip": "100.100.101.0",
    "profile_id": 2,
    "prefix_len": 24
  }
]

```

**Create an IPv4 assignment**

```text
POST /net/

```

**Request body**

Host assignment:

```json
{
  "ip": "100.100.100.101",
  "profile_id": 1,
  "prefix_len": 32
}

```

Subnet assignment:

```json
{
  "ip": "100.100.101.0",
  "profile_id": 1,
  "prefix_len": 24
}

```

**Success response**

```text
201 Created

```

The response body contains the created IPv4 network object.

**Retrieve an IPv4 assignment**

```text
GET /net/{int_ip}

```

`int_ip` is the unsigned integer representation of the address or network address.

Example:

```text
GET /net/1684301056

```

retrieves the record identified by `100.100.101.0`.

**Success response**

```text
200 OK

```

The response body contains an IPv4 network object.

**Update an IPv4 assignment**

```text
PATCH /net/{int_ip}

```

**Request body**

```json
{
  "ip": "100.100.101.0",
  "profile_id": 2,
  "prefix_len": 24
}

```

**Success response**

```text
200 OK

```

The response body contains the updated IPv4 network object.

**Delete an IPv4 assignment**

```text
DELETE /net/{int_ip}

```

**Success response**

```text
204 No Content

```

There is no request body.

## IPv6 networks

Supported prefix lengths are `/16` through `/128`.

**Retrieve all IPv6 assignments**

```text
GET /net6/

```

**Success response**

```text
200 OK

```

```json
[
  {
    "ip": "2001:db8:100::",
    "profile_id": 1,
    "prefix_len": 64
  }
]

```

**Create an IPv6 assignment**

```text
POST /net6/

```

**Request body**

```json
{
  "ip": "2001:db8:100::",
  "profile_id": 1,
  "prefix_len": 64
}

```

**Success response**

```text
201 Created

```

The response body contains the created IPv6 network object.

**Retrieve an IPv6 assignment**

```text
GET /net6/{ipv6}

```

**Path parameters**

<table id="bkmrk-parameter-type-descr-8"><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`ipv6`</td><td>IPv6 string</td><td>IPv6 address or network address identifying the record</td></tr></tbody></table>

**Success response**

```text
200 OK

```

The response body contains an IPv6 network object.

**Update an IPv6 assignment**

```text
PATCH /net6/{ipv6}

```

**Request body**

```json
{
  "ip": "2001:db8:100::",
  "profile_id": 2,
  "prefix_len": 64
}

```

**Success response**

```text
200 OK

```

The response body contains the updated IPv6 network object.

**Delete an IPv6 assignment**

```text
DELETE /net6/{ipv6}

```

**Success response**

```text
204 No Content

```

There is no request body.

## NAPT assignments

NAPT assignments map inclusive source-port ranges on a shared IPv4 address to filtering profiles.

Port constraints:

- Minimum: `0`
- Maximum: `65535`
- Default lower bound: `0`
- Default upper bound: `65535`
- Both boundaries are included in the assignment.

Individual resource paths use integer `int_ip`. Request and response bodies use dotted-decimal `ip` strings.

**Create a NAPT assignment**

```text
POST /napt/

```

**Request body**

```json
{
  "ip": "203.0.113.10",
  "lower_port_bound": 10000,
  "upper_port_bound": 19999,
  "profile_id": 1
}

```

**Success response**

```text
201 Created

```

The response body contains the created NAPT object.

**Create several NAPT assignments**

```text
POST /napt/batch

```

**Request body**

```json
[
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 10000,
    "upper_port_bound": 19999,
    "profile_id": 1
  },
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 20000,
    "upper_port_bound": 29999,
    "profile_id": 2
  }
]

```

**Success response**

```text
201 Created

```

The response is an array of created NAPT objects.

**Retrieve all NAPT assignments for an IPv4 address**

```text
GET /napt/{int_ip}

```

Example:

```text
GET /napt/3405803786

```

`3405803786` is the integer representation of `203.0.113.10`.

**Success response**

```text
200 OK

```

```json
[
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 10000,
    "upper_port_bound": 19999,
    "profile_id": 1
  },
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 20000,
    "upper_port_bound": 29999,
    "profile_id": 2
  }
]

```

**Retrieve one NAPT assignment**

```text
GET /napt/{int_ip}/{lower_port_bound}/{upper_port_bound}

```

Example:

```text
GET /napt/3405803786/10000/19999

```

**Success response**

```text
200 OK

```

The response body contains one NAPT object.

**Update a NAPT assignment**

```text
PATCH /napt/{int_ip}/{lower_port_bound}/{upper_port_bound}

```

The path identifies the current record. The body contains the desired resulting values.

**Request body**

```json
{
  "ip": "203.0.113.10",
  "lower_port_bound": 10000,
  "upper_port_bound": 19999,
  "profile_id": 2
}

```

**Success response**

```text
200 OK

```

The response body contains the updated NAPT object.

**Delete one NAPT assignment**

```text
DELETE /napt/{int_ip}/{lower_port_bound}/{upper_port_bound}

```

**Success response**

```text
204 No Content

```

There is no request body.

**Delete several NAPT assignments**

```text
DELETE /napt/batch

```

The batch-deletion request uses dotted-decimal `ip` strings, not integer `int_ip` values. It does not include `profile_id`.

**Request body**

```json
[
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 10000,
    "upper_port_bound": 19999
  },
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 20000,
    "upper_port_bound": 29999
  }
]

```

**Success response**

```text
204 No Content

```

## Profile schedules

A schedule switches the profile identified by `{profile_id}` to `target_profile_id` during configured periods.

The source profile and target profile must be different.

**Day and time values**

`day` accepts either:

- A lowercase weekday name: `monday` through `sunday`.
- An integer from `0` through `6`, where `0` is Monday and `6` is Sunday.

`time` uses `HH:MM` format. Valid times fall on 30-minute boundaries. `00:00` and `24:00` are supported.

**Retrieve a schedule**

```text
GET /profile/{profile_id}/schedule

```

**Success response**

```text
200 OK

```

```json
{
  "target_profile_id": 2,
  "periods": [
    {
      "start": {
        "day": "monday",
        "time": "18:00"
      },
      "end": {
        "day": "tuesday",
        "time": "08:00"
      }
    }
  ]
}

```

**Create a schedule**

```text
POST /profile/{profile_id}/schedule

```

**Request body**

```json
{
  "target_profile_id": 2,
  "periods": [
    {
      "start": {
        "day": "monday",
        "time": "18:00"
      },
      "end": {
        "day": "tuesday",
        "time": "08:00"
      }
    },
    {
      "start": {
        "day": 5,
        "time": "00:00"
      },
      "end": {
        "day": 5,
        "time": "24:00"
      }
    }
  ]
}

```

**Success response**

```text
201 Created

```

The response body contains the created schedule.

**Update a schedule**

```text
PATCH /profile/{profile_id}/schedule

```

The request may update the target profile, the periods, or both.

**Change the target profile**

```json
{
  "target_profile_id": 3
}

```

When only `target_profile_id` is supplied, it is changed for all existing periods.

**Replace all periods**

```json
{
  "periods": [
    {
      "start": {
        "day": "friday",
        "time": "18:00"
      },
      "end": {
        "day": "monday",
        "time": "08:00"
      }
    }
  ]
}

```

When `periods` is supplied, all previously configured periods are deleted and replaced by the submitted collection.

**Success response**

```text
200 OK

```

The response body contains the updated schedule.

**Delete a schedule**

```text
DELETE /profile/{profile_id}/schedule

```

**Success response**

```text
204 No Content

```

There is no request body.

## Complete curl examples

**Retrieve profiles**

```bash
curl \
  --request GET \
  --header "Authorization: Bearer <token>" \
  --header "Accept: application/json" \
  "<base-url>/profiles/"

```

**Create an IPv4 assignment**

```bash
curl \
  --request POST \
  --header "Authorization: Bearer <token>" \
  --header "Content-Type: application/json" \
  --data '{
    "ip": "100.100.100.101",
    "profile_id": 1,
    "prefix_len": 32
  }' \
  "<base-url>/net/"

```

**Replace profile categories and application rules**

```bash
curl \
  --request PATCH \
  --header "Authorization: Bearer <token>" \
  --header "Content-Type: application/json" \
  --data '{
    "cat_ids": [3, 4, 12, 66],
    "apps": [
      {
        "app_id": 12,
        "status": "allow"
      },
      {
        "app_id": 93,
        "status": "deny"
      }
    ]
  }' \
  "<base-url>/profiles/1"

```