Skip to content

Repository files navigation

Disclaimer

This package is not related to or developed by Porkbun. No relationship between the developer of this package and Porkbun exists.

All trademarks, logos and brand names are the property of their respective owners. All company, product and service names used in this package are for identification purposes only. Use of these names,trademarks and brands does not imply endorsement.

Porkbun DDNS

porkbun-ddns is a unofficial DDNS-Client for Porkbun Domains. This library will only update the records if the IP(s) have changed or the dns entry didn't exist before, it will also set/update A (IPv4) and AAAA (IPv6) records.

⚠️ v2.0.0 breaking changes

Version 2.0.0 introduces breaking changes to the public PorkbunDDNS API and the CLI:

  • Constructor: PorkbunDDNS(config, domain, ...) -> PorkbunDDNS(credentials, retry, domain, ...). Config is now AppConfig(credentials, retry, webhook) — build it with Credentials, RetryPolicy and WebhookConfig, or load it via extract_config.
  • CLI: the --fritzbox / -f flag is removed. Pipe fritzbox-ips into --public-ips instead: porkbun-ddns domain.com --public-ips "$(fritzbox-ips <fritzbox-ip>)".
  • Endpoint: Credentials.endpoint now defaults to the real Porkbun endpoint (previously it raised when unset).

See the full breakdown in PR #163.

Since porkbun-dynamic-dns-python is deprecated I took it into my own hands to code a decent DDNS Client for Porkbun. Inspired by con-f-use pull request, I built a pip Package and a docker container.

As alternative to cert-bun use my lego-certbot image.

Setup on Porkbun

Make sure that any domain you use this client with has API access enabled. See the below picture for reference.

API Access Enabled

If this is not enabled, you'll see an error about your API keys being invalid, despite them being correct.

CLI

Minimum required python version: 3.10

Install via pip

pip install porkbun-ddns

Usage

usage: porkbun-ddns [-h] [-c CONFIG] [-e ENDPOINT] [-pk APIKEY] [-sk SECRETAPIKEY]
              [--retry-count RETRY_COUNT] [--retry-delay RETRY_DELAY]
              [--webhook-url WEBHOOK_URL]
              [--webhook-template WEBHOOK_TEMPLATE]
              [--webhook-template-file WEBHOOK_TEMPLATE_FILE]
              [--log-level LOG_LEVEL] [-i [PUBLIC_IPS ...]] [-4 | -6] [-v]
              [--env_only]
              domain [subdomains ...]

positional arguments:
  domain                Domain to be updated
  subdomains            Subdomain(s)

options:
  -h, --help            show this help message and exit
  -c CONFIG, --config CONFIG
                        Path to config file (default:
                        ~/.config/porkbun-ddns-config.json)
  -e ENDPOINT, --endpoint ENDPOINT
                        The endpoint
  -pk APIKEY, --apikey APIKEY
                        The Porkbun-API-key
  -sk SECRETAPIKEY, --secretapikey SECRETAPIKEY
                        The secret API-key
  --retry-count RETRY_COUNT
                        Number of attempts for transient API failures
  --retry-delay RETRY_DELAY
                        Seconds to wait between retry attempts
  --webhook-url WEBHOOK_URL
                        Webhook URL to notify when IPs change
  --webhook-template WEBHOOK_TEMPLATE
                        Jinja2 template for the webhook payload
  --webhook-template-file WEBHOOK_TEMPLATE_FILE
                        Path to a file containing the Jinja2 webhook template
                        (takes precedence over --webhook-template)
  --log-level LOG_LEVEL
                        Set log verbosity (DEBUG, INFO, WARNING, ERROR,
                        CRITICAL)
  -i [PUBLIC_IPS ...], --public-ips [PUBLIC_IPS ...]
                        Public IPs (v4 and or v6)
  -4, --ipv4-only       Only set/update IPv4 A Records
  -6, --ipv6-only       Only set/update IPv6 AAAA Records
  -v, --verbose         Show Debug Output
  --env_only            Don't use any config, get all variables from the
                        environment

The parameter endpoint, apikey, secretapikey

These parameter are required for each run of the program. The program will take the values for these (in this order) from:

  1. The command-line-arguments (-pk pk1_xxx)
  2. The environment-variables (export PORKBUN_APIKEY='pk1_xxx')
  3. The config-file (apikey="pk_xxx")

