Bound native validation and pin offline GeoIP dependencies
This commit is contained in:
parent
99007b4518
commit
3d20330ee3
@ -12,11 +12,19 @@ final class NativeValidator
|
|||||||
public int $checks = 0;
|
public int $checks = 0;
|
||||||
public array $rejected = [];
|
public array $rejected = [];
|
||||||
private string $dir;
|
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));
|
$this->dir = sys_get_temp_dir().'/freenode-check-'.bin2hex(random_bytes(8));
|
||||||
mkdir($this->dir, 0700, true);
|
mkdir($this->dir, 0700, true);
|
||||||
|
if ($dataDirectory !== null) foreach (['Country.mmdb', 'geoip.metadb'] as $file) symlink($dataDirectory.'/'.$file, $this->dir.'/'.$file);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
@ -29,11 +37,13 @@ final class NativeValidator
|
|||||||
{
|
{
|
||||||
$binary = $format === 'singbox' ? $this->singbox : $this->mihomo;
|
$binary = $format === 'singbox' ? $this->singbox : $this->mihomo;
|
||||||
if (!is_executable($binary)) throw new RuntimeException('native_validator_not_executable:'.$format);
|
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 = $this->dir.'/'.bin2hex(random_bytes(6)).($format === 'clash' ? '.yaml' : '.json');
|
||||||
file_put_contents($file, $content); chmod($file, 0600);
|
file_put_contents($file, $content); chmod($file, 0600);
|
||||||
$command = $format === 'singbox' ? [$binary, 'check', '-c', $file] : [$binary, '-t', '-d', $this->dir, '-f', $file];
|
$command = $format === 'singbox' ? [$binary, 'check', '-c', $file] : [$binary, '-t', '-d', $this->dir, '-f', $file];
|
||||||
$process = new Process($command);
|
$process = new Process($command);
|
||||||
$process->setTimeout(45);
|
$process->setTimeout(min(45, $remaining));
|
||||||
try {
|
try {
|
||||||
$process->run();
|
$process->run();
|
||||||
$this->checks++;
|
$this->checks++;
|
||||||
@ -65,8 +75,7 @@ final class NativeValidator
|
|||||||
$chunks = array_chunk($nodes, (int) ceil(count($nodes) / 2), true);
|
$chunks = array_chunk($nodes, (int) ceil(count($nodes) / 2), true);
|
||||||
$good = [];
|
$good = [];
|
||||||
foreach ($chunks as $chunk) $good += $this->partition($format, $chunk, $render);
|
foreach ($chunks as $chunk) $good += $this->partition($format, $chunk, $render);
|
||||||
// A final whole-file check also catches cross-node constraints.
|
// Builder validates the final complete config once, including cross-node constraints.
|
||||||
if (!$this->check($format, $render($good))) throw new RuntimeException('combined_config_invalid:'.$format);
|
|
||||||
return $good;
|
return $good;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ final class Pipeline
|
|||||||
if (!$sources || !$sites) throw new RuntimeException('no_enabled_sources_or_sites');
|
if (!$sources || !$sites) throw new RuntimeException('no_enabled_sources_or_sites');
|
||||||
$sites[] = ['code' => '_admin', 'clients' => ['v2ray', 'clash', 'singbox']];
|
$sites[] = ['code' => '_admin', 'clients' => ['v2ray', 'clash', 'singbox']];
|
||||||
$clashTemplate = \Symfony\Component\Yaml\Yaml::parseFile(public_path('freenode/template/clash.txt'));
|
$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);
|
$result = (new Builder())->build($sources, $sites, $validator);
|
||||||
return self::store()->stage($result, date('Y-m-d'), (float) config('freenode.minimum_ratio', 0.5));
|
return self::store()->stage($result, date('Y-m-d'), (float) config('freenode.minimum_ratio', 0.5));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ return [
|
|||||||
'minimum_ratio' => (float) env('FREENODE_MINIMUM_RATIO', 0.5),
|
'minimum_ratio' => (float) env('FREENODE_MINIMUM_RATIO', 0.5),
|
||||||
'singbox_binary' => env('FREENODE_SINGBOX_BINARY', '/opt/master01-freenode/bin/sing-box'),
|
'singbox_binary' => env('FREENODE_SINGBOX_BINARY', '/opt/master01-freenode/bin/sing-box'),
|
||||||
'mihomo_binary' => env('FREENODE_MIHOMO_BINARY', '/opt/master01-freenode/bin/mihomo'),
|
'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_min' => (int) env('freenode_speed_min', 0),
|
||||||
'speed_max' => (int) env('freenode_speed_max', 0),
|
'speed_max' => (int) env('freenode_speed_max', 0),
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import urllib.request
|
|||||||
ASSETS = [
|
ASSETS = [
|
||||||
('sing-box', 'https://github.com/SagerNet/sing-box/releases/download/v1.14.3/sing-box-1.14.3-linux-amd64.tar.gz', 'e2bdf179c15a3652955dc44867e33ca0965c9c9b91b34267dcc5a5d639a5feee'),
|
('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'),
|
('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():
|
def main():
|
||||||
@ -20,12 +22,19 @@ def main():
|
|||||||
root = Path('/opt/master01-freenode/bin')
|
root = Path('/opt/master01-freenode/bin')
|
||||||
root.mkdir(mode=0o755, parents=True, exist_ok=True)
|
root.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||||
for name, url, expected in ASSETS:
|
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'})
|
request = urllib.request.Request(url, headers={'User-Agent': 'master01-freenode-validator'})
|
||||||
with urllib.request.urlopen(request, timeout=60) as response:
|
with urllib.request.urlopen(request, timeout=60) as response:
|
||||||
raw = response.read()
|
raw = response.read()
|
||||||
if hashlib.sha256(raw).hexdigest() != expected:
|
if hashlib.sha256(raw).hexdigest() != expected:
|
||||||
raise RuntimeError('Official archive checksum mismatch: ' + name)
|
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:
|
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'))
|
member = next(m for m in archive.getmembers() if m.isfile() and m.name.endswith('/sing-box'))
|
||||||
binary = archive.extractfile(member).read()
|
binary = archive.extractfile(member).read()
|
||||||
@ -33,7 +42,7 @@ def main():
|
|||||||
binary = gzip.decompress(raw)
|
binary = gzip.decompress(raw)
|
||||||
temp = root / ('.' + name + '.tmp')
|
temp = root / ('.' + name + '.tmp')
|
||||||
temp.write_bytes(binary)
|
temp.write_bytes(binary)
|
||||||
temp.chmod(0o755)
|
temp.chmod(0o644 if name in ('Country.mmdb', 'geoip.metadb') else 0o755)
|
||||||
os.replace(temp, root / name)
|
os.replace(temp, root / name)
|
||||||
print(name + ': archive verified, validator installed', flush=True)
|
print(name + ': archive verified, validator installed', flush=True)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user