diff --git a/scripts/node-onboarding/apply-template.php b/scripts/node-onboarding/apply-template.php index e7478b0..79a8a8b 100644 --- a/scripts/node-onboarding/apply-template.php +++ b/scripts/node-onboarding/apply-template.php @@ -2,7 +2,7 @@ require getcwd() . '/vendor/autoload.php'; $app = require getcwd() . '/bootstrap/app.php'; $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); -$opts = getopt('', ['backup:']); +$opts = getopt('', ['backup:', 'expected-current-sha256:']); $dir = $opts['backup'] ?? throw new RuntimeException('Missing backup'); umask(0077); $old = App\Models\SubscribeTemplate::where('name', 'singbox')->firstOrFail(); @@ -11,9 +11,11 @@ $replacement = file_get_contents(__DIR__ . '/sing-box-template.json'); $new = json_decode($replacement, true, flags: JSON_THROW_ON_ERROR); $current = json_decode($old->content, true, flags: JSON_THROW_ON_ERROR); if ($current === $new) { echo "Template already updated\n"; exit; } -if ($current !== $default) throw new RuntimeException('Custom template detected; inspect instead of overwriting'); +$expected = $opts['expected-current-sha256'] ?? ''; +$knownRevision = preg_match('/^[a-f0-9]{64}$/', $expected) && hash_equals($expected, hash('sha256', $old->content)); +if ($current !== $default && !$knownRevision) throw new RuntimeException('Custom or unexpected template detected; inspect instead of overwriting'); if (!is_dir($dir)) throw new RuntimeException('Backup directory missing'); -$path = "$dir/singbox-template-before.json"; +$path = "$dir/singbox-template-before-" . hash('sha256', $old->content) . '.json'; if (is_file($path)) throw new RuntimeException('Existing backup; inspect before retry'); file_put_contents($path, json_encode($old->getAttributes(), JSON_THROW_ON_ERROR)); App\Models\SubscribeTemplate::setContent('singbox', $replacement); diff --git a/scripts/node-onboarding/install-node.sh b/scripts/node-onboarding/install-node.sh index 0219848..3aa3ace 100644 --- a/scripts/node-onboarding/install-node.sh +++ b/scripts/node-onboarding/install-node.sh @@ -2,11 +2,27 @@ set -euo pipefail # Credential files are installed separately with mode 0600. [[ $(id -u) == 0 ]] || exit 1 +listen_port='' +tls_domain='' +for arg in "$@"; do + case "$arg" in + --listen-port=*) listen_port=${arg#*=} ;; + --tls-domain=*) tls_domain=${arg#*=} ;; + *) echo 'Usage: install-node.sh --listen-port=24443 --tls-domain=us01.example.com'; exit 1 ;; + esac +done +[[ $listen_port =~ ^[0-9]+$ && ${#listen_port} -le 5 ]] && (( 10#$listen_port >= 1024 && 10#$listen_port <= 65535 )) || { echo 'Explicit valid listen port required'; exit 1; } +[[ $tls_domain =~ ^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$ ]] || { echo 'Explicit TLS domain required'; exit 1; } [[ -s /etc/dpl-node/config.yml && -s /etc/dpl-node/secret.env ]] || { echo 'Private node configuration missing'; exit 1; } -if ss -lntH '( sport = :24443 )' | grep -q .; then - echo 'Port 24443 already in use; inspect before deployment'; exit 1 +[[ ! -e /etc/systemd/system/dpl-node.service && ! -e /opt/dpl-node/xboard-node ]] || { echo 'DPL backend already installed; use a separately reviewed Git update'; exit 1; } +if ss -lntH "( sport = :$listen_port )" | grep -q .; then + echo "Port $listen_port already in use; inspect before deployment"; exit 1 fi -systemctl is-active --quiet XrayR +if systemctl list-unit-files XrayR.service --no-legend | grep -q '^XrayR.service'; then + systemctl is-active --quiet XrayR || { echo 'Existing XrayR is not active; investigate before installation'; exit 1; } +fi +printf '%s\n' "$tls_domain" > /etc/dpl-node/certificate-domain +chmod 644 /etc/dpl-node/certificate-domain install -d -m 755 /opt/dpl-node install -d -m 700 /var/lib/dpl-node /var/lib/dpl-node/kernel curl --fail --location --silent --show-error \ diff --git a/scripts/node-onboarding/renew-node.sh b/scripts/node-onboarding/renew-node.sh index b163a2e..5dc67ce 100644 --- a/scripts/node-onboarding/renew-node.sh +++ b/scripts/node-onboarding/renew-node.sh @@ -1,5 +1,7 @@ #!/bin/sh set -eu # Copy under renewal-hooks/deploy only after auditing all existing hooks. -[ "${RENEWED_LINEAGE:-}" = /etc/letsencrypt/live/us01.deeplink-subscribe.com ] || exit 0 +[ -s /etc/dpl-node/certificate-domain ] || exit 0 +domain=$(cat /etc/dpl-node/certificate-domain) +[ "${RENEWED_LINEAGE:-}" = "/etc/letsencrypt/live/$domain" ] || exit 0 systemctl try-restart dpl-node diff --git a/scripts/node-onboarding/sing-box-template.json b/scripts/node-onboarding/sing-box-template.json index 66df858..cb1d16f 100644 --- a/scripts/node-onboarding/sing-box-template.json +++ b/scripts/node-onboarding/sing-box-template.json @@ -26,7 +26,6 @@ { "type": "https", "server": "223.5.5.5", - "detour": "direct", "tag": "local" } ], diff --git a/scripts/node-onboarding/update-cloudflare-ips.py b/scripts/node-onboarding/update-cloudflare-ips.py index 194fd55..f952490 100644 --- a/scripts/node-onboarding/update-cloudflare-ips.py +++ b/scripts/node-onboarding/update-cloudflare-ips.py @@ -15,7 +15,7 @@ for version in (4, 6): assert all(n.version == version and n.prefixlen >= (8 if version == 4 else 16) for n in nets), "Invalid trust range" ranges.extend(str(n) for n in nets) assert len(ranges) == len(set(ranges)), "Duplicate ranges" -data = ("# DPL only: synchronized from Cloudflare official lists.\n" + +data = ("# Subscription vhosts: synchronized from Cloudflare official lists.\n" + "".join(f"set_real_ip_from {n};\n" for n in ranges) + "real_ip_header CF-Connecting-IP;\nreal_ip_recursive off;\n").encode() old = target.read_bytes() if target.exists() else None diff --git a/scripts/node-onboarding/verify-node.py b/scripts/node-onboarding/verify-node.py index 832314f..0c99d86 100644 --- a/scripts/node-onboarding/verify-node.py +++ b/scripts/node-onboarding/verify-node.py @@ -4,6 +4,7 @@ import argparse import json import os import pathlib +import shutil import socket import subprocess import tempfile @@ -55,6 +56,13 @@ with tempfile.TemporaryDirectory(prefix='dpl-client-') as d: assert trace.get('ip') == a.expected_exit, 'Second exit check mismatch' print(json.dumps({'anytls': True, 'relay': q['host'], 'port': q['port'], 'strict_tls': True, 'exit_ip': ip, 'country': trace.get('loc'), 'https_tls': trace.get('tls')}, ensure_ascii=False)) + except Exception: + log.flush() + failure = pathlib.Path(a.qa).parent / 'client-failure.log' + shutil.copyfile(pathlib.Path(d) / 'client.log', failure) + failure.chmod(0o600) + print(f'Client failed; protected diagnostic log: {failure}') + raise finally: proc.terminate() try: