Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
eb83e8b
Initial plan
Copilot Feb 12, 2026
298dc92
Implement multi-domain support with alias zones
Copilot Feb 12, 2026
78a5234
Improve variable naming clarity in DNS handler
Copilot Feb 12, 2026
62a8651
Update documentation and Docker support for multi-domain feature
Copilot Feb 12, 2026
033d2d0
Simplify dns_server_udp_handler._update_response()
indisoluble Feb 12, 2026
87dfc11
dns_server_config_factory.make_config(): ensure
indisoluble Feb 12, 2026
527e6d4
DnsServerUdpHandler: return NOERROR for missing
indisoluble Feb 15, 2026
ee751cc
dns_server_config_factory._make_alias_zones(): simplify validation logic
indisoluble Feb 15, 2026
d8748cd
dns_server_config_factory.make_config(): simplify validation logic
indisoluble Feb 15, 2026
60a2be9
dns_server_config_factory.make_config(): simplify validation logic
indisoluble Feb 15, 2026
cc1024e
Set default value for alias zones to an empty list
indisoluble Feb 15, 2026
0f8b791
Set default value for alias zones to an empty list
indisoluble Feb 15, 2026
b6b4569
Define class ZoneOrigins
indisoluble Feb 15, 2026
d8f52d0
Integrate ZoneOrigins class into DNS config and handler
Copilot Feb 15, 2026
1216b0f
Clean up config factory: remove unused function and redundant validation
Copilot Feb 16, 2026
7883209
Simplify _make_zone_origins: remove unnecessary logging and restructu…
Copilot Feb 16, 2026
e45432b
Clean up config factory: remove unused function
indisoluble Feb 16, 2026
ab18993
Clean up config factory: remove unused function
indisoluble Feb 16, 2026
7d4a2dd
Remove unnecessary comments/texts
indisoluble Feb 16, 2026
30677c1
Dockerfile: small adjustments to the entrypoint
indisoluble Feb 16, 2026
b0762e5
README: small reformat and reduce length
indisoluble Feb 16, 2026
6a42800
Add __eq__, __hash__, __repr__ to ZoneOrigins and refactor test fixtures
Copilot Feb 17, 2026
ac6aa93
Reformat ZoneOrigins
indisoluble Feb 17, 2026
3bc0824
Simplify tests
indisoluble Feb 17, 2026
5235d2c
Simplify tests for main
indisoluble Feb 18, 2026
2d88566
Simplify tests for main
indisoluble Feb 18, 2026
c96b11f
Extend test for DNS server config factory
indisoluble Feb 20, 2026
817699f
Extend coverage for DnsServerUdpHandler
indisoluble Feb 21, 2026
1f8633f
Extend test cases for ZoneOrigins
indisoluble Feb 21, 2026
5992d77
Validate more consistently the configured provided
indisoluble Feb 21, 2026
e724bf9
Increase version to 0.1.26 and update cryptography
indisoluble Feb 21, 2026
baa4e2b
Update docker-compose.example.yml with new params
indisoluble Feb 21, 2026
d992207
Extend test-docker to cover multi-domain support
indisoluble Feb 21, 2026
6de9c08
Extend test-docker to cover multi-domain support
indisoluble Feb 21, 2026
0b9d4fc
Extend test-docker to cover multi-domain support
indisoluble Feb 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 104 additions & 11 deletions .github/workflows/test-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,122 @@ jobs:
# Verify the image was created
docker images a-healthy-dns:test

- name: test docker image with minimal configuration
- name: test docker image with alias zone configuration
run: |
# Start the DNS server in the background with minimal valid config
# Create an isolated network for deterministic container-to-container checks
docker network create --subnet 172.28.0.0/24 a-healthy-dns-test-net

# Start backend service that health checks can reach
docker run -d \
--name a-healthy-dns-backend \
--network a-healthy-dns-test-net \
--ip 172.28.0.10 \
nginx:alpine

