small-packages/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua
2026-05-23 14:30:42 +08:00

1570 lines
49 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Copyright (C) 2017 yushi studio <ywb94@qq.com>
-- Licensed to the public under the GNU General Public License v3.
module("luci.controller.shadowsocksr", package.seeall)
require "nixio"
require "nixio.fs"
require "luci.util"
require "luci.template"
require "luci.sys"
local i18n = require "luci.i18n"
local datatypes = require "luci.cbi.datatypes"
local json = require "luci.jsonc"
local uci = require "luci.model.uci".cursor()
local translate = i18n.translate
local CLASH_API_PORT = "16756"
local COMPONENT_HELPER = "/usr/share/shadowsocksr/update_components.sh"
local SERVER_DETECT_CACHE = "/tmp/ssrplus_server_detect.json"
local SERVER_DETECT_LOCK = "/tmp/ssrplus_server_detect.lock"
local CLASH_RULES_DIR = "/etc/ssrplus/clash"
local SUPPORTED_COMPONENTS = {
xray = true,
mihomo = true,
naiveproxy = true
}
local SUPPORTED_GEO_COMPONENTS = {
country_mmdb = true,
geosite = true,
v2ray_geo = true
}
local function trim(value)
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end
local function parse_nonnegative_int(value)
local number = tonumber(value)
if not number then
return nil
end
number = math.floor(number)
if number < 0 then
return nil
end
return number
end
local function sanitize_mac(value)
value = trim(value):upper():gsub("-", ":")
if value:match("^%x%x:%x%x:%x%x:%x%x:%x%x:%x%x$") then
return value
end
return ""
end
local function normalize_client_ip(value)
value = trim(value)
if value == "" then
return ""
end
if datatypes.cidr4(value) or datatypes.ip4addr(value) then
return value
end
return ""
end
local function get_clash_client_rule_csv_path(sid)
sid = trim(sid)
if sid == "" then
return nil
end
return string.format("%s/%s.csv", CLASH_RULES_DIR, sid)
end
local function csv_escape(value)
value = tostring(value or "")
if value:find('[",\n\r]') then
return '"' .. value:gsub('"', '""') .. '"'
end
return value
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 rules = {}
local csv_path = get_clash_client_rule_csv_path(sid)
if not csv_path or not nixio.fs.access(csv_path) then
return rules
end
local raw = nixio.fs.readfile(csv_path)
if not raw or raw == "" then
return rules
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
rules[#rules + 1] = {
id = tostring(#rules + 1),
enabled = cols[1] == "1" or tostring(cols[1] or ""):lower() == "true",
ip_addr = trim(cols[2] or ""),
policy_group = trim(cols[3] or ""),
remarks = trim(cols[4] or ""),
client_mac = sanitize_mac(cols[5] or "")
}
end
end
end
end
return rules
end
local function write_clash_client_rules_csv(sid, rows)
local csv_path = get_clash_client_rule_csv_path(sid)
if not csv_path then
return false
end
nixio.fs.mkdirr(CLASH_RULES_DIR)
local lines = { "enabled,client,policy,remarks,client_mac" }
for _, row in ipairs(rows or {}) do
lines[#lines + 1] = table.concat({
row.enabled == "1" and "1" or "0",
csv_escape(row.ip_addr or ""),
csv_escape(row.policy_group or ""),
csv_escape(row.remarks or ""),
csv_escape(row.client_mac or "")
}, ",")
end
return nixio.fs.writefile(csv_path, table.concat(lines, "\n") .. "\n")
end
local function collect_lan_clients()
local clients = {}
local seen = {}
luci.sys.net.host_hints(function(mac, ipv4, _, name)
local ip = trim(ipv4)
local norm_mac = sanitize_mac(mac)
if ip ~= "" and not seen[ip] then
seen[ip] = true
clients[#clients + 1] = {
ip = ip,
mac = norm_mac,
name = trim(name) ~= "" and trim(name) or ip
}
end
end)
table.sort(clients, function(a, b)
return tostring(a.name or a.ip) < tostring(b.name or b.ip)
end)
return clients
end
local function read_clash_client_rules(sid)
return read_clash_client_rules_csv(sid)
end
local function normalize_ping_ms(value, scale)
local num = tonumber(value)
if not num or num <= 0 then
return nil
end
local scaled = scale and (num * scale) or num
if scaled > 0 and scaled < 1 then
return 1
end
return math.floor(scaled + 0.5)
end
local function detect_tls_handshake_ms(domain, port, path, resolve_host, server_ip, is_websocket)
if not domain or domain == "" or not port or port <= 0 then
return nil
end
local final_host = (resolve_host and resolve_host ~= "") and resolve_host or domain
local resolve_arg = ""
if server_ip and server_ip ~= "" and final_host ~= server_ip then
resolve_arg = string.format("--resolve '%s:%d:%s' ", final_host, port, server_ip)
end
local ws_headers = ""
if is_websocket then
ws_headers = string.format(
"-H %s -H %s -H %s -H %s ",
luci.util.shellquote("Connection: Upgrade"),
luci.util.shellquote("Upgrade: websocket"),
luci.util.shellquote("Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ=="),
luci.util.shellquote("Sec-WebSocket-Version: 13")
)
end
local host_header = (final_host and final_host ~= "") and ("-H " .. luci.util.shellquote("Host: " .. final_host) .. " ") or ""
local url = string.format("https://%s:%d%s", final_host, port, path or "")
local cmd = string.format(
"curl --http1.1 -m 3 -ksS -o /dev/null %s%s%s -w 'time_connect=%%{time_connect}\\ntime_appconnect=%%{time_appconnect}\\nhttp_code=%%{http_code}' '%s' 2>/dev/null",
resolve_arg, host_header, ws_headers, url
)
local result = luci.sys.exec(cmd) or ""
local appconnect = tonumber(result:match("time_appconnect=([0-9.]+)"))
if appconnect and appconnect > 0 then
return normalize_ping_ms(appconnect, 1000)
end
local connect = tonumber(result:match("time_connect=([0-9.]+)"))
if connect and connect > 0 then
return normalize_ping_ms(connect, 1000)
end
return nil
end
local function urlencode(str)
if not str then return "" end
return tostring(str):gsub("[^%w%-_%.~]", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
local function is_ipv6_address(addr)
addr = tostring(addr or "")
return addr ~= "" and addr:find(":", 1, true) ~= nil
end
local function is_local_target(addr)
addr = tostring(addr or ""):lower()
if addr == "" then
return false
end
if addr == "localhost" or addr == "::1" or addr:match("%.local$") then
return true
end
if is_ipv6_address(addr) then
return addr:match("^fe[89ab]") ~= nil or addr:match("^fc") ~= nil or addr:match("^fd") ~= nil
end
local o1, o2 = addr:match("^(%d+)%.(%d+)%.")
o1 = tonumber(o1)
o2 = tonumber(o2)
if not o1 or not o2 then
return false
end
return o1 == 10
or o1 == 127
or (o1 == 169 and o2 == 254)
or (o1 == 172 and o2 >= 16 and o2 <= 31)
or (o1 == 192 and o2 == 168)
end
local function detect_tcp_connect_ms(domain, port)
if not domain or domain == "" or not port or port <= 0 then
return nil
end
local ip_version_arg = is_ipv6_address(domain) and "-6 " or ""
local cmd = string.format(
"nping %s--tcp-connect -q -c 1 -p %d %s 2>/dev/null",
ip_version_arg,
port,
luci.util.shellquote(domain)
)
local result = luci.sys.exec(cmd) or ""
local success = tonumber(result:match("Successful connections:%s*([0-9]+)"))
if success and success > 0 then
local avg_rtt = tonumber(result:match("Avg rtt:%s*([0-9.]+)ms"))
if avg_rtt and avg_rtt > 0 and avg_rtt < 1 and not is_local_target(domain) then
return nil
end
return normalize_ping_ms(avg_rtt)
end
return nil
end
local function get_clash_secret(sid)
return sid .. "_ssrplus_clash"
end
local function get_clash_cache_file(sid)
return "/etc/ssrplus/clash/" .. sid .. ".yaml"
end
local function get_clash_state_file(sid)
return "/etc/ssrplus/clash/" .. sid .. ".cache.db"
end
local function clash_process_running()
return luci.sys.call("(busybox ps -w 2>/dev/null || busybox ps) | grep ssr-retcp | grep -v grep >/dev/null") == 0
end
local function global_client_running()
local process_list = luci.sys.exec("busybox ps -w 2>/dev/null || busybox ps")
local global_server = uci:get_first("shadowsocksr", "global", "global_server", "nil")
local global_type = global_server ~= "nil" and (uci:get("shadowsocksr", global_server, "type") or "") or ""
if process_list:find("tcp.only.ssr.retcp")
or process_list:find("tcp.udp.ssr.retcp")
or process_list:find("local.ssr.retcp")
or process_list:find("local.udp.ssr.retcp") then
return true
end
if (global_type == "clash" or global_type == "tuic" or global_type == "ss")
and process_list:find("ssr%-retcp") then
return true
end
if (global_type == "clash" or global_type == "tuic" or global_type == "ss")
and process_list:find("mihomo")
and (process_list:find("/clash%-") or process_list:find("/tuic%-") or process_list:find("/ss%-")) then
return true
end
if global_type == "socks5"
and process_list:find("ipt2socks")
and (process_list:find("%-T") or process_list:find("%-%-tcp%-only")) then
return true
end
return false
end
local function get_active_node_runtime(sid)
if not sid or sid == "" or sid == "nil" or uci:get("shadowsocksr", sid) ~= "servers" then
return nil, nil
end
local stype = (uci:get("shadowsocksr", sid, "type") or ""):lower()
local proto = (uci:get("shadowsocksr", sid, "v2ray_protocol") or ""):lower()
local backend
local protocol
if stype == "ss" then
backend = translate("Mihomo")
protocol = translate("Shadowsocks")
elseif stype == "clash" then
backend = translate("Mihomo")
protocol = translate("Clash")
elseif stype == "tuic" then
backend = translate("Mihomo")
protocol = translate("TUIC")
elseif stype == "ssr" then
backend = translate("ShadowsocksR")
elseif stype == "ss-rust" then
backend = translate("Shadowsocks-rust")
elseif stype == "v2ray" then
local proto_map = {
vmess = "VMess",
vless = "VLESS",
trojan = "Trojan",
socks = "SOCKS5",
hysteria2 = "Hysteria2",
shadowsocks = "Shadowsocks",
http = "HTTP"
}
backend = translate("Xray")
if proto_map[proto] then
protocol = translate(proto_map[proto])
end
elseif stype == "trojan" then
backend = translate("Trojan")
elseif stype == "naiveproxy" then
backend = translate("NaiveProxy")
elseif stype == "socks5" then
backend = translate("SOCKS5")
elseif stype == "shadowtls" then
backend = translate("ShadowTLS")
elseif stype == "hysteria2" then
backend = translate("Hysteria2")
end
if not backend or backend == "" then
backend = trim(stype)
end
return backend, protocol
end
local function get_running_status_text()
local sid = uci:get_first("shadowsocksr", "global", "global_server", "nil")
local backend, protocol = get_active_node_runtime(sid)
if backend and backend ~= "" and protocol and protocol ~= "" and backend ~= protocol then
return string.format(translate("RUNNING in %s (%s) Mode"), backend, protocol)
end
if backend and backend ~= "" then
return string.format(translate("RUNNING in %s Mode"), backend)
end
return translate("RUNNING")
end
local function is_active_clash_node(sid)
if not sid then return false end
if uci:get("shadowsocksr", sid) ~= "servers" then return false end
if uci:get("shadowsocksr", sid, "type") ~= "clash" then return false end
if uci:get_first("shadowsocksr", "global", "global_server") ~= sid then return false end
return clash_process_running()
end
local function resolve_active_clash_sid(sid)
if is_active_clash_node(sid) then
return sid
end
local current_sid = uci:get_first("shadowsocksr", "global", "global_server")
if is_active_clash_node(current_sid) then
return current_sid
end
return nil
end
local function clash_api_request(sid, method, path, body)
sid = resolve_active_clash_sid(sid)
if not sid then
return nil
end
local secret = get_clash_secret(sid)
local cmd = string.format(
"curl -sL -m 5 --retry 1 -w '\n__CURL_STATUS__:%%{http_code}' -H %s -H %s -X %s http://127.0.0.1:%s%s %s",
luci.util.shellquote("Content-Type: application/json"),
luci.util.shellquote("Authorization: Bearer " .. secret),
method,
CLASH_API_PORT,
path,
body and ("-d " .. luci.util.shellquote(body)) or ""
)
local output = luci.sys.exec(cmd)
if not output or output == "" then
return nil
end
local body_output = output:gsub("\n__CURL_STATUS__:%d%d%d%s*$", "")
local code = output:match("__CURL_STATUS__:(%d%d%d)")
return {
code = tonumber(code),
body = body_output
}
end
local function use_fw4_backend()
return luci.sys.call("command -v fw4 >/dev/null") == 0
end
local function parse_clash_groups(raw)
local info = json.parse(raw or "")
local proxies = info and info.proxies or nil
local groups = {}
if type(proxies) ~= "table" then
return groups
end
for name, value in pairs(proxies) do
if type(value) == "table" and type(value.all) == "table" and #value.all > 0 then
groups[#groups + 1] = {
name = name,
type = value.type or "",
now = value.now or "",
all = value.all
}
end
end
table.sort(groups, function(a, b) return tostring(a.name) < tostring(b.name) end)
return groups
end
local function parse_kv_output(raw)
local data = {}
for line in tostring(raw or ""):gmatch("[^\r\n]+") do
local key, value = line:match("^([%w_]+)=(.*)$")
if key then
data[key] = value
end
end
return data
end
local function with_detect_cache_lock(fn)
for _ = 1, 40 do
if nixio.fs.mkdir(SERVER_DETECT_LOCK) then
local ok, ret = pcall(fn)
nixio.fs.rmdir(SERVER_DETECT_LOCK)
if ok then
return ret
end
return nil
end
nixio.nanosleep(0, 50000000)
end
return nil
end
local function load_detect_cache()
local raw = nixio.fs.readfile(SERVER_DETECT_CACHE)
if not raw or raw == "" then
return {}
end
local parsed = json.parse(raw)
return type(parsed) == "table" and parsed or {}
end
local function save_detect_cache_entry(sid, data)
if not sid or sid == "" then
return
end
with_detect_cache_lock(function()
local cache = load_detect_cache()
cache[sid] = data
nixio.fs.writefile(SERVER_DETECT_CACHE, json.stringify(cache))
end)
end
local function read_component_state(component, action)
if not SUPPORTED_COMPONENTS[component] then
return nil, 400, "unsupported_component"
end
local mirror = luci.http.formvalue("mirror") or ""
local cmd = string.format(
"COMPONENT_MIRROR=%s /bin/sh %s %s 2>/dev/null",
luci.util.shellquote(mirror),
luci.util.shellquote(COMPONENT_HELPER),
luci.util.shellquote(component .. "_" .. action)
)
return parse_kv_output(luci.sys.exec(cmd))
end
local function read_geo_state(component, action)
if not SUPPORTED_GEO_COMPONENTS[component] then
return nil, 400, "unsupported_component"
end
local mirror = luci.http.formvalue("mirror") or ""
local cmd = string.format(
"COMPONENT_MIRROR=%s /bin/sh %s %s 2>/dev/null",
luci.util.shellquote(mirror),
luci.util.shellquote(COMPONENT_HELPER),
luci.util.shellquote(component .. "_" .. action)
)
return parse_kv_output(luci.sys.exec(cmd))
end
local function write_component_json(data)
luci.http.prepare_content("application/json")
luci.http.write_json({
component = data.component or "xray",
installed = data.installed == "1",
current_version = data.current_version or "",
latest_version = data.latest_version or "",
previous_version = data.previous_version or "",
arch = data.arch or "",
asset = data.asset or "",
can_upgrade = data.can_upgrade == "1",
success = data.success == "1",
error = data.error or "",
message = data.message or ""
})
end
local function write_geo_json(data)
luci.http.prepare_content("application/json")
luci.http.write_json({
component = data.component or "country_mmdb",
installed = data.installed == "1",
current_version = data.current_version or "",
current_version_extra = data.current_version_extra or "",
latest_version = data.latest_version or "",
can_upgrade = data.can_upgrade == "1",
success = data.success == "1",
error = data.error or "",
message = data.message or ""
})
end
local function shell_quote(value)
return "'" .. tostring(value or ""):gsub("'", "'\\''") .. "'"
end
function index()
if not nixio.fs.access("/etc/config/shadowsocksr") then
call("act_reset")
end
local page
page = entry({"admin", "services", "shadowsocksr"}, alias("admin", "services", "shadowsocksr", "client"), _("ShadowSocksR Plus+"), 10)
page.dependent = true
page.acl_depends = { "luci-app-ssr-plus" }
entry({"admin", "services", "shadowsocksr", "client"}, cbi("shadowsocksr/client"), _("SSR Client"), 10).leaf = true
entry({"admin", "services", "shadowsocksr", "servers"}, arcombine(cbi("shadowsocksr/servers"), cbi("shadowsocksr/client-config")), _("Servers Nodes"), 20).leaf = true
entry({"admin", "services", "shadowsocksr", "control"}, cbi("shadowsocksr/control"), _("Access Control"), 30).leaf = true
entry({"admin", "services", "shadowsocksr", "advanced"}, cbi("shadowsocksr/advanced"), _("Advanced Settings"), 50).leaf = true
entry({"admin", "services", "shadowsocksr", "server"}, arcombine(cbi("shadowsocksr/server"), cbi("shadowsocksr/server-config")), _("SSR Server"), 60).leaf = true
entry({"admin", "services", "shadowsocksr", "component"}, cbi("shadowsocksr/component"), _("Component Update"), 65).leaf = true
entry({"admin", "services", "shadowsocksr", "status"}, form("shadowsocksr/status"), _("Status"), 70).leaf = true
entry({"admin", "services", "shadowsocksr", "check"}, call("check_status"))
entry({"admin", "services", "shadowsocksr", "refresh"}, call("refresh_data"))
entry({"admin", "services", "shadowsocksr", "subscribe"}, call("subscribe"))
entry({"admin", "services", "shadowsocksr", "component_local_status"}, call("component_local_status")).leaf = true
entry({"admin", "services", "shadowsocksr", "component_set_mirror"}, call("component_set_mirror")).leaf = true
entry({"admin", "services", "shadowsocksr", "component_status"}, call("component_status")).leaf = true
entry({"admin", "services", "shadowsocksr", "component_upgrade"}, call("component_upgrade")).leaf = true
entry({"admin", "services", "shadowsocksr", "geo_local_status"}, call("geo_local_status")).leaf = true
entry({"admin", "services", "shadowsocksr", "geo_status"}, call("geo_status")).leaf = true
entry({"admin", "services", "shadowsocksr", "geo_upgrade"}, call("geo_upgrade")).leaf = true
entry({"admin", "services", "shadowsocksr", "checkport"}, call("check_port"))
entry({"admin", "services", "shadowsocksr", "log"}, form("shadowsocksr/log"), _("Log"), 80).leaf = true
entry({"admin", "services", "shadowsocksr", "get_log"}, call("get_log")).leaf = true
entry({"admin", "services", "shadowsocksr", "clear_log"}, call("clear_log")).leaf = true
entry({"admin", "services", "shadowsocksr", "run"}, call("act_status"))
entry({"admin", "services", "shadowsocksr", "ping"}, call("act_ping"))
entry({"admin", "services", "shadowsocksr", "save_order"}, call("save_order")).leaf = true
entry({"admin", "services", "shadowsocksr", "delete_node"}, call("act_delete_node")).leaf = true
entry({"admin", "services", "shadowsocksr", "add_subscribe_item"}, call("add_subscribe_item")).leaf = true
entry({"admin", "services", "shadowsocksr", "delete_subscribe_item"}, call("delete_subscribe_item")).leaf = true
entry({"admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled"}, call("toggle_subscribe_item_enabled")).leaf = true
entry({"admin", "services", "shadowsocksr", "reset"}, call("act_reset"))
entry({"admin", "services", "shadowsocksr", "restart"}, call("act_restart"))
entry({"admin", "services", "shadowsocksr", "delete"}, call("act_delete"))
entry({"admin", "services", "shadowsocksr", "clash_panel"}, call("clash_panel")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_groups"}, call("clash_groups")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_switch"}, call("clash_switch")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_refresh"}, call("clash_refresh")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_reset_defaults"}, call("clash_reset_defaults")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_client_policies"}, call("clash_client_policies")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_client_rule_save"}, call("clash_client_rule_save")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_client_rule_clear"}, call("clash_client_rule_clear")).leaf = true
--[[Backup]]
entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true
end
function subscribe()
nixio.fs.remove(SERVER_DETECT_CACHE)
local sid = luci.http.formvalue("sid") or ""
local subscribe_arg = sid ~= "" and (" " .. luci.util.shellquote(sid)) or ""
local ret = luci.sys.call(": > /var/log/ssrplus.log && /usr/bin/lua /usr/share/shadowsocksr/subscribe.lua" .. subscribe_arg .. " >>/var/log/ssrplus.log 2>&1")
luci.http.prepare_content("application/json")
luci.http.write_json({ret = ret})
end
function save_order()
local order = luci.http.formvalue("order") or ""
local page = parse_nonnegative_int(luci.http.formvalue("page")) or 1
local page_size = parse_nonnegative_int(luci.http.formvalue("page_size")) or 0
local sids = {}
local all_sections = {}
local server_sections = {}
local server_positions = {}
local section_index = {}
local page_start
local page_end
for sid in order:gmatch("%S+") do
if uci:get("shadowsocksr", sid) == "servers" and not section_index[sid] then
sids[#sids + 1] = sid
section_index[sid] = true
end
end
uci:foreach("shadowsocksr", nil, function(section)
all_sections[#all_sections + 1] = section[".name"]
if section[".type"] == "servers" then
server_sections[#server_sections + 1] = section[".name"]
server_positions[#server_positions + 1] = #all_sections
end
end)
page_start = 1
page_end = #server_sections
if page_size > 0 then
page_start = ((math.max(page, 1) - 1) * page_size) + 1
page_end = math.min(page_start + page_size - 1, #server_sections)
end
local page_sections = {}
local page_lookup = {}
for index = page_start, page_end do
local sid = server_sections[index]
if sid then
page_sections[#page_sections + 1] = sid
page_lookup[sid] = true
end
end
local valid = #sids > 0 and #sids == #page_sections
if valid then
for _, sid in ipairs(sids) do
if not page_lookup[sid] then
valid = false
break
end
end
end
if valid then
for offset, sid in ipairs(sids) do
local absolute_index = page_start + offset - 1
local position = server_positions[absolute_index]
if not position then
valid = false
break
end
local cmd = string.format(
"uci -q reorder %s=%d >/dev/null 2>&1",
shell_quote("shadowsocksr." .. sid),
position - 1
)
if luci.sys.call(cmd) ~= 0 then
valid = false
break
end
all_sections[position] = sid
end
if valid then
valid = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1") == 0
end
end
luci.http.prepare_content("application/json")
luci.http.write_json({
ret = valid and 1 or 0,
count = #sids,
page = page,
page_size = page_size
})
end
function act_delete_node()
local sid = luci.http.formvalue("sid")
if not sid or sid == "" then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "missing sid" })
return
end
local del_cmd = luci.sys.call("uci -q delete shadowsocksr." .. sid)
if del_cmd ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "delete failed" })
return
end
local ret_cmd = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1")
if ret_cmd ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "commit failed" })
return
end
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 1, sid = sid })
end
function add_subscribe_item()
local sid = luci.sys.exec("uci add shadowsocksr server_subscribe_item"):gsub("%s+", "")
if not sid or sid == "" then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "add failed" })
return
end
local alias = string.format("Subscribe %s", sid:sub(-4))
-- set enabled
local subscribe_enabled = luci.sys.call("uci -q set shadowsocksr." .. sid .. ".enabled=1")
if subscribe_enabled ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "set enabled failed" })
return
end
-- set alias
local subscribe_alias = luci.sys.call("uci -q set shadowsocksr." .. sid .. ".alias='" .. alias .. "'")
if subscribe_alias ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "set alias failed" })
return
end
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 1, sid = sid, alias = alias, enabled = "1" })
end
function delete_subscribe_item()
local sid = trim(luci.http.formvalue("sid"))
if sid == "" then
luci.http.status(400, "Bad Request")
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "missing sid" })
return
end
local delete_subscribe_set = luci.sys.call("uci -q delete shadowsocksr." .. sid .. " 2>/dev/null")
if delete_subscribe_set ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "delete failed" })
return
end
-- commit
local delete_subscribe_cmd = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1")
if delete_subscribe_cmd ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "commit failed" })
return
end
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 1, sid = sid })
end
function toggle_subscribe_item_enabled()
local sid = trim(luci.http.formvalue("sid"))
local field = luci.http.formvalue("field")
local value = luci.http.formvalue("value")
-- 兼容旧调用方式(只传 enabled
if not field then
field = "enabled"
value = luci.http.formvalue("enabled") == "1" and "1" or "0"
end
-- 参数校验
if sid == "" then
luci.http.status(400, "Bad Request")
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "missing sid" })
return
end
-- 检查 sid 对应的 section 类型
if uci:get("shadowsocksr", sid) ~= "server_subscribe_item" then
luci.http.status(400, "Bad Request")
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "invalid_sid" })
return
end
-- 白名单:只允许以下字段
local allowed_fields = { enabled = true, alias = true, url = true }
if not allowed_fields[field] then
luci.http.status(400, "Bad Request")
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "unsupported field" })
return
end
-- 处理字段值
if field == "enabled" then
value = (value == "1" or value == "true") and "1" or "0"
elseif field == "alias" or field == "url" then
value = value and trim(value) or ""
end
-- 转义 value 中的单引号(避免破坏 uci 命令)
local escaped_value = value:gsub("'", "'\\''")
-- 使用外部 uci 命令设置值
local set_cmd = string.format("uci -q set shadowsocksr.%s.%s='%s'", sid, field, escaped_value)
local set_ret = luci.sys.call(set_cmd .. " >/dev/null 2>&1")
if set_ret ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "uci set failed" })
return
end
-- 提交更改
local ret_cmd = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1")
if ret_cmd ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "commit failed" })
return
end
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 1, sid = sid, field = field, value = value })
end
function component_status()
local component = luci.http.formvalue("component")
local data, status, err = read_component_state(component, "info")
if not data then
luci.http.status(status or 500, "Bad Request")
write_component_json({component = component, error = err or "bad_request"})
return
end
write_component_json(data)
end
function component_local_status()
local component = luci.http.formvalue("component")
local data, status, err = read_component_state(component, "local_info")
if not data then
luci.http.status(status or 500, "Bad Request")
write_component_json({component = component, error = err or "bad_request"})
return
end
write_component_json(data)
end
function component_set_mirror()
local mirror = luci.http.formvalue("mirror") or "direct"
local allowed = {
direct = true,
ghproxy = true,
ghproxy_cc = true,
ghfast = true,
jsdelivr = true
}
if not allowed[mirror] then
mirror = "direct"
end
uci:set("shadowsocksr", "@global[0]", "component_mirror", mirror)
uci:commit("shadowsocksr")
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 1, mirror = mirror })
end
function component_upgrade()
local component = luci.http.formvalue("component")
local data, status, err = read_component_state(component, "upgrade")
if not data then
luci.http.status(status or 500, "Bad Request")
write_component_json({component = component, error = err or "bad_request", success = "0"})
return
end
local info = read_component_state(component, "info")
if info then
for key, value in pairs(info) do
if data[key] == nil or data[key] == "" then
data[key] = value
end
end
if data.success == "1" then
data.can_upgrade = info.can_upgrade
end
end
write_component_json(data)
end
function geo_status()
local component = luci.http.formvalue("component")
local data, status, err = read_geo_state(component, "info")
if not data then
luci.http.status(status or 500, "Bad Request")
write_geo_json({component = component, error = err or "bad_request"})
return
end
write_geo_json(data)
end
function geo_local_status()
local component = luci.http.formvalue("component")
local data, status, err = read_geo_state(component, "local_info")
if not data then
luci.http.status(status or 500, "Bad Request")
write_geo_json({component = component, error = err or "bad_request"})
return
end
write_geo_json(data)
end
function geo_upgrade()
local component = luci.http.formvalue("component")
local data, status, err = read_geo_state(component, "upgrade")
if not data then
luci.http.status(status or 500, "Bad Request")
write_geo_json({component = component, error = err or "bad_request", success = "0"})
return
end
write_geo_json(data)
end
function act_status()
local e = {}
e.running = global_client_running()
if e.running then
e.status_text = get_running_status_text()
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function clash_panel()
local sid = luci.http.formvalue("sid")
if uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then
luci.http.status(404, "Not Found")
return
end
luci.template.render("shadowsocksr/clash_panel", {
sid = sid,
alias = uci:get("shadowsocksr", sid, "alias") or sid
})
end
function clash_groups()
local sid = luci.http.formvalue("sid")
local groups = {}
local active_sid = resolve_active_clash_sid(sid)
local active = active_sid ~= nil
if active then
local raw = clash_api_request(active_sid, "GET", "/proxies")
if raw then
groups = parse_clash_groups(raw.body)
end
end
luci.http.prepare_content("application/json")
luci.http.write_json({
active = active,
sid = active_sid,
groups = groups
})
end
function clash_switch()
local sid = luci.http.formvalue("sid")
local group = luci.http.formvalue("group")
local name = luci.http.formvalue("name")
if not sid or not group or not name then
luci.http.status(400, "Bad Request")
return
end
local active_sid = resolve_active_clash_sid(sid)
if not active_sid then
luci.http.status(409, "Conflict")
luci.http.prepare_content("application/json")
luci.http.write_json({success = false, message = "inactive"})
return
end
local body = string.format('{"name":"%s"}', tostring(name):gsub('"', '\\"'))
local path = "/proxies/" .. urlencode(group)
local ret = clash_api_request(active_sid, "PUT", path, body)
luci.http.prepare_content("application/json")
luci.http.write_json({success = ret ~= nil and ret.code and ret.code >= 200 and ret.code < 300, sid = active_sid})
end
function clash_refresh()
local sid = luci.http.formvalue("sid")
if not sid or uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then
luci.http.status(400, "Bad Request")
luci.http.prepare_content("application/json")
luci.http.write_json({success = false})
return
end
local cmd = string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1", luci.util.shellquote(sid))
local ok = luci.sys.call(cmd) == 0
local reapplied = false
if ok and is_active_clash_node(sid) then
luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
reapplied = true
end
luci.http.prepare_content("application/json")
luci.http.write_json({
success = ok,
cached = nixio.fs.access(get_clash_cache_file(sid)),
reapplied = reapplied
})
end
function clash_reset_defaults()
local sid = luci.http.formvalue("sid")
if not sid or uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then
luci.http.status(400, "Bad Request")
luci.http.prepare_content("application/json")
luci.http.write_json({success = false})
return
end
local state_file = get_clash_state_file(sid)
local cleared = false
if nixio.fs.access(state_file) then
cleared = nixio.fs.remove(state_file) or false
else
cleared = true
end
local reapplied = false
if cleared and is_active_clash_node(sid) then
luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
reapplied = true
end
luci.http.prepare_content("application/json")
luci.http.write_json({
success = cleared,
reapplied = reapplied
})
end
function clash_client_policies()
local sid = luci.http.formvalue("sid")
local active_sid = resolve_active_clash_sid(sid)
local groups = {}
if active_sid then
local raw = clash_api_request(active_sid, "GET", "/proxies")
if raw then
groups = parse_clash_groups(raw.body)
end
end
local policies = {}
local seen = {}
for _, group in ipairs(groups or {}) do
if group.name and not seen[group.name] then
seen[group.name] = true
policies[#policies + 1] = {
name = group.name,
label = group.name,
type = group.type or ""
}
end
for _, proxy_name in ipairs(group.all or {}) do
if proxy_name and proxy_name ~= "" and not seen[proxy_name] then
seen[proxy_name] = true
policies[#policies + 1] = {
name = proxy_name,
label = proxy_name,
type = "proxy"
}
end
end
end
table.sort(policies, function(a, b)
return tostring(a.label or a.name) < tostring(b.label or b.name)
end)
luci.http.prepare_content("application/json")
luci.http.write_json({
active = active_sid ~= nil,
sid = active_sid,
clients = collect_lan_clients(),
rules = read_clash_client_rules(sid),
policies = policies
})
end
function clash_client_rule_save()
local sid = luci.http.formvalue("sid")
local rows = {}
local max_rows = tonumber(luci.http.formvalue("count") or "0") or 0
for index = 1, math.min(max_rows, 256) do
local prefix = string.format("rule_%d_", index)
local ip_addr = normalize_client_ip(luci.http.formvalue(prefix .. "ip_addr"))
local enabled = luci.http.formvalue(prefix .. "enabled") == "1" and "1" or "0"
local remarks = trim(luci.http.formvalue(prefix .. "remarks"))
local policy_group = trim(luci.http.formvalue(prefix .. "policy_group"))
local client_mac = sanitize_mac(luci.http.formvalue(prefix .. "client_mac"))
if ip_addr ~= "" and policy_group ~= "" then
rows[#rows + 1] = {
enabled = enabled,
remarks = remarks,
ip_addr = ip_addr,
client_mac = client_mac,
policy_group = policy_group
}
end
end
if not sid or sid == "" or uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then
luci.http.status(400, "Bad Request")
luci.http.prepare_content("application/json")
luci.http.write_json({ success = false, error = "invalid_sid" })
return
end
write_clash_client_rules_csv(sid, rows)
local active_sid = resolve_active_clash_sid(sid)
local current_sid = uci:get_first("shadowsocksr", "global", "global_server")
local reapplied = false
if sid and sid ~= "" and uci:get("shadowsocksr", sid) == "servers"
and uci:get("shadowsocksr", sid, "type") == "clash"
and current_sid == sid then
luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
reapplied = true
end
luci.http.prepare_content("application/json")
luci.http.write_json({
success = true,
count = #rows,
reapplied = reapplied,
rules = read_clash_client_rules(sid)
})
end
function clash_client_rule_clear()
local sid = luci.http.formvalue("sid")
if not sid or sid == "" or uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then
luci.http.status(400, "Bad Request")
luci.http.prepare_content("application/json")
luci.http.write_json({ success = false, error = "invalid_sid" })
return
end
local csv_path = get_clash_client_rule_csv_path(sid)
if csv_path and nixio.fs.access(csv_path) then
nixio.fs.remove(csv_path)
end
local current_sid = uci:get_first("shadowsocksr", "global", "global_server")
local reapplied = false
if sid and sid ~= "" and uci:get("shadowsocksr", sid) == "servers"
and uci:get("shadowsocksr", sid, "type") == "clash"
and current_sid == sid then
luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
reapplied = true
end
luci.http.prepare_content("application/json")
luci.http.write_json({
success = true,
reapplied = reapplied,
rules = {}
})
end
function act_ping()
local e = {}
local domain = luci.http.formvalue("domain")
local port = tonumber(luci.http.formvalue("port") or 0)
local transport = (luci.http.formvalue("transport") or ""):lower()
local wsPath = luci.http.formvalue("wsPath") or ""
local host = luci.http.formvalue("host") or ""
local tls_host = luci.http.formvalue("tlsHost") or ""
local tls = luci.http.formvalue("tls")
local type = (luci.http.formvalue("type") or ""):lower()
local proto = (luci.http.formvalue("proto") or ""):lower()
local reality = luci.http.formvalue("reality")
local sid = luci.http.formvalue("sid")
e.index = luci.http.formvalue("index")
if sid and sid ~= "" and uci:get("shadowsocksr", sid) == "servers" then
domain = uci:get("shadowsocksr", sid, "server") or domain
port = tonumber(uci:get("shadowsocksr", sid, "server_port") or port or 0)
transport = (uci:get("shadowsocksr", sid, "transport") or transport or ""):lower()
wsPath = uci:get("shadowsocksr", sid, "ws_path") or wsPath
host = uci:get("shadowsocksr", sid, "ws_host") or host
tls_host = uci:get("shadowsocksr", sid, "tls_host") or tls_host
tls = uci:get("shadowsocksr", sid, "tls") or tls
type = (uci:get("shadowsocksr", sid, "type") or type or ""):lower()
proto = (uci:get("shadowsocksr", sid, "v2ray_protocol") or proto or ""):lower()
reality = uci:get("shadowsocksr", sid, "reality") or reality
end
local is_ip = domain and domain:match("^%d+%.%d+%.%d+%.%d+$")
local probe_host = (tls_host ~= "" and tls_host) or (host ~= "" and host) or domain
local is_reality = (reality == "1" or reality == "true")
local prefers_handshake_latency = (type == "v2ray") and not is_reality
-- 临时放行防火墙逻辑
local use_nft = use_fw4_backend()
local iret = false
if domain then
if use_nft then
iret = luci.sys.call("nft add element inet ss_spec ss_spec_wan_ac { " .. domain .. " } 2>/dev/null") == 0
else
iret = luci.sys.call("ipset add ss_spec_wan_ac " .. domain .. " 2>/dev/null") == 0
end
end
-- Hysteria2 节点轻量 UDP 端口检测
if proto:find("hysteria2") or type:find("hysteria2") then
local udp_cmd = string.format("nping --udp -c 1 -p %d %s 2>/dev/null", port, domain)
local udp_raw = luci.sys.exec(udp_cmd) or ""
local udp_rtt = udp_raw:match("Avg rtt:%s*([0-9.]+)ms")
local udp_unreachable = udp_raw:match("[Pp]ort [Uu]nreachable") or udp_raw:match("ICMP")
local udp_sent = udp_raw:match("Raw packets sent:%s*1")
-- UDP 服务通常不会主动回包,未收到应答不等于端口不可用。
-- 仅在出现明显的不可达迹象时标记 fail其余视为轻量可达。
e.socket = (udp_unreachable == nil) and (udp_sent ~= nil)
e.ping = udp_rtt and normalize_ping_ms(udp_rtt) or nil
if not e.ping then
local icmp_cmd = string.format("ping -c 1 -W 1 %s 2>/dev/null | grep -o 'time=[0-9.]*' | cut -d= -f2", domain)
e.ping = normalize_ping_ms(tonumber(luci.sys.exec(icmp_cmd)))
end
if not e.ping then
e.ping = 0
end
elseif transport == "ws" then
-- WebSocket 探测
local result = ""
local success = false
local icmp_cmd = string.format("ping -c 1 -W 1 %s 2>/dev/null | grep -o 'time=[0-9.]*' | cut -d= -f2", domain)
e.ping = normalize_ping_ms(tonumber(luci.sys.exec(icmp_cmd)))
-- WebSocket 探测 (适用于域名,或带 SNI 的 IP)
if not is_ip or probe_host ~= domain then
local resolve_arg = ""
local final_domain = probe_host
if is_ip and probe_host and probe_host ~= "" then
-- IP 模式下使用 --resolve 强制指定 SNI解决 TLS 握手失败
resolve_arg = string.format("--resolve '%s:%d:%s' ", probe_host, port, domain)
end
local prefix = (tls == '1') and "https://" or "http://"
local address = prefix .. final_domain .. ':' .. port .. wsPath
local cmd = string.format(
"curl --http1.1 -m 2 -ksN -o /dev/null %s" ..
"-w 'time_connect=%%{time_connect}\\nhttp_code=%%{http_code}' " ..
"%s" ..
"-H 'Connection: Upgrade' -H 'Upgrade: websocket' " ..
"-H 'Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==' " ..
"-H 'Sec-WebSocket-Version: 13' '%s'",
resolve_arg,
(probe_host and probe_host ~= "") and ("-H " .. luci.util.shellquote("Host: " .. probe_host) .. " ") or "",
address
)
result = luci.sys.exec(cmd) or ""
success = (string.match(result, "http_code=(%d+)") == "101")
end
-- 如果深度探测失败,或是不支持深测的纯 IP
if not success then
local socket = nixio.socket("inet", "stream")
if socket then
socket:setopt("socket", "rcvtimeo", 3)
socket:setopt("socket", "sndtimeo", 3)
success = socket:connect(domain, port)
socket:close()
end
--luci.sys.exec(string.format("echo 'Node %s (ws) failed deep test, using TCP fallback' >> /tmp/ping.log", domain))
end
e.socket = success
-- 延迟:优先 ping再 curl最后 nping tcp-connect
if not e.ping then
local ping_time = tonumber(string.match(result, "time_connect=(%d+.%d%d%d)"))
local appconnect_time = tonumber(string.match(result, "time_appconnect=(%d+.%d%d%d)"))
if appconnect_time and appconnect_time > 0 then
e.ping = normalize_ping_ms(appconnect_time, 1000)
elseif ping_time and ping_time > 0 then
e.ping = normalize_ping_ms(ping_time, 1000)
else
e.ping = detect_tcp_connect_ms(domain, port) or 0
end
end
else
-- 3. 非 WebSocket 节点的探测逻辑 (TCP / ICMP / UDP)
local socket = nixio.socket("inet", "stream")
if socket then
socket:setopt("socket", "rcvtimeo", 3)
socket:setopt("socket", "sndtimeo", 3)
e.socket = socket:connect(domain, port)
socket:close()
end
if prefers_handshake_latency and (tls == "1" or tls_host ~= "" or proto == "vless" or proto == "vmess") then
e.ping = detect_tls_handshake_ms(domain, port, "", probe_host, domain, false)
end
-- 延迟:优先真实握手,再 nping tcp-connect -> ping -> nping(udp)
if not e.ping then
if not is_reality then
e.ping = detect_tcp_connect_ms(domain, port)
end
end
if not e.ping then
local icmp_cmd = string.format("ping -c 1 -W 1 %s 2>/dev/null | grep -o 'time=[0-9.]*' | cut -d= -f2", domain)
e.ping = normalize_ping_ms(tonumber(luci.sys.exec(icmp_cmd)))
end
if not e.ping then
local udp_cmd = string.format("nping --udp -c 1 -p %d %s 2>/dev/null | grep -o 'Avg rtt: [0-9.]*ms' | awk '{print $3}' | sed 's/ms//' | head -1", port, domain)
local udp_res = luci.sys.exec(udp_cmd)
if udp_res and udp_res ~= "" then
local ping_num = tonumber(udp_res)
if ping_num then e.ping = normalize_ping_ms(ping_num) end
end
end
if (not e.ping or e.ping == 0) and domain and port > 0 then
local schemes = { "https", "http" }
for _, scheme in ipairs(schemes) do
local connect_cmd = string.format(
"curl -m 2 -ksS -o /dev/null -w 'time_connect=%%{time_connect}' %s://%s:%d 2>/dev/null",
scheme, domain, port
)
local connect_res = luci.sys.exec(connect_cmd) or ""
local connect_time = tonumber(connect_res:match("time_connect=([0-9.]+)"))
if connect_time and connect_time > 0 then
e.ping = normalize_ping_ms(connect_time, 1000)
break
end
end
end
end
-- 4. 清理防火墙规则
if iret then
if use_nft then
luci.sys.call("nft delete element inet ss_spec ss_spec_wan_ac { " .. domain .. " } 2>/dev/null")
else
luci.sys.call("ipset del ss_spec_wan_ac " .. domain .. " 2>/dev/null")
end
end
if sid and sid ~= "" then
save_detect_cache_entry(sid, {
server = domain or "",
port = port or 0,
type = type or "",
proto = proto or "",
socket = e.socket and true or false,
ping = tonumber(e.ping) or 0,
time = os.time()
})
end
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function check_status()
local e = {}
local target = luci.http.formvalue("set") or ""
e.ret = luci.sys.call("curl -m 3 -sS -o /dev/null http://www." .. target .. ".com >/dev/null 2>&1")
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function refresh_data()
local set = luci.http.formvalue("set")
local retstring = loadstring("return " .. luci.sys.exec("/usr/bin/lua /usr/share/shadowsocksr/update.lua " .. set))()
luci.http.prepare_content("application/json")
luci.http.write_json(retstring)
end
function check_port()
local retstring = "<br /><br />"
local s
local server_name = ""
local uci = require "luci.model.uci".cursor()
local use_nft = use_fw4_backend()
uci:foreach("shadowsocksr", "servers", function(s)
if s.type == "clash" then
retstring = retstring .. string.format("<font><b style='color:gray'>[%s] Clash panel node.</b></font><br />", s.alias or s[".name"])
return
end
if s.alias then
server_name = s.alias
elseif s.server and s.server_port then
server_name = s.server .. ":" .. s.server_port
end
-- 临时加入 set
local is_ipv6 = is_ipv6_address(s.server)
local iret = false
if not is_ipv6 then
if use_nft then
iret = luci.sys.call("nft add element inet ss_spec ss_spec_wan_ac { " .. s.server .. " } 2>/dev/null") == 0
else
iret = luci.sys.call("ipset add ss_spec_wan_ac " .. s.server .. " 2>/dev/null") == 0
end
end
-- TCP 测试
local socket = nixio.socket(is_ipv6 and "inet6" or "inet", "stream")
socket:setopt("socket", "rcvtimeo", 3)
socket:setopt("socket", "sndtimeo", 3)
local ret = socket:connect(s.server, s.server_port)
socket:close()
if ret then
retstring = retstring .. string.format("<font><b style='color:green'>[%s] OK.</b></font><br />", server_name)
else
retstring = retstring .. string.format("<font><b style='color:red'>[%s] Error.</b></font><br />", server_name)
end
-- 删除临时 set
if iret then
if use_nft then
luci.sys.call("nft delete element inet ss_spec ss_spec_wan_ac { " .. s.server .. " } 2>/dev/null")
else
luci.sys.call("ipset del ss_spec_wan_ac " .. s.server)
end
end
end)
luci.http.prepare_content("application/json")
luci.http.write_json({ret = retstring})
end
function act_reset()
luci.sys.call("/etc/init.d/shadowsocksr reset >/dev/null 2>&1")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr"))
end
function act_restart()
luci.sys.call("/etc/init.d/shadowsocksr restart > /dev/null 2>&1 &")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr"))
end
function act_delete()
luci.sys.call("/etc/init.d/shadowsocksr restart > /dev/null 2>&1 &")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers"))
end
function get_log()
luci.http.write(luci.sys.exec("[ -f '/var/log/ssrplus.log' ] && cat /var/log/ssrplus.log"))
end
function clear_log()
luci.sys.call("echo '' > /var/log/ssrplus.log")
end
function create_backup()
local backup_files = {
"/etc/config/shadowsocksr",
"/etc/ssrplus/*"
}
local date = os.date("%Y-%m-%d-%H-%M-%S")
local tar_file = "/tmp/shadowsocksr-" .. date .. "-backup.tar.gz"
nixio.fs.remove(tar_file)
local cmd = "tar -czf " .. tar_file .. " " .. table.concat(backup_files, " ")
luci.sys.call(cmd)
luci.http.header("Content-Disposition", "attachment; filename=shadowsocksr-" .. date .. "-backup.tar.gz")
luci.http.header("X-Backup-Filename", "shadowsocksr-" .. date .. "-backup.tar.gz")
luci.http.prepare_content("application/octet-stream")
luci.http.write(nixio.fs.readfile(tar_file))
nixio.fs.remove(tar_file)
end