InterNetX DynDNS is a containerized PHP worker that keeps one explicitly configured DNS host in sync with the current public IP address of the machine or network running the container.
Current provider: InterNetX / AutoDNS / SchlundTech-related DNS. Current interface: InterNetX XML.
The default live XML API endpoint remains https://gateway.autodns.com. Runtime API calls use the XML auth_session flow: create a session, reuse its hash for this run, then close it.
The generic worker flow is separated from the DNS provider/interface implementation. Provider-specific details for the current InterNetX XML support are documented in docs/providers/internetx-xml.md. Future providers, or a future InterNetX interface such as JSON, can be added behind the provider layer without rewriting the worker core.
Source code is available on GitHub: worryboy/internetx-dyndns Container image is available on Docker Hub: worryboy/internetx-dyndns
- runs as a PHP CLI worker in a container
- detects the current public IPv4 address
- optionally detects the current public IPv6 address
- reads one configured
TARGET_HOSTor multipleTARGET_HOSTS - creates an InterNetX XML AuthSession before provider validation
- fetches the current InterNetX zone data with a non-mutating zone inquiry
- updates existing
Aand optionalAAAArecords only when the detected IP changed for each configured target - closes the InterNetX XML AuthSession at the end of the run
- stores last successful IP values in a persistent state directory
- supports
DRY_RUN=truefor safe local validation before any live update - logs diagnostics without logging passwords or secret credentials
Copy .env.example to .env and fill in the required values.
Required:
INTERNETX_HOST=https://gateway.autodns.com
INTERNETX_USER=your-user
INTERNETX_PASSWORD=your-api-password
INTERNETX_CONTEXT=9
TARGET_HOST=subleveldomain.domain.comFor multi-target mode, use:
TARGET_HOSTS=app1.example.com,app2.example.com,app3.example.comFor safe local validation, keep:
DRY_RUN=true
FORCE_UPDATE_ON_NO_CHANGE=false
RUN_ONCE=true
DEBUG=trueFor a real update run, set:
DRY_RUN=false
FORCE_UPDATE_ON_NO_CHANGE=false
RUN_ONCE=false
DEBUG=falseOptional Pushover notifications:
PUSHOVER_APP_KEY=your-pushover-app-token
PUSHOVER_USER_KEY=your-pushover-user-key
PUSHOVER_LOCATION_PREFIX=Home-ServerAll three values are required to enable notifications. If any of them are missing, Pushover stays disabled.
PUSHOVER_LOCATION_PREFIX is placed at the beginning of the notification message. The legacy PUSHOVER_LOCATION_NAME variable is accepted temporarily as a deprecated fallback, but PUSHOVER_LOCATION_PREFIX wins if both are set.
Example:
Zurich IPv4 Address: 109.40.176.130
Single-target mode stays simple:
TARGET_HOST=subleveldomain.domain.combecomes zonedomain.comand subdomainsubleveldomain.TARGET_HOST=vpn.office.example.combecomes zoneexample.comand subdomainvpn.office.- By default, the zone is inferred from the last two labels.
- Set
TARGET_ZONE=example.co.ukif the zone cannot be inferred from the last two labels.
Multi-target mode uses a comma-separated TARGET_HOSTS list:
TARGET_HOSTS=app1.example.com,app2.example.com,app3.example.com- all configured hosts use the same detected public IPv4/IPv6 values in a cycle
- the worker authenticates once, validates all targets, updates only the targets that need changes, then closes the session
TARGET_ZONEis only supported in single-target mode- if a multi-target hostname needs an explicit zone hint, use
TARGET_HOST_ZONES=host=zone,host=zone - if both
TARGET_HOSTandTARGET_HOSTSare set, startup fails fast with a clear warning because that configuration is ambiguous
This is useful when one public host runs several services behind a reverse proxy such as Traefik and several DNS names should always resolve to that same host IP.
Set TARGET_HOST to one full hostname, or TARGET_HOSTS to several full hostnames that should all follow the same current public IP address:
TARGET_HOST=sublevel.domain.tldTARGET_HOSTS=app1.domain.tld,app2.domain.tld,app3.domain.tldThe worker interprets that value as:
- full host:
sublevel.domain.tld - zone/domain:
domain.tld - host/subdomain label:
sublevel
If the zone cannot be inferred from the last two labels in single-target mode, set TARGET_ZONE explicitly:
TARGET_HOST=home.example.co.uk
TARGET_ZONE=example.co.ukIf you use multi-target mode and one hostname is ambiguous, add an explicit per-host zone hint:
TARGET_HOSTS=app.example.co.uk,service.example.com.au
TARGET_HOST_ZONES=app.example.co.uk=example.co.uk,service.example.com.au=example.com.auAutomatic last-two-label inference works well for normal names such as app.example.com and vpn.office.example.org, but it is intentionally rejected for common multi-label ccTLD patterns such as:
app.example.co.ukservice.example.com.au
In those cases the worker now fails early with a clear error instead of silently guessing the wrong zone.
DNS record types map directly to enabled protocols:
- IPv4 updates use the target host's
Arecord. - IPv6 updates use the target host's
AAAArecord.
Examples:
ENABLE_IPV4=truerequires anArecord forsublevel.domain.tld.ENABLE_IPV6=truerequires anAAAArecord forsublevel.domain.tld.- If both are enabled, both record types should exist.
This worker updates existing record values. It does not create missing DNS records.
TTL controls how long DNS resolvers may cache the old value before asking again. For DynDNS-style records with changing home or office IP addresses, a short TTL is usually best; 300 seconds is a common practical starting point. Very low TTLs can increase DNS query volume and may be constrained by your provider.
The worker does not manage TTL explicitly. It reads the existing zone record, replaces the A or AAAA value, and preserves the rest of the returned record data. If the existing record already has a TTL, that TTL should remain unchanged by this worker. Configure TTL in the InterNetX/AutoDNS zone before relying on the updater.
Before running live updates, check the zone for every configured target:
TARGET_HOSTor eachTARGET_HOSTSentry is exactly the hostname you want to update.- The inferred or configured zone is correct.
- The target
Arecord exists if IPv4 updates are enabled. - The target
AAAArecord exists if IPv6 updates are enabled. - The TTL is suitable for a dynamic IP use case, for example around
300seconds.
| Variable | Group | Required? | Meaning | Example |
|---|---|---|---|---|
INTERNETX_HOST |
required | Yes | InterNetX/AutoDNS XML API endpoint. | https://gateway.autodns.com |
INTERNETX_USER |
required | Yes | XML API username used to create and delete the AuthSession. | my-user |
INTERNETX_PASSWORD |
required | Yes | XML API password used to create and delete the AuthSession. Never logged. | secret |
INTERNETX_CONTEXT |
required | Yes | XML API auth context. This setup uses 9. |
9 |
TARGET_HOST |
required target selection | Yes, unless TARGET_HOSTS is used |
One fully qualified host this worker may validate and update. | subleveldomain.domain.com |
TARGET_HOSTS |
optional multi-target | No | Comma-separated fully qualified hosts that should all use the same detected public IPs in one cycle. Use instead of TARGET_HOST. |
app1.example.com,app2.example.com |
TARGET_HOST_ZONES |
optional multi-target | No | Comma-separated host=zone mappings for ambiguous multi-target hosts such as app.example.co.uk. |
app.example.co.uk=example.co.uk |
ENABLE_IPV4 |
core runtime | Yes | Enables IPv4 public IP detection and A record update decisions. At least one of IPv4/IPv6 must be enabled. |
true |
ENABLE_IPV6 |
core runtime | Yes | Enables IPv6 public IP detection and AAAA record update decisions. At least one of IPv4/IPv6 must be enabled. |
false |
PUBLIC_IPV4_PROVIDERS |
core runtime | Yes | Comma-separated public IPv4 detection endpoints. Used only when ENABLE_IPV4=true. |
https://api.ipify.org |
PUBLIC_IPV6_PROVIDERS |
core runtime | Yes | Comma-separated public IPv6 detection endpoints. Used only when ENABLE_IPV6=true. |
https://api64.ipify.org |
DRY_RUN |
optional runtime/debug | No | Validates config, may run read-only zone preflight, detects public IP, compares state, and logs what would happen without performing a live DNS update. | true |
FORCE_UPDATE_ON_NO_CHANGE |
optional runtime/debug | No | When false (default), skip unnecessary live update requests if the detected public IP is unchanged and all targets are already in sync. When true, still allow live update requests in that no-change case. |
false |
PUSHOVER_APP_KEY |
optional notifications | No | Pushover application API token. Required together with PUSHOVER_USER_KEY and PUSHOVER_LOCATION_PREFIX to enable notifications. |
abc123... |
PUSHOVER_USER_KEY |
optional notifications | No | Pushover user or group key. Required together with PUSHOVER_APP_KEY and PUSHOVER_LOCATION_PREFIX to enable notifications. |
uQiR... |
PUSHOVER_LOCATION_PREFIX |
optional notifications | No | Short prefix placed at the beginning of the notification message. Legacy PUSHOVER_LOCATION_NAME is accepted temporarily as a deprecated fallback. |
Home-Server |
RUN_ONCE |
optional runtime/debug | No | Executes one check cycle and exits instead of looping. | true |
DEBUG |
optional runtime/debug | No | Prints detailed diagnostics without exposing secrets, including sanitized XML request/response payloads for provider calls. | true |
TARGET_ZONE |
optional runtime/debug | No | Overrides zone inference from TARGET_HOST in single-target mode only. |
example.co.uk |
INTERNETX_SYSTEM_NS |
optional runtime/debug | No | Optional zone inquiry selector for the first system-managed nameserver. Usually unset. | ns1.routing.net |
STATE_DIR |
optional runtime/debug | No | Directory for persisted last_ipv4 and last_ipv6 values. |
/app/state |
CHECK_INTERVAL_SECONDS |
optional runtime/debug | No | Sleep interval for continuous container mode. Ignored when RUN_ONCE=true. |
300 |
HTTP_CONNECT_TIMEOUT |
optional runtime/debug | No | HTTP connect timeout for IP and XML requests. | 10 |
HTTP_REQUEST_TIMEOUT |
optional runtime/debug | No | Total HTTP request timeout for IP and XML requests. | 20 |
LOG_TARGET |
optional runtime/debug | No | PHP stream or file path for logs. | php://stdout |
INTERNETX_* settings are provider-specific for the current InterNetX XML interface. INTERNETX_SYSTEM_NS is not part of XML authentication and is not required for the normal update path. InterNetX documents system_ns on the Zone object as the first system-managed nameserver, and ZoneInfo examples may include it. If set, this worker sends it as <system_ns> in the read-only ZoneInfo request. Normal DynDNS users usually leave it unset.
There are no optional advanced authentication settings in the current worker. Credentials are used for AuthSessionCreate and AuthSessionDelete; zone inquiry and update requests use <auth_session><hash>...</hash></auth_session>.
See docs/providers/internetx-xml.md for the provider-specific InterNetX XML configuration notes.
Use dry-run mode before allowing live DNS updates:
cp .env.example .env
# edit .env and set INTERNETX_USER, INTERNETX_PASSWORD, and TARGET_HOST or TARGET_HOSTS
docker compose build
docker compose run --rm internetx-dyndnsUse docker compose run --rm internetx-dyndns when RUN_ONCE=true. Do not use docker compose up -d for one-shot validation with the default restart policy, because Docker will restart the cleanly exited container and it can look like a failure loop.
The recommended validation flags are:
DRY_RUN=true
RUN_ONCE=true
DEBUG=trueDRY_RUN=truevalidates config, detects the public IP, creates and closes an InterNetX AuthSession, may run read-only InterNetX zone validation, compares state, and logs what would happen without sending a live DNS update.FORCE_UPDATE_ON_NO_CHANGE=falseis the default and avoids unnecessary live update requests when the detected public IP is unchanged and every target already matches that IP.RUN_ONCE=trueexecutes a single check cycle and exits instead of looping continuously. It is meant fordocker compose run --rm internetx-dyndns.DEBUG=trueprints detailed diagnostic logging without exposing passwords, session hashes, or sensitive credentials. Provider request/response payloads are sanitized and labeled asauth_session_create,read_only_preflight,live_mutation, orsession_cleanup.- With
DEBUG=true, the worker logs each runtime stage as it moves through config validation, public IP detection, authentication/session preflight, target validation, update decision, live mutation, and session cleanup. - If
PUSHOVER_APP_KEY,PUSHOVER_USER_KEY, andPUSHOVER_LOCATION_PREFIXare all configured, the worker can send a Pushover notification when a real public IP change is detected.
Public IP detection runs before any InterNetX authentication request, so local network/IP behavior is visible even if API login fails later. IPv4 and IPv6 results are logged explicitly:
- If
ENABLE_IPV4=false, the worker logsIPv4 detection disabled by configuration. - If IPv4 is found, the worker logs
Detected IPv4 address: ...andSUCCESS Public IPv4 detection completed. - If IPv4 is missing but IPv6 is available, the worker logs
WARNING IPv4 detection failed but IPv6 is available. - If
ENABLE_IPV6=false, the worker logsIPv6 detection disabled by configuration. - If
ENABLE_IPV6=trueand an address is found, the worker logsDetected IPv6 address: ...andSUCCESS Public IPv6 detection completed. - If
ENABLE_IPV6=truebut no IPv6 address is detected, the worker logsWARNING IPv6 detection enabled but no IPv6 address foundand continues if IPv4 is available. - If neither IPv4 nor IPv6 is detected, the worker logs
ERROR No usable public IP address detected; aborting before authenticationand stops immediately.
Terminal severity markers are colorized when logging to stdout or stderr: debug is dim gray, info is blue, success is green, warning is yellow, and error is red. Yellow means a partial detection problem that can still continue; red means no usable public IP was found and execution stops.
In continuous mode, unchanged cycles are intentionally concise. If a detected address matches the stored state, the worker logs IPv4 unchanged or IPv6 unchanged without repeating the full address in normal output. New or changed addresses are printed in full. Debug mode still includes the detected values for deeper diagnostics.
Unchanged public IP does not automatically mean every target is already synchronized. The worker still validates the configured targets against current DNS data and only skips live mutation by default when every target is already in sync. Newly added or out-of-sync targets may still require update even if the detected public IP itself is unchanged.
If you set FORCE_UPDATE_ON_NO_CHANGE=true, the worker is allowed to continue with a live update request even when the detected public IP itself did not change and all targets are already in sync. Most users should leave this disabled to save unnecessary API requests.
Pushover support is optional. It is enabled only when all three variables are configured:
PUSHOVER_APP_KEY=your-pushover-app-token
PUSHOVER_USER_KEY=your-pushover-user-key
PUSHOVER_LOCATION_PREFIX=Home-ServerNotification behavior:
- notifications are sent only when the detected public IP actually changed
- no notification is sent on unchanged runs
- no notification is sent just because a target was added or was out of sync
DRY_RUN=truesuppresses Pushover delivery even when a real IP change was detected- notification failure does not stop the DNS update workflow
Example message format:
Zurich IPv4 Address: 109.40.176.130
Home-Server IPv4 Address: 1.2.3.4
Home-Server IPv6 Address: 2001:db8::1234
If only one family changed, only that line is sent. The configured location prefix always appears at the beginning of the message text. For example, PUSHOVER_LOCATION_PREFIX=Berlin produces a line starting with Berlin.
- Dry-run mode: validates configuration, detects public IPs, performs read-only provider checks, logs what would happen, and suppresses both live DNS mutation and Pushover delivery.
- Live mode: performs DNS updates when required and may send a Pushover notification when a real public IP change is detected.
Example dry-run startup:
[2026-04-23T08:00:00+00:00] DEBUG Runtime stage entered stage=startup/config validation dry_run=true mutation_allowed=false
[2026-04-23T08:00:00+00:00] INFO Execution cycle started dry_run=true run_once=true
[2026-04-23T08:00:00+00:00] INFO Startup validation env_file=present config_source=.env/environment dry_run=true debug=true run_once=true ipv4_enabled=true ipv6_enabled=false
[2026-04-23T08:00:00+00:00] INFO XML authentication configuration api_host=present auth_flow=auth_session session_create_uses_credentials=true follow_up_auth=auth_session username=present password=present context=present
[2026-04-23T08:00:00+00:00] INFO Project DNS target configuration target_host=present system_ns_optional=not_set
[2026-04-23T08:00:00+00:00] INFO Optional runtime settings configured=DRY_RUN,DEBUG,RUN_ONCE
[2026-04-23T08:00:00+00:00] INFO Configuration format validated dry_run_config_sufficient=true
[2026-04-23T08:00:00+00:00] INFO Target host mapping full_host=subleveldomain.domain.com domain=domain.com zone=domain.com subdomain=subleveldomain target_zone_source=inferred_last_two_labels
[2026-04-23T08:00:01+00:00] DEBUG Runtime stage entered stage=public IP detection dry_run=true mutation_allowed=false live_mutation_attempted=false
[2026-04-23T08:00:01+00:00] INFO Starting public IP detection ipv6_enabled=false
[2026-04-23T08:00:01+00:00] INFO Detected IPv4 address: 203.0.113.10 ipv4=203.0.113.10
[2026-04-23T08:00:01+00:00] SUCCESS Public IPv4 detection completed ipv4=203.0.113.10
[2026-04-23T08:00:02+00:00] INFO IPv6 detection disabled by configuration attempted=false
[2026-04-23T08:00:02+00:00] SUCCESS Public IP detection completed with at least one usable address ipv4_detected=true ipv6_detected=false
[2026-04-23T08:00:03+00:00] DEBUG Runtime stage entered stage=authentication/session preflight dry_run=true mutation_allowed=false live_mutation_attempted=false
[2026-04-23T08:00:03+00:00] INFO Starting DNS provider authentication/session preflight provider=InterNetX provider_interface=XML auth_flow=auth_session mutation_allowed=false
[2026-04-23T08:00:03+00:00] DEBUG InterNetX XML request prepared operation=AuthSessionCreate task_code=1321001 api_call_type=auth_session_create stage=authentication/session preflight mutation=false dry_run=true auth_mode=session_create session_established=false payload=<request>...</request>
[2026-04-23T08:00:03+00:00] DEBUG InterNetX XML response received operation=AuthSessionCreate task_code=1321001 api_call_type=auth_session_create stage=authentication/session preflight mutation=false dry_run=true auth_mode=session_create session_established=false http_status=200 transport_success=true response_result_status_code=S1321001 response_result_status_type=success stid=20260423-app1 api_business_success=true payload=<response>...</response>
[2026-04-23T08:00:03+00:00] SUCCESS InterNetX session created auth_mode=auth_session session_hash=9b4b...73dd session_persisted=false
[2026-04-23T08:00:04+00:00] DEBUG Runtime stage entered stage=target/zone validation dry_run=true mutation_allowed=false live_mutation_attempted=false
[2026-04-23T08:00:04+00:00] INFO Validating read-only InterNetX zone access zone=domain.com subdomains=subleveldomain require_a_record=true require_aaaa_record=false mutation=false
[2026-04-23T08:00:04+00:00] DEBUG InterNetX XML request prepared operation=ZoneInfo task_code=0205 api_call_type=read_only_preflight stage=target/zone validation mutation=false dry_run=true auth_mode=auth_session session_established=true payload=<request>...</request>
[2026-04-23T08:00:04+00:00] DEBUG InterNetX XML response received operation=ZoneInfo task_code=0205 api_call_type=read_only_preflight stage=target/zone validation mutation=false dry_run=true auth_mode=auth_session session_established=true http_status=200 transport_success=true response_result_status_code=S0205 response_result_status_type=success stid=20260423-app1 api_business_success=true payload=<response>...</response>
[2026-04-23T08:00:04+00:00] INFO Read-only InterNetX zone access validation successful target_host=subleveldomain.domain.com
[2026-04-23T08:00:05+00:00] DEBUG Runtime stage entered stage=update decision dry_run=true mutation_allowed=false live_mutation_attempted=false
[2026-04-23T08:00:05+00:00] DEBUG Loaded previous state last_ipv4=none last_ipv6=none
[2026-04-23T08:00:05+00:00] DEBUG Update decision update_required=true ipv4_changed=true ipv6_changed=false dry_run=true mutation_allowed=false
[2026-04-23T08:00:05+00:00] INFO Dry-run would require update, but mutation is disabled ipv4=203.0.113.10 ipv4_changed=true update_required=true target_host=subleveldomain.domain.com target_zone=domain.com target_subdomain=subleveldomain intended_action=would_update_dns_records dry_run=true mutation_allowed=false
[2026-04-23T08:00:05+00:00] INFO Dry-run completed; no live mutation sent reason=dry_run_enabled mutation_allowed=false live_mutation_attempted=false
[2026-04-23T08:00:05+00:00] DEBUG Runtime stage entered stage=session cleanup dry_run=true mutation_allowed=false live_mutation_attempted=false
[2026-04-23T08:00:05+00:00] DEBUG InterNetX XML request prepared operation=AuthSessionDelete task_code=1321003 api_call_type=session_cleanup stage=session cleanup mutation=false dry_run=true auth_mode=session_delete session_established=true payload=<request>...</request>
[2026-04-23T08:00:05+00:00] SUCCESS InterNetX session closed auth_mode=session_delete session_hash=9b4b...73dd
The read-only access validation uses InterNetX ZoneInfo task 0205. It can confirm that the credentials can read the configured zone and that the configured record exists. It cannot guarantee every future provider-side update policy will allow mutation, but it is the safest non-mutating preflight this XML workflow provides.
The worker uses:
request-get.xmlwith task code0205for zone inquiryrequest-put.xmlwith task code0202for zone updateAuthSessionCreatewith task code1321001to create a session fromuser,password, andcontext<auth_session><hash>...</hash></auth_session>for follow-up zone inquiry and update callsAuthSessionDeletewith task code1321003to close the session
The session hash is treated as a secret. It is kept only in memory, never persisted, and redacted in sanitized XML debug logs. Short masked display such as 9b4b...73dd may appear in operational logs.
The updater does not create missing DNS records. If IPv4 is enabled and detected, the target A record must already exist. If IPv6 is enabled and detected, the target AAAA record must already exist too.
The current primary check is provider-side ZoneInfo before update decisions. A future verification pass should stay staged and optional: provider check first, configurable public resolver checks second, and local resolver checks only as a convenience. See docs/design/verification.md.
Build and run continuously:
cp .env.example .env
# edit .env and set RUN_ONCE=false
docker compose build
docker compose up -d
docker compose logs -fUse docker compose up -d for continuous mode with RUN_ONCE=false. The compose file has restart: unless-stopped, so RUN_ONCE=true will exit successfully and then be started again by Docker.
Run one safe validation cycle:
cp .env.example .env
# edit .env and keep DRY_RUN=true, RUN_ONCE=true, DEBUG=true
docker compose build
docker compose run --rm internetx-dyndnsThe compose setup contains one service: internetx-dyndns.
The worker is outbound-only in normal operation. It does not run a web server, expose ports, or accept inbound connections. The container only needs outbound HTTPS access to the configured public IP detection providers and the InterNetX/AutoDNS XML API endpoint. The compose configuration intentionally has no ports: mapping, uses a read-only root filesystem, drops Linux capabilities, and keeps only ./state:/app/state writable for persisted IP state.
Container layout:
- base image:
php:8.3-cli-alpine3.22 - app image:
internetx-dyndns:local - release image:
worryboy/internetx-dyndns:0.5.0 - worker entry point:
docker/start.sh - CLI entry point:
bin/dyndns.php - persistent state mount:
./state:/app/state
Image hardening notes:
- the image keeps only runtime libraries needed for PHP
curlanddom - extension build dependencies are installed in a temporary
.phpize-depspackage group and removed after build - the final image does not keep the shell
curltool because runtime HTTP requests are performed through the PHPcurlextension - some scanner findings may still come from the upstream official
php:8.3-cli-alpine3.22base image and Alpine base packages such astar,libcurl, ornghttp2 - those inherited findings are reduced by rebuilding on newer upstream PHP/Alpine base releases when they become available
Docker Desktop is not required. Any Docker-compatible backend that supports the Docker CLI and Compose can run the worker.
Colima is one valid option on macOS:
brew install colima docker docker-compose
colima start
docker version
docker compose versionVERSIONCHANGELOG.mdREADME.Docker.mdsrc/Core/DynDnsService.phpsrc/Core/PublicIpResolver.phpsrc/Config/Config.phpsrc/Provider/DnsProvider.phpsrc/Provider/InterNetX/InterNetXXmlProvider.phpsrc/Provider/InterNetX/InterNetXXmlGatewayClient.phpdocs/providers/internetx-xml.mddocs/design/verification.md.env.exampledocker-compose.ymlDockerfile
This repository descends from martinlowinski/php-dyndns through the small correction fork AndLindemann/php-dyndns. The current repository has since been substantially extended and reworked into a container-oriented InterNetX XML DynDNS worker.
Modern additions in this repository include XML auth_session handling, dry-run validation, multi-target updates, no-change policy, Pushover notifications, provider/interface separation, Docker packaging, and Traefik/CrowdSec example documentation.
The original author has confirmed that MIT licensing is acceptable and has added MIT licensing to the original upstream. This repository includes an MIT LICENSE and a concise ATTRIBUTION.md.