🌴 Sync 2026-03-05 23:51:42

This commit is contained in:
github-actions[bot] committed 2026-03-05 23:51:42 +08:00
1 parent 97c049ed0a
commit 0acff03804
14253 files changed
+3893239

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
# Copyright (C) 2018-2020 Lienol <lawlienol@gmail.com>
#
# This is free software, licensed under the GNU General Public License v3.
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for V2ray Server
LUCI_DEPENDS:=+luci-compat +unzip
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-v2ray-server
PKG_VERSION:=13
PKG_RELEASE:=1
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,58 @@
-- Copyright 2018-2019 Lienol <lawlienol@gmail.com>
module("luci.controller.v2ray_server", package.seeall)
local http = require "luci.http"
local v2ray = require "luci.model.cbi.v2ray_server.api.v2ray"
function index()
if not nixio.fs.access("/etc/config/v2ray_server") then
return
end
entry({"admin", "services", "v2ray_server"}, cbi("v2ray_server/index"), _("V2ray Server"), 3).dependent = true
entry({"admin", "services", "v2ray_server", "config"}, cbi("v2ray_server/user")).leaf = true
entry({"admin", "services", "v2ray_server", "users_status"}, call("users_status")).leaf = true
entry({"admin", "services", "v2ray_server", "check"}, call("v2ray_check")).leaf = true
entry({"admin", "services", "v2ray_server", "update"}, call("v2ray_update")).leaf = true
entry({"admin", "services", "v2ray_server", "get_log"}, call("get_log")).leaf = true
entry({"admin", "services", "v2ray_server", "clear_log"}, call("clear_log")).leaf = true
end
local function http_write_json(content)
http.prepare_content("application/json")
http.write_json(content or {code = 1})
end
function get_log()
luci.http.write(luci.sys.exec("[ -f '/var/log/v2ray_server/app.log' ] && cat /var/log/v2ray_server/app.log"))
end
function clear_log()
luci.sys.call("echo '' > /var/log/v2ray_server/app.log")
end
function users_status()
local e = {}
e.index = luci.http.formvalue("index")
e.status = luci.sys.call("ps -w| grep -v grep | grep '/var/etc/v2ray_server/" .. luci.http.formvalue("id") .. "' >/dev/null") == 0
http_write_json(e)
end
function v2ray_check()
local json = v2ray.to_check("")
http_write_json(json)
end
function v2ray_update()
local json = nil
local task = http.formvalue("task")
if task == "extract" then
json =
v2ray.to_extract(http.formvalue("file"), http.formvalue("subfix"))
elseif task == "move" then
json = v2ray.to_move(http.formvalue("file"))
else
json = v2ray.to_download(http.formvalue("url"))
end
http_write_json(json)
end
@@ -0,0 +1,109 @@
#!/usr/bin/lua
local action = arg[1]
local sys = require 'luci.sys'
local jsonc = require "luci.jsonc"
local ucic = require"luci.model.uci".cursor()
local CONFIG = "v2ray_server"
local CONFIG_PATH = "/var/etc/" .. CONFIG
local LOG_PATH = "/var/log/" .. CONFIG
local LOG_APP_FILE = LOG_PATH .. "/app.log"
local BIN_PATH = "/var/bin/"
local BIN_PATH_FILE = BIN_PATH .. CONFIG
local function log(...)
local f, err = io.open(LOG_APP_FILE, "a")
if f and err == nil then
local str = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
f:write(str .. "\n")
f:close()
end
end
local function cmd(cmd)
sys.call(cmd)
end
local function gen_include()
cmd(string.format("echo '#!/bin/sh' > /var/etc/%s.include", CONFIG))
local function extract_rules(a)
local result = "*" .. a
result = result .. "\n" .. sys.exec('iptables-save -t ' .. a .. ' | grep "V2RAY-SERVER" | sed -e "s/^-A \\(INPUT\\)/-I \\1 1/"')
result = result .. "COMMIT"
return result
end
local f, err = io.open("/var/etc/" .. CONFIG .. ".include", "a")
if f and err == nil then
f:write('iptables-save -c | grep -v "V2RAY-SERVER" | iptables-restore -c' .. "\n")
f:write('iptables-restore -n <<-EOT' .. "\n")
f:write(extract_rules("filter") .. "\n")
f:write("EOT" .. "\n")
f:close()
end
end
local function start()
local enabled = tonumber(ucic:get(CONFIG, "@global[0]", "enable") or 0)
if enabled == nil or enabled == 0 then
return
end
cmd(string.format("mkdir -p %s %s", CONFIG_PATH, LOG_PATH))
cmd(string.format("touch %s", LOG_APP_FILE))
cmd("iptables -N V2RAY-SERVER")
cmd("iptables -I INPUT -j V2RAY-SERVER")
ucic:foreach(CONFIG, "user", function(user)
local id = user[".name"]
local enable = user.enable
if enable and tonumber(enable) == 1 then
local remarks = user.remarks
local port = tonumber(user.port)
if nixio.fs.access("/usr/bin/xray") and not nixio.fs.access(BIN_PATH_FILE) then
cmd(string.format("mkdir -p %s", BIN_PATH))
cmd(string.format("cp -a /usr/bin/xray %s", BIN_PATH_FILE))
end
local bin = BIN_PATH_FILE
local config = {}
local config_file = CONFIG_PATH .. "/" .. id .. ".json"
config = require("luci.model.cbi.v2ray_server.api.gen_config").gen_config(user)
bin = bin .. " -config " .. config_file
if next(config) then
local f, err = io.open(config_file, "w")
if f and err == nil then
f:write(jsonc.stringify(config, 1))
f:close()
end
log(string.format("%s %s 生成配置文件并运行 - %s", remarks, port, config_file))
end
if bin then
cmd(bin .. ">/dev/null 2>&1 &")
end
local bind_local = user.bind_local or 0
if bind_local and tonumber(bind_local) ~= 1 then
cmd(string.format('iptables -A V2RAY-SERVER -p tcp --dport %s -m comment --comment "%s" -j ACCEPT', port, remarks))
cmd(string.format('iptables -A V2RAY-SERVER -p udp --dport %s -m comment --comment "%s" -j ACCEPT', port, remarks))
end
end
end)
gen_include()
end
local function stop()
cmd(string.format("ps -w | grep -v 'grep' | grep '%s/' | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1 &", CONFIG_PATH))
cmd("iptables -D INPUT -j V2RAY-SERVER 2>/dev/null")
cmd("iptables -F V2RAY-SERVER 2>/dev/null")
cmd("iptables -X V2RAY-SERVER 2>/dev/null")
cmd(string.format("rm -rf %s %s /var/etc/%s.include", CONFIG_PATH, LOG_APP_FILE, CONFIG))
end
if action then
if action == "start" then
start()
elseif action == "stop" then
stop()
end
end
@@ -0,0 +1,184 @@
module("luci.model.cbi.v2ray_server.api.gen_config", package.seeall)
function gen_config(user)
local settings = nil
local routing = nil
local outbounds = {
{protocol = "freedom", tag = "direct"}, {protocol = "blackhole", tag = "blocked"}
}
if user.protocol == "vmess" or user.protocol == "vless" then
if user.uuid then
local clients = {}
for i = 1, #user.uuid do
clients[i] = {
id = user.uuid[i],
flow = (user.xtls and user.xtls == "1") and user.flow or nil,
level = tonumber(user.level),
alterId = tonumber(user.alter_id)
}
end
settings = {
clients = clients,
decryption = user.decryption or "none"
}
end
elseif user.protocol == "socks" then
settings = {
auth = (user.auth and user.auth == "1") and "password" or "noauth",
accounts = (user.auth and user.auth == "1") and {
{
user = user.username,
pass = user.password
}
}
}
elseif user.protocol == "http" then
settings = {
allowTransparent = false,
accounts = (user.auth and user.auth == "1") and {
{
user = user.username,
pass = user.password
}
}
}
user.transport = "tcp"
user.tcp_guise = "none"
elseif user.protocol == "shadowsocks" then
settings = {
method = user.method,
password = user.password,
level = tonumber(user.level) or 1,
network = user.ss_network or "TCP,UDP"
}
elseif user.protocol == "trojan" then
if user.uuid then
local clients = {}
for i = 1, #user.uuid do
clients[i] = {
password = user.uuid[i],
level = tonumber(user.level)
}
end
settings = {
clients = clients
}
end
elseif user.protocol == "mtproto" then
settings = {
users = {
{
level = tonumber(user.level) or 1,
secret = (user.password == nil) and "" or user.password
}
}
}
end
routing = {
domainStrategy = "IPOnDemand",
rules = {
{
type = "field",
ip = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
outboundTag = (user.accept_lan == nil or user.accept_lan == "0") and "blocked" or "direct"
}
}
}
local config = {
log = {
-- error = "/var/etc/v2ray_server/log/" .. user[".name"] .. ".log",
loglevel = "warning"
},
-- 传入连接
inbounds = {
{
listen = (user.bind_local == "1") and "127.0.0.1" or nil,
port = tonumber(user.port),
protocol = user.protocol,
settings = settings,
streamSettings = {
network = user.transport,
security = "none",
xtlsSettings = (user.tls and user.tls == "1" and user.xtls and user.xtls == "1") and {
--alpn = {"http/1.1"},
disableSystemRoot = false,
certificates = {
{
certificateFile = user.tls_certificateFile,
keyFile = user.tls_keyFile
}
}
} or nil,
tlsSettings = (user.tls and user.tls == "1") and {
disableSystemRoot = false,
certificates = {
{
certificateFile = user.tls_certificateFile,
keyFile = user.tls_keyFile
}
}
} or nil,
tcpSettings = (user.transport == "tcp") and {
header = {
type = user.tcp_guise,
request = (user.tcp_guise == "http") and {
path = user.tcp_guise_http_path or {"/"},
headers = {
Host = user.tcp_guise_http_host or {}
}
} or nil
}
} or nil,
kcpSettings = (user.transport == "mkcp") and {
mtu = tonumber(user.mkcp_mtu),
tti = tonumber(user.mkcp_tti),
uplinkCapacity = tonumber(user.mkcp_uplinkCapacity),
downlinkCapacity = tonumber(user.mkcp_downlinkCapacity),
congestion = (user.mkcp_congestion == "1") and true or false,
readBufferSize = tonumber(user.mkcp_readBufferSize),
writeBufferSize = tonumber(user.mkcp_writeBufferSize),
seed = (user.mkcp_seed and user.mkcp_seed ~= "") and user.mkcp_seed or nil,
header = {type = user.mkcp_guise}
} or nil,
wsSettings = (user.transport == "ws") and {
acceptProxyProtocol = false,
headers = (user.ws_host) and {Host = user.ws_host} or nil,
path = user.ws_path
} or nil,
httpSettings = (user.transport == "h2") and {
path = user.h2_path, host = user.h2_host
} or nil,
dsSettings = (user.transport == "ds") and {
path = user.ds_path
} or nil,
quicSettings = (user.transport == "quic") and {
security = user.quic_security,
key = user.quic_key,
header = {type = user.quic_guise}
} or nil
}
}
},
-- 传出连接
outbounds = outbounds,
routing = routing
}
if user.tls and user.tls == "1" then
config.inbounds[1].streamSettings.security = "tls"
if user.xtls and user.xtls == "1" then
config.inbounds[1].streamSettings.security = "xtls"
config.inbounds[1].streamSettings.tlsSettings = nil
end
end
if user.transport == "mkcp" or user.transport == "quic" then
config.inbounds[1].streamSettings.security = "none"
config.inbounds[1].streamSettings.tlsSettings = nil
end
return config
end
@@ -0,0 +1,304 @@
module("luci.model.cbi.v2ray_server.api.v2ray", package.seeall)
local fs = require "nixio.fs"
local sys = require "luci.sys"
local uci = require"luci.model.uci".cursor()
local util = require "luci.util"
local i18n = require "luci.i18n"
local ipkg = require "luci.model.ipkg"
local appname = "v2ray_server"
local v2ray_api ="https://api.github.com/repos/XTLS/Xray-core/releases/latest"
local wget = "/usr/bin/wget"
local wget_args = {"--no-check-certificate", "--quiet", "--timeout=100", "--tries=3"}
local command_timeout = 300
local LEDE_BOARD = nil
local DISTRIB_TARGET = nil
local is_armv7 = false
local function _unpack(t, i)
i = i or 1
if t[i] ~= nil then return t[i], _unpack(t, i + 1) end
end
local function exec(cmd, args, writer, timeout)
local os = require "os"
local nixio = require "nixio"
local fdi, fdo = nixio.pipe()
local pid = nixio.fork()
if pid > 0 then
fdo:close()
if writer or timeout then
local starttime = os.time()
while true do
if timeout and os.difftime(os.time(), starttime) >= timeout then
nixio.kill(pid, nixio.const.SIGTERM)
return 1
end
if writer then
local buffer = fdi:read(2048)
if buffer and #buffer > 0 then
writer(buffer)
end
end
local wpid, stat, code = nixio.waitpid(pid, "nohang")
if wpid and stat == "exited" then return code end
if not writer and timeout then nixio.nanosleep(1) end
end
else
local wpid, stat, code = nixio.waitpid(pid)
return wpid and stat == "exited" and code
end
elseif pid == 0 then
nixio.dup(fdo, nixio.stdout)
fdi:close()
fdo:close()
nixio.exece(cmd, args, nil)
nixio.stdout:close()
os.exit(1)
end
end
local function compare_versions(ver1, comp, ver2)
local table = table
local av1 = util.split(ver1, "[%.%-]", nil, true)
local av2 = util.split(ver2, "[%.%-]", nil, true)
local max = table.getn(av1)
local n2 = table.getn(av2)
if (max < n2) then max = n2 end
for i = 1, max, 1 do
local s1 = av1[i] or ""
local s2 = av2[i] or ""
if comp == "~=" and (s1 ~= s2) then return true end
if (comp == "<" or comp == "<=") and (s1 < s2) then return true end
if (comp == ">" or comp == ">=") and (s1 > s2) then return true end
if (s1 ~= s2) then return false end
end
return not (comp == "<" or comp == ">")
end
local function auto_get_arch()
local arch = nixio.uname().machine or ""
if fs.access("/usr/lib/os-release") then
LEDE_BOARD = sys.exec("echo -n `grep 'LEDE_BOARD' /usr/lib/os-release | awk -F '[\\042\\047]' '{print $2}'`")
end
if fs.access("/etc/openwrt_release") then
DISTRIB_TARGET = sys.exec("echo -n `grep 'DISTRIB_TARGET' /etc/openwrt_release | awk -F '[\\042\\047]' '{print $2}'`")
end
if arch == "mips" then
if LEDE_BOARD and LEDE_BOARD ~= "" then
if string.match(LEDE_BOARD, "ramips") == "ramips" then
arch = "ramips"
else
arch = sys.exec("echo '" .. LEDE_BOARD .. "' | grep -oE 'ramips|ar71xx'")
end
elseif DISTRIB_TARGET and DISTRIB_TARGET ~= "" then
if string.match(DISTRIB_TARGET, "ramips") == "ramips" then
arch = "ramips"
else
arch = sys.exec("echo '" .. DISTRIB_TARGET .. "' | grep -oE 'ramips|ar71xx'")
end
end
end
return util.trim(arch)
end
local function get_file_info(arch)
local file_tree = ""
local sub_version = ""
if arch == "x86_64" then
file_tree = "64"
elseif arch == "aarch64" then
file_tree = "arm64"
elseif arch == "ramips" then
file_tree = "mipsle"
elseif arch == "ar71xx" then
file_tree = "mips"
elseif arch:match("^i[%d]86$") then
file_tree = "32"
elseif arch:match("^armv[5-8]") then
file_tree = "arm"
sub_version = arch:match("[5-8]")
if LEDE_BOARD and string.match(LEDE_BOARD, "bcm53xx") == "bcm53xx" then
sub_version = "5"
elseif DISTRIB_TARGET and string.match(DISTRIB_TARGET, "bcm53xx") ==
"bcm53xx" then
sub_version = "5"
end
sub_version = "5"
if sub_version == "7" then is_armv7 = true end
end
return file_tree, sub_version
end
local function get_api_json(url)
local jsonc = require "luci.jsonc"
local output = {}
-- exec(wget, { "-O-", url, _unpack(wget_args) },
-- function(chunk) output[#output + 1] = chunk end)
-- local json_content = util.trim(table.concat(output))
local json_content = luci.sys.exec(wget .. " --no-check-certificate --timeout=10 -t 1 -O- " .. url)
if json_content == "" then return {} end
return jsonc.parse(json_content) or {}
end
function get_v2ray_file_path() return "/usr/bin" end
function get_v2ray_version()
if get_v2ray_file_path() and get_v2ray_file_path() ~= "" then
if fs.access(get_v2ray_file_path() .. "/xray") then
return luci.sys.exec("echo -n `" .. get_v2ray_file_path() .. "/xray -version | awk '{print $2}' | sed -n 1P" .. "`")
end
end
return ""
end
function to_check(arch)
if not arch or arch == "" then arch = auto_get_arch() end
local file_tree, sub_version = get_file_info(arch)
if file_tree == "" then
return {
code = 1,
error = i18n.translate("Can't determine ARCH, or ARCH not supported.")
}
end
local json = get_api_json(v2ray_api)
if json.tag_name == nil then
return {
code = 1,
error = i18n.translate("Get remote version info failed.")
}
end
local remote_version = json.tag_name:match("[^v]+")
local needs_update = compare_versions(get_v2ray_version(), "<",remote_version)
local html_url, download_url
if needs_update then
html_url = json.html_url
for _, v in ipairs(json.assets) do
if v.name and v.name:match("linux%-" .. file_tree) then
download_url = v.browser_download_url
break
end
end
end
if needs_update and not download_url then
return {
code = 1,
now_version = get_v2ray_version(),
version = remote_version,
html_url = html_url,
error = i18n.translate("New version found, but failed to get new version download url.")
}
end
return {
code = 0,
update = needs_update,
now_version = get_v2ray_version(),
version = remote_version,
url = {html = html_url, download = download_url}
}
end
function to_download(url)
if not url or url == "" then
return {code = 1, error = i18n.translate("Download url is required.")}
end
sys.call("/bin/rm -f /tmp/v2ray_download.*")
local tmp_file = util.trim(util.exec("mktemp -u -t v2ray_download.XXXXXX"))
local result = exec(wget, {"-O", tmp_file, url, _unpack(wget_args)}, nil, command_timeout) == 0
if not result then
exec("/bin/rm", {"-f", tmp_file})
return {
code = 1,
error = i18n.translatef("File download failed or timed out: %s", url)
}
end
return {code = 0, file = tmp_file}
end
function to_extract(file, subfix)
local isinstall_unzip = ipkg.installed("unzip")
if isinstall_unzip == nil then
ipkg.update()
ipkg.install("unzip")
end
if not file or file == "" or not fs.access(file) then
return {code = 1, error = i18n.translate("File path required.")}
end
sys.call("/bin/rm -rf /tmp/v2ray_extract.*")
local tmp_dir = util.trim(util.exec("mktemp -d -t v2ray_extract.XXXXXX"))
local output = {}
exec("/usr/bin/unzip", {"-o", file, "-d", tmp_dir},
function(chunk) output[#output + 1] = chunk end)
local files = util.split(table.concat(output))
exec("/bin/rm", {"-f", file})
return {code = 0, file = tmp_dir}
end
function to_move(file)
if not file or file == "" then
sys.call("/bin/rm -rf /tmp/v2ray_extract.*")
return {code = 1, error = i18n.translate("Client file is required.")}
end
local client_file = get_v2ray_file_path()
sys.call("mkdir -p " .. client_file)
if not arch or arch == "" then arch = auto_get_arch() end
local file_tree, sub_version = get_file_info(arch)
local result = nil
result = exec("/bin/mv", {"-f", file .. "/xray", client_file}, nil, command_timeout) == 0
if not result or not fs.access(client_file) then
sys.call("/bin/rm -rf /tmp/v2ray_extract.*")
return {
code = 1,
error = i18n.translatef("Can't move new file to path: %s", client_file)
}
end
exec("/bin/chmod", {"-R", "755", client_file .. "/xray"})
sys.call("/bin/rm -rf /tmp/v2ray_extract.*")
return {code = 0}
end
@@ -0,0 +1,58 @@
local ds = require "luci.dispatcher"
m = Map("v2ray_server")
m.title = translate("V2ray Server")
t = m:section(TypedSection, "global", translate("Global Settings"))
t.anonymous = true
t.addremove = false
e = t:option(Flag, "enable", translate("Enable"))
e.rmempty = false
t:append(Template("v2ray_server/v2ray"))
t = m:section(TypedSection, "user", translate("Users Manager"))
t.anonymous = true
t.addremove = true
t.template = "cbi/tblsection"
t.extedit = ds.build_url("admin", "services", "v2ray_server", "config", "%s")
function t.create(t, e)
local uuid = luci.sys.exec("echo -n $(cat /proc/sys/kernel/random/uuid)") or ""
uuid = string.gsub(uuid, "-", "")
local e = TypedSection.create(t, uuid)
luci.http.redirect(ds.build_url("admin", "services", "v2ray_server", "config", uuid))
end
function t.remove(t, a)
t.map.proceed = true
t.map:del(a)
luci.http.redirect(ds.build_url("admin", "services", "v2ray_server"))
end
e = t:option(Flag, "enable", translate("Enable"))
e.width = "5%"
e.rmempty = false
e = t:option(DummyValue, "status", translate("Status"))
e.rawhtml = true
e.cfgvalue = function(t, n)
return string.format('<font class="users_status" hint="%s">%s</font>', n, translate("Collecting data..."))
end
e = t:option(DummyValue, "remarks", translate("Remarks"))
e = t:option(DummyValue, "port", translate("Port"))
e.width = "10%"
e = t:option(DummyValue, "protocol", translate("Protocol"))
e.cfgvalue = function(self, section)
local str = "未知"
local protocol = m:get(section, "protocol") or ""
if protocol ~= "" then str = (protocol:gsub("^%l", string.upper)) end
return str
end
m:append(Template("v2ray_server/log"))
m:append(Template("v2ray_server/users_list_status"))
return m
@@ -0,0 +1,315 @@
local d = require "luci.dispatcher"
local appname = "v2ray_server"
local v_ss_encrypt_method_list = {
"aes-128-cfb", "aes-256-cfb", "aes-128-gcm", "aes-256-gcm", "chacha20", "chacha20-ietf", "chacha20-poly1305", "chacha20-ietf-poly1305"
}
local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard"
}
m = Map(appname, translate("Server Config"))
m.redirect = d.build_url("admin", "services", appname)
s = m:section(NamedSection, arg[1], "user", "")
s.addremove = false
s.dynamic = false
enable = s:option(Flag, "enable", translate("Enable"))
enable.default = "1"
enable.rmempty = false
remarks = s:option(Value, "remarks", translate("Remarks"))
remarks.default = translate("Remarks")
remarks.rmempty = false
protocol = s:option(ListValue, "protocol", translate("Protocol"))
protocol:value("vmess", "Vmess")
protocol:value("vless", "VLESS")
protocol:value("http", "HTTP")
protocol:value("socks", "Socks")
protocol:value("shadowsocks", "Shadowsocks")
protocol:value("trojan", "Trojan")
protocol:value("mtproto", "MTProto")
port = s:option(Value, "port", translate("Port"))
port.datatype = "port"
port.rmempty = false
auth = s:option(Flag, "auth", translate("Auth"))
auth.validate = function(self, value, t)
if value and value == "1" then
local user_v = username:formvalue(t) or ""
local pass_v = password:formvalue(t) or ""
if user_v == "" or pass_v == "" then
return nil, translate("Username and Password must be used together!")
end
end
return value
end
auth:depends({ protocol = "socks" })
auth:depends({ protocol = "http" })
username = s:option(Value, "username", translate("Username"))
username:depends("auth", "1")
password = s:option(Value, "password", translate("Password"))
password.password = true
password:depends("auth", "1")
password:depends({ protocol = "shadowsocks" })
mtproto_password = s:option(Value, "mtproto_password", translate("Password"), translate("The MTProto protocol must be 32 characters and can only contain characters from 0 to 9 and a to f."))
mtproto_password:depends({ protocol = "mtproto" })
mtproto_password.default = arg[1]
function mtproto_password.cfgvalue(self, section)
return m:get(section, "password")
end
function mtproto_password.write(self, section, value)
m:set(section, "password", value)
end
decryption = s:option(Value, "decryption", translate("Encrypt Method"))
decryption.default = "none"
decryption:depends({ protocol = "vless" })
v_ss_encrypt_method = s:option(ListValue, "v_ss_encrypt_method", translate("Encrypt Method"))
for a, t in ipairs(v_ss_encrypt_method_list) do v_ss_encrypt_method:value(t) end
v_ss_encrypt_method:depends({ protocol = "shadowsocks" })
function v_ss_encrypt_method.cfgvalue(self, section)
return m:get(section, "method")
end
function v_ss_encrypt_method.write(self, section, value)
m:set(section, "method", value)
end
ss_network = s:option(ListValue, "ss_network", translate("Transport"))
ss_network.default = "tcp,udp"
ss_network:value("tcp", "TCP")
ss_network:value("udp", "UDP")
ss_network:value("tcp,udp", "TCP,UDP")
ss_network:depends({ protocol = "shadowsocks" })
uuid = s:option(DynamicList, "uuid", translate("ID") .. "/" .. translate("Password"))
for i = 1, 3 do
uuid:value(luci.sys.exec("echo -n $(cat /proc/sys/kernel/random/uuid)"))
end
uuid:depends({ protocol = "vmess" })
uuid:depends({ protocol = "vless" })
uuid:depends({ protocol = "trojan" })
alter_id = s:option(Value, "alter_id", translate("Alter ID"))
alter_id.default = 16
alter_id:depends({ protocol = "vmess" })
level = s:option(Value, "level", translate("User Level"))
level.default = 1
level:depends({ protocol = "vmess" })
level:depends({ protocol = "vless" })
level:depends({ protocol = "shadowsocks" })
level:depends({ protocol = "trojan" })
level:depends({ protocol = "mtproto" })
tls = s:option(Flag, "tls", translate("TLS"))
tls.default = 0
tls.validate = function(self, value, t)
if value then
if value == "1" then
local ca = tls_certificateFile:formvalue(t) or ""
local key = tls_keyFile:formvalue(t) or ""
if ca == "" or key == "" then
return nil, translate("Public key and Private key path can not be empty!")
end
end
return value
end
end
tls:depends({ protocol = "vmess" })
tls:depends({ protocol = "vless" })
tls:depends({ protocol = "socks" })
tls:depends({ protocol = "shadowsocks" })
xtls = s:option(Flag, "xtls", translate("XTLS"))
xtls.default = 0
xtls:depends({ protocol = "vless", tls = "1" })
flow = s:option(Value, "flow", translate("flow"))
flow.default = "xtls-rprx-direct"
flow:value("xtls-rprx-origin")
flow:value("xtls-rprx-origin-udp443")
flow:value("xtls-rprx-direct")
flow:value("xtls-rprx-direct-udp443")
flow:value("xtls-rprx-splice")
flow:value("xtls-rprx-splice-udp443")
flow:depends("xtls", "1")
-- [[ TLS部分 ]] --
tls_serverName = s:option(Value, "tls_serverName", translate("Domain"))
tls_serverName:depends("tls", "1")
tls_certificateFile = s:option(Value, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
tls_certificateFile.validate = function(self, value, t)
if value and value ~= "" then
if not nixio.fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
tls_certificateFile:depends("tls", "1")
tls_keyFile = s:option(Value, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
tls_keyFile.validate = function(self, value, t)
if value and value ~= "" then
if not nixio.fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
tls_keyFile:depends("tls", "1")
transport = s:option(ListValue, "transport", translate("Transport"))
transport:value("tcp", "TCP")
transport:value("mkcp", "mKCP")
transport:value("ws", "WebSocket")
transport:value("h2", "HTTP/2")
transport:value("ds", "DomainSocket")
transport:value("quic", "QUIC")
transport:depends({ protocol = "vmess" })
transport:depends({ protocol = "vless" })
transport:depends({ protocol = "socks" })
transport:depends({ protocol = "shadowsocks" })
transport:depends({ protocol = "trojan" })
-- [[ WebSocket部分 ]]--
ws_host = s:option(Value, "ws_host", translate("WebSocket Host"))
ws_host:depends("transport", "ws")
ws_host:depends("ss_transport", "ws")
ws_host:depends("trojan_transport", "h2+ws")
ws_host:depends("trojan_transport", "ws")
ws_path = s:option(Value, "ws_path", translate("WebSocket Path"))
ws_path:depends("transport", "ws")
ws_path:depends("ss_transport", "ws")
ws_path:depends("trojan_transport", "h2+ws")
ws_path:depends("trojan_transport", "ws")
-- [[ HTTP/2部分 ]]--
h2_host = s:option(Value, "h2_host", translate("HTTP/2 Host"))
h2_host:depends("transport", "h2")
h2_host:depends("ss_transport", "h2")
h2_host:depends("trojan_transport", "h2+ws")
h2_host:depends("trojan_transport", "h2")
h2_path = s:option(Value, "h2_path", translate("HTTP/2 Path"))
h2_path:depends("transport", "h2")
h2_path:depends("ss_transport", "h2")
h2_path:depends("trojan_transport", "h2+ws")
h2_path:depends("trojan_transport", "h2")
-- [[ TCP部分 ]]--
-- TCP伪装
tcp_guise = s:option(ListValue, "tcp_guise", translate("Camouflage Type"))
tcp_guise:value("none", "none")
tcp_guise:value("http", "http")
tcp_guise:depends("transport", "tcp")
-- HTTP域名
tcp_guise_http_host = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
tcp_guise_http_host:depends("tcp_guise", "http")
-- HTTP路径
tcp_guise_http_path = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
tcp_guise_http_path:depends("tcp_guise", "http")
-- [[ mKCP部分 ]]--
mkcp_guise = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('<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)'))
for a, t in ipairs(header_type_list) do mkcp_guise:value(t) end
mkcp_guise:depends("transport", "mkcp")
mkcp_mtu = s:option(Value, "mkcp_mtu", translate("KCP MTU"))
mkcp_mtu.default = "1350"
mkcp_mtu:depends("transport", "mkcp")
mkcp_tti = s:option(Value, "mkcp_tti", translate("KCP TTI"))
mkcp_tti.default = "20"
mkcp_tti:depends("transport", "mkcp")
mkcp_uplinkCapacity = s:option(Value, "mkcp_uplinkCapacity", translate("KCP uplinkCapacity"))
mkcp_uplinkCapacity.default = "5"
mkcp_uplinkCapacity:depends("transport", "mkcp")
mkcp_downlinkCapacity = s:option(Value, "mkcp_downlinkCapacity", translate("KCP downlinkCapacity"))
mkcp_downlinkCapacity.default = "20"
mkcp_downlinkCapacity:depends("transport", "mkcp")
mkcp_congestion = s:option(Flag, "mkcp_congestion", translate("KCP Congestion"))
mkcp_congestion:depends("transport", "mkcp")
mkcp_readBufferSize = s:option(Value, "mkcp_readBufferSize", translate("KCP readBufferSize"))
mkcp_readBufferSize.default = "1"
mkcp_readBufferSize:depends("transport", "mkcp")
mkcp_writeBufferSize = s:option(Value, "mkcp_writeBufferSize", translate("KCP writeBufferSize"))
mkcp_writeBufferSize.default = "1"
mkcp_writeBufferSize:depends("transport", "mkcp")
mkcp_seed = s:option(Value, "mkcp_seed", translate("KCP Seed"))
mkcp_seed:depends("transport", "mkcp")
-- [[ DomainSocket部分 ]]--
ds_path = s:option(Value, "ds_path", "Path", translate("A legal file path. This file must not exist before running V2Ray."))
ds_path:depends("transport", "ds")
-- [[ QUIC部分 ]]--
quic_security = s:option(ListValue, "quic_security", translate("Encrypt Method"))
quic_security:value("none")
quic_security:value("aes-128-gcm")
quic_security:value("chacha20-poly1305")
quic_security:depends("transport", "quic")
quic_key = s:option(Value, "quic_key", translate("Encrypt Method") .. translate("Key"))
quic_key:depends("transport", "quic")
quic_guise = s:option(ListValue, "quic_guise", translate("Camouflage Type"))
for a, t in ipairs(header_type_list) do quic_guise:value(t) end
quic_guise:depends("transport", "quic")
-- [[ VLESS Fallback部分 ]]--
--[[
fallback = s:option(Flag, "fallback", translate("Fallback"))
fallback:depends({ protocol = "vless", transport = "tcp", tls = "1" })
fallback_alpn = s:option(Value, "fallback_alpn", "Fallback alpn")
fallback_alpn:depends("fallback", "1")
fallback_path = s:option(Value, "fallback_path", "Fallback path")
fallback_path:depends("fallback", "1")
fallback_dest = s:option(Value, "fallback_dest", "Fallback dest")
fallback_dest:depends("fallback", "1")
fallback_xver = s:option(Value, "fallback_xver", "Fallback xver")
fallback_xver.default = 0
fallback_xver:depends("fallback", "1")
]]--
bind_local = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed locally,It is recommended to turn on when using reverse proxies."))
bind_local.default = "0"
accept_lan = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!"))
accept_lan.default = "0"
accept_lan.rmempty = false
return m
@@ -0,0 +1,31 @@
<script type="text/javascript">
//<![CDATA[
function clear_log(btn) {
XHR.get('<%=url([[admin]], [[services]], [[v2ray_server]], [[clear_log]])%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = "";
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
}
XHR.poll(3, '<%=url([[admin]], [[services]], [[v2ray_server]], [[get_log]])%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = x.responseText;
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<legend>
<%:Logs%>
</legend>
<input class="cbi-button cbi-input-remove" type="button" onclick="clear_log()" value="<%:Clear logs%>">
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="20" wrap="off" readonly="readonly"></textarea>
</fieldset>
@@ -0,0 +1,19 @@
<script type="text/javascript">
//<![CDATA[
var users_status = document.getElementsByClassName('users_status');
for(var i = 0; i < users_status.length; i++) {
var id = users_status[i].parentElement.parentElement.parentElement.id;
id = id.substr(id.lastIndexOf("-") + 1);
XHR.poll(1, '<%=url([[admin]], [[services]], [[v2ray_server]], [[users_status]])%>', {
index: i,
id: id
},
function(x, result) {
users_status[result.index].setAttribute("style","font-weight:bold;");
users_status[result.index].setAttribute("color",result.status ? "green":"red");
users_status[result.index].innerHTML = (result.status ? '✓' : 'X');
}
);
}
//]]>
</script>
@@ -0,0 +1,184 @@
<%
local v2ray_version=luci.sys.exec("/usr/bin/xray -version | awk '{print $2}' | sed -n 1P")
-%>
<script type="text/javascript">
//<![CDATA[
var v2rayInfo;
var tokenStr = '<%=token%>';
var noUpdateText = '<%:It is the latest version%>';
var updateSuccessText = '<%:Update successful%>';
var clickToUpdateText = '<%:Click to update%>';
var inProgressText = '<%:Updating...%>';
var unexpectedErrorText = '<%:Unexpected error%>';
var updateInProgressNotice = '<%:Updating, are you sure to close?%>';
var downloadingText = '<%:Downloading...%>';
var decompressioningText = '<%:Unpacking...%>';
var movingText = '<%:Moving...%>';
window.onload = function() {
var v2rayCheckBtn = document.getElementById('_v2ray-check_btn');
var v2rayDetailElm = document.getElementById('_v2ray-check_btn-detail');
};
function addPageNotice_v2ray() {
window.onbeforeunload = function(e) {
e.returnValue = updateInProgressNotice;
return updateInProgressNotice;
};
}
function removePageNotice_v2ray() {
window.onbeforeunload = undefined;
}
function onUpdateSuccess_v2ray(btn) {
alert(updateSuccessText);
if(btn) {
btn.value = updateSuccessText;
btn.placeholder = updateSuccessText;
btn.disabled = true;
}
window.setTimeout(function() {
window.location.reload();
}, 1000);
}
function onRequestError_v2ray(btn, errorMessage) {
btn.disabled = false;
btn.value = btn.placeholder;
if(errorMessage) {
alert(errorMessage);
}
}
function doAjaxGet(url, data, onResult) {
new XHR().get(url, data, function(_, json) {
var resultJson = json || {
'code': 1,
'error': unexpectedErrorText
};
if(typeof onResult === 'function') {
onResult(resultJson);
}
})
}
function onBtnClick_v2ray(btn) {
if(v2rayInfo === undefined) {
checkUpdate_v2ray(btn);
} else {
doUpdate_v2ray(btn);
}
}
function checkUpdate_v2ray(btn) {
btn.disabled = true;
btn.value = inProgressText;
addPageNotice_v2ray();
var ckeckDetailElm = document.getElementById(btn.id + '-detail');
doAjaxGet('<%=url([[admin]], [[services]], [[v2ray_server]], [[check]])%>', {
token: tokenStr,
arch: ''
}, function(json) {
removePageNotice_v2ray();
if(json.code) {
v2rayInfo = undefined;
onRequestError_v2ray(btn, json.error);
} else {
if(json.update) {
v2rayInfo = json;
btn.disabled = false;
btn.value = clickToUpdateText;
btn.placeholder = clickToUpdateText;
if(ckeckDetailElm) {
var urlNode = '';
if(json.version) {
urlNode = '<em style="color:red;">最新版本号:' + json.version + '</em>';
if(json.url && json.url.html) {
urlNode = '<a href="' + json.url.html + '" target="_blank">' + urlNode + '</a>';
}
}
ckeckDetailElm.innerHTML = urlNode;
}
} else {
btn.disabled = true;
btn.value = noUpdateText;
}
}
});
}
function doUpdate_v2ray(btn) {
btn.disabled = true;
btn.value = downloadingText;
addPageNotice_v2ray();
var v2rayUpdateUrl = '<%=url([[admin]], [[services]], [[v2ray_server]], [[update]])%>';
// Download file
doAjaxGet(v2rayUpdateUrl, {
token: tokenStr,
url: v2rayInfo ? v2rayInfo.url.download : ''
}, function(json) {
if(json.code) {
removePageNotice_v2ray();
onRequestError_v2ray(btn, json.error);
} else {
btn.value = decompressioningText;
// Extract file
doAjaxGet(v2rayUpdateUrl, {
token: tokenStr,
task: 'extract',
file: json.file,
subfix: v2rayInfo ? v2rayInfo.type : ''
}, function(json) {
if(json.code) {
removePageNotice_v2ray();
onRequestError_v2ray(btn, json.error);
} else {
btn.value = movingText;
// Move file to target dir
doAjaxGet(v2rayUpdateUrl, {
token: tokenStr,
task: 'move',
file: json.file
}, function(json) {
removePageNotice_v2ray();
if(json.code) {
onRequestError_v2ray(btn, json.error);
} else {
onUpdateSuccess_v2ray(btn);
}
})
}
})
}
})
}
//]]>
</script>
<div class="cbi-value">
<label class="cbi-value-title">V2ray
<%:Version%>
</label>
<div class="cbi-value-field">
<div class="cbi-value-description">
<span>【 <%=v2ray_version%>】</span>
<input class="cbi-button cbi-input-apply" type="submit" id="_v2ray-check_btn" onclick="onBtnClick_v2ray(this);" value="<%:Manually update%>">
<span id="_v2ray-check_btn-detail"></span>
</div>
</div>
</div>
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -0,0 +1,185 @@
msgid "V2ray Server"
msgstr "V2ray 服务器"
msgid "Global Settings"
msgstr "全局设置"
msgid "Caddy path"
msgstr "Caddy 路径"
msgid "if you want to run from memory, change the path, such as /tmp/caddy, Then save the application and update it manually."
msgstr "如果你希望从内存中运行,请更改路径,例如/tmp/caddy,然后保存应用后,再手动更新。"
msgid "Server Config"
msgstr "服务器配置"
msgid "Users Manager"
msgstr "用户管理"
msgid "Remarks"
msgstr "备注"
msgid "Bind Local"
msgstr "本机监听"
msgid "When selected, it can only be accessed locally,It is recommended to turn on when using reverse proxies."
msgstr "当勾选时,只能由本机访问此端口,当开启反向代理时建议勾选此项。"
msgid "Port"
msgstr "端口"
msgid "User name"
msgstr "用户名"
msgid "Password"
msgstr "密码"
msgid "Protocol"
msgstr "协议"
msgid "Null"
msgstr "无"
msgid "Alter ID"
msgstr "额外ID(AlterID)"
msgid "User Level"
msgstr "用户等级(Level)"
msgid "When OTA is enabled, V2Ray will reject connections that are not OTA enabled. This option is invalid when using AEAD encryption."
msgstr "开启 OTA 后,V2Ray 会拒绝未启用 OTA 的连接。当使用 AEAD 加密时,该选项无效。"
msgid "Transport"
msgstr "传输方式"
msgid "Camouflage Type"
msgstr "伪装类型"
msgid "Camouflage Domain,you can not fill in"
msgstr "伪装域名,也可以不填写"
msgid "Reverse Proxy"
msgstr "反向代理"
msgid "Reverse Proxy Type"
msgstr "反向代理类型"
msgid "can not has conflict"
msgstr "请不要冲突"
msgid "Use HTTPS"
msgstr "使用HTTPS"
msgid "TLS Settings"
msgstr "TLS配置"
msgid "Nginx does not support HTTP/2 reverse proxies"
msgstr "Nginx 不支持HTTP/2反向代理"
msgid "as:"
msgstr "如:"
msgid "Public key absolute path"
msgstr "公钥文件绝对路径"
msgid "Private key absolute path"
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)"
msgstr "<br>none:默认值,不进行伪装,发送的数据是没有特征的数据包。<br>srtp:伪装成 SRTP 数据包,会被识别为视频通话数据(如 FaceTime)。<br>utp:伪装成 uTP 数据包,会被识别为 BT 下载数据。<br>wechat-video:伪装成微信视频通话的数据包。<br>dtls:伪装成 DTLS 1.2 数据包。<br>wireguard:伪装成 WireGuard 数据包。(并不是真正的 WireGuard 协议)"
msgid "transit node"
msgstr "中转到此节点"
msgid "This is the node inside passwall, which you can't use if you don't have it installed."
msgstr "这里显示的是passwall里的节点,如果没有安装,将无法使用此功能。"
msgid "Accept LAN Access"
msgstr "接受局域网访问"
msgid "When selected, it can accessed lan , this will not be safe!"
msgstr "当勾选时,可以直接访问局域网,这将不安全!(非特殊情况不建议开启)"
msgid "Logs"
msgstr "日志"
msgid "Clear logs"
msgstr "清空日志"
msgid "Can't determine ARCH, or ARCH not supported."
msgstr "无法确认ARCH架构,或是不支持。"
msgid "Get remote version info failed."
msgstr "获取远程版本信息失败。"
msgid "New version found, but failed to get new version download url."
msgstr "发现新版本,但未能获得新版本的下载地址。"
msgid "Download url is required."
msgstr "请指定下载地址。"
msgid "File download failed or timed out: %s"
msgstr "文件下载失败或超时:%s"
msgid "File path required."
msgstr "请指定文件路径。"
msgid "Can't find client in file: %s"
msgstr "无法在文件中找到客户端:%s"
msgid "Client file is required."
msgstr "请指定客户端文件。"
msgid "The client file is not suitable for current device."
msgstr "客户端文件不适合当前设备。"
msgid "Can't move new file to path: %s"
msgstr "无法移动新文件到:%s"
msgid "Update..."
msgstr "更新中"
msgid "It is the latest version"
msgstr "已是最新版本"
msgid "Update successful"
msgstr "更新成功"
msgid "Click to update"
msgstr "点击更新"
msgid "Updating..."
msgstr "更新中"
msgid "Unexpected error"
msgstr "意外错误"
msgid "Updating, are you sure to close?"
msgstr "正在更新,你确认要关闭吗?"
msgid "Downloading..."
msgstr "下载中"
msgid "Unpacking..."
msgstr "解压中"
msgid "Moving..."
msgstr "移动中"
msgid "Enabled"
msgstr "启用"
msgid "Status"
msgstr "状态"
msgid "Current Condition"
msgstr "当前状态"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "RUNNING"
msgstr "运行中"
msgid "Manually update"
msgstr "检查更新"
@@ -0,0 +1,4 @@
config global
option enable '0'
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh /etc/rc.common
START=99
start() {
/usr/lib/lua/luci/model/cbi/v2ray_server/api/app.lua start
}
stop() {
/usr/lib/lua/luci/model/cbi/v2ray_server/api/app.lua stop
}
restart() {
stop
start
}
@@ -0,0 +1,28 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-v2ray_server.json" ] && {
cat > /usr/share/ucitrack/luci-app-v2ray_server.json << EEOF
{
"config": "v2ray_server",
"init": "v2ray_server"
}
EEOF
}
uci -q batch <<-EOF >/dev/null
delete firewall.v2ray_server
set firewall.v2ray_server=include
set firewall.v2ray_server.type=script
set firewall.v2ray_server.path=/var/etc/v2ray_server.include
set firewall.v2ray_server.reload=1
commit firewall
EOF
uci -q batch <<-EOF >/dev/null
delete ucitrack.@v2ray_server[-1]
add ucitrack v2ray_server
set ucitrack.@v2ray_server[-1].init=v2ray_server
commit ucitrack
EOF
rm -rf /tmp/luci-*cache
exit 0
@@ -0,0 +1,11 @@
{
"luci-app-v2ray-server": {
"description": "Grant UCI access for luci-app-v2ray-server",
"read": {
"uci": [ "v2ray_server" ]
},
"write": {
"uci": [ "v2ray_server" ]
}
}
}
@@ -0,0 +1,4 @@
{
"config": "v2ray_server",
"init": "v2ray_server"
}