# Start DNS server with hosted zone plus alias zones
docker run -d \
--name a-healthy-dns-test \
--network a-healthy-dns-test-net \
-p 53053:53053/udp \
-e DNS_HOSTED_ZONE="test.example.com" \
-e DNS_ZONE_RESOLUTIONS='{"www":{"ips":["127.0.0.1"],"health_port":80}}' \
-e DNS_ALIAS_ZONES='["test.other.com","test.another.com"]' \
-e DNS_ZONE_RESOLUTIONS='{"www":{"ips":["172.28.0.10"],"health_port":80}}' \
-e DNS_NAME_SERVERS='["ns1.test.example.com"]' \
-e DNS_PORT="53053" \
-e DNS_TEST_MIN_INTERVAL="1" \
-e DNS_TEST_TIMEOUT="1" \
-e DNS_LOG_LEVEL="debug" \
a-healthy-dns:test

- name: wait for dns server to start
run: |
# Wait a bit for the server to fully start
sleep 10
sleep 5

# Check if container is still running
# Check if containers are still running
docker ps | grep a-healthy-dns-backend
docker ps | grep a-healthy-dns-test

- name: test dns server functionality
run: |
set -euo pipefail

# Install dig for testing
sudo apt-get update && sudo apt-get install -y dnsutils

# Test DNS query (should get a response, even if NXDOMAIN)
# Using timeout to avoid hanging
timeout 10s dig @127.0.0.1 -p 53053 www.test.example.com || echo "DNS query completed (expected behavior for test)"

DNS_HOST="127.0.0.1"
DNS_PORT="53053"
BACKEND_IP="172.28.0.10"
EXPECTED_NS="ns1.test.example.com."

wait_for_a_record() {
local fqdn="$1"
local answer

for _ in $(seq 1 20); do
answer="$(dig +short +time=1 +tries=1 @"${DNS_HOST}" -p "${DNS_PORT}" "${fqdn}" A)"
printf '%s\n' "${answer}" | grep -qx "${BACKEND_IP}" && {
echo "[OK] A ${fqdn}"
return 0
}
sleep 1
done

echo "[FAIL] A ${fqdn}"
dig +nocmd +noall +comments +answer @"${DNS_HOST}" -p "${DNS_PORT}" "${fqdn}" A || true
return 1
}

assert_dns_status() {
local fqdn="$1"
local rtype="$2"
local expected_status="$3"
local output
local actual_status

output="$(dig +time=1 +tries=1 +noall +comments @"${DNS_HOST}" -p "${DNS_PORT}" "${fqdn}" "${rtype}")"
actual_status="$(printf '%s\n' "${output}" | sed -n 's/.*status: \([A-Z]*\).*/\1/p' | head -n 1)"

if [ "${actual_status}" = "${expected_status}" ]; then
echo "[OK] ${rtype} ${fqdn} ${actual_status}"
return 0
fi

echo "[FAIL] ${rtype} ${fqdn} expected=${expected_status} got=${actual_status:-none}"
printf '%s\n' "${output}"
return 1
}

assert_ns_record() {
local zone="$1"
local ns_answer

ns_answer="$(dig +short +time=1 +tries=1 @"${DNS_HOST}" -p "${DNS_PORT}" "${zone}" NS)"
printf '%s\n' "${ns_answer}" | grep -qx "${EXPECTED_NS}" || {
echo "[FAIL] NS ${zone}"
printf '%s\n' "${ns_answer}"
exit 1
}
echo "[OK] NS ${zone}"
}

for fqdn in \
"www.test.example.com" \
"www.test.other.com" \
"www.test.another.com"; do
wait_for_a_record "${fqdn}"
done

for zone in \
"test.example.com" \
"test.other.com" \
"test.another.com"; do
assert_ns_record "${zone}"
done

assert_dns_status "www.test.other.com" "AAAA" "NOERROR"
assert_dns_status "missing.test.other.com" "A" "NXDOMAIN"
assert_dns_status "www.not-configured.example.org" "A" "NXDOMAIN"

- name: test docker-compose configuration
run: |
Expand All @@ -66,17 +154,22 @@ jobs:
sudo apt-get install -y docker-compose

