- Fix response parsing: check report field, not just message - Pass user query text to LLM instead of ignoring it - Append user question to diagnostic prompt with system context - Increase timeout to 120s for LM Studio responses - Add command_status_from_args to core/common.uc exports Closes #38
152 lines
5.9 KiB
Ucode
152 lines
5.9 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let common = require("core.common");
|
|
let connections = require("config.connections");
|
|
let runtime_constants = require("singbox.constants");
|
|
|
|
let option = common.option;
|
|
let bool_option = common.bool_option;
|
|
let as_string = common.as_string;
|
|
let bool_value = common.bool_value;
|
|
|
|
function normalize_port_entry(value) {
|
|
value = trim(as_string(value));
|
|
if (value == "" || match(value, /^[0-9]+$/) == null)
|
|
return null;
|
|
let number = int(value);
|
|
return number >= 1 && number <= 65535 ? number : null;
|
|
}
|
|
|
|
// P2P leak protection can be pinned down to the torrent client's own ports so
|
|
// nothing sneaks past the protocol sniffer. Each entry is "proto" or
|
|
// "proto:port" or "proto:port-port" (tcp/udp, comma separated). Without a
|
|
// port the whole protocol is routed direct.
|
|
function p2p_direct_rules(settings) {
|
|
let rules = [];
|
|
for (let entry in split(option(settings, "p2p_ports", ""), ",")) {
|
|
entry = trim(entry);
|
|
if (entry == "")
|
|
continue;
|
|
let proto = "tcp";
|
|
let port_spec = "";
|
|
let colon = index(entry, ":");
|
|
if (colon >= 0) {
|
|
proto = trim(substr(entry, 0, colon));
|
|
port_spec = trim(substr(entry, colon + 1));
|
|
} else {
|
|
proto = entry;
|
|
}
|
|
if (proto != "tcp" && proto != "udp")
|
|
continue;
|
|
let rule = { protocol: [ proto ], action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG };
|
|
if (port_spec != "") {
|
|
let dash = index(port_spec, "-");
|
|
if (dash >= 0) {
|
|
let start = normalize_port_entry(substr(port_spec, 0, dash));
|
|
let end = normalize_port_entry(substr(port_spec, dash + 1));
|
|
if (start != null && end != null && start <= end)
|
|
rule.source_port_range = [ sprintf("%d:%d", start, end) ];
|
|
else
|
|
continue;
|
|
} else {
|
|
let port = normalize_port_entry(port_spec);
|
|
if (port != null)
|
|
rule.source_port = port;
|
|
else
|
|
continue;
|
|
}
|
|
}
|
|
push(rules, rule);
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
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" && type(runtime.dns_health_inbounds) == "array")
|
|
for (let inbound in runtime.dns_health_inbounds)
|
|
push(sniff_inbounds, inbound);
|
|
let result = {
|
|
rules: [
|
|
{ action: "sniff", inbound: sniff_inbounds },
|
|
{ action: "hijack-dns", port: 53 },
|
|
{ action: "hijack-dns", protocol: "dns" }
|
|
],
|
|
rule_set: [],
|
|
final: runtime_constants.DIRECT_OUTBOUND_TAG,
|
|
auto_detect_interface: output_network_interface == "" && !mwan3_active,
|
|
default_domain_resolver: type(runtime) == "object" && as_string(runtime.default_domain_resolver) != ""
|
|
? as_string(runtime.default_domain_resolver)
|
|
: runtime_constants.DNS_SERVER_TAG,
|
|
default_mark: runtime_constants.OUTBOUND_MARK
|
|
};
|
|
|
|
if (output_network_interface != "")
|
|
result.default_interface = output_network_interface;
|
|
if (bool_option(settings, "disable_quic", false))
|
|
push(result.rules, { action: "reject", inbound: runtime_constants.TPROXY_INBOUND_TAG, protocol: "quic" });
|
|
if (bool_option(settings, "isolate_p2p", false)) {
|
|
push(result.rules, { protocol: "bittorrent", action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG });
|
|
for (let rule in p2p_direct_rules(settings))
|
|
push(result.rules, rule);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function target(section, outbound_tag_name) {
|
|
let action = option(section, "action", "");
|
|
if (connections.is_connections_action(action) ||
|
|
action == "awg" || action == "warp" ||
|
|
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
|
|
action == "masque" || action == "openvpn" ||
|
|
action == "byedpi" || action == "zapret" || action == "zapret2")
|
|
return { action: "route", outbound: outbound_tag_name };
|
|
if (action == "bypass")
|
|
return { action: "route", outbound: runtime_constants.BYPASS_OUTBOUND_TAG };
|
|
if (action == "block")
|
|
return { action: "reject" };
|
|
if (action == "hosts" || action == "dns")
|
|
return { action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG };
|
|
return { unsupported: "unsupported action " + action };
|
|
}
|
|
|
|
function has_resolve_matchers(rule) {
|
|
return rule.domain != null || rule.domain_suffix != null || rule.domain_keyword != null ||
|
|
rule.domain_regex != null || rule.rule_set != null;
|
|
}
|
|
|
|
function resolve_rule_for_section(section, route_rule) {
|
|
let action = option(section, "action", "");
|
|
let should_resolve = action == "byedpi" ||
|
|
(connections.is_connections_action(action) &&
|
|
bool_option(section, "resolve_real_ip_for_routing", false));
|
|
|
|
if (!should_resolve)
|
|
return null;
|
|
|
|
if (!has_resolve_matchers(route_rule)) {
|
|
return {
|
|
warning: "Resolve real IP is enabled for '" + section[".name"] + "', but no domain or rule-set matchers found"
|
|
};
|
|
}
|
|
|
|
let resolve_rule = {};
|
|
for (let key in [ "inbound", "source_ip_cidr", "domain", "domain_suffix", "domain_keyword", "domain_regex", "rule_set", "port", "port_range" ]) {
|
|
if (route_rule[key] != null)
|
|
resolve_rule[key] = route_rule[key];
|
|
}
|
|
resolve_rule.action = "resolve";
|
|
resolve_rule.server = runtime_constants.DNS_SERVER_TAG;
|
|
|
|
return { rule: resolve_rule };
|
|
}
|
|
|
|
return {
|
|
config,
|
|
target,
|
|
has_resolve_matchers,
|
|
resolve_rule_for_section
|
|
}; |