From 3d20330ee36460886a2580ca9e2c9e6f4cec61ca Mon Sep 17 00:00:00 2001 From: hellcat Date: Sun, 18 Oct 2026 19:34:57 +0800 Subject: [PATCH] Bound native validation and pin offline GeoIP dependencies --- app/Services/Freenode/NativeValidator.php | 17 +++++++++++++---- app/Services/Freenode/Pipeline.php | 2 +- config/freenode.php | 1 + tools/freenode/install_validators.py | 13 +++++++++++-- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/Services/Freenode/NativeValidator.php b/app/Services/Freenode/NativeValidator.php index a9137489..4388d9a8 100644 --- a/app/Services/Freenode/NativeValidator.php +++ b/app/Services/Freenode/NativeValidator.php @@ -12,11 +12,19 @@ final class NativeValidator public int $checks = 0; public array $rejected = []; private string $dir; + private float $startedAt; - public function __construct(private string $singbox, private string $mihomo, private ?array $clashTemplate = null) + public function __construct(private string $singbox, private string $mihomo, private ?array $clashTemplate = null, ?string $dataDirectory = null) { + $this->startedAt = microtime(true); + if ($dataDirectory !== null) { + foreach (['Country.mmdb', 'geoip.metadb'] as $file) { + if (!is_readable($dataDirectory.'/'.$file)) throw new RuntimeException('missing_offline_geoip'); + } + } $this->dir = sys_get_temp_dir().'/freenode-check-'.bin2hex(random_bytes(8)); mkdir($this->dir, 0700, true); + if ($dataDirectory !== null) foreach (['Country.mmdb', 'geoip.metadb'] as $file) symlink($dataDirectory.'/'.$file, $this->dir.'/'.$file); } public function __destruct() @@ -29,11 +37,13 @@ final class NativeValidator { $binary = $format === 'singbox' ? $this->singbox : $this->mihomo; if (!is_executable($binary)) throw new RuntimeException('native_validator_not_executable:'.$format); + $remaining = 240 - (microtime(true) - $this->startedAt); + if ($remaining <= 0) throw new RuntimeException('native_validation_batch_timeout'); $file = $this->dir.'/'.bin2hex(random_bytes(6)).($format === 'clash' ? '.yaml' : '.json'); file_put_contents($file, $content); chmod($file, 0600); $command = $format === 'singbox' ? [$binary, 'check', '-c', $file] : [$binary, '-t', '-d', $this->dir, '-f', $file]; $process = new Process($command); - $process->setTimeout(45); + $process->setTimeout(min(45, $remaining)); try { $process->run(); $this->checks++; @@ -65,8 +75,7 @@ final class NativeValidator $chunks = array_chunk($nodes, (int) ceil(count($nodes) / 2), true); $good = []; foreach ($chunks as $chunk) $good += $this->partition($format, $chunk, $render); - // A final whole-file check also catches cross-node constraints. - if (!$this->check($format, $render($good))) throw new RuntimeException('combined_config_invalid:'.$format); + // Builder validates the final complete config once, including cross-node constraints. return $good; } } diff --git a/app/Services/Freenode/Pipeline.php b/app/Services/Freenode/Pipeline.php index 6a71c1cd..219d3d3d 100644 --- a/app/Services/Freenode/Pipeline.php +++ b/app/Services/Freenode/Pipeline.php @@ -41,7 +41,7 @@ final class Pipeline if (!$sources || !$sites) throw new RuntimeException('no_enabled_sources_or_sites'); $sites[] = ['code' => '_admin', 'clients' => ['v2ray', 'clash', 'singbox']]; $clashTemplate = \Symfony\Component\Yaml\Yaml::parseFile(public_path('freenode/template/clash.txt')); - $validator = new NativeValidator(config('freenode.singbox_binary'), config('freenode.mihomo_binary'), $clashTemplate); + $validator = new NativeValidator(config('freenode.singbox_binary'), config('freenode.mihomo_binary'), $clashTemplate, config('freenode.geoip_directory')); $result = (new Builder())->build($sources, $sites, $validator); return self::store()->stage($result, date('Y-m-d'), (float) config('freenode.minimum_ratio', 0.5)); } diff --git a/config/freenode.php b/config/freenode.php index 61214a37..73639b4e 100755 --- a/config/freenode.php +++ b/config/freenode.php @@ -6,6 +6,7 @@ return [ 'minimum_ratio' => (float) env('FREENODE_MINIMUM_RATIO', 0.5), 'singbox_binary' => env('FREENODE_SINGBOX_BINARY', '/opt/master01-freenode/bin/sing-box'), 'mihomo_binary' => env('FREENODE_MIHOMO_BINARY', '/opt/master01-freenode/bin/mihomo'), + 'geoip_directory' => env('FREENODE_GEOIP_DIRECTORY', '/opt/master01-freenode/data'), 'speed_min' => (int) env('freenode_speed_min', 0), 'speed_max' => (int) env('freenode_speed_max', 0), diff --git a/tools/freenode/install_validators.py b/tools/freenode/install_validators.py index 0aab3eaa..a9d1a359 100644 --- a/tools/freenode/install_validators.py +++ b/tools/freenode/install_validators.py @@ -12,6 +12,8 @@ import urllib.request ASSETS = [ ('sing-box', 'https://github.com/SagerNet/sing-box/releases/download/v1.14.3/sing-box-1.14.3-linux-amd64.tar.gz', 'e2bdf179c15a3652955dc44867e33ca0965c9c9b91b34267dcc5a5d639a5feee'), ('mihomo', 'https://github.com/MetaCubeX/mihomo/releases/download/v1.19.32/mihomo-linux-amd64-v1-v1.19.32.gz', '306f81e723e60ce6b828899a6fe83e1d00e9ecefb2dc8d4d849312a5bc00efdc'), + ('Country.mmdb', 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb', '64809d264be87ecfb7ac3ad19b7678893b6275038a7d4bf9eec72464a6c4f9fe'), + ('geoip.metadb', 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb', 'ec6701e428b7efe610fe40b187a69fb03324892a56792b033adc080d2b6c273e'), ] def main(): @@ -20,12 +22,19 @@ def main(): root = Path('/opt/master01-freenode/bin') root.mkdir(mode=0o755, parents=True, exist_ok=True) for name, url, expected in ASSETS: + if name in ('Country.mmdb', 'geoip.metadb'): + root = Path('/opt/master01-freenode/data') + root.mkdir(mode=0o755, parents=True, exist_ok=True) + else: + root = Path('/opt/master01-freenode/bin') request = urllib.request.Request(url, headers={'User-Agent': 'master01-freenode-validator'}) with urllib.request.urlopen(request, timeout=60) as response: raw = response.read() if hashlib.sha256(raw).hexdigest() != expected: raise RuntimeError('Official archive checksum mismatch: ' + name) - if name == 'sing-box': + if name in ('Country.mmdb', 'geoip.metadb'): + binary = raw + elif name == 'sing-box': with tarfile.open(fileobj=io.BytesIO(raw), mode='r:gz') as archive: member = next(m for m in archive.getmembers() if m.isfile() and m.name.endswith('/sing-box')) binary = archive.extractfile(member).read() @@ -33,7 +42,7 @@ def main(): binary = gzip.decompress(raw) temp = root / ('.' + name + '.tmp') temp.write_bytes(binary) - temp.chmod(0o755) + temp.chmod(0o644 if name in ('Country.mmdb', 'geoip.metadb') else 0o755) os.replace(temp, root / name) print(name + ': archive verified, validator installed', flush=True)