feat: WAN IP Guard - dynamic DNS updater with WhatsApp notifications
Checks public WAN IP every 5 minutes via systemd timer. On change: - Updates A-records via DirectAdmin API (login key auth) - Sends WhatsApp notification via wwebjs-api
This commit is contained in:
commit
c4c80e98af
9 changed files with 461 additions and 0 deletions
16
.env.example
Normal file
16
.env.example
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# DirectAdmin API credentials
|
||||||
|
DA_LOGIN_KEY=your_login_key_here
|
||||||
|
DA_USERNAME=your_username
|
||||||
|
DA_BASE_URL=https://panel.example.com:2222
|
||||||
|
DA_DOMAIN=example.com
|
||||||
|
|
||||||
|
# WhatsApp API (wwebjs-api)
|
||||||
|
WHATSAPP_API_ENDPOINT=http://localhost:3001
|
||||||
|
WHATSAPP_SESSION=your_session
|
||||||
|
ADMIN_PHONE=31600000000
|
||||||
|
|
||||||
|
# DNS records to update (comma-separated, use domain name for apex)
|
||||||
|
DNS_RECORDS=example.com,git,www
|
||||||
|
|
||||||
|
# State file path (where to store last known IP)
|
||||||
|
STATE_FILE=/var/lib/wan-ip-guard/current_ip
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.env
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
114
README.md
Normal file
114
README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
# WAN IP Guard
|
||||||
|
|
||||||
|
Bewaakt het publieke WAN IP en update automatisch DNS records bij wijziging, plus WhatsApp notificatie.
|
||||||
|
|
||||||
|
## Wat doet het?
|
||||||
|
|
||||||
|
- Controleert elke 5 minuten het publieke WAN IP
|
||||||
|
- Bij wijziging:
|
||||||
|
- Updatet A-records via de DirectAdmin API
|
||||||
|
- Stuurt een WhatsApp notificatie naar de admin
|
||||||
|
- Draait als systemd timer
|
||||||
|
|
||||||
|
## Installatie
|
||||||
|
|
||||||
|
### 1. Clone op je server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~
|
||||||
|
git clone <jouw-repo-url> wan-ip-guard
|
||||||
|
cd wan-ip-guard
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configureer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env met je credentials
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Installeer dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --omit=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Systemd setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# State directory
|
||||||
|
sudo mkdir -p /var/lib/wan-ip-guard
|
||||||
|
sudo chown $USER:$USER /var/lib/wan-ip-guard
|
||||||
|
|
||||||
|
# Pas de paden in systemd/*.service aan naar jouw setup
|
||||||
|
# (gebruiker, WorkingDirectory, ExecStart, EnvironmentFile)
|
||||||
|
|
||||||
|
# Systemd units
|
||||||
|
sudo cp systemd/wan-ip-guard.service /etc/systemd/system/
|
||||||
|
sudo cp systemd/wan-ip-guard.timer /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now wan-ip-guard.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuratie (.env)
|
||||||
|
|
||||||
|
| Variabele | Beschrijving | Voorbeeld |
|
||||||
|
|-----------|-------------|-----------|
|
||||||
|
| `DA_LOGIN_KEY` | DirectAdmin Login Key | `your_login_key` |
|
||||||
|
| `DA_USERNAME` | DirectAdmin gebruiker | `your_username` |
|
||||||
|
| `DA_BASE_URL` | DirectAdmin base URL | `https://panel.example.com:2222` |
|
||||||
|
| `DA_DOMAIN` | Domeinnaam | `example.com` |
|
||||||
|
| `WHATSAPP_API_ENDPOINT` | wwebjs-api URL | `http://localhost:3001` |
|
||||||
|
| `WHATSAPP_SESSION` | WhatsApp sessie naam | `your_session` |
|
||||||
|
| `ADMIN_PHONE` | Admin telefoonnummer | `31600000000` |
|
||||||
|
| `DNS_RECORDS` | Komma-gescheiden records | `example.com,git,www` |
|
||||||
|
| `STATE_FILE` | Pad naar state file | `/var/lib/wan-ip-guard/current_ip` |
|
||||||
|
|
||||||
|
## DirectAdmin Login Key aanmaken
|
||||||
|
|
||||||
|
1. Log in op het DirectAdmin panel
|
||||||
|
2. Ga naar **Accountbeheer** → **Login Keys**
|
||||||
|
3. Maak een nieuwe key aan met rechten voor **DNS Control**
|
||||||
|
4. Sla de key op (het wordt maar één keer getoond)
|
||||||
|
|
||||||
|
## Beheer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Timer status
|
||||||
|
systemctl status wan-ip-guard.timer
|
||||||
|
|
||||||
|
# Logs volgen
|
||||||
|
journalctl -u wan-ip-guard -f
|
||||||
|
|
||||||
|
# Handmatig draaien
|
||||||
|
cd ~/wan-ip-guard && node index.js
|
||||||
|
|
||||||
|
# Timer herstarten
|
||||||
|
sudo systemctl restart wan-ip-guard.timer
|
||||||
|
|
||||||
|
# State bestand resetten (forceer update bij volgende run)
|
||||||
|
rm /var/lib/wan-ip-guard/current_ip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hoe het werkt
|
||||||
|
|
||||||
|
1. **IP detectie:** Haalt het WAN IP op via `ifconfig.me` met fallback naar `api.ipify.org` en `icanhazip.com`
|
||||||
|
2. **State vergelijking:** Vergelijkt met het laatst opgeslagen IP in de state file
|
||||||
|
3. **DNS update:** Voor elke record: verwijder oud A-record + voeg nieuw A-record toe via DirectAdmin API
|
||||||
|
4. **Notificatie:** Stuurt WhatsApp bericht met oud/nieuw IP en status per record
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
wan-ip-guard/
|
||||||
|
├── index.js # Hoofdscript
|
||||||
|
├── package.json
|
||||||
|
├── .env.example # Config template
|
||||||
|
├── deploy.sh # Deployment script
|
||||||
|
├── systemd/
|
||||||
|
│ ├── wan-ip-guard.service
|
||||||
|
│ └── wan-ip-guard.timer
|
||||||
|
└── docs/
|
||||||
|
└── design-spec.md
|
||||||
|
```
|
||||||
57
deploy.sh
Executable file
57
deploy.sh
Executable file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# WAN IP Guard - Deployment script
|
||||||
|
# Configure the variables below for your setup, then run from your local machine
|
||||||
|
|
||||||
|
REMOTE_USER="your-ssh-user"
|
||||||
|
REMOTE_HOST="your-server"
|
||||||
|
REMOTE_DIR="/home/your-ssh-user/wan-ip-guard"
|
||||||
|
|
||||||
|
echo "=== WAN IP Guard Deployment ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Sync files to server
|
||||||
|
echo "[1/5] Syncing files to ${REMOTE_HOST}..."
|
||||||
|
rsync -avz --delete \
|
||||||
|
--exclude 'node_modules' \
|
||||||
|
--exclude '.git' \
|
||||||
|
--exclude '*.log' \
|
||||||
|
--exclude '.env' \
|
||||||
|
./ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
|
||||||
|
echo " Done."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Install dependencies
|
||||||
|
echo "[2/5] Installing npm dependencies..."
|
||||||
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cd ${REMOTE_DIR} && npm install --omit=dev"
|
||||||
|
echo " Done."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Create state directory
|
||||||
|
echo "[3/5] Creating state directory..."
|
||||||
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "sudo mkdir -p /var/lib/wan-ip-guard && sudo chown ${REMOTE_USER}:${REMOTE_USER} /var/lib/wan-ip-guard"
|
||||||
|
echo " Done."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. Install systemd service + timer
|
||||||
|
echo "[4/5] Installing systemd units..."
|
||||||
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "
|
||||||
|
sudo cp ${REMOTE_DIR}/systemd/wan-ip-guard.service /etc/systemd/system/
|
||||||
|
sudo cp ${REMOTE_DIR}/systemd/wan-ip-guard.timer /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable wan-ip-guard.timer
|
||||||
|
sudo systemctl start wan-ip-guard.timer
|
||||||
|
"
|
||||||
|
echo " Done."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. Verify
|
||||||
|
echo "[5/5] Verifying deployment..."
|
||||||
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl status wan-ip-guard.timer --no-pager"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== Deployment complete! ==="
|
||||||
|
echo "The timer runs every 5 minutes."
|
||||||
|
echo "Check logs: ssh ${REMOTE_USER}@${REMOTE_HOST} journalctl -u wan-ip-guard -f"
|
||||||
|
echo "Run manually: ssh ${REMOTE_USER}@${REMOTE_HOST} cd ${REMOTE_DIR} && node index.js"
|
||||||
203
index.js
Executable file
203
index.js
Executable file
|
|
@ -0,0 +1,203 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
stateFile: process.env.STATE_FILE || '/var/lib/wan-ip-guard/current_ip',
|
||||||
|
daBaseUrl: process.env.DA_BASE_URL,
|
||||||
|
daUsername: process.env.DA_USERNAME,
|
||||||
|
daLoginKey: process.env.DA_LOGIN_KEY,
|
||||||
|
daDomain: process.env.DA_DOMAIN,
|
||||||
|
whatsappApi: process.env.WHATSAPP_API_ENDPOINT || 'http://localhost:3001',
|
||||||
|
whatsappSession: process.env.WHATSAPP_SESSION || 'EdGpt',
|
||||||
|
adminPhone: process.env.ADMIN_PHONE,
|
||||||
|
dnsRecords: (process.env.DNS_RECORDS || '').split(',').map(s => s.trim()).filter(Boolean),
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logError(msg) {
|
||||||
|
console.error(`[${new Date().toISOString()}] ERROR: ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithTimeout(url, options = {}, timeout = 15000) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeout);
|
||||||
|
try {
|
||||||
|
return await fetch(url, { ...options, signal: controller.signal });
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWanIp() {
|
||||||
|
const services = [
|
||||||
|
'https://ifconfig.me',
|
||||||
|
'https://api.ipify.org',
|
||||||
|
'https://icanhazip.com',
|
||||||
|
];
|
||||||
|
for (const url of services) {
|
||||||
|
try {
|
||||||
|
const resp = await fetchWithTimeout(url, {}, 10000);
|
||||||
|
if (resp.ok) {
|
||||||
|
const ip = (await resp.text()).trim();
|
||||||
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
||||||
|
log(`WAN IP obtained from ${url}: ${ip}`);
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(`Could not get IP from ${url}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Could not determine WAN IP from any service');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readState() {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(CONFIG.stateFile, 'utf8').trim();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeState(ip) {
|
||||||
|
const dir = path.dirname(CONFIG.stateFile);
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fs.writeFileSync(CONFIG.stateFile, ip + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function daRequest(endpoint) {
|
||||||
|
const url = `${CONFIG.daBaseUrl}${endpoint}`;
|
||||||
|
const auth = Buffer.from(`${CONFIG.daUsername}:${CONFIG.daLoginKey}`).toString('base64');
|
||||||
|
const resp = await fetchWithTimeout(url, {
|
||||||
|
headers: { Authorization: `Basic ${auth}` },
|
||||||
|
});
|
||||||
|
const text = await resp.text();
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`DirectAdmin API ${resp.status}: ${text}`);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDnsRecords() {
|
||||||
|
const text = await daRequest(`/CMD_API_DNS_CONTROL?domain=${encodeURIComponent(CONFIG.daDomain)}`);
|
||||||
|
const records = {};
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
const match = line.match(/^(.+?)\s+\d+\s+IN\s+A\s+(\d+\.\d+\.\d+\.\d+)$/);
|
||||||
|
if (match) {
|
||||||
|
let name = match[1].trim();
|
||||||
|
if (name === `${CONFIG.daDomain}.` || name === CONFIG.daDomain) {
|
||||||
|
name = '';
|
||||||
|
}
|
||||||
|
records[name] = match[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDnsRecord(name, ip) {
|
||||||
|
const recordValue = `${name}=${ip}`;
|
||||||
|
const endpoint = `/CMD_API_DNS_CONTROL?domain=${encodeURIComponent(CONFIG.daDomain)}&action=select&arecs0=${encodeURIComponent(recordValue)}`;
|
||||||
|
await daRequest(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addDnsRecord(name, ip) {
|
||||||
|
const endpoint = `/CMD_API_DNS_CONTROL?domain=${encodeURIComponent(CONFIG.daDomain)}&action=add&type=A&name=${encodeURIComponent(name)}&value=${encodeURIComponent(ip)}`;
|
||||||
|
await daRequest(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendWhatsApp(message) {
|
||||||
|
const url = `${CONFIG.whatsappApi}/client/sendMessage/${CONFIG.whatsappSession}`;
|
||||||
|
const chatId = `${CONFIG.adminPhone}@c.us`;
|
||||||
|
try {
|
||||||
|
const resp = await fetchWithTimeout(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ chatId, content: message, contentType: 'string' }),
|
||||||
|
}, 10000);
|
||||||
|
if (!resp.ok) {
|
||||||
|
logError(`WhatsApp send failed: ${resp.status}`);
|
||||||
|
} else {
|
||||||
|
log('WhatsApp notification sent.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(`WhatsApp send error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
log('WAN IP Guard check starting.');
|
||||||
|
|
||||||
|
if (!CONFIG.daLoginKey || !CONFIG.daUsername || !CONFIG.daDomain) {
|
||||||
|
throw new Error('Missing required environment variables. Check .env file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIp = await getWanIp();
|
||||||
|
const savedIp = readState();
|
||||||
|
|
||||||
|
if (savedIp === currentIp) {
|
||||||
|
log(`IP unchanged (${currentIp}). Nothing to do.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`IP changed: ${savedIp || '(none)'} -> ${currentIp}`);
|
||||||
|
|
||||||
|
let dnsRecords = {};
|
||||||
|
try {
|
||||||
|
dnsRecords = await getDnsRecords();
|
||||||
|
log(`Current DNS A records: ${JSON.stringify(dnsRecords)}`);
|
||||||
|
} catch (e) {
|
||||||
|
logError(`Could not query DNS records: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const record of CONFIG.dnsRecords) {
|
||||||
|
const name = record === CONFIG.daDomain ? '' : record;
|
||||||
|
const label = name || '(apex)';
|
||||||
|
const oldIp = dnsRecords[name] || dnsRecords[record];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (oldIp) {
|
||||||
|
await deleteDnsRecord(name, oldIp);
|
||||||
|
}
|
||||||
|
await addDnsRecord(name, currentIp);
|
||||||
|
results.push(`✅ ${label}`);
|
||||||
|
log(`Updated ${label}: ${oldIp || '(none)'} -> ${currentIp}`);
|
||||||
|
} catch (e) {
|
||||||
|
results.push(`❌ ${label}: ${e.message}`);
|
||||||
|
logError(`Failed to update ${label}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCount = results.filter(r => r.startsWith('✅')).length;
|
||||||
|
const failCount = results.filter(r => r.startsWith('❌')).length;
|
||||||
|
|
||||||
|
const message = [
|
||||||
|
'🔄 *WAN IP Gewijzigd*',
|
||||||
|
'',
|
||||||
|
`*Oud:* ${savedIp || '(onbekend)'}`,
|
||||||
|
`*Nieuw:* ${currentIp}`,
|
||||||
|
'',
|
||||||
|
'*DNS Updates:*',
|
||||||
|
...results,
|
||||||
|
'',
|
||||||
|
`${successCount} gelukt, ${failCount} mislukt`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
await sendWhatsApp(message);
|
||||||
|
writeState(currentIp);
|
||||||
|
log(`State saved: ${currentIp}`);
|
||||||
|
log('Done.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => {
|
||||||
|
logError(`Fatal: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
30
package-lock.json
generated
Normal file
30
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "wan-ip-guard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "wan-ip-guard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "wan-ip-guard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Monitors WAN IP changes and updates DNS records + sends WhatsApp notification",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
systemd/wan-ip-guard.service
Normal file
12
systemd/wan-ip-guard.service
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[Unit]
|
||||||
|
Description=WAN IP Guard - Dynamic DNS updater
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# Change 'your-user' and paths to match your setup
|
||||||
|
User=your-user
|
||||||
|
EnvironmentFile=/home/your-user/wan-ip-guard/.env
|
||||||
|
WorkingDirectory=/home/your-user/wan-ip-guard
|
||||||
|
ExecStart=/usr/bin/node /home/your-user/wan-ip-guard/index.js
|
||||||
11
systemd/wan-ip-guard.timer
Normal file
11
systemd/wan-ip-guard.timer
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Run WAN IP Guard every 5 minutes
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=1min
|
||||||
|
OnUnitActiveSec=5min
|
||||||
|
AccuracySec=30s
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Loading…
Reference in a new issue