feat: поддержка AmneziaWG 3.1, keepalive ranges, безопасный ruleset HTTP client и Source-Aware DNS
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fe-app-tachyon",
|
||||
"version": "1.3.22",
|
||||
"version": "1.3.23",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -43,8 +43,13 @@ export function formatServiceStatusMessage(msg: string): string {
|
||||
return `${_('External NFQUEUE rules overlap with Tachyon')} ${provider} (${range})`;
|
||||
}
|
||||
|
||||
if (msg === 'legacy zapret runtime paths are still present and should be migrated') {
|
||||
return _('Legacy zapret runtime paths are still present and should be migrated');
|
||||
if (
|
||||
msg ===
|
||||
'legacy zapret runtime paths are still present and should be migrated'
|
||||
) {
|
||||
return _(
|
||||
'Legacy zapret runtime paths are still present and should be migrated',
|
||||
);
|
||||
}
|
||||
|
||||
const unexpectedProcessesMatch = msg.match(
|
||||
@@ -56,7 +61,9 @@ export function formatServiceStatusMessage(msg: string): string {
|
||||
}
|
||||
|
||||
if (msg.startsWith('standalone ')) {
|
||||
return _('Standalone service is active alongside Tachyon; policy or port conflicts are possible');
|
||||
return _(
|
||||
'Standalone service is active alongside Tachyon; policy or port conflicts are possible',
|
||||
);
|
||||
}
|
||||
|
||||
const packageInstalledNoBinMatch = msg.match(
|
||||
@@ -76,17 +83,33 @@ export function formatServiceStatusMessage(msg: string): string {
|
||||
}
|
||||
|
||||
if (msg.includes('ciadpi has restarted after exiting')) {
|
||||
return _('ByeDPI restarted after exiting; strategy or traffic load may be unstable');
|
||||
return _(
|
||||
'ByeDPI restarted after exiting; strategy or traffic load may be unstable',
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.includes('native Tailscale is configured, but the tailscale package is missing')) {
|
||||
return _('Native Tailscale is configured, but tailscale package is missing');
|
||||
if (
|
||||
msg.includes(
|
||||
'native Tailscale is configured, but the tailscale package is missing',
|
||||
)
|
||||
) {
|
||||
return _(
|
||||
'Native Tailscale is configured, but tailscale package is missing',
|
||||
);
|
||||
}
|
||||
if (msg.includes('native Tailscale is configured, but tailscaled is not running')) {
|
||||
return _('Native Tailscale is configured, but tailscaled is not running for every section');
|
||||
if (
|
||||
msg.includes(
|
||||
'native Tailscale is configured, but tailscaled is not running',
|
||||
)
|
||||
) {
|
||||
return _(
|
||||
'Native Tailscale is configured, but tailscaled is not running for every section',
|
||||
);
|
||||
}
|
||||
if (msg.includes('no server section uses native mode')) {
|
||||
return _('Tailscale package is installed, but no server section uses native mode');
|
||||
return _(
|
||||
'Tailscale package is installed, but no server section uses native mode',
|
||||
);
|
||||
}
|
||||
|
||||
return _(msg);
|
||||
|
||||
@@ -3,9 +3,9 @@ import { formatServiceStatusMessage } from '../formatServiceStatusMessage';
|
||||
|
||||
describe('formatServiceStatusMessage', () => {
|
||||
it('formats normal provider status', () => {
|
||||
expect(formatServiceStatusMessage('zapret2 provider status is normal')).toBe(
|
||||
'Provider status is normal: zapret2',
|
||||
);
|
||||
expect(
|
||||
formatServiceStatusMessage('zapret2 provider status is normal'),
|
||||
).toBe('Provider status is normal: zapret2');
|
||||
expect(formatServiceStatusMessage('byedpi provider status is normal')).toBe(
|
||||
'Provider status is normal: byedpi',
|
||||
);
|
||||
@@ -20,7 +20,8 @@ describe('formatServiceStatusMessage', () => {
|
||||
});
|
||||
|
||||
it('formats missing binary path', () => {
|
||||
const msg = 'action=zapret2 is configured, but zapret2 provider is not available at /opt/zapret2/nfq2/nfqws2';
|
||||
const msg =
|
||||
'action=zapret2 is configured, but zapret2 provider is not available at /opt/zapret2/nfq2/nfqws2';
|
||||
expect(formatServiceStatusMessage(msg)).toBe(
|
||||
'Action is configured, but binary is not available (zapret2 -> zapret2): /opt/zapret2/nfq2/nfqws2',
|
||||
);
|
||||
|
||||
@@ -1772,9 +1772,7 @@ function renderComponentCard(card: ComponentCard) {
|
||||
icon: action.icon,
|
||||
loading,
|
||||
disabled:
|
||||
action.disabled ||
|
||||
systemInfoLoading ||
|
||||
(anyActionLoading && !loading),
|
||||
action.disabled || systemInfoLoading || (anyActionLoading && !loading),
|
||||
onClick: () => void handleComponentAction(action),
|
||||
});
|
||||
});
|
||||
@@ -1788,9 +1786,7 @@ function renderComponentCard(card: ComponentCard) {
|
||||
icon: action.icon,
|
||||
loading,
|
||||
disabled:
|
||||
action.disabled ||
|
||||
systemInfoLoading ||
|
||||
(anyActionLoading && !loading),
|
||||
action.disabled || systemInfoLoading || (anyActionLoading && !loading),
|
||||
onClick: () => void handleComponentAction(action),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2160,7 +2160,9 @@ function formatServiceStatusMessage(msg) {
|
||||
return `${_("External NFQUEUE rules overlap with Tachyon")} ${provider} (${range})`;
|
||||
}
|
||||
if (msg === "legacy zapret runtime paths are still present and should be migrated") {
|
||||
return _("Legacy zapret runtime paths are still present and should be migrated");
|
||||
return _(
|
||||
"Legacy zapret runtime paths are still present and should be migrated"
|
||||
);
|
||||
}
|
||||
const unexpectedProcessesMatch = msg.match(
|
||||
/^unexpected Tachyon-managed ([^ ]+) processes are running without matching action=([^ ]+) rules$/
|
||||
@@ -2170,7 +2172,9 @@ function formatServiceStatusMessage(msg) {
|
||||
return `${_("Unexpected background processes")} (${bin}, action=${action})`;
|
||||
}
|
||||
if (msg.startsWith("standalone ")) {
|
||||
return _("Standalone service is active alongside Tachyon; policy or port conflicts are possible");
|
||||
return _(
|
||||
"Standalone service is active alongside Tachyon; policy or port conflicts are possible"
|
||||
);
|
||||
}
|
||||
const packageInstalledNoBinMatch = msg.match(
|
||||
/^(.+) package is installed, but (?:the provider binary|ciadpi) is not available at (.+)$/
|
||||
@@ -2187,16 +2191,28 @@ function formatServiceStatusMessage(msg) {
|
||||
return `${_("Package is not installed")} (${provider})${action ? `: action=${action} ${_("is unavailable")}` : ""}`;
|
||||
}
|
||||
if (msg.includes("ciadpi has restarted after exiting")) {
|
||||
return _("ByeDPI restarted after exiting; strategy or traffic load may be unstable");
|
||||
return _(
|
||||
"ByeDPI restarted after exiting; strategy or traffic load may be unstable"
|
||||
);
|
||||
}
|
||||
if (msg.includes("native Tailscale is configured, but the tailscale package is missing")) {
|
||||
return _("Native Tailscale is configured, but tailscale package is missing");
|
||||
if (msg.includes(
|
||||
"native Tailscale is configured, but the tailscale package is missing"
|
||||
)) {
|
||||
return _(
|
||||
"Native Tailscale is configured, but tailscale package is missing"
|
||||
);
|
||||
}
|
||||
if (msg.includes("native Tailscale is configured, but tailscaled is not running")) {
|
||||
return _("Native Tailscale is configured, but tailscaled is not running for every section");
|
||||
if (msg.includes(
|
||||
"native Tailscale is configured, but tailscaled is not running"
|
||||
)) {
|
||||
return _(
|
||||
"Native Tailscale is configured, but tailscaled is not running for every section"
|
||||
);
|
||||
}
|
||||
if (msg.includes("no server section uses native mode")) {
|
||||
return _("Tailscale package is installed, but no server section uses native mode");
|
||||
return _(
|
||||
"Tailscale package is installed, but no server section uses native mode"
|
||||
);
|
||||
}
|
||||
return _(msg);
|
||||
}
|
||||
|
||||
@@ -8620,6 +8620,8 @@ function createSectionContent(section) {
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_reject_after_time");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_keepalive_timeout");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_max_handshake_attempts");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_random_trailers");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_disable_cookies");
|
||||
|
||||
setState("success", _("Generated!"));
|
||||
resetAfter(2500);
|
||||
@@ -8880,8 +8882,10 @@ function createSectionContent(section) {
|
||||
const rja = getConfVal("RejectAfterTime") || "";
|
||||
const kpt = getConfVal("KeepaliveTimeout") || "";
|
||||
const mha = getConfVal("MaxHandshakeAttempts") || "";
|
||||
const randomTrailers = getConfVal("RandomTrailers") || "";
|
||||
const disableCookies = getConfVal("DisableCookies") || "";
|
||||
|
||||
const isV31 = !!(rka || rkt || rja || kpt || mha);
|
||||
const isV31 = !!(rka || rkt || rja || kpt || mha || randomTrailers || disableCookies);
|
||||
const isV30 = !isV31 && !!(hpk || cpa);
|
||||
const detectedVersion = isV31 ? "3.1" : (isV30 ? "3.0" : "2.0");
|
||||
|
||||
@@ -8903,6 +8907,8 @@ function createSectionContent(section) {
|
||||
setVal("awg_reject_after_time", rja);
|
||||
setVal("awg_keepalive_timeout", kpt);
|
||||
setVal("awg_max_handshake_attempts", mha);
|
||||
uci.set(UCI_PACKAGE, section_id, "awg_random_trailers", /^(1|true|yes|on)$/i.test(String(randomTrailers).trim()) ? "1" : "0");
|
||||
uci.set(UCI_PACKAGE, section_id, "awg_disable_cookies", /^(1|true|yes|on)$/i.test(String(disableCookies).trim()) ? "1" : "0");
|
||||
} else {
|
||||
setVal("awg_rekey_after_time", "");
|
||||
setVal("awg_rekey_timeout", "");
|
||||
@@ -8914,6 +8920,8 @@ function createSectionContent(section) {
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_reject_after_time");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_keepalive_timeout");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_max_handshake_attempts");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_random_trailers");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_disable_cookies");
|
||||
}
|
||||
|
||||
// --- [Peer] ---
|
||||
@@ -9034,8 +9042,14 @@ function createSectionContent(section) {
|
||||
"awg_keepalive",
|
||||
_("Persistent Keepalive"),
|
||||
);
|
||||
o.datatype = "uinteger";
|
||||
o.placeholder = "25";
|
||||
o.placeholder = "25-35";
|
||||
o.validate = function (_section_id, value) {
|
||||
if (!value) return true;
|
||||
if (/^\d+$/.test(value)) return true;
|
||||
const m = value.match(/^(\d+)-(\d+)$/);
|
||||
if (m && Number(m[1]) <= Number(m[2])) return true;
|
||||
return _("Enter a number or range, e.g. 25 or 25-35");
|
||||
};
|
||||
o.modalonly = true;
|
||||
o.rmempty = true;
|
||||
o.depends("action", "awg");
|
||||
@@ -9063,12 +9077,16 @@ function createSectionContent(section) {
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_reject_after_time");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_keepalive_timeout");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_max_handshake_attempts");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_random_trailers");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_disable_cookies");
|
||||
} else if (formvalue === "3.0") {
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_rekey_after_time");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_rekey_timeout");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_reject_after_time");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_keepalive_timeout");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_max_handshake_attempts");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_random_trailers");
|
||||
uci.unset(UCI_PACKAGE, section_id, "awg_disable_cookies");
|
||||
}
|
||||
return uci.set(UCI_PACKAGE, section_id, "awg_version", formvalue);
|
||||
};
|
||||
@@ -9143,6 +9161,24 @@ function createSectionContent(section) {
|
||||
addAwgV31Param("awg_keepalive_timeout", _("Keepalive Timeout"), _("AmneziaWG 3.1 keepalive timeout"));
|
||||
addAwgV31Param("awg_max_handshake_attempts", _("Max Handshake Attempts"), _("AmneziaWG 3.1 max handshake attempts"));
|
||||
|
||||
o = section.taboption(
|
||||
"settings", form.Flag, "awg_random_trailers", _("Random Trailers"),
|
||||
_("AmneziaWG 3.1 random packet trailers"),
|
||||
);
|
||||
o.modalonly = true;
|
||||
o.rmempty = false;
|
||||
o.default = "0";
|
||||
o.depends({ action: "awg", awg_version: "3.1" });
|
||||
|
||||
o = section.taboption(
|
||||
"settings", form.Flag, "awg_disable_cookies", _("Disable Cookies"),
|
||||
_("AmneziaWG 3.1 disable cookie replies"),
|
||||
);
|
||||
o.modalonly = true;
|
||||
o.rmempty = false;
|
||||
o.default = "0";
|
||||
o.depends({ action: "awg", awg_version: "3.1" });
|
||||
|
||||
|
||||
// ── WARP (Cloudflare WARP via sing-box-extended) ──────────────────────────
|
||||
|
||||
|
||||
@@ -508,6 +508,51 @@ function rule_sets_value(section) {
|
||||
return list_option_value_from_array(rule_sets(section));
|
||||
}
|
||||
|
||||
function combined_domain_condition_text(section) {
|
||||
if (type(raw_option(section, "domain")) != "array") {
|
||||
let value = option(section, "domain", "");
|
||||
if (value != "")
|
||||
return value;
|
||||
}
|
||||
|
||||
return option(section, "domain_suffix_text", "");
|
||||
}
|
||||
|
||||
function rule_config_or_null() {
|
||||
try {
|
||||
return require("config.rule");
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function rule_condition_csv(section, key, kind) {
|
||||
let rule_config = rule_config_or_null();
|
||||
if (!rule_config)
|
||||
return "";
|
||||
return rule_config.rule_condition_csv_value(
|
||||
key,
|
||||
kind,
|
||||
option(section, key + "_text_mode", "0"),
|
||||
option(section, "conditions_text_mode", "0"),
|
||||
option(section, key + "_text", ""),
|
||||
option(section, key, ""),
|
||||
combined_domain_condition_text(section),
|
||||
option(section, "domain_suffix", "")
|
||||
);
|
||||
}
|
||||
|
||||
function has_dns_matchers(section) {
|
||||
return rule_condition_csv(section, "domain", "domains") != "" ||
|
||||
rule_condition_csv(section, "domain_suffix", "domains") != "" ||
|
||||
rule_condition_csv(section, "domain_keyword", "generic") != "" ||
|
||||
rule_condition_csv(section, "domain_regex", "generic") != "" ||
|
||||
length(community_lists(section)) > 0 ||
|
||||
length(rule_sets(section)) > 0 ||
|
||||
length(rule_sets_with_subnets(section)) > 0 ||
|
||||
option(section, "domain_ip_lists", "") != "";
|
||||
}
|
||||
|
||||
const DSCP_ALIASES = {
|
||||
"cs0": 0, "cs1": 8, "cs2": 16, "cs3": 24, "cs4": 32, "cs5": 40, "cs6": 48, "cs7": 56,
|
||||
"ef": 46,
|
||||
@@ -1359,5 +1404,6 @@ return {
|
||||
geoip_country_value,
|
||||
subscription_download_targets,
|
||||
subscription_download_target_port,
|
||||
has_dns_matchers,
|
||||
item_index_from_cursor
|
||||
};
|
||||
@@ -1388,8 +1388,17 @@ function validate_dns_action(section, sections, context) {
|
||||
}
|
||||
if (length(connections.rule_sets_with_subnets(section)) > 0)
|
||||
fail_validation("DNS rule '" + name + "' can use domain-only rule sets, but subnet extraction is enabled. Disable 'Include IP addresses and subnets'. Aborted.");
|
||||
if (!dns_action_has_domain_matchers(section))
|
||||
fail_validation("DNS rule '" + name + "' must contain at least one domain condition or domain rule set. Aborted.");
|
||||
let fully_routed_ips = list_option(section, "fully_routed_ips");
|
||||
if (!dns_action_has_domain_matchers(section) && length(fully_routed_ips) == 0)
|
||||
fail_validation("DNS rule '" + name + "' must contain at least one domain condition, domain rule set, or forced device. Aborted.");
|
||||
for (let ip in fully_routed_ips) {
|
||||
if (!core_ip.valid_ip_or_cidr(ip))
|
||||
fail_validation("DNS rule '" + name + "' has an invalid forced device IP or subnet '" + ip + "'. Aborted.");
|
||||
}
|
||||
for (let ip in list_option(section, "source_ip_cidr")) {
|
||||
if (!core_ip.valid_ip_or_cidr(ip))
|
||||
fail_validation("DNS rule '" + name + "' has an invalid device filter IP or subnet '" + ip + "'. Aborted.");
|
||||
}
|
||||
if (!bool_option(section, "dns_detour_enabled", false))
|
||||
return;
|
||||
|
||||
|
||||
@@ -9,8 +9,11 @@ let rule_config = require("config.rule");
|
||||
let domain_config = require("config.domain");
|
||||
let connections = require("config.connections");
|
||||
let routing_rulesets = require("routing.rulesets");
|
||||
let runtime_constants = require("singbox.constants");
|
||||
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||||
const DNS_BLOCK_TARGET = getenv("SB_DNS_BLOCK_INBOUND_ADDRESS") || "127.0.0.43:1053";
|
||||
const DNS_SOURCE_SET = runtime_constants.DNS_SOURCE_SET;
|
||||
const DNS_SOURCE6_SET = runtime_constants.DNS_SOURCE6_SET;
|
||||
|
||||
let common_read_json_file = common.read_json_file;
|
||||
let list_option = common.list_option;
|
||||
@@ -1241,6 +1244,8 @@ function nft_create_runtime_base(table, localv4_set, common_set, port_set, ip_po
|
||||
!nft_create_inet_service_set(table, port_set) ||
|
||||
!nft_create_ipv4_port_set(table, ip_port_set) ||
|
||||
!nft_create_ipv6_port_set(table, ip_port6_set) ||
|
||||
!nft_create_ipv4_set(table, DNS_SOURCE_SET) ||
|
||||
!nft_create_ipv6_set(table, DNS_SOURCE6_SET) ||
|
||||
!nft_create_ifname_set(table, interface_set))
|
||||
return false;
|
||||
|
||||
@@ -1248,7 +1253,8 @@ function nft_create_runtime_base(table, localv4_set, common_set, port_set, ip_po
|
||||
if (!nft_add_set_elements(table, interface_set, interface))
|
||||
return false;
|
||||
|
||||
if (!nft_create_chain(table, "mangle", "{ type filter hook prerouting priority -149; policy accept; }") ||
|
||||
if (!nft_create_chain(table, "dns_redirect", "{ type nat hook prerouting priority -100; policy accept; }") ||
|
||||
!nft_create_chain(table, "mangle", "{ type filter hook prerouting priority -149; policy accept; }") ||
|
||||
!nft_create_chain(table, "mangle_output", "{ type route hook output priority -150; policy accept; }") ||
|
||||
!nft_create_priority_chains(table) ||
|
||||
!nft_create_chain(table, "parental_control", "{ }") ||
|
||||
@@ -1257,7 +1263,11 @@ function nft_create_runtime_base(table, localv4_set, common_set, port_set, ip_po
|
||||
!nft_create_chain(table, "proxy", "{ type filter hook prerouting priority -100; policy accept; }"))
|
||||
return false;
|
||||
|
||||
if (!nft_add_rule(table, "mangle", [ "ct", "status", "dnat", "return" ]) ||
|
||||
if (!nft_add_rule(table, "dns_redirect", [ "iifname", "@" + as_string(interface_set), "ip", "saddr", "@" + DNS_SOURCE_SET, "tcp", "dport", "53", "counter", "redirect", "to", ":" + as_string(runtime_constants.SOURCE_DNS_INBOUND_PORT) ]) ||
|
||||
!nft_add_rule(table, "dns_redirect", [ "iifname", "@" + as_string(interface_set), "ip", "saddr", "@" + DNS_SOURCE_SET, "udp", "dport", "53", "counter", "redirect", "to", ":" + as_string(runtime_constants.SOURCE_DNS_INBOUND_PORT) ]) ||
|
||||
!nft_add_rule(table, "dns_redirect", [ "iifname", "@" + as_string(interface_set), "ip6", "saddr", "@" + DNS_SOURCE6_SET, "tcp", "dport", "53", "counter", "redirect", "to", ":" + as_string(runtime_constants.SOURCE_DNS_INBOUND_PORT) ]) ||
|
||||
!nft_add_rule(table, "dns_redirect", [ "iifname", "@" + as_string(interface_set), "ip6", "saddr", "@" + DNS_SOURCE6_SET, "udp", "dport", "53", "counter", "redirect", "to", ":" + as_string(runtime_constants.SOURCE_DNS_INBOUND_PORT) ]) ||
|
||||
!nft_add_rule(table, "mangle", [ "ct", "status", "dnat", "return" ]) ||
|
||||
!nft_add_rule(table, "mangle", [ "jump", "parental_control" ]))
|
||||
return false;
|
||||
|
||||
@@ -2060,10 +2070,17 @@ function nft_rule_signature_body(body, section) {
|
||||
|
||||
let action = option(section, "action", "");
|
||||
body = signature_add_value(body, "rule." + section_name + ".action", action);
|
||||
if (action == "dns" || action == "hosts")
|
||||
if (action == "hosts")
|
||||
return body;
|
||||
if (action == "dns") {
|
||||
body = signature_add_value(body, "rule." + section_name + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
|
||||
body = signature_add_value(body, "rule." + section_name + ".source_aware_dns", connections.has_dns_matchers(section) ? "1" : "0");
|
||||
body = signature_add_value(body, "rule." + section_name + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
|
||||
return body;
|
||||
}
|
||||
body = signature_add_value(body, "rule." + section_name + ".ip_cidr", section_rule_condition_csv(section, "ip_cidr", "subnets"));
|
||||
body = signature_add_value(body, "rule." + section_name + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
|
||||
body = signature_add_value(body, "rule." + section_name + ".source_aware_dns", connections.has_dns_matchers(section) ? "1" : "0");
|
||||
body = signature_add_value(body, "rule." + section_name + ".ports", section_rule_ports_csv(section));
|
||||
body = signature_add_value(body, "rule." + section_name + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
|
||||
body = signature_add_value(body, "rule." + section_name + ".excluded_ips", option(section, "excluded_ips", ""));
|
||||
@@ -2385,6 +2402,65 @@ function nft_add_community_subnet_file_for_fixture_section(fixture_path, section
|
||||
return nft_add_community_subnet_file_for_section(fixture_section(fixture_path, section_name), service, filepath, table, common_set, ip_port_set, interface_set, discord_set, mark, chunk_size_text, common6_set, ip_port6_set, discord6_set);
|
||||
}
|
||||
|
||||
function source_aware_dns_values(sections, deferred_sections) {
|
||||
let seen = {};
|
||||
let values = [];
|
||||
|
||||
for (let section in sections) {
|
||||
if (!bool_option(section, "enabled", true) ||
|
||||
deferred_sections[as_string(section[".name"])])
|
||||
continue;
|
||||
|
||||
let action = section_action(section);
|
||||
|
||||
if (connections.has_dns_matchers(section)) {
|
||||
for (let value in nft_csv_values(section_source_ip_values(section))) {
|
||||
if (!seen[value]) {
|
||||
seen[value] = true;
|
||||
push(values, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action == "bypass" || action == "dns") {
|
||||
for (let value in list_option(section, "fully_routed_ips")) {
|
||||
value = trim(as_string(value));
|
||||
if (value != "" && !seen[value]) {
|
||||
seen[value] = true;
|
||||
push(values, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action == "dns") {
|
||||
for (let value in nft_csv_values(section_source_ip_values(section))) {
|
||||
if (!seen[value]) {
|
||||
seen[value] = true;
|
||||
push(values, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function nft_add_source_aware_dns_sources(sections, deferred_sections, table) {
|
||||
let values = source_aware_dns_values(sections, deferred_sections);
|
||||
if (length(values) == 0)
|
||||
return true;
|
||||
|
||||
return nft_add_csv_chunks_to_family_sets(
|
||||
join(",", values),
|
||||
table,
|
||||
DNS_SOURCE_SET,
|
||||
DNS_SOURCE6_SET,
|
||||
"ips",
|
||||
"",
|
||||
5000
|
||||
);
|
||||
}
|
||||
|
||||
function nft_populate_runtime_sets_from_sections(sections, populate_enabled, deferred_section_names, table, common_set, port_set, ip_port_set, interface_set, localv4_set, mark, common6_set, ip_port6_set, localv6_set) {
|
||||
if (!arg_bool(populate_enabled))
|
||||
return true;
|
||||
@@ -2393,6 +2469,9 @@ function nft_populate_runtime_sets_from_sections(sections, populate_enabled, def
|
||||
let mangle_chain_context = {};
|
||||
let inserted_fully_routed_ips = {};
|
||||
|
||||
if (!nft_add_source_aware_dns_sources(sections, deferred_sections, table))
|
||||
return false;
|
||||
|
||||
for (let section in sections)
|
||||
if (!nft_populate_runtime_set_for_section(section, deferred_sections, table, common_set, port_set, ip_port_set, interface_set, localv4_set, mark, mangle_chain_context, inserted_fully_routed_ips, common6_set, ip_port6_set, localv6_set))
|
||||
return false;
|
||||
|
||||
@@ -944,10 +944,17 @@ function nft_runtime_signature_body(settings, sections) {
|
||||
|
||||
let action = option(section, "action", "");
|
||||
body = signature_add_value(body, "rule." + name + ".action", action);
|
||||
if (action == "dns" || action == "hosts")
|
||||
if (action == "hosts")
|
||||
continue;
|
||||
if (action == "dns") {
|
||||
body = signature_add_value(body, "rule." + name + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
|
||||
body = signature_add_value(body, "rule." + name + ".source_aware_dns", connections.has_dns_matchers(section) ? "1" : "0");
|
||||
body = signature_add_value(body, "rule." + name + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
|
||||
continue;
|
||||
}
|
||||
body = signature_add_value(body, "rule." + name + ".ip_cidr", section_rule_condition_csv(section, "ip_cidr", "subnets"));
|
||||
body = signature_add_value(body, "rule." + name + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
|
||||
body = signature_add_value(body, "rule." + name + ".source_aware_dns", connections.has_dns_matchers(section) ? "1" : "0");
|
||||
body = signature_add_value(body, "rule." + name + ".ports", section_rule_ports_csv(section));
|
||||
body = signature_add_value(body, "rule." + name + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
|
||||
body = signature_add_value(body, "rule." + name + ".excluded_ips", option(section, "excluded_ips", ""));
|
||||
@@ -1379,11 +1386,13 @@ function append_sing_box_rule_signature_body(body, section, sections) {
|
||||
body = signature_add_value(body, prefix + ".domain_suffix", section_rule_condition_csv(section, "domain_suffix", "domains"));
|
||||
body = signature_add_value(body, prefix + ".domain_keyword", section_rule_condition_csv(section, "domain_keyword", "generic"));
|
||||
body = signature_add_value(body, prefix + ".domain_regex", section_rule_condition_csv(section, "domain_regex", "generic"));
|
||||
if (action != "dns") {
|
||||
if (action != "dns")
|
||||
body = signature_add_value(body, prefix + ".ip_cidr", section_rule_condition_csv(section, "ip_cidr", "subnets"));
|
||||
body = signature_add_value(body, prefix + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
|
||||
body = signature_add_value(body, prefix + ".source_ip_cidr", section_rule_condition_csv(section, "source_ip_cidr", "subnets"));
|
||||
if (action != "dns")
|
||||
body = signature_add_value(body, prefix + ".ports", section_rule_ports_csv(section));
|
||||
body = signature_add_value(body, prefix + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
|
||||
body = signature_add_value(body, prefix + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
|
||||
if (action != "dns") {
|
||||
body = signature_add_value(body, prefix + ".excluded_ips", option(section, "excluded_ips", ""));
|
||||
body = signature_add_value(body, prefix + ".excluded_protocol", option(section, "excluded_protocol", ""));
|
||||
body = signature_add_value(body, prefix + ".protocol", option(section, "protocol", ""));
|
||||
|
||||
@@ -18,6 +18,12 @@ const DNS_INBOUND_PORT = 53;
|
||||
const DNS_BLOCK_INBOUND_TAG = "dns-block-in";
|
||||
const DNS_BLOCK_INBOUND_ADDRESS = "127.0.0.43";
|
||||
const DNS_BLOCK_INBOUND_PORT = 1053;
|
||||
const SOURCE_DNS_INBOUND_TAG = "source-dns-in";
|
||||
const SOURCE_DNS_INBOUND_ADDRESS = "::";
|
||||
const SOURCE_DNS_INBOUND_PORT = 1603;
|
||||
const DNSMASQ_DNS_SERVER_TAG = "dnsmasq-server";
|
||||
const DNS_SOURCE_SET = "tachyon_dns_sources";
|
||||
const DNS_SOURCE6_SET = "tachyon_dns_sources6";
|
||||
|
||||
const SERVICE_MIXED_INBOUND_TAG = "service-mixed-in";
|
||||
const SERVICE_MIXED_INBOUND_ADDRESS = "127.0.0.1";
|
||||
@@ -56,6 +62,8 @@ const RESERVED_TAGS = {
|
||||
[TPROXY_INBOUND6_TAG]: true,
|
||||
[DNS_INBOUND_TAG]: true,
|
||||
[DNS_BLOCK_INBOUND_TAG]: true,
|
||||
[SOURCE_DNS_INBOUND_TAG]: true,
|
||||
[DNSMASQ_DNS_SERVER_TAG]: true,
|
||||
[SERVICE_MIXED_INBOUND_TAG]: true,
|
||||
[DIRECT_BYPASS_INBOUND_TAG]: true,
|
||||
[DIRECT_BYPASS_OUTBOUND_TAG]: true,
|
||||
@@ -106,6 +114,12 @@ return {
|
||||
DNS_BLOCK_INBOUND_TAG,
|
||||
DNS_BLOCK_INBOUND_ADDRESS,
|
||||
DNS_BLOCK_INBOUND_PORT,
|
||||
SOURCE_DNS_INBOUND_TAG,
|
||||
SOURCE_DNS_INBOUND_ADDRESS,
|
||||
SOURCE_DNS_INBOUND_PORT,
|
||||
DNSMASQ_DNS_SERVER_TAG,
|
||||
DNS_SOURCE_SET,
|
||||
DNS_SOURCE6_SET,
|
||||
SERVICE_MIXED_INBOUND_TAG,
|
||||
SERVICE_MIXED_INBOUND_ADDRESS,
|
||||
SERVICE_MIXED_INBOUND_PORT,
|
||||
|
||||
@@ -405,6 +405,7 @@ function tproxy_inbound_matcher() {
|
||||
}
|
||||
|
||||
function base_config(settings, service_address, runtime_context) {
|
||||
runtime_context = object_or_empty(runtime_context);
|
||||
let log_level = option(settings, "log_level", "warn");
|
||||
let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60");
|
||||
let turbo_cache = bool_option(settings, "dns_turbo_cache", false);
|
||||
@@ -494,6 +495,14 @@ function base_config(settings, service_address, runtime_context) {
|
||||
push(inbounds, { type: "tproxy", tag: runtime_constants.TPROXY_INBOUND6_TAG, listen: runtime_constants.TPROXY_INBOUND6_ADDRESS, listen_port: runtime_constants.TPROXY_INBOUND_PORT, tcp_fast_open: true, udp_fragment: true });
|
||||
}
|
||||
push(inbounds, { type: "direct", tag: runtime_constants.DNS_INBOUND_TAG, listen: runtime_constants.DNS_INBOUND_ADDRESS, listen_port: runtime_constants.DNS_INBOUND_PORT });
|
||||
if (runtime_context.source_aware_dns) {
|
||||
push(inbounds, {
|
||||
type: "direct",
|
||||
tag: runtime_constants.SOURCE_DNS_INBOUND_TAG,
|
||||
listen: core_ip.ipv6_supported() ? runtime_constants.SOURCE_DNS_INBOUND_ADDRESS : "0.0.0.0",
|
||||
listen_port: runtime_constants.SOURCE_DNS_INBOUND_PORT
|
||||
});
|
||||
}
|
||||
for (let inbound in dns_config.inbounds)
|
||||
push(inbounds, inbound);
|
||||
|
||||
@@ -502,7 +511,6 @@ function base_config(settings, service_address, runtime_context) {
|
||||
{ type: "direct", tag: runtime_constants.BYPASS_OUTBOUND_TAG }
|
||||
];
|
||||
|
||||
runtime_context = object_or_empty(runtime_context);
|
||||
runtime_context.dns_health_inbounds = dns_config.sniff_inbounds;
|
||||
runtime_context.default_domain_resolver = runtime_dns.default_domain_resolver(settings);
|
||||
|
||||
@@ -539,6 +547,14 @@ function base_config(settings, service_address, runtime_context) {
|
||||
? (match(sb_version_val, /^v?1\.1[0-3]\./) != null)
|
||||
: !is_extended_variant;
|
||||
|
||||
let is_sb_1_14_plus = sb_version_val != ""
|
||||
? (match(sb_version_val, /^v?1\.(1[4-9]|[2-9][0-9])\./) != null)
|
||||
: false;
|
||||
|
||||
let route_section = runtime_route.config(settings, runtime_context);
|
||||
if (is_sb_1_14_plus)
|
||||
route_section.default_http_client = "ruleset-http";
|
||||
|
||||
let cache_file_section = {
|
||||
enabled: true,
|
||||
path: cache_path,
|
||||
@@ -549,7 +565,7 @@ function base_config(settings, service_address, runtime_context) {
|
||||
else
|
||||
cache_file_section.store_dns = true;
|
||||
|
||||
return {
|
||||
let base_cfg = {
|
||||
log: {
|
||||
disabled: false,
|
||||
level: log_level,
|
||||
@@ -561,7 +577,7 @@ function base_config(settings, service_address, runtime_context) {
|
||||
endpoints: [],
|
||||
inbounds,
|
||||
outbounds: default_outbounds,
|
||||
route: runtime_route.config(settings, runtime_context),
|
||||
route: route_section,
|
||||
services: [],
|
||||
experimental: {
|
||||
cache_file: cache_file_section,
|
||||
@@ -569,6 +585,11 @@ function base_config(settings, service_address, runtime_context) {
|
||||
},
|
||||
__dns_hosts_predefined: dns_hosts_predefined
|
||||
};
|
||||
|
||||
if (is_sb_1_14_plus)
|
||||
base_cfg.http_clients = [{ tag: "ruleset-http" }];
|
||||
|
||||
return base_cfg;
|
||||
}
|
||||
|
||||
|
||||
@@ -991,6 +1012,78 @@ function add_content_blocking(config) {
|
||||
add_content_block_route_rules(config, profiles);
|
||||
}
|
||||
|
||||
function single_or_array(values) {
|
||||
if (type(values) != "array")
|
||||
return values;
|
||||
if (length(values) == 1)
|
||||
return values[0];
|
||||
return values;
|
||||
}
|
||||
|
||||
function source_aware_dns_sources(sections) {
|
||||
let sources = [];
|
||||
let seen = {};
|
||||
let add_source = function(value) {
|
||||
value = as_string(value);
|
||||
if (value == "" || seen[value])
|
||||
return;
|
||||
seen[value] = true;
|
||||
push(sources, value);
|
||||
};
|
||||
|
||||
for (let section in sections) {
|
||||
let action = option(section, "action", "");
|
||||
let has_dns_matchers = connections.has_dns_matchers(section);
|
||||
|
||||
if (has_dns_matchers) {
|
||||
for (let ip in list_option(section, "source_ip_cidr"))
|
||||
add_source(ip);
|
||||
}
|
||||
|
||||
if (action == "bypass") {
|
||||
for (let ip in list_option(section, "fully_routed_ips"))
|
||||
add_source(ip);
|
||||
}
|
||||
|
||||
if (action == "dns") {
|
||||
for (let ip in list_option(section, "fully_routed_ips"))
|
||||
add_source(ip);
|
||||
for (let ip in list_option(section, "source_ip_cidr"))
|
||||
add_source(ip);
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
function add_source_aware_dns_support(config, source_aware_dns) {
|
||||
if (length(source_aware_dns) == 0)
|
||||
return;
|
||||
|
||||
push(config.dns.servers, {
|
||||
type: "udp",
|
||||
tag: runtime_constants.DNSMASQ_DNS_SERVER_TAG,
|
||||
server: "127.0.0.1",
|
||||
server_port: 53
|
||||
});
|
||||
}
|
||||
|
||||
function add_source_aware_dns_fallback(config, source_aware_dns) {
|
||||
if (length(source_aware_dns) == 0)
|
||||
return;
|
||||
|
||||
let settings = runtime_settings_cache || {};
|
||||
let rewrite_ttl = int_option(settings, "dns_rewrite_ttl", "60");
|
||||
|
||||
push(config.dns.rules, {
|
||||
action: "route",
|
||||
server: runtime_constants.DNSMASQ_DNS_SERVER_TAG,
|
||||
inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ],
|
||||
source_ip_cidr: single_or_array(source_aware_dns),
|
||||
rewrite_ttl
|
||||
});
|
||||
}
|
||||
|
||||
function generate_config(output_path, service_address, mwan3_active, supports_xhttp) {
|
||||
ctx.runtime_ruleset_folder = runtime_ruleset_folder;
|
||||
runtime_supports_xhttp = supports_xhttp == null || as_string(supports_xhttp) == ""
|
||||
@@ -1007,7 +1100,12 @@ function generate_config(output_path, service_address, mwan3_active, supports_xh
|
||||
if (length(sections) == 0 && length(servers) == 0)
|
||||
runtime_generate_unsupported("no enabled sections");
|
||||
|
||||
let config = base_config(settings, service_address, { mwan3_active: cli_bool(mwan3_active) });
|
||||
let source_aware_dns = source_aware_dns_sources(sections);
|
||||
let config = base_config(settings, service_address, {
|
||||
mwan3_active: cli_bool(mwan3_active),
|
||||
source_aware_dns: length(source_aware_dns) > 0
|
||||
});
|
||||
add_source_aware_dns_support(config, source_aware_dns);
|
||||
let taken = reserved_runtime_tag_set(config.outbounds);
|
||||
reserve_section_outbound_tags(sections, taken);
|
||||
for (let server in servers)
|
||||
@@ -1018,6 +1116,7 @@ function generate_config(output_path, service_address, mwan3_active, supports_xh
|
||||
for (let section in sections)
|
||||
add_route_for_section(config, section);
|
||||
add_server_routes(config, servers, sections);
|
||||
add_source_aware_dns_fallback(config, source_aware_dns);
|
||||
|
||||
// Append dns_hosts predefined rules AFTER section DNS rules so that
|
||||
// FakeIP/section-level DNS routing takes precedence over hardcoded IPs
|
||||
|
||||
@@ -1296,8 +1296,23 @@ function add_awg_endpoint(config, section) {
|
||||
let mtu = int_option(section, "awg_mtu", "1280");
|
||||
endpoint.mtu = mtu > 0 ? mtu : 1280;
|
||||
|
||||
let keepalive = int_option(section, "awg_keepalive", "25");
|
||||
endpoint.peers[0].persistent_keepalive_interval = keepalive > 0 ? keepalive : 25;
|
||||
let sb_variant_file = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
|
||||
let sb_version_file = getenv("SB_VERSION_STATE_FILE") || "/etc/tachyon/sing-box-version";
|
||||
let sb_version_val = trim(fs.readfile(sb_version_file) || "");
|
||||
let sb_variant_val = trim(fs.readfile(sb_variant_file) || "");
|
||||
let is_lx = sb_variant_val == "lx" || index(sb_version_val, "-lx") >= 0;
|
||||
|
||||
let keepalive = int_or_range_option(section, "awg_keepalive", 25);
|
||||
if (is_lx) {
|
||||
endpoint.peers[0].persistent_keepalive_interval = keepalive;
|
||||
} else {
|
||||
if (type(keepalive) == "string") {
|
||||
let m = match(keepalive, /^([0-9]+)/);
|
||||
endpoint.peers[0].persistent_keepalive_interval = m ? int(m[1], 10) : 25;
|
||||
} else {
|
||||
endpoint.peers[0].persistent_keepalive_interval = keepalive > 0 ? keepalive : 25;
|
||||
}
|
||||
}
|
||||
|
||||
let jc = int_option(section, "awg_jc", "4");
|
||||
if (jc > 10) jc = 10;
|
||||
@@ -1331,10 +1346,6 @@ function add_awg_endpoint(config, section) {
|
||||
let j2 = awg_cps_option(section, "awg_j2");
|
||||
let j3 = awg_cps_option(section, "awg_j3");
|
||||
let itime = int_option(section, "awg_itime", "0");
|
||||
let sb_variant_file = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
|
||||
let sb_version_file = getenv("SB_VERSION_STATE_FILE") || "/etc/tachyon/sing-box-version";
|
||||
let sb_version_val = trim(fs.readfile(sb_version_file) || "");
|
||||
let is_lx = sb_version_val != "" ? (index(sb_version_val, "-lx") >= 0) : (trim(fs.readfile(sb_variant_file) || "") == "lx");
|
||||
|
||||
if (is_lx) {
|
||||
// sing-box-lx expects AWG fields at the endpoint root (AWG 2.0 schema).
|
||||
@@ -1382,7 +1393,8 @@ function add_awg_endpoint(config, section) {
|
||||
if (awg_ver == "") {
|
||||
if (option(section, "awg_rekey_after_time", "") != "" || option(section, "awg_rekey_timeout", "") != "" ||
|
||||
option(section, "awg_reject_after_time", "") != "" || option(section, "awg_keepalive_timeout", "") != "" ||
|
||||
option(section, "awg_max_handshake_attempts", "") != "") {
|
||||
option(section, "awg_max_handshake_attempts", "") != "" ||
|
||||
bool_option(section, "awg_random_trailers", false) || bool_option(section, "awg_disable_cookies", false)) {
|
||||
awg_ver = "3.1";
|
||||
} else if (option(section, "awg_header_protection_key", "") != "" || option(section, "awg_content_padding_addition", "") != "") {
|
||||
awg_ver = "3.0";
|
||||
@@ -1446,6 +1458,13 @@ function add_awg_endpoint(config, section) {
|
||||
if (is_lx) endpoint.max_handshake_attempts = mha;
|
||||
else endpoint.amnezia.max_handshake_attempts = mha;
|
||||
}
|
||||
|
||||
if (is_lx) {
|
||||
if (bool_option(section, "awg_random_trailers", false))
|
||||
endpoint.random_trailers = true;
|
||||
if (bool_option(section, "awg_disable_cookies", false))
|
||||
endpoint.disable_cookies = true;
|
||||
}
|
||||
}
|
||||
|
||||
let detour = option(section, "awg_detour", "");
|
||||
|
||||
@@ -1047,6 +1047,18 @@ function add_dns_server_for_section(config, section) {
|
||||
return server_tags;
|
||||
}
|
||||
|
||||
function source_dns_inbound_matcher() {
|
||||
return [ runtime_constants.SOURCE_DNS_INBOUND_TAG ];
|
||||
}
|
||||
|
||||
function add_source_dns_matchers(rule, source_ip_cidr) {
|
||||
if (length(source_ip_cidr) == 0)
|
||||
return;
|
||||
|
||||
rule.inbound = source_dns_inbound_matcher();
|
||||
rule.source_ip_cidr = single_or_array(source_ip_cidr);
|
||||
}
|
||||
|
||||
function add_dns_action_rules_for_section(config, section) {
|
||||
let domains = domain_conditions(section);
|
||||
let domain = domains.domain;
|
||||
@@ -1055,6 +1067,8 @@ function add_dns_action_rules_for_section(config, section) {
|
||||
let domain_regex = domains.domain_regex;
|
||||
let rule_set_tags = [];
|
||||
let section_name = section[".name"];
|
||||
let source_ip_cidr = legacy_condition_values(section, "source_ip_cidr");
|
||||
let fully_routed_ips = list_option(section, "fully_routed_ips");
|
||||
|
||||
for (let community in connections.community_lists(section)) {
|
||||
let ensured = ensure_community_ruleset(config, section_name, as_string(community));
|
||||
@@ -1087,6 +1101,15 @@ function add_dns_action_rules_for_section(config, section) {
|
||||
let has_inline_domains = length(domain) > 0 || length(domain_suffix) > 0 ||
|
||||
length(domain_keyword) > 0 || length(domain_regex) > 0;
|
||||
|
||||
if (length(fully_routed_ips) > 0) {
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: server_tag,
|
||||
rewrite_ttl
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, fully_routed_ips);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
if (has_inline_domains) {
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
@@ -1097,17 +1120,20 @@ function add_dns_action_rules_for_section(config, section) {
|
||||
add_domain_array(dns_rule, "domain_suffix", domain_suffix);
|
||||
add_domain_array(dns_rule, "domain_keyword", domain_keyword);
|
||||
add_domain_array(dns_rule, "domain_regex", domain_regex);
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
if (length(rule_set_tags) > 0) {
|
||||
push_dns_matcher_rule(config, {
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: server_tag,
|
||||
rewrite_ttl,
|
||||
rule_set: single_or_array(rule_set_tags)
|
||||
});
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
if (!has_inline_domains && length(rule_set_tags) == 0)
|
||||
if (!has_inline_domains && length(rule_set_tags) == 0 && length(fully_routed_ips) == 0)
|
||||
ctx.runtime_generate_unsupported("DNS action '" + section_name + "' has no domain matchers");
|
||||
}
|
||||
|
||||
@@ -1446,15 +1472,18 @@ function add_combined_route_for_section(config, section) {
|
||||
add_domain_array(dns_rule, "domain_suffix", domain_suffix);
|
||||
add_domain_array(dns_rule, "domain_keyword", domain_keyword);
|
||||
add_domain_array(dns_rule, "domain_regex", domain_regex);
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
if (length(dns_rule_set_tags) > 0) {
|
||||
push_dns_matcher_rule(config, {
|
||||
let dns_rule = {
|
||||
action: "route",
|
||||
server: section_dns_server(section),
|
||||
rewrite_ttl,
|
||||
rule_set: single_or_array(dns_rule_set_tags)
|
||||
});
|
||||
};
|
||||
add_source_dns_matchers(dns_rule, source_ip_cidr);
|
||||
push_dns_matcher_rule(config, dns_rule);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,15 +65,20 @@ function config(settings, runtime) {
|
||||
let output_network_interface = option(settings, "output_network_interface", "");
|
||||
let mwan3_active = type(runtime) == "object" && bool_value(runtime.mwan3_active);
|
||||
let sniff_inbounds = [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.DNS_INBOUND_TAG ];
|
||||
if (type(runtime) == "object" && bool_value(runtime.source_aware_dns))
|
||||
push(sniff_inbounds, runtime_constants.SOURCE_DNS_INBOUND_TAG);
|
||||
if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array")
|
||||
for (let inbound in runtime.dns_health_inbounds)
|
||||
push(sniff_inbounds, inbound);
|
||||
let rules = [
|
||||
{ action: "sniff", inbound: sniff_inbounds },
|
||||
{ action: "hijack-dns", port: 53 },
|
||||
{ action: "hijack-dns", protocol: "dns" }
|
||||
];
|
||||
if (type(runtime) == "object" && bool_value(runtime.source_aware_dns))
|
||||
push(rules, { action: "hijack-dns", inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ] });
|
||||
let result = {
|
||||
rules: [
|
||||
{ action: "sniff", inbound: sniff_inbounds },
|
||||
{ action: "hijack-dns", port: 53 },
|
||||
{ action: "hijack-dns", protocol: "dns" }
|
||||
],
|
||||
rules,
|
||||
rule_set: [],
|
||||
final: runtime_constants.DIRECT_OUTBOUND_TAG,
|
||||
auto_detect_interface: output_network_interface == "" && !mwan3_active,
|
||||
|
||||
@@ -339,14 +339,16 @@ function add_standard_inbound(config, section, protocol, tag_name) {
|
||||
let sb_variant_file = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
|
||||
let sb_version_file = getenv("SB_VERSION_STATE_FILE") || "/etc/tachyon/sing-box-version";
|
||||
let sb_version_val = trim(fs.readfile(sb_version_file) || "");
|
||||
let is_lx = sb_version_val != "" ? (index(sb_version_val, "-lx") >= 0) : (trim(fs.readfile(sb_variant_file) || "") == "lx");
|
||||
let sb_variant_val = trim(fs.readfile(sb_variant_file) || "");
|
||||
let is_lx = sb_variant_val == "lx" || index(sb_version_val, "-lx") >= 0;
|
||||
|
||||
// AmneziaWG version handling: 2.0, 3.0, and 3.1 (sing-box-lx v1.14.0-lx.32+)
|
||||
let awg_ver = option(section, "awg_version", "");
|
||||
if (awg_ver == "") {
|
||||
if (option(section, "awg_rekey_after_time", "") != "" || option(section, "awg_rekey_timeout", "") != "" ||
|
||||
option(section, "awg_reject_after_time", "") != "" || option(section, "awg_keepalive_timeout", "") != "" ||
|
||||
option(section, "awg_max_handshake_attempts", "") != "") {
|
||||
option(section, "awg_max_handshake_attempts", "") != "" ||
|
||||
bool_option(section, "awg_random_trailers", false) || bool_option(section, "awg_disable_cookies", false)) {
|
||||
awg_ver = "3.1";
|
||||
} else if (option(section, "awg_header_protection_key", "") != "" || option(section, "awg_content_padding_addition", "") != "") {
|
||||
awg_ver = "3.0";
|
||||
@@ -408,6 +410,11 @@ function add_standard_inbound(config, section, protocol, tag_name) {
|
||||
|
||||
let mha = option(section, "awg_max_handshake_attempts", "");
|
||||
if (mha != "") inbound.max_handshake_attempts = mha;
|
||||
|
||||
if (bool_option(section, "awg_random_trailers", false))
|
||||
inbound.random_trailers = true;
|
||||
if (bool_option(section, "awg_disable_cookies", false))
|
||||
inbound.disable_cookies = true;
|
||||
}
|
||||
} else {
|
||||
if (i1 != "") amnezia.i1 = i1;
|
||||
|
||||
+12
-2
@@ -604,9 +604,13 @@ fi
|
||||
if grep -Fq $'nft\tadd\telement\tinet\tTachyonTable\ttachyon_rule_ports_with_domain_ports\t{ 8443 }' "$NFT_LOG"; then
|
||||
fail "ports with destination matchers should not populate global port set"
|
||||
fi
|
||||
if grep -Fq '192.0.2.53' "$NFT_LOG" || grep -Fq '192.168.1.53' "$NFT_LOG" || grep -Fq 'tachyon_rule_dns_only' "$NFT_LOG"; then
|
||||
fail "DNS action should not populate nft sets or fully routed rules"
|
||||
if grep -Fq '192.0.2.53' "$NFT_LOG" || grep -Fq 'tachyon_rule_dns_only' "$NFT_LOG"; then
|
||||
fail "DNS action should not populate routing nft sets"
|
||||
fi
|
||||
if grep -Fq $'mangle\tiifname\t@tachyon_interfaces\tip\tsaddr\t192.168.1.53' "$NFT_LOG"; then
|
||||
fail "DNS action should not create fully routed mangle rules"
|
||||
fi
|
||||
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tTachyonTable\ttachyon_dns_sources\t{ 192.168.1.53/32 }' "DNS action populates dns sources set"
|
||||
if [ "$(grep -F $'ip\tsaddr\t192.168.1.20/32' "$NFT_LOG" | wc -l | tr -d ' ')" != "3" ]; then
|
||||
fail "duplicate fully routed source should insert exactly one tcp/udp/local rule set"
|
||||
fi
|
||||
@@ -670,6 +674,8 @@ bypass
|
||||
198.51.100.1,203.0.113.0/24
|
||||
[rule.text_rule.source_ip_cidr]
|
||||
|
||||
[rule.text_rule.source_aware_dns]
|
||||
1
|
||||
[rule.text_rule.ports]
|
||||
443,80,443-444
|
||||
[rule.text_rule.fully_routed_ips]
|
||||
@@ -696,6 +702,8 @@ proxy
|
||||
10.0.0.0/8,192.0.2.1
|
||||
[rule.default_enabled.source_ip_cidr]
|
||||
|
||||
[rule.default_enabled.source_aware_dns]
|
||||
1
|
||||
[rule.default_enabled.ports]
|
||||
53,853
|
||||
[rule.default_enabled.fully_routed_ips]
|
||||
@@ -756,6 +764,8 @@ bypass
|
||||
10.0.0.0/8,192.0.2.1
|
||||
[rule.enabled.source_ip_cidr]
|
||||
|
||||
[rule.enabled.source_aware_dns]
|
||||
1
|
||||
[rule.enabled.ports]
|
||||
443,80,443-444
|
||||
[rule.enabled.fully_routed_ips]
|
||||
|
||||
@@ -86,4 +86,33 @@ ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
grep -Fq '"download_detour": "first_proxy-out"' "$output2" || \
|
||||
fail "global download_lists_via_proxy without explicit section must default to first enabled proxy section"
|
||||
|
||||
# 3. Verify http_clients and default_http_client handling:
|
||||
# Omitted on sing-box < 1.14 (1.12, 1.13, Leadaxe, Extended) to avoid unknown field crash
|
||||
output_v13="$WORK_DIR/out_v13.json"
|
||||
mkdir -p "$output_v13.section-cache" "$output_v13.rulesets"
|
||||
printf 'v1.13.5\n' > "$WORK_DIR/sb_v13"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v13" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture.json" "$output_v13" "127.0.0.1" "0" "1"
|
||||
|
||||
if grep -q '"http_clients"' "$output_v13"; then
|
||||
fail "sing-box < 1.14 must not have http_clients (causes fatal unknown field crash)"
|
||||
fi
|
||||
if grep -q '"default_http_client"' "$output_v13"; then
|
||||
fail "sing-box < 1.14 must not have default_http_client (causes fatal unknown field crash)"
|
||||
fi
|
||||
|
||||
# Included on sing-box 1.14+
|
||||
output_v14="$WORK_DIR/out_v14.json"
|
||||
mkdir -p "$output_v14.section-cache" "$output_v14.rulesets"
|
||||
printf 'v1.14.0\n' > "$WORK_DIR/sb_v14"
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/sb_v14" \
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$WORK_DIR/fixture.json" "$output_v14" "127.0.0.1" "0" "1"
|
||||
|
||||
grep -q '"http_clients"' "$output_v14" || \
|
||||
fail "sing-box 1.14+ must include http_clients"
|
||||
grep -q '"default_http_client": "ruleset-http"' "$output_v14" || \
|
||||
fail "sing-box 1.14+ must configure default_http_client"
|
||||
|
||||
printf "ruleset download detour checks passed\n"
|
||||
|
||||
+33
-4
@@ -97,15 +97,26 @@ cat >"$WORK_DIR/awg_fixture.json" <<'JSON'
|
||||
"awg_rekey_timeout": "5-6",
|
||||
"awg_reject_after_time": "171-193",
|
||||
"awg_keepalive_timeout": "11-18",
|
||||
"awg_max_handshake_attempts": "15-24"
|
||||
"awg_max_handshake_attempts": "15-24",
|
||||
"awg_random_trailers": "1",
|
||||
"awg_disable_cookies": "1"
|
||||
}
|
||||
],
|
||||
"section": [
|
||||
{
|
||||
".name": "default_direct",
|
||||
".name": "my_awg_section",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "bypass"
|
||||
"action": "awg",
|
||||
"awg_version": "3.1",
|
||||
"awg_private_key": "cHJpdmF0ZV9rZXk=",
|
||||
"awg_peer_public_key": "cGVlcl9wdWJsaWNfa2V5",
|
||||
"awg_server_address": "1.2.3.4",
|
||||
"awg_server_port": "51820",
|
||||
"awg_local_address": ["10.0.0.2/32"],
|
||||
"awg_keepalive": "25-35",
|
||||
"awg_random_trailers": "1",
|
||||
"awg_disable_cookies": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -114,6 +125,10 @@ JSON
|
||||
mkdir -p "$WORK_DIR/out.section-cache"
|
||||
out_json="$WORK_DIR/singbox_awg.json"
|
||||
|
||||
# Test 1: Leadaxe variant via variant state file
|
||||
printf 'lx\n' > "$WORK_DIR/sing-box-variant"
|
||||
SB_VARIANT_STATE_FILE="$WORK_DIR/sing-box-variant" \
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/nonexistent" \
|
||||
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/singbox/generator.uc" generate-config-fixture \
|
||||
"$WORK_DIR/awg_fixture.json" "$out_json" "127.0.0.1" 0 0
|
||||
|
||||
@@ -135,5 +150,19 @@ grep -q '"rekey_timeout": "5-6"' "$out_json" || fail "AWG 3.1 server missing rek
|
||||
grep -q '"reject_after_time": "171-193"' "$out_json" || fail "AWG 3.1 server missing reject_after_time"
|
||||
grep -q '"keepalive_timeout": "11-18"' "$out_json" || fail "AWG 3.1 server missing keepalive_timeout"
|
||||
grep -q '"max_handshake_attempts": "15-24"' "$out_json" || fail "AWG 3.1 server missing max_handshake_attempts"
|
||||
grep -q '"random_trailers": true' "$out_json" || fail "AWG 3.1 server missing random_trailers"
|
||||
grep -q '"disable_cookies": true' "$out_json" || fail "AWG 3.1 server missing disable_cookies"
|
||||
|
||||
printf 'AWG 2.0, 3.0, and 3.1 server configuration checks passed\n'
|
||||
# Check AWG 3.1 outbound on Leadaxe (range keepalive passed through, trailers/cookies present)
|
||||
grep -q '"persistent_keepalive_interval": "25-35"' "$out_json" || fail "Leadaxe AWG outbound missing range keepalive"
|
||||
|
||||
# Test 2: Extended/Standard variant (range keepalive must degrade safely to integer 25)
|
||||
printf 'extended\n' > "$WORK_DIR/sing-box-variant"
|
||||
SB_VARIANT_STATE_FILE="$WORK_DIR/sing-box-variant" \
|
||||
SB_VERSION_STATE_FILE="$WORK_DIR/nonexistent" \
|
||||
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/singbox/generator.uc" generate-config-fixture \
|
||||
"$WORK_DIR/awg_fixture.json" "$out_json" "127.0.0.1" 0 0
|
||||
|
||||
grep -q '"persistent_keepalive_interval": 25' "$out_json" || fail "Non-lx AWG outbound must safely fallback to integer keepalive"
|
||||
|
||||
printf 'AWG 2.0, 3.0, and 3.1 server and outbound configuration checks passed\n'
|
||||
|
||||
Reference in New Issue
Block a user