Metadata-Version: 2.4
Name: netcup-dyndns-and-trusted-proxies-updater
Version: 1.4.0
Summary: Netcup DynDns and Nexcloud Trusted Proxies Updater
Author: Massoud Ahmed
Project-URL: Repository, https://github.com/sowoi/netcup-dyndns-and-trusted-proxies-updater
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.34.2
Requires-Dist: tqdm>=4.70.0
Dynamic: license-file

# netcup-dyndns-and-trusted-proxies-updater
<!-- TOC -->
* [netcup-dyndns-and-trusted-proxies-updater](#netcup-dyndns-and-trusted-proxies-updater)
  * [Prerequisites](#prerequisites)
  * [Installation](#installation)
  * [Updating](#updating)
  * [Command-Line Arguments](#command-line-arguments)
  * [Docker Installation (alternative)](#docker-installation-alternative)
  * [Providing Secrets at Runtime](#providing-secrets-at-runtime)
  * [Login Limits and ntfy Notifications](#login-limits-and-ntfy-notifications)
  * [Usage](#usage)
  * [Configuration](#configuration)
  * [Contributing](#contributing)
  * [License](#license)
<!-- TOC -->

This script is designed for users with a dynamic dual-stack address, using Netcup as their provider. 
It automatically updates your IPv6 address in the Trusted Proxy configuration for Nextcloud.

The script checks the current IPv4/IPv6 address of the host and updates the corresponding values at Netcup if necessary.
Additionally, it updates the Trusted Proxies configuration in Nextcloud via the OCC CLI.

## Prerequisites
This script needs Python 3.13 or newer.

## Installation

Install the PyPI project with your preferred tool:
```
pipx install netcup-dyndns-and-trusted-proxies-updater
uv tool install netcup-dyndns-and-trusted-proxies-updater
pip install netcup-dyndns-and-trusted-proxies-updater
```

## Updating

Update to the latest release with the same tool you used for the installation:
```
pipx upgrade netcup-dyndns-and-trusted-proxies-updater
uv tool upgrade netcup-dyndns-and-trusted-proxies-updater
pip install --upgrade netcup-dyndns-and-trusted-proxies-updater
```

To install a specific version instead, for example to roll back:
```
pipx install --force netcup-dyndns-and-trusted-proxies-updater==1.4.0
uv tool install --force netcup-dyndns-and-trusted-proxies-updater==1.4.0
pip install netcup-dyndns-and-trusted-proxies-updater==1.4.0
```

Confirm the installed version afterwards with `netcup-dyndns --version`. Updating
does not touch your settings file or cache directory. See [CHANGELOG.md](CHANGELOG.md)
for the changes in each release.

If you run the script from a cloned repository, update it with:
```
git pull
uv sync
```

## Using an Installed Tool

All three installation methods provide the `netcup-dyndns` command. Confirm the
installed version and repository URL with:
```
netcup-dyndns --version
```

By default, the command creates `.settings.json` and `.temp` in the current
working directory. First inspect the paths it will use:
```
netcup-dyndns --show-paths
```

Run it once to create the settings file, edit the generated file with your
credentials and domains, then run the updater again:
```
netcup-dyndns
```

For a service, cron job, or any other setup where the current directory is not
stable, specify absolute paths explicitly:
```
netcup-dyndns \
  --settings-file /etc/netcup-dyndns/settings.json \
  --cache-dir /var/lib/netcup-dyndns
```
Use the same `--settings-file` and `--cache-dir` values for every run. The
cache directory stores the last observed IP addresses, retry state and login backoff.

You can also clone this repository to run it from source:
```
git clone <repository-url>
cd netcup-dyndns-and-trusted-proxies-updater
uv run src/netcup-dyndns.py
```
The first run will create a settings.json file and a temp folder in your project directory.
3. Configure the settings.json file with the following parameters:

```Json
{
    "API_PASSWORD": "",
    "API_KEY": "",
    "CUSTOMER_ID": "",
    "NETCUP_DOMAIN": "",
    "NEXTCLOUD_PATH": "",
    "TRUSTED_PROXIES_POS": "",
    "PARALLEL_PROCESSES": 1,
    "IP_MODE": "both",
    "DISABLE_NEXTCLOUD_NGINX": false,
    "RATE_LIMIT_BACKOFF_MINUTES": [10, 30, 60],
    "NTFY_SERVER": "https://ntfy.sh",
    "NTFY_TOPIC": "",
    "NTFY_TOKEN": ""
}
```

API_PASSWORD: Your Netcup API password.  
API_KEY: Your Netcup API key.  
CUSTOMER_ID: Your Netcup customer ID.  
NETCUP_DOMAIN: The domain name(s) you want to update, separated by commas (e.g., example.com, example.net).  
NEXTCLOUD_PATH: The file path to your Nextcloud instance (required unless DISABLE_NEXTCLOUD_NGINX is true).
TRUSTED_PROXIES_POS: The position in the TrustedProxies configuration where the new IP address should be added (e.g., the first, second, etc.; required unless DISABLE_NEXTCLOUD_NGINX is true).
PARALLEL_PROCESSES: Number of parallel threads for DNS updates (Default: 1 for sequential execution. Values > 1 enable the ThreadPoolExecutor).
IP_MODE: Determines which IP types to update. Options are "both" (default), "ipv4", or "ipv6".
DISABLE_NEXTCLOUD_NGINX: Set to true to disable all Nextcloud OCC and Nginx reload tasks. Useful if you only want to use the script as a pure DynDNS client (Default: false).
RATE_LIMIT_BACKOFF_MINUTES: Minimum waiting times in minutes between login attempts after netcup refused a login because of too many logins (Default: [10, 30, 60]). See [Login Limits and ntfy Notifications](#login-limits-and-ntfy-notifications).
NTFY_SERVER: The ntfy server used for notifications (Default: https://ntfy.sh).
NTFY_TOPIC: The ntfy topic that receives a notification when the login is still refused after the last backoff step. Leave empty to disable notifications (Default: empty).
NTFY_TOKEN: Optional ntfy access token for protected topics, sent as `Authorization: Bearer <token>` (Default: empty).

Settings files created by an earlier version do not contain the `RATE_LIMIT_BACKOFF_MINUTES`
and `NTFY_*` keys. The defaults apply until you add them.

## Command-Line Arguments

Every setting can also be provided as a command-line argument. Command-line arguments
always take precedence over `.settings.json` (and any secret provider overrides, see
[Providing Secrets at Runtime](#providing-secrets-at-runtime)), which is useful for
one-off overrides or wiring the script into other tooling without editing the settings
file. Run with `--help` to see all available options:

```
netcup-dyndns --help
```

```
options:
  -h, --help            show this help message and exit
  --version             show the version and repository URL, then exit
  --settings-file PATH  path to the settings JSON file
  --cache-dir PATH      directory for temporary IP and retry cache files
  --show-paths          show the resolved settings-file and cache-directory paths, then exit
  --force               Force a DNS update of all domains, ignoring the cached
                        IPv4/IPv6 addresses.
  --api-password API_PASSWORD
                        Netcup API password. Overrides API_PASSWORD.
  --api-key API_KEY     Netcup API key. Overrides API_KEY.
  --customer-id CUSTOMER_ID
                        Netcup customer ID. Overrides CUSTOMER_ID.
  --netcup-domain NETCUP_DOMAIN
                        Comma-separated domain(s) to update, e.g.
                        'example.com,example.net'. Overrides NETCUP_DOMAIN.
  --nextcloud-path NEXTCLOUD_PATH
                        Path to the Nextcloud installation. Overrides
                        NEXTCLOUD_PATH.
  --trusted-proxies-pos TRUSTED_PROXIES_POS
                        Position in the trusted_proxies configuration to
                        update. Overrides TRUSTED_PROXIES_POS.
  --parallel-processes PARALLEL_PROCESSES
                        Number of parallel DNS-update workers. Overrides
                        PARALLEL_PROCESSES.
  --ip-mode {both,ipv4,ipv6}
                        Which IP types to update: 'both' (default), 'ipv4', or
                        'ipv6'. Overrides IP_MODE.
  --disable-nextcloud-nginx, --no-disable-nextcloud-nginx
                        Disable the Nextcloud OCC / Nginx reload tasks (use
                        --no-disable-nextcloud-nginx to force-enable them).
                        Overrides DISABLE_NEXTCLOUD_NGINX.
  --rate-limit-backoff-minutes MINUTES
                        Comma-separated minimum waiting times in minutes
                        between login attempts after netcup refused a login
                        because of too many logins, e.g. '10,30,60'.
                        Overrides RATE_LIMIT_BACKOFF_MINUTES.
  --ntfy-server NTFY_SERVER
                        ntfy server URL for notifications. Overrides
                        NTFY_SERVER.
  --ntfy-topic NTFY_TOPIC
                        ntfy topic to notify; empty disables notifications.
                        Overrides NTFY_TOPIC.
  --ntfy-token NTFY_TOKEN
                        ntfy access token for protected topics. Overrides
                        NTFY_TOKEN.
```

Example, overriding just the domain for a single run without touching `.settings.json`:
```
netcup-dyndns --netcup-domain example.com,example.net
```

## Docker Installation (alternative)

Instead of installing `uv` and Python locally, you can run this project as a lightweight
Docker container. The provided `Docker/Dockerfile` uses a multi-stage build: `uv` and dependency
resolution happen only in a throw-away build stage, and the final runtime image is based
on `python:3.13-slim` with just the resolved virtual environment and application code —
no `uv`, compilers, or other build tools are present in the image you actually run.

1. Create and fill in your `.settings.json` file (see [Configuration](#configuration) for
   the available keys), then make sure it is readable by the container:
   ```
   chmod 644 .settings.json
   ```

2. Build and start the container with Docker Compose:
   ```
   docker compose -f Docker/docker-compose.yml up -d --build
   ```
   This mounts `.settings.json` read-write into the container and persists the cached IP
   addresses in a named volume (`dyndns-cache`) across restarts. By default, the container
   checks for IP changes every `UPDATE_INTERVAL_SECONDS` (300s/5 minutes); adjust this in
   `Docker/docker-compose.yml` as needed.

   Alternatively, to trigger a single run (e.g. from a host cron job or systemd timer)
   instead of running continuously:
   ```
   docker compose -f Docker/docker-compose.yml run --rm -e RUN_ONCE=true netcup-dyndns-updater
   ```

3. **Nextcloud/Nginx integration caveat:** `nginx_trusted_proxies_configuration` shells out
   to `sudo`, `php occ`, and `systemctl restart nginx` on the host. These are not available
   inside the minimal container, so set `DISABLE_NEXTCLOUD_NGINX: true` in `.settings.json`
   when running via Docker unless you specifically bind-mount those host binaries into a
   privileged container (not recommended). Running purely as a DynDNS client works out of
   the box.

## Providing Secrets at Runtime

As an alternative to storing credentials in plain text in `.settings.json`, the script
supports two optional, opt-in mechanisms to override settings at runtime. Both are
disabled unless explicitly configured via environment variables, and neither requires any
additional dependency.

**1. Secret files (Docker/Kubernetes secrets convention)**

Set an environment variable named `<KEY>_FILE` pointing at a file whose contents will be
read (and trimmed) to override the corresponding setting, e.g.:
```
API_PASSWORD_FILE=/run/secrets/api_password
API_KEY_FILE=/run/secrets/api_key
CUSTOMER_ID_FILE=/run/secrets/customer_id
```
This works with Docker Compose `secrets:`, Kubernetes `Secret` volumes, or any tool that
projects a secret's value into a file (including an OpenBAO Agent injector sidecar).

**2. Direct retrieval from an OpenBAO (or Vault-compatible) server**

Configure the following environment variables to have the script fetch a KV v2 secret at
startup and use its values to override matching settings keys (`API_PASSWORD`, `API_KEY`,
`CUSTOMER_ID`, etc.):
```
OPENBAO_ADDR=https://openbao.example.internal:8200
OPENBAO_TOKEN=s.xxxxxxxxxxxxxxxxxxxx      # or OPENBAO_TOKEN_FILE=/run/secrets/openbao_token
OPENBAO_SECRET_PATH=secret/data/netcup-dyndns   # optional, this is the default
```
The secret is expected at `<OPENBAO_ADDR>/v1/<OPENBAO_SECRET_PATH>` in the standard KV v2
response shape (`{"data": {"data": {...}}}`). Only keys already recognized by
`.settings.json` are applied; unknown keys in the secret are ignored.

If both mechanisms are configured, secret-file overrides take precedence over OpenBAO
values, since they are applied last. Neither mechanism is required — you can continue to
configure everything directly in `.settings.json`. Command-line arguments (see
[Command-Line Arguments](#command-line-arguments)) are applied after both, and therefore
always take precedence over secret provider values as well.

## Login Limits and ntfy Notifications

The netcup API blocks logins for a while when there have been too many of them. The
script recognizes such a block when the login

* fails with a refused connection (`Connection refused`),
* is answered with HTTP status `429 Too Many Requests`, or
* is rejected with a netcup error message about too many logins or a reached limit.

In that case, the script logs a `Connection Refused` error, stops logging in for the
remaining domains of the run, and waits before the next attempt. With the default
`RATE_LIMIT_BACKOFF_MINUTES` of `[10, 30, 60]`:

| Refused login | Next attempt no sooner than |
|---------------|-----------------------------|
| 1st           | 10 minutes later            |
| 2nd           | 30 minutes later            |
| 3rd           | 60 minutes later            |
| 4th and later | 60 minutes later, and a ntfy notification is sent after the 4th |

Runs that start before the next attempt is due (for example from a cron job every five
minutes or the Docker loop) exit immediately without contacting any API. The waiting
times are minimums: the attempt happens on the first run after the waiting time has
passed. Once a login succeeds again, every configured domain is updated, the backoff is
reset, and, if a notification was sent, a second notification reports that logins work
again. Refused logins do not count against the per-domain retry budget.

Notifications are sent only if `NTFY_TOPIC` is set. To use them:

1. Pick a hard-to-guess topic name on [ntfy.sh](https://ntfy.sh) (or on your own ntfy
   server) and subscribe to it in the ntfy app.
2. Set `NTFY_TOPIC` in `.settings.json`. Set `NTFY_SERVER` for a self-hosted server and
   `NTFY_TOKEN` if the topic requires authentication.

`NTFY_TOKEN` can also be provided at runtime as described in
[Providing Secrets at Runtime](#providing-secrets-at-runtime), e.g. via `NTFY_TOKEN_FILE`.

The backoff state is stored in `rate_limit.json` in the cache directory. Running with
`--force` attempts a login even while the backoff is active; deleting `rate_limit.json`
resets the backoff completely.

## Usage

To run the script periodically, set up a cron job or a systemd timer. 
This will ensure your IP address is regularly checked and updated.

While updating DNS records, a progress bar is shown. Its total is derived from the
number of subdomains listed in NETCUP_DOMAIN (each subdomain accounts for one A and
one AAAA record), and it advances by one step for every record that is successfully
updated, reaching 100% once all A and AAAA records for every configured subdomain
have been updated:

```
Updating DNS records: 100%|██████████| 4/4 records
```

Regular progress is logged to stdout with timestamps and log levels (INFO/WARNING).
Errors are logged in red (ERROR) so failures stand out immediately.

When multiple domains or subdomains are configured, the script prints a summary at
the end grouped by domain instead of a flat sequential list, e.g.:

```
example.com
  - sub          A     -> 1.2.3.4
  - sub          AAAA  -> ::1
  - www          A     -> 1.2.3.4
example.net
  - app          A     -> 1.2.3.4
```

## Configuration

Upon the first execution, the script creates a settings.json file and a temp folder.

The API identifiers for Netcup (API key, password, and customer ID) must be configured in the settings.json file.
In the NETCUP_DOMAIN field, list all desired domain names separated by commas (e.g., example.com, example.net).
NEXTCLOUD_PATH should point to the directory where your Nextcloud instance is located.
TRUSTED_PROXIES_POS specifies the position in the TrustedProxies configuration where the new IP address should be inserted.

## Contributing

When bumping the version in `pyproject.toml`, add a matching `## [x.y.z] - YYYY-MM-DD`
section to [CHANGELOG.md](CHANGELOG.md). After the pull request is merged into `main`,
that section is published as the notes of the GitHub release.

To ensure proper code formatting, run the following command:
```
uv run --dev ruff check
```

To run tests:
```
uv run --dev pytest
```

To run tests with a coverage report:
```
uv run --dev pytest --cov=src --cov-report=term-missing
```

## License
Licensed under the terms of GNU General Public License v3.0. See LICENSE file.
