Cliente Python para gestionar DNS en el panel XMS (Digital Value). CLI, biblioteca y servidor dyndns2/checkip compatible con ddclient. A, AAAA y TXT.
Project description
xms-dns
PyPI Python License: CC BY-NC 4.0
Cliente Python para gestionar DNS en el panel XMS (Digital Value). Incluye CLI (xms-dns), biblioteca y servidor dyndns2/checkip (xms-dns-server) compatible con ddclient. Soporta registros A, AAAA y TXT.
Setup
pip install xms-dns
For development:
git clone https://github.com/soukron/xms-dns.git
cd xms-dns
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Config file (optional, no secrets)
Create ~/.config/xms-dns/config with non-sensitive settings only:
mkdir -p ~/.config/xms-dns
cat > ~/.config/xms-dns/config <<'EOF'
XMS_BASE_URL=https://xms.digitalvalue.es
XMS_LOGIN=admin#example.test
EOF
chmod 600 ~/.config/xms-dns/config
Passwords are not read from this file. Use xms-dns login instead.
Environment variables override the config file. Override paths with XMS_CONFIG_DIR or XMS_CACHE_DIR.
Login
xms-dns login
Resolves settings in this order:
- Environment variables (
XMS_BASE_URL,XMS_LOGIN,XMS_PASSWORD) - Config file (
XMS_BASE_URL,XMS_LOGINonly) - Interactive prompts for anything still missing (password always prompted unless in env)
Connection type is always siempre (permanent session). On success, the session cookie is saved to ~/.cache/xms-dns/auth.cache.
HTTP calls
Login and operations are separate executions:
xms-dns login— authenticates against the panel and saves the session (its own HTTP calls; not counted against other commands).- Any other command — exactly one HTTP request to the panel, using the cached session. Credentials are never sent again.
Example: xms-dns domains is a single GET to Operacion=Dominios. xms-dns list is a single GET to load the zone.
The only exception is upsert when the value changes: XMS has no inline edit, so the client deletes and recreates the record (two or three requests).
CLI
Global flags:
-o/--output— output format:table(default),csv,json, oryaml-v/--verbose— log HTTP requests to stderr--debug— log requests and response bodies to stderr (includes verbose)
xms-dns login
xms-dns version
xms-dns whoami
xms-dns list gmbros.net
xms-dns -o csv list gmbros.net
xms-dns -o json domains
xms-dns -v list gmbros.net
xms-dns --debug upsert gmbros.net dyndns-test A 203.0.113.10
version and whoami
xms-dns version # versión instalada (texto plano)
xms-dns -o json version # {"version": "0.2.2"}
xms-dns whoami # config cargada + validez de la sesión cacheada
xms-dns -o json whoami
# {"xms_base_url": "...", "xms_login": "admin#example.test", "session_valid": "yes"}
whoami lee XMS_BASE_URL y XMS_LOGIN desde entorno o config (no pide contraseña) y comprueba la sesión cacheada con una petición al panel.
DNS commands use the cached session only. If not logged in or session expired:
error: Not logged in. Run: xms-dns login
Server (dyndns2 + checkip)
Start the dynamic DNS server:
xms-dns-server
Environment variables:
| Variable | Default | Purpose |
|---|---|---|
XMS_BASE_URL |
https://xms.digitalvalue.es |
XMS panel URL |
XMS_SERVER_HOST |
127.0.0.1 |
Bind address |
XMS_SERVER_PORT |
8080 |
Bind port |
XMS_TRUST_PROXY |
unset | Set to 1 to trust X-Forwarded-For / X-Real-Ip for client IP and access logs |
XMS_CACHE_DIR |
~/.cache/xms-dns |
Session cache directory |
Reverse proxy (production): if
xms-dns-serverruns behind nginx, Caddy, Traefik, etc., setXMS_TRUST_PROXY=1. Otherwise/checkipand updates withoutmyipsee the proxy IP (127.0.0.1) instead of the client. Only enable this when a trusted proxy overwritesX-Forwarded-For— do not expose the server directly to the internet with this flag on.
Authentication
The server does not use xms-dns login. Each dyndns2 client sends XMS credentials via HTTP Basic auth:
username→ XMS login (e.g.admin#example.test)password→ XMS password
The session cookie is cached per user in ~/.cache/xms-dns/server/{sha256}.cache (separate from the CLI auth.cache). If the cached session expires, the server re-authenticates with the credentials from the current request.
Endpoints
dyndns2
GET /nic/update?hostname=home.example.test&myip=...— actualización dyndns2. Respuesta en texto plano:good {value},nochg {value},badauth,nohost, etc. Siempre HTTP 200 (los clientes leen el cuerpo, no el código).- Tipos de registro: A (por defecto), AAAA, TXT.
- A / AAAA: dirección en
myip, o se usa la IP del cliente (X-Forwarded-ForconXMS_TRUST_PROXY=1). - AAAA:
type=AAAAo IPv6 enmyip(auto-detectado). - TXT:
type=TXTy valor entxt=(ovalue=).
- A / AAAA: dirección en
Credenciales XMS vía HTTP Basic auth (username = login XMS, p. ej. admin#dominio.test).
Ejemplo ddclient:
protocol=dyndns2
server=dyndns.example.test
login=admin#example.test
password=your-xms-password
home.example.test
Synology (proveedor DDNS personalizado):
https://__USERNAME__:__PASSWORD__@dyndns.example.test/nic/update?hostname=__HOSTNAME__&myip=__MYIP__
checkip (compatible con ifconfig.me)
| Ruta | Respuesta |
|---|---|
GET /checkip |
IPv4 del cliente (texto plano) |
GET /checkip?format=pfsense |
ip=… (legacy pfSense) |
GET /checkip/ip |
IPv4 |
GET /checkip/ua |
cabecera User-Agent |
GET /checkip/lang |
Accept-Language |
GET /checkip/encoding |
Accept-Encoding |
GET /checkip/mime |
Accept |
GET /checkip/charset |
Accept-Charset |
GET /checkip/forwarded |
X-Forwarded-For (raw) |
GET /checkip/all |
todos los campos, una línea key: value por campo |
GET /checkip/all.json |
mismo contenido en JSON |
curl https://dyndns.example.test/checkip
curl https://dyndns.example.test/checkip/ua
curl https://dyndns.example.test/checkip/all
curl https://dyndns.example.test/checkip/all.json
Campos en /all y /all.json: ip_addr, remote_host (siempre unavailable), user_agent, port, language, referer, connection, keep_alive, method, encoding, mime, charset, via, forwarded.
En un navegador (cabecera Accept: text/html), la raíz de checkip (/checkip o / vía host dedicado) muestra una página HTML con la tabla de conexión y ejemplos CLI. curl y clientes API siguen recibiendo texto plano.
Access log
El servidor escribe una línea de access log por petición (formato uvicorn), con la IP real del cliente cuando XMS_TRUST_PROXY=1 (lee X-Forwarded-For / X-Real-Ip).
Ejemplos:
INFO: 170.253.60.198:42188 - "GET /checkip (ip=170.253.60.198) HTTP/1.1" 200 OK
INFO: 170.253.60.198:42188 - "GET /nic/update?hostname=home.example.test&myip=… (good type=A value=170.253.60.198) HTTP/1.1" 200 OK
WARNING: 170.253.60.198:42188 - "GET /nic/update?hostname=… (badauth type=A reason=XMS authentication failed) HTTP/1.1" 200 OK
Los logs de dyndns2 no incluyen login ni otras credenciales.
Production deployment (Docker + Traefik)
Ejemplo de despliegue con Traefik: un contenedor xms-dns-server (PyPI o build local), TLS en el edge y rate limit por IP.
Variables típicas (config.env):
TRAEFIK_HOST=dyndns.example.test
TRAEFIK_CHECKIP_HOST=checkip.example.test # HTTPS (websecure) + HTTP (web, puerto 80)
TRAEFIK_ENTRYPOINT=websecure
TRAEFIK_ENTRYPOINT_HTTP=web
XMS_TRUST_PROXY=1
XMS_CACHE_DIR=/data/cache
checkip en HTTP y HTTPS: Traefik expone checkip.* en el puerto 443 (TLS) y también en el 80 (sin redirect). dyndns.* redirige HTTP→HTTPS por router. El edge Traefik no debe tener redirect global en el entrypoint web (solo redirects por servicio).
Host dedicado para checkip (opcional): Traefik puede reescribir rutas sin redirección HTTP, apuntando al mismo backend:
| Petición pública | Backend |
|---|---|
https://checkip.example.test/ |
/checkip |
https://checkip.example.test/ua |
/checkip/ua |
https://checkip.example.test/all.json |
/checkip/all.json |
Labels Traefik (extracto):
# Raíz → /checkip
traefik.http.middlewares.xms-checkip-root.replacepath.path: /checkip
traefik.http.routers.xms-checkip-root.rule: Host(`checkip.example.test`) && Path(`/`)
traefik.http.routers.xms-checkip-root.middlewares: xms-checkip-root@docker,…
# /ua, /all, … → /checkip/…
traefik.http.middlewares.xms-checkip-rewrite.replacepathregex.regex: ^/(.*)
traefik.http.middlewares.xms-checkip-rewrite.replacepathregex.replacement: /checkip/$1
traefik.http.routers.xms-checkip.rule: Host(`checkip.example.test`)
traefik.http.routers.xms-checkip.middlewares: xms-checkip-rewrite@docker,…
Requiere registro DNS para checkip.example.test (misma IP que el edge). Let's Encrypt vía Traefik.
If myip is omitted on dyndns2 updates, the client IP is used (see X-Forwarded-For when XMS_TRUST_PROXY=1).
Put TLS termination on a reverse proxy in production; bind locally by default. Remember XMS_TRUST_PROXY=1 on the server process.
Library
from xms_dns import XmsClient
# Login once (CLI does this via `xms-dns login`)
with XmsClient(base_url="...", login="...", password="...") as client:
client.authenticate()
# Later operations reuse auth.cache
with XmsClient.from_env() as client:
client.ensure_session()
client.upsert_record("gmbros.net", "home", "A", "203.0.113.10")
Tests
All default tests use mocked HTTP responses and do not contact the live XMS panel:
pytest
Releasing
CI runs on every push/PR to main (tests only). PyPI publish runs on tags, not on every push.
The package version comes from the git tag (hatch-vcs), not from pyproject.toml:
# 1. Update CHANGELOG.md
# 2. Commit and push
git commit -am "Release 0.1.2"
git push origin main
# 3. Tag and push — version 0.1.2 is taken from v0.1.2
git tag v0.1.2
git push origin v0.1.2
One-time PyPI setup (Trusted Publishing)
No API token in GitHub secrets. Configure once in PyPI:
- https://pypi.org/manage/project/xms-dns/settings/publishing/
- Add a new pending publisher → GitHub
- Owner:
soukron, repository:xms-dns, workflow:publish.yml, environment:pypi - In GitHub: repo Settings → Environments → New environment → name it
pypi(no secrets needed)
Live smoke test
xms-dns login # or set XMS_PASSWORD for one-shot login inside the script
XMS_LIVE=1 python scripts/smoke_test.py
Security
- Do not store passwords in the config file
- Keep
~/.cache/xms-dns/auth.cacheprivate (chmod 600) - Keep server session caches private too:
~/.cache/xms-dns/server/*.cache(chmod 600)
License
Copyright (c) 2026 Sergio Garcia.
Licensed under CC BY-NC 4.0: non-commercial use with attribution required.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xms_dns-0.2.3.tar.gz.
File metadata
- Download URL: xms_dns-0.2.3.tar.gz
- Upload date:
- Size: 41.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e1e64c3917c03fb37395c0150723a0b01898079e470173e5a7f2fb23f234602
|
|
| MD5 |
97cbb72a09974e57fafe00cfc2e0da73
|
|
| BLAKE2b-256 |
d017dc1af4fb6b3a5bbed16de98bc9522ed48cb89b16a0bb622dd3f2689d955a
|
Provenance
The following attestation bundles were made for xms_dns-0.2.3.tar.gz:
Publisher:
publish.yml on soukron/xms-dns
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xms_dns-0.2.3.tar.gz -
Subject digest:
8e1e64c3917c03fb37395c0150723a0b01898079e470173e5a7f2fb23f234602 - Sigstore transparency entry: 2143188826
- Sigstore integration time:
-
Permalink:
soukron/xms-dns@d0af7cff7119f9b076ab035acb3b2e755d9ba92b -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/soukron
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d0af7cff7119f9b076ab035acb3b2e755d9ba92b -
Trigger Event:
push
-
Statement type:
File details
Details for the file xms_dns-0.2.3-py3-none-any.whl.
File metadata
- Download URL: xms_dns-0.2.3-py3-none-any.whl
- Upload date:
- Size: 52.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9ca77ba202746724993e9d0c2ab2748acecffad0387ce4bc6d71482750621dfd
|
|
| MD5 |
a4869ca417ac01f1cb8f4a5beff278dd
|
|
| BLAKE2b-256 |
59db1e213ced544a7eda949874e63868a96c7c09d38cf6dd586e5482be4f1a0e
|
Provenance
The following attestation bundles were made for xms_dns-0.2.3-py3-none-any.whl:
Publisher:
publish.yml on soukron/xms-dns
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xms_dns-0.2.3-py3-none-any.whl -
Subject digest:
9ca77ba202746724993e9d0c2ab2748acecffad0387ce4bc6d71482750621dfd - Sigstore transparency entry: 2143189025
- Sigstore integration time:
-
Permalink:
soukron/xms-dns@d0af7cff7119f9b076ab035acb3b2e755d9ba92b -
Branch / Tag:
refs/tags/v0.2.3 - Owner: https://github.com/soukron
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d0af7cff7119f9b076ab035acb3b2e755d9ba92b -
Trigger Event:
push
-
Statement type: