op-packages/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua
2026-03-22 12:50:34 +08:00

477 lines
17 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"
local uci = require("luci.model.uci").cursor()
local function sh_uci_commit(config)
luci.sys.call(string.format("uci -q commit %s", config))
end
local function is_old_uci()
return luci.sys.call("grep -E 'require[ \t]*\"uci\"' /usr/lib/lua/luci/model/uci.lua >/dev/null 2>&1") == 0
end
local function uci_save(cursor, config, commit, apply)
if is_old_uci() then
cursor:save(config)
if commit then
cursor:commit(config)
if apply then
luci.sys.call("/etc/init.d/" .. config .. " reload > /dev/null 2>&1 &")
end
end
else
commit = true
if commit then
if apply then
cursor:commit(config)
else
sh_uci_commit(config)
end
end
end
end
local function url(...)
local url = string.format("admin/services/%s", "shadowsocksr")
local args = { ... }
for i, v in ipairs(args) do
if v and v ~= "" then
url = url .. "/" .. v
end
end
return require "luci.dispatcher".build_url(url)
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", {autoapply = true}), 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", "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", "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", "reset"}, call("act_reset"))
entry({"admin", "services", "shadowsocksr", "restart"}, call("act_restart"))
entry({"admin", "services", "shadowsocksr", "delete"}, call("act_delete"))
--[[ API ]]
entry({"admin", "services", "shadowsocksr", "add_node"}, call("act_add_node")).leaf = true
entry({"admin", "services", "shadowsocksr", "remove_node"}, call("act_remove"))
entry({"admin", "services", "shadowsocksr", "save_node_order"}, call("act_save_order")).leaf = true
entry({"admin", "services", "shadowsocksr", "get_now_use_node"}, call("act_get_now_use_node")).leaf = true
entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况
--[[Backup]]
entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true
end
function check_site(host, port)
local nixio = require "nixio"
local socket = nixio.socket("inet", "stream")
socket:setopt("socket", "rcvtimeo", 2)
socket:setopt("socket", "sndtimeo", 2)
local ret = socket:connect(host, port)
socket:close()
return ret
end
function get_ip_geo_info()
local result = luci.sys.exec('curl --retry 3 -m 10 -LfsA "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36" http://ip-api.com/json/')
local json = require "luci.jsonc"
local info = json.parse(result)
return {
flag = string.lower(info.countryCode) or "un",
country = get_country_name(info.countryCode) or "Unknown",
ip = info.query,
isp = info.isp
}
end
function get_country_name(countryCode)
local country_names = {
US = "美国", CN = "中国", JP = "日本", GB = "英国", DE = "德国",
FR = "法国", BR = "巴西", IT = "意大利", RU = "俄罗斯", CA = "加拿大",
KR = "韩国", ES = "西班牙", AU = "澳大利亚", MX = "墨西哥", ID = "印度尼西亚",
NL = "荷兰", TR = "土耳其", CH = "瑞士", SA = "沙特阿拉伯", SE = "瑞典",
PL = "波兰", BE = "比利时", AR = "阿根廷", NO = "挪威", AT = "奥地利",
TW = "台湾", ZA = "南非", TH = "泰国", DK = "丹麦", MY = "马来西亚",
PH = "菲律宾", SG = "新加坡", IE = "爱尔兰", HK = "香港", FI = "芬兰",
CL = "智利", PT = "葡萄牙", GR = "希腊", IL = "以色列", NZ = "新西兰",
CZ = "捷克", RO = "罗马尼亚", VN = "越南", UA = "乌克兰", HU = "匈牙利",
AE = "阿联酋", CO = "哥伦比亚", IN = "印度", EG = "埃及", PE = "秘鲁", TW = "台湾"
}
return country_names[countryCode]
end
function check_ip()
local e = {}
local port = 80
local geo_info = get_ip_geo_info(ip)
e.ip = geo_info.ip
e.flag = geo_info.flag
e.country = geo_info.country
e.isp = geo_info.isp
e.baidu = check_site('www.baidu.com', port)
e.taobao = check_site('www.taobao.com', port)
e.google = check_site('www.google.com', port)
e.youtube = check_site('www.youtube.com', port)
luci.http.prepare_content('application/json')
luci.http.write_json(e)
end
function subscribe()
luci.sys.call("/usr/bin/lua /usr/share/shadowsocksr/subscribe.lua >>/var/log/ssrplus.log")
luci.http.prepare_content("application/json")
luci.http.write_json({ret = 1})
end
function act_status()
local e = {}
e.running = luci.sys.call("busybox ps -w | grep ssr-retcp | grep -v grep >/dev/null") == 0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
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 tls = luci.http.formvalue("tls")
local host = luci.http.formvalue("host")
local type = (luci.http.formvalue("type") or ""):lower()
local proto = (luci.http.formvalue("proto") or ""):lower()
e.index = luci.http.formvalue("index")
local is_ip = domain and domain:match("^%d+%.%d+%.%d+%.%d+$")
-- 临时放行防火墙逻辑
local use_nft = luci.sys.call("command -v nft >/dev/null") == 0
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 节点检测
if proto:find("hysteria2") or type:find("hysteria2") then
local node_id = e.index
-- 调用Shell测试脚本
local cmd = string.format(
"/usr/share/shadowsocksr/hy2_test.sh url_test_hy2 %s",
node_id
)
local res = luci.sys.exec(cmd) or ""
-- 解析结果
local http_code, time_pre = string.match(res, "(%d+):([%d%.]+)")
if http_code == "200" or http_code == "204" then
e.socket = true
e.ping = math.floor(tonumber(time_pre or 0) * 1000)
else
e.socket = false
e.ping = 0
end
elseif transport == "ws" then
-- WebSocket 探测
local result = ""
local success = false
-- WebSocket 探测 (适用于域名,或带 SNI 的 IP)
if not is_ip or (host and host ~= "") then
local resolve_arg = ""
local final_domain = domain
if is_ip and host and host ~= "" then
-- IP 模式下使用 --resolve 强制指定 SNI解决 TLS 握手失败
resolve_arg = string.format("--resolve '%s:%d:%s' ", host, port, domain)
final_domain = host
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}' " ..
"-H 'Connection: Upgrade' -H 'Upgrade: websocket' " ..
"-H 'Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==' " ..
"-H 'Sec-WebSocket-Version: 13' '%s'",
resolve_arg, 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
-- 解析延迟 (优先用 curl 数据,失败则回退到 tcping)
local ping_time = tonumber(string.match(result, "time_connect=(%d+.%d%d%d)"))
if ping_time and ping_time > 0 then
e.ping = math.floor(ping_time * 1000)
else
local tcping_cmd = string.format("tcping -q -c 1 -t 1 -p %d %s 2>/dev/null | grep -o 'time=[0-9]*' | cut -d= -f2", port, domain)
e.ping = tonumber(luci.sys.exec(tcping_cmd)) or 0
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
-- 延迟tcping -> ping -> nping(udp)
local tcping_cmd = string.format("tcping -q -c 1 -t 1 -p %d %s 2>/dev/null | grep -o 'time=[0-9]*' | cut -d= -f2", port, domain)
e.ping = tonumber(luci.sys.exec(tcping_cmd))
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 = 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 = math.floor(ping_num) 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
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function check_status()
local e = {}
e.ret = luci.sys.call("/usr/bin/ssr-check www." .. luci.http.formvalue("set") .. ".com 80 3 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 = luci.sys.call("command -v nft >/dev/null") == 0
uci:foreach("shadowsocksr", "servers", function(s)
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 iret = false
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
-- TCP 测试
local socket = nixio.socket("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.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr"))
end
function act_delete()
uci:delete_all("shadowsocksr", "servers", function(s)
if s.hashkey or s.isSubscribe then
return true
else
return false
end
end)
uci:commit("shadowsocksr")
for file in nixio.fs.glob("/tmp/sub_md5_*") do
nixio.fs.remove(file)
end
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 act_add_node()
local redirect = luci.http.formvalue("redirect")
local used_sid = {}
local next_sid = 1
uci:foreach("shadowsocksr", "servers", function(s)
local num = s[".name"]:match("^cfg(%x%x)")
if num then
local n = tonumber(num, 16)
used_sid[n] = true
end
end)
local function get_next_sid()
while used_sid[next_sid] do
next_sid = next_sid + 1
end
used_sid[next_sid] = true
return next_sid
end
local sid = uci:section("shadowsocksr", "servers", nil)
local suffix = sid:sub(-4)
uci:delete("shadowsocksr", sid)
local id = get_next_sid()
local cfgid = string.format("cfg%02x%s", id, suffix)
uci:section("shadowsocksr", "servers", cfgid)
uci_save(uci, "shadowsocksr")
if redirect == "1" then
luci.http.redirect(url("servers", cfgid))
else
luci.http.write_json({ result = cfgid })
end
end
function act_remove()
local id = luci.http.formvalue("id")
if id then
uci:delete("shadowsocksr", id)
uci:commit("shadowsocksr")
end
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers"))
end
function act_save_order()
local ids = luci.http.formvalue("ids") or ""
local new_order = {}
for id in ids:gmatch("([^,]+)") do
new_order[#new_order + 1] = id
end
for idx, name in ipairs(new_order) do
luci.sys.call(string.format("uci -q reorder %s.%s=%d", "shadowsocksr", name, idx - 1))
end
sh_uci_commit("shadowsocksr")
luci.http.write_json({ status = "ok" })
end
function act_get_now_use_node()
local result = {}
local tcp_node = uci:get_first("shadowsocksr", "global", "global_server")
if tcp_node then
result["TCP"] = tcp_node
end
local udp_node = uci:get_first("shadowsocksr", "global", "udp_relay_server")
if udp_node then
result["UDP"] = udp_node
end
local netflix_node = uci:get_first("shadowsocksr", "global", "netflix_server")
if netflix_node then
result["netflix"] = netflix_node
end
local socks5_node = uci:get_first("shadowsocksr", "socks5_proxy", "server")
if socks5_node then
result["socks5"] = socks5_node
end
luci.http.prepare_content("application/json")
luci.http.write_json(result)
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