🤞 Sync 2026-11-10 21:15:30

This commit is contained in:
github-actions[bot] committed 2026-11-10 21:15:30 +08:00
1 parent 42de8331af
commit 4a38dae8ff
27 files changed
+1281 -235

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.11.3 PKG_VERSION:=26.11.3
PKG_RELEASE:=373 PKG_RELEASE:=374
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -362,16 +362,22 @@ function gen_outbound(flag, node, tag, proxy_table)
["User-Agent"] = node.user_agent ["User-Agent"] = node.user_agent
} or nil, } or nil,
} }
local path = api.UrlDecode(node.ws_path) local path, query = (node.ws_path or "/"):match("^(.-)%?(.*)$")
local path_dat = api.split(path, "?") local params, query_params = {}, {}
local params = {} for v in (query or ""):gmatch("[^&]+") do
for _, v in pairs(api.split(path_dat[2], '&')) do local t = api.split(api.UrlDecode(v), '=')
local t = api.split(v, '=')
params[t[1]] = t[2] params[t[1]] = t[2]
if t[1] ~= "ed" and t[1] ~= "eh" then
table.insert(query_params, v)
end
end end
local ed = tonumber(params.ed) local ed = tonumber(params.ed)
if ed then if ed then
v2ray_transport.path = path_dat[1] -- Consume early-data options while preserving the remaining query.
v2ray_transport.path = path
if #query_params > 0 then
v2ray_transport.path = path .. "?" .. table.concat(query_params, "&")
end
v2ray_transport.max_early_data = ed v2ray_transport.max_early_data = ed
end end
v2ray_transport.early_data_header_name = params.eh or "Sec-WebSocket-Protocol" v2ray_transport.early_data_header_name = params.eh or "Sec-WebSocket-Protocol"
+25 -20
View File
@@ -12,7 +12,7 @@ local GLOBAL = {
local xray_version = api.get_app_version("xray") local xray_version = api.get_app_version("xray")
local xray_min_version = "26.7.11" local xray_min_version = "26.9.9"
local function get_domain_excluded() local function get_domain_excluded()
local path = string.format("/etc/%s/rules/domains_excluded", api.c_config) local path = string.format("/etc/%s/rules/domains_excluded", api.c_config)
@@ -355,6 +355,30 @@ function gen_outbound(flag, node, tag, proxy_table)
} }
} }
udp[#udp+1] = r udp[#udp+1] = r
elseif node.hysteria2_hop and node.hysteria2_hop ~= "" then
udp[#udp+1] = {
type = "udphop",
settings = {
mode = "intervallocal,intervalremote",
remotePorts = string.gsub(node.hysteria2_hop, ":", "-"),
interval = (function(v)
if not v then return 30 end
if v:find("-", 1, true) then
local min, max = v:match("^(%d+)%-(%d+)$")
min = tonumber(min)
max = tonumber(max)
if min and max then
min = (min >= 5) and min or 5
max = (max >= min) and max or min
return min .. "-" .. max
end
return 30
end
v = tonumber((v or "30"):match("^%d+"))
return (v and v >= 5) and v or 30
end)(node.hysteria2_hop_interval)
}
}
end end
finalmask.udp = udp finalmask.udp = udp
local up = tonumber(node.hysteria2_up_mbps) or 0 local up = tonumber(node.hysteria2_up_mbps) or 0
@@ -363,25 +387,6 @@ function gen_outbound(flag, node, tag, proxy_table)
congestion = (up <= 0 and down <= 0) and "bbr" or "brutal", congestion = (up <= 0 and down <= 0) and "bbr" or "brutal",
brutalUp = up > 0 and (up .. "mbps") or nil, brutalUp = up > 0 and (up .. "mbps") or nil,
brutalDown = down > 0 and (down .. "mbps") or nil, brutalDown = down > 0 and (down .. "mbps") or nil,
udpHop = (node.hysteria2_hop) and {
ports = string.gsub(node.hysteria2_hop, ":", "-"),
interval = (function(v)
if not v then return 30 end
if v:find("-", 1, true) then
local min, max = v:match("^(%d+)%-(%d+)$")
min = tonumber(min)
max = tonumber(max)
if min and max then
min = (min >= 5) and min or 5
max = (max >= min) and max or min
return min .. "-" .. max
end
return 30
end
v = tonumber((v or "30"):match("^%d+"))
return (v and v >= 5) and v or 30
end)(node.hysteria2_hop_interval)
} or nil,
maxIdleTimeout = (function(t) maxIdleTimeout = (function(t)
t = tonumber(tostring(t or "30"):match("^%d+")) t = tonumber(tostring(t or "30"):match("^%d+"))
return (t and t >= 4 and t <= 120) and t or 30 return (t and t >= 4 and t <= 120) and t or 30
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.11.3 PKG_VERSION:=26.11.3
PKG_RELEASE:=174 PKG_RELEASE:=175
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -810,16 +810,6 @@ o = s:option(Value, "ws_path", translate("WebSocket Path"))
o.placeholder = "/" o.placeholder = "/"
o:depends({ transport = "ws" }) o:depends({ transport = "ws" })
o = s:option(Flag, "ws_enableEarlyData", translate("Enable early data"))
o:depends({ transport = "ws" })
o = s:option(Value, "ws_maxEarlyData", translate("Early data length"))
o.default = "1024"
o:depends({ ws_enableEarlyData = true })
o = s:option(Value, "ws_earlyDataHeaderName", translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol"))
o:depends({ ws_enableEarlyData = true })
-- [[ HTTPUpgrade ]]-- -- [[ HTTPUpgrade ]]--
o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host")) o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host"))
o:depends({ transport = "httpupgrade" }) o:depends({ transport = "httpupgrade" })
@@ -459,6 +459,10 @@ o:depends({ transport = "ws" })
o = s:option(Value, "ws_path", translate("WebSocket Path")) o = s:option(Value, "ws_path", translate("WebSocket Path"))
o:depends({ transport = "ws" }) o:depends({ transport = "ws" })
o = s:option(Value, "ws_earlyDataHeaderName", translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol"))
o.placeholder = "Sec-WebSocket-Protocol"
o:depends({ transport = "ws" })
-- [[ HTTPUpgrade ]]-- -- [[ HTTPUpgrade ]]--
o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host")) o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host"))
@@ -366,9 +366,26 @@ function gen_outbound(flag, node, tag, proxy_table)
Host = node.ws_host, Host = node.ws_host,
["User-Agent"] = node.user_agent ["User-Agent"] = node.user_agent
} or nil, } or nil,
max_early_data = tonumber(node.ws_maxEarlyData) or nil,
early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil -- For compatibility with Xray-core, set it to Sec-WebSocket-Protocol. It needs to be consistent with the server.
} }
local path, query = (node.ws_path or "/"):match("^(.-)%?(.*)$")
local params, query_params = {}, {}
for v in (query or ""):gmatch("[^&]+") do
local t = api.split(api.UrlDecode(v), '=')
params[t[1]] = t[2]
if t[1] ~= "ed" and t[1] ~= "eh" then
table.insert(query_params, v)
end
end
local ed = tonumber(params.ed)
if ed then
-- Consume early-data options while preserving the remaining query.
v2ray_transport.path = path
if #query_params > 0 then
v2ray_transport.path = path .. "?" .. table.concat(query_params, "&")
end
v2ray_transport.max_early_data = ed
end
v2ray_transport.early_data_header_name = params.eh or "Sec-WebSocket-Protocol"
end end
if node.transport == "httpupgrade" then if node.transport == "httpupgrade" then
@@ -822,7 +839,7 @@ function gen_config_server(node)
type = "ws", type = "ws",
path = node.ws_path or "/", path = node.ws_path or "/",
headers = (node.ws_host ~= nil) and { Host = node.ws_host } or nil, headers = (node.ws_host ~= nil) and { Host = node.ws_host } or nil,
early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil -- For compatibility with Xray-core, set it to Sec-WebSocket-Protocol. It needs to be consistent with the server. early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or "Sec-WebSocket-Protocol"
} }
end end
@@ -12,7 +12,7 @@ local GLOBAL = {
local xray_version = api.get_app_version("xray") local xray_version = api.get_app_version("xray")
local xray_min_version = "26.7.11" local xray_min_version = "26.9.9"
local function get_domain_excluded() local function get_domain_excluded()
local path = "/usr/share/passwall2/domains_excluded" local path = "/usr/share/passwall2/domains_excluded"
@@ -364,6 +364,30 @@ function gen_outbound(flag, node, tag, proxy_table)
} }
} }
udp[#udp+1] = r udp[#udp+1] = r
elseif node.hysteria2_hop and node.hysteria2_hop ~= "" then
udp[#udp+1] = {
type = "udphop",
settings = {
mode = "intervallocal,intervalremote",
remotePorts = string.gsub(node.hysteria2_hop, ":", "-"),
interval = (function(v)
if not v then return 30 end
if v:find("-", 1, true) then
local min, max = v:match("^(%d+)%-(%d+)$")
min = tonumber(min)
max = tonumber(max)
if min and max then
min = (min >= 5) and min or 5
max = (max >= min) and max or min
return min .. "-" .. max
end
return 30
end
v = tonumber((v or "30"):match("^%d+"))
return (v and v >= 5) and v or 30
end)(node.hysteria2_hop_interval)
}
}
end end
finalmask.udp = udp finalmask.udp = udp
local up = tonumber(node.hysteria2_up_mbps) or 0 local up = tonumber(node.hysteria2_up_mbps) or 0
@@ -372,25 +396,6 @@ function gen_outbound(flag, node, tag, proxy_table)
congestion = (up <= 0 and down <= 0) and "bbr" or "brutal", congestion = (up <= 0 and down <= 0) and "bbr" or "brutal",
brutalUp = up > 0 and (up .. "mbps") or nil, brutalUp = up > 0 and (up .. "mbps") or nil,
brutalDown = down > 0 and (down .. "mbps") or nil, brutalDown = down > 0 and (down .. "mbps") or nil,
udpHop = (node.hysteria2_hop) and {
ports = string.gsub(node.hysteria2_hop, ":", "-"),
interval = (function(v)
if not v then return 30 end
if v:find("-", 1, true) then
local min, max = v:match("^(%d+)%-(%d+)$")
min = tonumber(min)
max = tonumber(max)
if min and max then
min = (min >= 5) and min or 5
max = (max >= min) and max or min
return min .. "-" .. max
end
return 30
end
v = tonumber((v or "30"):match("^%d+"))
return (v and v >= 5) and v or 30
end)(node.hysteria2_hop_interval)
} or nil,
maxIdleTimeout = (function(t) maxIdleTimeout = (function(t)
t = tonumber(tostring(t or "30"):match("^%d+")) t = tonumber(tostring(t or "30"):match("^%d+"))
return (t and t >= 4 and t <= 120) and t or 30 return (t and t >= 4 and t <= 120) and t or 30
@@ -254,10 +254,6 @@ local current_node = map:get(section)
if (v_transport === "ws") { if (v_transport === "ws") {
params += opt.query("host", dom_prefix + "ws_host"); params += opt.query("host", dom_prefix + "ws_host");
params += opt.query("path", dom_prefix + "ws_path"); params += opt.query("path", dom_prefix + "ws_path");
if (v_type == "sing-box" && opt.get(dom_prefix + "ws_enableEarlyData").checked) {
var ws_maxEarlyData = opt.get(dom_prefix + "ws_maxEarlyData").value;
params += "?ed=" + ws_maxEarlyData;
}
} else if (v_transport === "http") { } else if (v_transport === "http") {
params += opt.query("host", dom_prefix + "http_host"); params += opt.query("host", dom_prefix + "http_host");
params += opt.query("path", dom_prefix + "http_path"); params += opt.query("path", dom_prefix + "http_path");
@@ -376,10 +372,6 @@ local current_node = map:get(section)
if (v_transport === "ws") { if (v_transport === "ws") {
info.host = opt.get(dom_prefix + "ws_host").value; info.host = opt.get(dom_prefix + "ws_host").value;
info.path = opt.get(dom_prefix + "ws_path").value; info.path = opt.get(dom_prefix + "ws_path").value;
if (v_type == "sing-box" && opt.get(dom_prefix + "ws_enableEarlyData").checked) {
var ws_maxEarlyData = opt.get(dom_prefix + "ws_maxEarlyData").value;
info.path = info.path + "?ed=" + ws_maxEarlyData;
}
} else if (v_transport === "http") { } else if (v_transport === "http") {
info.host = opt.get(dom_prefix + "http_host").value; info.host = opt.get(dom_prefix + "http_host").value;
info.path = opt.get(dom_prefix + "http_path").value; info.path = opt.get(dom_prefix + "http_path").value;
@@ -436,10 +428,6 @@ local current_node = map:get(section)
if (v_transport === "ws") { if (v_transport === "ws") {
params += opt.query("host", dom_prefix + "ws_host"); params += opt.query("host", dom_prefix + "ws_host");
params += opt.query("path", dom_prefix + "ws_path"); params += opt.query("path", dom_prefix + "ws_path");
if (v_type == "sing-box" && opt.get(dom_prefix + "ws_enableEarlyData").checked) {
var ws_maxEarlyData = opt.get(dom_prefix + "ws_maxEarlyData").value;
params += encodeURIComponent("?ed=" + ws_maxEarlyData);
}
} else if (v_transport === "http") { } else if (v_transport === "http") {
params += opt.query("host", dom_prefix + "http_host"); params += opt.query("host", dom_prefix + "http_host");
params += opt.query("path", dom_prefix + "http_path"); params += opt.query("path", dom_prefix + "http_path");
@@ -528,10 +516,6 @@ local current_node = map:get(section)
if (v_transport === "ws") { if (v_transport === "ws") {
params += opt.query("host", dom_prefix + "ws_host"); params += opt.query("host", dom_prefix + "ws_host");
params += opt.query("path", dom_prefix + "ws_path"); params += opt.query("path", dom_prefix + "ws_path");
if (v_type == "sing-box" && opt.get(dom_prefix + "ws_enableEarlyData").checked) {
var ws_maxEarlyData = opt.get(dom_prefix + "ws_maxEarlyData").value;
params += "?ed=" + ws_maxEarlyData;
}
} else if (v_transport === "http") { } else if (v_transport === "http") {
params += opt.query("host", dom_prefix + "http_host"); params += opt.query("host", dom_prefix + "http_host");
params += opt.query("path", dom_prefix + "http_path"); params += opt.query("path", dom_prefix + "http_path");
@@ -1111,24 +1095,6 @@ local current_node = map:get(section)
} else if (queryParam.type === "ws") { } else if (queryParam.type === "ws") {
opt.set(dom_prefix + 'ws_host', queryParam.host || ""); opt.set(dom_prefix + 'ws_host', queryParam.host || "");
opt.set(dom_prefix + 'ws_path', queryParam.path || ""); opt.set(dom_prefix + 'ws_path', queryParam.path || "");
if (dom_prefix == "singbox_" && queryParam.path && queryParam.path.length > 1) {
var ws_path_params = {};
var ws_path_dat = queryParam.path.split('?');
var ws_path = ws_path_dat[0];
var ws_path_params = {};
var ws_path_params_array = (ws_path_dat[1] || '').split('&');
for (i = 0; i < ws_path_params_array.length; i++) {
var kv = ws_path_params_array[i].split('=');
ws_path_params[decodeURIComponent(kv[0]).toLowerCase()] = decodeURIComponent(kv[1] || '');
}
if (ws_path_params.ed) {
opt.set(dom_prefix + 'ws_path', ws_path);
opt.set(dom_prefix + 'ws_enableEarlyData', true);
opt.set(dom_prefix + 'ws_maxEarlyData', ws_path_params.ed);
opt.set(dom_prefix + 'ws_earlyDataHeaderName', 'Sec-WebSocket-Protocol');
}
}
} else if (queryParam.type === "http") { } else if (queryParam.type === "http") {
if (dom_prefix == "xray_") { if (dom_prefix == "xray_") {
opt.set(dom_prefix + 'xhttp_mode', "stream-one"); opt.set(dom_prefix + 'xhttp_mode', "stream-one");
@@ -1259,24 +1225,6 @@ local current_node = map:get(section)
} else if (queryParam.type === "ws") { } else if (queryParam.type === "ws") {
opt.set(dom_prefix + 'ws_host', queryParam.host || ""); opt.set(dom_prefix + 'ws_host', queryParam.host || "");
opt.set(dom_prefix + 'ws_path', queryParam.path || ""); opt.set(dom_prefix + 'ws_path', queryParam.path || "");
if (dom_prefix == "singbox_" && queryParam.path && queryParam.path.length > 1) {
var ws_path_params = {};
var ws_path_dat = queryParam.path.split('?');
var ws_path = ws_path_dat[0];
var ws_path_params = {};
var ws_path_params_array = (ws_path_dat[1] || '').split('&');
for (i = 0; i < ws_path_params_array.length; i++) {
var kv = ws_path_params_array[i].split('=');
ws_path_params[decodeURIComponent(kv[0]).toLowerCase()] = decodeURIComponent(kv[1] || '');
}
if (ws_path_params.ed) {
opt.set(dom_prefix + 'ws_path', ws_path);
opt.set(dom_prefix + 'ws_enableEarlyData', true);
opt.set(dom_prefix + 'ws_maxEarlyData', ws_path_params.ed);
opt.set(dom_prefix + 'ws_earlyDataHeaderName', 'Sec-WebSocket-Protocol');
}
}
} else if (queryParam.type === "h2" || queryParam.type === "http") { } else if (queryParam.type === "h2" || queryParam.type === "http") {
if (dom_prefix == "xray_") { if (dom_prefix == "xray_") {
opt.set(dom_prefix + 'xhttp_mode', "stream-one"); opt.set(dom_prefix + 'xhttp_mode', "stream-one");
@@ -1446,24 +1394,6 @@ local current_node = map:get(section)
} else if (ssm.net === "ws") { } else if (ssm.net === "ws") {
opt.set(dom_prefix + 'ws_host', ssm.host); opt.set(dom_prefix + 'ws_host', ssm.host);
opt.set(dom_prefix + 'ws_path', ssm.path); opt.set(dom_prefix + 'ws_path', ssm.path);
if (dom_prefix == "singbox_" && ssm.path && ssm.path.length > 1) {
var ws_path_params = {};
var ws_path_dat = ssm.path.split('?');
var ws_path = ws_path_dat[0];
var ws_path_params = {};
var ws_path_params_array = (ws_path_dat[1] || '').split('&');
for (i = 0; i < ws_path_params_array.length; i++) {
var kv = ws_path_params_array[i].split('=');
ws_path_params[decodeURIComponent(kv[0]).toLowerCase()] = decodeURIComponent(kv[1] || '');
}
if (ws_path_params.ed) {
opt.set(dom_prefix + 'ws_path', ws_path);
opt.set(dom_prefix + 'ws_enableEarlyData', true);
opt.set(dom_prefix + 'ws_maxEarlyData', ws_path_params.ed);
opt.set(dom_prefix + 'ws_earlyDataHeaderName', 'Sec-WebSocket-Protocol');
}
}
} else if (ssm.net === "http") { } else if (ssm.net === "http") {
if (dom_prefix == "xray_") { if (dom_prefix == "xray_") {
opt.set(dom_prefix + 'xhttp_mode', "stream-one"); opt.set(dom_prefix + 'xhttp_mode', "stream-one");
@@ -1589,24 +1519,6 @@ local current_node = map:get(section)
} else if (queryParam.type === "ws") { } else if (queryParam.type === "ws") {
opt.set(dom_prefix + 'ws_host', queryParam.host || ""); opt.set(dom_prefix + 'ws_host', queryParam.host || "");
opt.set(dom_prefix + 'ws_path', queryParam.path || ""); opt.set(dom_prefix + 'ws_path', queryParam.path || "");
if (dom_prefix == "singbox_" && queryParam.path && queryParam.path.length > 1) {
var ws_path_params = {};
var ws_path_dat = queryParam.path.split('?');
var ws_path = ws_path_dat[0];
var ws_path_params = {};
var ws_path_params_array = (ws_path_dat[1] || '').split('&');
for (i = 0; i < ws_path_params_array.length; i++) {
var kv = ws_path_params_array[i].split('=');
ws_path_params[decodeURIComponent(kv[0]).toLowerCase()] = decodeURIComponent(kv[1] || '');
}
if (ws_path_params.ed) {
opt.set(dom_prefix + 'ws_path', ws_path);
opt.set(dom_prefix + 'ws_enableEarlyData', true);
opt.set(dom_prefix + 'ws_maxEarlyData', ws_path_params.ed);
opt.set(dom_prefix + 'ws_earlyDataHeaderName', 'Sec-WebSocket-Protocol');
}
}
} else if (queryParam.type === "h2" || queryParam.type === "http") { } else if (queryParam.type === "h2" || queryParam.type === "http") {
if (dom_prefix == "xray_") { if (dom_prefix == "xray_") {
opt.set(dom_prefix + 'xhttp_mode', "stream-one"); opt.set(dom_prefix + 'xhttp_mode', "stream-one");
-6
View File
@@ -1585,12 +1585,6 @@ msgstr "همزمانی XUDP Mux"
msgid "Padding" msgid "Padding"
msgstr "پدینگ (Padding)" msgstr "پدینگ (Padding)"
msgid "Enable early data"
msgstr "فعال‌سازی داده‌های اولیه (Early Data)"
msgid "Early data length"
msgstr "طول داده‌های اولیه"
msgid "Early data header name" msgid "Early data header name"
msgstr "نام هدر داده‌های اولیه" msgstr "نام هدر داده‌های اولیه"
-6
View File
@@ -1586,12 +1586,6 @@ msgstr "Макс. параллельных соединений XUDP"
msgid "Padding" msgid "Padding"
msgstr "Заполнение" msgstr "Заполнение"
msgid "Enable early data"
msgstr "Включить 0-RTT данные"
msgid "Early data length"
msgstr "Макс. длина 0-RTT данных"
msgid "Early data header name" msgid "Early data header name"
msgstr "Имя HTTP-заголовка для 0-RTT" msgstr "Имя HTTP-заголовка для 0-RTT"
-6
View File
@@ -1577,12 +1577,6 @@ msgstr "XUDP 最大并发连接数"
msgid "Padding" msgid "Padding"
msgstr "填充" msgstr "填充"
msgid "Enable early data"
msgstr "启用前置数据"
msgid "Early data length"
msgstr "前置数据最大长度"
msgid "Early data header name" msgid "Early data header name"
msgstr "前置数据 HTTP 头名" msgstr "前置数据 HTTP 头名"
-6
View File
@@ -1577,12 +1577,6 @@ msgstr "XUDP 最大並發連接數"
msgid "Padding" msgid "Padding"
msgstr "填充" msgstr "填充"
msgid "Enable early data"
msgstr "啟用前置數据"
msgid "Early data length"
msgstr "前置數据最大长度"
msgid "Early data header name" msgid "Early data header name"
msgstr "前置數据 HTTP 头名" msgstr "前置數据 HTTP 头名"
@@ -679,21 +679,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
if info.net == 'ws' then if info.net == 'ws' then
result.ws_host = info.host result.ws_host = info.host
result.ws_path = info.path result.ws_path = info.path
if result.type == "sing-box" and info.path then
local ws_path_dat = split(info.path, "?")
local ws_path = ws_path_dat[1]
local ws_path_params = {}
for _, v in pairs(split(ws_path_dat[2], '&')) do
local t = split(v, '=')
ws_path_params[t[1]] = t[2]
end
if ws_path_params.ed and tonumber(ws_path_params.ed) then
result.ws_path = ws_path
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_path_params.ed)
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
end
end
end end
if info.net == "http" then if info.net == "http" then
if result.type == "Xray" then if result.type == "Xray" then
@@ -936,21 +921,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
if params.type == 'ws' then if params.type == 'ws' then
result.ws_host = params.host result.ws_host = params.host
result.ws_path = params.path result.ws_path = params.path
if result.type == "sing-box" and params.path then
local ws_path_dat = split(params.path, "%?")
local ws_path = ws_path_dat[1]
local ws_path_params = {}
for _, v in pairs(split(ws_path_dat[2], '&')) do
local t = split(v, '=')
ws_path_params[t[1]] = t[2]
end
if ws_path_params.ed and tonumber(ws_path_params.ed) then
result.ws_path = ws_path
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_path_params.ed)
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
end
end
end end
if params.type == "http" then if params.type == "http" then
if result.type == "sing-box" then if result.type == "sing-box" then
@@ -1152,21 +1122,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
if params.type == 'ws' then if params.type == 'ws' then
result.ws_host = params.host result.ws_host = params.host
result.ws_path = params.path result.ws_path = params.path
if result.type == "sing-box" and params.path then
local ws_path_dat = split(params.path, "%?")
local ws_path = ws_path_dat[1]
local ws_path_params = {}
for _, v in pairs(split(ws_path_dat[2], '&')) do
local t = split(v, '=')
ws_path_params[t[1]] = t[2]
end
if ws_path_params.ed and tonumber(ws_path_params.ed) then
result.ws_path = ws_path
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_path_params.ed)
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
end
end
end end
if params.type == "http" then if params.type == "http" then
if result.type == "sing-box" then if result.type == "sing-box" then
@@ -1281,21 +1236,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
if params.type == 'ws' then if params.type == 'ws' then
result.ws_host = params.host result.ws_host = params.host
result.ws_path = params.path result.ws_path = params.path
if result.type == "sing-box" and params.path then
local ws_path_dat = split(params.path, "%?")
local ws_path = ws_path_dat[1]
local ws_path_params = {}
for _, v in pairs(split(ws_path_dat[2], '&')) do
local t = split(v, '=')
ws_path_params[t[1]] = t[2]
end
if ws_path_params.ed and tonumber(ws_path_params.ed) then
result.ws_path = ws_path
result.ws_enableEarlyData = "1"
result.ws_maxEarlyData = tonumber(ws_path_params.ed)
result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol"
end
end
end end
if params.type == "http" then if params.type == "http" then
if result.type == "sing-box" then if result.type == "sing-box" then
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+22
View File
@@ -0,0 +1,22 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
#
# LuCI application for v2rayA.
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for v2rayA
LUCI_DEPENDS:=+luci-base +v2raya
LUCI_PKGARCH:=all
LUCI_MAINTAINER:=Amr Fateem <amr.fateem@yahoo.com>
PKG_VERSION:=2.5.13
PKG_RELEASE:=1
PKG_LICENSE:=Apache-2.0
CONFLICTS:=luci-app-v2raya-nextgen
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require form';
'require uci';
'require ui';
'require view';
return view.extend({
load: function () {
return uci.load('v2raya');
},
render: function () {
var m, s, o;
m = new form.Map('v2raya', _('v2rayA'),
_('Settings for the v2rayA service. Save & Apply restarts the service so the new values take effect. v2rayA 2.5.x uses only its bundled v2raya_core; Xray-core and sing-box are not supported backends.'));
s = m.section(form.NamedSection, 'config', 'v2raya');
s.tab('general', _('General'));
s.tab('proxy', _('Transparent proxy'),
_('The transparent proxy mode, routing rules and nodes are configured in the v2rayA web interface. These options control core start-up and hooks.'));
s.tab('logging', _('Logging'));
s.tab('advanced', _('Advanced'));
o = s.taboption('general', form.Flag, 'enabled', _('Enable service'));
o.default = '0';
o.rmempty = false;
o.description = _('Start v2rayA at boot. You can also start or stop it from the overview page.');
o = s.taboption('general', form.Value, 'address', _('Listening address'));
o.placeholder = '0.0.0.0:2017';
o.rmempty = false;
o.validate = function (section_id, value) {
if (!value)
return _('An address is required.');
if (!/^(\[[0-9a-fA-F:]+\]|[A-Za-z0-9._-]+):[0-9]{1,5}$/.test(value))
return _('Enter an address and port, for example 0.0.0.0:2017 or [::]:2017.');
return true;
};
o.description = _('The web interface listens on this address. Use 127.0.0.1:2017 to keep it off the LAN.');
o = s.taboption('general', form.Value, 'baseurl', _('Base URL'));
o.placeholder = '/';
o.description = _('URL prefix when v2rayA runs behind a reverse proxy.');
o.validate = function (section_id, value) {
if (value && value.charAt(0) !== '/')
return _('The base URL must start with /.');
return true;
};
o = s.taboption('general', form.ListValue, 'ipv6_support', _('IPv6 support'));
o.value('auto', _('Auto'));
o.value('on', _('On'));
o.value('off', _('Off'));
o.default = 'auto';
o.description = _('Make sure IPv6 connectivity works before forcing it on.');
o = s.taboption('general', form.ListValue, 'nftables_support', _('nftables support'));
o.value('auto', _('Auto'));
o.value('on', _('On'));
o.value('off', _('Off'));
o.default = 'auto';
o.description = _('nftables is the default firewall backend on OpenWrt 22.03 and later.');
o = s.taboption('proxy', form.Value, 'core_startup_timeout', _('Core startup timeout'));
o.datatype = 'uinteger';
o.placeholder = '15';
o.default = '15';
o.description = _('Seconds to wait for v2raya_core to start. Raise this on slow MIPS routers.');
o = s.taboption('proxy', form.Flag, 'redirect_respect_bound_device', _('Respect bound devices in redirect mode'));
o.default = '0';
o.description = _('Let sockets bound to a device bypass the proxy. Requires Linux 5.14 or later and cgroup v2.');
o = s.taboption('proxy', form.Value, 'transparent_hook', _('Transparent proxy hook'));
o.placeholder = '/usr/bin/example-hook';
o.description = _('Executable called around transparent proxy setup and teardown.');
o = s.taboption('proxy', form.Value, 'core_hook', _('Core life-cycle hook'));
o.placeholder = '/usr/bin/example-hook';
o.description = _('Executable called around v2raya_core start and stop.');
o = s.taboption('proxy', form.Value, 'plugin_manager', _('Plugin manager'));
o.placeholder = '/usr/bin/example-plugin-manager';
o.description = _('Executable used to parse protocols that v2rayA does not know natively.');
o = s.taboption('logging', form.ListValue, 'log_level', _('Log level'));
o.value('trace', _('Trace'));
o.value('debug', _('Debug'));
o.value('info', _('Info'));
o.value('warn', _('Warning'));
o.value('error', _('Error'));
o.default = 'warn';
o.description = _('Warning keeps flash writes down. Use Info while debugging.');
o = s.taboption('logging', form.Value, 'log_max_days', _('Keep logs for'));
o.datatype = 'uinteger';
o.placeholder = '1';
o.default = '1';
o.description = _('Number of days to keep rotated log files.');
o = s.taboption('logging', form.Flag, 'log_disable_color', _('Disable colours in the log'));
o.default = '1';
o.description = _('The log file is plain text, so colours are not useful there.');
o = s.taboption('logging', form.Flag, 'log_disable_timestamp', _('Disable timestamps in the log'));
o.default = '0';
o.description = _('Only relevant when v2rayA writes to syslog or journald.');
o = s.taboption('advanced', form.Value, 'v2ray_assetsdir', _('Rule data directory'));
o.placeholder = '/usr/share/v2raya';
o.default = '/usr/share/v2raya';
o.description = _('Directory containing geoip.dat and geosite.dat. Leave empty for auto-detection.');
o = s.taboption('advanced', form.Value, 'v2ray_bin', _('Core binary (v2raya_core only)'));
o.placeholder = _('Auto-detect');
o.description = _('Path to v2raya_core. Leave empty to use the binary next to v2raya or in PATH. Only the matching v2raya_core is supported; Xray-core and sing-box use incompatible configuration formats.');
o = s.taboption('advanced', form.Value, 'v2ray_confdir', _('Extra configuration directory'));
o.placeholder = _('None');
o.description = _('Additional v2ray configuration files merged into the generated configuration.');
o = s.taboption('advanced', form.Value, 'gomemlimit', _('Go memory limit'));
o.placeholder = '64MiB';
o.default = '64MiB';
o.description = _('Soft memory limit for both v2raya and v2raya_core (GOMEMLIMIT). Use 0 to disable, or 96MiB on a larger router.');
o = s.taboption('advanced', form.Value, 'gomaxprocs', _('Go scheduler threads'));
o.datatype = 'uinteger';
o.placeholder = '2';
o.default = '2';
o.description = _('GOMAXPROCS. 0 uses every CPU core; 2 is enough for most routers.');
o = s.taboption('advanced', form.Value, 'gogc', _('Go GC percent'));
o.datatype = 'uinteger';
o.placeholder = '0';
o.default = '0';
o.description = _('GOGC. 0 keeps the Go default; lower values use less memory and more CPU.');
o = s.taboption('advanced', form.Value, 'nofile', _('Open file limit'));
o.datatype = 'uinteger';
o.placeholder = '16384';
o.default = '16384';
o.description = _('Maximum open file descriptors. 16384 is plenty for a home router and bounds a connection flood.');
return m.render();
},
handleSaveApply: function (ev, mode) {
/*
* Applying the UCI delta emits a config.change event; the init
* script's procd reload trigger restarts the service with the new
* values. Restarting here as well would stop and start it twice.
*/
return this.handleSave(ev).then(function () {
return uci.apply();
}).then(function () {
ui.addNotification(null, E('p', {},
_('Settings applied. The service is restarting.')), 'info');
}).catch(function (err) {
ui.addNotification(null, E('p', {},
_('The configuration was not applied: %s').format(err)), 'danger');
});
}
});
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require dom';
'require fs';
'require poll';
'require ui';
'require view';
var LOG_PATH = '/var/log/v2raya/v2raya.log';
var MAX_LINES = 500;
return view.extend({
render: function () {
var logEl = E('pre', {
id: 'v2raya-log',
style: 'white-space:pre-wrap;word-break:break-all;max-height:65vh;overflow:auto;padding:8px;margin:0;background:rgba(0,0,0,0.04);border-radius:4px'
}, _('Loading…'));
var refresh = function () {
return L.resolveDefault(fs.read_direct(LOG_PATH, 'text'), null).then(function (text) {
if (text === null) {
dom.content(logEl, _('The log file does not exist yet. Start the service and try again.'));
return;
}
var lines = text.replace(/\s+$/, '').split('\n');
if (lines.length > MAX_LINES)
lines = lines.slice(-MAX_LINES);
var atBottom = (logEl.scrollTop + logEl.clientHeight + 20) >= logEl.scrollHeight;
dom.content(logEl, lines.join('\n') || _('The log is empty.'));
if (atBottom)
logEl.scrollTop = logEl.scrollHeight;
}).catch(function (err) {
dom.content(logEl, _('Unable to read the log: %s').format(err));
});
};
var refreshBtn = E('button', { class: 'btn cbi-button cbi-button-action' }, _('Refresh'));
var clearBtn = E('button', { class: 'btn cbi-button cbi-button-reset' }, _('Clear log'));
refreshBtn.addEventListener('click', function () { refresh(); });
clearBtn.addEventListener('click', function () {
if (!confirm(_('Clear the v2rayA log file?')))
return;
L.resolveDefault(fs.write(LOG_PATH, ''), null).then(function (res) {
if (res === null)
ui.addNotification(null, E('p', {}, _('Unable to clear the log file.')), 'danger');
else
ui.addNotification(null, E('p', {}, _('Log file cleared.')), 'info');
refresh();
});
});
refresh();
poll.add(refresh, 5);
return E('div', { class: 'cbi-map' }, [
E('h2', {}, _('v2rayA log')),
E('div', { class: 'cbi-map-descr' },
_('Showing the last %d lines of %s. Refreshes every %d seconds.')
.format(MAX_LINES, LOG_PATH, L.env.pollinterval || 5)),
E('div', { class: 'cbi-section' }, [
logEl,
E('div', { style: 'margin-top:8px' }, [ refreshBtn, ' ', clearBtn ])
])
]);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});
@@ -0,0 +1,280 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require view';
'require fs';
'require poll';
'require rpc';
'require uci';
'require ui';
var callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: [ 'name' ],
expect: { '': {} }
});
function serviceRunning() {
return L.resolveDefault(callServiceList('v2raya'), {}).then(function (res) {
var inst = res && res.v2raya && res.v2raya.instances && res.v2raya.instances.v2raya;
return !!(inst && inst.running);
});
}
function commandVersion(command, args) {
return L.resolveDefault(fs.exec(command, args || []), null).then(function (res) {
if (!res || res.code !== 0 || !res.stdout)
return null;
var line = res.stdout.trim().split('\n')[0].trim();
return line || null;
});
}
function detectBackends() {
return L.resolveDefault(fs.exec('/usr/libexec/v2raya-detect', []), null).then(function (res) {
var info = {};
if (!res || res.code !== 0)
return info;
(res.stdout || '').split('\n').forEach(function (line) {
var pos = line.indexOf('=');
if (pos > 0)
info[line.substring(0, pos)] = line.substring(pos + 1);
});
return info;
});
}
function backendRow(label, path, version, packageFlag, note) {
var field = [];
if (path) {
field.push(E('strong', {}, _('Detected')));
field.push(' — ', E('code', {}, path));
if (version)
field.push(E('div', { class: 'cbi-section-descr' }, version));
}
else {
field.push(E('span', { style: 'color:#777' }, _('Not installed')));
}
if (packageFlag === '1')
field.push(E('div', { class: 'cbi-section-descr' }, _('Package installed')));
if (note)
field.push(E('div', { class: 'cbi-section-descr' }, note));
return E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, label),
E('div', { class: 'cbi-value-field' }, field)
]);
}
function parseListenAddress(address) {
var match = String(address || '').match(/^\[?([^\]]*)\]?:(\d+)$/);
var host = match ? match[1] : '';
var port = match ? match[2] : '2017';
if (!host || host === '0.0.0.0' || host === '::')
host = window.location.hostname;
if (host.indexOf(':') !== -1)
host = '[' + host + ']';
return { host: host, port: port };
}
function renderStatus(running) {
var el = document.getElementById('v2raya-status');
if (!el)
return;
el.textContent = running ? _('Running') : _('Stopped');
el.style.color = running ? '#2e7d32' : '#c62828';
}
function setBusy(button, busy) {
if (!button)
return;
button.disabled = busy;
}
function runAction(action, button) {
setBusy(button, true);
return L.resolveDefault(fs.exec('/etc/init.d/v2raya', [ action ]), null).then(function (res) {
setBusy(button, false);
if (!res || res.code !== 0) {
ui.addNotification(null, E('p', {},
(res && (res.stderr || res.stdout)) || _('The service command failed.')), 'danger');
return;
}
ui.addNotification(null, E('p', {}, _('Service action "%s" completed.').format(action)), 'info');
return serviceRunning().then(renderStatus);
}).catch(function (err) {
setBusy(button, false);
ui.addNotification(null, E('p', {}, _('Unable to run the service command: %s').format(err)), 'danger');
});
}
function startService(button) {
uci.set('v2raya', 'config', 'enabled', '1');
return L.resolveDefault(uci.save(), null).then(function () {
return runAction('start', button);
}).then(function () {
/*
* Persist enabled=1 so the service also starts after a reboot.
* The resulting config.change event may cause one more restart;
* that is expected after an apply and the page polls the state.
*/
return L.resolveDefault(uci.apply(), null);
});
}
return view.extend({
load: function () {
return Promise.all([
uci.load('v2raya'),
commandVersion('/usr/bin/v2raya', [ '--version' ]),
commandVersion('/usr/bin/v2raya_core', [ 'version' ]),
detectBackends()
]);
},
render: function (data) {
var address = uci.get('v2raya', 'config', 'address') || '0.0.0.0:2017';
var baseurl = uci.get('v2raya', 'config', 'baseurl') || '/';
if (baseurl === '')
baseurl = '/';
var listen = parseListenAddress(address);
var weburl = 'http://' + listen.host + ':' + listen.port +
(baseurl === '/' ? '/' : '/' + baseurl.replace(/^\/+|\/+$/g, '') + '/');
var serviceVersion = data[1] || _('not detected');
var coreVersion = data[2] || _('not detected');
var backends = data[3] || {};
var detectAvailable = Object.keys(backends).length > 0;
var enabledServices = (backends.services_enabled || '').split(',').filter(function (name) {
return name && name !== 'v2raya';
});
var backendChildren = [
E('h3', {}, _('Detected cores and proxy services')),
E('p', { class: 'cbi-section-descr' },
_('v2rayA 2.5.x runs only its bundled v2raya_core. Existing Xray-core, v2ray and sing-box installations are detected here so they are not duplicated or replaced; they are never used as the v2rayA backend.'))
];
if (detectAvailable) {
backendChildren.push(
backendRow(_('v2raya_core (required)'), backends.core_path, backends.core_version, null,
_('This is the only core v2rayA 2.5.x supports.')),
backendRow(_('Xray-core'), backends.xray_path, backends.xray_version, backends.xray_package,
_('Left untouched. Do not point the core binary setting at this.')),
backendRow(_('v2ray-core'), backends.v2ray_path, backends.v2ray_version, backends.v2ray_package,
_('Left untouched. Do not point the core binary setting at this.')),
backendRow(_('sing-box'), backends.singbox_path, backends.singbox_version, backends.singbox_package,
_('Left untouched. sing-box uses a different configuration format and cannot run v2rayA configs.')),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Rule data')),
E('div', { class: 'cbi-value-field' },
(backends.geoip_present === '1' && backends.geosite_present === '1')
? _('geoip.dat and geosite.dat are installed')
: _('Missing. v2rayA downloads the data on first start and needs internet access; install v2raya-geoip and v2raya-geosite for offline use.'))
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Other proxy services')),
E('div', { class: 'cbi-value-field' },
backends.services_present ? backends.services_present.split(',').join(', ') : _('None detected'))
])
);
}
else {
backendChildren.push(E('p', { class: 'cbi-section-descr' },
_('Core and service detection is unavailable. The installed v2raya package does not provide the detection helper; version 2.5.x and later install it.')));
}
if (detectAvailable && !backends.core_path)
backendChildren.push(E('div', { class: 'alert-message warning' },
_('v2raya_core is missing. Install the v2raya-core package.')));
if (detectAvailable && backends.old_v2raya_package === '1')
backendChildren.push(E('div', { class: 'alert-message warning' },
_('An older v2raya package is still installed. Remove it to avoid duplicate services.')));
if (detectAvailable && enabledServices.length)
backendChildren.push(E('div', { class: 'alert-message warning' },
_('These proxy services are enabled at boot: %s. Run only one transparent-proxy stack, otherwise duplicate firewall rules will conflict.').format(enabledServices.join(', '))));
var status = E('span', { id: 'v2raya-status', style: 'font-weight:bold' }, _('Checking…'));
var startBtn = E('button', { class: 'btn cbi-button cbi-button-apply' }, _('Start'));
var stopBtn = E('button', { class: 'btn cbi-button cbi-button-reset' }, _('Stop'));
var restartBtn = E('button', { class: 'btn cbi-button cbi-button-action' }, _('Restart'));
startBtn.addEventListener('click', function () { startService(startBtn); });
stopBtn.addEventListener('click', function () { runAction('stop', stopBtn); });
restartBtn.addEventListener('click', function () { runAction('restart', restartBtn); });
var refresh = function () {
return serviceRunning().then(renderStatus);
};
refresh();
poll.add(refresh, 5);
return E('div', { class: 'cbi-map' }, [
E('h2', {}, _('v2rayA')),
E('div', { class: 'cbi-map-descr' }, [
_('A web client for its own Xray-based core, with transparent proxy support.'),
' ',
E('a', { href: weburl, target: '_blank', rel: 'noreferrer noopener' }, weburl)
]),
E('div', { class: 'cbi-section' }, [
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Service')),
E('div', { class: 'cbi-value-field' }, status)
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('v2rayA version')),
E('div', { class: 'cbi-value-field' }, serviceVersion)
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('v2raya_core version')),
E('div', { class: 'cbi-value-field' }, coreVersion)
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Listening address')),
E('div', { class: 'cbi-value-field' }, address)
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Web interface')),
E('div', { class: 'cbi-value-field' },
E('a', { href: weburl, target: '_blank', rel: 'noreferrer noopener' }, _('Open v2rayA')))
])
]),
E('div', { class: 'cbi-section' }, backendChildren),
E('div', { class: 'cbi-section' }, [
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Actions')),
E('div', { class: 'cbi-value-field' }, [ startBtn, ' ', stopBtn, ' ', restartBtn ])
]),
E('p', { class: 'cbi-section-descr' },
_('These buttons control the background service. Connecting the proxy is done in the v2rayA web UI with the Connect button.'))
]),
E('div', { class: 'cbi-section' }, [
E('p', { class: 'cbi-section-descr' },
_('This independent, community-run packaging project is not affiliated with the upstream v2rayA authors. It is AI-assisted and human-maintained.'))
])
]);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});
+163
View File
@@ -0,0 +1,163 @@
msgid ""
msgstr ""
"Project-Id-Version: luci-app-v2raya\n"
"Report-Msgid-Bugs-To: https://github.com/AmrFateem/luci-V2raya-NextGen/issues\n"
"POT-Creation-Date: 2026-11-07 00:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "v2rayA"
msgstr ""
msgid "Overview"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Log"
msgstr ""
msgid "Running"
msgstr ""
msgid "Stopped"
msgstr ""
msgid "Checking…"
msgstr ""
msgid "Start"
msgstr ""
msgid "Stop"
msgstr ""
msgid "Restart"
msgstr ""
msgid "Service"
msgstr ""
msgid "v2rayA version"
msgstr ""
msgid "v2raya_core version"
msgstr ""
msgid "Listening address"
msgstr ""
msgid "Web interface"
msgstr ""
msgid "Open v2rayA"
msgstr ""
msgid "Actions"
msgstr ""
msgid "Enable service"
msgstr ""
msgid "General"
msgstr ""
msgid "Transparent proxy"
msgstr ""
msgid "Logging"
msgstr ""
msgid "Advanced"
msgstr ""
msgid "IPv6 support"
msgstr ""
msgid "nftables support"
msgstr ""
msgid "Auto"
msgstr ""
msgid "On"
msgstr ""
msgid "Off"
msgstr ""
msgid "Log level"
msgstr ""
msgid "Trace"
msgstr ""
msgid "Debug"
msgstr ""
msgid "Info"
msgstr ""
msgid "Warning"
msgstr ""
msgid "Error"
msgstr ""
msgid "Keep logs for"
msgstr ""
msgid "Clear log"
msgstr ""
msgid "Refresh"
msgstr ""
msgid "The log is empty."
msgstr ""
msgid "The log file does not exist yet. Start the service and try again."
msgstr ""
msgid "v2rayA log"
msgstr ""
msgid "Settings for the v2rayA service. Save & Apply restarts the service so the new values take effect. v2rayA 2.5.x uses only its bundled v2raya_core; Xray-core and sing-box are not supported backends."
msgstr ""
msgid "This independent, community-run packaging project is not affiliated with the upstream v2rayA authors. It is AI-assisted and human-maintained."
msgstr ""
msgid "Warning keeps flash writes down. Use Info while debugging."
msgstr ""
msgid "Go memory limit"
msgstr ""
msgid "Soft memory limit for both v2raya and v2raya_core (GOMEMLIMIT). Use 0 to disable, or 96MiB on a larger router."
msgstr ""
msgid "Go scheduler threads"
msgstr ""
msgid "GOMAXPROCS. 0 uses every CPU core; 2 is enough for most routers."
msgstr ""
msgid "Go GC percent"
msgstr ""
msgid "GOGC. 0 keeps the Go default; lower values use less memory and more CPU."
msgstr ""
msgid "Open file limit"
msgstr ""
msgid "Maximum open file descriptors. 16384 is plenty for a home router and bounds a connection flood."
msgstr ""
@@ -0,0 +1,37 @@
{
"admin/services/v2raya": {
"title": "v2rayA",
"order": 60,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-v2raya" ],
"uci": { "v2raya": true }
}
},
"admin/services/v2raya/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "v2raya/overview"
}
},
"admin/services/v2raya/config": {
"title": "Settings",
"order": 20,
"action": {
"type": "view",
"path": "v2raya/config"
}
},
"admin/services/v2raya/log": {
"title": "Log",
"order": 30,
"action": {
"type": "view",
"path": "v2raya/log"
}
}
}
@@ -0,0 +1,25 @@
{
"luci-app-v2raya": {
"description": "Grant access to the v2rayA service and configuration",
"read": {
"file": {
"/etc/init.d/v2raya": [ "exec" ],
"/usr/bin/v2raya": [ "exec" ],
"/usr/bin/v2raya_core": [ "exec" ],
"/usr/libexec/v2raya-detect": [ "exec" ],
"/var/log/v2raya/v2raya.log": [ "read" ]
},
"ubus": {
"service": [ "list" ]
},
"uci": [ "v2raya" ]
},
"write": {
"file": {
"/etc/init.d/v2raya": [ "exec" ],
"/var/log/v2raya/v2raya.log": [ "write" ]
},
"uci": [ "v2raya" ]
}
}
}
+2 -2
View File
@@ -6,10 +6,10 @@ include $(TOPDIR)/rules.mk
PKG_UPSTREAM_NAME:=natmap PKG_UPSTREAM_NAME:=natmap
PKG_NAME:=natmapt PKG_NAME:=natmapt
PKG_UPSTREAM_VERSION:=20260214 PKG_UPSTREAM_VERSION:=20261109
PKG_UPSTREAM_GITHASH:= PKG_UPSTREAM_GITHASH:=
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)$(if $(PKG_UPSTREAM_GITHASH),~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH))) PKG_VERSION:=$(PKG_UPSTREAM_VERSION)$(if $(PKG_UPSTREAM_GITHASH),~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH)))
PKG_RELEASE:=19 PKG_RELEASE:=20
SCRIPTS_VERSION:=0.2026.01.24 SCRIPTS_VERSION:=0.2026.01.24
PKG_SOURCE_SUBDIR:=$(PKG_UPSTREAM_NAME)-$(PKG_UPSTREAM_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_UPSTREAM_NAME)-$(PKG_UPSTREAM_VERSION)
+74
View File
@@ -0,0 +1,74 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
#
# v2raya_core for OpenWrt. This is the merged xray-core based binary from
# the v2rayA 2.5.x tree. It is built from the same source tarball as the
# service and must always carry the same version.
include $(TOPDIR)/rules.mk
PKG_NAME:=v2raya-core
PKG_VERSION:=2.5.14
PKG_RELEASE:=1
PKG_SOURCE:=v2rayA-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/v2rayA/v2rayA/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/v2rayA-core-$(PKG_VERSION)/core
PKG_LICENSE:=MPL-2.0
PKG_LICENSE_FILES:=../LICENSE
PKG_MAINTAINER:=Amr Fateem <amr.fateem@yahoo.com>
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
# Keep this in sync with v2raya/Makefile.
V2RAYA_GO_TOOLCHAIN?=go1.27.3+auto
GO_PKG:=github.com/v2rayA/v2raya-core
GO_PKG_BUILD_PKG:=github.com/v2rayA/v2raya-core/main
GO_PKG_LDFLAGS_X:=main.Version=$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
# The core is pure Go. CGO stays disabled on every target, including mips:
# this is the same configuration upstream uses for its release binaries and
# it keeps the binary self-contained.
GO_PKG_TARGET_VARS:=$(filter-out CGO_ENABLED=%,$(GO_PKG_TARGET_VARS)) CGO_ENABLED=0
GO_PKG_DEFAULT_LDFLAGS:=$(filter-out -linkmode external,$(GO_PKG_DEFAULT_LDFLAGS))
GO_PKG_BUILD_VARS:=$(filter-out GOTOOLCHAIN=%,$(GO_PKG_BUILD_VARS)) GOTOOLCHAIN=$(V2RAYA_GO_TOOLCHAIN)
TAR_OPTIONS+= --strip-components 1
TAR_CMD=$(HOST_TAR) -C $(1)/.. $(TAR_OPTIONS)
define Package/v2raya-core
TITLE:=v2raya_core - the merged xray-core used by v2rayA
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
CONFLICTS:=v2raya-core-nextgen
URL:=https://github.com/v2rayA/v2rayA
endef
define Package/v2raya-core/description
v2raya_core combines xray-core with the MultiObservatory support and the
extra protocols required by v2rayA 2.5.x. The service and the core must
always be installed at the same version.
endef
define Package/v2raya-core/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/main $(1)/usr/bin/v2raya_core
endef
$(eval $(call GoBinPackage,v2raya-core))
$(eval $(call BuildPackage,v2raya-core))
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
case "$1" in
v2raya-core)
/usr/bin/v2raya_core version | grep -F "$2"
;;
*)
exit 1
;;
esac
+65
View File
@@ -0,0 +1,65 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
#
# GeoIP database used by v2rayA. The package is architecture independent and
# is updated independently of the v2rayA release by scripts/update-version.sh.
include $(TOPDIR)/rules.mk
PKG_NAME:=v2raya-geoip
PKG_VERSION:=202611050539
PKG_RELEASE:=1
PKG_LICENSE:=CC-BY-SA-4.0
PKG_MAINTAINER:=Amr Fateem <amr.fateem@yahoo.com>
include $(INCLUDE_DIR)/package.mk
GEOIP_FILE:=geoip.dat.$(PKG_VERSION)
GEOIP_HASH:=eedb24b3b9fcba27e06b92c0428dab90d9496083091ae4cf39e11387fd99d0e2
define Download/geoip
URL:=https://github.com/v2fly/geoip/releases/download/$(PKG_VERSION)/
URL_FILE:=geoip.dat
FILE:=$(GEOIP_FILE)
HASH:=skip
endef
define Package/v2raya-geoip
TITLE:=GeoIP database for v2rayA
SECTION:=net
CATEGORY:=Network
PKGARCH:=all
URL:=https://github.com/v2fly/geoip
endef
define Package/v2raya-geoip/description
GeoIP database (geoip.dat) used by v2rayA and v2raya_core for routing.
endef
define Package/v2raya-geoip/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
if [ -x /etc/init.d/v2raya ] && /etc/init.d/v2raya enabled; then
/etc/init.d/v2raya restart
fi
}
endef
define Build/Prepare
$(call Build/Prepare/Default)
$(call Download,geoip)
endef
define Build/Compile
endef
define Package/v2raya-geoip/install
$(INSTALL_DIR) $(1)/usr/share/v2raya
$(INSTALL_DATA) $(DL_DIR)/$(GEOIP_FILE) $(1)/usr/share/v2raya/geoip.dat
endef
$(eval $(call Download,geoip))
$(eval $(call BuildPackage,v2raya-geoip))
+66
View File
@@ -0,0 +1,66 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
#
# GeoSite database used by v2rayA. The package is architecture independent
# and is updated independently of the v2rayA release by
# scripts/update-version.sh.
include $(TOPDIR)/rules.mk
PKG_NAME:=v2raya-geosite
PKG_VERSION:=20261107053621
PKG_RELEASE:=1
PKG_LICENSE:=MIT
PKG_MAINTAINER:=Amr Fateem <amr.fateem@yahoo.com>
include $(INCLUDE_DIR)/package.mk
GEOSITE_FILE:=dlc.dat.$(PKG_VERSION)
GEOSITE_HASH:=f110a7b96b3e3ed9c19cf18ae0ac5f8c46de54fa778863609c0a3b8f1738b58c
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(PKG_VERSION)/
URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE)
HASH:=skip
endef
define Package/v2raya-geosite
TITLE:=GeoSite database for v2rayA
SECTION:=net
CATEGORY:=Network
PKGARCH:=all
URL:=https://github.com/v2fly/domain-list-community
endef
define Package/v2raya-geosite/description
GeoSite database (geosite.dat) used by v2rayA and v2raya_core for routing.
endef
define Package/v2raya-geosite/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
if [ -x /etc/init.d/v2raya ] && /etc/init.d/v2raya enabled; then
/etc/init.d/v2raya restart
fi
}
endef
define Build/Prepare
$(call Build/Prepare/Default)
$(call Download,geosite)
endef
define Build/Compile
endef
define Package/v2raya-geosite/install
$(INSTALL_DIR) $(1)/usr/share/v2raya
$(INSTALL_DATA) $(DL_DIR)/$(GEOSITE_FILE) $(1)/usr/share/v2raya/geosite.dat
endef
$(eval $(call Download,geosite))
$(eval $(call BuildPackage,v2raya-geosite))