First Commit
This commit is contained in:
parent
51ca26f5ce
commit
7d1ba02c42
8 changed files with 1106 additions and 61 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Ignore environment files containing secrets
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
211
README.md
211
README.md
|
|
@ -41,9 +41,8 @@ SECRET_KEY=sk1_your_secret_key_here
|
||||||
# Required: The domain to update
|
# Required: The domain to update
|
||||||
DOMAIN=example.com
|
DOMAIN=example.com
|
||||||
|
|
||||||
# Required for multi-machine setup: Unique identifier for this machine
|
# Required: Name of the machine for tracking
|
||||||
# Each machine should have a different MACHINE_ID
|
MACHINE_ID="exmample_machine"
|
||||||
MACHINE_ID=server1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting Porkbun API Credentials
|
### Getting Porkbun API Credentials
|
||||||
|
|
@ -149,24 +148,6 @@ This means:
|
||||||
- If you ever want to manually take control of a record that was created by this script,
|
- If you ever want to manually take control of a record that was created by this script,
|
||||||
you can remove or change the `ddns:<MACHINE_ID>` note and the script will stop managing it.
|
you can remove or change the `ddns:<MACHINE_ID>` note and the script will stop managing it.
|
||||||
|
|
||||||
### Automated Updates with Cron
|
|
||||||
|
|
||||||
To automatically update DNS every 5 minutes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
crontab -e
|
|
||||||
```
|
|
||||||
|
|
||||||
Add this line (adjust path as needed):
|
|
||||||
```cron
|
|
||||||
*/5 * * * * /home/user/porkbun/updateDNS.sh >> /var/log/porkbun-ddns.log 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
Or every hour:
|
|
||||||
```cron
|
|
||||||
0 * * * * /home/user/porkbun/updateDNS.sh >> /var/log/porkbun-ddns.log 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dry-run mode
|
## Dry-run mode
|
||||||
|
|
||||||
You can run the script in **dry-run** mode to see what it would do without actually
|
You can run the script in **dry-run** mode to see what it would do without actually
|
||||||
|
|
@ -188,6 +169,135 @@ In dry-run mode:
|
||||||
This is useful when first configuring the script or making changes to your DNS setup,
|
This is useful when first configuring the script or making changes to your DNS setup,
|
||||||
so you can verify behavior before applying it.
|
so you can verify behavior before applying it.
|
||||||
|
|
||||||
|
## Systemd integration helpers
|
||||||
|
|
||||||
|
This repository also includes helper scripts to install the updater as a systemd service,
|
||||||
|
uninstall it cleanly, and view logs more easily.
|
||||||
|
|
||||||
|
### `install.sh` – install systemd timer + service
|
||||||
|
|
||||||
|
`install.sh` sets up a `porkbun-ddns` systemd service and timer that will run
|
||||||
|
`updateDNS.sh` for you on a schedule (default: every 5 minutes).
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
|
||||||
|
- Verifies required files exist in the current directory:
|
||||||
|
- `updateDNS.sh`
|
||||||
|
- `.env`
|
||||||
|
- `porkbun-ddns.service`
|
||||||
|
- `porkbun-ddns.timer`
|
||||||
|
- Ensures `updateDNS.sh` is executable.
|
||||||
|
- Checks for required dependencies: `curl`, `jq`, `systemctl`.
|
||||||
|
- Validates that `.env` contains `API_KEY`, `SECRET_KEY`, `DOMAIN`, and `MACHINE_ID`.
|
||||||
|
- Runs `updateDNS.sh` once as a test (with your current config).
|
||||||
|
- Installs and enables:
|
||||||
|
- `/etc/systemd/system/porkbun-ddns.service`
|
||||||
|
- `/etc/systemd/system/porkbun-ddns.timer`
|
||||||
|
- Starts the timer and shows its status.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/porkbun
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Do **not** run `install.sh` directly as root; it will use `sudo` when needed.
|
||||||
|
- The service runs as the current user (it rewrites `User=` and `Group=` in
|
||||||
|
the service file to match who ran `install.sh`).
|
||||||
|
|
||||||
|
After installation, some useful commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status porkbun-ddns.timer
|
||||||
|
systemctl status porkbun-ddns.service
|
||||||
|
journalctl -u porkbun-ddns.service -n 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### `uninstall.sh` – remove systemd timer + service
|
||||||
|
|
||||||
|
`uninstall.sh` cleanly removes the `porkbun-ddns` systemd units while leaving your
|
||||||
|
scripts and configuration files intact.
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
|
||||||
|
- Stops `porkbun-ddns.timer` and `porkbun-ddns.service` if they are running.
|
||||||
|
- Disables the timer so it no longer starts at boot.
|
||||||
|
- Removes `/etc/systemd/system/porkbun-ddns.service` and
|
||||||
|
`/etc/systemd/system/porkbun-ddns.timer`.
|
||||||
|
- Reloads systemd and resets any failed state for these units.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/porkbun
|
||||||
|
./uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Do **not** run `uninstall.sh` as root; it will use `sudo` for systemd operations.
|
||||||
|
- Your local files remain:
|
||||||
|
- `updateDNS.sh`
|
||||||
|
- `.env`
|
||||||
|
- `porkbun-ddns.service`
|
||||||
|
- `porkbun-ddns.timer`
|
||||||
|
so you can reinstall later with `./install.sh`.
|
||||||
|
|
||||||
|
### `logs.sh` – convenient log viewer
|
||||||
|
|
||||||
|
`logs.sh` is a small wrapper around `journalctl` and `systemctl` to make it easier
|
||||||
|
to inspect the `porkbun-ddns.service` logs and status.
|
||||||
|
|
||||||
|
Basic usage (show last 50 lines):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/porkbun
|
||||||
|
./logs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Common options:
|
||||||
|
|
||||||
|
- Follow logs in real-time (like `tail -f`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./logs.sh -f
|
||||||
|
```
|
||||||
|
|
||||||
|
- Show last N lines:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./logs.sh -n 100
|
||||||
|
```
|
||||||
|
|
||||||
|
- Show logs from today only:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./logs.sh -t
|
||||||
|
```
|
||||||
|
|
||||||
|
- Show logs from the last hour:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./logs.sh -h
|
||||||
|
```
|
||||||
|
|
||||||
|
- Show only error messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./logs.sh -e
|
||||||
|
```
|
||||||
|
|
||||||
|
- Show service and timer status plus recent runs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./logs.sh -s
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `./logs.sh --help` for the full list of options.
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. **Load Configuration** - Reads API credentials and settings from `.env`
|
1. **Load Configuration** - Reads API credentials and settings from `.env`
|
||||||
|
|
@ -195,13 +305,12 @@ so you can verify behavior before applying it.
|
||||||
3. **Retrieve DNS Records** - Gets all existing DNS records for the domain and displays them
|
3. **Retrieve DNS Records** - Gets all existing DNS records for the domain and displays them
|
||||||
4. **Check for Duplicates** - Detects if multiple records exist for this machine ID
|
4. **Check for Duplicates** - Detects if multiple records exist for this machine ID
|
||||||
5. **Skip if Unchanged** - If IP matches and exactly one record exists, skips update
|
5. **Skip if Unchanged** - If IP matches and exactly one record exists, skips update
|
||||||
6. **Clean Old Records** - Deletes A/ALIAS/CNAME records with no machine ID (migration cleanup)
|
6. **Delete Own Records** - Removes this machine's previous A records (by matching notes field)
|
||||||
7. **Delete Own Records** - Removes this machine's previous A records (by matching notes field)
|
7. **Verify Cleanup** - Ensures all old records were deleted before creating new ones
|
||||||
8. **Verify Cleanup** - Ensures all old records were deleted before creating new ones
|
8. **Create New Record** - Adds fresh A record with current IP and machine ID in notes
|
||||||
9. **Create New Record** - Adds fresh A record with current IP and machine ID in notes
|
9. **Verify Creation** - Confirms exactly one record exists for this machine
|
||||||
10. **Verify Creation** - Confirms exactly one record exists for this machine
|
|
||||||
|
|
||||||
> Note: The script now only deletes and verifies records that it manages
|
> Note: The script only deletes and verifies records that it manages
|
||||||
> (those whose notes are exactly `ddns:<MACHINE_ID>`). Other records in the
|
> (those whose notes are exactly `ddns:<MACHINE_ID>`). Other records in the
|
||||||
> same zone are left untouched.
|
> same zone are left untouched.
|
||||||
|
|
||||||
|
|
@ -228,18 +337,9 @@ so you can verify behavior before applying it.
|
||||||
|
|
||||||
1. **Immediately revoke** the API keys in your Porkbun account
|
1. **Immediately revoke** the API keys in your Porkbun account
|
||||||
2. **Generate new keys** and update your `.env` file
|
2. **Generate new keys** and update your `.env` file
|
||||||
3. **Remove secrets from git history** if they were committed:
|
|
||||||
```bash
|
|
||||||
git filter-branch --force --index-filter \
|
|
||||||
"git rm --cached --ignore-unmatch .env" \
|
|
||||||
--prune-empty --tag-name-filter cat -- --all
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Error: "dig: command not found"
|
|
||||||
The script now uses `curl` instead of `dig`, so this shouldn't happen. If you see this, update to the latest version.
|
|
||||||
|
|
||||||
### Error: "API access not enabled"
|
### Error: "API access not enabled"
|
||||||
1. Log in to Porkbun
|
1. Log in to Porkbun
|
||||||
2. Go to your domain settings
|
2. Go to your domain settings
|
||||||
|
|
@ -258,7 +358,7 @@ The script now uses `curl` instead of `dig`, so this shouldn't happen. If you se
|
||||||
- Check DNS records in Porkbun dashboard to see notes field (look for `ddns:<MACHINE_ID>`)
|
- Check DNS records in Porkbun dashboard to see notes field (look for `ddns:<MACHINE_ID>`)
|
||||||
|
|
||||||
### IP not updating
|
### IP not updating
|
||||||
- Check script is running (add to cron for automatic updates)
|
- Check script is running
|
||||||
- Verify your public IP actually changed
|
- Verify your public IP actually changed
|
||||||
- Check Porkbun API status
|
- Check Porkbun API status
|
||||||
- Review logs for error messages
|
- Review logs for error messages
|
||||||
|
|
@ -278,23 +378,23 @@ Example output with the new structured logging format:
|
||||||
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Using environment file: /home/user/porkbun/.env
|
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Using environment file: /home/user/porkbun/.env
|
||||||
IP source: Porkbun API (/ping)
|
IP source: Porkbun API (/ping)
|
||||||
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Current Public IP: 45.73.134.145
|
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Current Public IP: 45.73.134.145
|
||||||
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Machine ID: KACPER_NIX
|
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Machine ID: EXAMPLE_MACHINE
|
||||||
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Domain: aksal.cloud
|
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Domain: example.com
|
||||||
|
|
||||||
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Processing DNS record for: aksal.cloud (IP: 45.73.134.145)
|
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Processing DNS record for: example.com (IP: 45.73.134.145)
|
||||||
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Machine ID: KACPER_NIX
|
Mon Nov 24 12:04:38 PM EST 2025 [INFO] Machine ID: EXAMPLE_MACHINE
|
||||||
Mon Nov 24 12:04:39 PM EST 2025 [INFO] Existing records for FQDN 'aksal.cloud':
|
Mon Nov 24 12:04:39 PM EST 2025 [INFO] Existing records for FQDN 'example.com':
|
||||||
Type: A, Name: aksal.cloud, Content: 45.73.134.145, Notes: ddns:KACPER_NIX, Managed: true, ID: 508029248
|
Type: A, Name: example.com, Content: 45.73.134.145, Notes: ddns:EXAMPLE_MACHINE, Managed: true, ID: 508029248
|
||||||
Type: NS, Name: aksal.cloud, Content: curitiba.porkbun.com, Notes: none, Managed: false, ID: 506591857
|
Type: NS, Name: example.com, Content: curitiba.porkbun.com, Notes: none, Managed: false, ID: 506591857
|
||||||
Type: NS, Name: aksal.cloud, Content: fortaleza.porkbun.com, Notes: none, Managed: false, ID: 506591856
|
Type: NS, Name: example.com, Content: fortaleza.porkbun.com, Notes: none, Managed: false, ID: 506591856
|
||||||
Type: NS, Name: aksal.cloud, Content: maceio.porkbun.com, Notes: none, Managed: false, ID: 506591854
|
Type: NS, Name: example.com, Content: maceio.porkbun.com, Notes: none, Managed: false, ID: 506591854
|
||||||
Type: NS, Name: aksal.cloud, Content: salvador.porkbun.com, Notes: none, Managed: false, ID: 506591855
|
Type: NS, Name: example.com, Content: salvador.porkbun.com, Notes: none, Managed: false, ID: 506591855
|
||||||
Mon Nov 24 12:04:39 PM EST 2025 [INFO] Record already exists with correct IP (45.73.134.145). No update needed.
|
Mon Nov 24 12:04:39 PM EST 2025 [INFO] Record already exists with correct IP (45.73.134.145). No update needed.
|
||||||
|
|
||||||
Mon Nov 24 12:04:39 PM EST 2025 [INFO] Processing DNS record for: *.aksal.cloud (IP: 45.73.134.145)
|
Mon Nov 24 12:04:39 PM EST 2025 [INFO] Processing DNS record for: *.example.com (IP: 45.73.134.145)
|
||||||
Mon Nov 24 12:04:39 PM EST 2025 [INFO] Machine ID: KACPER_NIX
|
Mon Nov 24 12:04:39 PM EST 2025 [INFO] Machine ID: EXAMPLE_MACHINE
|
||||||
Mon Nov 24 12:04:40 PM EST 2025 [INFO] Existing records for FQDN '*.aksal.cloud':
|
Mon Nov 24 12:04:40 PM EST 2025 [INFO] Existing records for FQDN '*.example.com':
|
||||||
Type: A, Name: *.aksal.cloud, Content: 45.73.134.145, Notes: ddns:KACPER_NIX, Managed: true, ID: 508029259
|
Type: A, Name: *.example.com, Content: 45.73.134.145, Notes: ddns:EXAMPLE_MACHINE, Managed: true, ID: 508029259
|
||||||
Mon Nov 24 12:04:40 PM EST 2025 [INFO] Record already exists with correct IP (45.73.134.145). No update needed.
|
Mon Nov 24 12:04:40 PM EST 2025 [INFO] Record already exists with correct IP (45.73.134.145). No update needed.
|
||||||
|
|
||||||
Mon Nov 24 12:04:40 PM EST 2025 [INFO] DNS update completed successfully
|
Mon Nov 24 12:04:40 PM EST 2025 [INFO] DNS update completed successfully
|
||||||
|
|
@ -310,18 +410,9 @@ Mon Nov 24 12:04:40 PM EST 2025 [INFO] DNS update completed successfully
|
||||||
|
|
||||||
This script is provided as-is for use with Porkbun DNS services.
|
This script is provided as-is for use with Porkbun DNS services.
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues with:
|
|
||||||
- **This script**: Open an issue or review the code
|
|
||||||
- **Porkbun API**: Contact [Porkbun Support](https://porkbun.com/support)
|
|
||||||
- **DNS concepts**: See [Porkbun DNS Documentation](https://kb.porkbun.com/)
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Improvements welcome! Consider adding:
|
Improvements welcome! Consider adding:
|
||||||
- IPv6 support (AAAA records)
|
- IPv6 support (AAAA records)
|
||||||
- Email notifications on IP change
|
- Email notifications on IP change
|
||||||
- Systemd service file
|
|
||||||
- Docker container version
|
|
||||||
- Health check endpoint
|
- Health check endpoint
|
||||||
185
install.sh
Executable file
185
install.sh
Executable file
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Installation script for Porkbun Dynamic DNS systemd service
|
||||||
|
#
|
||||||
|
# This script will:
|
||||||
|
# 1. Verify prerequisites
|
||||||
|
# 2. Copy service files to systemd directory
|
||||||
|
# 3. Enable and start the timer
|
||||||
|
# 4. Show status and usage information
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Get script directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "Porkbun DDNS Service Installer"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [ "$EUID" -eq 0 ]; then
|
||||||
|
echo -e "${RED}Error: Do not run this script as root or with sudo${NC}"
|
||||||
|
echo "The script will request sudo privileges when needed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify required files exist
|
||||||
|
echo "Checking required files..."
|
||||||
|
REQUIRED_FILES=(
|
||||||
|
"$SCRIPT_DIR/updateDNS.sh"
|
||||||
|
"$SCRIPT_DIR/.env"
|
||||||
|
"$SCRIPT_DIR/porkbun-ddns.service"
|
||||||
|
"$SCRIPT_DIR/porkbun-ddns.timer"
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in "${REQUIRED_FILES[@]}"; do
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
echo -e "${RED}Error: Required file not found: $file${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo -e "${GREEN}✓ All required files found${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if updateDNS.sh is executable
|
||||||
|
if [ ! -x "$SCRIPT_DIR/updateDNS.sh" ]; then
|
||||||
|
echo "Making updateDNS.sh executable..."
|
||||||
|
chmod +x "$SCRIPT_DIR/updateDNS.sh"
|
||||||
|
echo -e "${GREEN}✓ Made updateDNS.sh executable${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for required commands
|
||||||
|
echo "Checking dependencies..."
|
||||||
|
MISSING_DEPS=()
|
||||||
|
for cmd in curl jq systemctl; do
|
||||||
|
if ! command -v "$cmd" &> /dev/null; then
|
||||||
|
MISSING_DEPS+=("$cmd")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
||||||
|
echo -e "${RED}Error: Missing required dependencies: ${MISSING_DEPS[*]}${NC}"
|
||||||
|
echo "Please install them and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓ All dependencies installed${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify .env file has required variables
|
||||||
|
echo "Validating .env configuration..."
|
||||||
|
if ! grep -q "^API_KEY=" "$SCRIPT_DIR/.env" || \
|
||||||
|
! grep -q "^SECRET_KEY=" "$SCRIPT_DIR/.env" || \
|
||||||
|
! grep -q "^DOMAIN=" "$SCRIPT_DIR/.env" || \
|
||||||
|
! grep -q "^MACHINE_ID=" "$SCRIPT_DIR/.env"; then
|
||||||
|
echo -e "${YELLOW}Warning: .env file may be incomplete${NC}"
|
||||||
|
echo "Required variables: API_KEY, SECRET_KEY, DOMAIN, MACHINE_ID"
|
||||||
|
echo ""
|
||||||
|
read -p "Continue anyway? (y/N) " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ .env configuration looks good${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test the script
|
||||||
|
echo "Testing DNS update script..."
|
||||||
|
if "$SCRIPT_DIR/updateDNS.sh"; then
|
||||||
|
echo -e "${GREEN}✓ Script test passed${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Error: Script test failed${NC}"
|
||||||
|
echo "Please fix the script configuration before installing the service."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if service is already installed
|
||||||
|
if systemctl list-unit-files | grep -q "porkbun-ddns.timer"; then
|
||||||
|
echo -e "${YELLOW}Warning: porkbun-ddns service is already installed${NC}"
|
||||||
|
read -p "Reinstall? (y/N) " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Stopping existing service..."
|
||||||
|
sudo systemctl stop porkbun-ddns.timer 2>/dev/null || true
|
||||||
|
sudo systemctl disable porkbun-ddns.timer 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create temporary service files with correct paths
|
||||||
|
echo "Preparing service files..."
|
||||||
|
TEMP_SERVICE=$(mktemp)
|
||||||
|
TEMP_TIMER=$(mktemp)
|
||||||
|
|
||||||
|
# Replace paths in service file
|
||||||
|
sed "s|/home/kacper/Documents/platformIO/email/porkbun|$SCRIPT_DIR|g" \
|
||||||
|
"$SCRIPT_DIR/porkbun-ddns.service" > "$TEMP_SERVICE"
|
||||||
|
|
||||||
|
# Update user/group in service file
|
||||||
|
CURRENT_USER=$(whoami)
|
||||||
|
CURRENT_GROUP=$(id -gn)
|
||||||
|
sed -i "s|User=kacper|User=$CURRENT_USER|g" "$TEMP_SERVICE"
|
||||||
|
sed -i "s|Group=kacper|Group=$CURRENT_GROUP|g" "$TEMP_SERVICE"
|
||||||
|
|
||||||
|
# Copy timer file as-is
|
||||||
|
cp "$SCRIPT_DIR/porkbun-ddns.timer" "$TEMP_TIMER"
|
||||||
|
|
||||||
|
# Install service files
|
||||||
|
echo "Installing systemd service files..."
|
||||||
|
sudo cp "$TEMP_SERVICE" /etc/systemd/system/porkbun-ddns.service
|
||||||
|
sudo cp "$TEMP_TIMER" /etc/systemd/system/porkbun-ddns.timer
|
||||||
|
|
||||||
|
# Clean up temp files
|
||||||
|
rm "$TEMP_SERVICE" "$TEMP_TIMER"
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
sudo chmod 644 /etc/systemd/system/porkbun-ddns.service
|
||||||
|
sudo chmod 644 /etc/systemd/system/porkbun-ddns.timer
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Service files installed${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
echo "Reloading systemd daemon..."
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
echo -e "${GREEN}✓ Systemd reloaded${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Enable and start timer
|
||||||
|
echo "Enabling and starting porkbun-ddns.timer..."
|
||||||
|
sudo systemctl enable porkbun-ddns.timer
|
||||||
|
sudo systemctl start porkbun-ddns.timer
|
||||||
|
echo -e "${GREEN}✓ Service enabled and started${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Show status
|
||||||
|
echo "======================================"
|
||||||
|
echo "Installation Complete!"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
echo "Service Status:"
|
||||||
|
sudo systemctl status porkbun-ddns.timer --no-pager -l || true
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}The Porkbun DDNS service is now running!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Useful commands:"
|
||||||
|
echo " View timer status: systemctl status porkbun-ddns.timer"
|
||||||
|
echo " View service logs: journalctl -u porkbun-ddns.service -f"
|
||||||
|
echo " List timers: systemctl list-timers"
|
||||||
|
echo " Run manually: sudo systemctl start porkbun-ddns.service"
|
||||||
|
echo " Stop service: sudo systemctl stop porkbun-ddns.timer"
|
||||||
|
echo " Uninstall: ./uninstall.sh"
|
||||||
|
echo ""
|
||||||
|
echo "The service will update DNS records every 5 minutes."
|
||||||
|
echo "Check logs with: journalctl -u porkbun-ddns.service -n 50"
|
||||||
204
logs.sh
Executable file
204
logs.sh
Executable file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Log viewer for Porkbun Dynamic DNS systemd service
|
||||||
|
#
|
||||||
|
# This script provides convenient access to service logs via journalctl
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
SERVICE_NAME="porkbun-ddns.service"
|
||||||
|
TIMER_NAME="porkbun-ddns.timer"
|
||||||
|
|
||||||
|
# Check if systemctl is available
|
||||||
|
if ! command -v systemctl &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: systemctl not found${NC}"
|
||||||
|
echo "This script requires systemd."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if journalctl is available
|
||||||
|
if ! command -v journalctl &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: journalctl not found${NC}"
|
||||||
|
echo "This script requires journalctl."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to show help
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
${GREEN}Porkbun DDNS Log Viewer${NC}
|
||||||
|
|
||||||
|
Usage: $0 [OPTION]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f, --follow Follow logs in real-time (like tail -f)
|
||||||
|
-n, --lines NUMBER Show last N lines (default: 50)
|
||||||
|
-t, --today Show logs from today only
|
||||||
|
-h, --hour Show logs from the last hour
|
||||||
|
-b, --boot Show logs since last boot
|
||||||
|
-a, --all Show all logs (no limit)
|
||||||
|
-s, --status Show service and timer status
|
||||||
|
-e, --errors Show only error messages
|
||||||
|
-v, --verbose Show verbose output (all log levels)
|
||||||
|
--help Show this help message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$0 Show last 50 log lines
|
||||||
|
$0 -f Follow logs in real-time
|
||||||
|
$0 -n 100 Show last 100 lines
|
||||||
|
$0 -t Show today's logs
|
||||||
|
$0 -h Show last hour's logs
|
||||||
|
$0 -e Show only errors
|
||||||
|
$0 -s Show service status
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to show status
|
||||||
|
show_status() {
|
||||||
|
echo -e "${BLUE}=== Timer Status ===${NC}"
|
||||||
|
systemctl status "$TIMER_NAME" --no-pager -l || true
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}=== Service Status ===${NC}"
|
||||||
|
systemctl status "$SERVICE_NAME" --no-pager -l || true
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}=== Next Scheduled Runs ===${NC}"
|
||||||
|
systemctl list-timers "$TIMER_NAME" --no-pager || true
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}=== Recent Runs ===${NC}"
|
||||||
|
journalctl -u "$SERVICE_NAME" -n 5 --no-pager -o short-precise
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
FOLLOW=false
|
||||||
|
LINES=50
|
||||||
|
SINCE=""
|
||||||
|
PRIORITY=""
|
||||||
|
SHOW_ALL=false
|
||||||
|
SHOW_STATUS=false
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
# Default: show last 50 lines
|
||||||
|
:
|
||||||
|
else
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-f|--follow)
|
||||||
|
FOLLOW=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-n|--lines)
|
||||||
|
LINES="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-t|--today)
|
||||||
|
SINCE="today"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--hour)
|
||||||
|
SINCE="1 hour ago"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-b|--boot)
|
||||||
|
SINCE="boot"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-a|--all)
|
||||||
|
SHOW_ALL=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-s|--status)
|
||||||
|
SHOW_STATUS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-e|--errors)
|
||||||
|
PRIORITY="err"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-v|--verbose)
|
||||||
|
PRIORITY=""
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Error: Unknown option: $1${NC}"
|
||||||
|
echo "Use --help for usage information"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if service exists
|
||||||
|
if ! systemctl list-unit-files | grep -q "$SERVICE_NAME"; then
|
||||||
|
echo -e "${YELLOW}Warning: $SERVICE_NAME is not installed${NC}"
|
||||||
|
echo "Install the service first with: ./install.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show status if requested
|
||||||
|
if [ "$SHOW_STATUS" = true ]; then
|
||||||
|
show_status
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build journalctl command
|
||||||
|
CMD="journalctl -u $SERVICE_NAME"
|
||||||
|
|
||||||
|
# Add follow flag
|
||||||
|
if [ "$FOLLOW" = true ]; then
|
||||||
|
CMD="$CMD -f"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add line limit
|
||||||
|
if [ "$SHOW_ALL" = false ] && [ "$FOLLOW" = false ]; then
|
||||||
|
CMD="$CMD -n $LINES"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add time filter
|
||||||
|
if [ -n "$SINCE" ]; then
|
||||||
|
if [ "$SINCE" = "boot" ]; then
|
||||||
|
CMD="$CMD -b"
|
||||||
|
else
|
||||||
|
CMD="$CMD --since \"$SINCE\""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add priority filter
|
||||||
|
if [ -n "$PRIORITY" ]; then
|
||||||
|
CMD="$CMD -p $PRIORITY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add no-pager for non-follow mode
|
||||||
|
if [ "$FOLLOW" = false ]; then
|
||||||
|
CMD="$CMD --no-pager"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show what we're doing
|
||||||
|
if [ "$FOLLOW" = true ]; then
|
||||||
|
echo -e "${GREEN}Following logs for $SERVICE_NAME...${NC}"
|
||||||
|
echo "Press Ctrl+C to stop"
|
||||||
|
echo ""
|
||||||
|
elif [ -n "$SINCE" ]; then
|
||||||
|
echo -e "${GREEN}Showing logs since $SINCE...${NC}"
|
||||||
|
echo ""
|
||||||
|
elif [ "$SHOW_ALL" = true ]; then
|
||||||
|
echo -e "${GREEN}Showing all logs for $SERVICE_NAME...${NC}"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}Showing last $LINES log lines for $SERVICE_NAME...${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Execute the command
|
||||||
|
eval "$CMD"
|
||||||
14
porkbun-ddns.service
Normal file
14
porkbun-ddns.service
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Porkbun Dynamic DNS Update
|
||||||
|
After=network.target
|
||||||
|
Wants=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=porkbun-ddns-user
|
||||||
|
Group=porkbun-ddns-group
|
||||||
|
WorkingDirectory=/opt/porkbun-ddns
|
||||||
|
ExecStart=/opt/porkbun-ddns/updateDNS.sh
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
17
porkbun-ddns.timer
Normal file
17
porkbun-ddns.timer
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Porkbun Dynamic DNS Update Timer
|
||||||
|
Documentation=file:///opt/porkbun-ddns/README.md
|
||||||
|
Requires=porkbun-ddns.service
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Run 2 minutes after boot
|
||||||
|
OnBootSec=2min
|
||||||
|
|
||||||
|
# Run every 5 minutes
|
||||||
|
OnUnitActiveSec=5min
|
||||||
|
|
||||||
|
# If the system was off when a timer should have run, run it on next boot
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
149
uninstall.sh
Executable file
149
uninstall.sh
Executable file
|
|
@ -0,0 +1,149 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Uninstallation script for Porkbun Dynamic DNS systemd service
|
||||||
|
#
|
||||||
|
# This script will:
|
||||||
|
# 1. Stop and disable the timer
|
||||||
|
# 2. Remove service files from systemd
|
||||||
|
# 3. Reload systemd daemon
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "Porkbun DDNS Service Uninstaller"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [ "$EUID" -eq 0 ]; then
|
||||||
|
echo -e "${RED}Error: Do not run this script as root or with sudo${NC}"
|
||||||
|
echo "The script will request sudo privileges when needed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if systemctl is available
|
||||||
|
if ! command -v systemctl &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: systemctl not found${NC}"
|
||||||
|
echo "This script requires systemd."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if service is installed
|
||||||
|
if ! systemctl list-unit-files | grep -q "porkbun-ddns.timer"; then
|
||||||
|
echo -e "${YELLOW}porkbun-ddns service is not installed${NC}"
|
||||||
|
echo "Nothing to uninstall."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Confirm uninstallation
|
||||||
|
echo -e "${YELLOW}This will remove the porkbun-ddns systemd service.${NC}"
|
||||||
|
echo "Your DNS update script and configuration will NOT be deleted."
|
||||||
|
echo ""
|
||||||
|
read -p "Continue with uninstallation? (y/N) " -n 1 -r
|
||||||
|
echo ""
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Uninstallation cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Stop the timer if running
|
||||||
|
echo "Stopping porkbun-ddns.timer..."
|
||||||
|
if systemctl is-active --quiet porkbun-ddns.timer; then
|
||||||
|
sudo systemctl stop porkbun-ddns.timer
|
||||||
|
echo -e "${GREEN}✓ Timer stopped${NC}"
|
||||||
|
else
|
||||||
|
echo "Timer is not running"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Stop the service if running
|
||||||
|
echo "Stopping porkbun-ddns.service..."
|
||||||
|
if systemctl is-active --quiet porkbun-ddns.service; then
|
||||||
|
sudo systemctl stop porkbun-ddns.service
|
||||||
|
echo -e "${GREEN}✓ Service stopped${NC}"
|
||||||
|
else
|
||||||
|
echo "Service is not running"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Disable the timer
|
||||||
|
echo "Disabling porkbun-ddns.timer..."
|
||||||
|
if systemctl is-enabled --quiet porkbun-ddns.timer; then
|
||||||
|
sudo systemctl disable porkbun-ddns.timer
|
||||||
|
echo -e "${GREEN}✓ Timer disabled${NC}"
|
||||||
|
else
|
||||||
|
echo "Timer was not enabled"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Remove service files
|
||||||
|
echo "Removing service files..."
|
||||||
|
REMOVED=0
|
||||||
|
|
||||||
|
if [ -f /etc/systemd/system/porkbun-ddns.service ]; then
|
||||||
|
sudo rm /etc/systemd/system/porkbun-ddns.service
|
||||||
|
echo -e "${GREEN}✓ Removed porkbun-ddns.service${NC}"
|
||||||
|
((REMOVED++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /etc/systemd/system/porkbun-ddns.timer ]; then
|
||||||
|
sudo rm /etc/systemd/system/porkbun-ddns.timer
|
||||||
|
echo -e "${GREEN}✓ Removed porkbun-ddns.timer${NC}"
|
||||||
|
((REMOVED++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $REMOVED -eq 0 ]; then
|
||||||
|
echo -e "${YELLOW}No service files found to remove${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Remove override directory if it exists
|
||||||
|
if [ -d /etc/systemd/system/porkbun-ddns.service.d ]; then
|
||||||
|
sudo rm -rf /etc/systemd/system/porkbun-ddns.service.d
|
||||||
|
echo -e "${GREEN}✓ Removed service override directory${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
echo "Reloading systemd daemon..."
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
echo -e "${GREEN}✓ Systemd reloaded${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Reset failed state if exists
|
||||||
|
sudo systemctl reset-failed porkbun-ddns.service 2>/dev/null || true
|
||||||
|
sudo systemctl reset-failed porkbun-ddns.timer 2>/dev/null || true
|
||||||
|
|
||||||
|
# Verify uninstallation
|
||||||
|
echo "Verifying uninstallation..."
|
||||||
|
if systemctl list-unit-files | grep -q "porkbun-ddns"; then
|
||||||
|
echo -e "${RED}Warning: Some porkbun-ddns units still found${NC}"
|
||||||
|
systemctl list-unit-files | grep porkbun-ddns
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ All service units removed${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "Uninstallation Complete!"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}The porkbun-ddns systemd service has been removed.${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Your files are still available:"
|
||||||
|
echo " - updateDNS.sh (DNS update script)"
|
||||||
|
echo " - .env (configuration)"
|
||||||
|
echo " - porkbun-ddns.service (service template)"
|
||||||
|
echo " - porkbun-ddns.timer (timer template)"
|
||||||
|
echo ""
|
||||||
|
echo "You can:"
|
||||||
|
echo " - Reinstall with: ./install.sh"
|
||||||
|
echo " - Run manually with: ./updateDNS.sh"
|
||||||
|
echo " - Delete all files if no longer needed"
|
||||||
382
updateDNS.sh
Executable file
382
updateDNS.sh
Executable file
|
|
@ -0,0 +1,382 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Porkbun Dynamic DNS Update Script
|
||||||
|
# Updates DNS A records with current public IP address
|
||||||
|
# Supports multi-machine failover configurations
|
||||||
|
#
|
||||||
|
# Author: Auto-generated
|
||||||
|
# License: MIT
|
||||||
|
|
||||||
|
# Exit on error, undefined variables, and pipe failures
|
||||||
|
set -euo pipefail
|
||||||
|
# NOTE:
|
||||||
|
# - Critical operations should use explicit error checks: `if ! cmd; then ...; fi`
|
||||||
|
# - Some calls intentionally suppress errors with `||` to allow fallbacks (e.g. IP detection)
|
||||||
|
# - Be careful adding new commands to conditionals or pipelines; unexpected `set -e` behavior
|
||||||
|
# can cause early exits if errors aren't handled explicitly.
|
||||||
|
|
||||||
|
# Trap errors and cleanup (final safety net; most errors are handled explicitly)
|
||||||
|
trap 'echo "Error on line $LINENO. Exit code: $?" >&2' ERR
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||||
|
set -a # Mark variables for export
|
||||||
|
# Info-level log: configuration file found
|
||||||
|
printf '%s [INFO] Using environment file: %s/.env\n' "$(date)" "$SCRIPT_DIR" >&2
|
||||||
|
source "$SCRIPT_DIR/.env"
|
||||||
|
set +a # Stop marking variables for export
|
||||||
|
else
|
||||||
|
printf '%s [ERROR] .env file not found in %s\n' "$(date)" "$SCRIPT_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate required environment variables without leaking secrets
|
||||||
|
if [ -z "${API_KEY:-}" ] || [ -z "${SECRET_KEY:-}" ] || [ -z "${DOMAIN:-}" ]; then
|
||||||
|
printf '%s [ERROR] API_KEY, SECRET_KEY, and DOMAIN must be set in .env file\n' "$(date)" >&2
|
||||||
|
printf '%s [ERROR] Current values:\n' "$(date)" >&2
|
||||||
|
if [ -n "${API_KEY:-}" ]; then
|
||||||
|
printf '%s [ERROR] API_KEY=<set>\n' "$(date)" >&2
|
||||||
|
else
|
||||||
|
printf '%s [ERROR] API_KEY=<not set>\n' "$(date)" >&2
|
||||||
|
fi
|
||||||
|
if [ -n "${SECRET_KEY:-}" ]; then
|
||||||
|
printf '%s [ERROR] SECRET_KEY=<set>\n' "$(date)" >&2
|
||||||
|
else
|
||||||
|
printf '%s [ERROR] SECRET_KEY=<not set>\n' "$(date)" >&2
|
||||||
|
fi
|
||||||
|
printf '%s [ERROR] DOMAIN=%s\n' "$(date)" "${DOMAIN:-<not set>}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Warn if .env permissions are too permissive
|
||||||
|
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||||
|
env_perms=$(stat -c '%a' "$SCRIPT_DIR/.env" 2>/dev/null || echo "")
|
||||||
|
if [ -n "$env_perms" ] && [ "$env_perms" -gt 600 ]; then
|
||||||
|
printf '%s [WARN] %s/.env has permissions %s; consider restricting to 600 to protect secrets\n' "$(date)" "$SCRIPT_DIR" "$env_perms" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# MACHINE_ID is required for multi-machine setups
|
||||||
|
if [ -z "${MACHINE_ID:-}" ]; then
|
||||||
|
printf '%s [WARN] MACHINE_ID not set in .env file\n' "$(date)" >&2
|
||||||
|
printf '%s [INFO] Using hostname as MACHINE_ID: %s\n' "$(date)" "$(hostname)" >&2
|
||||||
|
MACHINE_ID="$(hostname)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate MACHINE_ID doesn't contain special characters that could cause issues
|
||||||
|
if [[ "$MACHINE_ID" =~ [^a-zA-Z0-9_-] ]]; then
|
||||||
|
printf '%s [ERROR] MACHINE_ID contains invalid characters. Only alphanumeric, dash, and underscore allowed.\n' "$(date)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PORKBUN_API_URL="https://api-ipv4.porkbun.com/api/json/v3"
|
||||||
|
NOTES_PREFIX="ddns:"
|
||||||
|
|
||||||
|
# Optional dry-run mode. When DRY_RUN=1, destructive or mutating API calls
|
||||||
|
# (dns/create, dns/delete) are logged but not executed.
|
||||||
|
DRY_RUN=${DRY_RUN:-0}
|
||||||
|
|
||||||
|
# Check for required commands
|
||||||
|
for cmd in curl jq; do
|
||||||
|
if ! command -v "$cmd" &> /dev/null; then
|
||||||
|
printf '%s [ERROR] Required command %s not found. Please install it.\n' "$(date)" "$cmd" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
function get_public_ip() {
|
||||||
|
local ip=""
|
||||||
|
|
||||||
|
# Try Porkbun's ping endpoint first (most reliable for their API)
|
||||||
|
local auth_json
|
||||||
|
if ! auth_json=$(jq -n \
|
||||||
|
--arg apikey "$API_KEY" \
|
||||||
|
--arg secretapikey "$SECRET_KEY" \
|
||||||
|
'{apikey: $apikey, secretapikey: $secretapikey}' 2>&1); then
|
||||||
|
printf '%s [ERROR] Failed to create auth JSON: %s\n' "$(date)" "$auth_json" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ping_response
|
||||||
|
ping_response=$(curl -s --max-time 10 --retry 2 -X POST "$PORKBUN_API_URL/ping" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$auth_json" 2>&1 || echo "")
|
||||||
|
|
||||||
|
if [ -n "$ping_response" ]; then
|
||||||
|
ip=$(echo "$ping_response" | jq -r '.yourIp // empty' 2>/dev/null || echo "")
|
||||||
|
if [ -n "$ip" ]; then
|
||||||
|
printf '%s [INFO] IP source: Porkbun API (/ping)\n' "$(date)" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback methods if Porkbun ping fails
|
||||||
|
if [ -z "$ip" ]; then
|
||||||
|
ip=$(curl -s --max-time 5 --retry 2 https://api.ipify.org 2>/dev/null || echo "")
|
||||||
|
if [ -n "$ip" ]; then
|
||||||
|
printf '%s [INFO] IP source: api.ipify.org\n' "$(date)" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ip" ]; then
|
||||||
|
ip=$(curl -s --max-time 5 --retry 2 https://icanhazip.com 2>/dev/null || echo "")
|
||||||
|
if [ -n "$ip" ]; then
|
||||||
|
printf '%s [INFO] IP source: icanhazip.com\n' "$(date)" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ip" ]; then
|
||||||
|
printf '%s [ERROR] Failed to retrieve public IP address from any source\n' "$(date)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate IP format (basic IPv4 validation)
|
||||||
|
if ! [[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||||
|
printf '%s [ERROR] Invalid IP address format: %s\n' "$(date)" "$ip" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
function register_host_dns() {
|
||||||
|
local host="$1"
|
||||||
|
local domain="$2"
|
||||||
|
local public_ip="$3"
|
||||||
|
|
||||||
|
# Validate inputs
|
||||||
|
if [ -z "$domain" ]; then
|
||||||
|
printf '%s [ERROR] Domain parameter is empty\n' "$(date)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$public_ip" ]; then
|
||||||
|
printf '%s [ERROR] Public IP parameter is empty\n' "$(date)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build FQDN for display
|
||||||
|
local fqdn
|
||||||
|
if [ -z "$host" ]; then
|
||||||
|
fqdn="$domain"
|
||||||
|
else
|
||||||
|
fqdn="${host}.${domain}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s [INFO] Processing DNS record for: %s (IP: %s)\n' "$(date)" "$fqdn" "$public_ip"
|
||||||
|
printf '%s [INFO] Machine ID: %s\n' "$(date)" "$MACHINE_ID"
|
||||||
|
|
||||||
|
# Create auth JSON for Porkbun API
|
||||||
|
local auth_json
|
||||||
|
if ! auth_json=$(jq -n \
|
||||||
|
--arg apikey "$API_KEY" \
|
||||||
|
--arg secretapikey "$SECRET_KEY" \
|
||||||
|
'{apikey: $apikey, secretapikey: $secretapikey}' 2>&1); then
|
||||||
|
printf '%s [ERROR] Failed to create auth JSON: %s\n' "$(date)" "$auth_json" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve all DNS records for the domain
|
||||||
|
local records
|
||||||
|
if ! records=$(curl -s --max-time 10 --retry 2 -X POST "$PORKBUN_API_URL/dns/retrieve/$domain" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$auth_json" 2>&1); then
|
||||||
|
printf '%s [ERROR] Failed to retrieve DNS records: %s\n' "$(date)" "$records" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if response is valid JSON
|
||||||
|
if ! echo "$records" | jq empty 2>/dev/null; then
|
||||||
|
printf '%s [ERROR] API returned invalid JSON. Response: %s\n' "$(date)" "$records" >&2
|
||||||
|
printf '%s [ERROR] Check if API access is enabled for domain: %s\n' "$(date)" "$domain" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if API call was successful
|
||||||
|
local status
|
||||||
|
status=$(echo "$records" | jq -r '.status // "error"')
|
||||||
|
if [ "$status" == "ERROR" ]; then
|
||||||
|
local message
|
||||||
|
message=$(echo "$records" | jq -r '.message // "Unknown error"')
|
||||||
|
printf '%s [ERROR] Failed to retrieve DNS records. Message: %s\n' "$(date)" "$message" >&2
|
||||||
|
printf '%s [ERROR] Make sure API access is enabled for domain: %s\n' "$(date)" "$domain" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show what records exist for this FQDN
|
||||||
|
printf "%s [INFO] Existing records for FQDN '%s':\n" "$(date)" "$fqdn"
|
||||||
|
echo "$records" | jq -r --arg fqdn "$fqdn" --arg prefix "$NOTES_PREFIX" '.records[] | select(.name == $fqdn) | " Type: \(.type), Name: \(.name), Content: \(.content), Notes: \(.notes // "none"), Managed: \( (.notes // "") | startswith($prefix) ), ID: \(.id)"'
|
||||||
|
|
||||||
|
# Check for duplicate records with the same machine ID (only within managed records)
|
||||||
|
local machine_records
|
||||||
|
machine_records=$(echo "$records" | jq -r --arg fqdn "$fqdn" --arg machine_id "$MACHINE_ID" --arg prefix "$NOTES_PREFIX" '.records[] | select(.name == $fqdn and .type == "A" and ((.notes // "") == ($prefix + $machine_id)))')
|
||||||
|
local record_count
|
||||||
|
record_count=$(echo "$machine_records" | jq -s 'length')
|
||||||
|
|
||||||
|
if [ "$record_count" -gt 1 ]; then
|
||||||
|
printf '%s [WARN] Found %d A records for machine %s (expected 0 or 1)\n' "$(date)" "$record_count" "$MACHINE_ID" >&2
|
||||||
|
printf '%s [WARN] This may indicate concurrent script execution or previous failures. Cleaning up...\n' "$(date)" >&2
|
||||||
|
# Will be cleaned up in the deletion step below
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if this machine already has a record with the current IP
|
||||||
|
local existing_ip
|
||||||
|
existing_ip=$(echo "$records" | jq -r --arg fqdn "$fqdn" --arg machine_id "$MACHINE_ID" --arg prefix "$NOTES_PREFIX" '.records[] | select(.name == $fqdn and .type == "A" and ((.notes // "") == ($prefix + $machine_id))) | .content' | head -1)
|
||||||
|
|
||||||
|
if [ "$existing_ip" == "$public_ip" ] && [ "$record_count" -eq 1 ]; then
|
||||||
|
printf "%s [INFO] Record already exists with correct IP (%s). No update needed.\n" "$(date)" "$public_ip"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# IP has changed, log it
|
||||||
|
if [ -n "$existing_ip" ] && [ "$existing_ip" != "$public_ip" ]; then
|
||||||
|
printf "%s [INFO] IP changed from %s to %s\n" "$(date)" "$existing_ip" "$public_ip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we have duplicates, we need to clean them up
|
||||||
|
if [ "$record_count" -gt 1 ]; then
|
||||||
|
printf "%s [INFO] Cleaning up %d duplicate records for machine '%s'\n" "$(date)" "$record_count" "$MACHINE_ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete A/ALIAS/CNAME records that belong to this MACHINE_ID within the managed namespace
|
||||||
|
# Managed records are identified by notes starting with NOTES_PREFIX (e.g. "ddns:<machine_id>")
|
||||||
|
printf "%s [INFO] Deleting managed records for machine '%s' (notes starting with '%s')\n" "$(date)" "$MACHINE_ID" "$NOTES_PREFIX"
|
||||||
|
|
||||||
|
local delete_count=0
|
||||||
|
while IFS= read -r record_id; do
|
||||||
|
if [ -n "$record_id" ]; then
|
||||||
|
printf "%s [INFO] Deleting record ID: %s\n" "$(date)" "$record_id"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
printf "%s [INFO] DRY RUN: Skipping actual delete for record ID %s\n" "$(date)" "$record_id"
|
||||||
|
else
|
||||||
|
if curl -s --max-time 10 --retry 2 -X POST "$PORKBUN_API_URL/dns/delete/$domain/$record_id" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$auth_json" > /dev/null 2>&1; then
|
||||||
|
((delete_count++)) || true
|
||||||
|
else
|
||||||
|
printf '%s [WARN] Failed to delete record ID: %s\n' "$(date)" "$record_id" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(echo "$records" | jq -r --arg fqdn "$fqdn" --arg machine_id "$MACHINE_ID" --arg prefix "$NOTES_PREFIX" '.records[] | select(.name == $fqdn and (.type == "A" or .type == "ALIAS" or .type == "CNAME") and ((.notes // "") == ($prefix + $machine_id))) | .id')
|
||||||
|
|
||||||
|
if [ $delete_count -gt 0 ]; then
|
||||||
|
printf "%s [INFO] Deleted %d record(s)\n" "$(date)" "$delete_count"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify no duplicates remain before creating new record
|
||||||
|
# Re-fetch records to ensure we have latest state
|
||||||
|
if ! records=$(curl -s --max-time 10 --retry 2 -X POST "$PORKBUN_API_URL/dns/retrieve/$domain" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$auth_json" 2>&1); then
|
||||||
|
printf '%s [WARN] Failed to re-verify DNS records after deletion: %s\n' "$(date)" "$records" >&2
|
||||||
|
# Continue anyway - deletion likely succeeded
|
||||||
|
else
|
||||||
|
local remaining_count
|
||||||
|
remaining_count=$(echo "$records" | jq -r --arg fqdn "$fqdn" --arg machine_id "$MACHINE_ID" --arg prefix "$NOTES_PREFIX" '[.records[] | select(.name == $fqdn and .type == "A" and ((.notes // "") == ($prefix + $machine_id))) ] | length')
|
||||||
|
if [ "$remaining_count" -gt 0 ]; then
|
||||||
|
printf '%s [ERROR] Found %d A record(s) still present for machine %s after deletion\n' "$(date)" "$remaining_count" "$MACHINE_ID" >&2
|
||||||
|
printf '%s [ERROR] This should not happen. Possible API delay or concurrent execution.\n' "$(date)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create new A record for this machine's IP with machine ID in notes
|
||||||
|
printf "%s [INFO] Creating new A record for '%s' with IP: %s\n" "$(date)" "$fqdn" "$public_ip"
|
||||||
|
|
||||||
|
# Build JSON with machine ID in notes field within managed namespace
|
||||||
|
local notes_value
|
||||||
|
notes_value="${NOTES_PREFIX}${MACHINE_ID}"
|
||||||
|
local create_json
|
||||||
|
if ! create_json=$(jq -n \
|
||||||
|
--arg apikey "$API_KEY" \
|
||||||
|
--arg secretapikey "$SECRET_KEY" \
|
||||||
|
--arg name "$host" \
|
||||||
|
--arg type "A" \
|
||||||
|
--arg content "$public_ip" \
|
||||||
|
--arg notes "$notes_value" \
|
||||||
|
'{apikey: $apikey, secretapikey: $secretapikey, name: $name, type: $type, content: $content, ttl: 300, notes: $notes}' 2>&1); then
|
||||||
|
printf '%s [ERROR] Failed to create JSON for new record: %s\n' "$(date)" "$create_json" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
printf "%s [INFO] DRY RUN: Skipping actual create for '%s' (would create A %s -> %s with notes '%s')\n" "$(date)" "$fqdn" "$fqdn" "$public_ip" "$notes_value"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local create_response
|
||||||
|
if ! create_response=$(curl -s --max-time 10 --retry 2 -X POST "$PORKBUN_API_URL/dns/create/$domain" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$create_json" 2>&1); then
|
||||||
|
printf '%s [ERROR] Failed to create DNS record: %s\n' "$(date)" "$create_response" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local create_status
|
||||||
|
create_status=$(echo "$create_response" | jq -r '.status // "error"')
|
||||||
|
if [ "$create_status" == "SUCCESS" ]; then
|
||||||
|
printf "%s [INFO] Successfully created A record for %s (machine: %s)\n" "$(date)" "$fqdn" "$MACHINE_ID"
|
||||||
|
|
||||||
|
# Final verification: ensure exactly one record exists
|
||||||
|
if records=$(curl -s --max-time 10 --retry 2 -X POST "$PORKBUN_API_URL/dns/retrieve/$domain" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$auth_json" 2>&1); then
|
||||||
|
local final_count
|
||||||
|
final_count=$(echo "$records" | jq -r --arg fqdn "$fqdn" --arg machine_id "$MACHINE_ID" --arg prefix "$NOTES_PREFIX" '[.records[] | select(.name == $fqdn and .type == "A" and ((.notes // "") == ($prefix + $machine_id))) ] | length')
|
||||||
|
if [ "$final_count" -eq 1 ]; then
|
||||||
|
printf "%s [INFO] Verified: Exactly 1 A record exists for machine '%s'\n" "$(date)" "$MACHINE_ID"
|
||||||
|
elif [ "$final_count" -gt 1 ]; then
|
||||||
|
printf '%s [WARN] Found %d A records for machine %s after creation (expected 1)\n' "$(date)" "$final_count" "$MACHINE_ID" >&2
|
||||||
|
printf '%s [WARN] This may indicate concurrent script execution. The extra records will be cleaned up on next run.\n' "$(date)" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
local error_msg
|
||||||
|
error_msg=$(echo "$create_response" | jq -r '.message // "Unknown error"')
|
||||||
|
printf '%s [ERROR] Failed to create DNS record. Message: %s\n' "$(date)" "$error_msg" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
main() {
|
||||||
|
local exit_code=0
|
||||||
|
|
||||||
|
# Get public IP
|
||||||
|
local pip
|
||||||
|
if ! pip=$(get_public_ip); then
|
||||||
|
printf '%s [ERROR] Failed to get public IP\n' "$(date)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%s [INFO] Current Public IP: %s\n" "$(date)" "$pip"
|
||||||
|
printf "%s [INFO] Machine ID: %s\n" "$(date)" "$MACHINE_ID"
|
||||||
|
printf "%s [INFO] Domain: %s\n\n" "$(date)" "$DOMAIN"
|
||||||
|
|
||||||
|
# Update DNS records for the configured domain
|
||||||
|
# Both root domain and wildcard will have this machine's A record
|
||||||
|
if ! register_host_dns "" "$DOMAIN" "$pip"; then
|
||||||
|
printf '%s [ERROR] Failed to update root domain DNS record\n' "$(date)" >&2
|
||||||
|
exit_code=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! register_host_dns "*" "$DOMAIN" "$pip"; then
|
||||||
|
printf '%s [ERROR] Failed to update wildcard DNS record\n' "$(date)" >&2
|
||||||
|
exit_code=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
printf "\n%s [INFO] DNS update completed successfully\n" "$(date)"
|
||||||
|
else
|
||||||
|
printf "\n%s [ERROR] DNS update completed with errors\n" "$(date)" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exit_code
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main
|
||||||
Loading…
Add table
Add a link
Reference in a new issue