mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 14:03:56 +08:00
158 lines
5.6 KiB
Ucode
158 lines
5.6 KiB
Ucode
#!/usr/bin/ucode
|
|
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* runtime/domain_ruleset.uc <source .txt> <destination .json>
|
|
*
|
|
* The domain-list counterpart of china_ip_ruleset.uc: turn a bare host-name
|
|
* list into a sing-box source rule-set of `domain_suffix` rules, so the DNS
|
|
* side of the China split reads a local file instead of downloading
|
|
* geosite-geolocation-cn.srs from GitHub on a schedule.
|
|
*
|
|
* Why this exists, in the terms that decided it: the two consumers of the
|
|
* China split - the nft mainland set and the sing-box rule-set - were reading
|
|
* two different files. The kernel's came from china_ip4.txt, the resolver's
|
|
* from a remote .srs, and the remote copy is what a cold start has to wait for
|
|
* (through the node, when download_detour says so) before the inbounds bind.
|
|
* With the local copy there is nothing to wait for and nothing to keep in step.
|
|
*
|
|
* The entry is a suffix, not an exact domain: china_list.txt holds registrable
|
|
* domains ("163.com", "taobao.com"), and the DNS rule has to match every name
|
|
* under them, which is what `domain_suffix` means to sing-box. `domain` would
|
|
* match only the name itself and quietly stop matching www.<domain> for the
|
|
* entire list.
|
|
*
|
|
* One rule holding the whole list, for the same reason china_ip_ruleset.uc
|
|
* gives: sing-box matches rule-sets by walking a prefix-compressed structure,
|
|
* and splitting the list would only add rules to walk per query.
|
|
*
|
|
* Entries are validated here rather than trusted. The strict parser on the
|
|
* other side is the authority for what a valid name is, and one bad entry
|
|
* makes it reject the *whole* rule-set - which would take the client's start
|
|
* with it. A malformed line is skipped and reported, exactly like the blank
|
|
* lines and comments a list may carry.
|
|
*/
|
|
'use strict';
|
|
import { readfile, writefile, rename, unlink } from 'fs';
|
|
|
|
if (length(ARGV) < 2 || ARGV[0] == null || ARGV[1] == null) {
|
|
warn(sprintf('usage: domain_ruleset.uc <source .txt> <destination .json>\n'));
|
|
exit(1);
|
|
}
|
|
const src = ARGV[0];
|
|
const dst = ARGV[1];
|
|
const data = readfile(src);
|
|
if (type(data) !== 'string') {
|
|
warn(sprintf('cannot read %s\n', src));
|
|
exit(1);
|
|
}
|
|
|
|
const suffixes = [];
|
|
let skipped = 0;
|
|
|
|
/* A structural hostname check, in process.
|
|
*
|
|
* validation() from homeproxy-pro.uc is the obvious tool and the wrong one here:
|
|
* it shells out to /sbin/validate_data once per call, and this list is 111k
|
|
* lines. That is 111k process spawns. china_ip_ruleset.uc makes the same call
|
|
* for the same reason - its is_v4/is_v6 are hand-written rather than delegated.
|
|
*
|
|
* Deliberately structural, not exhaustive: the goal is to keep a line that
|
|
* cannot be a domain out of the file, since one bad entry makes sing-box reject
|
|
* the whole rule-set and take the client's start with it. The strict parser on
|
|
* the other side stays the authority for what is really valid. */
|
|
function is_hostname(entry) {
|
|
if (length(entry) < 1 || length(entry) > 253)
|
|
return false;
|
|
|
|
/* Anything outside this set is a path, a URL, a port, an email, or a
|
|
* comment marker that survived the updater's filter. */
|
|
if (!match(entry, /^[A-Za-z0-9.-]+$/))
|
|
return false;
|
|
|
|
/* A leading dot, a doubled dot, or a trailing dot all mean the list is
|
|
* not in the bare form this emits; sing-box treats them differently from
|
|
* what the DNS rule expects to match. */
|
|
if (substr(entry, 0, 1) == '.' || substr(entry, -1, 1) == '.')
|
|
return false;
|
|
if (match(entry, /\.\./))
|
|
return false;
|
|
|
|
for (let label in split(entry, '.')) {
|
|
if (length(label) < 1 || length(label) > 63)
|
|
return false;
|
|
/* A label may not start or end with a hyphen (RFC 1035). */
|
|
if (substr(label, 0, 1) == '-' || substr(label, -1, 1) == '-')
|
|
return false;
|
|
}
|
|
|
|
/* An all-numeric set of labels is a bare IPv4 address, not a domain:
|
|
* the structure above accepts "192.168.1.1" quite happily, and a
|
|
* domain_suffix rule holding an address never matches a query. No TLD
|
|
* is all digits, so this cannot cost a real name. IPv6 never reaches
|
|
* here at all - it carries colons and was rejected above. */
|
|
let all_numeric = true;
|
|
for (let label in split(entry, '.')) {
|
|
if (!match(label, /^[0-9]+$/)) {
|
|
all_numeric = false;
|
|
break;
|
|
}
|
|
}
|
|
if (all_numeric)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
for (let line in split(data, /\n/)) {
|
|
const entry = trim(line);
|
|
if (entry == '')
|
|
continue;
|
|
if (match(entry, /^[#;]/))
|
|
continue;
|
|
|
|
/* The updater already strips `full:` prefixes and anything with a colon
|
|
* (update_resources.sh's china_list case), so what arrives here is a bare
|
|
* name. Anything that is not still a hostname is dropped rather than
|
|
* passed on. */
|
|
if (!is_hostname(entry)) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
push(suffixes, entry);
|
|
}
|
|
|
|
if (skipped > 0)
|
|
warn(sprintf('skipped %d malformed entries in %s\n', skipped, src));
|
|
|
|
/* An empty rule-set is worse than no rule-set: it matches nothing, so every
|
|
* name the list should have covered falls through to whatever the DNS side
|
|
* defaults to. Refuse rather than install one - the previous file stays. */
|
|
if (length(suffixes) == 0) {
|
|
warn(sprintf('no usable domain entries in %s\n', src));
|
|
exit(1);
|
|
}
|
|
|
|
/* The temporary name is a sibling so the rename stays on one filesystem: the
|
|
* watcher must never read a half-written file. `%.J` is the same serializer
|
|
* the config generator uses, so the output is the canonical sing-box form. */
|
|
const rendered = sprintf('%.J\n', {
|
|
version: 3,
|
|
rules: [
|
|
{ domain_suffix: suffixes }
|
|
]
|
|
});
|
|
|
|
const tmp = dst + '.tmp';
|
|
if (!writefile(tmp, rendered)) {
|
|
warn(sprintf('cannot write %s\n', tmp));
|
|
exit(1);
|
|
}
|
|
if (!rename(tmp, dst)) {
|
|
unlink(tmp);
|
|
warn(sprintf('cannot install %s\n', dst));
|
|
exit(1);
|
|
}
|
|
|
|
printf('%d domain suffixes\n', length(suffixes));
|