🎨 Sync 2026-06-04 22:17:22

This commit is contained in:
github-actions[bot] committed 2026-06-04 22:17:22 +08:00
1 parent d4045244be
commit e50963f221
71 files changed
+15700 -6848

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

@@ -0,0 +1,939 @@
#!/usr/bin/lua
require "nixio.fs"
require "luci.model.uci"
local ok_lyaml, lyaml = pcall(require, "lyaml")
if not ok_lyaml then
io.stderr:write("lyaml_not_found\n")
os.exit(2)
end
local uci = require "luci.model.uci".cursor()
local ok_jsonc, jsonc = pcall(require, "luci.jsonc")
local function read_file(path)
local data = nixio.fs.readfile(path)
if not data or data == "" then
return nil
end
return data
end
local function write_file(path, data)
return nixio.fs.writefile(path, data)
end
local function load_yaml(path)
local raw = read_file(path)
if not raw then
return nil, "read_failed"
end
local ok, parsed = pcall(lyaml.load, raw)
if not ok or type(parsed) ~= "table" then
return nil, "parse_failed"
end
return parsed
end
local function dump_yaml(path, data)
local ok, rendered = pcall(lyaml.dump, { data })
if not ok or not rendered then
return nil, "dump_failed"
end
write_file(path, rendered)
return true
end
local function split_filter_words(text)
local items = {}
for part in tostring(text or ""):gmatch("[^/]+") do
if part ~= "" then
items[#items + 1] = part
end
end
return items
end
local function trim(value)
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end
local function parse_csv_line(line)
local cols = {}
local cur = ""
local in_quote = false
local i = 1
while i <= #line do
local ch = line:sub(i, i)
if ch == '"' then
if in_quote and line:sub(i + 1, i + 1) == '"' then
cur = cur .. '"'
i = i + 1
else
in_quote = not in_quote
end
elseif ch == "," and not in_quote then
cols[#cols + 1] = cur
cur = ""
else
cur = cur .. ch
end
i = i + 1
end
cols[#cols + 1] = cur
return cols
end
local function read_clash_client_rules_csv(sid)
local rows = {}
sid = trim(sid)
if sid == "" then
return rows
end
local csv_path = string.format("/etc/ssrplus/clash/%s.csv", sid)
local raw = read_file(csv_path)
if not raw or raw == "" then
return rows
end
local first = true
for line in tostring(raw):gsub("\r", ""):gmatch("[^\n]+") do
local text = trim(line)
if text ~= "" then
if first and text:lower() == "enabled,client,policy,remarks,client_mac" then
first = false
else
local cols = parse_csv_line(line)
if #cols >= 4 then
rows[#rows + 1] = {
enabled = cols[1],
ip_addr = trim(cols[2] or ""),
policy_group = trim(cols[3] or ""),
remarks = trim(cols[4] or ""),
client_mac = trim(cols[5] or "")
}
end
end
end
end
return rows
end
local function has_proxy_sections(doc)
return type(doc.proxies) == "table" or type(doc["proxy-providers"]) == "table"
end
local function validate(path)
local doc = load_yaml(path)
if not doc then
return false
end
return has_proxy_sections(doc)
end
local function filter(path, filter_words)
local doc, err = load_yaml(path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
local words = split_filter_words(filter_words)
if #words == 0 then
return true
end
local removed = {}
local proxies = {}
for _, proxy in ipairs(doc.proxies or {}) do
local name = tostring(proxy.name or "")
local matched = false
for _, word in ipairs(words) do
if name:find(word, 1, true) then
matched = true
removed[name] = true
break
end
end
if not matched then
proxies[#proxies + 1] = proxy
end
end
doc.proxies = proxies
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group.proxies) == "table" then
local kept = {}
for _, name in ipairs(group.proxies) do
if not removed[tostring(name)] then
kept[#kept + 1] = name
end
end
group.proxies = kept
end
end
local count = 0
for _ in pairs(removed) do
count = count + 1
end
dump_yaml(path, doc)
io.stdout:write(tostring(count), "\n")
return true
end
local function deep_merge(dst, src)
if type(dst) ~= "table" or type(src) ~= "table" then
return src
end
for k, v in pairs(src) do
if type(v) == "table" and type(dst[k]) == "table" then
dst[k] = deep_merge(dst[k], v)
else
dst[k] = v
end
end
return dst
end
local function strip_runtime_conflicts(doc)
doc.tun = nil
doc.listeners = nil
doc["redir-port"] = nil
doc["tproxy-port"] = nil
doc["socks-port"] = nil
doc["mixed-port"] = nil
doc.port = nil
doc["external-controller"] = nil
doc.secret = nil
doc["allow-lan"] = nil
if type(doc.dns) == "table" then
doc.dns["fake-ip-range"] = nil
doc.dns["fake-ip-filter"] = nil
end
end
local function group_requires_candidates(group)
local gtype = tostring(group and group.type or ""):lower()
return gtype == "select"
or gtype == "fallback"
or gtype == "load-balance"
or gtype == "url-test"
or gtype == "relay"
end
local function has_nonempty_sequence(value)
return type(value) == "table" and next(value) ~= nil
end
local function fill_empty_proxy_groups(doc)
local changed = 0
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group) == "table"
and group_requires_candidates(group)
and not has_nonempty_sequence(group.proxies)
and not has_nonempty_sequence(group.use)
then
group.proxies = { "DIRECT" }
changed = changed + 1
end
end
return changed
end
local function strip_incompatible_script_rules(doc)
local kept = {}
local removed = 0
local has_script_rule = false
for _, rule in ipairs(doc.rules or {}) do
local text = tostring(rule or "")
if text:match("^SCRIPT,") then
removed = removed + 1
else
kept[#kept + 1] = rule
if text:match("^SCRIPT,") then
has_script_rule = true
end
end
end
if removed > 0 then
doc.rules = kept
end
if not has_script_rule then
doc.script = nil
end
return removed
end
local function prepare(input_path, output_path)
local doc, err = load_yaml(input_path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
if not has_proxy_sections(doc) then
io.stderr:write("missing_proxy_sections\n")
return false
end
strip_runtime_conflicts(doc)
local filled_groups = fill_empty_proxy_groups(doc)
local stripped_rules = strip_incompatible_script_rules(doc)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
io.stdout:write(string.format("filled_groups=%d stripped_script_rules=%d\n", filled_groups, stripped_rules))
return true
end
local function merge(raw_path, overlay_path, output_path)
local raw_doc, raw_err = load_yaml(raw_path)
if not raw_doc then
io.stderr:write(raw_err or "parse_failed", "\n")
return false
end
local overlay_doc, overlay_err = load_yaml(overlay_path)
if not overlay_doc then
io.stderr:write(overlay_err or "parse_failed", "\n")
return false
end
strip_runtime_conflicts(raw_doc)
local filled_groups = fill_empty_proxy_groups(raw_doc)
local stripped_rules = strip_incompatible_script_rules(raw_doc)
local merged = deep_merge(raw_doc, overlay_doc)
local ok, rendered = pcall(lyaml.dump, { merged })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
io.stdout:write(string.format("filled_groups=%d stripped_script_rules=%d\n", filled_groups, stripped_rules))
return true
end
local function append_client_policy_rules(runtime_path, sid)
local doc, err = load_yaml(runtime_path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
local valid_policies = {}
for _, proxy in ipairs(doc.proxies or {}) do
if type(proxy) == "table" and proxy.name and proxy.name ~= "" then
valid_policies[tostring(proxy.name)] = true
end
end
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group) == "table" and group.name and group.name ~= "" then
valid_policies[tostring(group.name)] = true
end
end
local custom_rules = {}
for _, section in ipairs(read_clash_client_rules_csv(sid)) do
if tostring(section.enabled or "0") == "1" then
local ip_addr = tostring(section.ip_addr or "")
local policy_group = tostring(section.policy_group or "")
if ip_addr ~= "" and policy_group ~= "" and valid_policies[policy_group] then
if not ip_addr:find("/", 1, true) then
ip_addr = ip_addr .. "/32"
end
custom_rules[#custom_rules + 1] = string.format("SRC-IP-CIDR,%s,%s", ip_addr, policy_group)
end
end
end
if #custom_rules == 0 then
io.stdout:write("client_rules=0\n")
return true
end
local existing_rules = {}
for _, rule in ipairs(doc.rules or {}) do
local text = tostring(rule or "")
if not text:match("^SRC%-IP%-CIDR,") then
existing_rules[#existing_rules + 1] = rule
end
end
doc.rules = {}
for _, rule in ipairs(custom_rules) do
doc.rules[#doc.rules + 1] = rule
end
for _, rule in ipairs(existing_rules) do
doc.rules[#doc.rules + 1] = rule
end
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(runtime_path, rendered)
io.stdout:write(string.format("client_rules=%d\n", #custom_rules))
return true
end
local function bool_enabled(value)
return value == "1" or value == 1 or value == true or value == "true"
end
local function split_csv(value)
local items = {}
for part in tostring(value or ""):gmatch("[^,%s]+") do
items[#items + 1] = part
end
return items
end
local function get_server_field(sid, option, default)
local value = uci:get("shadowsocksr", sid, option)
if value == nil or value == "" then
return default
end
return value
end
local function get_filter_aaaa()
local value = uci:get_first("shadowsocksr", "global", "filter_aaaa", "1")
if value == nil or value == "" then
value = uci:get_first("shadowsocksr", "global", "mosdns_ipv6", "1")
end
return value
end
local function build_tuic_runtime_doc(sid, local_port, socks_port, mode)
local server = get_server_field(sid, "server", "")
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local tuic_ip = get_server_field(sid, "tuic_ip", "")
local tls_host = get_server_field(sid, "tls_host", "")
local ipstack_prefer = get_server_field(sid, "ipstack_prefer", "")
local dns_mode = uci:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local proxy = {
name = sid,
type = "tuic",
server = server,
port = server_port,
uuid = get_server_field(sid, "tuic_uuid", ""),
password = get_server_field(sid, "tuic_passwd", ""),
["udp-relay-mode"] = get_server_field(sid, "udp_relay_mode", "native"),
["congestion-controller"] = get_server_field(sid, "congestion_control", "cubic"),
["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0")),
["disable-sni"] = bool_enabled(get_server_field(sid, "disable_sni", "0")),
["reduce-rtt"] = bool_enabled(get_server_field(sid, "zero_rtt_handshake", "0"))
}
if tuic_ip ~= "" then
proxy.ip = tuic_ip
end
if tls_host ~= "" then
proxy.sni = tls_host
end
local alpn = split_csv(get_server_field(sid, "tuic_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
local heartbeat = tonumber(get_server_field(sid, "heartbeat", "0"))
if heartbeat and heartbeat > 0 then
proxy["heartbeat-interval"] = heartbeat * 1000
end
local timeout = tonumber(get_server_field(sid, "timeout", "0"))
if timeout and timeout > 0 then
proxy["request-timeout"] = timeout * 1000
end
local max_udp_packet_size = tonumber(get_server_field(sid, "tuic_max_package_size", "0"))
if max_udp_packet_size and max_udp_packet_size > 0 then
proxy["max-udp-relay-packet-size"] = max_udp_packet_size
end
if ipstack_prefer ~= "" then
proxy["ip-version"] = ipstack_prefer == "v6first" and "ipv6-prefer" or "ipv4-prefer"
end
local listen_port = tonumber(local_port)
local socks_listen = tonumber(socks_port)
local doc = {
["allow-lan"] = true,
["bind-address"] = "0.0.0.0",
mode = "rule",
["log-level"] = "silent",
["find-process-mode"] = "off",
["unified-delay"] = true,
["tcp-concurrent"] = true,
["routing-mark"] = 255,
proxies = { proxy },
["proxy-groups"] = {
{
name = "PROXY",
type = "select",
proxies = { sid }
}
},
rules = { "MATCH,PROXY" },
tun = { enable = false },
profile = { ["store-selected"] = true },
dns = {
enable = dns_mode == "7",
["enhanced-mode"] = "redir-host",
listen = "127.0.0.1:5335",
ipv6 = get_filter_aaaa() ~= "1"
}
}
if mode == "socks" then
doc["socks-port"] = listen_port
else
doc["redir-port"] = listen_port
doc["tproxy-port"] = listen_port
if socks_listen and socks_listen > 0 then
doc["socks-port"] = socks_listen
end
end
return doc
end
local function generate_tuic_runtime(sid, output_path, local_port, socks_port, mode)
local doc = build_tuic_runtime_doc(sid, local_port, socks_port, mode)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local function parse_plugin_opts(value)
local result = {}
for part in tostring(value or ""):gmatch("[^;]+") do
local key, val = part:match("^%s*([^=]+)=?(.*)%s*$")
if key and key ~= "" then
result[key] = val or ""
end
end
return result
end
local function split_host_port(value)
local text = tostring(value or "")
if text == "" then
return "", ""
end
local host, port = text:match("^%[(.-)%]:(%d+)$")
if host and port then
return host, port
end
host, port = text:match("^(.-):(%d+)$")
if host and port then
return host, port
end
return text, ""
end
local function bool_default(value, default)
if value == nil or value == "" then
return default
end
return bool_enabled(value)
end
local function number_or_nil(value)
if value == nil or value == "" then
return nil
end
return tonumber(value)
end
local function string_or_nil(value)
if value == nil or value == "" then
return nil
end
return tostring(value)
end
local function pick_plugin_opt(plugin_opts, ...)
for i = 1, select("#", ...) do
local key = select(i, ...)
local value = plugin_opts[key]
if value ~= nil and value ~= "" then
return value
end
end
return nil
end
local function parse_plugin_headers(plugin_opts)
local headers = {}
local raw_headers = pick_plugin_opt(plugin_opts, "headers", "header")
if raw_headers and ok_jsonc and jsonc then
local decoded = jsonc.parse(raw_headers)
if type(decoded) == "table" then
for key, value in pairs(decoded) do
headers[tostring(key)] = tostring(value)
end
end
end
if raw_headers and next(headers) == nil then
for part in tostring(raw_headers):gmatch("[^|,]+") do
local key, value = part:match("^%s*([^=:]+)%s*[:=]%s*(.-)%s*$")
if key and key ~= "" and value and value ~= "" then
headers[key] = value
end
end
end
for key, value in pairs(plugin_opts) do
local header_name = key:match("^headers[%.:](.+)$")
or key:match("^header[%.:](.+)$")
or key:match("^header_(.+)$")
if header_name and header_name ~= "" and value ~= "" then
headers[header_name] = value
end
end
return next(headers) and headers or nil
end
local function get_plugin_client_fingerprint(sid, plugin_opts)
return string_or_nil(
pick_plugin_opt(
plugin_opts,
"client-fingerprint",
"client_fingerprint",
"fingerprint"
) or get_server_field(sid, "fingerprint", "")
)
end
local function normalize_plugin_name(plugin)
local value = tostring(plugin or ""):lower()
if value == "" or value == "none" then
return ""
end
if value == "simple-obfs" then
return "obfs-local"
end
if value == "obfs" then
return "obfs-local"
end
if value == "shadowtls" then
return "shadow-tls"
end
if value == "gost" then
return "gost-plugin"
end
if value == "kcp-tun" then
return "kcptun"
end
return value
end
local function build_shadowsocks_plugin(proxy, sid)
local plugin = normalize_plugin_name(get_server_field(sid, "plugin", ""))
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", ""))
if plugin == "" then
return
end
if plugin == "obfs-local" then
proxy.plugin = "obfs"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "obfs", "mode") or "http",
host = string_or_nil(pick_plugin_opt(plugin_opts, "obfs-host", "obfs_host", "host"))
}
return
end
if plugin == "v2ray-plugin" or plugin == "xray-plugin" then
proxy.plugin = "v2ray-plugin"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "mode") or "websocket",
tls = bool_default(pick_plugin_opt(plugin_opts, "tls"), false),
fingerprint = string_or_nil(pick_plugin_opt(plugin_opts, "fingerprint")),
["skip-cert-verify"] = bool_default(pick_plugin_opt(plugin_opts, "skip-cert-verify", "skip_cert_verify", "insecure"), false),
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
path = string_or_nil(pick_plugin_opt(plugin_opts, "path")),
mux = bool_default(pick_plugin_opt(plugin_opts, "mux"), false),
headers = parse_plugin_headers(plugin_opts),
["v2ray-http-upgrade"] = bool_default(pick_plugin_opt(plugin_opts, "v2ray-http-upgrade", "v2ray_http_upgrade"), false)
}
return
end
if plugin == "gost-plugin" then
proxy.plugin = "gost-plugin"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "mode") or "websocket",
tls = bool_default(pick_plugin_opt(plugin_opts, "tls"), false),
fingerprint = string_or_nil(pick_plugin_opt(plugin_opts, "fingerprint")),
["skip-cert-verify"] = bool_default(pick_plugin_opt(plugin_opts, "skip-cert-verify", "skip_cert_verify", "insecure"), false),
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
path = string_or_nil(pick_plugin_opt(plugin_opts, "path")),
mux = bool_default(pick_plugin_opt(plugin_opts, "mux"), false),
headers = parse_plugin_headers(plugin_opts)
}
return
end
if plugin == "shadow-tls" then
local host, port = split_host_port(pick_plugin_opt(plugin_opts, "host") or "")
local version
if plugin_opts.v3 == "1" or plugin_opts.version == "3" then
version = 3
elseif plugin_opts.v2 == "1" or plugin_opts.version == "2" then
version = 2
elseif plugin_opts.v1 == "1" or plugin_opts.version == "1" then
version = 1
end
proxy.plugin = "shadow-tls"
proxy["client-fingerprint"] = get_plugin_client_fingerprint(sid, plugin_opts)
proxy["plugin-opts"] = {
host = host ~= "" and host or nil,
port = number_or_nil(port),
password = string_or_nil(pick_plugin_opt(plugin_opts, "passwd", "password")),
version = version
}
return
end
if plugin == "restls" then
proxy.plugin = "restls"
proxy["client-fingerprint"] = get_plugin_client_fingerprint(sid, plugin_opts)
proxy["plugin-opts"] = {
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
password = string_or_nil(pick_plugin_opt(plugin_opts, "passwd", "password")),
["version-hint"] = string_or_nil(pick_plugin_opt(plugin_opts, "version-hint", "version_hint")),
["restls-script"] = string_or_nil(pick_plugin_opt(plugin_opts, "restls-script", "restls_script"))
}
return
end
if plugin == "kcptun" then
proxy.plugin = "kcptun"
proxy["plugin-opts"] = {
key = string_or_nil(pick_plugin_opt(plugin_opts, "key", "passwd", "password")),
crypt = string_or_nil(pick_plugin_opt(plugin_opts, "crypt")),
mode = string_or_nil(pick_plugin_opt(plugin_opts, "mode")),
conn = number_or_nil(pick_plugin_opt(plugin_opts, "conn")),
autoexpire = number_or_nil(pick_plugin_opt(plugin_opts, "autoexpire")),
scavengettl = number_or_nil(pick_plugin_opt(plugin_opts, "scavengettl")),
mtu = number_or_nil(pick_plugin_opt(plugin_opts, "mtu")),
ratelimit = number_or_nil(pick_plugin_opt(plugin_opts, "ratelimit")),
sndwnd = number_or_nil(pick_plugin_opt(plugin_opts, "sndwnd")),
rcvwnd = number_or_nil(pick_plugin_opt(plugin_opts, "rcvwnd")),
datashard = number_or_nil(pick_plugin_opt(plugin_opts, "datashard")),
parityshard = number_or_nil(pick_plugin_opt(plugin_opts, "parityshard")),
dscp = number_or_nil(pick_plugin_opt(plugin_opts, "dscp")),
nocomp = bool_default(pick_plugin_opt(plugin_opts, "nocomp"), false),
acknodelay = bool_default(pick_plugin_opt(plugin_opts, "acknodelay"), false),
nodelay = number_or_nil(pick_plugin_opt(plugin_opts, "nodelay")),
interval = number_or_nil(pick_plugin_opt(plugin_opts, "interval")),
resend = number_or_nil(pick_plugin_opt(plugin_opts, "resend")),
sockbuf = number_or_nil(pick_plugin_opt(plugin_opts, "sockbuf")),
smuxver = number_or_nil(pick_plugin_opt(plugin_opts, "smuxver")),
smuxbuf = number_or_nil(pick_plugin_opt(plugin_opts, "smuxbuf")),
framesize = number_or_nil(pick_plugin_opt(plugin_opts, "framesize")),
streambuf = number_or_nil(pick_plugin_opt(plugin_opts, "streambuf")),
keepalive = number_or_nil(pick_plugin_opt(plugin_opts, "keepalive"))
}
return
end
proxy.plugin = plugin
if next(plugin_opts) then
proxy["plugin-opts"] = plugin_opts
end
end
local function build_kcptun_plugin(proxy, sid)
if not bool_enabled(get_server_field(sid, "kcp_enable", "0")) then
return
end
proxy.plugin = "kcptun"
proxy.port = tonumber(get_server_field(sid, "kcp_port", "0")) or proxy.port
proxy["plugin-opts"] = {
key = get_server_field(sid, "kcp_password", ""),
mode = "fast",
mtu = 1350
}
end
local function build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode)
local dns_mode = uci:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local server = get_server_field(sid, "server", "")
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local method = get_server_field(sid, "encrypt_method_ss", "none")
local password = get_server_field(sid, "password", "")
local proxy = {
name = sid,
type = "ss",
server = server,
port = server_port,
cipher = method,
password = password,
udp = true,
tfo = bool_enabled(get_server_field(sid, "fast_open", "0"))
}
if get_server_field(sid, "type", "") == "ss" then
build_kcptun_plugin(proxy, sid)
end
if proxy.plugin == nil then
build_shadowsocks_plugin(proxy, sid)
end
local doc = {
["allow-lan"] = true,
["bind-address"] = "0.0.0.0",
mode = "rule",
["log-level"] = "silent",
["find-process-mode"] = "off",
["unified-delay"] = true,
["tcp-concurrent"] = true,
["routing-mark"] = 255,
proxies = { proxy },
["proxy-groups"] = {
{
name = "PROXY",
type = "select",
proxies = { sid }
}
},
rules = { "MATCH,PROXY" },
tun = { enable = false },
profile = { ["store-selected"] = true },
dns = {
enable = dns_mode == "7",
["enhanced-mode"] = "redir-host",
listen = "127.0.0.1:5335",
ipv6 = get_filter_aaaa() ~= "1"
}
}
local listen_port = tonumber(local_port)
local socks_listen = tonumber(socks_port)
if mode == "socks" then
doc["socks-port"] = listen_port
else
doc["redir-port"] = listen_port
doc["tproxy-port"] = listen_port
if socks_listen and socks_listen > 0 then
doc["socks-port"] = socks_listen
end
end
return doc
end
local function generate_shadowsocks_runtime(sid, output_path, local_port, socks_port, mode)
local doc = build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local function build_shadowsocks_server_doc(sid)
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local method = get_server_field(sid, "encrypt_method_ss", "aes-128-gcm")
local password = get_server_field(sid, "password", "")
local listener = {
name = sid,
type = "shadowsocks",
listen = "::",
port = server_port,
cipher = method,
password = password,
udp = true,
tfo = bool_enabled(get_server_field(sid, "fast_open", "0"))
}
local plugin = normalize_plugin_name(get_server_field(sid, "plugin", ""))
if plugin == "obfs-local" then
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", ""))
listener.obfs = plugin_opts.obfs or plugin_opts.mode or "http"
listener.obfs_opts = {
mode = plugin_opts.obfs or plugin_opts.mode or "http",
host = plugin_opts["obfs-host"] or plugin_opts.obfs_host or plugin_opts.host or nil
}
end
return {
["allow-lan"] = true,
["bind-address"] = "*",
["log-level"] = "silent",
["find-process-mode"] = "off",
listeners = { listener }
}
end
local function generate_shadowsocks_server(sid, output_path)
local doc = build_shadowsocks_server_doc(sid)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local action = arg[1]
if action == "validate" then
os.exit(validate(arg[2]) and 0 or 1)
elseif action == "filter" then
os.exit(filter(arg[2], arg[3]) and 0 or 1)
elseif action == "prepare" then
os.exit(prepare(arg[2], arg[3]) and 0 or 1)
elseif action == "merge" then
os.exit(merge(arg[2], arg[3], arg[4]) and 0 or 1)
elseif action == "append_client_policy_rules" then
os.exit(append_client_policy_rules(arg[2], arg[3]) and 0 or 1)
elseif action == "tuic" then
os.exit(generate_tuic_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss" then
os.exit(generate_shadowsocks_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss_server" then
os.exit(generate_shadowsocks_server(arg[2], arg[3]) and 0 or 1)
else
io.stderr:write("usage: clash_yaml.lua validate <yaml> | filter <yaml> <words> | prepare <input> <output> | merge <raw> <overlay> <output> | append_client_policy_rules <runtime_yaml> <sid> | tuic <sid> <output> <local_port> [socks_port] [mode] | ss <sid> <output> <local_port> [socks_port] [mode] | ss_server <sid> <output>\n")
os.exit(1)
end
@@ -2,7 +2,6 @@
require "luci.sys"
local ucursor = require "luci.model.uci".cursor()
local datatypes = require "luci.cbi.datatypes"
local json = require "luci.jsonc"
local server_section = arg[1]
@@ -12,12 +11,6 @@ local socks_port = arg[4] or "0"
local chain = arg[5] or "0"
local GLOBAL = {
DNS_SERVER = {},
DNS_HOSTNAME = {},
VPS_EXCLUDE = {}
}
-- 辅助函数:拆分字符串(若 luci.util 未加载则定义)
local function split(str, pat)
local t = {}
@@ -38,27 +31,52 @@ local server = ucursor:get_all("shadowsocksr", server_section)
local socks_server = ucursor:get_all("shadowsocksr", "@socks5_proxy[0]") or {}
local xray_fragment = ucursor:get_all("shadowsocksr", "@global_xray_fragment[0]") or {}
local xray_noise = ucursor:get_all("shadowsocksr", "@xray_noise_packets[0]") or {}
local default_node_local_port = ucursor:get_first("shadowsocksr", "global", "default_node_local_port", "1234")
local dns_mode = ucursor:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local dns_ipv4_only = ucursor:get_first("shadowsocksr", "global", "filter_aaaa")
if not dns_ipv4_only or dns_ipv4_only == "" then
dns_ipv4_only = ucursor:get_first("shadowsocksr", "global", "mosdns_ipv6", "1")
end
local builtin_dns_server = ucursor:get_first("shadowsocksr", "global", "tunnel_forward", "8.8.4.4:53")
local outbound_settings = nil
local xray_version = nil
local xray_version_val = 0
local xray_builtin_dns = nil
local node_id = server_section
local remarks = server.alias or ""
local b64decode = nixio.bin.b64decode
local b64encode = nixio.bin.b64encode
local effective_node_local_port = tonumber(server.local_port) or tonumber(default_node_local_port) or 1234
-- 解析 URL(简单实现,仅用于 DoH)
local function parseURL(url)
if not url then return nil end
local schema, rest = url:match("^(https?)://(.*)$")
if not schema then return nil end
local host, port_str = rest:match("^([^:]+):?(%d*)/?.*$")
local port = tonumber(port_str) or (schema == "https" and 443 or 80)
return { host = host, port = port, schema = schema }
if server.type == "ss-rust" then
server.type = "ss"
end
if server.type == "ss-rust" or server.type == "ss-libev" then
server.type = "ss"
local function parse_realm_uri(uri)
if type(uri) ~= "string" then return nil end
-- realm://token@server/realm_id?query
local token, server_url, realm_id, query = trim(uri):match("^realm://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not token or not server_url or not realm_id then return nil end
realm_id = realm_id:gsub("/+$", "")
local realm = {
token = token,
server_url = server_url,
realm_id = realm_id
}
-- 解析 query 中的 stun=
if query and query ~= "" then
local stun_servers = {}
for key, value in query:gmatch("([^&=?]+)=([^&]+)") do
if key == "stun" and value ~= "" then
stun_servers[#stun_servers + 1] = value
end
end
if #stun_servers > 0 then
realm.stun_servers = stun_servers
end
end
return realm
end
-- base64 解码
@@ -120,9 +138,25 @@ local function cleanEmptyTables(t)
return next(t) and t or nil
end
local function format_host(host)
host = tostring(host or "")
if host ~= "" and host:find(":", 1, true) and not host:match("^%[.*%]$") then
return "[" .. host .. "]"
end
return host
end
local function format_host_port(host, port)
host = format_host(host)
if port == nil or port == "" then
return host
end
return host .. ":" .. tostring(port)
end
-- 确保正确判断程序是否存在
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
-- 获取 Xray 版本号
@@ -203,20 +237,33 @@ function wireguard()
-- 处理 reserved 字段,支持逗号分隔的数字或 Base64 编码
local reserved = nil
if server.reserved then
local bytes = {}
if not server.reserved:match("[^%d,]+") then
-- 纯数字和逗号,解析为数字列表
server.reserved:gsub("%d+", function(b)
bytes[#bytes + 1] = tonumber(b)
end)
else
-- Base64 编码的二进制数据
local result = base64Decode(server.reserved)
for i = 1, #result do
bytes[i] = result:byte(i)
local all_bytes = {}
local reserved_values = server.reserved
-- 确保是 table 类型
if type(reserved_values) ~= "table" then
reserved_values = {reserved_values}
end
for _, reserved_str in ipairs(reserved_values) do
if type(reserved_str) == "string" then
if not reserved_str:match("[^%d,]+") then
-- 数字和逗号格式
reserved_str:gsub("%d+", function(b)
all_bytes[#all_bytes + 1] = tonumber(b)
end)
else
-- Base64 格式
local result = base64Decode(reserved_str)
if result then
for i = 1, #result do
all_bytes[#all_bytes + 1] = result:byte(i)
end
end
end
end
end
reserved = #bytes > 0 and bytes or nil
reserved = #all_bytes > 0 and all_bytes or nil
end
outbound_settings = {
@@ -226,15 +273,22 @@ function wireguard()
{
publicKey = server.peer_pubkey,
preSharedKey = server.preshared_key,
endpoint = server.server .. ":" .. server.server_port,
endpoint = format_host_port(server.server, server.server_port),
keepAlive = tonumber(server.keepaliveperiod),
allowedIPs = (server.allowedips) or nil,
}
},
noKernelTun = (server.kernelmode == "1") and true or false,
kernelMode = (server.kernelmode == "1") and true or false,
reserved = reserved,
mtu = tonumber(server.mtu)
}
if server.finalmask and server.finalmask ~= "" then
local ok, fm = pcall(json.parse, base64Decode(server.finalmask))
if ok and type(fm) == "table" then
outbound_settings.streamSettings = outbound_settings.streamSettings or {}
outbound_settings.streamSettings.finalmask = fm
end
end
end
function xray_hysteria2()
outbound_settings = {
@@ -299,6 +353,35 @@ local Xray = {
-- 初始化 outbounds 表
outbounds = {},
}
if server.type == "v2ray" and dns_mode == "7" and os.getenv("SSR_SWITCH_PROBE") ~= "1" then
local dns_host = builtin_dns_server:match("^([^:]+)") or "8.8.4.4"
local dns_port = tonumber(builtin_dns_server:match(":(%d+)$")) or 53
Xray.dns = {
queryStrategy = (dns_ipv4_only == "1") and "UseIPv4" or "UseIP",
servers = {
string.format("tcp://%s:%d", dns_host, dns_port)
}
}
table.insert(Xray.inbounds, {
listen = "127.0.0.1",
port = 5335,
protocol = "dokodemo-door",
settings = {
address = dns_host,
port = dns_port,
network = "tcp,udp"
},
tag = "builtin-dns-in"
})
xray_builtin_dns = {
address = dns_host,
port = dns_port
}
end
-- 传入连接
-- 添加 dokodemo-door 配置,如果 local_port 不为 0
if local_port ~= "0" then
@@ -349,11 +432,11 @@ if proto and proto:find("tcp") and socks_port ~= "0" then
-- socks
protocol = "socks",
port = tonumber(socks_port),
settings = {
settings = {
auth = socks_server.socks5_auth or "noauth",
udp = true,
mixed = ((socks_server.socks5_mixed == '1') and true or false) or (socks_server.server == 'same') and nil,
accounts = (socks_server.server ~= "same" and (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth")) and {
mixed = ((socks_server.socks5_mixed == '1') and true or false) or nil,
accounts = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth") and {
{
user = socks_server.socks5_user,
pass = socks_server.socks5_pass
@@ -529,7 +612,7 @@ Xray.outbounds = {
local finalmask = {}
local PT = server.v2ray_protocol
local TP = server.transport
if server.transport == "kcp" then
if TP == "kcp" then
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
local udp = {}
@@ -547,14 +630,36 @@ Xray.outbounds = {
udp[#udp+1] = c
finalmask.udp = udp
elseif PT == "hysteria2" then
local udp = {}
if (server.flag_obfs == "1" and (server.obfs_type and server.obfs_type ~= "")) then
finalmask.udp = {{
type = server.obfs_type,
local o = {
type = "salamander",
settings = server.salamander and {
password = server.salamander
password = server.salamander,
packetSize = server.obfs_type == "gecko" and "512-1200" or nil
} or nil
}}
}
udp[#udp+1] = o
end
if server.hysteria2_realms then
local realm = parse_realm_uri(server.hysteria2_realm_url)
local url, stun
if realm then
if realm.token and realm.server_url and realm.realm_id then
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
stun = realm.stun_servers or server.hysteria2_realm_stun
end
local r = {
type = "realm",
settings = {
url = url,
stunServers = stun
}
}
udp[#udp+1] = r
end
finalmask.udp = udp
local up = tonumber(server.uplink_capacity) or 0
local down = tonumber(server.downlink_capacity) or 0
finalmask.quicParams = {
@@ -642,7 +747,6 @@ Xray.outbounds = {
end)(),
sockopt = {
mark = 255,
domainStrategy = server.domain_strategy or "UseIP",
tcpFastOpen = (function()
if server.transport == "xhttp" then
return (server.fast_open == "1") and true or false
@@ -670,18 +774,24 @@ Xray.outbounds = {
}
}
table.insert(Xray.outbounds, {
protocol = "freedom",
tag = "direct",
settings = {
domainStrategy = server.domain_strategy or "UseIP" -- 可根据需要改为 direct_dns_query_strategy
},
streamSettings = {
sockopt = {
mark = 255
if xray_builtin_dns then
table.insert(Xray.outbounds, {
protocol = "dns",
tag = "builtin-dns-out",
settings = {
network = "tcp",
address = xray_builtin_dns.address,
port = xray_builtin_dns.port
}
}
})
})
Xray.routing = Xray.routing or {}
Xray.routing.rules = Xray.routing.rules or {}
table.insert(Xray.routing.rules, {
type = "field",
inboundTag = { "builtin-dns-in" },
outboundTag = "builtin-dns-out"
})
end
-- 添加带有 fragment 设置的 dialerproxy 配置
if xray_fragment.fragment ~= "0" or (xray_fragment.noise ~= "0" and xray_noise.enabled ~= "0") then
@@ -712,121 +822,6 @@ if xray_fragment.fragment ~= "0" or (xray_fragment.noise ~= "0" and xray_noise.e
})
end
-- Xray DNS 解析配置
if datatypes.hostname(server.server) and server.domain_resolver and (server.domain_resolver_dns or server.domain_resolver_dns_https) then
-- 解析 DNS 服务器配置
local dns_proto = server.domain_resolver
local config_address
local config_port
if dns_proto == "https" then
local _a = parseURL(server.domain_resolver_dns_https)
if _a then
config_address = server.domain_resolver_dns_https
config_port = _a.port or 443
if _a.hostname and datatypes.hostname(_a.hostname) then
GLOBAL.DNS_HOSTNAME[_a.hostname] = true
end
end
else
local server_address = server.domain_resolver_dns
config_port = 53
local parts = split(server_address, ":")
if #parts > 1 then
server_address = parts[1]
config_port = tonumber(parts[#parts])
end
config_address = server_address
if dns_proto == "tcp" then
config_address = dns_proto .. "://" .. server_address .. ":" .. config_port
end
end
-- 存入 GLOBAL.DNS_SERVER(去重)
local dns_key = dns_proto .. "|" .. config_address .. "|" .. tostring(config_port)
if not GLOBAL.DNS_SERVER[dns_key] then
GLOBAL.DNS_SERVER[dns_key] = {
tag = "dns-node-" .. node_id,
address = config_address,
port = config_port,
finalQuery = true,
disableCache = false,
serveStale = true,
serveExpiredTTL = 30,
domains = {}
}
end
-- 添加当前节点域名到该 DNS 服务器的 domains 列表
local domain = "full:" .. server.server
local exists
for _, d in ipairs(GLOBAL.DNS_SERVER[dns_key].domains) do
if d == domain then exists = true; break end
end
if not exists then
table.insert(GLOBAL.DNS_SERVER[dns_key].domains, domain)
end
GLOBAL.VPS_EXCLUDE[server.server] = true
-- 构建 Xray.dns
local dns_servers = { "localhost" }
for key, dns_server in pairs(GLOBAL.DNS_SERVER) do
table.insert(dns_servers, {
tag = dns_server.tag,
address = dns_server.address,
port = dns_server.port,
domains = dns_server.domains,
finalQuery = dns_server.finalQuery,
serveStale = dns_server.serveStale,
serveExpiredTTL = dns_server.serveExpiredTTL,
disableCache = dns_server.disableCache
})
end
Xray.dns = {
servers = dns_servers,
disableFallback = true,
disableFallbackIfMatch = true,
useSystemHosts = true,
queryStrategy = "UseIP",
disableCache = false,
tag = "dns-global" -- 用于 routing
}
end
-- 代理出站的 tag(与 outbound 中的 tag 保持一致)
local proxy_tag = (remarks ~= nil and remarks ~= "") and (node_id .. ":" .. remarks) or node_id
-- 构建 routing 规则表
local routing_rules = {}
-- 1. 为每个自定义 DNS 服务器添加直连规则(inboundTag 匹配 dns-node-xxx)
if GLOBAL.DNS_SERVER and next(GLOBAL.DNS_SERVER) then
for _, dns_server in pairs(GLOBAL.DNS_SERVER) do
table.insert(routing_rules, {
inboundTag = { dns_server.tag },
outboundTag = "direct"
})
end
end
-- 2. 添加全局 DNS 直连规则(需要 Xray.dns 中已设置 tag = "dns-global")
if Xray.dns and Xray.dns.tag then
table.insert(routing_rules, {
inboundTag = { "dns-global" },
outboundTag = "direct"
})
end
-- 3. 添加默认规则:所有 TCP/UDP 流量走代理
table.insert(routing_rules, {
network = "tcp,udp",
ruleTag = "default",
outboundTag = proxy_tag
})
-- 构建 routing 对象
Xray.routing = {
rules = routing_rules,
domainStrategy = "AsIs"
}
local cipher = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:AES128-SHA:AES256-SHA:DES-CBC3-SHA"
local cipher13 = "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384"
local trojan = {
@@ -874,7 +869,7 @@ local trojan = {
}
}
local naiveproxy = {
proxy = (server.username and server.password and server.server and server.server_port) and "https://" .. server.username .. ":" .. server.password .. "@" .. server.server .. ":" .. server.server_port,
proxy = (server.username and server.password and server.server and server.server_port) and "https://" .. server.username .. ":" .. server.password .. "@" .. format_host_port(server.server, server.server_port),
listen = (proto == "redir") and "redir" .. "://0.0.0.0:" .. tonumber(local_port) or "socks" .. "://0.0.0.0:" .. tonumber(local_port),
["insecure-concurrency"] = tonumber(server.concurrency) or 1
}
@@ -895,16 +890,16 @@ local hysteria2 = {
server.server_port and
(
server.port_range and
(server.server .. ":" .. server.server_port .. "," .. string.gsub(server.port_range, ":", "-"))
(format_host_port(server.server, server.server_port) .. "," .. string.gsub(server.port_range, ":", "-"))
or
(server.server .. ":" .. server.server_port)
(format_host_port(server.server, server.server_port))
)
or
(
server.port_range and
server.server .. ":" .. string.gsub(server.port_range, ":", "-")
format_host(server.server) .. ":" .. string.gsub(server.port_range, ":", "-")
or
server.server and server.server .. ":443"
server.server and format_host_port(server.server, "443")
)
),
bandwidth = (server.uplink_capacity or server.downlink_capacity) and {
@@ -1003,7 +998,7 @@ local hysteria2 = {
}
local shadowtls = {
client = {
server_addr = server.server_port and server.server .. ":" .. server.server_port or nil,
server_addr = server.server_port and format_host_port(server.server, server.server_port) or nil,
listen = "127.0.0.1:" .. tonumber(local_port),
tls_names = server.shadowtls_sni,
password = server.password
@@ -1017,7 +1012,7 @@ local chain_sslocal = {
locals = local_port ~= "0" and {
{
local_address = "0.0.0.0",
local_port = (chain_local_port == "0" and tonumber(server.local_port) or tonumber(chain_local_port)),
local_port = (chain_local_port == "0" and effective_node_local_port or tonumber(chain_local_port)),
mode = (proto:find("tcp,udp") and "tcp_and_udp") or proto .. "_only",
protocol = "redir",
tcp_redir = "redirect",
@@ -1046,7 +1041,7 @@ local chain_sslocal = {
local chain_vmess = {
inbounds = (local_port ~= "0") and {
{
port = (chain_local_port == "0" and tonumber(server.local_port) or tonumber(chain_local_port)),
port = (chain_local_port == "0" and effective_node_local_port or tonumber(chain_local_port)),
protocol = "dokodemo-door",
settings = {
network = proto,
@@ -1082,7 +1077,7 @@ local chain_vmess = {
}
local tuic = {
relay = {
server = server.server_port and server.server .. ":" .. server.server_port,
server = server.server_port and format_host_port(server.server, server.server_port),
ip = server.tuic_ip,
uuid = server.tuic_uuid,
password = server.tuic_passwd,
@@ -1,95 +0,0 @@
#!/bin/sh
argv1=$1
argv2=$2
argv3=$3
argv4=$4
argv5=$5
argv6=$6
argv7=$7
argv8=$8
argv9=$9
cat <<-EOF >$argv1
base {
log_debug = off;
log_info = off;
log = stderr;
daemon = on;
redirector = iptables;
reuseport = on;
}
EOF
tcp() {
if [ "$argv7" == "0" ]; then
cat <<-EOF >>$argv1
redsocks {
bind = "0.0.0.0:$argv4";
relay = "$argv5:$argv6";
type = socks5;
autoproxy = 0;
timeout = 10;
}
EOF
else
cat <<-EOF >>$argv1
redsocks {
bind = "0.0.0.0:$argv4";
relay = "$argv5:$argv6";
type = socks5;
autoproxy = 0;
timeout = 10;
login = "$argv8";
password = "$argv9";
}
EOF
fi
}
udp() {
if [ "$argv7" == "0" ]; then
cat <<-EOF >>$argv1
redudp {
bind = "0.0.0.0:$argv4";
relay = "$argv5:$argv6";
type = socks5;
udp_timeout = 10;
}
EOF
else
cat <<-EOF >>$argv1
redudp {
bind = "0.0.0.0:$argv4";
relay = "$argv5:$argv6";
type = socks5;
udp_timeout = 10;
login = "$argv8";
password = "$argv9";
}
EOF
fi
}
case "$argv2" in
socks5)
case "$argv3" in
tcp)
tcp
;;
udp)
udp
;;
*)
tcp
udp
;;
esac
;;
*)
cat <<-EOF >>$argv1
redsocks {
bind = "0.0.0.0:$argv4";
type = direct;
interface = $argv3;
autoproxy = 0;
timeout = 10;
}
EOF
;;
esac
@@ -18,38 +18,12 @@ case "$USE_TABLES" in
exit 1
;;
esac
netflix() {
local port="$1"
if [ -f "$TMP_DNSMASQ_PATH/gfw_list.conf" ] && [ -s /etc/ssrplus/netflix.list ]; then
grep -vE '^\s*#|^\s*$' /etc/ssrplus/netflix.list > /tmp/ssrplus_netflix.list.clean
if [ -s /tmp/ssrplus_netflix.list.clean ]; then
grep -v -F -f /tmp/ssrplus_netflix.list.clean "$TMP_DNSMASQ_PATH/gfw_list.conf" > "$TMP_DNSMASQ_PATH/gfw_list.conf.tmp"
mv "$TMP_DNSMASQ_PATH/gfw_list.conf.tmp" "$TMP_DNSMASQ_PATH/gfw_list.conf"
if [ -f "$TMP_DNSMASQ_PATH/gfw_base.conf" ]; then
grep -v -F -f /tmp/ssrplus_netflix.list.clean "$TMP_DNSMASQ_PATH/gfw_base.conf" > "$TMP_DNSMASQ_PATH/gfw_base.conf.tmp"
mv "$TMP_DNSMASQ_PATH/gfw_base.conf.tmp" "$TMP_DNSMASQ_PATH/gfw_base.conf"
fi
fi
rm -f /tmp/ssrplus_netflix.list.clean
fi
if [ "$nft_support" = "1" ]; then
# 移除 ipset
cat /etc/ssrplus/netflix.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/server=\/&\/127.0.0.1#$port\nnftset=\/&\/inet#ss_spec#netflix/" >$TMP_DNSMASQ_PATH/netflix_forward.conf
elif [ "$nft_support" = "0" ]; then
cat /etc/ssrplus/netflix.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/server=\/&\/127.0.0.1#$port\nipset=\/&\/netflix/" >$TMP_DNSMASQ_PATH/netflix_forward.conf
fi
}
mkdir -p $TMP_DNSMASQ_PATH
run_mode=$(uci_get_by_type global run_mode router)
run_mode=$(normalize_run_mode)
if [ "$run_mode" = "oversea" ]; then
cp -rf /etc/ssrplus/oversea_list.conf $TMP_DNSMASQ_PATH/
else
cp -rf /etc/ssrplus/gfw_list.conf $TMP_DNSMASQ_PATH/
cp -rf /etc/ssrplus/gfw_base.conf $TMP_DNSMASQ_PATH/
fi
cp -rf /etc/ssrplus/gfw_list.conf $TMP_DNSMASQ_PATH/
cp -rf /etc/ssrplus/gfw_base.conf $TMP_DNSMASQ_PATH/
for conf_file in gfw_base.conf gfw_list.conf; do
conf="$TMP_DNSMASQ_PATH/$conf_file"
@@ -67,36 +41,41 @@ for conf_file in gfw_base.conf gfw_list.conf; do
fi
done
if [ "$(uci_get_by_type global netflix_enable 0)" == "1" ]; then
# 只有开启 NetFlix分流 才需要取值
SHUNT_SERVER=$(uci_get_by_type global netflix_server nil)
else
# 没有开启 设置为 nil
SHUNT_SERVER=nil
fi
case "$SHUNT_SERVER" in
nil)
rm -f $TMP_DNSMASQ_PATH/netflix_forward.conf
;;
$(uci_get_by_type global global_server nil) | $switch_server | same)
netflix $dns_port
;;
*)
netflix $tmp_shunt_dns_port
;;
esac
# 此处使用 for 方式读取 防止 /etc/ssrplus/ 目录下的 black.list white.list deny.list 等2个或多个文件一行中存在空格 比如:# abc.com 而丢失:server
# Optimize: Batch filter using grep
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do
if [ -s "$list_file" ]; then
grep -vE '^\s*#|^\s*$' "$list_file" > "${list_file}.clean"
# 清理注释和空行
grep -vE '^\s*#|^\s*$' "$list_file" | sed 's/\r//g' > "${list_file}.clean"
if [ -s "${list_file}.clean" ]; then
for target_file in "$TMP_DNSMASQ_PATH/gfw_list.conf" "$TMP_DNSMASQ_PATH/gfw_base.conf"; do
if [ -f "$target_file" ]; then
grep -v -F -f "${list_file}.clean" "$target_file" > "${target_file}.tmp"
mv "${target_file}.tmp" "$target_file"
fi
[ -f "$target_file" ] || continue
tmp_file="${target_file}.tmp"
awk -v list="${list_file}.clean" '
BEGIN {
while ((getline line < list) > 0) {
gsub(/\r/, "", line)
if (line != "") {
domain[line] = 1
# 同时支持 *.domain
domain["*." line] = 1
}
}
close(list)
}
{
# 提取 server=/domain/xxx
if (match($0, /^server=\/([^\/]+)\//, m)) {
if (m[1] in domain) next
}
# 提取 ipset=/domain/xxx
if (match($0, /^ipset=\/([^\/]+)\//, m)) {
if (m[1] in domain) next
}
print
}
' "$target_file" > "$tmp_file"
mv "$tmp_file" "$target_file"
done
fi
rm -f "${list_file}.clean"
@@ -114,19 +93,45 @@ fi
cat /etc/ssrplus/deny.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/address=\/&\//" >$TMP_DNSMASQ_PATH/denylist.conf
if [ "$(uci_get_by_type global adblock 0)" == "1" ]; then
cp -f /etc/ssrplus/ad.conf $TMP_DNSMASQ_PATH/
cp -f /etc/ssrplus/ad.conf "$TMP_DNSMASQ_PATH/"
if [ -f "$TMP_DNSMASQ_PATH/ad.conf" ]; then
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list /etc/ssrplus/netflix.list; do
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do
if [ -s "$list_file" ]; then
grep -vE '^\s*#|^\s*$' "$list_file" > "${list_file}.clean"
# 清理注释 & 空行
grep -vE '^\s*#|^\s*$' "$list_file" | sed 's/\r//g' > "${list_file}.clean"
if [ -s "${list_file}.clean" ]; then
grep -v -F -f "${list_file}.clean" "$TMP_DNSMASQ_PATH/ad.conf" > "$TMP_DNSMASQ_PATH/ad.conf.tmp"
mv "$TMP_DNSMASQ_PATH/ad.conf.tmp" "$TMP_DNSMASQ_PATH/ad.conf"
tmp_file="$TMP_DNSMASQ_PATH/ad.conf.tmp"
awk -v list="${list_file}.clean" '
BEGIN {
while ((getline line < list) > 0) {
gsub(/\r/, "", line)
if (line != "") {
domain[line] = 1
# 支持泛域名
domain["*." line] = 1
}
}
close(list)
}
{
keep = 1
# 精确匹配 server=/domain/
if (match($0, /^server=\/([^\/]+)\//, m)) {
if (m[1] in domain) keep = 0
}
# 精确匹配 ipset=/domain/
if (match($0, /^ipset=\/([^\/]+)\//, m)) {
if (m[1] in domain) keep = 0
}
if (keep) print
}
' "$TMP_DNSMASQ_PATH/ad.conf" > "$tmp_file"
mv "$tmp_file" "$TMP_DNSMASQ_PATH/ad.conf"
fi
rm -f "${list_file}.clean"
fi
done
fi
else
rm -f $TMP_DNSMASQ_PATH/ad.conf
rm -f "$TMP_DNSMASQ_PATH/ad.conf"
fi
@@ -1,175 +0,0 @@
#!/bin/sh
# /usr/share/shadowsocksr/test.sh
CONFIG="shadowsocksr"
LOCK_PATH=/tmp/lock
TMP_PATH=/var/etc/ssrplus
uci_get_by_name() {
local ret=$(uci -q get $CONFIG.$1.$2 2>/dev/null)
echo "${ret:=$3}"
}
uci_get_by_type() {
local ret=$(uci -q get $CONFIG.@$1[0].$2 2>/dev/null)
echo "${ret:=$3}"
}
check_port_exists() {
local port=$1
local protocol=$2
[ -n "$protocol" ] || protocol="tcp,udp"
local result=
if [ "$protocol" = "tcp" ]; then
result=$(netstat -tln | grep -c ":$port ")
elif [ "$protocol" = "udp" ]; then
result=$(netstat -uln | grep -c ":$port ")
elif [ "$protocol" = "tcp,udp" ]; then
result=$(netstat -tuln | grep -c ":$port ")
fi
echo "${result}"
}
set_cache_var() {
local key="${1}"
shift 1
local val="$@"
[ -n "${key}" ] && [ -n "${val}" ] && {
sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1
echo "${key}=\"${val}\"" >> $TMP_PATH/var
eval ${key}=\"${val}\"
}
}
get_cache_var() {
local key="${1}"
[ -n "${key}" ] && [ -s "$TMP_PATH/var" ] && {
echo $(cat $TMP_PATH/var | grep "^${key}=" | awk -F '=' '{print $2}' | tail -n 1 | awk -F'"' '{print $2}')
}
}
#uci_get_by_port() {
# local port=$1
# while netstat -tuln 2>/dev/null | grep -q ":${port} "; do
# port=$((port + 1))
# done
# echo $port
uci_get_by_port() {
local default_start_port=2001
local min_port=1025
local max_port=49151
local port="$1"
local protocol=$(echo "$2" | tr 'A-Z' 'a-z')
local LOCK_FILE="${LOCK_PATH}/${CONFIG}_get_prot.lock"
while ! mkdir "$LOCK_FILE" 2>/dev/null; do
sleep 1
done
if [ "$port" = "auto" ]; then
local now last_time diff last_port
now=$(date +%s 2>/dev/null)
last_time=$(get_cache_var "last_get_new_port_time")
if [ -n "$now" ] && [ -n "$last_time" ]; then
diff=$(expr "$now" - "$last_time")
[ "$diff" -lt 0 ] && diff=$(expr 0 - "$diff")
else
diff=999
fi
if [ "$diff" -gt 10 ]; then
port=$default_start_port
else
last_port=$(get_cache_var "last_get_new_port_auto")
if [ -n "$last_port" ]; then
port=$(expr "$last_port" + 1)
else
port=$default_start_port
fi
fi
fi
[ "$port" -lt $min_port -o "$port" -gt $max_port ] && port=$default_start_port
local start_port="$port"
while :; do
if [ "$(check_port_exists "$port" "$protocol")" = 0 ]; then
break
fi
port=$(expr "$port" + 1)
if [ "$port" -gt $max_port ]; then
port=$min_port
fi
[ "$port" = "$start_port" ] && {
rmdir "$LOCK_FILE" 2>/dev/null
return 1
}
done
if [ "$1" = "auto" ]; then
set_cache_var "last_get_new_port_auto" "$port"
[ -n "$now" ] && set_cache_var "last_get_new_port_time" "$now"
fi
rmdir "$LOCK_FILE" 2>/dev/null
echo "$port"
}
url_test_hy2() {
local node_id=$1
# 读取配置
local server=$(uci_get_by_name ${node_id} server)
local port=$(uci_get_by_name ${node_id} server_port)
local auth=$(uci_get_by_name ${node_id} hy2_auth)
local tls=$(uci_get_by_name ${node_id} tls)
local insecure=$(uci_get_by_name ${node_id} insecure)
local tls_host=$(uci_get_by_name ${node_id} tls_host)
# 获取本地端口
# local tmp_port=$(uci_get_by_port 48900 tcp,udp)
local tmp_port=$(uci_get_by_port auto tcp,udp)
# 生成Hysteria2配置文件
local config_file="/tmp/hy2_test_${node_id}.yaml"
cat > "$config_file" <<-EOF
server: ${server}:${port}
auth: "${auth}"
tls:
insecure: true
EOF
# 如果 tls_host 非空,动态添加 sni 行
[ -n "$tls_host" ] && echo " sni: \"${tls_host}\"" >> "$config_file"
# 追加 socks5 监听配置
cat >> "$config_file" <<-EOF
socks5:
listen: 127.0.0.1:${tmp_port}
EOF
# echo "Debug: 配置文件已生成: $config_file" >&2
# 启动Hysteria2客户端
hysteria client --disable-update-check -c "$config_file" >/dev/null 2>&1 &
local pid=$!
echo $pid > "/tmp/hy2_test_${node_id}.pid"
# 等待端口启动
sleep 1
# 测试代理
# local result=$(curl --connect-timeout 3 --max-time 3 -s -o /dev/null -I -w "%{http_code}:%{time_pretransfer}" --socks5 127.0.0.1:${tmp_port} "${probeUrl}" 2>/dev/null)
local curlx="socks5h://127.0.0.1:${tmp_port}"
local probeUrl=$(uci_get_by_type server_subscribe url_test_url https://www.google.com/generate_204)
local result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -skL -w "%{http_code}:%{time_pretransfer}" -x ${curlx} "${probeUrl}" 2>/dev/null)
# 清理
# kill -9 $pid 2>/dev/null
local pid_file="/tmp/hy2_test_${node_id}.pid"
[ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1
pgrep -af "hysteria.*${config_file}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1
rm -f "$config_file" "$pid_file"
echo $result
}
case $1 in
url_test_hy2)
url_test_hy2 $2
;;
esac
@@ -1,35 +1,35 @@
config global
option global_server 'nil'
option netflix_server 'nil'
option netflix_proxy '0'
option threads '0'
option run_mode 'router'
option dports '2'
option custom_ports '80,443'
option pdnsd_enable '1'
option prefer_nft '1'
option filter_aaaa '1'
option tunnel_forward '8.8.4.4:53'
option monitor_enable '1'
option enable_switch '1'
option switch_time '667'
option switch_timeout '5'
option switch_try_count '3'
option shunt_dns '1'
option default_node_local_port '1234'
option component_mirror 'direct'
option gfwlist_url 'https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt'
option chnroute_url 'https://ispip.clang.cn/all_cn.txt'
option nfip_url 'https://fastly.jsdelivr.net/gh/QiuSimons/Netflix_IP/NF_only.txt'
option adblock_url 'https://anti-ad.net/anti-ad-for-dnsmasq.conf'
config server_subscribe
option proxy '0'
option proxy '1'
option auto_update '1'
option config_auto_update_mode '0'
option config_update_interval '60'
option auto_update_week_time '*'
option auto_update_day_time '2'
option auto_update_min_time '0'
option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99'
option filter_words '过期/套餐/剩余/QQ群/官网/防失联/回国'
option filter_words '过期/套餐/剩余/网址/QQ群/官网/防失联/回国'
config access_control
option lan_ac_mode '0'
@@ -47,6 +47,11 @@ config socks5_proxy
option server 'nil'
option local_port '1080'
config http_proxy
option server 'nil'
option local_port '3128'
option http_auth 'none'
config server_global
option enable_server '0'
@@ -54,3 +59,9 @@ config global_xray_fragment
option fragment '0'
option noise '0'
config clash_client_group
option enabled '0'
option remarks ''
option ip_addr ''
option client_mac ''
option policy_group ''
@@ -1,6 +1,43 @@
#!/bin/sh
LOCK_DIR="/var/run/ssrplusupdate.lock"
LOOP_STAMP="/var/run/ssrplusupdate.loop"
MODE="$(uci -q get shadowsocksr.@server_subscribe[0].config_auto_update_mode 2>/dev/null || echo 0)"
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
if [ "$1" = "loop" ]; then
[ "$(uci -q get shadowsocksr.@server_subscribe[0].auto_update 2>/dev/null || echo 0)" = "1" ] || exit 0
[ "$MODE" = "1" ] || exit 0
INTERVAL="$(uci -q get shadowsocksr.@server_subscribe[0].config_update_interval 2>/dev/null || echo 60)"
case "$INTERVAL" in
''|*[!0-9]*)
INTERVAL=60
;;
esac
[ "$INTERVAL" -gt 0 ] 2>/dev/null || INTERVAL=60
NOW="$(date +%s)"
LAST_RUN=0
[ -f "$LOOP_STAMP" ] && LAST_RUN="$(cat "$LOOP_STAMP" 2>/dev/null || echo 0)"
case "$LAST_RUN" in
''|*[!0-9]*)
LAST_RUN=0
;;
esac
if [ $((NOW - LAST_RUN)) -lt $((INTERVAL * 60)) ]; then
exit 0
fi
fi
/usr/bin/lua /usr/share/shadowsocksr/update.lua
sleep 2s
/usr/share/shadowsocksr/chinaipset.sh /var/etc/ssrplus/china_ssr.txt
sleep 2s
/usr/bin/lua /usr/share/shadowsocksr/subscribe.lua
if [ "$1" = "loop" ]; then
date +%s > "$LOOP_STAMP"
fi
File diff suppressed because it is too large. Load diff
@@ -250,10 +250,6 @@ if args then
update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", args, TMP_DNSMASQ_PATH .. "/ad.conf")
os.exit(0)
end
if args == "nfip_data" then
update(uci:get_first("shadowsocksr", "global", "nfip_url"), "/etc/ssrplus/netflixip.list", args, TMP_DNSMASQ_PATH .. "/netflixip.list")
os.exit(0)
end
else
log("正在更新【GFW列表】数据库")
update(uci:get_first("shadowsocksr", "global", "gfwlist_url"), "/etc/ssrplus/gfw_list.conf", "gfw_data", TMP_DNSMASQ_PATH .. "/gfw_list.conf")
@@ -267,10 +263,4 @@ else
log("正在更新【广告屏蔽】数据库")
update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", "ad_data", TMP_DNSMASQ_PATH .. "/ad.conf")
end
if uci:get_first("shadowsocksr", "global", "netflix_enable", "0") == "1" then
log("正在更新【Netflix IP段】数据库")
update(uci:get_first("shadowsocksr", "global", "nfip_url"), "/etc/ssrplus/netflixip.list", "nfip_data", TMP_DNSMASQ_PATH .. "/netflixip.list")
end
-- log("正在更新【Netflix IP段】数据库")
-- update(uci:get_first("shadowsocksr", "global", "nfip_url"), "/etc/ssrplus/netflixip.list", "nfip_data")
end
File diff suppressed because it is too large. Load diff