# Validate the compose file syntax
docker-compose -f docker-compose.example.yml config > /dev/null && echo " docker-compose.example.yml is valid"
docker-compose -f docker-compose.example.yml config > /dev/null && echo "[OK] docker-compose.example.yml is valid"
else
echo "docker-compose.example.yml not found"
fi

- name: cleanup
if: always()
run: |
# Stop and remove test container
# Stop and remove test containers
docker stop a-healthy-dns-backend || true
docker stop a-healthy-dns-test || true
docker rm a-healthy-dns-backend || true
docker rm a-healthy-dns-test || true

# Remove test network
docker network rm a-healthy-dns-test-net || true

# Remove test image
docker rmi a-healthy-dns:test || true
38 changes: 28 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,16 @@ ENV PATH="/home/appuser/.local/bin:$PATH" \
WORKDIR /app

# Default environment variables for all parameters
ENV DNS_HOSTED_ZONE="" \
ENV DNS_PORT="53" \
DNS_LOG_LEVEL="" \
DNS_HOSTED_ZONE="" \
DNS_ALIAS_ZONES="" \
DNS_ZONE_RESOLUTIONS="" \
DNS_TEST_MIN_INTERVAL="" \
DNS_TEST_TIMEOUT="" \
DNS_NAME_SERVERS="" \
DNS_PORT="53" \
DNS_LOG_LEVEL="info" \
DNS_TEST_MIN_INTERVAL="30" \
DNS_TEST_TIMEOUT="2" \
DNS_PRIV_KEY_PATH="" \
DNS_PRIV_KEY_ALG="RSASHA256"
DNS_PRIV_KEY_ALG=""

# Expose the default DNS port (static at build time)
EXPOSE 53/udp
Expand All @@ -94,16 +95,33 @@ ENTRYPOINT ["tini", "--", "sh", "-c", "\
echo 'Error: DNS_NAME_SERVERS environment variable is required'; \
exit 1; \
fi; \
ARGS=\"--hosted-zone $DNS_HOSTED_ZONE\"; \
ARGS=\"$ARGS --zone-resolutions $DNS_ZONE_RESOLUTIONS\"; \
ARGS=\"$ARGS --ns $DNS_NAME_SERVERS\"; \
ARGS=\"$ARGS --port $DNS_PORT\"; \
ARGS=\"--port $DNS_PORT\"; \
if [ -n \"$DNS_LOG_LEVEL\" ]; then \
ARGS=\"$ARGS --log-level $DNS_LOG_LEVEL\"; \
fi; \
if [ -n \"$DNS_HOSTED_ZONE\" ]; then \
ARGS=\"$ARGS --hosted-zone $DNS_HOSTED_ZONE\"; \
fi; \
if [ -n \"$DNS_ALIAS_ZONES\" ]; then \
ARGS=\"$ARGS --alias-zones $DNS_ALIAS_ZONES\"; \
fi; \
if [ -n \"$DNS_ZONE_RESOLUTIONS\" ]; then \
ARGS=\"$ARGS --zone-resolutions $DNS_ZONE_RESOLUTIONS\"; \
fi; \
if [ -n \"$DNS_TEST_MIN_INTERVAL\" ]; then \
ARGS=\"$ARGS --test-min-interval $DNS_TEST_MIN_INTERVAL\"; \
fi; \
if [ -n \"$DNS_TEST_TIMEOUT\" ]; then \
ARGS=\"$ARGS --test-timeout $DNS_TEST_TIMEOUT\"; \
fi; \
if [ -n \"$DNS_NAME_SERVERS\" ]; then \
ARGS=\"$ARGS --ns $DNS_NAME_SERVERS\"; \
fi; \
if [ -n \"$DNS_PRIV_KEY_PATH\" ]; then \
ARGS=\"$ARGS --priv-key-path $DNS_PRIV_KEY_PATH\"; \
fi; \
if [ -n \"$DNS_PRIV_KEY_ALG\" ]; then \
ARGS=\"$ARGS --priv-key-alg $DNS_PRIV_KEY_ALG\"; \
fi; \
echo \"Starting a-healthy-dns with arguments: $ARGS\"; \
exec a-healthy-dns $ARGS"]
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This ensures that DNS queries only return healthy endpoints, providing automatic