In Docker use the API_ENDPOINT environment-variable instead.

So if a value is set through the CLI and in the file, the CLI-value will be used. This allows for a default-configuration in the config-file, whose settings can be selectively overridden through enviromnment-variables or CLI-arguments.

The parameter retry_count, retry_delay

Transient API failures (unreachable endpoint, timeouts, HTTP 5xx) are retried automatically, HTTP 4xx errors (e.g. invalid API keys) fail immediately. Default is 3 attempts with a 5 seconds delay between them.

The program will take the values for these (in this order) from:

  1. The command-line-arguments (--retry-count 3)
  2. The environment-variables (export PORKBUN_RETRY_COUNT='3')
  3. The config-file (retry_count="3")

The parameter webhook_url, webhook_template, webhook_template_file

When the IP(s) of your records change, an aggregated webhook-notification can be POSTed to a URL of your choice. This works out of the box with Slack, MS Teams, Mattermost and Google Chat.

The program will take the values for these (in this order) from:

  1. The command-line-arguments (--webhook-url 'https://...')
  2. The environment-variables (export PORKBUN_WEBHOOK_URL='https://...')
  3. The config-file (webhook_url="https://...")

In Docker use the WEBHOOK_URL, WEBHOOK_TEMPLATE and WEBHOOK_TEMPLATE_FILE environment-variables instead.

The payload can be customized with an inline Jinja2-template (--webhook-template) or a template-file (--webhook-template-file), where the file takes precedence over the inline one. If neither is set, the following Slack-compatible default is used:

{"text": "IP changed: {{ old_ips | join(', ') }} -> {{ new_ips | join(', ') }} ({{ domain }})"}

The following context-variables are available in templates: changes (list of changes, each {record_type, fqdn, old_ip|None, new_ip}), old_ips (previous IPs), new_ips (new IPs), domain (the updated domain) and timestamp (ISO-8601 UTC timestamp of the notification).

One notification is sent per run, after all records have been updated. Notifications are fire-and-forget: a failure to deliver never crashes the update-loop.

The parameter log_level

Controls the verbosity of the logs. Accepts standard logging level names, case-insensitively: DEBUG, INFO, WARNING, ERROR, CRITICAL (default INFO). Set it via --log-level WARNING on the CLI or LOG_LEVEL=WARNING in Docker.

When both the legacy --verbose/DEBUG and LOG_LEVEL are set, LOG_LEVEL wins. An invalid value logs a warning and falls back to INFO, it never crashes.

Examples

# using the default config-file in ~/.config/porkbun-ddns-config.json:
$ porkbun-ddns domain.com my_subdomain

# Using only environment variables:
# PORKBUN_APIKEY
# PORKBUN_SECRETAPIKEY
# PORKBUN_ENDPOINT (Optional)
$ porkbun-ddns domain.com my_subdomain --env_only

# Specific config-file:
$ porkbun-ddns domain.com my_subdomain -c "./config.json"

# Multiple subdomains:
$ porkbun-ddns domain.com my_subdomain_1 my_subdomain_2 my_subdomain_3

# Set root and subdomains:
$ porkbun-ddns domain.com @ my_subdomain_1 my_subdomain_2 my_subdomain_3

# Set wildcard domain:
$ porkbun-ddns domain.com '*'

# Set IP's explicit
$ porkbun-ddns domain.com my_subdomain -i '1.2.3.4' '1234:abcd:0:4567::8900'

# Use Fritz!Box to obtain IP's (via fritzbox-ips sidecar) and set IPv4 A Record only
$ porkbun-ddns domain.com my_subdomain --public-ips "$(fritzbox-ips fritz.box)" -4

You can set up a cron job get the full path to porkbun-ddns with which porkbun-ddns, then execute crontab -e and add the following line:

*/30 * * * * <PORKBUN-DDNS-PATH>/porkbun-ddns "<YOUR-PATH>/config.json" domain.com my.subdomain >/dev/null 2>&1

config.json example:

{
  "endpoint":"https://api.porkbun.com/api/json/v3",
  "apikey": "pk1_xxx",
  "secretapikey": "sk1_xxx",
  "retry_count": "3",
  "retry_delay": "5",
  "webhook_url": "https://hooks.slack.com/services/..."
}

