ops: complete subscription template and node activation workflow
This commit is contained in:
parent
1537ebf65c
commit
12d9f2b47c
20
scripts/node-onboarding/apply-template.php
Normal file
20
scripts/node-onboarding/apply-template.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
require getcwd() . '/vendor/autoload.php';
|
||||
$app = require getcwd() . '/bootstrap/app.php';
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$opts = getopt('', ['backup:']);
|
||||
$dir = $opts['backup'] ?? throw new RuntimeException('Missing backup');
|
||||
umask(0077);
|
||||
$old = App\Models\SubscribeTemplate::where('name', 'singbox')->firstOrFail();
|
||||
$default = json_decode(file_get_contents('resources/rules/default.sing-box.json'), true, flags: JSON_THROW_ON_ERROR);
|
||||
$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');
|
||||
if (!is_dir($dir)) throw new RuntimeException('Backup directory missing');
|
||||
$path = "$dir/singbox-template-before.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);
|
||||
echo "DPL default subscription template updated; custom templates and other airports untouched\n";
|
||||
@ -5,7 +5,7 @@ cd "$(dirname "$0")"
|
||||
test -s /etc/dpl-letsencrypt/live/c2.deeplink-subscribe.com/fullchain.pem
|
||||
docker image inspect sha256:d8807a5b2442bc25ce4fbc1312354b9dcbb0f538d75181ef735f4ea980760e70 >/dev/null
|
||||
docker image inspect sha256:31ff8ea3e8f958a835edbd1579709017da75b742ff3a5984ea59d12ea0775577 >/dev/null
|
||||
install -d -m 700 /var/lib/dpl-sgw
|
||||
install -d -m 755 /var/lib/dpl-sgw
|
||||
install -d -m 755 /var/lib/dpl-sgw/acme /var/lib/dpl-sgw/ssl
|
||||
ln -sfn /etc/dpl-letsencrypt/live/c2.deeplink-subscribe.com/fullchain.pem /var/lib/dpl-sgw/ssl/cert.pem
|
||||
ln -sfn /etc/dpl-letsencrypt/live/c2.deeplink-subscribe.com/privkey.pem /var/lib/dpl-sgw/ssl/key.pem
|
||||
|
||||
43
scripts/node-onboarding/promote.php
Normal file
43
scripts/node-onboarding/promote.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
// Run only after real relay, subscription, certificate and traffic checks pass.
|
||||
require getcwd() . '/vendor/autoload.php';
|
||||
$app = require getcwd() . '/bootstrap/app.php';
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use App\Models\Plan;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerGroup;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
$opts = getopt('', ['backup:', 'plans:', 'group:']);
|
||||
$dir = $opts['backup'] ?? throw new RuntimeException('Missing backup');
|
||||
$ids = array_map('intval', explode(',', $opts['plans'] ?? ''));
|
||||
if (!$ids || in_array(0, $ids, true)) throw new RuntimeException('Explicit plan IDs required');
|
||||
$m = json_decode(file_get_contents("$dir/manifest.json"), true, flags: JSON_THROW_ON_ERROR);
|
||||
$groupId = (int)($opts['group'] ?? $m['group_id']);
|
||||
$node = Server::findOrFail($m['node_id']);
|
||||
$qa = User::findOrFail($m['test_user_id']);
|
||||
if (!$node->is_online || $qa->u + $qa->d <= 0) throw new RuntimeException('Online and traffic evidence missing');
|
||||
if (is_file("$dir/promoted.json")) { echo "Already promoted; verify current state separately\n"; exit; }
|
||||
$plans = Plan::whereIn('id', $ids)->get();
|
||||
if ($plans->count() !== count($ids)) throw new RuntimeException('Plan list mismatch');
|
||||
foreach ($plans as $p) if ($p->group_id !== null && $p->group_id !== $groupId) throw new RuntimeException('Plan already assigned elsewhere');
|
||||
$users = User::whereIn('plan_id', $ids)->whereNull('group_id')->get(['id','group_id','updated_at']);
|
||||
umask(0077);
|
||||
file_put_contents("$dir/before-promotion.json", json_encode([
|
||||
'plans' => $plans->map->getAttributes(), 'user_groups' => $users,
|
||||
'node_groups' => $node->group_ids, 'group' => ServerGroup::findOrFail($groupId)->getAttributes(),
|
||||
'subscribe_url' => admin_setting('subscribe_url'), 'qa_banned' => $qa->banned,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
DB::transaction(function () use ($ids, $users, $node, $qa, $groupId) {
|
||||
$group = ServerGroup::findOrFail($groupId);
|
||||
if (str_starts_with($group->name, 'dpl 接入验收 ')) { $group->name = '基础节点'; $group->save(); }
|
||||
Plan::whereIn('id', $ids)->whereNull('group_id')->update(['group_id' => $groupId]);
|
||||
User::whereIn('id', $users->pluck('id'))->whereNull('group_id')->update(['group_id' => $groupId]);
|
||||
$node->group_ids = [(string)$groupId]; $node->save();
|
||||
$qa->banned = 1; $qa->saveQuietly();
|
||||
admin_setting(['subscribe_url' => 'https://c2.deeplink-subscribe.com']);
|
||||
});
|
||||
$result = ['node_id' => $node->id, 'group_id' => $groupId, 'plan_ids' => $ids,
|
||||
'existing_users_assigned' => $users->count(), 'qa_disabled' => true];
|
||||
file_put_contents("$dir/promoted.json", json_encode($result, JSON_THROW_ON_ERROR));
|
||||
echo json_encode($result) . PHP_EOL;
|
||||
16
scripts/node-onboarding/readiness.php
Normal file
16
scripts/node-onboarding/readiness.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require getcwd() . '/vendor/autoload.php';
|
||||
$app = require getcwd() . '/bootstrap/app.php';
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$opts = getopt('', ['backup:']);
|
||||
$dir = $opts['backup'] ?? throw new RuntimeException('Missing backup');
|
||||
$m = json_decode(file_get_contents("$dir/manifest.json"), true, flags: JSON_THROW_ON_ERROR);
|
||||
$node = App\Models\Server::findOrFail($m['node_id']);
|
||||
$qa = App\Models\User::findOrFail($m['test_user_id']);
|
||||
echo json_encode([
|
||||
'node_id' => $node->id, 'name' => $node->name, 'is_online' => $node->is_online,
|
||||
'last_check_at' => $node->last_check_at, 'last_push_at' => $node->last_push_at,
|
||||
'qa_upload' => $qa->u, 'qa_download' => $qa->d, 'qa_banned' => $qa->banned,
|
||||
'plans' => App\Models\Plan::orderBy('id')->get(['id', 'group_id']),
|
||||
'existing_users_with_selected_plan_and_null_group' => App\Models\User::whereIn('plan_id', [1,6,7,8])->whereNull('group_id')->count(),
|
||||
], JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
141
scripts/node-onboarding/sing-box-template.json
Normal file
141
scripts/node-onboarding/sing-box-template.json
Normal file
@ -0,0 +1,141 @@
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"clash_mode": "global",
|
||||
"server": "remote"
|
||||
},
|
||||
{
|
||||
"clash_mode": "direct",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"rule_set": [
|
||||
"geosite-cn"
|
||||
],
|
||||
"server": "local"
|
||||
}
|
||||
],
|
||||
"servers": [
|
||||
{
|
||||
"type": "https",
|
||||
"server": "1.1.1.1",
|
||||
"detour": "节点选择",
|
||||
"tag": "remote"
|
||||
},
|
||||
{
|
||||
"type": "https",
|
||||
"server": "223.5.5.5",
|
||||
"detour": "direct",
|
||||
"tag": "local"
|
||||
}
|
||||
],
|
||||
"strategy": "prefer_ipv4"
|
||||
},
|
||||
"experimental": {
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"path": "cache.db",
|
||||
"cache_id": "cache_db",
|
||||
"store_fakeip": true
|
||||
}
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"auto_route": true,
|
||||
"address": [
|
||||
"172.19.0.1/30",
|
||||
"2001:0470:f9da:fdfa::1/64"
|
||||
],
|
||||
"mtu": 9000,
|
||||
"stack": "system",
|
||||
"strict_route": true,
|
||||
"type": "tun"
|
||||
},
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 2333,
|
||||
"tag": "socks-in",
|
||||
"type": "socks",
|
||||
"users": []
|
||||
},
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 2334,
|
||||
"tag": "mixed-in",
|
||||
"type": "mixed",
|
||||
"users": []
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "节点选择",
|
||||
"type": "selector",
|
||||
"default": "自动选择",
|
||||
"outbounds": [
|
||||
"自动选择"
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "direct",
|
||||
"type": "direct"
|
||||
},
|
||||
{
|
||||
"tag": "自动选择",
|
||||
"type": "urltest",
|
||||
"outbounds": []
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"auto_detect_interface": true,
|
||||
"rules": [
|
||||
{
|
||||
"action": "resolve",
|
||||
"strategy": "prefer_ipv4"
|
||||
},
|
||||
{
|
||||
"action": "sniff"
|
||||
},
|
||||
{
|
||||
"protocol": "dns",
|
||||
"action": "hijack-dns"
|
||||
},
|
||||
{
|
||||
"clash_mode": "direct",
|
||||
"outbound": "direct"
|
||||
},
|
||||
{
|
||||
"clash_mode": "global",
|
||||
"outbound": "节点选择"
|
||||
},
|
||||
{
|
||||
"ip_is_private": true,
|
||||
"outbound": "direct"
|
||||
},
|
||||
{
|
||||
"rule_set": [
|
||||
"geosite-cn",
|
||||
"geoip-cn"
|
||||
],
|
||||
"outbound": "direct"
|
||||
}
|
||||
],
|
||||
"rule_set": [
|
||||
{
|
||||
"tag": "geosite-cn",
|
||||
"type": "remote",
|
||||
"format": "binary",
|
||||
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
|
||||
"download_detour": "自动选择"
|
||||
},
|
||||
{
|
||||
"tag": "geoip-cn",
|
||||
"type": "remote",
|
||||
"format": "binary",
|
||||
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
|
||||
"download_detour": "自动选择"
|
||||
}
|
||||
],
|
||||
"default_domain_resolver": "local"
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ p.add_argument('--qa', required=True)
|
||||
p.add_argument('--binary', required=True)
|
||||
p.add_argument('--expected-exit', required=True)
|
||||
p.add_argument('--port', type=int, default=19080)
|
||||
p.add_argument('--subscription')
|
||||
a = p.parse_args()
|
||||
q = json.loads(pathlib.Path(a.qa).read_text())
|
||||
c = {'log': {'level': 'error'}, 'dns': {'servers': [{'type': 'local', 'tag': 'local'}]},
|
||||
@ -21,13 +22,21 @@ c = {'log': {'level': 'error'}, 'dns': {'servers': [{'type': 'local', 'tag': 'lo
|
||||
'outbounds': [{'type': 'anytls', 'tag': 'qa', 'server': q['host'], 'server_port': q['port'],
|
||||
'password': q['uuid'], 'tls': {'enabled': True, 'server_name': q['sni'], 'insecure': False}}],
|
||||
'route': {'default_domain_resolver': 'local', 'final': 'qa'}}
|
||||
if a.subscription:
|
||||
c = json.loads(pathlib.Path(a.subscription).read_text())
|
||||
nodes = [x for x in c['outbounds'] if x.get('type') == 'anytls']
|
||||
assert len(nodes) == 1 and nodes[0]['tls'].get('insecure') is False
|
||||
assert nodes[0]['server'] == q['host'] and nodes[0]['server_port'] == q['port']
|
||||
assert nodes[0]['password'] == q['uuid']
|
||||
c['inbounds'] = [{'type': 'mixed', 'listen': '127.0.0.1', 'listen_port': a.port}]
|
||||
c['log'] = {'level': 'error'}
|
||||
os.umask(0o077)
|
||||
with tempfile.TemporaryDirectory(prefix='dpl-client-') as d:
|
||||
config = pathlib.Path(d) / 'client.json'
|
||||
config.write_text(json.dumps(c))
|
||||
env = dict(os.environ, GOMEMLIMIT='64MiB', GOGC='75')
|
||||
with (pathlib.Path(d) / 'client.log').open('wb') as log:
|
||||
proc = subprocess.Popen([a.binary, 'run', '-c', str(config)], env=env, stdout=log, stderr=log)
|
||||
proc = subprocess.Popen([a.binary, 'run', '-c', str(config)], cwd=d, env=env, stdout=log, stderr=log)
|
||||
try:
|
||||
for _ in range(50):
|
||||
assert proc.poll() is None, 'Client exited during startup'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user