- **Health Checking**: Continuously monitors IP addresses via TCP connectivity tests
- **Dynamic Updates**: Automatically updates DNS zones based on health check results
- **Multi-Domain Support**: Serve multiple domains with the same records without duplicating health checks
- **Configurable TTL**: TTL calculation based on health check intervals
- **Threaded Operations**: Background health checking
- **Multiple Records**: Support for multiple IP addresses per subdomain with individual health tracking
Expand Down Expand Up @@ -120,6 +121,7 @@ a-healthy-dns \
- `--test-min-interval`: Minimum interval between connectivity tests in seconds (default: 30)
- `--test-timeout`: Maximum time to wait for health check response in seconds (default: 2)
- `--log-level`: Logging level (debug, info, warning, error, critical) (default: info)
- `--alias-zones`: Additional domain names that resolve to the same records as the hosted zone (JSON array, default: [])

#### DNSSEC Parameters

Expand Down Expand Up @@ -161,6 +163,23 @@ The `--zone-resolutions` parameter accepts a JSON object with the following stru
- Health checks run continuously in the background at the configured interval
- TTL values are automatically calculated based on health check intervals

### Multi-Domain Support

The DNS server supports serving multiple domains that resolve to the same IP addresses without duplicating health checks. This is achieved through the `--alias-zones` parameter:

```bash
a-healthy-dns \
--hosted-zone primary.com \
--alias-zones '["alias1.com", "alias2.com"]' \
--zone-resolutions '{"www": {"ips": ["192.168.1.100"], "health_port": 8080}}' \
--ns '["ns1.primary.com"]'
```

With this configuration:
- `www.primary.com` → resolves to `192.168.1.100`
- `www.alias1.com` → resolves to `192.168.1.100` (same IP)
- `www.alias2.com` → resolves to `192.168.1.100` (same IP)

### Example Deployment

For a deployment serving `example.com`:
Expand Down Expand Up @@ -232,6 +251,7 @@ docker-compose up -d
- `DNS_LOG_LEVEL`: Logging level (default: info)
- `DNS_TEST_MIN_INTERVAL`: Minimum interval between connectivity tests in seconds (default: 30)
- `DNS_TEST_TIMEOUT`: Timeout for each connection test in seconds (default: 2)
- `DNS_ALIAS_ZONES`: Additional domain names that resolve to the same records (JSON array, default: [])
- `DNS_PRIV_KEY_PATH`: Path to DNSSEC private key PEM file
- `DNS_PRIV_KEY_ALG`: DNSSEC private key algorithm (default: RSASHA256)

Expand Down
9 changes: 5 additions & 4 deletions docker-compose.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ services:
DNS_HOSTED_ZONE: "example.com"
DNS_ZONE_RESOLUTIONS: '{"www":{"ips":["192.168.1.100","192.168.1.101"],"health_port":8080},"api":{"ips":["192.168.1.102"],"health_port":8000}}'
DNS_NAME_SERVERS: '["ns1.example.com", "ns2.example.com"]'

# Optional parameters (with their default values)
# DNS_PORT: "53053"
# DNS_LOG_LEVEL: "info"
# DNS_ALIAS_ZONES: '[]'
# DNS_TEST_MIN_INTERVAL: "30"
# DNS_TEST_TIMEOUT: "2"
# DNS_PRIV_KEY_ALG: "RSASHA256"

# Optional DNSSEC private key path (if you have one)

# Optional DNSSEC parameters (if you have one)
# DNS_PRIV_KEY_PATH: "/app/keys/private.pem"
# DNS_PRIV_KEY_ALG: "RSASHA256"

# volumes:
# Mount a directory for DNSSEC keys (optional)
Expand Down
Loading