mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-07-27 08:31:44 +08:00
update 2026-05-23 01:48:24
This commit is contained in:
parent
3cb04f969c
commit
3a16aba72c
@ -16,14 +16,11 @@ PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin
|
||||
|
||||
LUCI_TITLE:=LuCI support for PassWall 2
|
||||
@ -100,17 +97,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy
|
||||
select PACKAGE_naiveproxy
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client
|
||||
bool "Include Shadowsocks Libev Client"
|
||||
select PACKAGE_shadowsocks-libev-ss-local
|
||||
select PACKAGE_shadowsocks-libev-ss-redir
|
||||
default y
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server
|
||||
bool "Include Shadowsocks Libev Server"
|
||||
select PACKAGE_shadowsocks-libev-ss-server
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client
|
||||
bool "Include Shadowsocks Rust Client"
|
||||
depends on !i386
|
||||
@ -139,12 +125,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs
|
||||
select PACKAGE_simple-obfs-client
|
||||
default y
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client
|
||||
bool "Include tuic-client"
|
||||
depends on aarch64||arm||i386||x86_64
|
||||
select PACKAGE_tuic-client
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin
|
||||
bool "Include V2ray-Plugin (Shadowsocks Plugin)"
|
||||
select PACKAGE_v2ray-plugin
|
||||
|
||||
@ -59,6 +59,7 @@ function index()
|
||||
entry({"admin", "services", appname, "server_user"}, cbi(appname .. "/server/user")).leaf = true
|
||||
|
||||
--[[ API ]]
|
||||
entry({"admin", "services", appname, "server_user_update"}, call("server_user_update")).leaf = true
|
||||
entry({"admin", "services", appname, "server_user_status"}, call("server_user_status")).leaf = true
|
||||
entry({"admin", "services", appname, "server_user_log"}, call("server_user_log")).leaf = true
|
||||
entry({"admin", "services", appname, "server_get_log"}, call("server_get_log")).leaf = true
|
||||
@ -629,6 +630,23 @@ function rollback_rules()
|
||||
http_write_json_ok()
|
||||
end
|
||||
|
||||
function server_user_update()
|
||||
local id = http.formvalue("id") -- Node id
|
||||
local data = http.formvalue("data") -- json new Data
|
||||
if id and data then
|
||||
local data_t = jsonParse(data) or {}
|
||||
if next(data_t) then
|
||||
for k, v in pairs(data_t) do
|
||||
uci:set(appname .. "_server", id, k, v)
|
||||
end
|
||||
api.uci_save(uci, appname .. "_server")
|
||||
http_write_json_ok()
|
||||
return
|
||||
end
|
||||
end
|
||||
http_write_json_error()
|
||||
end
|
||||
|
||||
function server_user_status()
|
||||
local e = {}
|
||||
e.index = http.formvalue("index")
|
||||
|
||||
@ -78,103 +78,10 @@ o.rmempty = false
|
||||
|
||||
o = s:option(Value, "interface", translate("Source Interface"))
|
||||
o:value("", translate("All"))
|
||||
-- Populate with actual kernel network devices instead of UCI interface names,
|
||||
-- because the backend (nftables iifname / iptables -i) matches kernel device names.
|
||||
do
|
||||
local nfs = require "nixio.fs"
|
||||
local _cursor = require("luci.model.uci").cursor()
|
||||
local _sysnet = "/sys/class/net/"
|
||||
|
||||
-- Map UCI interface names to their device names and vice versa
|
||||
local _iface_to_dev = {}
|
||||
local _dev_to_ifaces = {}
|
||||
local _iface_proto = {}
|
||||
_cursor:foreach("network", "interface", function(sec)
|
||||
local name = sec[".name"]
|
||||
if name ~= "loopback" then
|
||||
_iface_proto[name] = sec.proto
|
||||
if sec.device then
|
||||
_iface_to_dev[name] = sec.device
|
||||
_dev_to_ifaces[sec.device] = _dev_to_ifaces[sec.device] or {}
|
||||
table.insert(_dev_to_ifaces[sec.device], name)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Classify device type using sysfs attributes
|
||||
local function classify_sysfs(dev)
|
||||
if nfs.stat(_sysnet .. dev .. "/bridge", "type") == "dir" then
|
||||
return translate("Bridge")
|
||||
elseif nfs.stat(_sysnet .. dev .. "/wireless", "type") == "dir" then
|
||||
return translate("Wireless Adapter")
|
||||
elseif dev:match("^tun") or dev:match("^tap") or dev:match("^wg") or dev:match("^ppp") then
|
||||
return translate("Tunnel Interface")
|
||||
else
|
||||
return translate("Ethernet Adapter")
|
||||
end
|
||||
end
|
||||
|
||||
-- Classify offline UCI interfaces by config hints
|
||||
local function classify_uci(dev_name, proto)
|
||||
if dev_name and dev_name:match("^br%-") then
|
||||
return translate("Bridge")
|
||||
elseif proto == "wireguard" or proto == "pppoe" or proto == "pptp" or proto == "l2tp" then
|
||||
return translate("Tunnel Interface")
|
||||
else
|
||||
return translate("Interface")
|
||||
end
|
||||
end
|
||||
|
||||
local _seen = {}
|
||||
local _devices = {}
|
||||
|
||||
-- Active kernel devices from /sys/class/net/.
|
||||
-- Skip bridge member ports (/master) and DSA master devices (/dsa) because
|
||||
-- nftables iifname matches the parent bridge for routed traffic, not
|
||||
-- individual member ports. Also skip internal virtual devices.
|
||||
local _iter = nfs.dir(_sysnet)
|
||||
if _iter then
|
||||
for dev in _iter do
|
||||
if dev ~= "lo"
|
||||
and not dev:match("^veth")
|
||||
and not dev:match("^ifb")
|
||||
and not dev:match("^gre")
|
||||
and not dev:match("^sit")
|
||||
and not dev:match("^ip6tnl")
|
||||
and not dev:match("^erspan")
|
||||
and not nfs.stat(_sysnet .. dev .. "/master", "type")
|
||||
and not nfs.stat(_sysnet .. dev .. "/dsa", "type")
|
||||
then
|
||||
local dtype = classify_sysfs(dev)
|
||||
local label = dtype .. ': "' .. dev .. '"'
|
||||
if _dev_to_ifaces[dev] then
|
||||
label = label .. " (" .. table.concat(_dev_to_ifaces[dev], ", ") .. ")"
|
||||
end
|
||||
_devices[#_devices + 1] = { name = dev, label = label, sort = dtype .. ":" .. dev }
|
||||
_seen[dev] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- UCI interfaces whose device does not currently exist (down tunnels, VPNs, etc.).
|
||||
-- Stored by UCI name since the kernel device is not available yet.
|
||||
-- Dedup by device: if two interfaces share a device, only one is shown.
|
||||
for iface, dev in pairs(_iface_to_dev) do
|
||||
if not _seen[dev] then
|
||||
local dtype = classify_uci(dev, _iface_proto[iface])
|
||||
local label = dtype .. ': "' .. iface .. '"'
|
||||
-- Sort offline entries after active devices
|
||||
_devices[#_devices + 1] = { name = iface, label = label, sort = "zzz:" .. iface }
|
||||
_seen[dev] = true
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(_devices, function(a, b) return a.sort < b.sort end)
|
||||
for _, d in ipairs(_devices) do
|
||||
o:value(d.name, d.label)
|
||||
end
|
||||
local iface = api.get_network_devices()
|
||||
for _, d in ipairs(iface) do
|
||||
o:value(d.name, d.label)
|
||||
end
|
||||
|
||||
o.validate = function(self, value, section)
|
||||
if value == "" or value:match("^[a-zA-Z0-9][a-zA-Z0-9%.%_%-]*$") then
|
||||
return value
|
||||
|
||||
@ -14,6 +14,7 @@ formvalue_key = "cbid." .. appname .. "." .. arg[1] .. "."
|
||||
|
||||
local header = Template(appname .. "/node_config/header")
|
||||
header.api = api
|
||||
header.config = m.config
|
||||
header.section = arg[1]
|
||||
m:append(header)
|
||||
|
||||
@ -91,6 +92,7 @@ end
|
||||
|
||||
local footer = Template(appname .. "/node_config/footer")
|
||||
footer.api = api
|
||||
footer.config = m.config
|
||||
footer.section = arg[1]
|
||||
|
||||
m:append(footer)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
local api = require "luci.passwall2.api"
|
||||
local appname = api.appname
|
||||
local uci = api.uci
|
||||
local has_ss = api.is_finded("ss-redir")
|
||||
local has_ss_rust = api.is_finded("sslocal")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
@ -12,10 +11,6 @@ local vmess_type = {}
|
||||
local vless_type = {}
|
||||
local hysteria2_type = {}
|
||||
local xray_version = api.get_app_version("xray")
|
||||
if has_ss then
|
||||
local s = "shadowsocks-libev"
|
||||
table.insert(ss_type, s)
|
||||
end
|
||||
if has_ss_rust then
|
||||
local s = "shadowsocks-rust"
|
||||
table.insert(ss_type, s)
|
||||
|
||||
@ -15,7 +15,6 @@ end
|
||||
|
||||
m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
|
||||
|
||||
local has_ss = api.is_finded("ss-redir")
|
||||
local has_ss_rust = api.is_finded("sslocal")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
@ -26,10 +25,6 @@ local vmess_type = {}
|
||||
local vless_type = {}
|
||||
local hysteria2_type = {}
|
||||
local xray_version = api.get_app_version("xray")
|
||||
if has_ss then
|
||||
local s = "shadowsocks-libev"
|
||||
table.insert(ss_type, s)
|
||||
end
|
||||
if has_ss_rust then
|
||||
local s = "shadowsocks-rust"
|
||||
table.insert(ss_type, s)
|
||||
@ -223,6 +218,14 @@ if #hysteria2_type > 0 then
|
||||
for key, value in pairs(hysteria2_type) do
|
||||
o:value(value)
|
||||
end
|
||||
|
||||
o = s:option(Value, "hysteria_up_mbps", "Hy/Hy2 " .. translate("Max upload Mbps"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = "100"
|
||||
|
||||
o = s:option(Value, "hysteria_down_mbps", "Hy/Hy2 " .. translate("Max download Mbps"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = "100"
|
||||
end
|
||||
|
||||
o = s:option(Flag, "boot_update", translate("Update Once on Boot"), translate("Updates the subscription the first time runs automatically after each system boot."))
|
||||
@ -287,6 +290,7 @@ o = s:option(ListValue, "chain_proxy", translate("Chain Proxy"))
|
||||
o:value("", translate("Close(Not use)"))
|
||||
o:value("1", translate("Preproxy Node"))
|
||||
o:value("2", translate("Landing Node"))
|
||||
o:value("3", translate("Outbound Interface"))
|
||||
|
||||
local descrStr = "Chained proxy works only with Xray or Sing-box nodes.<br>"
|
||||
descrStr = descrStr .. "You can only use manual or imported nodes as chained nodes."
|
||||
@ -304,6 +308,14 @@ o2.description = descrStr
|
||||
o2.template = appname .. "/cbi/nodes_listvalue"
|
||||
o2.group = {}
|
||||
|
||||
o3 = s:option(Value, "outbound_iface", translate("Outbound Interface"))
|
||||
o3:depends({ ["chain_proxy"] = "3" })
|
||||
o3:value("", translate("All"))
|
||||
local iface = api.get_network_devices()
|
||||
for _, d in ipairs(iface) do
|
||||
o3:value(d.name, d.label)
|
||||
end
|
||||
|
||||
for k, v in pairs(nodes_table) do
|
||||
if (v.type == "Xray" or v.type == "sing-box") and (not v.chain_proxy or v.chain_proxy == "") and v.add_mode ~= "2" then
|
||||
o1:value(v.id, v.remark)
|
||||
|
||||
@ -78,6 +78,7 @@ if not arg_select_proto:find("_") then
|
||||
load_normal_options = true
|
||||
end
|
||||
|
||||
local netdev_list = api.get_network_devices()
|
||||
local node_list = api.get_node_list()
|
||||
local fallback_list = {}
|
||||
local is_balancer = nil
|
||||
@ -270,8 +271,10 @@ end -- [[ Load balancing End ]]
|
||||
|
||||
if load_iface_options then -- [[ Custom Interface Start ]]
|
||||
o = s:option(Value, _n("iface"), translate("Interface"))
|
||||
o.default = "eth1"
|
||||
o:depends({ [_n("protocol")] = "_iface" })
|
||||
for _, d in ipairs(netdev_list) do
|
||||
o:value(d.name, d.label)
|
||||
end
|
||||
end -- [[ Custom Interface End ]]
|
||||
|
||||
|
||||
@ -793,6 +796,7 @@ if not load_shunt_options then
|
||||
if not (load_iface_options or load_balancing_options) then
|
||||
-- Special node cannot be use pre-proxy.
|
||||
o:value("1", translate("Preproxy Node"))
|
||||
o:value("3", translate("Outbound Interface"))
|
||||
end
|
||||
o:value("2", translate("Landing Node"))
|
||||
|
||||
@ -801,6 +805,13 @@ if not load_shunt_options then
|
||||
o1.template = appname .. "/cbi/nodes_listvalue"
|
||||
o1.group = {}
|
||||
|
||||
o3 = s:option(Value, _n("outbound_iface"), translate("Outbound Interface"))
|
||||
o3:depends({ [_n("chain_proxy")] = "3" })
|
||||
o3:value("", translate("All"))
|
||||
for _, d in ipairs(netdev_list) do
|
||||
o3:value(d.name, d.label)
|
||||
end
|
||||
|
||||
o2 = s:option(ListValue, _n("to_node"), translate("Landing Node"), translate("Only support a layer of proxy."))
|
||||
o2:depends({ [_n("chain_proxy")] = "2" })
|
||||
o2.template = appname .. "/cbi/nodes_listvalue"
|
||||
|
||||
@ -94,6 +94,7 @@ if not arg_select_proto:find("_") then
|
||||
load_normal_options = true
|
||||
end
|
||||
|
||||
local netdev_list = api.get_network_devices()
|
||||
local node_list = api.get_node_list()
|
||||
|
||||
if load_urltest_options then -- [[ URLTest Start ]]
|
||||
@ -198,8 +199,10 @@ end -- [[ URLTest End ]]
|
||||
|
||||
if load_iface_options then -- [[ Custom Interface Start ]]
|
||||
o = s:option(Value, _n("iface"), translate("Interface"))
|
||||
o.default = "eth1"
|
||||
o:depends({ [_n("protocol")] = "_iface" })
|
||||
for _, d in ipairs(netdev_list) do
|
||||
o:value(d.name, d.label)
|
||||
end
|
||||
end -- [[ Custom Interface End ]]
|
||||
|
||||
|
||||
@ -875,8 +878,11 @@ if not load_shunt_options then
|
||||
o1.group = {}
|
||||
|
||||
o3 = s:option(Value, _n("outbound_iface"), translate("Outbound Interface"))
|
||||
o3.default = "eth1"
|
||||
o3:depends({ [_n("chain_proxy")] = "3" })
|
||||
o3:value("", translate("All"))
|
||||
for _, d in ipairs(netdev_list) do
|
||||
o3:value(d.name, d.label)
|
||||
end
|
||||
|
||||
o2 = s:option(ListValue, _n("to_node"), translate("Landing Node"), translate("Only support a layer of proxy."))
|
||||
o2:depends({ [_n("chain_proxy")] = "2", [_n("hysteria2_realms")] = false })
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
local m, s = ...
|
||||
|
||||
if not api.is_finded("ss-local") then
|
||||
return
|
||||
end
|
||||
|
||||
type_name = "SS"
|
||||
|
||||
-- [[ Shadowsocks Libev ]]
|
||||
|
||||
s.fields["type"]:value(type_name, "Shadowsocks Libev")
|
||||
|
||||
if s.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local option_prefix = "ss_"
|
||||
|
||||
local function _n(name)
|
||||
return option_prefix .. name
|
||||
end
|
||||
|
||||
local ss_encrypt_method_list = {
|
||||
"rc4-md5", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr",
|
||||
"aes-192-ctr", "aes-256-ctr", "bf-cfb", "salsa20", "chacha20", "chacha20-ietf",
|
||||
"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
|
||||
"xchacha20-ietf-poly1305"
|
||||
}
|
||||
|
||||
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, _n("port"), translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, _n("password"), translate("Password"))
|
||||
o.password = true
|
||||
|
||||
o = s:option(Value, _n("method"), translate("Encrypt Method"))
|
||||
for a, t in ipairs(ss_encrypt_method_list) do o:value(t) end
|
||||
|
||||
o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 300
|
||||
|
||||
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
|
||||
o.default = 0
|
||||
|
||||
o = s:option(Flag, _n("plugin_enabled"), translate("plugin"))
|
||||
o.default = 0
|
||||
|
||||
o = s:option(ListValue, _n("plugin"), "SIP003 " .. translate("plugin"))
|
||||
o.default = "none"
|
||||
o:value("none", translate("none"))
|
||||
if api.is_finded("xray-plugin") then o:value("xray-plugin") end
|
||||
if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end
|
||||
if api.is_finded("obfs-local") then o:value("obfs-local") end
|
||||
o:depends({ [_n("plugin_enabled")] = true })
|
||||
|
||||
o = s:option(Value, _n("plugin_opts"), translate("opts"))
|
||||
o:depends({ [_n("plugin_enabled")] = true })
|
||||
|
||||
api.luci_types(arg[1], m, s, type_name, option_prefix)
|
||||
@ -1,145 +0,0 @@
|
||||
local m, s = ...
|
||||
|
||||
if not api.is_finded("tuic-client") then
|
||||
return
|
||||
end
|
||||
|
||||
type_name = "TUIC"
|
||||
|
||||
-- [[ TUIC ]]
|
||||
|
||||
s.fields["type"]:value(type_name, "TUIC")
|
||||
|
||||
if s.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local option_prefix = "tuic_"
|
||||
|
||||
local function _n(name)
|
||||
return option_prefix .. name
|
||||
end
|
||||
|
||||
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, _n("port"), translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, _n("uuid"), translate("ID"))
|
||||
o.password = true
|
||||
|
||||
-- Tuic Password for remote server connect
|
||||
o = s:option(Value, _n("password"), translate("TUIC User Password For Connect Remote Server"))
|
||||
o.password = true
|
||||
o.rmempty = true
|
||||
o.default = ""
|
||||
o.rewrite_option = o.option
|
||||
|
||||
--[[
|
||||
-- Tuic username for local socks connect
|
||||
o = s:option(Value, _n("socks_username"), translate("TUIC UserName For Local Socks"))
|
||||
o.rmempty = true
|
||||
o.default = ""
|
||||
o.rewrite_option = o.option
|
||||
|
||||
-- Tuic Password for local socks connect
|
||||
o = s:option(Value, _n("socks_password"), translate("TUIC Password For Local Socks"))
|
||||
o.password = true
|
||||
o.rmempty = true
|
||||
o.default = ""
|
||||
o.rewrite_option = o.option
|
||||
--]]
|
||||
|
||||
o = s:option(Value, _n("ip"), translate("Set the TUIC proxy server ip address"))
|
||||
o.datatype = "ipaddr"
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(ListValue, _n("udp_relay_mode"), translate("UDP relay mode"))
|
||||
o:value("native", translate("native"))
|
||||
o:value("quic", translate("QUIC"))
|
||||
o.default = "native"
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(ListValue, _n("congestion_control"), translate("Congestion control algorithm"))
|
||||
o:value("bbr", translate("BBR"))
|
||||
o:value("cubic", translate("CUBIC"))
|
||||
o:value("new_reno", translate("New Reno"))
|
||||
o.default = "cubic"
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, _n("heartbeat"), translate("Heartbeat interval(second)"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = "3"
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, _n("timeout"), translate("Timeout for establishing a connection to server(second)"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = "8"
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, _n("gc_interval"), translate("Garbage collection interval(second)"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = "3"
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, _n("gc_lifetime"), translate("Garbage collection lifetime(second)"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = "15"
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, _n("send_window"), translate("TUIC send window"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 20971520
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, _n("receive_window"), translate("TUIC receive window"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 10485760
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Value, _n("max_package_size"), translate("TUIC Maximum packet size the socks5 server can receive from external, in bytes"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 1500
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
--Tuic settings for the local inbound socks5 server
|
||||
o = s:option(Flag, _n("dual_stack"), translate("Set if the listening socket should be dual-stack"))
|
||||
o.default = 0
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Flag, _n("disable_sni"), translate("Disable SNI"))
|
||||
o.default = 0
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(Flag, _n("zero_rtt_handshake"), translate("Enable 0-RTT QUIC handshake"))
|
||||
o.default = 0
|
||||
o.rmempty = true
|
||||
o.rewrite_option = o.option
|
||||
|
||||
o = s:option(ListValue, _n("tls_alpn"), translate("TLS ALPN"))
|
||||
o.rmempty = true
|
||||
o.default = ""
|
||||
o:value("", translate("Default"))
|
||||
o:value("h3")
|
||||
o:value("h2")
|
||||
o:value("h3,h2")
|
||||
o:value("http/1.1")
|
||||
o:value("h2,http/1.1")
|
||||
o:value("h3,h2,http/1.1")
|
||||
o:value("spdy/3.1")
|
||||
o:value("h3,spdy/3.1")
|
||||
o.rewrite_option = o.option
|
||||
|
||||
api.luci_types(arg[1], m, s, type_name, option_prefix)
|
||||
@ -1,28 +1,29 @@
|
||||
local m, s = ...
|
||||
|
||||
local api = require "luci.passwall2.api"
|
||||
|
||||
if not api.finded_com("hysteria") then
|
||||
return
|
||||
end
|
||||
|
||||
local type_name = "Hysteria2"
|
||||
|
||||
-- [[ Hysteria2 ]]
|
||||
|
||||
s.fields["type"]:value(type_name, "Hysteria2")
|
||||
|
||||
if s.val["type"] and s.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local option_prefix = "hysteria2_"
|
||||
|
||||
local function _n(name)
|
||||
return option_prefix .. name
|
||||
end
|
||||
|
||||
-- [[ Hysteria2 ]]
|
||||
|
||||
s.fields["type"]:value(type_name, "Hysteria2")
|
||||
|
||||
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
|
||||
|
||||
o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o.rmempty = false
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Flag, _n("realms"), translate("Realms"))
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
local m, s = ...
|
||||
|
||||
local api = require "luci.passwall2.api"
|
||||
|
||||
if not api.finded_com("xray") then
|
||||
return
|
||||
end
|
||||
|
||||
local type_name = "Xray"
|
||||
|
||||
-- [[ Xray ]]
|
||||
|
||||
s.fields["type"]:value(type_name, "Xray")
|
||||
if not s.fields["type"].default then
|
||||
s.fields["type"].default = type_name
|
||||
end
|
||||
|
||||
if s.val["type"] and s.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local option_prefix = "xray_"
|
||||
|
||||
local function _n(name)
|
||||
@ -22,13 +31,6 @@ local header_type_list = {
|
||||
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
|
||||
}
|
||||
|
||||
-- [[ Xray ]]
|
||||
|
||||
s.fields["type"]:value(type_name, "Xray")
|
||||
if not s.fields["type"].default then
|
||||
s.fields["type"].default = type_name
|
||||
end
|
||||
|
||||
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
|
||||
|
||||
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
|
||||
@ -487,8 +489,11 @@ o:depends({ [_n("outbound_node")] = "_socks"})
|
||||
o:depends({ [_n("outbound_node")] = "_http"})
|
||||
|
||||
o = s:option(Value, _n("outbound_node_iface"), translate("Interface"))
|
||||
o.default = "eth1"
|
||||
o:depends({ [_n("outbound_node")] = "_iface"})
|
||||
local netdev_list = api.get_network_devices()
|
||||
for _, d in ipairs(netdev_list) do
|
||||
o:value(d.name, d.label)
|
||||
end
|
||||
|
||||
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
|
||||
o.rows = 10
|
||||
|
||||
@ -1,30 +1,13 @@
|
||||
local m, s = ...
|
||||
|
||||
local api = require "luci.passwall2.api"
|
||||
|
||||
local singbox_bin = api.finded_com("sing-box")
|
||||
|
||||
if not singbox_bin then
|
||||
return
|
||||
end
|
||||
|
||||
local fs = api.fs
|
||||
|
||||
local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'")
|
||||
|
||||
local type_name = "sing-box"
|
||||
|
||||
local option_prefix = "singbox_"
|
||||
|
||||
local function _n(name)
|
||||
return option_prefix .. name
|
||||
end
|
||||
|
||||
local ss_method_list = {
|
||||
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
|
||||
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
|
||||
}
|
||||
|
||||
-- [[ Sing-Box ]]
|
||||
|
||||
s.fields["type"]:value(type_name, "Sing-Box")
|
||||
@ -32,6 +15,23 @@ if not s.fields["type"].default then
|
||||
s.fields["type"].default = type_name
|
||||
end
|
||||
|
||||
if s.val["type"] and s.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local option_prefix = "singbox_"
|
||||
|
||||
local function _n(name)
|
||||
return option_prefix .. name
|
||||
end
|
||||
|
||||
local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'")
|
||||
|
||||
local ss_method_list = {
|
||||
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
|
||||
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
|
||||
}
|
||||
|
||||
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
|
||||
|
||||
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
|
||||
@ -58,7 +58,6 @@ o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o.rmempty = false
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Flag, _n("auth"), translate("Auth"))
|
||||
@ -302,7 +301,7 @@ o:depends({ [_n("protocol")] = "tuic" })
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
o.validate = function(self, value, t)
|
||||
if value and value ~= "" then
|
||||
if not nixio.fs.access(value) then
|
||||
if not fs.access(value) then
|
||||
return nil, translate("Can't find this file!")
|
||||
else
|
||||
return value
|
||||
@ -321,7 +320,7 @@ o:depends({ [_n("protocol")] = "tuic" })
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
o.validate = function(self, value, t)
|
||||
if value and value ~= "" then
|
||||
if not nixio.fs.access(value) then
|
||||
if not fs.access(value) then
|
||||
return nil, translate("Can't find this file!")
|
||||
else
|
||||
return value
|
||||
@ -464,8 +463,11 @@ o:depends({ [_n("outbound_node")] = "_socks" })
|
||||
o:depends({ [_n("outbound_node")] = "_http" })
|
||||
|
||||
o = s:option(Value, _n("outbound_node_iface"), translate("Interface"))
|
||||
o.default = "eth1"
|
||||
o:depends({ [_n("outbound_node")] = "_iface" })
|
||||
local netdev_list = api.get_network_devices()
|
||||
for _, d in ipairs(netdev_list) do
|
||||
o:value(d.name, d.label)
|
||||
end
|
||||
|
||||
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
|
||||
o.rows = 10
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
local m, s = ...
|
||||
|
||||
local api = require "luci.passwall2.api"
|
||||
|
||||
if not api.is_finded("ssserver") then
|
||||
return
|
||||
end
|
||||
|
||||
local type_name = "SS-Rust"
|
||||
|
||||
-- [[ Shadowsocks Rust ]]
|
||||
|
||||
s.fields["type"]:value(type_name, translate("Shadowsocks Rust"))
|
||||
|
||||
if s.val["type"] and s.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local option_prefix = "ssrust_"
|
||||
|
||||
local function _n(name)
|
||||
@ -20,10 +26,6 @@ local ssrust_encrypt_method_list = {
|
||||
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305"
|
||||
}
|
||||
|
||||
-- [[ Shadowsocks Rust ]]
|
||||
|
||||
s.fields["type"]:value(type_name, translate("Shadowsocks Rust"))
|
||||
|
||||
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
|
||||
|
||||
o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
local m, s = ...
|
||||
|
||||
local api = require "luci.passwall2.api"
|
||||
|
||||
if not api.is_finded("ss-server") then
|
||||
return
|
||||
end
|
||||
|
||||
local type_name = "SS"
|
||||
|
||||
local option_prefix = "ss_"
|
||||
|
||||
local function _n(name)
|
||||
return option_prefix .. name
|
||||
end
|
||||
|
||||
local ss_encrypt_method_list = {
|
||||
"rc4-md5", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr",
|
||||
"aes-192-ctr", "aes-256-ctr", "bf-cfb", "camellia-128-cfb",
|
||||
"camellia-192-cfb", "camellia-256-cfb", "salsa20", "chacha20",
|
||||
"chacha20-ietf", -- aead
|
||||
"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
|
||||
"xchacha20-ietf-poly1305"
|
||||
}
|
||||
|
||||
-- [[ Shadowsocks ]]
|
||||
|
||||
s.fields["type"]:value(type_name, translate("Shadowsocks"))
|
||||
|
||||
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
|
||||
|
||||
o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
o.datatype = "port"
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Value, _n("password"), translate("Password"))
|
||||
o.password = true
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(ListValue, _n("method"), translate("Encrypt Method"))
|
||||
for a, t in ipairs(ss_encrypt_method_list) do o:value(t) end
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
|
||||
o.datatype = "uinteger"
|
||||
o.default = 300
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
|
||||
o.default = "0"
|
||||
o:depends({ [_n("custom")] = false })
|
||||
|
||||
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
|
||||
o.rows = 10
|
||||
o.wrap = "off"
|
||||
o:depends({ [_n("custom")] = true })
|
||||
o.validate = function(self, value, t)
|
||||
if value and api.jsonc.parse(value) then
|
||||
return value
|
||||
else
|
||||
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
|
||||
end
|
||||
end
|
||||
o.custom_cfgvalue = function(self, section, value)
|
||||
local config_str = m:get(section, "config_str")
|
||||
if config_str then
|
||||
return api.base64Decode(config_str)
|
||||
end
|
||||
end
|
||||
o.custom_write = function(self, section, value)
|
||||
m:set(section, "config_str", api.base64Encode(value) or "")
|
||||
end
|
||||
|
||||
o = s:option(Flag, _n("log"), translate("Log"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
api.luci_types(arg[1], m, s, type_name, option_prefix)
|
||||
@ -1,13 +1,19 @@
|
||||
local m, s = ...
|
||||
|
||||
local api = require "luci.passwall2.api"
|
||||
|
||||
if not api.is_finded("ssr-server") then
|
||||
return
|
||||
end
|
||||
|
||||
local type_name = "SSR"
|
||||
|
||||
-- [[ ShadowsocksR ]]
|
||||
|
||||
s.fields["type"]:value(type_name, translate("ShadowsocksR"))
|
||||
|
||||
if s.val["type"] and s.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local option_prefix = "ssr_"
|
||||
|
||||
local function _n(name)
|
||||
@ -33,10 +39,6 @@ local ssr_obfs_list = {
|
||||
"tls1.0_session_auth", "tls1.2_ticket_auth"
|
||||
}
|
||||
|
||||
-- [[ ShadowsocksR ]]
|
||||
|
||||
s.fields["type"]:value(type_name, translate("ShadowsocksR"))
|
||||
|
||||
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
|
||||
|
||||
o = s:option(Value, _n("port"), translate("Listen Port"))
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
local api = require "luci.passwall2.api"
|
||||
local appname = api.appname
|
||||
local fs = api.fs
|
||||
local types_dir = "/usr/lib/lua/luci/model/cbi/" .. appname .. "/server/type/"
|
||||
api = require "luci.passwall2.api"
|
||||
appname = api.appname
|
||||
fs = api.fs
|
||||
|
||||
m = Map("passwall2_server", translate("Server Config"))
|
||||
m.redirect = api.url("server")
|
||||
@ -11,12 +10,21 @@ if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(m.redirect)
|
||||
end
|
||||
|
||||
local header = Template(appname .. "/server/config_header")
|
||||
header.api = api
|
||||
header.config = m.config
|
||||
header.section = arg[1]
|
||||
m:append(header)
|
||||
|
||||
m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
|
||||
|
||||
s = m:section(NamedSection, arg[1], "user", "")
|
||||
s.addremove = false
|
||||
s.dynamic = false
|
||||
|
||||
local types_dir = "/usr/lib/lua/luci/model/cbi/" .. appname .. "/server/type/"
|
||||
s.val = m:get(arg[1]) or {}
|
||||
|
||||
o = s:option(Flag, "enable", translate("Enable"))
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
@ -32,8 +40,6 @@ for filename in fs.dir(types_dir) do
|
||||
table.insert(type_table, filename)
|
||||
end
|
||||
table.sort(type_table, function(a, b)
|
||||
if a == "socks.lua" then return true end
|
||||
if b == "socks.lua" then return false end
|
||||
return a < b
|
||||
end)
|
||||
|
||||
@ -42,4 +48,10 @@ for index, value in ipairs(type_table) do
|
||||
setfenv(p_func, getfenv(1))(m, s)
|
||||
end
|
||||
|
||||
local footer = Template(appname .. "/server/config_footer")
|
||||
footer.api = api
|
||||
footer.config = m.config
|
||||
footer.section = arg[1]
|
||||
m:append(footer)
|
||||
|
||||
return m
|
||||
|
||||
@ -1367,6 +1367,11 @@ function set_apply_on_parse(map)
|
||||
end
|
||||
|
||||
function luci_types(id, m, s, type_name, option_prefix)
|
||||
local fv_type
|
||||
local field_type = s.fields["type"]
|
||||
if field_type then
|
||||
fv_type = field_type:formvalue(id)
|
||||
end
|
||||
local rewrite_option_table = {}
|
||||
for key, value in pairs(s.fields) do
|
||||
if key:find(option_prefix) == 1 then
|
||||
@ -1435,6 +1440,10 @@ function luci_types(id, m, s, type_name, option_prefix)
|
||||
else
|
||||
s.fields[key]:depends({ type = type_name })
|
||||
end
|
||||
|
||||
if fv_type and fv_type ~= type_name then
|
||||
s.fields[key].rmempty = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1623,3 +1632,83 @@ function parse_realm_uri(uri)
|
||||
end
|
||||
return realm
|
||||
end
|
||||
|
||||
function get_network_devices()
|
||||
local _sysnet = "/sys/class/net/"
|
||||
-- Map UCI interface names to their device names and vice versa
|
||||
local _iface_to_dev = {}
|
||||
local _dev_to_ifaces = {}
|
||||
local _iface_proto = {}
|
||||
uci:foreach("network", "interface", function(sec)
|
||||
local name = sec[".name"]
|
||||
if name ~= "loopback" then
|
||||
_iface_proto[name] = sec.proto
|
||||
if sec.device then
|
||||
_iface_to_dev[name] = sec.device
|
||||
_dev_to_ifaces[sec.device] = _dev_to_ifaces[sec.device] or {}
|
||||
table.insert(_dev_to_ifaces[sec.device], name)
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- Classify device type using sysfs attributes
|
||||
local function classify_sysfs(dev)
|
||||
if fs.stat(_sysnet .. dev .. "/bridge", "type") == "dir" then
|
||||
return i18n.translate("Bridge")
|
||||
elseif fs.stat(_sysnet .. dev .. "/wireless", "type") == "dir" then
|
||||
return i18n.translate("Wireless Adapter")
|
||||
elseif dev:match("^tun") or dev:match("^tap") or dev:match("^wg") or dev:match("^ppp") then
|
||||
return i18n.translate("Tunnel Interface")
|
||||
else
|
||||
return i18n.translate("Ethernet Adapter")
|
||||
end
|
||||
end
|
||||
-- Classify offline UCI interfaces by config hints
|
||||
local function classify_uci(dev_name, proto)
|
||||
if dev_name and dev_name:match("^br%-") then
|
||||
return i18n.translate("Bridge")
|
||||
elseif proto == "wireguard" or proto == "pppoe" or proto == "pptp" or proto == "l2tp" then
|
||||
return i18n.translate("Tunnel Interface")
|
||||
else
|
||||
return i18n.translate("Interface")
|
||||
end
|
||||
end
|
||||
|
||||
local _seen = {}
|
||||
local _devices = {}
|
||||
-- Active kernel devices from /sys/class/net/
|
||||
-- Skip bridge member ports (/master) and DSA master devices (/dsa)
|
||||
local _iter = fs.dir(_sysnet)
|
||||
if _iter then
|
||||
for dev in _iter do
|
||||
if dev ~= "lo"
|
||||
and not dev:match("^veth")
|
||||
and not dev:match("^ifb")
|
||||
and not dev:match("^gre")
|
||||
and not dev:match("^sit")
|
||||
and not dev:match("^ip6tnl")
|
||||
and not dev:match("^erspan")
|
||||
and not fs.stat(_sysnet .. dev .. "/master", "type")
|
||||
and not fs.stat(_sysnet .. dev .. "/dsa", "type")
|
||||
then
|
||||
local dtype = classify_sysfs(dev)
|
||||
local label = dtype .. ': "' .. dev .. '"'
|
||||
if _dev_to_ifaces[dev] then
|
||||
label = label .. " (" .. table.concat(_dev_to_ifaces[dev], ", ") .. ")"
|
||||
end
|
||||
_devices[#_devices + 1] = { name = dev, label = label, sort = dtype .. ":" .. dev }
|
||||
_seen[dev] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
-- UCI interfaces whose device does not currently exist
|
||||
for iface, dev in pairs(_iface_to_dev) do
|
||||
if not _seen[dev] then
|
||||
local dtype = classify_uci(dev, _iface_proto[iface])
|
||||
local label = dtype .. ': "' .. iface .. '"'
|
||||
_devices[#_devices + 1] = { name = iface, label = label, sort = "zzz:" .. iface }
|
||||
_seen[dev] = true
|
||||
end
|
||||
end
|
||||
table.sort(_devices, function(a, b) return a.sort < b.sort end)
|
||||
return _devices
|
||||
end
|
||||
|
||||
@ -120,7 +120,7 @@ local function start()
|
||||
local config_file = CONFIG_PATH .. "/" .. id .. ".json"
|
||||
local udp_forward = 1
|
||||
local type = user.type or ""
|
||||
if type == "SS" or type == "SSR" then
|
||||
if type == "SSR" then
|
||||
if user.custom == "1" and user.config_str then
|
||||
config = jsonc.parse(api.base64Decode(user.config_str))
|
||||
else
|
||||
|
||||
@ -75,11 +75,7 @@ function gen_config(var)
|
||||
reuse_port = true
|
||||
}
|
||||
|
||||
if node.type == "SS" then
|
||||
config.plugin = plugin_file or nil
|
||||
config.plugin_opts = (plugin_file) and node.plugin_opts or nil
|
||||
config.mode = mode
|
||||
elseif node.type == "SSR" then
|
||||
if node.type == "SSR" then
|
||||
config.protocol = node.protocol
|
||||
config.protocol_param = node.protocol_param
|
||||
config.obfs = node.obfs
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
module("luci.passwall2.util_tuic", package.seeall)
|
||||
local api = require "luci.passwall2.api"
|
||||
local uci = api.uci
|
||||
local jsonc = api.jsonc
|
||||
|
||||
function gen_config(var)
|
||||
local node_id = var["node"]
|
||||
if not node_id then
|
||||
print("node Cannot be empty!")
|
||||
return
|
||||
end
|
||||
local node = uci:get_all("passwall2", node_id)
|
||||
local local_addr = var["local_addr"]
|
||||
local local_port = var["local_port"]
|
||||
local server_host = var["server_host"] or (node.address or ""):lower()
|
||||
local server_port = var["server_port"] or node.port
|
||||
local loglevel = var["loglevel"] or "warn"
|
||||
|
||||
local tuic= {
|
||||
relay = {
|
||||
server = server_host .. ":" .. server_port,
|
||||
ip = node.tuic_ip,
|
||||
uuid = node.uuid,
|
||||
password = node.tuic_password,
|
||||
-- certificates = node.tuic_certificate and { node.tuic_certpath } or nil,
|
||||
udp_relay_mode = node.tuic_udp_relay_mode,
|
||||
congestion_control = node.tuic_congestion_control,
|
||||
heartbeat = (tonumber(node.tuic_heartbeat) or 3) .. "s",
|
||||
timeout = (tonumber(node.tuic_timeout) or 8) .. "s",
|
||||
gc_interval = (tonumber(node.tuic_gc_interval) or 3) .. "s",
|
||||
gc_lifetime = (tonumber(node.tuic_gc_lifetime) or 15) .. "s",
|
||||
alpn = (node.tuic_tls_alpn and node.tuic_tls_alpn ~= "") and (function()
|
||||
local alpn = {}
|
||||
string.gsub(node.tuic_tls_alpn, '[^,]+', function(w)
|
||||
table.insert(alpn, w)
|
||||
end)
|
||||
if #alpn > 0 then return alpn end
|
||||
return nil
|
||||
end)() or nil,
|
||||
disable_sni = (node.tuic_disable_sni == "1"),
|
||||
zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1"),
|
||||
send_window = tonumber(node.tuic_send_window),
|
||||
receive_window = tonumber(node.tuic_receive_window)
|
||||
},
|
||||
["local"] = {
|
||||
server = "[::]:" .. local_port,
|
||||
username = node.tuic_socks_username,
|
||||
password = node.tuic_socks_password,
|
||||
dual_stack = (node.tuic_dual_stack == "1") and true or false,
|
||||
max_packet_size = tonumber(node.tuic_max_package_size)
|
||||
},
|
||||
log_level = loglevel
|
||||
}
|
||||
return jsonc.stringify(tuic, 1)
|
||||
end
|
||||
|
||||
_G.gen_config = gen_config
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
if func then
|
||||
local var = nil
|
||||
if arg[2] then
|
||||
var = jsonc.parse(arg[2])
|
||||
end
|
||||
print(func(var))
|
||||
end
|
||||
end
|
||||
@ -1219,6 +1219,11 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
end
|
||||
if node.chain_proxy == "3" and node.outbound_iface then
|
||||
if outbound.streamSettings and outbound.streamSettings.sockopt then
|
||||
outbound.streamSettings.sockopt.interface = node.outbound_iface
|
||||
end
|
||||
end
|
||||
return default_outTag, last_insert_outbound
|
||||
end
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ local api = self.api
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
waitForElement('select[name*="<%=api.appname%>"][name*="type"]', function(el) {
|
||||
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
|
||||
let o_val = el.value;
|
||||
el.addEventListener("change", () => {
|
||||
el.blur();
|
||||
@ -22,7 +22,7 @@ local api = self.api
|
||||
}
|
||||
});
|
||||
});
|
||||
waitForElement('select[name*="<%=api.appname%>"][name*="protocol"]', function(el) {
|
||||
waitForElement('select[name*="<%=self.config%>"][name*="protocol"]', function(el) {
|
||||
let o_val = el.value;
|
||||
el.addEventListener("change", () => {
|
||||
el.blur();
|
||||
|
||||
@ -21,7 +21,7 @@ local api = self.api
|
||||
|
||||
function getOption(opt) {
|
||||
var obj;
|
||||
var id = 'cbid.<%=api.appname%>.' + node_id + '.' + opt;
|
||||
var id = 'cbid.<%=self.config%>.' + node_id + '.' + opt;
|
||||
obj = document.getElementsByName(id)[0] || document.getElementById(id);
|
||||
if (obj) {
|
||||
var combobox = document.getElementById('cbi.combobox.' + id);
|
||||
|
||||
@ -4,13 +4,12 @@ local api = require "luci.passwall2.api"
|
||||
local appname = api.appname
|
||||
local map = self.map
|
||||
|
||||
local has_ss = api.is_finded("ss-redir")
|
||||
local has_ss_rust = api.is_finded("sslocal")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
local has_hysteria2 = api.finded_com("hysteria")
|
||||
|
||||
local ss_type = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local ss_type = api.get_core("ss_type", {{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local trojan_type = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local vmess_type = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local vless_type = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
@ -165,10 +164,7 @@ local current_node = map:get(section)
|
||||
|
||||
var dom_prefix = null
|
||||
var protocol = ""
|
||||
if (v_type === "SS") {
|
||||
dom_prefix = "ss_"
|
||||
protocol = "ss"
|
||||
} else if (v_type === "SS-Rust") {
|
||||
if (v_type === "SS-Rust") {
|
||||
dom_prefix = "ssrust_"
|
||||
protocol = "ss"
|
||||
} else if (v_type === "SSR") {
|
||||
@ -196,7 +192,7 @@ local current_node = map:get(section)
|
||||
}
|
||||
}
|
||||
var url = null;
|
||||
if (v_type === "SS" || v_type === "SS-Rust" || ((v_type === "sing-box" || v_type === "Xray") && opt.get(dom_prefix + "protocol").value === "shadowsocks")) {
|
||||
if (v_type === "SS-Rust" || ((v_type === "sing-box" || v_type === "Xray") && opt.get(dom_prefix + "protocol").value === "shadowsocks")) {
|
||||
protocol = "ss"
|
||||
var v_port = opt.get(dom_prefix + "port");
|
||||
var v_method = opt.get(dom_prefix + "method") || opt.get(dom_prefix + "ss_method");
|
||||
@ -1003,14 +999,6 @@ local current_node = map:get(section)
|
||||
} else if (ss_type == "shadowsocks-rust") {
|
||||
dom_prefix = "ssrust_"
|
||||
opt.set('type', "SS-Rust");
|
||||
} else {
|
||||
if (["2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"].includes(method)) {
|
||||
dom_prefix = "ssrust_"
|
||||
opt.set('type', "SS-Rust");
|
||||
} else {
|
||||
dom_prefix = "ss_"
|
||||
opt.set('type', "SS");
|
||||
}
|
||||
}
|
||||
|
||||
const _method = (method || "none").toLowerCase();
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
<%
|
||||
local api = self.api
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
|
||||
let o_val = el.value;
|
||||
el.addEventListener("change", () => {
|
||||
el.blur();
|
||||
if (o_val != el.value) {
|
||||
let save = true;
|
||||
if (save) {
|
||||
//document.getElementsByClassName('cbi-button-save')[0].click();
|
||||
update_node({
|
||||
enable: getOption("enable").value,
|
||||
remarks: getOption("remarks").value,
|
||||
type: getOption("type").value,
|
||||
protocol: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@ -0,0 +1,56 @@
|
||||
<%
|
||||
local api = self.api
|
||||
-%>
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
let node_id = "<%=self.section%>";
|
||||
let node_config_url = '<%=api.url("server_user")%>/' + node_id;
|
||||
|
||||
function waitForElement(selector, callback) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) return callback(el);
|
||||
const observer = new MutationObserver(() => {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) {
|
||||
observer.disconnect();
|
||||
callback(el);
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function getOption(opt) {
|
||||
var obj;
|
||||
var id = 'cbid.<%=self.config%>.' + node_id + '.' + opt;
|
||||
obj = document.getElementsByName(id)[0] || document.getElementById(id);
|
||||
if (obj) {
|
||||
var combobox = document.getElementById('cbi.combobox.' + id);
|
||||
if (combobox) {
|
||||
obj.combobox = combobox;
|
||||
}
|
||||
var div = document.getElementById(id);
|
||||
if (div && div.getElementsByTagName("li").length > 0) {
|
||||
obj = div;
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function update_node(data) {
|
||||
XHR.get('<%=api.url("server_user_update")%>', {
|
||||
id: node_id,
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
function(x, data) {
|
||||
if (x && x.status == 200 && data.code == 1) {
|
||||
window.location.href = node_config_url;
|
||||
}
|
||||
else {
|
||||
alert("<%:Error%>");
|
||||
}
|
||||
});
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
@ -564,9 +564,6 @@ msgstr "اصلی"
|
||||
msgid "Transport Plugin"
|
||||
msgstr "افزونه انتقال"
|
||||
|
||||
msgid "Shadowsocks secondary encryption"
|
||||
msgstr "رمزنگاری ثانویه Shadowsocks"
|
||||
|
||||
msgid "Obfs Type"
|
||||
msgstr "نوع مبهمسازی (Obfs)"
|
||||
|
||||
@ -1365,18 +1362,6 @@ msgstr "زمانی استفاده میشود که لینک گره شامل ا
|
||||
msgid "%s Node Use Type"
|
||||
msgstr "نوع استفاده گره %s"
|
||||
|
||||
msgid "Set the TUIC proxy server ip address"
|
||||
msgstr "تنظیم آدرس IP سرور پروکسی TUIC"
|
||||
|
||||
msgid "TUIC User Password For Connect Remote Server"
|
||||
msgstr "رمز عبور کاربر TUIC برای اتصال به سرور راه دور"
|
||||
|
||||
msgid "TUIC UserName For Local Socks"
|
||||
msgstr "نام کاربری TUIC برای Socks محلی"
|
||||
|
||||
msgid "TUIC Password For Local Socks"
|
||||
msgstr "رمز عبور TUIC برای Socks محلی"
|
||||
|
||||
msgid "UDP relay mode"
|
||||
msgstr "حالت بازپخش UDP"
|
||||
|
||||
@ -1389,33 +1374,12 @@ msgstr "الگوریتم کنترل ازدحام"
|
||||
msgid "Heartbeat interval(second)"
|
||||
msgstr "بازه ضربان قلب (ثانیه)"
|
||||
|
||||
msgid "Timeout for establishing a connection to server(second)"
|
||||
msgstr "مهلت زمانی برای برقراری اتصال به سرور (ثانیه)"
|
||||
|
||||
msgid "Garbage collection interval(second)"
|
||||
msgstr "بازه جمعآوری زباله (ثانیه)"
|
||||
|
||||
msgid "Garbage collection lifetime(second)"
|
||||
msgstr "طول عمر جمعآوری زباله (ثانیه)"
|
||||
|
||||
msgid "Disable SNI"
|
||||
msgstr "غیرفعال کردن SNI"
|
||||
|
||||
msgid "Enable 0-RTT QUIC handshake"
|
||||
msgstr "فعالسازی دست تکان دادن 0-RTT QUIC"
|
||||
|
||||
msgid "TUIC send window"
|
||||
msgstr "پنجره ارسال TUIC"
|
||||
|
||||
msgid "TUIC receive window"
|
||||
msgstr "پنجره دریافت TUIC"
|
||||
|
||||
msgid "TUIC Maximum packet size the socks5 server can receive from external, in bytes"
|
||||
msgstr "حداکثر اندازه بسته TUIC که سرور socks5 میتواند از خارج دریافت کند، به بایت"
|
||||
|
||||
msgid "Set if the listening socket should be dual-stack"
|
||||
msgstr "تنظیم کنید که سوکت شنونده دو پشتهای (dual-stack) باشد یا خیر"
|
||||
|
||||
msgid "<br />none: default, no masquerade, data sent is packets with no characteristics.<br />srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).<br />utp: packets disguised as uTP will be recognized as bittorrent downloaded data.<br />wechat-video: packets disguised as WeChat video calls.<br />dtls: disguised as DTLS 1.2 packet.<br />wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)<br />dns: Disguising traffic as DNS requests."
|
||||
msgstr "<br />none: پیشفرض، بدون استتار، دادههای ارسالی بستههای بدون مشخصه هستند.<br />srtp: مبدل شده به بسته SRTP، به عنوان داده تماس تصویری (مانند FaceTime) شناخته میشود.<br />utp: بستههای مبدل شده به uTP به عنوان داده دانلود شده bittorrent شناخته میشوند.<br />wechat-video: بستههای مبدل شده به تماسهای تصویری WeChat.<br />dtls: مبدل شده به بسته DTLS 1.2.<br />wireguard: مبدل شده به بسته WireGuard. (نه واقعاً پروتکل WireGuard)<br />dns: پنهان کردن ترافیک به عنوان درخواستهای DNS."
|
||||
|
||||
@ -2323,9 +2287,6 @@ msgstr "Xray از افزونه %s پشتیبانی نمیکند."
|
||||
msgid "Plugin options Incomplete."
|
||||
msgstr "گزینههای افزونه ناقص است."
|
||||
|
||||
msgid "shadowsocks-libev unsupport 2022 encryption."
|
||||
msgstr "shadowsocks-libev از رمزنگاری 2022 پشتیبانی نمیکند."
|
||||
|
||||
msgid "Please replace %s to support %s transmission method."
|
||||
msgstr "لطفا برای پشتیبانی از روش انتقال %s، %s را جایگزین کنید."
|
||||
|
||||
|
||||
@ -559,9 +559,6 @@ msgstr "Оригинал"
|
||||
msgid "Transport Plugin"
|
||||
msgstr "Плагин транспорта"
|
||||
|
||||
msgid "Shadowsocks secondary encryption"
|
||||
msgstr "Дополнительное шифрование Shadowsocks"
|
||||
|
||||
msgid "global_padding"
|
||||
msgstr "Включить padding-заполнение"
|
||||
|
||||
@ -1366,18 +1363,6 @@ msgstr "Используется, если в ссылке на узел это
|
||||
msgid "%s Node Use Type"
|
||||
msgstr "Тип использования сервера %s"
|
||||
|
||||
msgid "Set the TUIC proxy server ip address"
|
||||
msgstr "Указать IP-адрес сервера TUIC"
|
||||
|
||||
msgid "TUIC User Password For Connect Remote Server"
|
||||
msgstr "Пароль для подключения к удалённому серверу TUIC"
|
||||
|
||||
msgid "TUIC UserName For Local Socks"
|
||||
msgstr "Имя пользователя для локального SOCKS TUIC"
|
||||
|
||||
msgid "TUIC Password For Local Socks"
|
||||
msgstr "Пароль для локального SOCKS TUIC"
|
||||
|
||||
msgid "UDP relay mode"
|
||||
msgstr "Режим ретрансляции UDP"
|
||||
|
||||
@ -1390,33 +1375,12 @@ msgstr "Алгоритм контроля перегрузок"
|
||||
msgid "Heartbeat interval(second)"
|
||||
msgstr "Интервал heartbeat-пакетов (сек)"
|
||||
|
||||
msgid "Timeout for establishing a connection to server(second)"
|
||||
msgstr "Тайм-аут установки соединения с сервером (сек)"
|
||||
|
||||
msgid "Garbage collection interval(second)"
|
||||
msgstr "Интервал очистки фрагментов UDP (сек)"
|
||||
|
||||
msgid "Garbage collection lifetime(second)"
|
||||
msgstr "Время жизни фрагментов UDP на сервере (сек)"
|
||||
|
||||
msgid "Disable SNI"
|
||||
msgstr "Отключить SNI"
|
||||
|
||||
msgid "Enable 0-RTT QUIC handshake"
|
||||
msgstr "Включить 0-RTT рукопожатие QUIC"
|
||||
|
||||
msgid "TUIC send window"
|
||||
msgstr "Окно отправки TUIC (макс. байт без подтверждения: по умолчанию 8 МБ × 2)"
|
||||
|
||||
msgid "TUIC receive window"
|
||||
msgstr "Окно приёма TUIC (макс. байт без подтверждения: по умолчанию 8 МБ)"
|
||||
|
||||
msgid "TUIC Maximum packet size the socks5 server can receive from external, in bytes"
|
||||
msgstr "Макс. размер пакета, который SOCKS5-сервер TUIC может принять извне (в байтах)"
|
||||
|
||||
msgid "Set if the listening socket should be dual-stack"
|
||||
msgstr "Включить поддержку dual-stack для слушающего сокета"
|
||||
|
||||
msgid "<br />none: default, no masquerade, data sent is packets with no characteristics.<br />srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).<br />utp: packets disguised as uTP will be recognized as bittorrent downloaded data.<br />wechat-video: packets disguised as WeChat video calls.<br />dtls: disguised as DTLS 1.2 packet.<br />wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)<br />dns: Disguising traffic as DNS requests."
|
||||
msgstr "<br />none: по умолчанию, без маскировки, данные отправляются обычными пакетами.<br />srtp: маскировка под SRTP — трафик определяется как видеозвонок (например, FaceTime).<br />utp: маскировка под uTP — трафик определяется как BitTorrent.<br />wechat-video: маскировка под видеозвонок WeChat.<br />dtls: маскировка под DTLS 1.2.<br />wireguard: маскировка под WireGuard (не настоящий протокол WireGuard).<br />dns: маскировка трафика под DNS-запросы."
|
||||
|
||||
@ -2321,9 +2285,6 @@ msgstr "Xray не поддерживает плагин %s."
|
||||
msgid "Plugin options Incomplete."
|
||||
msgstr "Опции плагина указаны не полностью."
|
||||
|
||||
msgid "shadowsocks-libev unsupport 2022 encryption."
|
||||
msgstr "shadowsocks-libev не поддерживает шифрование 2022."
|
||||
|
||||
msgid "Please replace %s to support %s transmission method."
|
||||
msgstr "Пожалуйста, замените %s, чтобы поддержать метод передачи %s."
|
||||
|
||||
|
||||
@ -556,9 +556,6 @@ msgstr "原版"
|
||||
msgid "Transport Plugin"
|
||||
msgstr "传输层插件"
|
||||
|
||||
msgid "Shadowsocks secondary encryption"
|
||||
msgstr "Shadowsocks 二次加密"
|
||||
|
||||
msgid "Obfs Type"
|
||||
msgstr "混淆类型"
|
||||
|
||||
@ -1357,18 +1354,6 @@ msgstr "当节点链接未包含该参数时,将使用此设置。"
|
||||
msgid "%s Node Use Type"
|
||||
msgstr "%s 节点使用类型"
|
||||
|
||||
msgid "Set the TUIC proxy server ip address"
|
||||
msgstr "指定远程TUIC服务器IP"
|
||||
|
||||
msgid "TUIC User Password For Connect Remote Server"
|
||||
msgstr "用于远程TUIC服务器连接的密码"
|
||||
|
||||
msgid "TUIC UserName For Local Socks"
|
||||
msgstr "用于本地Socks服务器连接的用户名"
|
||||
|
||||
msgid "TUIC Password For Local Socks"
|
||||
msgstr "用于本地Socks服务器连接的密码"
|
||||
|
||||
msgid "UDP relay mode"
|
||||
msgstr "UDP中继模式"
|
||||
|
||||
@ -1381,33 +1366,12 @@ msgstr "拥塞控制算法"
|
||||
msgid "Heartbeat interval(second)"
|
||||
msgstr "保活心跳包发送间隔(单位:秒)"
|
||||
|
||||
msgid "Timeout for establishing a connection to server(second)"
|
||||
msgstr "连接超时时间(单位:秒)"
|
||||
|
||||
msgid "Garbage collection interval(second)"
|
||||
msgstr "UDP数据包片残片清理间隔(单位:秒)"
|
||||
|
||||
msgid "Garbage collection lifetime(second)"
|
||||
msgstr "UDP数据包残片在服务器的保留时间(单位:秒)"
|
||||
|
||||
msgid "Disable SNI"
|
||||
msgstr "关闭SNI服务器名称指示"
|
||||
|
||||
msgid "Enable 0-RTT QUIC handshake"
|
||||
msgstr "客户端启用 0-RTT QUIC 连接握手"
|
||||
|
||||
msgid "TUIC send window"
|
||||
msgstr "发送窗口(无需确认即可发送的最大字节数:默认8Mb*2)"
|
||||
|
||||
msgid "TUIC receive window"
|
||||
msgstr "接收窗口(无需确认即可接收的最大字节数:默认8Mb)"
|
||||
|
||||
msgid "TUIC Maximum packet size the socks5 server can receive from external, in bytes"
|
||||
msgstr "TUIC socks5 服务器可以从外部接收的最大数据包大小(以字节为单位)"
|
||||
|
||||
msgid "Set if the listening socket should be dual-stack"
|
||||
msgstr "设置监听套接字为双栈"
|
||||
|
||||
msgid "<br />none: default, no masquerade, data sent is packets with no characteristics.<br />srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).<br />utp: packets disguised as uTP will be recognized as bittorrent downloaded data.<br />wechat-video: packets disguised as WeChat video calls.<br />dtls: disguised as DTLS 1.2 packet.<br />wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)<br />dns: Disguising traffic as DNS requests."
|
||||
msgstr "<br />none:默认值,不进行伪装,发送的数据是没有特征的数据包。<br />srtp:伪装成 SRTP 数据包,会被识别为视频通话数据(如 FaceTime)。<br />utp:伪装成 uTP 数据包,会被识别为 BT 下载数据。<br />wechat-video:伪装成微信视频通话的数据包。<br />dtls:伪装成 DTLS 1.2 数据包。<br />wireguard:伪装成 WireGuard 数据包。(并不是真正的 WireGuard 协议)<br />dns:把流量伪装成 DNS 请求。"
|
||||
|
||||
@ -2309,9 +2273,6 @@ msgstr "Xray 不支持 %s 插件。"
|
||||
msgid "Plugin options Incomplete."
|
||||
msgstr "插件选项不完整。"
|
||||
|
||||
msgid "shadowsocks-libev unsupport 2022 encryption."
|
||||
msgstr "shadowsocks-libev 不支持2022加密。"
|
||||
|
||||
msgid "Please replace %s to support %s transmission method."
|
||||
msgstr "请更换 %s 以支持 %s 传输方式。"
|
||||
|
||||
|
||||
@ -556,9 +556,6 @@ msgstr "原版"
|
||||
msgid "Transport Plugin"
|
||||
msgstr "傳輸層插件"
|
||||
|
||||
msgid "Shadowsocks secondary encryption"
|
||||
msgstr "Shadowsocks 二次加密"
|
||||
|
||||
msgid "Obfs Type"
|
||||
msgstr "混淆類型"
|
||||
|
||||
@ -1357,18 +1354,6 @@ msgstr "當節點連結未包含該參數時,將使用此設定。"
|
||||
msgid "%s Node Use Type"
|
||||
msgstr "%s 節點使用類型"
|
||||
|
||||
msgid "Set the TUIC proxy server ip address"
|
||||
msgstr "指定遠程TUIC服務器IP"
|
||||
|
||||
msgid "TUIC User Password For Connect Remote Server"
|
||||
msgstr "用於遠程TUIC服務器連接的密碼"
|
||||
|
||||
msgid "TUIC UserName For Local Socks"
|
||||
msgstr "用於本地Socks服務器連接的用戶名"
|
||||
|
||||
msgid "TUIC Password For Local Socks"
|
||||
msgstr "用於本地Socks服務器連接的密碼"
|
||||
|
||||
msgid "UDP relay mode"
|
||||
msgstr "UDP中继模式"
|
||||
|
||||
@ -1381,33 +1366,12 @@ msgstr "拥塞控制算法"
|
||||
msgid "Heartbeat interval(second)"
|
||||
msgstr "保活心跳包發送間隔(單位:秒)"
|
||||
|
||||
msgid "Timeout for establishing a connection to server(second)"
|
||||
msgstr "連接超時時間(單位:秒)"
|
||||
|
||||
msgid "Garbage collection interval(second)"
|
||||
msgstr "UDP數据包片残片清理間隔(單位:秒)"
|
||||
|
||||
msgid "Garbage collection lifetime(second)"
|
||||
msgstr "UDP數据包残片在服務器的保留時間(單位:秒)"
|
||||
|
||||
msgid "Disable SNI"
|
||||
msgstr "關閉SNI服務器名称指示"
|
||||
|
||||
msgid "Enable 0-RTT QUIC handshake"
|
||||
msgstr "客戶端啟用 0-RTT QUIC 連接握手"
|
||||
|
||||
msgid "TUIC send window"
|
||||
msgstr "發送窗口(無需確認即可發送的最大字节數:默認8Mb*2)"
|
||||
|
||||
msgid "TUIC receive window"
|
||||
msgstr "接收窗口(無需確認即可接收的最大字节數:默認8Mb)"
|
||||
|
||||
msgid "TUIC Maximum packet size the socks5 server can receive from external, in bytes"
|
||||
msgstr "TUIC socks5 服務器可以从外部接收的最大數据包大小(以字节為單位)"
|
||||
|
||||
msgid "Set if the listening socket should be dual-stack"
|
||||
msgstr "設置監聽套接字為双栈"
|
||||
|
||||
msgid "<br />none: default, no masquerade, data sent is packets with no characteristics.<br />srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).<br />utp: packets disguised as uTP will be recognized as bittorrent downloaded data.<br />wechat-video: packets disguised as WeChat video calls.<br />dtls: disguised as DTLS 1.2 packet.<br />wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)<br />dns: Disguising traffic as DNS requests."
|
||||
msgstr "<br />none:默認值,不进行伪装,發送的數据是沒有特征的數据包。<br />srtp:伪装成 SRTP 數据包,会被识别為視频通话數据(如 FaceTime)。<br />utp:伪装成 uTP 數据包,会被识别為 BT 下載數据。<br />wechat-video:伪装成微信視频通话的數据包。<br />dtls:伪装成 DTLS 1.2 數据包。<br />wireguard:伪装成 WireGuard 數据包。(並不是真正的 WireGuard 協議)<br />dns:把流量伪装成 DNS 請求。"
|
||||
|
||||
@ -2315,9 +2279,6 @@ msgstr "Xray 不支持 %s 插件。"
|
||||
msgid "Plugin options Incomplete."
|
||||
msgstr "插件選項不完整。"
|
||||
|
||||
msgid "shadowsocks-libev unsupport 2022 encryption."
|
||||
msgstr "shadowsocks-libev 不支持2022加密。"
|
||||
|
||||
msgid "Please replace %s to support %s transmission method."
|
||||
msgstr "請更換 %s 以支援 %s 傳輸方式。"
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ UTIL_SS=$LUA_UTIL_PATH/util_shadowsocks.lua
|
||||
UTIL_XRAY=$LUA_UTIL_PATH/util_xray.lua
|
||||
UTIL_NAIVE=$LUA_UTIL_PATH/util_naiveproxy.lua
|
||||
UTIL_HYSTERIA2=$LUA_UTIL_PATH/util_hysteria2.lua
|
||||
UTIL_TUIC=$LUA_UTIL_PATH/util_tuic.lua
|
||||
SINGBOX_BIN=$(first_type $(config_t_get global_app sing_box_file) sing-box)
|
||||
XRAY_BIN=$(first_type $(config_t_get global_app xray_file) xray)
|
||||
|
||||
@ -467,18 +466,6 @@ run_socks() {
|
||||
lua $UTIL_SS gen_config "${_json_arg}" > $config_file
|
||||
[ -z "$no_run" ] && ln_run ${QUEUE_RUN} "$(first_type ssr-local)" "ssr-local" $log_file -c "$config_file" -v -u
|
||||
;;
|
||||
ss)
|
||||
json_add_string "local_addr" "${bind}"
|
||||
json_add_string "local_port" "${socks_port}"
|
||||
json_add_string "mode" "tcp_and_udp"
|
||||
[ -z "$no_run" ] && {
|
||||
local plugin_sh="${config_file%.json}_plugin.sh"
|
||||
json_add_string "plugin_sh" "${plugin_sh}"
|
||||
}
|
||||
local _json_arg="$(json_dump)"
|
||||
lua $UTIL_SS gen_config "${_json_arg}" > $config_file
|
||||
[ -z "$no_run" ] && ln_run ${QUEUE_RUN} "$(first_type ss-local)" "ss-local" $log_file -c "$config_file" -v
|
||||
;;
|
||||
ss-rust)
|
||||
json_add_string "local_socks_address" "${bind}"
|
||||
json_add_string "local_socks_port" "${socks_port}"
|
||||
@ -509,13 +496,6 @@ run_socks() {
|
||||
lua $UTIL_HYSTERIA2 gen_config "${_json_arg}" > $config_file
|
||||
[ -z "$no_run" ] && ln_run ${QUEUE_RUN} "$(first_type $(config_t_get global_app hysteria_file))" "hysteria" $log_file -c "$config_file" client
|
||||
;;
|
||||
tuic)
|
||||
json_add_string "local_addr" "${bind}"
|
||||
json_add_string "local_port" "${socks_port}"
|
||||
local _json_arg="$(json_dump)"
|
||||
lua $UTIL_TUIC gen_config "${_json_arg}" > $config_file
|
||||
[ -z "$no_run" ] && ln_run ${QUEUE_RUN} "$(first_type tuic-client)" "tuic-client" $log_file -c "$config_file"
|
||||
;;
|
||||
esac
|
||||
|
||||
# http to socks
|
||||
|
||||
@ -25,7 +25,6 @@ local log = api.log
|
||||
local i18n = api.i18n
|
||||
local lyaml = require "lyaml"
|
||||
|
||||
local has_ss = api.is_finded("ss-redir")
|
||||
local has_ss_rust = api.is_finded("sslocal")
|
||||
local has_ssr = api.is_finded("ssr-local") and api.is_finded("ssr-redir")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
@ -36,7 +35,7 @@ local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "fi
|
||||
local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
|
||||
local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
|
||||
-- Nodes should be retrieved using the core type (if not set on the node subscription page, the default type will be used automatically).
|
||||
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
@ -44,7 +43,6 @@ local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"h
|
||||
local core_has = {
|
||||
["xray"] = has_xray,
|
||||
["sing-box"] = has_singbox,
|
||||
["shadowsocks-libev"] = has_ss,
|
||||
["shadowsocks-rust"] = has_ss_rust,
|
||||
["hysteria2"] = has_hysteria2
|
||||
}
|
||||
@ -460,9 +458,7 @@ end
|
||||
|
||||
-- Configure the SS protocol implementation type
|
||||
local function set_ss_implementation(ss_type, result)
|
||||
if ss_type == "shadowsocks-libev" and has_ss then
|
||||
result.type = "SS"
|
||||
elseif ss_type == "shadowsocks-rust" and has_ss_rust then
|
||||
if ss_type == "shadowsocks-rust" and has_ss_rust then
|
||||
result.type = 'SS-Rust'
|
||||
elseif ss_type == "xray" and has_xray then
|
||||
result.type = 'Xray'
|
||||
@ -485,6 +481,7 @@ local function parseClashNode(node, add_mode, group, sub_cfg)
|
||||
local sub_vmess_type = DEFAULT_VMESS_TYPE
|
||||
local sub_vless_type = DEFAULT_VLESS_TYPE
|
||||
local sub_hysteria2_type = DEFAULT_HYSTERIA2_TYPE
|
||||
local sub_hy_up_mbps, sub_hy_down_mbps = 1000, 1000
|
||||
if sub_cfg then
|
||||
if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then
|
||||
sub_allowinsecure = nil
|
||||
@ -509,6 +506,8 @@ local function parseClashNode(node, add_mode, group, sub_cfg)
|
||||
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
|
||||
sub_hysteria2_type = hysteria2_type
|
||||
end
|
||||
sub_hy_up_mbps = sub_cfg.hysteria_up_mbps
|
||||
sub_hy_down_mbps = sub_cfg.hysteria_down_mbps
|
||||
end
|
||||
local result = {
|
||||
timeout = 60,
|
||||
@ -778,6 +777,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
local sub_vmess_type = DEFAULT_VMESS_TYPE
|
||||
local sub_vless_type = DEFAULT_VLESS_TYPE
|
||||
local sub_hysteria2_type = DEFAULT_HYSTERIA2_TYPE
|
||||
local sub_hy_up_mbps, sub_hy_down_mbps = 1000, 1000
|
||||
if sub_cfg then
|
||||
if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then
|
||||
sub_allowinsecure = nil
|
||||
@ -802,6 +802,8 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
|
||||
sub_hysteria2_type = hysteria2_type
|
||||
end
|
||||
sub_hy_up_mbps = sub_cfg.hysteria_up_mbps
|
||||
sub_hy_down_mbps = sub_cfg.hysteria_down_mbps
|
||||
end
|
||||
local result = {
|
||||
timeout = 60,
|
||||
@ -1113,20 +1115,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
end
|
||||
end
|
||||
|
||||
if result.type == "SS" then
|
||||
local aead2022_methods = { "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" }
|
||||
local aead2022 = false
|
||||
for k, v in ipairs(aead2022_methods) do
|
||||
if method:lower() == v:lower() then
|
||||
aead2022 = true
|
||||
end
|
||||
end
|
||||
if aead2022 then
|
||||
-- shadowsocks-libev does not support 2022 encryption.
|
||||
result.error_msg = i18n.translatef("shadowsocks-libev unsupport 2022 encryption.")
|
||||
end
|
||||
end
|
||||
|
||||
if params.type then
|
||||
params.type = string.lower(params.type)
|
||||
if result.type == "sing-box" and params.type == "raw" then
|
||||
@ -1140,7 +1128,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
else
|
||||
result.transport = params.type
|
||||
end
|
||||
if result.type ~= "SS-Rust" and result.type ~= "SS" then
|
||||
if result.type ~= "SS-Rust" then
|
||||
if params.type == 'ws' then
|
||||
result.ws_host = params.host
|
||||
result.ws_path = params.path
|
||||
@ -1647,8 +1635,8 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
|
||||
result.alpn = params.alpn
|
||||
result.hysteria_up_mbps = params.upmbps
|
||||
result.hysteria_down_mbps = params.downmbps
|
||||
result.hysteria_up_mbps = params.upmbps or sub_hy_up_mbps
|
||||
result.hysteria_down_mbps = params.downmbps or sub_hy_down_mbps
|
||||
result.hysteria_hop = params.mport
|
||||
|
||||
elseif szType == 'hysteria2' or szType == 'hy2' then
|
||||
@ -1692,6 +1680,8 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
result.tls_CertByName = params.vcn
|
||||
local insecure = params.allowinsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
|
||||
result.hysteria2_up_mbps = params.upmbps or sub_hy_up_mbps
|
||||
result.hysteria2_down_mbps = params.downmbps or sub_hy_down_mbps
|
||||
result.hysteria2_hop = params.mport
|
||||
if params["obfs-password"] or params["obfs_password"] then
|
||||
result.hysteria2_obfs_type = "salamander"
|
||||
@ -2180,7 +2170,7 @@ local function update_node(manual)
|
||||
local list = v["list"]
|
||||
local sub_cfg = v["sub_cfg"]
|
||||
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
|
||||
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
|
||||
local preproxy_node_group, to_node_group, outbound_iface_group, chain_node_type = "", "", "", ""
|
||||
-- Subscription Group Chain Agent
|
||||
local function valid_chain_node(node)
|
||||
if not node then return "" end
|
||||
@ -2200,6 +2190,8 @@ local function update_node(manual)
|
||||
domain_strategy = (sub_cfg.domain_strategy == "UseIPv4" or sub_cfg.domain_strategy == "UseIPv6") and sub_cfg.domain_strategy or nil
|
||||
preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or ""
|
||||
to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or ""
|
||||
outbound_iface_group = (sub_cfg.chain_proxy == "3") and sub_cfg.outbound_iface or ""
|
||||
chain_node_type = (outbound_iface_group ~= "") and "iface" or chain_node_type
|
||||
end
|
||||
for _, vv in ipairs(list) do
|
||||
local cfgid = uci:section(appname, "nodes", api.gen_short_uuid())
|
||||
@ -2235,6 +2227,9 @@ local function update_node(manual)
|
||||
elseif to_node_group ~= "" then
|
||||
uci:set(appname, cfgid, "chain_proxy", "2")
|
||||
uci:set(appname, cfgid, "to_node", to_node_group)
|
||||
elseif outbound_iface_group ~= "" then
|
||||
uci:set(appname, cfgid, "chain_proxy", "3")
|
||||
uci:set(appname, cfgid, "outbound_iface", outbound_iface_group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -647,6 +647,8 @@ function index()
|
||||
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"))
|
||||
@ -665,7 +667,9 @@ end
|
||||
|
||||
function subscribe()
|
||||
nixio.fs.remove(SERVER_DETECT_CACHE)
|
||||
local ret = luci.sys.call(": > /var/log/ssrplus.log && /usr/bin/lua /usr/share/shadowsocksr/subscribe.lua >>/var/log/ssrplus.log 2>&1")
|
||||
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
|
||||
@ -775,8 +779,67 @@ function act_delete_node()
|
||||
return
|
||||
end
|
||||
|
||||
local com_cmd = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1")
|
||||
if com_cmd ~= 0 then
|
||||
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
|
||||
@ -788,21 +851,69 @@ end
|
||||
|
||||
function toggle_subscribe_item_enabled()
|
||||
local sid = trim(luci.http.formvalue("sid"))
|
||||
local enabled = luci.http.formvalue("enabled") == "1" and "1" or "0"
|
||||
local field = luci.http.formvalue("field")
|
||||
local value = luci.http.formvalue("value")
|
||||
|
||||
if sid == "" or uci:get("shadowsocksr", sid) ~= "server_subscribe_item" then
|
||||
-- 兼容旧调用方式(只传 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
|
||||
|
||||
uci:set("shadowsocksr", sid, "enabled", enabled)
|
||||
uci:save("shadowsocksr")
|
||||
uci:commit("shadowsocksr")
|
||||
-- 白名单:只允许以下字段
|
||||
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, enabled = enabled })
|
||||
luci.http.write_json({ ret = 1, sid = sid, field = field, value = value })
|
||||
end
|
||||
|
||||
function component_status()
|
||||
@ -1342,6 +1453,10 @@ function act_ping()
|
||||
})
|
||||
end
|
||||
|
||||
if e.ping then
|
||||
e.ping = math.floor(e.ping + 0.5)
|
||||
end
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
|
||||
@ -142,7 +142,7 @@ local function set_apply_on_parse(map)
|
||||
local old = map.on_after_save
|
||||
map.on_after_save = function(self)
|
||||
if old then old(self) end
|
||||
map:set("@global[0]", "timestamp", os.time())
|
||||
--map:set("@global[0]", "timestamp", os.time())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -64,7 +64,7 @@ local function set_apply_on_parse(map)
|
||||
local old = map.on_after_save
|
||||
map.on_after_save = function(self)
|
||||
if old then old(self) end
|
||||
map:set("@global[0]", "timestamp", os.time())
|
||||
-- map:set("@global[0]", "timestamp", os.time())
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -552,8 +552,9 @@ s = m:section(TypedSection, "server_subscribe_item", translate("Subscribe URL"))
|
||||
s.anonymous = true
|
||||
s.addremove = true
|
||||
s.sortable = true
|
||||
s.template = "cbi/tblsection"
|
||||
s.template_addremove = "shadowsocksr/subscribe_actions_footer"
|
||||
s.template = "shadowsocksr/subscribe_actions_footer"
|
||||
--s.template = "cbi/tblsection"
|
||||
--s.template_addremove = "shadowsocksr/subscribe_actions_footer"
|
||||
s.description = translate("Manage multiple subscribe URLs, including Clash subscriptions. Only enabled entries are included when updating all subscriptions.")
|
||||
|
||||
o = s:option(Flag, "enabled", translate("Enable"))
|
||||
|
||||
@ -199,16 +199,17 @@ local autostart = self.map and self.map.ssr_subscribe_autostart
|
||||
}
|
||||
|
||||
function startSubscribe(options, done) {
|
||||
options = options || {};
|
||||
running = true;
|
||||
updateLog('');
|
||||
setStatus('<%:Starting subscription update...%>', '#0072c3');
|
||||
setStatus(options.title || '<%:Starting subscription update...%>', '#0072c3');
|
||||
openModal();
|
||||
|
||||
clearPoll();
|
||||
fetchLog();
|
||||
pollTimer = window.setInterval(fetchLog, 1000);
|
||||
|
||||
XHR.get('<%=subscribe_url%>', null, function(x, rv) {
|
||||
XHR.get('<%=subscribe_url%>', options.sid ? { sid: options.sid } : null, function(x, rv) {
|
||||
var ok = !!(rv && typeof rv.ret !== 'undefined' && Number(rv.ret) === 0);
|
||||
if (typeof done === 'function') {
|
||||
done(ok);
|
||||
|
||||
@ -1,62 +1,295 @@
|
||||
<%-
|
||||
local rowcnt = 0
|
||||
|
||||
local function rowstyle()
|
||||
rowcnt = rowcnt + 1
|
||||
if rowcnt % 2 == 0 then
|
||||
return " cbi-rowstyle-1"
|
||||
end
|
||||
return " cbi-rowstyle-2"
|
||||
end
|
||||
|
||||
local function width(o)
|
||||
if o.width then
|
||||
if type(o.width) == "number" then
|
||||
return ' style="width:%dpx"' % o.width
|
||||
end
|
||||
return ' style="width:%s"' % o.width
|
||||
end
|
||||
return ""
|
||||
end
|
||||
-%>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
window.ssrUpdateSubscribeItem = window.ssrUpdateSubscribeItem || function(btn) {
|
||||
if (!btn) return false;
|
||||
|
||||
var sid = btn.getAttribute('data-sid') || '';
|
||||
var title = btn.getAttribute('data-title') || '';
|
||||
var original = btn.getAttribute('data-original-value') || btn.value || 'Update';
|
||||
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Updating...%>';
|
||||
window.ssrSubscribeRun = window.ssrSubscribeRun || function(_, done) {
|
||||
if (done) done();
|
||||
};
|
||||
window.ssrSubscribeRun({
|
||||
sid: sid,
|
||||
title: title
|
||||
}, function() {
|
||||
btn.disabled = false;
|
||||
btn.value = original;
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
function saveAllSubscribeItems(btn) {
|
||||
// var btn = document.querySelector('#ssr-subscribe-toolbar .cbi-button-save');
|
||||
if (!btn) return;
|
||||
|
||||
var originalText = btn.value;
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Saving...%>';
|
||||
|
||||
var fieldsMap = {};
|
||||
var allInputs = document.querySelectorAll('[name^="cbid.shadowsocksr."]');
|
||||
allInputs.forEach(function(input) {
|
||||
var match = input.name.match(/^cbid\.shadowsocksr\.([^.]+)\.(url|alias|enabled)$/);
|
||||
if (!match) return;
|
||||
|
||||
var sid = match[1];
|
||||
var field = match[2];
|
||||
if (!fieldsMap[sid]) fieldsMap[sid] = {};
|
||||
if (field === 'enabled') {
|
||||
fieldsMap[sid][field] = input.checked ? '1' : '0';
|
||||
} else {
|
||||
fieldsMap[sid][field] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
var promises = [];
|
||||
for (var sid in fieldsMap) {
|
||||
var fields = fieldsMap[sid];
|
||||
for (var field in fields) {
|
||||
var value = fields[field];
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
XHR.post(
|
||||
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled")%>',
|
||||
{ sid: sid, field: field, value: value },
|
||||
function(x, rv) {
|
||||
if (rv && Number(rv.ret) === 1) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('<%:Save failed:%> ' + sid + '.' + field + ': ' + (rv && rv.error ? rv.error : '<%:Unknown error%>')));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
btn.disabled = false;
|
||||
btn.value = originalText;
|
||||
alert('<%:No subscription items found%>');
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all(promises).then(function() {
|
||||
btn.disabled = false;
|
||||
btn.value = originalText;
|
||||
alert('<%:All settings saved successfully.%>');
|
||||
}).catch(function(err) {
|
||||
btn.disabled = false;
|
||||
btn.value = originalText;
|
||||
alert('<%:Save failed:%>' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSubscribeItem(btn) {
|
||||
var sid = btn.getAttribute('data-sid');
|
||||
if (!sid) return;
|
||||
if (!confirm('<%:Are you sure you want to delete subscribe link: %s ?%>'.replace('%s', sid))) return;
|
||||
|
||||
btn.disabled = true;
|
||||
XHR.post('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "delete_subscribe_item")%>',
|
||||
{ sid: sid },
|
||||
function(x, rv) {
|
||||
btn.disabled = false;
|
||||
if (rv && rv.ret === 1) {
|
||||
var row = btn.closest('tr');
|
||||
if (row) row.remove();
|
||||
var table = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table');
|
||||
if (table.querySelectorAll('.cbi-section-table-row:not(.cbi-section-table-titles)').length === 0) {
|
||||
var placeholder = document.createElement('tr');
|
||||
placeholder.className = 'tr cbi-section-table-row placeholder';
|
||||
placeholder.innerHTML = '<td class="td" colspan="4"><em><%:This section contains no values yet%></em></td>';
|
||||
table.appendChild(placeholder);
|
||||
}
|
||||
} else {
|
||||
alert('<%:Delete failed:%> ' + (rv ? rv.error : '<%:Unknown error%>'));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createEmptyRow(sid) {
|
||||
var templateRow = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row:not(.cbi-section-table-titles):not(.placeholder)');
|
||||
if (!templateRow) {
|
||||
console.error('No template row found');
|
||||
return null;
|
||||
}
|
||||
|
||||
var newRow = templateRow.cloneNode(true);
|
||||
newRow.id = 'cbi-shadowsocksr-server_subscribe_item-' + sid;
|
||||
|
||||
var inputs = newRow.querySelectorAll('input');
|
||||
inputs.forEach(function(input) {
|
||||
if (input.name) {
|
||||
input.name = input.name.replace(/cbid\.shadowsocksr\.[^.]+\./, 'cbid.shadowsocksr.' + sid + '.');
|
||||
}
|
||||
if (input.id) {
|
||||
input.id = input.id.replace(/cfg[^.]+/, sid);
|
||||
}
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = false;
|
||||
input.defaultChecked = false;
|
||||
} else if (input.type === 'text' || input.type === 'url') {
|
||||
input.value = '';
|
||||
input.defaultValue = '';
|
||||
}
|
||||
input.disabled = false;
|
||||
});
|
||||
|
||||
var delBtn = newRow.querySelector('.cbi-button-remove');
|
||||
if (delBtn) {
|
||||
delBtn.setAttribute('data-sid', sid);
|
||||
delBtn.removeAttribute('onclick');
|
||||
delBtn.onclick = function(e) { deleteSubscribeItem(this); return false; };
|
||||
}
|
||||
var updateBtn = newRow.querySelector('.cbi-button-apply');
|
||||
if (updateBtn) {
|
||||
updateBtn.setAttribute('data-sid', sid);
|
||||
}
|
||||
return newRow;
|
||||
}
|
||||
|
||||
function addSubscribeItem() {
|
||||
var btn = document.getElementById('ssr-add-btn');
|
||||
if (!btn) return;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Adding...%>';
|
||||
|
||||
XHR.post(
|
||||
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "add_subscribe_item")%>',
|
||||
{},
|
||||
function(x, rv) {
|
||||
btn.disabled = false;
|
||||
btn.value = '<%:Add%>';
|
||||
if (rv && rv.ret === 1 && rv.sid) {
|
||||
var newRow = createEmptyRow(rv.sid);
|
||||
if (newRow) {
|
||||
var aliasInput = newRow.querySelector('input[name$=".alias"]');
|
||||
if (aliasInput) {
|
||||
aliasInput.value = rv.alias || '';
|
||||
aliasInput.defaultValue = rv.alias || '';
|
||||
}
|
||||
var urlInput = newRow.querySelector('input[name$=".url"]');
|
||||
if (urlInput) {
|
||||
urlInput.value = rv.url || '';
|
||||
urlInput.defaultValue = rv.url || '';
|
||||
}
|
||||
var enabledInput = newRow.querySelector('input[type="checkbox"]');
|
||||
if (enabledInput) {
|
||||
var checked = (rv.enabled === '1');
|
||||
enabledInput.checked = checked;
|
||||
enabledInput.defaultChecked = checked;
|
||||
}
|
||||
var table = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table');
|
||||
var placeholder = table.querySelector('.placeholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
table.appendChild(newRow);
|
||||
}
|
||||
} else {
|
||||
alert('<%:Add failed:%> ' + (rv && rv.error ? rv.error : '<%:Unknown error%>'));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//]]></script>
|
||||
<div class="cbi-section cbi-tblsection" id="cbi-<%=self.config%>-<%=self.sectiontype%>">
|
||||
<% if self.title and #self.title > 0 then -%>
|
||||
<h3><%=self.title%></h3>
|
||||
<%- end %>
|
||||
<%- if self.sortable then -%>
|
||||
<input type="hidden" id="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" name="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" value="" />
|
||||
<%- end -%>
|
||||
<div class="cbi-section-descr"><%=self.description%></div>
|
||||
<table class="table cbi-section-table">
|
||||
<tr class="tr cbi-section-table-titles anonymous">
|
||||
<% for _, node in ipairs(self.children) do if not node.optional and node.option ~= "_update" then -%>
|
||||
<th class="th cbi-section-table-cell"<%=width(node) .. attr("data-widget", node.typename or "")%>><%=node.title%></th>
|
||||
<% end end -%>
|
||||
<th class="th cbi-section-table-cell cbi-section-actions" style="width:1%"><%:Actions%></th>
|
||||
</tr>
|
||||
|
||||
<%
|
||||
local isempty = true
|
||||
for i, section in ipairs(self:cfgsections()) do
|
||||
isempty = false
|
||||
local colorclass = rowstyle()
|
||||
local scope = {
|
||||
valueheader = "cbi/cell_valueheader",
|
||||
valuefooter = "cbi/cell_valuefooter"
|
||||
}
|
||||
-%>
|
||||
<tr class="tr cbi-section-table-row<%=colorclass%>" id="cbi-<%=self.config%>-<%=section%>">
|
||||
<%
|
||||
for _, node in ipairs(self.children) do
|
||||
if not node.optional and node.option ~= "_update" then
|
||||
node:render(section, scope)
|
||||
end
|
||||
end
|
||||
local alias = self.map:get(section, "alias") or string.format("Subscribe %s", section:sub(-4))
|
||||
local update_title = "Subscribe: " .. tostring(alias)
|
||||
-%>
|
||||
<td class="td cbi-section-table-cell nowrap cbi-section-actions">
|
||||
<div class="ssr-subscribe-actions">
|
||||
<input class="btn cbi-button cbi-button-save" type="button" value="<%:Save%>" title="<%:Save%>" onclick="saveAllSubscribeItems(this)" />
|
||||
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" data-sid="<%=section%>" onclick="deleteSubscribeItem(this); return false;" />
|
||||
<input
|
||||
type="button"
|
||||
class="btn cbi-button cbi-button-apply"
|
||||
value="<%:Update%>"
|
||||
data-original-value="<%:Update%>"
|
||||
<%=attr("data-sid", section) .. attr("data-title", update_title)%>
|
||||
onclick="return window.ssrUpdateSubscribeItem(this)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<%- end -%>
|
||||
|
||||
<%- if isempty then -%>
|
||||
<tr class="tr cbi-section-table-row placeholder">
|
||||
<td class="td" colspan="4"><em><%:This section contains no values yet%></em></td>
|
||||
</tr>
|
||||
<%- end -%>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="cbi-section-create cbi-tblsection-create ssr-subscribe-toolbar" id="ssr-subscribe-toolbar">
|
||||
<input
|
||||
class="btn cbi-button cbi-button-add"
|
||||
type="submit"
|
||||
value="<%:Add%>"
|
||||
name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>"
|
||||
title="<%:Add%>"
|
||||
/>
|
||||
|
||||
<input
|
||||
class="btn cbi-button cbi-button-save ssr-subscribe-save-settings"
|
||||
type="submit"
|
||||
value="<%:Save%>"
|
||||
title="<%:Save%>"
|
||||
/>
|
||||
|
||||
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Add%>" id="ssr-add-btn" title="<%:Add%>" />
|
||||
<!-- <input class="btn cbi-button cbi-button-save" type="button" value="<%:Save%>" title="<%:Save%>" onclick="saveAllSubscribeItems(this)" /> -->
|
||||
<!-- <input class="btn cbi-button cbi-button-save ssr-subscribe-save-settings" type="submit" value="<%:Save%>" title="<%:Save%>" name="cbi.apply" /> -->
|
||||
<div class="ssr-subscribe-toolbar-slot ssr-subscribe-toolbar-slot-delete"></div>
|
||||
<div class="ssr-subscribe-toolbar-slot ssr-subscribe-toolbar-slot-update"></div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
(function() {
|
||||
function bindSubscribeItemEnabledAutoSave() {
|
||||
var root = document.getElementById('cbi-shadowsocksr-server_subscribe_item');
|
||||
if (!root || root.getAttribute('data-enabled-autosave-bound') === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
root.addEventListener('change', function(ev) {
|
||||
var target = ev.target;
|
||||
if (!target || target.type !== 'checkbox' || !target.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
var matched = target.name.match(/^cbid\.shadowsocksr\.([^.]+)\.enabled$/);
|
||||
if (!matched) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sid = matched[1];
|
||||
var original = target.checked;
|
||||
target.disabled = true;
|
||||
|
||||
XHR.post(
|
||||
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled")%>',
|
||||
{ sid: sid, enabled: original ? '1' : '0' },
|
||||
function(x, rv) {
|
||||
target.disabled = false;
|
||||
if (!(rv && Number(rv.ret) === 1)) {
|
||||
target.checked = !original;
|
||||
alert('Failed to save subscription enable state.');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
root.setAttribute('data-enabled-autosave-bound', '1');
|
||||
}
|
||||
|
||||
function styleActionButton(btn, extraClass) {
|
||||
if (!btn) return;
|
||||
btn.classList.remove('cbi-button-apply', 'cbi-button-reset');
|
||||
@ -94,14 +327,26 @@
|
||||
toolbar.setAttribute('data-ssr-subscribe-actions-ready', '1');
|
||||
}
|
||||
|
||||
function bindAddButton() {
|
||||
var addBtn = document.getElementById('ssr-add-btn');
|
||||
if (addBtn && !addBtn.getAttribute('data-bound')) {
|
||||
addBtn.addEventListener('click', addSubscribeItem);
|
||||
addBtn.setAttribute('data-bound', '1');
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bindSubscribeItemEnabledAutoSave);
|
||||
document.addEventListener('DOMContentLoaded', mountSubscribeActions);
|
||||
window.addEventListener('load', bindSubscribeItemEnabledAutoSave);
|
||||
window.addEventListener('load', mountSubscribeActions);
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
mountSubscribeActions();
|
||||
bindAddButton();
|
||||
});
|
||||
window.addEventListener('load', function() {
|
||||
mountSubscribeActions();
|
||||
bindAddButton();
|
||||
});
|
||||
} else {
|
||||
window.setTimeout(bindSubscribeItemEnabledAutoSave, 0);
|
||||
window.setTimeout(mountSubscribeActions, 0);
|
||||
window.setTimeout(bindAddButton, 0);
|
||||
}
|
||||
})();
|
||||
//]]></script>
|
||||
@ -114,7 +359,8 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#ssr-subscribe-toolbar > .btn.cbi-button-add {
|
||||
#ssr-subscribe-toolbar > .btn.cbi-button-add,
|
||||
#ssr-subscribe-toolbar > .btn.cbi-button-save {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -143,17 +389,13 @@
|
||||
}
|
||||
|
||||
#ssr-subscribe-toolbar > .btn.cbi-button-add,
|
||||
#ssr-subscribe-toolbar > .ssr-subscribe-save-settings,
|
||||
#ssr-subscribe-toolbar > .btn.cbi-button-save,
|
||||
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot .btn {
|
||||
min-height: 2.1rem;
|
||||
padding-top: 0.18rem;
|
||||
padding-bottom: 0.18rem;
|
||||
}
|
||||
|
||||
#ssr-subscribe-toolbar > .ssr-subscribe-save-settings {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot .btn.cbi-button-add {
|
||||
margin: 0;
|
||||
width: auto;
|
||||
@ -174,7 +416,7 @@
|
||||
}
|
||||
|
||||
#ssr-subscribe-toolbar > .btn.cbi-button-add,
|
||||
#ssr-subscribe-toolbar > .ssr-subscribe-save-settings,
|
||||
#ssr-subscribe-toolbar > .btn.cbi-button-save,
|
||||
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-delete,
|
||||
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-update {
|
||||
margin-left: 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -21,13 +21,13 @@ define Download/geoip
|
||||
HASH:=e9002979e0df72bce1c8751ff70725386594c551db684b7a232935b8b2bb8aa2
|
||||
endef
|
||||
|
||||
GEOSITE_VER:=20260520170255
|
||||
GEOSITE_VER:=20260522120028
|
||||
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
|
||||
define Download/geosite
|
||||
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
|
||||
URL_FILE:=dlc.dat
|
||||
FILE:=$(GEOSITE_FILE)
|
||||
HASH:=441d689a2f129132e0dd4c51517fe370a2e66eb2cccb86e4f5cd010559d0b5ce
|
||||
HASH:=1c039aacd6346523f7a316d63318c62d4333864f7a88519e445777005cf65ef3
|
||||
endef
|
||||
|
||||
GEOSITE_IRAN_VER:=202605180152
|
||||
|
||||
Loading…
Reference in New Issue
Block a user