Docker compose

services:
  porkbun-ddns:
    image: "mietzen/porkbun-ddns:latest"
    container_name: porkbun-ddns
    environment:
      DOMAIN: "domain.com" # Your Porkbun domain
      SUBDOMAINS: "my_subdomain,my_other_subdomain,my_subsubdomain.my_subdomain" # Subdomains comma spreaded
      SECRETAPIKEY: "<YOUR-SECRETAPIKEY>" # Your Porkbun Secret-API-Key
      APIKEY: "<YOUR-APIKEY>" # Your Porkbun API-Key
      # API_ENDPOINT: "https://api.porkbun.com/api/json/v3" # Override the Porkbun API endpoint (e.g. a mirror/proxy)
      # PUBLIC_IPS: "1.2.3.4,2001:043e::1" # Set if you got static IP's (wins over FRITZBOX)
      # FRITZBOX: "192.168.178.1" # Use Fritz!BOX to obtain Public IP's (queried directly by the entrypoint)
      # SLEEP: "300" # Seconds to sleep between DynDNS runs
      # IPV4: "TRUE" # Set IPv4 address
      # IPV6: "TRUE" # Set IPv6 address
      # DEBUG: "FALSE" # DEBUG LOGGING
      # LOG_LEVEL: "WARNING" # Set log verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
      # RETRY_COUNT: "3" # Number of attempts for transient API failures
      # RETRY_DELAY: "5" # Seconds to wait between retry attempts
      # WEBHOOK_URL: "https://hooks.slack.com/services/..." # POST an IP-change notification to this URL (Slack, MS Teams, Mattermost, Google Chat compatible by default)
      # WEBHOOK_TEMPLATE: '{"text": "IP changed: {{ old_ips | join(", ") }} -> {{ new_ips | join(", ") }} ({{ domain }})"}' # Optional custom Jinja2 template
      # WEBHOOK_TEMPLATE_FILE: "/path/to/template.j2" # Optional Jinja2 template file (takes precedence over WEBHOOK_TEMPLATE)
    restart: unless-stopped

# # Uncomment below to let it detect ipv6 address:
#     networks:
#       - ipv6_enabled

# networks:
#   ipv6_enabled:
#     enable_ipv6: true

Docker run

docker run -d \
  -e DOMAIN="domain.com" \
  -e SUBDOMAINS="my_subdomain,my_other_subdomain,my_subsubdomain.my_subdomain" \
  -e SECRETAPIKEY="<YOUR-SECRETAPIKEY>" \
  -e APIKEY="<YOUR-APIKEY>" \
  -e LOG_LEVEL="WARNING" \
  -e WEBHOOK_URL="https://hooks.slack.com/services/..." \
  --name porkbun-ddns \
  --restart unless-stopped \
  mietzen/porkbun-ddns:latest

Python

Minimum required python version: 3.10

from pathlib import Path
from porkbun_ddns import PorkbunDDNS
from porkbun_ddns.config import AppConfig, Credentials, RetryPolicy, WebhookConfig, DEFAULT_ENDPOINT, extract_config


# Build config directly:
app = AppConfig(
    credentials=Credentials(apikey="YOUR-APIKEY", secretapikey="YOUR-SECRETAPIKEY", endpoint=DEFAULT_ENDPOINT),
    retry=RetryPolicy(),  # defaults: retry_count=3, retry_delay=5
    webhook=WebhookConfig(),
)
porkbun_ddns = PorkbunDDNS(app.credentials, app.retry, 'domain.com')

# Or load from config file:
# app = extract_config(Path("./config.json"))
# porkbun_ddns = PorkbunDDNS(app.credentials, app.retry, 'domain.com')

# With static IPs:
# porkbun_ddns = PorkbunDDNS(app.credentials, app.retry, 'domain.com', public_ips=['1.2.3.4', '1234:abcd:0:4567::8900'])

# With Fritz!Box (via fritzbox-ips sidecar CLI):
# porkbun-ddns domain.com --public-ips "$(fritzbox-ips fritz.box)"

porkbun_ddns.set_subdomain('my_subdomain')
porkbun_ddns.update_records()

About

porkbun-ddns is an unoffical DDNS-Client for Porkbun Domains.

Topics

Resources

Stars

249 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages