Apps/SplitHorizonApp/README.md
A DNS App (plugin) for Technitium DNS Server that enables split-horizon DNS functionality and address translation, allowing administrators to serve different DNS responses and translate IP addresses based on the network location of the requesting client.
This application is designed to:
It integrates with the Technitium DNS Server runtime to provide network-aware DNS resolution and address translation within primary, secondary, forwarder, and stub zones.
The Split Horizon App extends the core DNS server functionality by providing network-based DNS response differentiation and automatic IP address translation.
Its primary functions include:
This application is intended to support administrators in implementing policy-driven, network-segmented, and topology-aware DNS controls.
Configuration for Split Horizon App is stored in:
dnsApp.config
The configuration file defines two independent feature sets:
All supported options are documented below. Unspecified parameters use default values.
This feature allows you to create APP records in primary and forwarder zones that return different sets of A, AAAA, or CNAME records based on the client's network.
To respond with different records to different clients:
SplitHorizon.SimpleAddress for A and AAAA recordsSplitHorizon.SimpleCNAME for CNAME recordsEach APP record is configured with a JSON document defining network-to-address mappings.
{
"public": [
"1.1.1.1",
"2.2.2.2"
],
"private": [
"192.168.1.1",
"::1"
],
"custom-networks": [
"172.16.1.1"
],
"10.0.0.0/8": [
"10.1.1.1"
]
}
{
"public": "api.example.com",
"private": "api.example.corp",
"custom-networks": "custom.example.corp",
"10.0.0.0/8": "api.intranet.example.corp"
}
Keys can be one of the following:
| Key Type | Description | Example |
|---|---|---|
| Network CIDR | Specific network in CIDR notation | "10.0.0.0/8", "2001:db8::/32" |
| Named Network | Custom network name defined in global configuration | "custom-networks" |
private | RFC 1918 private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) | "private" |
public | All IPs outside RFC 1918 private ranges | "public" |
Important: Clients not matching any defined network are processed as if the APP record doesn't exist, falling through to other record types (e.g., FWD records).
Translates IP addresses in DNS responses for A and AAAA requests based on the client's network address and configured 1:1 translation rules. Also supports reverse (PTR) queries for translated addresses.
This feature operates as both a post-processor (modifies responses before delivery) and a request handler (serves authoritative responses for reverse lookups).
| Property | Type | Default | Description |
|---|---|---|---|
appPreference | Integer | 40 | App execution order when multiple apps implement IDnsApplicationPreference |
networks | Object | {} | Map of custom network names to arrays of CIDR addresses |
enableAddressTranslation | Boolean | false | Master switch to enable/disable address translation globally |
domainGroupMap | Object | {} | Maps queried domains to named translation groups; longest matching domain takes precedence |
networkGroupMap | Object | {} | Maps client networks (CIDR) to named translation groups |
groups | Array | [] | Array of translation group configurations |
The networks object defines named network collections that can be referenced in APP records.
Purpose: Centralize network definitions for reuse across multiple APP records.
Example:
"networks": {
"custom-networks": [
"172.16.1.0/24",
"172.16.10.0/24",
"172.16.2.1"
],
"branch-offices": [
"10.100.0.0/16",
"10.101.0.0/16"
]
}
Formatting Rules:
Maps client source networks to translation groups using most-specific subnet matching.
When both domainGroupMap and networkGroupMap are configured, domain-based matching takes precedence for forward A/AAAA translation. The app first looks for the longest matching domain in domainGroupMap; if no domain match is found, it falls back to networkGroupMap and then uses the most-specific network match.
Example:
"networkGroupMap": {
"10.0.0.0/8": "local1",
"172.16.0.0/12": "local2",
"192.168.0.0/16": "local3",
"192.168.1.0/24": "local1"
}
Matching Logic:
192.168.1.10 matches 192.168.1.0/24 (local1) instead of 192.168.0.0/16 (local3)Each group in the groups array defines translation behavior for clients matched to that group.
| Property | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Unique identifier matching a key in networkGroupMap |
enabled | Boolean | No (default: true) | Enables/disables translation for this group |
translateReverseLookups | Boolean | No (default: false) | Enables PTR query translation for internal IPs |
externalToInternalTranslation | Object | Yes | Map of external network ranges to internal network ranges |
Maps external (public) IP ranges to internal (private) IP ranges using 1:1 translation.
Rules:
Example:
"externalToInternalTranslation": {
"1.2.3.0/24": "10.0.0.0/24",
"5.6.7.8/32": "10.0.0.5/32"
}
Translation behavior:
1.2.3.4 → Internal IP 10.0.0.45.6.7.8 → Internal IP 10.0.0.5The following example demonstrates a complete and valid configuration using both features.
{
"networks": {
"custom-networks": [
"172.16.1.0/24",
"172.16.10.0/24",
"172.16.2.1"
],
"branch-offices": [
"10.100.0.0/16"
]
},
"enableAddressTranslation": true,
"networkGroupMap": {
"10.0.0.0/8": "local1",
"172.16.0.0/12": "local2",
"192.168.0.0/16": "local3"
},
"groups": [
{
"name": "local1",
"enabled": true,
"translateReverseLookups": true,
"externalToInternalTranslation": {
"1.2.3.0/24": "10.0.0.0/24",
"5.6.7.8/32": "10.0.0.5/32"
}
},
{
"name": "local2",
"enabled": true,
"translateReverseLookups": true,
"externalToInternalTranslation": {
"1.2.3.4/32": "172.16.0.4/32",
"5.6.7.8/32": "172.16.0.5/32"
}
},
{
"name": "local3",
"enabled": true,
"translateReverseLookups": true,
"externalToInternalTranslation": {
"1.2.3.4/32": "192.168.0.4/32",
"5.6.7.8/32": "192.168.0.5/32"
}
}
]
}
The app supports both IPv4 and IPv6 network specifications in CIDR notation.
192.168.1.0/24 – Standard Class C network (256 addresses)10.0.0.0/8 – Entire private Class A range (16.7M addresses)172.16.0.0/12 – Private Class B range (1M addresses)203.0.113.0/26 – Subnet with 64 addresses192.168.1.100/32 – Single host2001:db8::/32 – Documentation prefixfd00::/8 – Unique local addresses (ULA)fe80::/10 – Link-local addresses::/0 – All IPv6 addresses2001:db8::1/128 – Single hostThe internal processing pipeline follows these steps:
Query Reception and Group Matching
Client source IP is matched against networkGroupMap using most-specific subnet matching to determine the translation group.
Response Filtering
Translation is applied only if:
NoErrorenabled: trueIP Address Translation
For each A or AAAA record in the response:
externalToInternalTranslation mappingsResponse Delivery
Modified response with translated IPs is returned to the client.
Note: NXDOMAIN, SERVFAIL, and NODATA responses are passed through unmodified.
When translateReverseLookups is enabled for a group:
PTR Query Detection
App identifies PTR queries for domains in the in-addr.arpa or ip6.arpa namespaces.
Internal IP Matching
If the queried IP falls within an internal network range defined in externalToInternalTranslation:
CNAME Response Generation
App returns a CNAME record pointing to the PTR domain of the corresponding external IP.
Example:
"1.2.3.0/24": "10.0.0.0/24"4.0.0.10.in-addr.arpa (internal IP 10.0.0.4)CNAME 4.3.2.1.in-addr.arpa (external IP 1.2.3.4)The APP record processing pipeline:
Query Interception
DNS query is received for a domain with an APP record configured.
Client Network Identification
Client source IP is extracted and evaluated against:
networks configurationpublic, private)Network Matching
First matching network key is selected using this priority:
private or public matchResponse Generation
Fallback Handling
If no network matches, the APP record is ignored and processing continues with other record types (FWD, A, AAAA, etc.).
Organizations with separate internal and external DNS namespaces serve internal-only records (intranet, file servers, internal APIs) to corporate network clients while providing only public-facing records to external users.
Configuration approach: Use APP records with private and public keys to differentiate responses.
Remote workers connecting via VPN receive internal DNS records for corporate resources, while their non-VPN traffic uses public DNS, enabling seamless access to both corporate and internet resources.
Configuration approach: Map VPN subnet to a named network group with internal IP responses in APP records.
Hosting providers serve different DNS responses for the same domain based on the requesting client's network, enabling tenant isolation and customized DNS views for different customer segments.
Configuration approach: Define per-tenant network groups with APP records returning tenant-specific service endpoints.
Organizations using external public IPs for internal resources translate public DNS responses to internal RFC 1918 addresses for clients inside the network, avoiding hairpin NAT issues.
Configuration approach: Enable address translation with external-to-internal mappings for each client network group.
Organizations with multiple data centers direct clients to region-specific infrastructure by serving location-appropriate DNS records based on the client's network address.
Configuration approach: Use named networks representing geographic regions with APP records pointing to regional service endpoints.
Development teams receive DNS records pointing to staging infrastructure while production networks resolve to live systems, enabling parallel operation without namespace conflicts.
Configuration approach: Map development networks to staging CNAMEs, production networks to production CNAMEs in APP records.
Symptoms: Clients receive no response or unexpected IP addresses when querying domains with APP records configured.
Diagnostic Steps:
SplitHorizon.SimpleAddress or SplitHorizon.SimpleCNAME).public or private keywords to verify basic functionality.Resolution:
networks configuration and reference it in the APP record.192.168.1.0/24, not 192.168.1.*).Symptoms: Clients receive external IP addresses instead of translated internal addresses in DNS responses.
Diagnostic Steps:
enableAddressTranslation is set to true in the global configuration.networkGroupMap.enabled: true.externalToInternalTranslation./24).Resolution:
networkGroupMap with the appropriate group name.Symptoms: PTR queries for internal IPs return NXDOMAIN or the actual internal hostname instead of the external IP's PTR record.
Diagnostic Steps:
translateReverseLookups is set to true for the client's group.externalToInternalTranslation.in-addr.arpa or ip6.arpa zone.Resolution:
translateReverseLookups for the appropriate group.Symptoms: APP records referencing named networks (e.g., "custom-networks") do not match clients from those networks.
Diagnostic Steps:
networks configuration.networks object.Resolution:
"networks": {
"custom-networks": [
"172.16.1.0/24"
]
}
Symptoms: Clients in overlapping network ranges receive inconsistent responses or translation behavior.
Diagnostic Steps:
networkGroupMap.Resolution:
/24) take precedence over broader ranges (e.g., /16).0.0.0.0/0, ::/0) for default behavior.Symptoms: Modifications to dnsApp.config or APP record configurations do not alter DNS resolution behavior.
Diagnostic Steps:
Resolution: