Bound native validation and pin offline GeoIP dependencies

This commit is contained in:
hellcat 2026-10-18 19:34:57 +08:00
parent 99007b4518
commit 3d20330ee3
4 changed files with 26 additions and 7 deletions

View File

@ -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;
}
}

View File

@ -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));
}

View File

@ -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),

View File

@ -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)