🍓 Sync 2026-08-18 01:07:28

This commit is contained in:
github-actions[bot] committed 2026-08-18 01:07:28 +08:00
1 parent 522b0fc0b6
commit f3ba5f7b5d
53 files changed
+4613 -4898

No files matched your search

+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=amule
PKG_VERSION:=3.0.0
PKG_RELEASE:=2
PKG_VERSION:=2.3.3
PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/amule-project/amule/tar.gz/$(PKG_VERSION)?
+2 -2
View File
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=antileech
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git
PKG_SOURCE_DATE:=2021-12-05
PKG_SOURCE_URL:=https://github.com/persmule/amule-dlp.antiLeech.git
PKG_SOURCE_VERSION:=1b14ed828ed227933333de31ffb8e24e6c10451c
PKG_SOURCE_VERSION:=null
PKG_MIRROR_HASH:=skip
PKG_FIXUP:=autoreconf
+2 -2
View File
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_BAIDUDRIVE:=$(ARCH)
PKG_NAME:=baidudrive
PKG_VERSION:=linkeasefull-runtime-v3.0.8
PKG_RELEASE:=10
PKG_VERSION:=1.0.4
PKG_RELEASE:=11
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/baidudrive-runtime-v$(PKG_VERSION)/
PKG_HASH:=skip
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=BaiduPCS-Go
PKG_VERSION:=4.0.1
PKG_RELEASE:=5
PKG_VERSION:=3.9.0
PKG_RELEASE:=6
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/qjfoidnh/BaiduPCS-Go/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=boltbrowser
PKG_RELEASE:=6
PKG_RELEASE:=7
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/br0xen/boltbrowser.git
PKG_SOURCE_DATE:=2023-05-31
PKG_SOURCE_VERSION:=6ad3511ddb3c271fbaed2f22e409aa453ed89042
PKG_SOURCE_VERSION:=null
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-3.0-or-later
+42
View File
@@ -0,0 +1,42 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=ipt2socks-rs
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/fokx/ipt2socks-rs.git
PKG_SOURCE_VERSION:=5ec743ea93e88c5733614b6588f5eb0a25a3d5db
PKG_MIRROR_HASH:=skip
PKG_BUILD_DEPENDS:=rust/host
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/rust/rust-package.mk
define Package/ipt2socks-rs
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
TITLE:=Convert iptables/nftables to SOCKS5
URL:=https://github.com/fokx/ipt2socks-rs.git
DEPENDS:=$$(RUST_ARCH_DEPENDS)
endef
define Package/ipt2socks-rs/description
A utility to convert iptables/nftables (REDIRECT/TPROXY) traffic to SOCKS5 (TCP/UDP).
endef
define Package/ipt2socks-rs/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) \
$(PKG_INSTALL_DIR)/bin/ipt2socks-rs \
$(1)/usr/bin/ipt2socks
endef
$(eval $(call BuildPackage,ipt2socks-rs))
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease-common-bin
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=6
PKG_RELEASE:=7
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=78c1b63d4be857054a8e6e37bc22eb235e3f151e
PKG_SOURCE_VERSION:=3cdc853c91c55560e118f1f7e343d1ae0bd5e566
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=9
PKG_RELEASE:=10
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=78c1b63d4be857054a8e6e37bc22eb235e3f151e
PKG_SOURCE_VERSION:=3cdc853c91c55560e118f1f7e343d1ae0bd5e566
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+2 -2
View File
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkeasefull
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=3.0.5
PKG_RELEASE:=8
PKG_RELEASE:=9
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
PKG_HASH:=skip
endif
PKG_SOURCE_VERSION:=78c1b63d4be857054a8e6e37bc22eb235e3f151e
PKG_SOURCE_VERSION:=3cdc853c91c55560e118f1f7e343d1ae0bd5e566
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
+1 -1
View File
@@ -11,7 +11,7 @@ LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +curl +bandix
PKG_MAINTAINER:=timsaya
PKG_VERSION:=0.12.9
PKG_RELEASE:=7
PKG_RELEASE:=8
include $(TOPDIR)/feeds/luci/luci.mk
@@ -9,11 +9,6 @@
var BANDIX_COLOR_UPLOAD = '#f97316'; // 橙色 - 上传/上行
var BANDIX_COLOR_DOWNLOAD = '#06b6d4'; // 青色 - 下载/下行
// 远程广告配置
var BANDIX_AD_CONFIG_URL = 'https://raw.githubusercontent.com/timsaya/bandix-ads/main/ads.json';
var BANDIX_AD_DEFAULT_REFRESH_INTERVAL = 3600;
var BANDIX_AD_RETRY_INTERVAL = 900;
// 悬浮框背景色定义
var BANDIX_TOOLTIP_BG_OPENWRT2020_LIGHT = '#ffffff'; // OpenWrt 2020 浅色主题悬浮框背景
var BANDIX_TOOLTIP_BG_OPENWRT2020_DARK = '#2a2a2a'; // OpenWrt 2020 深色主题悬浮框背景
@@ -562,143 +557,11 @@ return view.extend({
transform: translateY(-1px);
}
.bandix-ad-row {
display: grid;
grid-template-columns: repeat(var(--bandix-ad-count, 6), minmax(112px, 1fr));
gap: 8px;
margin: 12px 0;
overflow-x: auto;
scrollbar-width: thin;
}
.bandix-ad-slot {
flex: 1 1 0;
min-width: 112px;
padding: 0;
aspect-ratio: 3 / 1;
border: 1px solid rgba(107, 114, 128, 0.35);
border-radius: 6px;
background: linear-gradient(135deg, rgba(225, 37, 27, 0.08), rgba(107, 114, 128, 0.04));
display: flex;
align-items: center;
color: inherit;
box-sizing: border-box;
overflow: hidden;
position: relative;
text-decoration: none;
transition: border-color 0.2s ease, transform 0.2s ease;
}
.bandix-ad-slot:not(.bandix-ad-disabled):hover,
.bandix-ad-slot:not(.bandix-ad-disabled):focus-visible {
border-color: rgba(225, 37, 27, 0.75);
box-shadow: 0 4px 12px rgba(225, 37, 27, 0.12);
transform: translateY(-2px);
}
.bandix-ad-disabled {
cursor: default;
}
.bandix-ad-label {
align-self: flex-start;
padding: 1px 5px;
border: none;
border-radius: 3px;
background: rgba(225, 37, 27, 0.88);
color: #ffffff;
font-size: 0.5625rem;
font-weight: 700;
letter-spacing: 0.08em;
line-height: 1.4;
}
.bandix-ad-copy {
align-self: stretch;
flex: 1 1 auto;
min-width: 0;
padding: 6px 4px 6px 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 3px;
box-sizing: border-box;
}
.bandix-ad-title {
width: 100%;
font-size: 0.6875rem;
font-weight: 600;
line-height: 1.25;
text-align: left;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.bandix-ad-media {
align-self: stretch;
flex: 0 0 33.333333%;
min-width: 0;
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.92);
}
.bandix-ad-image {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.bandix-ad-placeholder {
flex: 0 0 33.333333%;
aspect-ratio: 1 / 1;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
padding: 3px;
background: rgba(255, 255, 255, 0.92);
color: #e1251b;
font-size: 0.625rem;
font-weight: 700;
line-height: 1.2;
text-align: center;
box-sizing: border-box;
}
/* 移动端隐藏版本信息和更新徽章 */
@media (max-width: 768px) {
.bandix-version-wrapper {
display: none;
}
.bandix-ad-row {
display: flex;
gap: 6px;
padding-bottom: 4px;
}
.bandix-ad-slot {
flex: 0 0 180px;
min-width: 180px;
height: 60px;
}
.bandix-ad-copy {
padding-left: 7px;
}
.bandix-ad-title {
font-size: 0.625rem;
}
}
.device-mode-group {
@@ -2956,25 +2819,6 @@ return view.extend({
])
]),
// 广告数据从公开仓库动态加载
E('div', {
'class': 'bandix-ad-row',
'id': 'bandix-ad-row',
'aria-label': _('Advertising spaces')
}, [1, 2, 3, 4, 5, 6].map(function (index) {
return E('div', {
'class': 'bandix-ad-slot bandix-ad-disabled',
'data-ad-slot': String(index),
'aria-disabled': 'true'
}, [
E('div', { 'class': 'bandix-ad-copy' }, [
E('span', { 'class': 'bandix-ad-label' }, _('AD')),
E('span', { 'class': 'bandix-ad-title' }, _('Advertising space') + ' ' + index)
]),
E('span', { 'class': 'bandix-ad-placeholder' }, _('Advertising space'))
]);
})),
// 警告提示(包含在线设备数)
E('div', {
'class': 'bandix-alert' + (getThemeType() === 'wide' ? ' wide-theme' : '')
@@ -8147,205 +7991,6 @@ return view.extend({
updateTrafficIncrements(startMs, endMs, null, null);
}, 700);
// 异步加载远程广告(不阻塞主流程)
(function () {
var refreshTimer = null;
function resolveHttpsUrl(value, baseUrl) {
if (typeof value !== 'string' || !value) return null;
try {
var resolved = new URL(value, baseUrl);
return resolved.protocol === 'https:' ? resolved.href : null;
} catch (e) {
return null;
}
}
function scheduleRefresh(seconds) {
var interval = Number(seconds);
if (!Number.isFinite(interval)) interval = BANDIX_AD_DEFAULT_REFRESH_INTERVAL;
interval = Math.max(300, Math.min(86400, interval));
if (refreshTimer !== null) window.clearTimeout(refreshTimer);
refreshTimer = window.setTimeout(loadAdvertisements, interval * 1000);
}
function normalizeAdvertisementLocale(value) {
if (typeof value !== 'string') return '';
var locale = value.trim().replace(/_/g, '-').toLowerCase();
if (!locale || locale === 'auto' || locale === 'und') return '';
if (locale === 'zh' || locale === 'zh-cn' || locale === 'zh-sg' || locale === 'zh-hans') {
return 'zh-hans';
}
if (locale === 'zh-tw' || locale === 'zh-hk' || locale === 'zh-mo' || locale === 'zh-hant') {
return 'zh-hant';
}
return locale;
}
function getAdvertisementLocale() {
var candidates = [];
if (L.env) candidates.push(L.env.lang || L.env.language || '');
var htmlLanguage = document.documentElement.getAttribute('lang');
if (htmlLanguage) candidates.push(htmlLanguage);
if (window.navigator) candidates.push(window.navigator.language || '');
for (var i = 0; i < candidates.length; i++) {
var locale = normalizeAdvertisementLocale(candidates[i]);
if (locale) return locale;
}
return '';
}
function selectLocalizedAdvertisements(config) {
// 兼容旧版单语言配置,方便两个仓库分开部署
if (config.version === 1 && Array.isArray(config.ads)) {
return { locale: 'legacy', ads: config.ads };
}
if (config.version !== 2 || !config.locales || typeof config.locales !== 'object') {
throw new Error('Invalid advertising configuration');
}
var currentLocale = getAdvertisementLocale();
var defaultLocale = normalizeAdvertisementLocale(config.default_locale);
var localeCandidates = [];
if (currentLocale) {
localeCandidates.push(currentLocale);
var separatorIndex = currentLocale.indexOf('-');
if (separatorIndex > 0) localeCandidates.push(currentLocale.substring(0, separatorIndex));
}
if (defaultLocale) localeCandidates.push(defaultLocale);
for (var i = 0; i < localeCandidates.length; i++) {
var candidate = localeCandidates[i];
if (Object.prototype.hasOwnProperty.call(config.locales, candidate) &&
Array.isArray(config.locales[candidate])) {
return { locale: candidate, ads: config.locales[candidate] };
}
}
throw new Error('No advertising locale is available');
}
function renderAdvertisements(config) {
var adRow = document.getElementById('bandix-ad-row');
if (!adRow || !config) {
throw new Error('Invalid advertising configuration');
}
var selection = selectLocalizedAdvertisements(config);
adRow.setAttribute('data-ad-locale', selection.locale);
var ads = selection.ads.slice(0, 6).map(function (ad, index) {
if (!ad || typeof ad !== 'object') ad = {};
var href = resolveHttpsUrl(ad.href, BANDIX_AD_CONFIG_URL);
var image = resolveHttpsUrl(ad.image, BANDIX_AD_CONFIG_URL);
var title = typeof ad.title === 'string' ? ad.title.trim() : _('Advertising space') + ' ' + (index + 1);
if (!title) title = _('Advertising space') + ' ' + (index + 1);
var alt = typeof ad.alt === 'string' ? ad.alt.trim() : title;
return {
enabled: ad.enabled === true && href !== null,
href: href,
image: image,
title: title,
alt: alt || title
};
});
while (ads.length < 6) {
ads.push({
enabled: false,
href: null,
image: null,
title: _('Advertising space') + ' ' + (ads.length + 1),
alt: _('Advertising space')
});
}
while (adRow.firstChild) adRow.removeChild(adRow.firstChild);
ads.forEach(function (ad, index) {
var tagName = ad.enabled ? 'a' : 'div';
var attributes = {
'class': 'bandix-ad-slot' + (ad.enabled ? '' : ' bandix-ad-disabled'),
'data-ad-slot': String(index + 1),
'title': ad.title
};
if (ad.enabled) {
attributes.href = ad.href;
attributes.target = '_blank';
attributes.rel = 'noopener noreferrer sponsored';
} else {
attributes['aria-disabled'] = 'true';
}
var content = [E('div', { 'class': 'bandix-ad-copy' }, [
E('span', { 'class': 'bandix-ad-label' }, _('AD')),
E('span', { 'class': 'bandix-ad-title' }, ad.title)
])];
if (ad.enabled && ad.image) {
content.push(E('div', { 'class': 'bandix-ad-media' }, [
E('img', {
'class': 'bandix-ad-image',
'src': ad.image,
'alt': ad.alt,
'loading': 'lazy',
'referrerpolicy': 'no-referrer'
})
]));
} else {
content.push(E('span', { 'class': 'bandix-ad-placeholder' }, _('Advertising space')));
}
adRow.appendChild(E(tagName, attributes, content));
});
adRow.style.setProperty('--bandix-ad-count', '6');
}
function loadAdvertisements() {
var adRow = document.getElementById('bandix-ad-row');
if (!adRow) return;
if (typeof window.fetch !== 'function') {
console.debug('Failed to load advertisements: Fetch API is unavailable');
scheduleRefresh(BANDIX_AD_RETRY_INTERVAL);
return;
}
var separator = BANDIX_AD_CONFIG_URL.indexOf('?') === -1 ? '?' : '&';
var configUrl = BANDIX_AD_CONFIG_URL + separator + '_=' + Date.now();
window.fetch(configUrl, {
method: 'GET',
mode: 'cors',
credentials: 'omit',
cache: 'no-store'
}).then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
}).then(function (config) {
renderAdvertisements(config);
scheduleRefresh(config.refresh_interval);
}).catch(function (err) {
console.debug('Failed to load advertisements:', err);
scheduleRefresh(BANDIX_AD_RETRY_INTERVAL);
});
}
window.setTimeout(loadAdvertisements, 200);
})();
// 异步加载版本信息(不阻塞主流程)
(function () {
// 延迟执行,确保页面先完成初始化
-9
View File
@@ -1094,15 +1094,6 @@ msgstr "有更新"
msgid "Update available, click to go to settings"
msgstr "有更新,点击前往设置"
msgid "Advertising spaces"
msgstr "广告位"
msgid "Advertising space"
msgstr "广告位"
msgid "AD"
msgstr "广告"
msgid "Traffic Statistics"
msgstr "流量统计"
-9
View File
@@ -1279,12 +1279,3 @@ msgstr "TCX 出站錨點程式 ID"
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
msgstr "當 tc_order 為 before/after 時使用。必須是同一介面上的有效出站程式 ID。"
msgid "Advertising spaces"
msgstr "廣告位"
msgid "Advertising space"
msgstr "廣告位"
msgid "AD"
msgstr "廣告"
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-mosdns
PKG_VERSION:=1.7.5
PKG_RELEASE:=12
PKG_VERSION:=1.7.6
PKG_RELEASE:=13
LUCI_TITLE:=LuCI Support for mosdns
LUCI_PKGARCH:=all
@@ -2,7 +2,7 @@
'use strict';
import { popen, writefile, stat, readfile } from 'fs';
import { popen, stat, readfile, open } from 'fs';
import { cursor } from 'uci';
function to_array(val) {
@@ -99,8 +99,12 @@ const methods = {
try {
let path = get_logfile_path_internal();
if (path) {
writefile(path, '');
return { success: true };
let f = open(path, 'w');
if (f) {
f.close();
return { success: true };
}
return { error: 'Failed to open log file for writing.' };
} else {
return { error: 'Log file path could not be determined.' };
}
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.8.12
PKG_RELEASE:=225
PKG_RELEASE:=226
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -8,7 +8,6 @@ if not arg[1] or not m:get(arg[1]) then
luci.http.redirect(m.redirect)
end
fs = require "nixio.fs"
formvalue_key = "cbid." .. m.config .. "." .. arg[1] .. "."
m:appendTemplate("/node_config/header", {section = arg[1]})
@@ -24,7 +23,7 @@ m:foreach("nodes", function(s)
end
end)
s = m:section(NamedSection, arg[1], "nodes", translate("Node Config"))
local s = m:section(NamedSection, arg[1], "nodes", translate("Node Config"))
s.addremove = false
s.dynamic = false
@@ -74,33 +73,35 @@ end
o = s:option(ListValue, "type", translate("Type"))
if api.is_finded("ipt2socks") then
s.fields["type"]:value("Socks", translate("Socks"))
local type_name = "Socks"
if s.val["type"] == "Socks" then
local function _n(name)
return "socks_" .. name
end
o = s:option(ListValue, _n("del_protocol"), " ") --始终隐藏,用于删除 protocol
o:depends({ [_n("__hide")] = "1" })
s.fields["type"]:value(type_name, "Socks")
if s.val["type"] == type_name then
local s2 = NamedSection(m, arg[1], "server")
s2.type_name = type_name
s2.option_prefix = "socks_"
o = s2:option(ListValue, "del_protocol", " ") --始终隐藏,用于删除 protocol
o:depends({ __hide = "1" })
o.rewrite_option = "protocol"
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
o = s2:option(Value, "address", translate("Address (Support Domain Name)"))
o = s:option(Value, _n("port"), translate("Port"))
o = s2:option(Value, "port", translate("Port"))
o.datatype = "port"
o = s:option(Value, _n("username"), translate("Username"))
o = s2:option(Value, "username", translate("Username"))
o = s:option(Value, _n("password"), translate("Password"))
o = s2:option(Value, "password", translate("Password"))
o.password = true
api.luci_types(arg[1], m, s, "Socks", "socks_")
api.luci_types(s, s2)
end
end
local type_table = {}
for filename in fs.dir(types_dir) do
for filename in api.fs.dir(types_dir) do
table.insert(type_table, filename)
end
table.sort(type_table)
@@ -76,6 +76,20 @@ o = s:option(Flag, "log", translate("Enable") .. " " .. translate("Log"))
o.default = 1
o.rmempty = false
o = s:option(DummyValue, "_log", translate("Log File"))
o.rawhtml = true
o.cfgvalue = function(t, n)
local log_path = api.TMP_PATH .. "/" .. arg[1] .. ".log"
local log_url = api.url("get_socks_log") .. "?name=" .. arg[1]
return string.format(
'<code>%s</code>&nbsp;&nbsp;<input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="window.open(\'%s\', \'_blank\')" />',
log_path,
translate("View Log"),
log_url
)
end
o:depends("log", true)
o = s:option(Flag, "enable_autoswitch", translate("Auto Switch"))
o.default = 0
o.rmempty = false
@@ -0,0 +1,917 @@
local singbox_bin = api.finded_com("sing-box")
if not singbox_bin then
return
end
-- [[ sing-box ]]
local m, s1 = ...
local type_name = "sing-box"
s1.fields["type"]:value(type_name, "Sing-Box")
if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
if s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "singbox_"
local formvalue_proto = luci.http.formvalue(formvalue_key .. "protocol")
if formvalue_proto then s1.val["protocol"] = formvalue_proto end
local arg_select_proto = luci.http.formvalue("select_proto") or ""
local ss_method_new_list = {
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
local ss_method_old_list = {
"aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5", "chacha20-ietf", "xchacha20",
}
local security_list = { "none", "auto", "aes-128-gcm", "chacha20-poly1305", "zero" }
local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'")
local singbox_version = api.get_app_version("sing-box"):match("[^v]+")
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("socks", "Socks")
o:value("http", "HTTP")
o:value("shadowsocks", "Shadowsocks")
o:value("vmess", "Vmess")
o:value("trojan", "Trojan")
if singbox_tags:find("with_wireguard") then
o:value("wireguard", "WireGuard")
end
if singbox_tags:find("with_quic") then
o:value("hysteria", "Hysteria")
end
o:value("vless", "VLESS")
if singbox_tags:find("with_quic") then
o:value("tuic", "TUIC")
end
if singbox_tags:find("with_quic") then
o:value("hysteria2", "Hysteria2")
end
o:value("anytls", "AnyTLS")
o:value("ssh", "SSH")
if singbox_tags:find("with_naive_outbound") then
o:value("naive", "NaïveProxy")
end
o:value("_urltest", translate("URLTest"))
o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface"))
function o.custom_cfgvalue(self, section)
if arg_select_proto ~= "" then
return arg_select_proto
else
return m:get(section, self.config_option)
end
end
local load_urltest_options = s1.val["protocol"] == "_urltest" or arg_select_proto == "_urltest"
local load_shunt_options = s1.val["protocol"] == "_shunt" or arg_select_proto == "_shunt"
local load_iface_options = s1.val["protocol"] == "_iface" or arg_select_proto == "_iface"
local load_normal_options = true
if load_urltest_options or load_shunt_options or load_iface_options then
load_normal_options = nil
end
if not arg_select_proto:find("_") then
load_normal_options = true
end
local netdev_list = api.get_network_devices()
local node_list = api.get_node_list()
if load_urltest_options then -- [[ URLTest Start ]]
o = s:option(ListValue, "node_add_mode", translate("Node Addition Method"))
o:depends({ protocol = "_urltest" })
o.default = "manual"
o:value("manual", translate("Manual"))
o:value("batch", translate("Batch"))
o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
if k1 == "socks_list" or k1 == "normal_list" then
for i, v in ipairs(v1) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""
end
end
end
-- 读取旧 DynamicList
function o.custom_cfgvalue(self, section)
return table.concat(m:get(section, "urltest_node") or {}, " ")
end
-- 写入保持 DynamicList
function o.custom_write(self, section, value)
local old = m:get(section, "urltest_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m:set(section, "urltest_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m:set(section, "urltest_node", new)
return
end
end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
o.widget = "checkbox"
o:value("default", translate("default"))
for k, v in pairs(groups) do
o:value(api.UrlEncode(k), k)
end
o = s:option(Value, "node_match_rule", translate("Node Matching Rules"))
o:depends({ node_add_mode = "batch" })
local descrStr = "Example: <code>^A && B && !C && D$</code><br>"
descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).<br>"
descrStr = descrStr .. "Conditions are joined by <code>&&</code> (AND), and their order does not affect the result.<br>"
descrStr = descrStr .. "Multiple groups can be separated by <code>||</code> (OR), matching succeeds if any group matches.<br>"
descrStr = descrStr .. "Example: <code>A && B || C && D</code> means (A AND B) OR (C AND D)."
o.description = translate(descrStr)
o = s:option(Value, "urltest_url", translate("Probe URL"))
o:depends({ protocol = "_urltest" })
o:value("https://cp.cloudflare.com/", "Cloudflare")
o:value("https://www.gstatic.com/generate_204", "Gstatic")
o:value("https://www.google.com/generate_204", "Google")
o:value("https://www.youtube.com/generate_204", "YouTube")
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
o.default = o.keylist[3]
o.description = translate("The URL used to detect the connection status.")
o = s:option(Value, "urltest_interval", translate("Test interval"))
o:depends({ protocol = "_urltest" })
o.default = "3m"
o.placeholder = "3m"
o.description = translate("The interval between initiating probes.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.") .. "<br>" ..
translate("Test interval must be less or equal than idle timeout.")
o = s:option(Value, "urltest_tolerance", translate("Test tolerance"), translate("The test tolerance in milliseconds."))
o:depends({ protocol = "_urltest" })
o.datatype = "uinteger"
o.placeholder = "50"
o.default = "50"
o = s:option(Value, "urltest_idle_timeout", translate("Idle timeout"))
o:depends({ protocol = "_urltest" })
o.placeholder = "30m"
o.default = "30m"
o.description = translate("The idle timeout.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.")
o = s:option(Flag, "urltest_interrupt_exist_connections", translate("Interrupt existing connections"))
o:depends({ protocol = "_urltest" })
o.default = "0"
o.description = translate("Interrupt existing connections when the selected outbound has changed.")
end -- [[ URLTest End ]]
if load_iface_options then -- [[ 自定义接口 Start ]]
o = s:option(Value, "iface", translate("Interface"))
o:depends({ protocol = "_iface" })
for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
end
if load_normal_options then
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
o = s:option(Value, "port", translate("Port"))
o.datatype = "port"
o = s:option(Value, "uuid", translate("ID"))
o.password = true
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "tuic" })
o = s:option(Value, "username", translate("Username"))
o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "ssh" })
o:depends({ protocol = "naive" })
o = s:option(Value, "password", translate("Password"))
o.password = true
o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "anytls" })
o:depends({ protocol = "ssh" })
o:depends({ protocol = "naive" })
o = s:option(ListValue, "security", translate("Encrypt Method"))
for a, t in ipairs(security_list) do o:value(t) end
o:depends({ protocol = "vmess" })
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
o.rewrite_option = "method"
for a, t in ipairs(ss_method_new_list) do o:value(t) end
for a, t in ipairs(ss_method_old_list) do o:value(t) end
o:depends({ protocol = "shadowsocks" })
o = s:option(Flag, "uot", translate("UDP over TCP"))
o:depends({ protocol = "socks" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "naive" })
o = s:option(Value, "alter_id", "Alter ID")
o.datatype = "uinteger"
o.default = "0"
o:depends({ protocol = "vmess" })
o = s:option(Flag, "global_padding", "global_padding", translate("Protocol parameter. Will waste traffic randomly if enabled."))
o.default = "0"
o:depends({ protocol = "vmess" })
o = s:option(Flag, "authenticated_length", "authenticated_length", translate("Protocol parameter. Enable length block encryption."))
o.default = "0"
o:depends({ protocol = "vmess" })
o = s:option(ListValue, "flow", translate("flow"))
o.default = ""
o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:depends({ protocol = "vless", tls = true })
if singbox_tags:find("with_quic") then
o = s:option(Value, "hysteria_hop", translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_hop_interval", translate("Hop Interval(second)"), translate("Example:") .. "30 (≥5)")
o.datatype = "uinteger"
o.placeholder = "30"
o.default = "30"
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_obfs", translate("Obfs Password"))
o:depends({ protocol = "hysteria" })
o = s:option(ListValue, "hysteria_auth_type", translate("Auth Type"))
o:value("disable", translate("Disable"))
o:value("string", translate("STRING"))
o:value("base64", translate("BASE64"))
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_auth_password", translate("Auth Password"))
o.password = true
o:depends({ protocol = "hysteria", hysteria_auth_type = "string"})
o:depends({ protocol = "hysteria", hysteria_auth_type = "base64"})
o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps"))
o.default = "10"
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps"))
o.default = "50"
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window"))
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_recv_window", translate("QUIC connection receive window"))
o:depends({ protocol = "hysteria" })
o = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection"))
o:depends({ protocol = "hysteria" })
end
if singbox_tags:find("with_quic") then
o = s:option(ListValue, "tuic_congestion_control", translate("Congestion control algorithm"))
o.default = "cubic"
o:value("bbr", translate("BBR"))
o:value("cubic", translate("CUBIC"))
o:value("new_reno", translate("New Reno"))
o:depends({ protocol = "tuic" })
o = s:option(ListValue, "tuic_udp_relay_mode", translate("UDP relay mode"))
o.default = "native"
o:value("native", translate("native"))
o:value("quic", translate("QUIC"))
o:depends({ protocol = "tuic" })
--[[
o = s:option(Flag, "tuic_udp_over_stream", translate("UDP over stream"))
o:depends({ protocol = "tuic" })
]]--
o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake"))
o.default = 0
o:depends({ protocol = "tuic" })
o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)"))
o.datatype = "uinteger"
o.default = "3"
o:depends({ protocol = "tuic" })
o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:value("spdy/3.1")
o:value("h3,spdy/3.1")
o:depends({ protocol = "tuic" })
end
if singbox_tags:find("with_quic") then
o = s:option(Value, "hysteria2_hop", translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(Value, "hysteria2_hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5."))
o.datatype = "or(uinteger,portrange)"
o.placeholder = "30"
o.default = "30"
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
o.default = "0"
if api.compare_versions(singbox_version, ">=", "1.14.0") then
o:depends({ protocol = "hysteria2"})
else
o:depends({ protocol = "__hide"})
end
o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ hysteria2_realms = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ hysteria2_realms = "1" })
o = s:option(Value, "hysteria2_auth_password", translate("Auth Password"))
o.password = true
o:depends({ protocol = "hysteria2"})
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password"))
o:depends({ hysteria2_obfs_type = "salamander" })
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps"))
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o:depends({ protocol = "hysteria2"})
o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o:depends({ protocol = "hysteria2"})
o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection"))
o.default = "0"
o:depends({ protocol = "hysteria2"})
end
-- [[ SSH config start ]] --
o = s:option(TextValue, "ssh_priv_key", translate("Private Key"))
o.rows = 5
o.wrap = "off"
o:depends({ protocol = "ssh" })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Value, "ssh_priv_key_pp", translate("Private Key Passphrase"))
o.password = true
o:depends({ protocol = "ssh" })
o = s:option(DynamicList, "ssh_host_key", translate("Host Key"), translate("Accept any if empty."))
o:depends({ protocol = "ssh" })
o = s:option(DynamicList, "ssh_host_key_algo", translate("Host Key Algorithms"))
o:depends({ protocol = "ssh" })
o = s:option(Value, "ssh_client_version", translate("Client Version"), translate("Random version will be used if empty."))
o:depends({ protocol = "ssh" })
-- [[ SSH config end ]] --
-- [[ naive start ]] --
o = s:option(Value, "naive_insecure_concurrency", translate("Concurrent Tunnels"))
o.datatype = "uinteger"
o.placeholder = "0"
o.default = "0"
o:depends({ protocol = "naive" })
o = s:option(Flag, "naive_quic", translate("QUIC"))
o.default = 0
o:depends({ protocol = "naive" })
o = s:option(ListValue, "naive_congestion_control", translate("Congestion control algorithm"))
o.default = "bbr"
o:value("bbr", translate("BBR"))
o:value("bbr2", translate("BBRv2"))
o:value("cubic", translate("CUBIC"))
o:value("reno", translate("New Reno"))
o:depends({ naive_quic = "1" })
-- [[ naive end ]] --
o = s:option(Flag, "tls", translate("TLS"))
o.default = 0
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "http" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "anytls" })
o = s:option(ListValue, "alpn", translate("ALPN"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ tls = true })
o:depends({ protocol = "hysteria" })
o = s:option(Flag, "tls_disable_sni", translate("Disable SNI"), translate("Do not send server name in ClientHello."))
o.default = "0"
o:depends({ tls = true })
o:depends({ protocol = "hysteria"})
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain"))
o:depends({ tls = true })
o:depends({ protocol = "hysteria"})
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "naive" })
o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
o:depends({ tls = true })
o:depends({ protocol = "hysteria"})
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)"))
o.default = "0"
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria"})
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "naive" })
o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ tls_certificate = true })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Value, "cipherSuites", translate("Cipher Suites"), '<a href="https://go.dev/src/crypto/tls/cipher_suites.go#L44" target="_blank">***</a>' .. " " .. translate("Configures the list of supported cipher suites, separated by :"))
o:depends({ tls = true })
o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
o:depends({ tls = true, flow = "", reality = false })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o:depends({ protocol = "naive" })
o = s:option(TextValue, "ech_config", translate("ECH Config"))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ ech = true })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Value, "ech_query_server_name", translate("ECH Query Domain"), translate("Overrides the domain name used for ECH HTTPS record queries."))
o:depends({ ech = true })
if singbox_tags:find("with_utls") then
o = s:option(Flag, "utls", translate("uTLS"))
o.default = "0"
o:depends({ tls = true })
o = s:option(ListValue, "fingerprint", translate("Finger Print"))
o:value("chrome")
o:value("firefox")
o:value("edge")
o:value("safari")
o:value("360")
o:value("qq")
o:value("ios")
o:value("android")
o:value("random")
o:value("randomized")
o.default = "chrome"
o:depends({ utls = true })
-- [[ REALITY部分 ]] --
o = s:option(Flag, "reality", translate("REALITY"))
o.default = 0
o:depends({ protocol = "vless", tls = true })
o:depends({ protocol = "vmess", tls = true })
o:depends({ protocol = "shadowsocks", tls = true })
o:depends({ protocol = "trojan", tls = true })
o:depends({ protocol = "anytls", tls = true })
o = s:option(Value, "reality_publicKey", translate("Public Key"))
o:depends({ reality = true })
o = s:option(Value, "reality_shortId", translate("Short Id"))
o:depends({ reality = true })
end
o = s:option(ListValue, "transport", translate("Transport"))
o:value("tcp", "TCP")
o:value("http", "HTTP")
o:value("ws", "WebSocket")
o:value("httpupgrade", "HTTPUpgrade")
if singbox_tags:find("with_quic") then
o:value("quic", "QUIC")
end
if singbox_tags:find("with_grpc") then
o:value("grpc", "gRPC")
else o:value("grpc", "gRPC-lite")
end
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
if singbox_tags:find("with_wireguard") then
o = s:option(Value, "wireguard_public_key", translate("Public Key"))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_secret_key", translate("Private Key"))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key"))
o:depends({ protocol = "wireguard" })
o = s:option(DynamicList, "wireguard_local_address", translate("Local Address"))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_mtu", translate("MTU"))
o.default = "1420"
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings."))
o:depends({ protocol = "wireguard" })
end
-- [[ TCP部分(模拟) ]]--
o = s:option(ListValue, "tcp_guise", translate("Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
o:depends({ transport = "tcp" })
o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
o:depends({ tcp_guise = "http" })
o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
o.placeholder = "/"
o:depends({ tcp_guise = "http" })
-- [[ HTTP部分 ]]--
o = s:option(DynamicList, "http_host", translate("HTTP Host"))
o:depends({ transport = "http" })
o = s:option(Value, "http_path", translate("HTTP Path"))
o.placeholder = "/"
o:depends({ transport = "http" })
o = s:option(Flag, "http_h2_health_check", translate("Health check"))
o:depends({ tls = true, transport = "http" })
o = s:option(Value, "http_h2_read_idle_timeout", translate("Idle timeout"))
o.default = "15"
o:depends({ http_h2_health_check = true })
o = s:option(Value, "http_h2_health_check_timeout", translate("Health check timeout"))
o.default = "15"
o:depends({ http_h2_health_check = true })
-- [[ WebSocket部分 ]]--
o = s:option(Value, "ws_host", translate("WebSocket Host"))
o:depends({ transport = "ws" })
o = s:option(Value, "ws_path", translate("WebSocket Path"))
o.placeholder = "/"
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部分 ]]--
o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host"))
o:depends({ transport = "httpupgrade" })
o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path"))
o.placeholder = "/"
o:depends({ transport = "httpupgrade" })
-- [[ gRPC部分 ]]--
o = s:option(Value, "grpc_serviceName", "ServiceName")
o:depends({ transport = "grpc" })
o = s:option(Flag, "grpc_health_check", translate("Health check"))
o:depends({ transport = "grpc" })
o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout"))
o.default = "15"
o:depends({ grpc_health_check = true })
o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout"))
o.default = "15"
o:depends({ grpc_health_check = true })
o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream"))
o.default = "0"
o:depends({ grpc_health_check = true })
-- [[ User-Agent ]]--
o = s:option(Value, "user_agent", translate("User-Agent"))
o.default = ""
o:value("", translate("default"))
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge")
o:value("Go-http-client/1.1", "golang")
o:value("curl/7.68.0", "curl")
o:depends({ tcp_guise = "http" })
o:depends({ transport = "http" })
o:depends({ transport = "ws" })
o:depends({ transport = "httpupgrade" })
o:depends({ protocol = "naive" })
-- [[ Mux ]]--
o = s:option(Flag, "mux", translate("Mux"))
o.rmempty = false
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless", flow = "" })
o:depends({ protocol = "shadowsocks", uot = "" })
o:depends({ protocol = "trojan" })
o = s:option(ListValue, "mux_type", translate("Mux"))
o:value("smux")
o:value("yamux")
o:value("h2mux")
o:depends({ mux = true })
o = s:option(Value, "mux_concurrency", translate("Mux concurrency"))
o.default = 4
o:depends({ mux = true, tcpbrutal = false })
o = s:option(Flag, "mux_padding", translate("Padding"))
o.default = 0
o:depends({ mux = true })
-- [[ TCP Brutal ]]--
o = s:option(Flag, "tcpbrutal", translate("TCP Brutal"))
o.default = 0
o:depends({ mux = true })
o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps"))
o.default = "10"
o:depends({ tcpbrutal = true })
o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps"))
o.default = "50"
o:depends({ tcpbrutal = true })
o = s:option(Flag, "shadowtls", "ShadowTLS")
o.default = 0
o:depends({ protocol = "vmess", tls = false })
o:depends({ protocol = "shadowsocks", tls = false })
o = s:option(ListValue, "shadowtls_version", "ShadowTLS " .. translate("Version"))
o.default = "1"
o:value("1", "ShadowTLS v1")
o:value("2", "ShadowTLS v2")
o:value("3", "ShadowTLS v3")
o:depends({ shadowtls = true })
o = s:option(Value, "shadowtls_password", "ShadowTLS " .. translate("Password"))
o.password = true
o:depends({ shadowtls = true, shadowtls_version = "2" })
o:depends({ shadowtls = true, shadowtls_version = "3" })
o = s:option(Value, "shadowtls_serverName", "ShadowTLS " .. translate("Domain"))
o:depends({ shadowtls = true })
if singbox_tags:find("with_utls") then
o = s:option(Flag, "shadowtls_utls", "ShadowTLS " .. translate("uTLS"))
o.default = "0"
o:depends({ shadowtls = true })
o = s:option(ListValue, "shadowtls_fingerprint", "ShadowTLS " .. translate("Finger Print"))
o:value("chrome")
o:value("firefox")
o:value("edge")
o:value("safari")
-- o:value("360")
o:value("qq")
o:value("ios")
-- o:value("android")
o:value("random")
-- o:value("randomized")
o.default = "chrome"
o:depends({ shadowtls = true, shadowtls_utls = true })
end
-- [[ SIP003 plugin ]]--
o = s:option(Flag, "plugin_enabled", translate("plugin"))
o.default = 0
o:depends({ protocol = "shadowsocks" })
o = s:option(ListValue, "plugin", "SIP003 " .. translate("plugin"))
o.default = "obfs-local"
o:depends({ plugin_enabled = true })
o:value("obfs-local")
o:value("v2ray-plugin")
o = s:option(Value, "plugin_opts", translate("opts"))
o:depends({ plugin_enabled = true })
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
o:value("", translate("Auto"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("https", "DoH")
o = s:option(Value, "domain_resolver_dns", "DNS")
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = o.keylist[1]
o:depends({ domain_resolver = "tcp" })
o:depends({ domain_resolver = "udp" })
o = s:option(Value, "domain_resolver_dns_https", "DNS")
o:value("https://120.53.53.53/dns-query", "DNSPod")
o:value("https://223.5.5.5/dns-query", "AliDNS")
o.default = o.keylist[1]
o:depends({ domain_resolver = "https" })
o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
o.default = ""
o:value("", translate("Auto"))
o:value("prefer_ipv4", translate("Prefer IPv4"))
o:value("prefer_ipv6", translate("Prefer IPv6"))
o:value("ipv4_only", translate("IPv4 Only"))
o:value("ipv6_only", translate("IPv6 Only"))
local protocols = s.fields["protocol"].keylist
if #protocols > 0 then
for i, v in ipairs(protocols) do
if not v:find("^_") then
local depends_condition = { protocol = v }
if v == "hysteria2" then
depends_condition["hysteria2_realms"] = false
end
s.fields["address"]:depends(depends_condition)
s.fields["port"]:depends(depends_condition)
s.fields["domain_resolver"]:depends(depends_condition)
s.fields["domain_strategy"]:depends(depends_condition)
end
end
end
end
-- [[ Normal single node End ]]
if not load_shunt_options then
o = s:option(ListValue, "chain_proxy", translate("Chain Proxy"))
o:value("", translate("Close(Not use)"))
if not (load_iface_options or load_urltest_options) then
-- Special node cannot be use pre-proxy.
o:value("1", translate("Preproxy Node"))
o:value("3", translate("Outbound Interface"))
end
o:value("2", translate("Landing Node"))
o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"), translate("Only support a layer of proxy."))
o1:depends({ chain_proxy = "1", hysteria2_realms = false })
o1.template = m:template_path("/cbi/nodes_listvalue")
o1.group = {}
o3 = s:option(Value, "outbound_iface", translate("Outbound Interface"))
o3:depends({ chain_proxy = "3" })
o3:value("", translate("All"))
for _, d in ipairs(netdev_list) do
o3:value(d.name, d.label)
end
o2 = s:option(ListValue, "to_node", translate("Landing Node"), translate("Only support a layer of proxy."))
o2:depends({ chain_proxy = "2", hysteria2_realms = false })
o2.template = m:template_path("/cbi/nodes_listvalue")
o2.group = {}
for k1, v1 in pairs(node_list) do
if k1 ~= "shunt_list" and k1 ~= "iface_list" then
for i, v in ipairs(v1) do
if v.id ~= arg[1] then
o1:value(v.id, v.remark)
o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
if k1 == "normal_list" then
-- Landing Node not support use special node.
o2:value(v.id, v.remark)
o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
end
end
end
end
api.luci_types(s1, s)
if load_shunt_options then
local current_node = m:get(arg[1]) or {}
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s1, {
node_id = arg[1],
node = current_node,
node_list = node_list,
})
end
@@ -0,0 +1,894 @@
if not api.finded_com("xray") then
return
end
-- [[ Xray ]]
local m, s1 = ...
local type_name = "Xray"
s1.fields["type"]:value(type_name, "Xray")
if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
if s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "xray_"
local formvalue_proto = luci.http.formvalue(formvalue_key .. "protocol")
if formvalue_proto then s1.val["protocol"] = formvalue_proto end
local arg_select_proto = luci.http.formvalue("select_proto") or ""
local ss_method_list = {
"aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305", "xchacha20-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
local security_list = { "auto", "aes-128-gcm", "chacha20-poly1305" }
local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
}
local xray_version = api.get_app_version("xray")
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("socks", translate("Socks"))
o:value("http", translate("HTTP"))
o:value("vmess", translate("Vmess"))
o:value("vless", translate("VLESS"))
o:value("shadowsocks", translate("Shadowsocks"))
o:value("trojan", translate("Trojan"))
o:value("wireguard", translate("WireGuard"))
if api.compare_versions(xray_version, ">=", "26.1.13") then
o:value("hysteria2", translate("Hysteria2"))
end
if api.compare_versions(xray_version, ">=", "1.8.12") then
o:value("_balancing", translate("Balancing"))
end
o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface"))
function o.custom_cfgvalue(self, section)
if arg_select_proto ~= "" then
return arg_select_proto
else
return m:get(section, self.config_option)
end
end
local load_balancing_options = s1.val["protocol"] == "_balancing" or arg_select_proto == "_balancing"
local load_shunt_options = s1.val["protocol"] == "_shunt" or arg_select_proto == "_shunt"
local load_iface_options = s1.val["protocol"] == "_iface" or arg_select_proto == "_iface"
local load_normal_options = true
if load_balancing_options or load_shunt_options or load_iface_options then
load_normal_options = nil
end
if not arg_select_proto:find("_") then
load_normal_options = true
end
local netdev_list = api.get_network_devices()
local node_list = api.get_node_list()
local fallback_list = {}
local is_balancer = nil
for k, e in ipairs(node_list.balancing_list or {}) do
if e.id ~= arg[1] then
fallback_list[#fallback_list + 1] = {
id = e["id"],
remark = e["remark"],
group = e["group"],
fallback = e.o["fallback_node"],
}
else
is_balancer = true
end
end
if load_balancing_options then -- [[ Load balancing Start ]]
o = s:option(ListValue, "node_add_mode", translate("Node Addition Method"))
o:depends({ protocol = "_balancing" })
o.default = "manual"
o:value("manual", translate("Manual"))
o:value("batch", translate("Batch"))
o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
if k1 == "socks_list" or k1 == "normal_list" then
for i, v in ipairs(v1) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""
end
end
end
-- 读取旧 DynamicList
function o.custom_cfgvalue(self, section)
return table.concat(m:get(section, "balancing_node") or {}, " ")
end
-- 写入保持 DynamicList
function o.custom_write(self, section, value)
local old = m:get(section, "balancing_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m:set(section, "balancing_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m:set(section, "balancing_node", new)
return
end
end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
o.widget = "checkbox"
o:value("default", translate("default"))
for k, v in pairs(groups) do
o:value(api.UrlEncode(k), k)
end
o = s:option(Value, "node_match_rule", translate("Node Matching Rules"))
o:depends({ node_add_mode = "batch" })
local descrStr = "Example: <code>^A && B && !C && D$</code><br>"
descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).<br>"
descrStr = descrStr .. "Conditions are joined by <code>&&</code> (AND), and their order does not affect the result.<br>"
descrStr = descrStr .. "Multiple groups can be separated by <code>||</code> (OR), matching succeeds if any group matches.<br>"
descrStr = descrStr .. "Example: <code>A && B || C && D</code> means (A AND B) OR (C AND D)."
o.description = translate(descrStr)
o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy"))
o:depends({ protocol = "_balancing" })
o:value("random")
o:value("roundRobin")
o:value("leastPing")
o:value("leastLoad")
o.default = "random"
-- Fallback Node
o = s:option(ListValue, "fallback_node", translate("Fallback Node"))
o.group = {"",""}
o:value("", translate("Close(Not use)"))
o:value("_direct", translate("Direct Connection"))
o:depends({ protocol = "_balancing" })
o.template = m:template_path("/cbi/nodes_listvalue")
-- 最大 fallback 套娃层数
local MAX_FALLBACK_DEPTH = 3
-- 检查是否会形成回环
local function will_loop(start_id, target_id, depth)
depth = depth or 0
-- 超过最大深度后停止递归
if depth >= MAX_FALLBACK_DEPTH then
return false
end
for _, v in ipairs(fallback_list) do
if v.id == target_id then
local fb = v.fallback
-- 没有 fallback
if not fb or fb == "" or fb == "_direct" then
return false
end
-- 检测到回环
if fb == start_id then
return true
end
-- 继续递归检查
return will_loop(start_id, fb, depth + 1)
end
end
return false
end
-- 获取 fallback 链深度
local function get_fallback_depth(id, depth)
depth = depth or 0
if depth >= MAX_FALLBACK_DEPTH then
return depth
end
for _, v in ipairs(fallback_list) do
if v.id == id then
local fb = v.fallback
if not fb or fb == "" or fb == "_direct" then
return depth
end
return get_fallback_depth(fb, depth + 1)
end
end
return depth
end
for _, v in ipairs(fallback_list) do
local depth = get_fallback_depth(v.id)
-- 超过最大套娃层数后,不允许继续选择 balancer
if depth < MAX_FALLBACK_DEPTH
and not will_loop(arg[1], v.id)
then
o:value(v.id, v.remark)
o.group[#o.group + 1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
for k1, v1 in pairs(node_list) do
if k1 == "socks_list" or k1 == "normal_list" or k1 == "urltest_list" then
for i, v in ipairs(v1) do
o:value(v.id, v.remark)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
end
-- 探测地址
o = s:option(Flag, "useCustomProbeUrl", translate("Use Custom Probe URL"), translate("By default the built-in probe URL will be used, enable this option to use a custom probe URL."))
o:depends({ protocol = "_balancing" })
o = s:option(Value, "probeUrl", translate("Probe URL"))
o:depends({ useCustomProbeUrl = true })
o:value("https://cp.cloudflare.com/", "Cloudflare")
o:value("https://www.gstatic.com/generate_204", "Gstatic")
o:value("https://www.google.com/generate_204", "Google")
o:value("https://www.youtube.com/generate_204", "YouTube")
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
o.default = o.keylist[3]
o.description = translate("The URL used to detect the connection status.")
-- 探测间隔
o = s:option(Value, "probeInterval", translate("Probe Interval"))
o:depends({ protocol = "_balancing" })
o.default = "1m"
o.placeholder = "1m"
o.description = translate("The interval between initiating probes.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.")
o = s:option(Value, "expected", translate("Preferred Node Count"))
o:depends({ balancingStrategy = "leastLoad" })
o.datatype = "uinteger"
o.default = "2"
o.placeholder = "2"
o.description = translate("The load balancer selects the optimal number of nodes, and traffic is randomly distributed among them.")
o = s:option(Value, "tolerance", translate("Failure Tolerance (%)"))
o:depends({ balancingStrategy = "leastLoad" })
o.datatype = "uinteger"
o.default = "10"
o.placeholder = "10"
o.description = translate("The maximum acceptable speed test failure rate. For example, 1 means allowing a 1% failure rate.")
end -- [[ 负载均衡 End ]]
if load_iface_options then -- [[ 自定义接口 Start ]]
o = s:option(Value, "iface", translate("Interface"))
o:depends({ protocol = "_iface" })
for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
end -- [[ 自定义接口 End ]]
if load_normal_options then
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
o = s:option(Value, "port", translate("Port"))
o.datatype = "port"
o = s:option(Value, "uuid", translate("ID"))
o.password = true
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o = s:option(Value, "username", translate("Username"))
o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o = s:option(Value, "password", translate("Password"))
o.password = true
o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
o = s:option(ListValue, "security", translate("Encrypt Method"))
for a, t in ipairs(security_list) do o:value(t) end
o:depends({ protocol = "vmess" })
o = s:option(Value, "encryption", translate("Encrypt Method") .. " (encryption)")
o.default = "none"
o.placeholder = "none"
o:depends({ protocol = "vless" })
o.validate = function(self, value)
value = api.trim(value)
return (value == "" and "none" or value)
end
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
o.rewrite_option = "method"
for a, t in ipairs(ss_method_list) do o:value(t) end
o:depends({ protocol = "shadowsocks" })
o = s:option(ListValue, "flow", translate("flow"))
o.default = ""
o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:value("xtls-rprx-vision-udp443")
o:depends({ protocol = "vless" })
---- [[hysteria2]]
o = s:option(Value, "hysteria2_hop", translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(Value, "hysteria2_hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5."))
o.datatype = "or(uinteger,portrange)"
o.placeholder = "30"
o.default = "30"
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
o.default = "0"
if api.compare_versions(xray_version, ">", "26.5.9") then
o:depends({ protocol = "hysteria2"})
else
o:depends({ protocol = "__hide"})
end
o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ hysteria2_realms = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ hysteria2_realms = "1" })
o = s:option(Value, "hysteria2_auth_password", translate("Auth Password"))
o.password = true
o:depends({ protocol = "hysteria2"})
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password"))
o:depends({ hysteria2_obfs_type = "salamander" })
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps"))
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o:depends({ protocol = "hysteria2"})
o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o:depends({ protocol = "hysteria2"})
o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection"))
o.default = "0"
o:depends({ protocol = "hysteria2"})
---- [[hysteria2 end]]
o = s:option(Flag, "tls", translate("TLS"))
o.default = 0
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "shadowsocks" })
o = s:option(Flag, "reality", translate("REALITY"), translate("Only recommend to use with VLESS-TCP-XTLS-Vision."))
o.default = 0
o:depends({ tls = true, transport = "raw" })
o:depends({ tls = true, transport = "ws" })
o:depends({ tls = true, transport = "grpc" })
o:depends({ tls = true, transport = "httpupgrade" })
o:depends({ tls = true, transport = "xhttp" })
o = s:option(ListValue, "alpn", translate("alpn"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ tls = true, reality = false })
-- o = s:option(Value, "minversion", translate("minversion"))
-- o.default = "1.3"
-- o:value("1.3")
-- o:depends({ tls = true })
o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain"))
o:depends({ tls = true })
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2" })
o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
o = s:option(Value, "tls_CertByName", translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2" })
o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)"))
o.default = "0"
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2" })
o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ tls_certificate = true })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(TextValue, "ech_config", translate("ECH Config"))
o.default = ""
o.rows = 5
o.wrap = "soft"
o:depends({ ech = true })
o.validate = function(self, value)
return api.trim(value:gsub("[\r\n]", ""))
end
-- [[ REALITY部分 ]] --
o = s:option(Value, "reality_publicKey", translate("Public Key"))
o:depends({ tls = true, reality = true })
o = s:option(Value, "reality_shortId", translate("Short Id"))
o:depends({ tls = true, reality = true })
o = s:option(Value, "reality_spiderX", translate("Spider X"))
o.placeholder = "/"
o:depends({ tls = true, reality = true })
o = s:option(Flag, "utls", translate("uTLS"))
o.default = "0"
o:depends({ tls = true, reality = false })
o = s:option(ListValue, "fingerprint", translate("Finger Print"))
o:value("chrome")
o:value("firefox")
o:value("edge")
o:value("safari")
o:value("360")
o:value("qq")
o:value("ios")
o:value("android")
o:value("random")
o:value("randomized")
o:value("unsafe")
o.default = "chrome"
o:depends({ tls = true, utls = true })
o:depends({ tls = true, reality = true })
o = s:option(Value, "cipherSuites", translate("Cipher Suites"), '<a href="https://go.dev/src/crypto/tls/cipher_suites.go#L44" target="_blank">***</a>' .. " " .. translate("Configures the list of supported cipher suites, separated by :"))
o:depends({ tls = true, reality = false })
o = s:option(Flag, "use_mldsa65Verify", translate("ML-DSA-65"))
o.default = "0"
o:depends({ tls = true, reality = true })
o = s:option(TextValue, "reality_mldsa65Verify", "ML-DSA-65 " .. translate("Public key"))
o.default = ""
o.rows = 5
o.wrap = "soft"
o:depends({ use_mldsa65Verify = true })
o.validate = function(self, value)
return api.trim(value:gsub("[\r\n]", ""))
end
o = s:option(ListValue, "transport", translate("Transport"))
o:value("raw", "RAW (TCP)")
o:value("mkcp", "mKCP")
o:value("ws", "WebSocket")
o:value("grpc", "gRPC")
o:value("httpupgrade", "HttpUpgrade")
o:value("xhttp", "XHTTP")
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
o = s:option(Value, "wireguard_public_key", translate("Public Key"))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_secret_key", translate("Private Key"))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key"))
o:depends({ protocol = "wireguard" })
o = s:option(DynamicList, "wireguard_local_address", translate("Local Address"))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_mtu", translate("MTU"))
o.default = "1420"
o:depends({ protocol = "wireguard" })
if api.compare_versions(xray_version, ">=", "1.8.0") then
o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings."))
o:depends({ protocol = "wireguard" })
end
o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive"))
o.default = "0"
o:depends({ protocol = "wireguard" })
-- [[ RAW部分 ]]--
-- TCP伪装
o = s:option(ListValue, "tcp_guise", translate("Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
o:depends({ transport = "raw" })
-- HTTP域名
o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
o:depends({ tcp_guise = "http" })
-- HTTP路径
o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
o.placeholder = "/"
o:depends({ tcp_guise = "http" })
-- [[ mKCP部分 ]]--
o = 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)<br />dns: Disguising traffic as DNS requests.'))
for a, t in ipairs(header_type_list) do o:value(t) end
o:depends({ transport = "mkcp" })
o = s:option(Value, "mkcp_domain", translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
o:depends({ mkcp_guise = "dns" })
o = s:option(Value, "mkcp_mtu", translate("KCP MTU"))
o.datatype = "uinteger"
o.default = 1350
o:depends({ transport = "mkcp" })
o = s:option(Value, "mkcp_seed", translate("KCP Seed"))
o:depends({ transport = "mkcp" })
-- [[ WebSocket部分 ]]--
o = s:option(Value, "ws_host", translate("WebSocket Host"))
o:depends({ transport = "ws" })
o = s:option(Value, "ws_path", translate("WebSocket Path"))
o.placeholder = "/"
o:depends({ transport = "ws" })
o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)"))
o.datatype = "integer"
o:depends({ transport = "ws" })
-- [[ gRPC部分 ]]--
o = s:option(Value, "grpc_serviceName", "ServiceName")
o:depends({ transport = "grpc" })
o = s:option(ListValue, "grpc_mode", "gRPC " .. translate("Transfer mode"))
o:value("gun")
o:value("multi")
o:depends({ transport = "grpc" })
o = s:option(Flag, "grpc_health_check", translate("Health check"))
o:depends({ transport = "grpc" })
o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout"))
o.default = "10"
o:depends({ grpc_health_check = true })
o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout"))
o.default = "20"
o:depends({ grpc_health_check = true })
o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream"))
o.default = "0"
o:depends({ grpc_health_check = true })
o = s:option(Value, "grpc_initial_windows_size", translate("Initial Windows Size"))
o.default = "0"
o:depends({ transport = "grpc" })
-- [[ HttpUpgrade部分 ]]--
o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host"))
o:depends({ transport = "httpupgrade" })
o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path"))
o.placeholder = "/"
o:depends({ transport = "httpupgrade" })
-- [[ XHTTP部分 ]]--
o = s:option(ListValue, "xhttp_mode", "XHTTP " .. translate("Mode"))
o:depends({ transport = "xhttp" })
o.default = "auto"
o:value("auto")
o:value("packet-up")
o:value("stream-up")
o:value("stream-one")
o = s:option(Value, "xhttp_host", translate("XHTTP Host"))
o:depends({ transport = "xhttp" })
o = s:option(Value, "xhttp_path", translate("XHTTP Path"))
o.placeholder = "/"
o:depends({ transport = "xhttp" })
o = s:option(Flag, "use_xhttp_extra", translate("XHTTP Extra"))
o.default = "0"
o:depends({ transport = "xhttp" })
o = s:option(TextValue, "xhttp_extra", " ", translate("An XHttpObject in JSON format, used for sharing."))
o:depends({ use_xhttp_extra = true })
o.rows = 10
o.wrap = "off"
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "xhttp_extra")
if raw then
return api.base64Decode(raw)
end
end
o.custom_write = function(self, section, value)
m:set(section, "xhttp_extra", api.base64Encode(value) or "")
local success, data = pcall(api.jsonc.parse, value)
if success and data then
local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address)
or (data.downloadSettings and data.downloadSettings.address)
if address and address ~= "" then
address = address:gsub("^%[", ""):gsub("%]$", "")
m:set(section, "download_address", address)
else
m:del(section, "download_address")
end
else
m:del(section, "download_address")
end
end
o.custom_remove = function(self, section, value)
m:del(section, "xhttp_extra")
m:del(section, "download_address")
end
-- [[ User-Agent ]]--
o = s:option(Value, "user_agent", translate("User-Agent"))
o.default = ""
o:value("", translate("default"))
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge")
o:value("Go-http-client/1.1", "golang")
o:value("curl/7.68.0", "curl")
o:depends({ tcp_guise = "http" })
o:depends({ transport = "ws" })
o:depends({ transport = "httpupgrade" })
o:depends({ transport = "xhttp" })
o:depends({ transport = "grpc" })
-- [[ Mux.Cool ]]--
o = s:option(Flag, "mux", "Mux", translate("Enable Mux.Cool"))
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless", transport = "raw" })
o:depends({ protocol = "vless", transport = "ws" })
o:depends({ protocol = "vless", transport = "grpc" })
o:depends({ protocol = "vless", transport = "httpupgrade" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
o = s:option(Value, "mux_concurrency", translate("Mux concurrency"))
o.default = -1
o:depends({ mux = true })
o = s:option(Value, "xudp_concurrency", translate("XUDP Mux concurrency"))
o.default = 8
o:depends({ mux = true })
--[[FinalMask]]
o = s:option(Flag, "use_finalmask", "FinalMask")
o.default = "0"
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "wireguard" })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(TextValue, "finalmask", " ")
o:depends({ use_finalmask = true })
o.rows = 10
o.wrap = "off"
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "FinalMask " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
return api.base64Decode(raw)
end
end
o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "")
end
--[[Fast Open]]
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required"))
o.default = 0
--[[tcpMptcp]]
o = s:option(Flag, "tcpMptcp", "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration."))
o.default = 0
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
o:value("", translate("Auto"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("https", "DoH")
o = s:option(Value, "domain_resolver_dns", "DNS")
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = o.keylist[1]
o:depends({ domain_resolver = "tcp" })
o:depends({ domain_resolver = "udp" })
o = s:option(Value, "domain_resolver_dns_https", "DNS")
o:value("https://120.53.53.53/dns-query", "DNSPod")
o:value("https://223.5.5.5/dns-query", "AliDNS")
o.default = o.keylist[1]
o:depends({ domain_resolver = "https" })
o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
o.default = ""
o:value("", translate("Auto"))
o:value("UseIPv4v6", translate("Prefer IPv4"))
o:value("UseIPv6v4", translate("Prefer IPv6"))
o:value("UseIPv4", translate("IPv4 Only"))
o:value("UseIPv6", translate("IPv6 Only"))
o = s:option(Flag, "happy_eyeballs", translate("Enable Happy Eyeballs"), translate("Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection."))
o.default = 0
local protocols = s.fields["protocol"].keylist
if #protocols > 0 then
for i, v in ipairs(protocols) do
if not v:find("^_") then
local depends_condition = { protocol = v }
if v == "hysteria2" then
depends_condition["hysteria2_realms"] = false
end
s.fields["address"]:depends(depends_condition)
s.fields["port"]:depends(depends_condition)
s.fields["domain_resolver"]:depends(depends_condition)
s.fields["happy_eyeballs"]:depends(depends_condition)
local strategy_depends = api.clone(depends_condition)
strategy_depends["happy_eyeballs"] = false
s.fields["domain_strategy"]:depends(strategy_depends)
if v ~= "hysteria2" then
s.fields["tcp_fast_open"]:depends({ protocol = v })
s.fields["tcpMptcp"]:depends({ protocol = v })
end
end
end
end
end
-- [[ Normal single node End ]]
if not load_shunt_options then
o = s:option(ListValue, "chain_proxy", translate("Chain Proxy"))
o:value("", translate("Close(Not use)"))
if not (load_iface_options or load_balancing_options) then
-- Special node cannot be use pre-proxy.
o:value("1", translate("Preproxy Node"))
o:value("3", translate("Outbound Interface"))
end
o:value("2", translate("Landing Node"))
o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"), translate("Only support a layer of proxy."))
o1:depends({ chain_proxy = "1", hysteria2_realms = false })
o1.template = m:template_path("/cbi/nodes_listvalue")
o1.group = {}
o3 = s:option(Value, "outbound_iface", translate("Outbound Interface"))
o3:depends({ chain_proxy = "3" })
o3:value("", translate("All"))
for _, d in ipairs(netdev_list) do
o3:value(d.name, d.label)
end
o2 = s:option(ListValue, "to_node", translate("Landing Node"), translate("Only support a layer of proxy."))
o2:depends({ chain_proxy = "2", hysteria2_realms = false })
o2.template = m:template_path("/cbi/nodes_listvalue")
o2.group = {}
for k1, v1 in pairs(node_list) do
if k1 ~= "shunt_list" and k1 ~= "iface_list" then
for i, v in ipairs(v1) do
if v.id ~= arg[1] then
o1:value(v.id, v.remark)
o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
if k1 == "normal_list" then
-- Landing Node not support use special node.
o2:value(v.id, v.remark)
o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
end
end
end
end
api.luci_types(s1, s)
if load_shunt_options then
local current_node = m:get(arg[1]) or {}
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s1, {
node_id = arg[1],
node = current_node,
node_list = node_list,
})
end
@@ -0,0 +1,72 @@
if not api.is_finded("sslocal") then
return
end
-- [[ Shadowsocks Rust ]]
local m, s1 = ...
local type_name = "SS-Rust"
s1.fields["type"]:value(type_name, "Shadowsocks Rust")
if s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "ssrust_"
local ssrust_encrypt_method_list = {
"none", "plain",
"aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
o = s:option(ListValue, "del_protocol") --始终隐藏,用于删除 protocol
o:depends({ __hide = "1" })
o.rewrite_option = "protocol"
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
o = s:option(Value, "port", translate("Port"))
o.datatype = "port"
o = s:option(Value, "password", translate("Password"))
o.password = true
o = s:option(Value, "ss_method", translate("Encrypt Method"))
for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end
o = s:option(Value, "timeout", translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required"))
o.default = 0
o = s:option(Flag, "plugin_enabled", translate("plugin"))
o.default = 0
o = s:option(Value, "plugin", "SIP003 " .. translate("plugin"), translate("Supports custom SIP003 plugins, Make sure the plugin is installed."))
o.default = "none"
o:value("none", translate("none"))
if api.is_finded("xray-plugin") then o:value("xray-plugin") end
if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end
if api.is_finded("obfs-local") then o:value("obfs-local") end
if api.is_finded("shadow-tls") then o:value("shadow-tls") end
o:depends({ plugin_enabled = true })
o.validate = function(self, value, t)
if value and value ~= "" and value ~= "none" then
if not api.is_finded(value) then
return nil, value .. ": " .. translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(Value, "plugin_opts", translate("opts"))
o:depends({ plugin_enabled = true })
api.luci_types(s1, s)
@@ -1,70 +1,66 @@
local m, s = ...
if not api.is_finded("ssr-local") and not api.is_finded("ssr-redir")then
return
end
type_name = "SSR"
-- [[ ShadowsocksR Libev ]]
s.fields["type"]:value(type_name, "ShadowsocksR Libev")
if s.val["type"] ~= type_name then
return
end
local option_prefix = "ssr_"
local function _n(name)
return option_prefix .. name
end
local ssr_encrypt_method_list = {
"none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb",
"aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
"cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20",
"chacha20-ietf"
}
local ssr_protocol_list = {
"origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple",
"auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5",
"auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c",
"auth_chain_d", "auth_chain_e", "auth_chain_f"
}
local ssr_obfs_list = {
"plain", "http_simple", "http_post", "random_head", "tls_simple",
"tls1.0_session_auth", "tls1.2_ticket_auth"
}
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
o = s:option(Value, _n("port"), translate("Port"))
o.datatype = "port"
o = s:option(Value, _n("password"), translate("Password"))
o.password = true
o = s:option(ListValue, _n("method"), translate("Encrypt Method"))
for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
for a, t in ipairs(ssr_protocol_list) do o:value(t) end
o = s:option(Value, _n("protocol_param"), translate("Protocol_param"))
o = s:option(ListValue, _n("obfs"), translate("Obfs"))
for a, t in ipairs(ssr_obfs_list) do o:value(t) end
o = s:option(Value, _n("obfs_param"), translate("Obfs_param"))
o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
o.default = 0
api.luci_types(arg[1], m, s, type_name, option_prefix)
if not api.is_finded("ssr-local") and not api.is_finded("ssr-redir")then
return
end
-- [[ ShadowsocksR Libev ]]
local m, s1 = ...
local type_name = "SSR"
s1.fields["type"]:value(type_name, "ShadowsocksR Libev")
if s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "ssr_"
local ssr_encrypt_method_list = {
"none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb",
"aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
"cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20",
"chacha20-ietf"
}
local ssr_protocol_list = {
"origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple",
"auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5",
"auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c",
"auth_chain_d", "auth_chain_e", "auth_chain_f"
}
local ssr_obfs_list = {
"plain", "http_simple", "http_post", "random_head", "tls_simple",
"tls1.0_session_auth", "tls1.2_ticket_auth"
}
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
o = s:option(Value, "port", translate("Port"))
o.datatype = "port"
o = s:option(Value, "password", translate("Password"))
o.password = true
o = s:option(ListValue, "method", translate("Encrypt Method"))
for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end
o = s:option(ListValue, "protocol", translate("Protocol"))
for a, t in ipairs(ssr_protocol_list) do o:value(t) end
o = s:option(Value, "protocol_param", translate("Protocol_param"))
o = s:option(ListValue, "obfs", translate("Obfs"))
for a, t in ipairs(ssr_obfs_list) do o:value(t) end
o = s:option(Value, "obfs_param", translate("Obfs_param"))
o = s:option(Value, "timeout", translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required"))
o.default = 0
api.luci_types(s1, s)
@@ -0,0 +1,131 @@
if not api.finded_com("hysteria") then
return
end
-- [[ Hysteria2 ]]
local m, s1 = ...
local type_name = "Hysteria2"
s1.fields["type"]:value(type_name, "Hysteria2")
if s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "hysteria2_"
local function _n(name)
return s.option_prefix .. name
end
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("udp", "UDP")
o = s:option(Value, "address", translate("Address (Support Domain Name)"))
o:depends({ realms = false })
o = s:option(Value, "port", translate("Port"))
o.datatype = "port"
o:depends({ realms = false })
o = s:option(Value, "hop", translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o.rewrite_option = _n(o.option)
o:depends({ realms = false })
o = s:option(Value, "hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5."))
o.datatype = "or(uinteger,portrange)"
o.placeholder = "30"
o.default = "30"
o.rewrite_option = _n(o.option)
o:depends({ realms = false })
o = s:option(Flag, "realms", translate("Realms"))
o.default = "0"
o.rewrite_option = _n(o.option)
o = s:option(Value, "realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o.rewrite_option = _n(o.option)
o:depends({ realms = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, "realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o.rewrite_option = _n(o.option)
o:depends({ realms = "1" })
o = s:option(Value, "auth_password", translate("Auth Password"))
o.password = true
o.rewrite_option = _n(o.option)
o = s:option(ListValue, "obfs_type", translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o.rewrite_option = _n(o.option)
o = s:option(Value, "obfs_password", translate("Obfs Password"))
o.rewrite_option = _n(o.option)
o:depends({ obfs_type = "salamander" })
o:depends({ obfs_type = "gecko" })
o = s:option(Value, "obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ obfs_type = "gecko" })
o.rewrite_option = _n(o.option)
o = s:option(Value, "obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ obfs_type = "gecko" })
o.rewrite_option = _n(o.option)
o = s:option(Flag, "fast_open", translate("Fast Open"))
o.default = "0"
o = s:option(Value, "tls_serverName", translate("Domain"))
o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
o = s:option(Value, "tls_pinSHA256", translate("PinSHA256"),translate("Certificate fingerprint"))
o = s:option(Value, "up_mbps", translate("Max upload Mbps"))
o.rewrite_option = _n(o.option)
o = s:option(Value, "down_mbps", translate("Max download Mbps"))
o.rewrite_option = _n(o.option)
o = s:option(Value, "recv_window", translate("QUIC stream receive window"))
o.rewrite_option = _n(o.option)
o = s:option(Value, "recv_window_conn", translate("QUIC connection receive window"))
o.rewrite_option = _n(o.option)
o = s:option(Value, "idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o.rewrite_option = _n(o.option)
o = s:option(Value, "keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o.rewrite_option = _n(o.option)
o = s:option(Flag, "disable_mtu_discovery", translate("Disable MTU detection"))
o.default = "0"
o.rewrite_option = _n(o.option)
o = s:option(Flag, "lazy_start", translate("Lazy Start"))
o.default = "0"
o.rewrite_option = _n(o.option)
api.luci_types(s1, s)
@@ -1,131 +0,0 @@
local m, s = ...
if not api.finded_com("hysteria") then
return
end
type_name = "Hysteria2"
-- [[ Hysteria2 ]]
s.fields["type"]:value(type_name, "Hysteria2")
if s.val["type"] ~= type_name then
return
end
local option_prefix = "hysteria2_"
local function _n(name)
return option_prefix .. name
end
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
o:value("udp", "UDP")
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
o:depends({ [_n("realms")] = false })
o = s:option(Value, _n("port"), translate("Port"))
o.datatype = "port"
o:depends({ [_n("realms")] = false })
o = s:option(Value, _n("hop"), translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o.rewrite_option = o.option
o:depends({ [_n("realms")] = false })
o = s:option(Value, _n("hop_interval"), translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5."))
o.datatype = "or(uinteger,portrange)"
o.placeholder = "30"
o.default = "30"
o.rewrite_option = o.option
o:depends({ [_n("realms")] = false })
o = s:option(Flag, _n("realms"), translate("Realms"))
o.default = "0"
o.rewrite_option = o.option
o = s:option(Value, _n("realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o.rewrite_option = o.option
o:depends({ [_n("realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o.rewrite_option = o.option
o:depends({ [_n("realms")] = "1" })
o = s:option(Value, _n("auth_password"), translate("Auth Password"))
o.password = true
o.rewrite_option = o.option
o = s:option(ListValue, _n("obfs_type"), translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o.rewrite_option = o.option
o = s:option(Value, _n("obfs_password"), translate("Obfs Password"))
o.rewrite_option = o.option
o:depends({ [_n("obfs_type")] = "salamander" })
o:depends({ [_n("obfs_type")] = "gecko" })
o = s:option(Value, _n("obfs_MinPacketSize"), translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ [_n("obfs_type")] = "gecko" })
o.rewrite_option = o.option
o = s:option(Value, _n("obfs_MaxPacketSize"), translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ [_n("obfs_type")] = "gecko" })
o.rewrite_option = o.option
o = s:option(Flag, _n("fast_open"), translate("Fast Open"))
o.default = "0"
o = s:option(Value, _n("tls_serverName"), translate("Domain"))
o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
o = s:option(Value, _n("tls_pinSHA256"), translate("PinSHA256"),translate("Certificate fingerprint"))
o = s:option(Value, _n("up_mbps"), translate("Max upload Mbps"))
o.rewrite_option = o.option
o = s:option(Value, _n("down_mbps"), translate("Max download Mbps"))
o.rewrite_option = o.option
o = s:option(Value, _n("recv_window"), translate("QUIC stream receive window"))
o.rewrite_option = o.option
o = s:option(Value, _n("recv_window_conn"), translate("QUIC connection receive window"))
o.rewrite_option = o.option
o = s:option(Value, _n("idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o.rewrite_option = o.option
o = s:option(Value, _n("keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o.rewrite_option = o.option
o = s:option(Flag, _n("disable_mtu_discovery"), translate("Disable MTU detection"))
o.default = "0"
o.rewrite_option = o.option
o = s:option(Flag, _n("lazy_start"), translate("Lazy Start"))
o.default = "0"
o.rewrite_option = o.option
api.luci_types(arg[1], m, s, type_name, option_prefix)
@@ -1,900 +0,0 @@
local m, s = ...
if not api.finded_com("xray") then
return
end
local jsonc = api.jsonc
type_name = "Xray"
-- [[ Xray ]]
s.fields["type"]:value(type_name, "Xray")
if not s.fields["type"].default then
s.fields["type"].default = type_name
end
if s.val["type"] ~= type_name then
return
end
local option_prefix = "xray_"
local function _n(name)
return option_prefix .. name
end
local formvalue_proto = luci.http.formvalue(formvalue_key .. _n("protocol"))
if formvalue_proto then s.val["protocol"] = formvalue_proto end
local arg_select_proto = luci.http.formvalue("select_proto") or ""
local ss_method_list = {
"aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305", "xchacha20-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
local security_list = { "auto", "aes-128-gcm", "chacha20-poly1305" }
local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
}
local xray_version = api.get_app_version("xray")
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
o:value("socks", translate("Socks"))
o:value("http", translate("HTTP"))
o:value("vmess", translate("Vmess"))
o:value("vless", translate("VLESS"))
o:value("shadowsocks", translate("Shadowsocks"))
o:value("trojan", translate("Trojan"))
o:value("wireguard", translate("WireGuard"))
if api.compare_versions(xray_version, ">=", "26.1.13") then
o:value("hysteria2", translate("Hysteria2"))
end
if api.compare_versions(xray_version, ">=", "1.8.12") then
o:value("_balancing", translate("Balancing"))
end
o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface"))
function o.custom_cfgvalue(self, section)
if arg_select_proto ~= "" then
return arg_select_proto
else
return m:get(section, self.option:sub(1 + #option_prefix))
end
end
local load_balancing_options = s.val["protocol"] == "_balancing" or arg_select_proto == "_balancing"
local load_shunt_options = s.val["protocol"] == "_shunt" or arg_select_proto == "_shunt"
local load_iface_options = s.val["protocol"] == "_iface" or arg_select_proto == "_iface"
local load_normal_options = true
if load_balancing_options or load_shunt_options or load_iface_options then
load_normal_options = nil
end
if not arg_select_proto:find("_") then
load_normal_options = true
end
local netdev_list = api.get_network_devices()
local node_list = api.get_node_list()
local fallback_list = {}
local is_balancer = nil
for k, e in ipairs(node_list.balancing_list or {}) do
if e.id ~= arg[1] then
fallback_list[#fallback_list + 1] = {
id = e["id"],
remark = e["remark"],
group = e["group"],
fallback = e.o["fallback_node"],
}
else
is_balancer = true
end
end
if load_balancing_options then -- [[ Load balancing Start ]]
o = s:option(ListValue, _n("node_add_mode"), translate("Node Addition Method"))
o:depends({ [_n("protocol")] = "_balancing" })
o.default = "manual"
o:value("manual", translate("Manual"))
o:value("batch", translate("Batch"))
o = s:option(MultiValue, _n("balancing_node"), translate("Load balancing node list"), translate("Load balancing node list, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
o:depends({ [_n("node_add_mode")] = "manual" })
o.widget = "checkbox"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
if k1 == "socks_list" or k1 == "normal_list" then
for i, v in ipairs(v1) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""
end
end
end
-- 读取旧 DynamicList
function o.custom_cfgvalue(self, section)
return table.concat(m:get(section, "balancing_node") or {}, " ")
end
-- 写入保持 DynamicList
function o.custom_write(self, section, value)
local old = m:get(section, "balancing_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m:set(section, "balancing_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m:set(section, "balancing_node", new)
return
end
end
o = s:option(MultiValue, _n("node_group"), translate("Select Group"))
o:depends({ [_n("node_add_mode")] = "batch" })
o.widget = "checkbox"
o:value("default", translate("default"))
for k, v in pairs(groups) do
o:value(api.UrlEncode(k), k)
end
o = s:option(Value, _n("node_match_rule"), translate("Node Matching Rules"))
o:depends({ [_n("node_add_mode")] = "batch" })
local descrStr = "Example: <code>^A && B && !C && D$</code><br>"
descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).<br>"
descrStr = descrStr .. "Conditions are joined by <code>&&</code> (AND), and their order does not affect the result.<br>"
descrStr = descrStr .. "Multiple groups can be separated by <code>||</code> (OR), matching succeeds if any group matches.<br>"
descrStr = descrStr .. "Example: <code>A && B || C && D</code> means (A AND B) OR (C AND D)."
o.description = translate(descrStr)
o = s:option(ListValue, _n("balancingStrategy"), translate("Balancing Strategy"))
o:depends({ [_n("protocol")] = "_balancing" })
o:value("random")
o:value("roundRobin")
o:value("leastPing")
o:value("leastLoad")
o.default = "random"
-- Fallback Node
o = s:option(ListValue, _n("fallback_node"), translate("Fallback Node"))
o.group = {"",""}
o:value("", translate("Close(Not use)"))
o:value("_direct", translate("Direct Connection"))
o:depends({ [_n("protocol")] = "_balancing" })
o.template = m:template_path("/cbi/nodes_listvalue")
-- 最大 fallback 套娃层数
local MAX_FALLBACK_DEPTH = 3
-- 检查是否会形成回环
local function will_loop(start_id, target_id, depth)
depth = depth or 0
-- 超过最大深度后停止递归
if depth >= MAX_FALLBACK_DEPTH then
return false
end
for _, v in ipairs(fallback_list) do
if v.id == target_id then
local fb = v.fallback
-- 没有 fallback
if not fb or fb == "" or fb == "_direct" then
return false
end
-- 检测到回环
if fb == start_id then
return true
end
-- 继续递归检查
return will_loop(start_id, fb, depth + 1)
end
end
return false
end
-- 获取 fallback 链深度
local function get_fallback_depth(id, depth)
depth = depth or 0
if depth >= MAX_FALLBACK_DEPTH then
return depth
end
for _, v in ipairs(fallback_list) do
if v.id == id then
local fb = v.fallback
if not fb or fb == "" or fb == "_direct" then
return depth
end
return get_fallback_depth(fb, depth + 1)
end
end
return depth
end
for _, v in ipairs(fallback_list) do
local depth = get_fallback_depth(v.id)
-- 超过最大套娃层数后,不允许继续选择 balancer
if depth < MAX_FALLBACK_DEPTH
and not will_loop(arg[1], v.id)
then
o:value(v.id, v.remark)
o.group[#o.group + 1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
for k1, v1 in pairs(node_list) do
if k1 == "socks_list" or k1 == "normal_list" or k1 == "urltest_list" then
for i, v in ipairs(v1) do
o:value(v.id, v.remark)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
end
-- 探测地址
o = s:option(Flag, _n("useCustomProbeUrl"), translate("Use Custom Probe URL"), translate("By default the built-in probe URL will be used, enable this option to use a custom probe URL."))
o:depends({ [_n("protocol")] = "_balancing" })
o = s:option(Value, _n("probeUrl"), translate("Probe URL"))
o:depends({ [_n("useCustomProbeUrl")] = true })
o:value("https://cp.cloudflare.com/", "Cloudflare")
o:value("https://www.gstatic.com/generate_204", "Gstatic")
o:value("https://www.google.com/generate_204", "Google")
o:value("https://www.youtube.com/generate_204", "YouTube")
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
o.default = o.keylist[3]
o.description = translate("The URL used to detect the connection status.")
-- 探测间隔
o = s:option(Value, _n("probeInterval"), translate("Probe Interval"))
o:depends({ [_n("protocol")] = "_balancing" })
o.default = "1m"
o.placeholder = "1m"
o.description = translate("The interval between initiating probes.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.")
o = s:option(Value, _n("expected"), translate("Preferred Node Count"))
o:depends({ [_n("balancingStrategy")] = "leastLoad" })
o.datatype = "uinteger"
o.default = "2"
o.placeholder = "2"
o.description = translate("The load balancer selects the optimal number of nodes, and traffic is randomly distributed among them.")
o = s:option(Value, _n("tolerance"), translate("Failure Tolerance (%)"))
o:depends({ [_n("balancingStrategy")] = "leastLoad" })
o.datatype = "uinteger"
o.default = "10"
o.placeholder = "10"
o.description = translate("The maximum acceptable speed test failure rate. For example, 1 means allowing a 1% failure rate.")
end -- [[ 负载均衡 End ]]
if load_iface_options then -- [[ 自定义接口 Start ]]
o = s:option(Value, _n("iface"), translate("Interface"))
o:depends({ [_n("protocol")] = "_iface" })
for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
end -- [[ 自定义接口 End ]]
if load_normal_options then
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
o = s:option(Value, _n("port"), translate("Port"))
o.datatype = "port"
o = s:option(Value, _n("uuid"), translate("ID"))
o.password = true
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o = s:option(Value, _n("username"), translate("Username"))
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "socks" })
o = s:option(Value, _n("password"), translate("Password"))
o.password = true
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
o = s:option(ListValue, _n("security"), translate("Encrypt Method"))
for a, t in ipairs(security_list) do o:value(t) end
o:depends({ [_n("protocol")] = "vmess" })
o = s:option(Value, _n("encryption"), translate("Encrypt Method") .. " (encryption)")
o.default = "none"
o.placeholder = "none"
o:depends({ [_n("protocol")] = "vless" })
o.validate = function(self, value)
value = api.trim(value)
return (value == "" and "none" or value)
end
o = s:option(ListValue, _n("ss_method"), translate("Encrypt Method"))
o.rewrite_option = "method"
for a, t in ipairs(ss_method_list) do o:value(t) end
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(ListValue, _n("flow"), translate("flow"))
o.default = ""
o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:value("xtls-rprx-vision-udp443")
o:depends({ [_n("protocol")] = "vless" })
---- [[hysteria2]]
o = s:option(Value, _n("hysteria2_hop"), translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o = s:option(Value, _n("hysteria2_hop_interval"), translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5."))
o.datatype = "or(uinteger,portrange)"
o.placeholder = "30"
o.default = "30"
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o = s:option(Flag, _n("hysteria2_realms"), translate("Realms"))
o.default = "0"
if api.compare_versions(xray_version, ">", "26.5.9") then
o:depends({ [_n("protocol")] = "hysteria2"})
else
o:depends({ [_n("protocol")] = "__hide"})
end
o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ [_n("hysteria2_realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ [_n("hysteria2_realms")] = "1" })
o = s:option(Value, _n("hysteria2_auth_password"), translate("Auth Password"))
o.password = true
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password"))
o:depends({ [_n("hysteria2_obfs_type")] = "salamander" })
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_obfs_MinPacketSize"), translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_obfs_MaxPacketSize"), translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Value, _n("hysteria2_keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Flag, _n("hysteria2_disable_mtu_discovery"), translate("Disable MTU detection"))
o.default = "0"
o:depends({ [_n("protocol")] = "hysteria2"})
---- [[hysteria2 end]]
o = s:option(Flag, _n("tls"), translate("TLS"))
o.default = 0
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(Flag, _n("reality"), translate("REALITY"), translate("Only recommend to use with VLESS-TCP-XTLS-Vision."))
o.default = 0
o:depends({ [_n("tls")] = true, [_n("transport")] = "raw" })
o:depends({ [_n("tls")] = true, [_n("transport")] = "ws" })
o:depends({ [_n("tls")] = true, [_n("transport")] = "grpc" })
o:depends({ [_n("tls")] = true, [_n("transport")] = "httpupgrade" })
o:depends({ [_n("tls")] = true, [_n("transport")] = "xhttp" })
o = s:option(ListValue, _n("alpn"), translate("alpn"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
-- o = s:option(Value, _n("minversion"), translate("minversion"))
-- o.default = "1.3"
-- o:value("1.3")
-- o:depends({ [_n("tls")] = true })
o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("tls_pinSHA256"), translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
o = s:option(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(TextValue, _n("tls_certificate_pem"), " ", translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ [_n("tls_certificate")] = true })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Flag, _n("ech"), translate("ECH"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o = s:option(TextValue, _n("ech_config"), translate("ECH Config"))
o.default = ""
o.rows = 5
o.wrap = "soft"
o:depends({ [_n("ech")] = true })
o.validate = function(self, value)
return api.trim(value:gsub("[\r\n]", ""))
end
-- [[ REALITY部分 ]] --
o = s:option(Value, _n("reality_publicKey"), translate("Public Key"))
o:depends({ [_n("tls")] = true, [_n("reality")] = true })
o = s:option(Value, _n("reality_shortId"), translate("Short Id"))
o:depends({ [_n("tls")] = true, [_n("reality")] = true })
o = s:option(Value, _n("reality_spiderX"), translate("Spider X"))
o.placeholder = "/"
o:depends({ [_n("tls")] = true, [_n("reality")] = true })
o = s:option(Flag, _n("utls"), translate("uTLS"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o = s:option(ListValue, _n("fingerprint"), translate("Finger Print"))
o:value("chrome")
o:value("firefox")
o:value("edge")
o:value("safari")
o:value("360")
o:value("qq")
o:value("ios")
o:value("android")
o:value("random")
o:value("randomized")
o:value("unsafe")
o.default = "chrome"
o:depends({ [_n("tls")] = true, [_n("utls")] = true })
o:depends({ [_n("tls")] = true, [_n("reality")] = true })
o = s:option(Value, _n("cipherSuites"), translate("Cipher Suites"), '<a href="https://go.dev/src/crypto/tls/cipher_suites.go#L44" target="_blank">***</a>' .. " " .. translate("Configures the list of supported cipher suites, separated by :"))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o = s:option(Flag, _n("use_mldsa65Verify"), translate("ML-DSA-65"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = true })
o = s:option(TextValue, _n("reality_mldsa65Verify"), "ML-DSA-65 " .. translate("Public key"))
o.default = ""
o.rows = 5
o.wrap = "soft"
o:depends({ [_n("use_mldsa65Verify")] = true })
o.validate = function(self, value)
return api.trim(value:gsub("[\r\n]", ""))
end
o = s:option(ListValue, _n("transport"), translate("Transport"))
o:value("raw", "RAW (TCP)")
o:value("mkcp", "mKCP")
o:value("ws", "WebSocket")
o:value("grpc", "gRPC")
o:value("httpupgrade", "HttpUpgrade")
o:value("xhttp", "XHTTP")
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
o = s:option(Value, _n("wireguard_public_key"), translate("Public Key"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_secret_key"), translate("Private Key"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_preSharedKey"), translate("Pre shared key"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(DynamicList, _n("wireguard_local_address"), translate("Local Address"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_mtu"), translate("MTU"))
o.default = "1420"
o:depends({ [_n("protocol")] = "wireguard" })
if api.compare_versions(xray_version, ">=", "1.8.0") then
o = s:option(Value, _n("wireguard_reserved"), translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings."))
o:depends({ [_n("protocol")] = "wireguard" })
end
o = s:option(Value, _n("wireguard_keepAlive"), translate("Keep Alive"))
o.default = "0"
o:depends({ [_n("protocol")] = "wireguard" })
-- [[ RAW部分 ]]--
-- TCP伪装
o = s:option(ListValue, _n("tcp_guise"), translate("Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
o:depends({ [_n("transport")] = "raw" })
-- HTTP域名
o = s:option(DynamicList, _n("tcp_guise_http_host"), translate("HTTP Host"))
o:depends({ [_n("tcp_guise")] = "http" })
-- HTTP路径
o = s:option(DynamicList, _n("tcp_guise_http_path"), translate("HTTP Path"))
o.placeholder = "/"
o:depends({ [_n("tcp_guise")] = "http" })
-- [[ mKCP部分 ]]--
o = s:option(ListValue, _n("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)<br />dns: Disguising traffic as DNS requests.'))
for a, t in ipairs(header_type_list) do o:value(t) end
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
o:depends({ [_n("mkcp_guise")] = "dns" })
o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU"))
o.datatype = "uinteger"
o.default = 1350
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed"))
o:depends({ [_n("transport")] = "mkcp" })
-- [[ WebSocket部分 ]]--
o = s:option(Value, _n("ws_host"), translate("WebSocket Host"))
o:depends({ [_n("transport")] = "ws" })
o = s:option(Value, _n("ws_path"), translate("WebSocket Path"))
o.placeholder = "/"
o:depends({ [_n("transport")] = "ws" })
o = s:option(Value, _n("ws_heartbeatPeriod"), translate("HeartbeatPeriod(second)"))
o.datatype = "integer"
o:depends({ [_n("transport")] = "ws" })
-- [[ gRPC部分 ]]--
o = s:option(Value, _n("grpc_serviceName"), "ServiceName")
o:depends({ [_n("transport")] = "grpc" })
o = s:option(ListValue, _n("grpc_mode"), "gRPC " .. translate("Transfer mode"))
o:value("gun")
o:value("multi")
o:depends({ [_n("transport")] = "grpc" })
o = s:option(Flag, _n("grpc_health_check"), translate("Health check"))
o:depends({ [_n("transport")] = "grpc" })
o = s:option(Value, _n("grpc_idle_timeout"), translate("Idle timeout"))
o.default = "10"
o:depends({ [_n("grpc_health_check")] = true })
o = s:option(Value, _n("grpc_health_check_timeout"), translate("Health check timeout"))
o.default = "20"
o:depends({ [_n("grpc_health_check")] = true })
o = s:option(Flag, _n("grpc_permit_without_stream"), translate("Permit without stream"))
o.default = "0"
o:depends({ [_n("grpc_health_check")] = true })
o = s:option(Value, _n("grpc_initial_windows_size"), translate("Initial Windows Size"))
o.default = "0"
o:depends({ [_n("transport")] = "grpc" })
-- [[ HttpUpgrade部分 ]]--
o = s:option(Value, _n("httpupgrade_host"), translate("HttpUpgrade Host"))
o:depends({ [_n("transport")] = "httpupgrade" })
o = s:option(Value, _n("httpupgrade_path"), translate("HttpUpgrade Path"))
o.placeholder = "/"
o:depends({ [_n("transport")] = "httpupgrade" })
-- [[ XHTTP部分 ]]--
o = s:option(ListValue, _n("xhttp_mode"), "XHTTP " .. translate("Mode"))
o:depends({ [_n("transport")] = "xhttp" })
o.default = "auto"
o:value("auto")
o:value("packet-up")
o:value("stream-up")
o:value("stream-one")
o = s:option(Value, _n("xhttp_host"), translate("XHTTP Host"))
o:depends({ [_n("transport")] = "xhttp" })
o = s:option(Value, _n("xhttp_path"), translate("XHTTP Path"))
o.placeholder = "/"
o:depends({ [_n("transport")] = "xhttp" })
o = s:option(Flag, _n("use_xhttp_extra"), translate("XHTTP Extra"))
o.default = "0"
o:depends({ [_n("transport")] = "xhttp" })
o = s:option(TextValue, _n("xhttp_extra"), " ", translate("An XHttpObject in JSON format, used for sharing."))
o:depends({ [_n("use_xhttp_extra")] = true })
o.rows = 10
o.wrap = "off"
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "xhttp_extra")
if raw then
return api.base64Decode(raw)
end
end
o.custom_write = function(self, section, value)
m:set(section, "xhttp_extra", api.base64Encode(value) or "")
local success, data = pcall(jsonc.parse, value)
if success and data then
local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address)
or (data.downloadSettings and data.downloadSettings.address)
if address and address ~= "" then
address = address:gsub("^%[", ""):gsub("%]$", "")
m:set(section, "download_address", address)
else
m:del(section, "download_address")
end
else
m:del(section, "download_address")
end
end
o.custom_remove = function(self, section, value)
m:del(section, "xhttp_extra")
m:del(section, "download_address")
end
-- [[ User-Agent ]]--
o = s:option(Value, _n("user_agent"), translate("User-Agent"))
o.default = ""
o:value("", translate("default"))
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge")
o:value("Go-http-client/1.1", "golang")
o:value("curl/7.68.0", "curl")
o:depends({ [_n("tcp_guise")] = "http" })
o:depends({ [_n("transport")] = "ws" })
o:depends({ [_n("transport")] = "httpupgrade" })
o:depends({ [_n("transport")] = "xhttp" })
o:depends({ [_n("transport")] = "grpc" })
-- [[ Mux.Cool ]]--
o = s:option(Flag, _n("mux"), "Mux", translate("Enable Mux.Cool"))
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "raw" })
o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "ws" })
o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "grpc" })
o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "httpupgrade" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
o = s:option(Value, _n("mux_concurrency"), translate("Mux concurrency"))
o.default = -1
o:depends({ [_n("mux")] = true })
o = s:option(Value, _n("xudp_concurrency"), translate("XUDP Mux concurrency"))
o.default = 8
o:depends({ [_n("mux")] = true })
--[[FinalMask]]
o = s:option(Flag, _n("use_finalmask"), "FinalMask")
o.default = "0"
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "wireguard" })
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o = s:option(TextValue, _n("finalmask"), " ")
o:depends({ [_n("use_finalmask")] = true })
o.rows = 10
o.wrap = "off"
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "FinalMask " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
return api.base64Decode(raw)
end
end
o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "")
end
--[[Fast Open]]
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
o.default = 0
--[[tcpMptcp]]
o = s:option(Flag, _n("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration."))
o.default = 0
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"))
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
o:value("", translate("Auto"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("https", "DoH")
o = s:option(Value, _n("domain_resolver_dns"), "DNS")
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = o.keylist[1]
o:depends({ [_n("domain_resolver")] = "tcp" })
o:depends({ [_n("domain_resolver")] = "udp" })
o = s:option(Value, _n("domain_resolver_dns_https"), "DNS")
o:value("https://120.53.53.53/dns-query", "DNSPod")
o:value("https://223.5.5.5/dns-query", "AliDNS")
o.default = o.keylist[1]
o:depends({ [_n("domain_resolver")] = "https" })
o = s:option(ListValue, _n("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
o.default = ""
o:value("", translate("Auto"))
o:value("UseIPv4v6", translate("Prefer IPv4"))
o:value("UseIPv6v4", translate("Prefer IPv6"))
o:value("UseIPv4", translate("IPv4 Only"))
o:value("UseIPv6", translate("IPv6 Only"))
o = s:option(Flag, _n("happy_eyeballs"), translate("Enable Happy Eyeballs"), translate("Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection."))
o.default = 0
local protocols = s.fields[_n("protocol")].keylist
if #protocols > 0 then
for i, v in ipairs(protocols) do
if not v:find("^_") then
local depends_condition = { [_n("protocol")] = v }
if v == "hysteria2" then
depends_condition[_n("hysteria2_realms")] = false
end
s.fields[_n("address")]:depends(depends_condition)
s.fields[_n("port")]:depends(depends_condition)
s.fields[_n("domain_resolver")]:depends(depends_condition)
s.fields[_n("happy_eyeballs")]:depends(depends_condition)
local strategy_depends = api.clone(depends_condition)
strategy_depends[_n("happy_eyeballs")] = false
s.fields[_n("domain_strategy")]:depends(strategy_depends)
if v ~= "hysteria2" then
s.fields[_n("tcp_fast_open")]:depends({ [_n("protocol")] = v })
s.fields[_n("tcpMptcp")]:depends({ [_n("protocol")] = v })
end
end
end
end
end
-- [[ Normal single node End ]]
if not load_shunt_options then
o = s:option(ListValue, _n("chain_proxy"), translate("Chain Proxy"))
o:value("", translate("Close(Not use)"))
if not (load_iface_options or load_balancing_options) then
-- Special node cannot be use pre-proxy.
o:value("1", translate("Preproxy Node"))
o:value("3", translate("Outbound Interface"))
end
o:value("2", translate("Landing Node"))
o1 = s:option(ListValue, _n("preproxy_node"), translate("Preproxy Node"), translate("Only support a layer of proxy."))
o1:depends({ [_n("chain_proxy")] = "1", [_n("hysteria2_realms")] = false })
o1.template = m:template_path("/cbi/nodes_listvalue")
o1.group = {}
o3 = s:option(Value, _n("outbound_iface"), translate("Outbound Interface"))
o3:depends({ [_n("chain_proxy")] = "3" })
o3:value("", translate("All"))
for _, d in ipairs(netdev_list) do
o3:value(d.name, d.label)
end
o2 = s:option(ListValue, _n("to_node"), translate("Landing Node"), translate("Only support a layer of proxy."))
o2:depends({ [_n("chain_proxy")] = "2", [_n("hysteria2_realms")] = false })
o2.template = m:template_path("/cbi/nodes_listvalue")
o2.group = {}
for k1, v1 in pairs(node_list) do
if k1 ~= "shunt_list" and k1 ~= "iface_list" then
for i, v in ipairs(v1) do
if v.id ~= arg[1] then
o1:value(v.id, v.remark)
o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
if k1 == "normal_list" then
-- Landing Node not support use special node.
o2:value(v.id, v.remark)
o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
end
end
end
end
api.luci_types(arg[1], m, s, type_name, option_prefix)
if load_shunt_options then
local current_node = m:get(arg[1]) or {}
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s, {
node_id = arg[1],
node = current_node,
node_list = node_list,
})
end
@@ -1,921 +0,0 @@
local m, s = ...
local singbox_bin = api.finded_com("sing-box")
if not singbox_bin then
return
end
type_name = "sing-box"
-- [[ sing-box ]]
s.fields["type"]:value(type_name, "Sing-Box")
if not s.fields["type"].default then
s.fields["type"].default = type_name
end
if s.val["type"] ~= type_name then
return
end
local option_prefix = "singbox_"
local function _n(name)
return option_prefix .. name
end
local formvalue_proto = luci.http.formvalue(formvalue_key .. _n("protocol"))
if formvalue_proto then s.val["protocol"] = formvalue_proto end
local arg_select_proto = luci.http.formvalue("select_proto") or ""
local ss_method_new_list = {
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
local ss_method_old_list = {
"aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5", "chacha20-ietf", "xchacha20",
}
local security_list = { "none", "auto", "aes-128-gcm", "chacha20-poly1305", "zero" }
local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'")
local singbox_version = api.get_app_version("sing-box"):match("[^v]+")
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
o:value("socks", "Socks")
o:value("http", "HTTP")
o:value("shadowsocks", "Shadowsocks")
o:value("vmess", "Vmess")
o:value("trojan", "Trojan")
if singbox_tags:find("with_wireguard") then
o:value("wireguard", "WireGuard")
end
if singbox_tags:find("with_quic") then
o:value("hysteria", "Hysteria")
end
o:value("vless", "VLESS")
if singbox_tags:find("with_quic") then
o:value("tuic", "TUIC")
end
if singbox_tags:find("with_quic") then
o:value("hysteria2", "Hysteria2")
end
o:value("anytls", "AnyTLS")
o:value("ssh", "SSH")
if singbox_tags:find("with_naive_outbound") then
o:value("naive", "NaïveProxy")
end
o:value("_urltest", translate("URLTest"))
o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface"))
function o.custom_cfgvalue(self, section)
if arg_select_proto ~= "" then
return arg_select_proto
else
return m:get(section, self.option:sub(1 + #option_prefix))
end
end
local load_urltest_options = s.val["protocol"] == "_urltest" or arg_select_proto == "_urltest"
local load_shunt_options = s.val["protocol"] == "_shunt" or arg_select_proto == "_shunt"
local load_iface_options = s.val["protocol"] == "_iface" or arg_select_proto == "_iface"
local load_normal_options = true
if load_urltest_options or load_shunt_options or load_iface_options then
load_normal_options = nil
end
if not arg_select_proto:find("_") then
load_normal_options = true
end
local netdev_list = api.get_network_devices()
local node_list = api.get_node_list()
if load_urltest_options then -- [[ URLTest Start ]]
o = s:option(ListValue, _n("node_add_mode"), translate("Node Addition Method"))
o:depends({ [_n("protocol")] = "_urltest" })
o.default = "manual"
o:value("manual", translate("Manual"))
o:value("batch", translate("Batch"))
o = s:option(MultiValue, _n("urltest_node"), translate("URLTest node list"), translate("List of nodes to test, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
o:depends({ [_n("node_add_mode")] = "manual" })
o.widget = "checkbox"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
if k1 == "socks_list" or k1 == "normal_list" then
for i, v in ipairs(v1) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""
end
end
end
-- 读取旧 DynamicList
function o.custom_cfgvalue(self, section)
return table.concat(m:get(section, "urltest_node") or {}, " ")
end
-- 写入保持 DynamicList
function o.custom_write(self, section, value)
local old = m:get(section, "urltest_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m:set(section, "urltest_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m:set(section, "urltest_node", new)
return
end
end
o = s:option(MultiValue, _n("node_group"), translate("Select Group"))
o:depends({ [_n("node_add_mode")] = "batch" })
o.widget = "checkbox"
o:value("default", translate("default"))
for k, v in pairs(groups) do
o:value(api.UrlEncode(k), k)
end
o = s:option(Value, _n("node_match_rule"), translate("Node Matching Rules"))
o:depends({ [_n("node_add_mode")] = "batch" })
local descrStr = "Example: <code>^A && B && !C && D$</code><br>"
descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).<br>"
descrStr = descrStr .. "Conditions are joined by <code>&&</code> (AND), and their order does not affect the result.<br>"
descrStr = descrStr .. "Multiple groups can be separated by <code>||</code> (OR), matching succeeds if any group matches.<br>"
descrStr = descrStr .. "Example: <code>A && B || C && D</code> means (A AND B) OR (C AND D)."
o.description = translate(descrStr)
o = s:option(Value, _n("urltest_url"), translate("Probe URL"))
o:depends({ [_n("protocol")] = "_urltest" })
o:value("https://cp.cloudflare.com/", "Cloudflare")
o:value("https://www.gstatic.com/generate_204", "Gstatic")
o:value("https://www.google.com/generate_204", "Google")
o:value("https://www.youtube.com/generate_204", "YouTube")
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
o.default = o.keylist[3]
o.description = translate("The URL used to detect the connection status.")
o = s:option(Value, _n("urltest_interval"), translate("Test interval"))
o:depends({ [_n("protocol")] = "_urltest" })
o.default = "3m"
o.placeholder = "3m"
o.description = translate("The interval between initiating probes.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.") .. "<br>" ..
translate("Test interval must be less or equal than idle timeout.")
o = s:option(Value, _n("urltest_tolerance"), translate("Test tolerance"), translate("The test tolerance in milliseconds."))
o:depends({ [_n("protocol")] = "_urltest" })
o.datatype = "uinteger"
o.placeholder = "50"
o.default = "50"
o = s:option(Value, _n("urltest_idle_timeout"), translate("Idle timeout"))
o:depends({ [_n("protocol")] = "_urltest" })
o.placeholder = "30m"
o.default = "30m"
o.description = translate("The idle timeout.") .. "<br>" ..
translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are <code>s</code>, <code>m</code>, <code>h</code>, which correspond to seconds, minutes, and hours, respectively.") .. "<br>" ..
translate("When the unit is not filled in, it defaults to seconds.")
o = s:option(Flag, _n("urltest_interrupt_exist_connections"), translate("Interrupt existing connections"))
o:depends({ [_n("protocol")] = "_urltest" })
o.default = "0"
o.description = translate("Interrupt existing connections when the selected outbound has changed.")
end -- [[ URLTest End ]]
if load_iface_options then -- [[ 自定义接口 Start ]]
o = s:option(Value, _n("iface"), translate("Interface"))
o:depends({ [_n("protocol")] = "_iface" })
for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
end
if load_normal_options then
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
o = s:option(Value, _n("port"), translate("Port"))
o.datatype = "port"
o = s:option(Value, _n("uuid"), translate("ID"))
o.password = true
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "tuic" })
o = s:option(Value, _n("username"), translate("Username"))
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "ssh" })
o:depends({ [_n("protocol")] = "naive" })
o = s:option(Value, _n("password"), translate("Password"))
o.password = true
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "anytls" })
o:depends({ [_n("protocol")] = "ssh" })
o:depends({ [_n("protocol")] = "naive" })
o = s:option(ListValue, _n("security"), translate("Encrypt Method"))
for a, t in ipairs(security_list) do o:value(t) end
o:depends({ [_n("protocol")] = "vmess" })
o = s:option(ListValue, _n("ss_method"), translate("Encrypt Method"))
o.rewrite_option = "method"
for a, t in ipairs(ss_method_new_list) do o:value(t) end
for a, t in ipairs(ss_method_old_list) do o:value(t) end
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(Flag, _n("uot"), translate("UDP over TCP"))
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "naive" })
o = s:option(Value, _n("alter_id"), "Alter ID")
o.datatype = "uinteger"
o.default = "0"
o:depends({ [_n("protocol")] = "vmess" })
o = s:option(Flag, _n("global_padding"), "global_padding", translate("Protocol parameter. Will waste traffic randomly if enabled."))
o.default = "0"
o:depends({ [_n("protocol")] = "vmess" })
o = s:option(Flag, _n("authenticated_length"), "authenticated_length", translate("Protocol parameter. Enable length block encryption."))
o.default = "0"
o:depends({ [_n("protocol")] = "vmess" })
o = s:option(ListValue, _n("flow"), translate("flow"))
o.default = ""
o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:depends({ [_n("protocol")] = "vless", [_n("tls")] = true })
if singbox_tags:find("with_quic") then
o = s:option(Value, _n("hysteria_hop"), translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_hop_interval"), translate("Hop Interval(second)"), translate("Example:") .. "30 (≥5)")
o.datatype = "uinteger"
o.placeholder = "30"
o.default = "30"
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_obfs"), translate("Obfs Password"))
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(ListValue, _n("hysteria_auth_type"), translate("Auth Type"))
o:value("disable", translate("Disable"))
o:value("string", translate("STRING"))
o:value("base64", translate("BASE64"))
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_auth_password"), translate("Auth Password"))
o.password = true
o:depends({ [_n("protocol")] = "hysteria", [_n("hysteria_auth_type")] = "string"})
o:depends({ [_n("protocol")] = "hysteria", [_n("hysteria_auth_type")] = "base64"})
o = s:option(Value, _n("hysteria_up_mbps"), translate("Max upload Mbps"))
o.default = "10"
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_down_mbps"), translate("Max download Mbps"))
o.default = "50"
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_recv_window_conn"), translate("QUIC stream receive window"))
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_recv_window"), translate("QUIC connection receive window"))
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Flag, _n("hysteria_disable_mtu_discovery"), translate("Disable MTU detection"))
o:depends({ [_n("protocol")] = "hysteria" })
end
if singbox_tags:find("with_quic") then
o = s:option(ListValue, _n("tuic_congestion_control"), translate("Congestion control algorithm"))
o.default = "cubic"
o:value("bbr", translate("BBR"))
o:value("cubic", translate("CUBIC"))
o:value("new_reno", translate("New Reno"))
o:depends({ [_n("protocol")] = "tuic" })
o = s:option(ListValue, _n("tuic_udp_relay_mode"), translate("UDP relay mode"))
o.default = "native"
o:value("native", translate("native"))
o:value("quic", translate("QUIC"))
o:depends({ [_n("protocol")] = "tuic" })
--[[
o = s:option(Flag, _n("tuic_udp_over_stream"), translate("UDP over stream"))
o:depends({ [_n("protocol")] = "tuic" })
]]--
o = s:option(Flag, _n("tuic_zero_rtt_handshake"), translate("Enable 0-RTT QUIC handshake"))
o.default = 0
o:depends({ [_n("protocol")] = "tuic" })
o = s:option(Value, _n("tuic_heartbeat"), translate("Heartbeat interval(second)"))
o.datatype = "uinteger"
o.default = "3"
o:depends({ [_n("protocol")] = "tuic" })
o = s:option(ListValue, _n("tuic_alpn"), translate("QUIC TLS ALPN"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:value("spdy/3.1")
o:value("h3,spdy/3.1")
o:depends({ [_n("protocol")] = "tuic" })
end
if singbox_tags:find("with_quic") then
o = s:option(Value, _n("hysteria2_hop"), translate("Port hopping range"))
o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).")
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o = s:option(Value, _n("hysteria2_hop_interval"), translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5."))
o.datatype = "or(uinteger,portrange)"
o.placeholder = "30"
o.default = "30"
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o = s:option(Flag, _n("hysteria2_realms"), translate("Realms"))
o.default = "0"
if api.compare_versions(singbox_version, ">=", "1.14.0") then
o:depends({ [_n("protocol")] = "hysteria2"})
else
o:depends({ [_n("protocol")] = "__hide"})
end
o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ [_n("hysteria2_realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ [_n("hysteria2_realms")] = "1" })
o = s:option(Value, _n("hysteria2_auth_password"), translate("Auth Password"))
o.password = true
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password"))
o:depends({ [_n("hysteria2_obfs_type")] = "salamander" })
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_obfs_MinPacketSize"), translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_obfs_MaxPacketSize"), translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Value, _n("hysteria2_keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Flag, _n("hysteria2_disable_mtu_discovery"), translate("Disable MTU detection"))
o.default = "0"
o:depends({ [_n("protocol")] = "hysteria2"})
end
-- [[ SSH config start ]] --
o = s:option(TextValue, _n("ssh_priv_key"), translate("Private Key"))
o.rows = 5
o.wrap = "off"
o:depends({ [_n("protocol")] = "ssh" })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase"))
o.password = true
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(DynamicList, _n("ssh_host_key"), translate("Host Key"), translate("Accept any if empty."))
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(DynamicList, _n("ssh_host_key_algo"), translate("Host Key Algorithms"))
o:depends({ [_n("protocol")] = "ssh" })
o = s:option(Value, _n("ssh_client_version"), translate("Client Version"), translate("Random version will be used if empty."))
o:depends({ [_n("protocol")] = "ssh" })
-- [[ SSH config end ]] --
-- [[ naive start ]] --
o = s:option(Value, _n("naive_insecure_concurrency"), translate("Concurrent Tunnels"))
o.datatype = "uinteger"
o.placeholder = "0"
o.default = "0"
o:depends({ [_n("protocol")] = "naive" })
o = s:option(Flag, _n("naive_quic"), translate("QUIC"))
o.default = 0
o:depends({ [_n("protocol")] = "naive" })
o = s:option(ListValue, _n("naive_congestion_control"), translate("Congestion control algorithm"))
o.default = "bbr"
o:value("bbr", translate("BBR"))
o:value("bbr2", translate("BBRv2"))
o:value("cubic", translate("CUBIC"))
o:value("reno", translate("New Reno"))
o:depends({ [_n("naive_quic")] = "1" })
-- [[ naive end ]] --
o = s:option(Flag, _n("tls"), translate("TLS"))
o.default = 0
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "anytls" })
o = s:option(ListValue, _n("alpn"), translate("ALPN"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Flag, _n("tls_disable_sni"), translate("Disable SNI"), translate("Do not send server name in ClientHello."))
o.default = "0"
o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria"})
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("tls_serverName"), "SNI " .. translate("Domain"))
o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria"})
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("protocol")] = "naive" })
o = s:option(Flag, _n("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
o:depends({ [_n("tls")] = true })
o:depends({ [_n("protocol")] = "hysteria"})
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Flag, _n("tls_certificate"), translate("TLS Certificate (PEM)"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria"})
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("protocol")] = "naive" })
o = s:option(TextValue, _n("tls_certificate_pem"), " ", translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ [_n("tls_certificate")] = true })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Value, _n("cipherSuites"), translate("Cipher Suites"), '<a href="https://go.dev/src/crypto/tls/cipher_suites.go#L44" target="_blank">***</a>' .. " " .. translate("Configures the list of supported cipher suites, separated by :"))
o:depends({ [_n("tls")] = true })
o = s:option(Flag, _n("ech"), translate("ECH"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("flow")] = "", [_n("reality")] = false })
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria" })
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o:depends({ [_n("protocol")] = "naive" })
o = s:option(TextValue, _n("ech_config"), translate("ECH Config"))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ [_n("ech")] = true })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Value, _n("ech_query_server_name"), translate("ECH Query Domain"), translate("Overrides the domain name used for ECH HTTPS record queries."))
o:depends({ [_n("ech")] = true })
if singbox_tags:find("with_utls") then
o = s:option(Flag, _n("utls"), translate("uTLS"))
o.default = "0"
o:depends({ [_n("tls")] = true })
o = s:option(ListValue, _n("fingerprint"), translate("Finger Print"))
o:value("chrome")
o:value("firefox")
o:value("edge")
o:value("safari")
o:value("360")
o:value("qq")
o:value("ios")
o:value("android")
o:value("random")
o:value("randomized")
o.default = "chrome"
o:depends({ [_n("utls")] = true })
-- [[ REALITY部分 ]] --
o = s:option(Flag, _n("reality"), translate("REALITY"))
o.default = 0
o:depends({ [_n("protocol")] = "vless", [_n("tls")] = true })
o:depends({ [_n("protocol")] = "vmess", [_n("tls")] = true })
o:depends({ [_n("protocol")] = "shadowsocks", [_n("tls")] = true })
o:depends({ [_n("protocol")] = "trojan", [_n("tls")] = true })
o:depends({ [_n("protocol")] = "anytls", [_n("tls")] = true })
o = s:option(Value, _n("reality_publicKey"), translate("Public Key"))
o:depends({ [_n("reality")] = true })
o = s:option(Value, _n("reality_shortId"), translate("Short Id"))
o:depends({ [_n("reality")] = true })
end
o = s:option(ListValue, _n("transport"), translate("Transport"))
o:value("tcp", "TCP")
o:value("http", "HTTP")
o:value("ws", "WebSocket")
o:value("httpupgrade", "HTTPUpgrade")
if singbox_tags:find("with_quic") then
o:value("quic", "QUIC")
end
if singbox_tags:find("with_grpc") then
o:value("grpc", "gRPC")
else o:value("grpc", "gRPC-lite")
end
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
if singbox_tags:find("with_wireguard") then
o = s:option(Value, _n("wireguard_public_key"), translate("Public Key"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_secret_key"), translate("Private Key"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_preSharedKey"), translate("Pre shared key"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(DynamicList, _n("wireguard_local_address"), translate("Local Address"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_mtu"), translate("MTU"))
o.default = "1420"
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_reserved"), translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings."))
o:depends({ [_n("protocol")] = "wireguard" })
end
-- [[ TCP部分(模拟) ]]--
o = s:option(ListValue, _n("tcp_guise"), translate("Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
o:depends({ [_n("transport")] = "tcp" })
o = s:option(DynamicList, _n("tcp_guise_http_host"), translate("HTTP Host"))
o:depends({ [_n("tcp_guise")] = "http" })
o = s:option(DynamicList, _n("tcp_guise_http_path"), translate("HTTP Path"))
o.placeholder = "/"
o:depends({ [_n("tcp_guise")] = "http" })
-- [[ HTTP部分 ]]--
o = s:option(DynamicList, _n("http_host"), translate("HTTP Host"))
o:depends({ [_n("transport")] = "http" })
o = s:option(Value, _n("http_path"), translate("HTTP Path"))
o.placeholder = "/"
o:depends({ [_n("transport")] = "http" })
o = s:option(Flag, _n("http_h2_health_check"), translate("Health check"))
o:depends({ [_n("tls")] = true, [_n("transport")] = "http" })
o = s:option(Value, _n("http_h2_read_idle_timeout"), translate("Idle timeout"))
o.default = "15"
o:depends({ [_n("http_h2_health_check")] = true })
o = s:option(Value, _n("http_h2_health_check_timeout"), translate("Health check timeout"))
o.default = "15"
o:depends({ [_n("http_h2_health_check")] = true })
-- [[ WebSocket部分 ]]--
o = s:option(Value, _n("ws_host"), translate("WebSocket Host"))
o:depends({ [_n("transport")] = "ws" })
o = s:option(Value, _n("ws_path"), translate("WebSocket Path"))
o.placeholder = "/"
o:depends({ [_n("transport")] = "ws" })
o = s:option(Flag, _n("ws_enableEarlyData"), translate("Enable early data"))
o:depends({ [_n("transport")] = "ws" })
o = s:option(Value, _n("ws_maxEarlyData"), translate("Early data length"))
o.default = "1024"
o:depends({ [_n("ws_enableEarlyData")] = true })
o = s:option(Value, _n("ws_earlyDataHeaderName"), translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol"))
o:depends({ [_n("ws_enableEarlyData")] = true })
-- [[ HTTPUpgrade部分 ]]--
o = s:option(Value, _n("httpupgrade_host"), translate("HTTPUpgrade Host"))
o:depends({ [_n("transport")] = "httpupgrade" })
o = s:option(Value, _n("httpupgrade_path"), translate("HTTPUpgrade Path"))
o.placeholder = "/"
o:depends({ [_n("transport")] = "httpupgrade" })
-- [[ gRPC部分 ]]--
o = s:option(Value, _n("grpc_serviceName"), "ServiceName")
o:depends({ [_n("transport")] = "grpc" })
o = s:option(Flag, _n("grpc_health_check"), translate("Health check"))
o:depends({ [_n("transport")] = "grpc" })
o = s:option(Value, _n("grpc_idle_timeout"), translate("Idle timeout"))
o.default = "15"
o:depends({ [_n("grpc_health_check")] = true })
o = s:option(Value, _n("grpc_health_check_timeout"), translate("Health check timeout"))
o.default = "15"
o:depends({ [_n("grpc_health_check")] = true })
o = s:option(Flag, _n("grpc_permit_without_stream"), translate("Permit without stream"))
o.default = "0"
o:depends({ [_n("grpc_health_check")] = true })
-- [[ User-Agent ]]--
o = s:option(Value, _n("user_agent"), translate("User-Agent"))
o.default = ""
o:value("", translate("default"))
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge")
o:value("Go-http-client/1.1", "golang")
o:value("curl/7.68.0", "curl")
o:depends({ [_n("tcp_guise")] = "http" })
o:depends({ [_n("transport")] = "http" })
o:depends({ [_n("transport")] = "ws" })
o:depends({ [_n("transport")] = "httpupgrade" })
o:depends({ [_n("protocol")] = "naive" })
-- [[ Mux ]]--
o = s:option(Flag, _n("mux"), translate("Mux"))
o.rmempty = false
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless", [_n("flow")] = "" })
o:depends({ [_n("protocol")] = "shadowsocks", [_n("uot")] = "" })
o:depends({ [_n("protocol")] = "trojan" })
o = s:option(ListValue, _n("mux_type"), translate("Mux"))
o:value("smux")
o:value("yamux")
o:value("h2mux")
o:depends({ [_n("mux")] = true })
o = s:option(Value, _n("mux_concurrency"), translate("Mux concurrency"))
o.default = 4
o:depends({ [_n("mux")] = true, [_n("tcpbrutal")] = false })
o = s:option(Flag, _n("mux_padding"), translate("Padding"))
o.default = 0
o:depends({ [_n("mux")] = true })
-- [[ TCP Brutal ]]--
o = s:option(Flag, _n("tcpbrutal"), translate("TCP Brutal"))
o.default = 0
o:depends({ [_n("mux")] = true })
o = s:option(Value, _n("tcpbrutal_up_mbps"), translate("Max upload Mbps"))
o.default = "10"
o:depends({ [_n("tcpbrutal")] = true })
o = s:option(Value, _n("tcpbrutal_down_mbps"), translate("Max download Mbps"))
o.default = "50"
o:depends({ [_n("tcpbrutal")] = true })
o = s:option(Flag, _n("shadowtls"), "ShadowTLS")
o.default = 0
o:depends({ [_n("protocol")] = "vmess", [_n("tls")] = false })
o:depends({ [_n("protocol")] = "shadowsocks", [_n("tls")] = false })
o = s:option(ListValue, _n("shadowtls_version"), "ShadowTLS " .. translate("Version"))
o.default = "1"
o:value("1", "ShadowTLS v1")
o:value("2", "ShadowTLS v2")
o:value("3", "ShadowTLS v3")
o:depends({ [_n("shadowtls")] = true })
o = s:option(Value, _n("shadowtls_password"), "ShadowTLS " .. translate("Password"))
o.password = true
o:depends({ [_n("shadowtls")] = true, [_n("shadowtls_version")] = "2" })
o:depends({ [_n("shadowtls")] = true, [_n("shadowtls_version")] = "3" })
o = s:option(Value, _n("shadowtls_serverName"), "ShadowTLS " .. translate("Domain"))
o:depends({ [_n("shadowtls")] = true })
if singbox_tags:find("with_utls") then
o = s:option(Flag, _n("shadowtls_utls"), "ShadowTLS " .. translate("uTLS"))
o.default = "0"
o:depends({ [_n("shadowtls")] = true })
o = s:option(ListValue, _n("shadowtls_fingerprint"), "ShadowTLS " .. translate("Finger Print"))
o:value("chrome")
o:value("firefox")
o:value("edge")
o:value("safari")
-- o:value("360")
o:value("qq")
o:value("ios")
-- o:value("android")
o:value("random")
-- o:value("randomized")
o.default = "chrome"
o:depends({ [_n("shadowtls")] = true, [_n("shadowtls_utls")] = true })
end
-- [[ SIP003 plugin ]]--
o = s:option(Flag, _n("plugin_enabled"), translate("plugin"))
o.default = 0
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(ListValue, _n("plugin"), "SIP003 " .. translate("plugin"))
o.default = "obfs-local"
o:depends({ [_n("plugin_enabled")] = true })
o:value("obfs-local")
o:value("v2ray-plugin")
o = s:option(Value, _n("plugin_opts"), translate("opts"))
o:depends({ [_n("plugin_enabled")] = true })
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"))
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
o:value("", translate("Auto"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("https", "DoH")
o = s:option(Value, _n("domain_resolver_dns"), "DNS")
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = o.keylist[1]
o:depends({ [_n("domain_resolver")] = "tcp" })
o:depends({ [_n("domain_resolver")] = "udp" })
o = s:option(Value, _n("domain_resolver_dns_https"), "DNS")
o:value("https://120.53.53.53/dns-query", "DNSPod")
o:value("https://223.5.5.5/dns-query", "AliDNS")
o.default = o.keylist[1]
o:depends({ [_n("domain_resolver")] = "https" })
o = s:option(ListValue, _n("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect."))
o.default = ""
o:value("", translate("Auto"))
o:value("prefer_ipv4", translate("Prefer IPv4"))
o:value("prefer_ipv6", translate("Prefer IPv6"))
o:value("ipv4_only", translate("IPv4 Only"))
o:value("ipv6_only", translate("IPv6 Only"))
local protocols = s.fields[_n("protocol")].keylist
if #protocols > 0 then
for i, v in ipairs(protocols) do
if not v:find("^_") then
local depends_condition = { [_n("protocol")] = v }
if v == "hysteria2" then
depends_condition[_n("hysteria2_realms")] = false
end
s.fields[_n("address")]:depends(depends_condition)
s.fields[_n("port")]:depends(depends_condition)
s.fields[_n("domain_resolver")]:depends(depends_condition)
s.fields[_n("domain_strategy")]:depends(depends_condition)
end
end
end
end
-- [[ Normal single node End ]]
if not load_shunt_options then
o = s:option(ListValue, _n("chain_proxy"), translate("Chain Proxy"))
o:value("", translate("Close(Not use)"))
if not (load_iface_options or load_urltest_options) then
-- Special node cannot be use pre-proxy.
o:value("1", translate("Preproxy Node"))
o:value("3", translate("Outbound Interface"))
end
o:value("2", translate("Landing Node"))
o1 = s:option(ListValue, _n("preproxy_node"), translate("Preproxy Node"), translate("Only support a layer of proxy."))
o1:depends({ [_n("chain_proxy")] = "1", [_n("hysteria2_realms")] = false })
o1.template = m:template_path("/cbi/nodes_listvalue")
o1.group = {}
o3 = s:option(Value, _n("outbound_iface"), translate("Outbound Interface"))
o3:depends({ [_n("chain_proxy")] = "3" })
o3:value("", translate("All"))
for _, d in ipairs(netdev_list) do
o3:value(d.name, d.label)
end
o2 = s:option(ListValue, _n("to_node"), translate("Landing Node"), translate("Only support a layer of proxy."))
o2:depends({ [_n("chain_proxy")] = "2", [_n("hysteria2_realms")] = false })
o2.template = m:template_path("/cbi/nodes_listvalue")
o2.group = {}
for k1, v1 in pairs(node_list) do
if k1 ~= "shunt_list" and k1 ~= "iface_list" then
for i, v in ipairs(v1) do
if v.id ~= arg[1] then
o1:value(v.id, v.remark)
o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
if k1 == "normal_list" then
-- Landing Node not support use special node.
o2:value(v.id, v.remark)
o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
end
end
end
end
end
api.luci_types(arg[1], m, s, type_name, option_prefix)
if load_shunt_options then
local current_node = m:get(arg[1]) or {}
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s, {
node_id = arg[1],
node = current_node,
node_list = node_list,
})
end
@@ -1,76 +0,0 @@
local m, s = ...
if not api.is_finded("sslocal") then
return
end
type_name = "SS-Rust"
-- [[ Shadowsocks Rust ]]
s.fields["type"]:value(type_name, "Shadowsocks Rust")
if s.val["type"] ~= type_name then
return
end
local option_prefix = "ssrust_"
local function _n(name)
return option_prefix .. name
end
local ssrust_encrypt_method_list = {
"none", "plain",
"aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
o = s:option(ListValue, _n("del_protocol")) --始终隐藏,用于删除 protocol
o:depends({ [_n("__hide")] = "1" })
o.rewrite_option = "protocol"
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
o = s:option(Value, _n("port"), translate("Port"))
o.datatype = "port"
o = s:option(Value, _n("password"), translate("Password"))
o.password = true
o = s:option(Value, _n("method"), translate("Encrypt Method"))
for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end
o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))
o.default = 0
o = s:option(Flag, _n("plugin_enabled"), translate("plugin"))
o.default = 0
o = s:option(Value, _n("plugin"), "SIP003 " .. translate("plugin"), translate("Supports custom SIP003 plugins, Make sure the plugin is installed."))
o.default = "none"
o:value("none", translate("none"))
if api.is_finded("xray-plugin") then o:value("xray-plugin") end
if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end
if api.is_finded("obfs-local") then o:value("obfs-local") end
if api.is_finded("shadow-tls") then o:value("shadow-tls") end
o:depends({ [_n("plugin_enabled")] = true })
o.validate = function(self, value, t)
if value and value ~= "" and value ~= "none" then
if not api.is_finded(value) then
return nil, value .. ": " .. translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(Value, _n("plugin_opts"), translate("opts"))
o:depends({ [_n("plugin_enabled")] = true })
api.luci_types(arg[1], m, s, type_name, option_prefix)
@@ -17,7 +17,7 @@ m:foreach("user", function(s)
user_list[#user_list + 1] = s
end)
s = m:section(NamedSection, arg[1], "server", translate("Server Config"))
local s = m:section(NamedSection, arg[1], "server", translate("Server Config"))
s.addremove = false
s.dynamic = false
@@ -39,8 +39,6 @@ for filename in api.fs.dir(types_dir) do
table.insert(type_table, filename)
end
table.sort(type_table, function(a, b)
if a == "socks.lua" then return true end
if b == "socks.lua" then return false end
return a < b
end)
@@ -0,0 +1,36 @@
if not api.is_finded("microsocks") then
return
end
-- [[ microsocks ]]
local m, s1 = ...
local type_name = "Socks"
s1.fields["type"]:value(type_name, "Socks")
if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "socks_"
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o = s:option(Flag, "auth", translate("Auth"))
o = s:option(ListValue, "user", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ auth = true })
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
api.luci_types(s1, s)
@@ -0,0 +1,522 @@
local singbox_bin = api.finded_com("sing-box")
if not singbox_bin then
return
end
-- [[ Sing-Box ]]
local m, s1 = ...
local type_name = "sing-box"
s1.fields["type"]:value(type_name, "Sing-Box")
if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
if not s1.val["type"] then
s1.val["type"] = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "singbox_"
local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'")
local ss_method_list = {
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
o = s:option(Flag, "custom", translate("Use Custom Config"))
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("mixed", "Mixed")
o:value("socks", "Socks")
o:value("http", "HTTP")
o:value("shadowsocks", "Shadowsocks")
o:value("vmess", "Vmess")
o:value("vless", "VLESS")
o:value("trojan", "Trojan")
o:value("naive", "Naive")
if singbox_tags:find("with_quic") then
o:value("hysteria", "Hysteria")
end
if singbox_tags:find("with_quic") then
o:value("tuic", "TUIC")
end
if singbox_tags:find("with_quic") then
o:value("hysteria2", "Hysteria2")
end
o:value("anytls", "AnyTLS")
if singbox_tags:find("with_wireguard") then
o:value("wireguard", "WireGuard")
end
o:value("direct", "Direct")
o:depends({ custom = false })
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(DynamicList, "users", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ protocol = "mixed" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "http" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "naive" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "anytls" })
o:depends({ protocol = "wireguard" })
if singbox_tags:find("with_quic") then
o = s:option(Value, "hysteria_obfs", translate("Obfs Password"))
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps"))
o.default = "100"
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps"))
o.default = "100"
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window"))
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_recv_window_client", translate("QUIC connection receive window"))
o:depends({ protocol = "hysteria" })
o = s:option(Value, "hysteria_max_conn_client", translate("QUIC concurrent bidirectional streams"))
o.default = "1024"
o:depends({ protocol = "hysteria" })
o = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection"))
o:depends({ protocol = "hysteria" })
end
if singbox_tags:find("with_quic") then
o = s:option(ListValue, "tuic_congestion_control", translate("Congestion control algorithm"))
o.default = "cubic"
o:value("bbr", translate("BBR"))
o:value("cubic", translate("CUBIC"))
o:value("new_reno", translate("New Reno"))
o:depends({ protocol = "tuic" })
o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake"))
o.default = 0
o:depends({ protocol = "tuic" })
o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)"))
o.datatype = "uinteger"
o.default = "3"
o:depends({ protocol = "tuic" })
o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:value("spdy/3.1")
o:value("h3,spdy/3.1")
o:depends({ protocol = "tuic" })
end
if singbox_tags:find("with_quic") then
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
o.default = "0"
o:depends({ protocol = "hysteria2"})
o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ hysteria2_realms = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ hysteria2_realms = "1" })
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password"))
o:depends({ hysteria2_obfs_type = "salamander" })
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control"), translate("Commands the client to use the BBR flow control algorithm"))
o.default = 0
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps"))
o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false })
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false })
end
o = s:option(ListValue, "d_protocol", translate("Destination protocol"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP")
o:depends({ protocol = "direct" })
o = s:option(Value, "d_address", translate("Destination address"))
o:depends({ protocol = "direct" })
o = s:option(Value, "d_port", translate("Destination port"))
o.datatype = "port"
o:depends({ protocol = "direct" })
o = s:option(Value, "decryption", translate("Encrypt Method"))
o.default = "none"
o:depends({ protocol = "vless" })
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
o.rewrite_option = "method"
for a, t in ipairs(ss_method_list) do o:value(t) end
o:depends({ protocol = "shadowsocks" })
o = s:option(Value, "ss_password", translate("Password"))
o:depends({ protocol = "shadowsocks" })
o = s:option(ListValue, "flow", translate("flow"))
o.default = ""
o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:depends({ protocol = "vless" , tls = true })
o = s:option(Flag, "tls", translate("TLS"))
o.default = 0
o.validate = function(self, value, t)
if value then
local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil
if reality and reality == "1" then return value end
if value == "1" then
local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or ""
local key = s.fields["tls_keyFile"] and s.fields["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
o:depends({ protocol = "http" })
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "anytls" })
-- https://github.com/SagerNet/sing-box/commit/d2a04c4e41e6cef0937331cb6d10211f431caaab
if singbox_tags:find("with_utls") then
-- [[ REALITY部分 ]] --
o = s:option(Flag, "reality", translate("REALITY"))
o.default = 0
o:depends({ protocol = "http", tls = true })
o:depends({ protocol = "vmess", tls = true })
o:depends({ protocol = "vless", tls = true })
o:depends({ protocol = "trojan", tls = true })
o:depends({ protocol = "anytls", tls = true })
o = s:option(Value, "reality_private_key", translate("Private Key"))
o:depends({ reality = true })
o = s:option(Value, "reality_shortId", translate("Short Id"))
o:depends({ reality = true })
o = s:option(Value, "reality_handshake_server", translate("Handshake Server"))
o.default = "google.com"
o:depends({ reality = true })
o = s:option(Value, "reality_handshake_server_port", translate("Handshake Server Port"))
o.datatype = "port"
o.default = "443"
o:depends({ reality = true })
end
o = s:option(ListValue, "alpn", translate("ALPN"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria" })
-- [[ TLS部分 ]] --
o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o:depends({ tls = true, reality = false })
o:depends({ protocol = "naive" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o:depends({ tls = true, reality = false })
o:depends({ protocol = "naive" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
o:depends({ tls = true, flow = "", reality = false })
o:depends({ protocol = "naive" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(TextValue, "ech_key", translate("ECH Key"))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ ech = true })
o.validate = function(self, value)
value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n")
value = value:gsub("^%s*\n", "")
if value:sub(-1) == "\n" then
value = value:sub(1, -2)
end
return value
end
o = s:option(ListValue, "transport", translate("Transport"))
o:value("tcp", "TCP")
o:value("http", "HTTP")
o:value("ws", "WebSocket")
o:value("httpupgrade", "HTTPUpgrade")
o:value("quic", "QUIC")
o:value("grpc", "gRPC")
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "trojan" })
-- [[ HTTP部分 ]]--
o = s:option(DynamicList, "http_host", translate("HTTP Host"))
o:depends({ transport = "http" })
o = s:option(Value, "http_path", translate("HTTP Path"))
o:depends({ transport = "http" })
-- [[ WebSocket部分 ]]--
o = s:option(Value, "ws_host", translate("WebSocket Host"))
o:depends({ transport = "ws" })
o = s:option(Value, "ws_path", translate("WebSocket Path"))
o:depends({ transport = "ws" })
-- [[ HTTPUpgrade部分 ]]--
o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host"))
o:depends({ transport = "httpupgrade" })
o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path"))
o:depends({ transport = "httpupgrade" })
-- [[ gRPC部分 ]]--
o = s:option(Value, "grpc_serviceName", "ServiceName")
o:depends({ transport = "grpc" })
-- [[ Mux ]]--
o = s:option(Flag, "mux", translate("Mux"))
o.rmempty = false
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless", flow = "" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
-- [[ TCP Brutal ]]--
o = s:option(Flag, "tcpbrutal", translate("TCP Brutal"))
o.default = 0
o:depends({ mux = true })
o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps"))
o.default = "10"
o:depends({ tcpbrutal = true })
o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps"))
o.default = "50"
o:depends({ tcpbrutal = true })
if singbox_tags:find("with_wireguard") then
o = s:option(Flag, "wireguard_system_interface", translate("System interface"))
o.default = 0
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_mtu", "MTU")
o.default = "1408"
o:depends({ protocol = "wireguard" })
o = s:option(DynamicList, "wireguard_local_address", translate("Local Address"))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_private_key", translate("Private Key"))
o.datatype = "base64"
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_public_key", translate("Public Key"))
o.datatype = "base64"
o:depends({ protocol = "wireguard" })
o = s:option(DummyValue, "gen_wireguard_key")
o.template = m:template_path("/server/gen_wireguard_key")
o:depends({ protocol = "wireguard" })
end
o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0"
o:depends({ custom = false })
o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!"))
o.default = "0"
o:depends({ custom = false })
local nodes_table = {}
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" and e.type == type_name then
nodes_table[#nodes_table + 1] = {
id = e[".name"],
remarks = e["remark"],
group = e["group"]
}
end
end
o = s:option(ListValue, "outbound_node", translate("outbound node"))
o:value("", translate("Close"))
o:value("_socks", translate("Custom Socks"))
o:value("_http", translate("Custom HTTP"))
o:value("_iface", translate("Custom Interface"))
o.template = m:template_path("/cbi/nodes_listvalue")
o.group = {"","","",""}
for k, v in pairs(nodes_table) do
o:value(v.id, v.remarks)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
o:depends({ custom = false })
o = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)"))
o:depends({ outbound_node = "_socks" })
o:depends({ outbound_node = "_http" })
o = s:option(Value, "outbound_node_port", translate("Port"))
o.datatype = "port"
o:depends({ outbound_node = "_socks" })
o:depends({ outbound_node = "_http" })
o = s:option(Value, "outbound_node_username", translate("Username"))
o:depends({ outbound_node = "_socks" })
o:depends({ outbound_node = "_http" })
o = s:option(Value, "outbound_node_password", translate("Password"))
o.password = true
o:depends({ outbound_node = "_socks" })
o:depends({ outbound_node = "_http" })
o = s:option(Value, "outbound_node_iface", translate("Interface"))
o:depends({ outbound_node = "_iface" })
local netdev_list = api.get_network_devices()
for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
o = s:option(ListValue, "loglevel", translate("Log Level"))
o.default = "info"
o:value("debug")
o:value("info")
o:value("warn")
o:value("error")
o:depends({ log = true })
api.luci_types(s1, s)
@@ -0,0 +1,536 @@
if not api.finded_com("xray") then
return
end
-- [[ Xray ]]
local m, s1 = ...
local type_name = "Xray"
s1.fields["type"]:value(type_name, "Xray")
if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "xray_"
local ss_method_list = {
"aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
}
o = s:option(Flag, "custom", translate("Use Custom Config"))
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("socks", "Socks")
o:value("http", "HTTP")
o:value("vmess", "Vmess")
o:value("vless", "VLESS")
o:value("shadowsocks", "Shadowsocks")
o:value("trojan", "Trojan")
o:value("hysteria2", "Hysteria2")
o:value("wireguard", "WireGuard")
o:value("dokodemo-door", "dokodemo-door")
o:depends({ custom = false })
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(DynamicList, "users", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "wireguard" })
o = s:option(ListValue, "d_protocol", translate("Destination protocol"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP")
o:depends({ protocol = "dokodemo-door" })
o = s:option(Value, "d_address", translate("Destination address"))
o:depends({ protocol = "dokodemo-door" })
o = s:option(Value, "d_port", translate("Destination port"))
o.datatype = "port"
o:depends({ protocol = "dokodemo-door" })
o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)")
o.default = "none"
o.placeholder = "none"
o:depends({ protocol = "vless" })
o.validate = function(self, value)
value = api.trim(value)
return (value == "" and "none" or value)
end
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
o.rewrite_option = "method"
for a, t in ipairs(ss_method_list) do o:value(t) end
o:depends({ protocol = "shadowsocks" })
o = s:option(Value, "ss_password", translate("Password"))
o:depends({ protocol = "shadowsocks" })
o = s:option(ListValue, "ss_network", translate("Transport"))
o.default = "tcp,udp"
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP")
o:depends({ protocol = "shadowsocks" })
o = s:option(Flag, "udp_forward", translate("UDP Forward"))
o.default = "1"
o.rmempty = false
o:depends({ protocol = "socks" })
o = s:option(ListValue, "flow", translate("flow"))
o.default = ""
o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:depends({ protocol = "vless" })
---- [[ hysteria2 ]]
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
o.default = "0"
o:depends({ protocol = "hysteria2"})
o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ hysteria2_realms = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ hysteria2_realms = "1" })
o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password"))
o:depends({ hysteria2_obfs_type = "salamander" })
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ hysteria2_obfs_type = "gecko" })
o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control"))
o.default = 0
o:depends({ protocol = "hysteria2" })
o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps"))
o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false })
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false })
---- [[ TLS ]]
o = s:option(Flag, "tls", translate("TLS"))
o.default = 0
o.validate = function(self, value, t)
if value then
local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil
if reality and reality == "1" then return value end
if value == "1" then
local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or ""
local key = s.fields["tls_keyFile"] and s.fields["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
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
-- [[ REALITY部分 ]] --
o = s:option(Flag, "reality", translate("REALITY"))
o.default = 0
o:depends({ tls = true })
o = s:option(Value, "reality_private_key", translate("Private Key"))
o:depends({ reality = true })
o = s:option(DynamicList, "reality_shortId", translate("Short Id"))
o:depends({ reality = true })
o = s:option(Value, "reality_dest", translate("Dest"))
o.default = "google.com:443"
o:depends({ reality = true })
o = s:option(DynamicList, "reality_serverNames", translate("serverNames"))
o:depends({ reality = true })
o = s:option(ListValue, "alpn", translate("alpn"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ tls = true, reality = false })
o = s:option(Flag, "use_mldsa65Seed", translate("ML-DSA-65"))
o.default = "0"
o:depends({ reality = true })
o = s:option(TextValue, "reality_mldsa65Seed", "ML-DSA-65 " .. translate("Private Key"))
o.default = ""
o.rows = 5
o.wrap = "soft"
o:depends({ use_mldsa65Seed = true })
o.validate = function(self, value)
return api.trim(value:gsub("[\r\n]", ""))
end
-- o = s:option(Value, "minversion", translate("minversion"))
-- o.default = "1.3"
-- o:value("1.3")
--o:depends({ tls = true })
-- [[ TLS部分 ]] --
o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2"})
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2"})
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
o:depends({ tls = true, reality = false })
o = s:option(TextValue, "ech_key", translate("ECH Key"))
o.default = ""
o.rows = 5
o.wrap = "soft"
o:depends({ ech = true })
o.validate = function(self, value)
return api.trim(value:gsub("[\r\n]", ""))
end
o = s:option(ListValue, "transport", translate("Transport"))
o:value("raw", "RAW")
o:value("mkcp", "mKCP")
o:value("ws", "WebSocket")
o:value("grpc", "gRPC")
o:value("httpupgrade", "HttpUpgrade")
o:value("xhttp", "XHTTP")
o:depends({ protocol = "vmess" })
o:depends({ protocol = "vless" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
-- [[ WebSocket部分 ]]--
o = s:option(Value, "ws_host", translate("WebSocket Host"))
o:depends({ transport = "ws" })
o = s:option(Value, "ws_path", translate("WebSocket Path"))
o:depends({ transport = "ws" })
-- [[ HttpUpgrade部分 ]]--
o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host"))
o:depends({ transport = "httpupgrade" })
o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path"))
o.placeholder = "/"
o:depends({ transport = "httpupgrade" })
-- [[ XHTTP部分 ]]--
o = s:option(Value, "xhttp_host", translate("XHTTP Host"))
o:depends({ transport = "xhttp" })
o = s:option(Value, "xhttp_path", translate("XHTTP Path"))
o.placeholder = "/"
o:depends({ transport = "xhttp" })
o = s:option(Value, "xhttp_maxuploadsize", translate("maxUploadSize"))
o.default = "1000000"
o:depends({ transport = "xhttp" })
o = s:option(Value, "xhttp_maxconcurrentuploads", translate("maxConcurrentUploads"))
o.default = "10"
o:depends({ transport = "xhttp" })
-- [[ TCP部分 ]]--
-- TCP伪装
o = s:option(ListValue, "tcp_guise", translate("Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
o:depends({ transport = "raw" })
-- HTTP域名
o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
o:depends({ tcp_guise = "http" })
-- HTTP路径
o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
o:depends({ tcp_guise = "http" })
-- [[ mKCP部分 ]]--
o = 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)<br />dns: Disguising traffic as DNS requests.'))
for a, t in ipairs(header_type_list) do o:value(t) end
o:depends({ transport = "mkcp" })
o = s:option(Value, "mkcp_domain", translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
o:depends({ mkcp_guise = "dns" })
o = s:option(Value, "mkcp_mtu", translate("KCP MTU"))
o.datatype = "uinteger"
o.default = 1350
o:depends({ transport = "mkcp" })
o = s:option(Value, "mkcp_seed", translate("KCP Seed"))
o:depends({ transport = "mkcp" })
-- [[ gRPC部分 ]]--
o = s:option(Value, "grpc_serviceName", "ServiceName")
o:depends({ transport = "grpc" })
--[[FinalMask]]
o = s:option(Flag, "use_finalmask", "FinalMask")
o.default = "0"
o:depends({ custom = false, protocol = "vmess" })
o:depends({ custom = false, protocol = "vless" })
o:depends({ custom = false, protocol = "trojan" })
o:depends({ custom = false, protocol = "shadowsocks" })
o:depends({ custom = false, protocol = "hysteria2", hysteria2_realms = false })
o = s:option(TextValue, "finalmask", "FinalMask JSON")
o:depends({ use_finalmask = true })
o.rows = 10
o.wrap = "off"
o.datatype = "json"
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
return api.base64Decode(raw)
end
end
o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "")
end
--[[acceptProxyProtocol]]
o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
o.default = "0"
o:depends({ custom = false })
--[[Fast Open]]
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ protocol = "vmess", custom = false })
o:depends({ protocol = "vless", custom = false })
o:depends({ protocol = "shadowsocks", custom = false })
o:depends({ protocol = "trojan", custom = false })
-- [[ Fallback部分 ]]--
o = s:option(Flag, "fallback", translate("Fallback"))
o:depends({ protocol = "vless", transport = "raw" })
o:depends({ protocol = "trojan", transport = "raw" })
--[[
o = s:option(Value, "fallback_alpn", "Fallback alpn")
o:depends({ fallback = true })
o = s:option(Value, "fallback_path", "Fallback path")
o:depends({ fallback = true })
o = s:option(Value, "fallback_dest", "Fallback dest")
o:depends({ fallback = true })
o = s:option(Value, "fallback_xver", "Fallback xver")
o.default = 0
o:depends({ fallback = true })
]]--
o = s:option(DynamicList, "fallback_list", "Fallback", translate("format: dest,path,xver"))
o:depends({ fallback = true })
-- Not supported at present
--[[
o = s:option(Flag, "wireguard_system_interface", translate("System interface"))
o.default = 0
o:depends({ protocol = "wireguard" })
]]--
o = s:option(Value, "wireguard_mtu", "MTU")
o.default = "1420"
o:depends({ protocol = "wireguard" })
-- Not supported at present
--[[
o = s:option(DynamicList, "wireguard_local_address", translate("Local Address"))
o:depends({ protocol = "wireguard" })
]]--
o = s:option(Value, "wireguard_private_key", translate("Private Key"))
o.datatype = "base64"
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_public_key", translate("Public Key"))
o.datatype = "base64"
o:depends({ protocol = "wireguard" })
o = s:option(DummyValue, "gen_wireguard_key")
o.template = m:template_path("/server/gen_wireguard_key")
o:depends({ protocol = "wireguard" })
o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0"
o:depends({ custom = false })
o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!"))
o.default = "0"
o:depends({ custom = false })
local nodes_table = {}
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" and e.type == type_name then
nodes_table[#nodes_table + 1] = {
id = e[".name"],
remarks = e["remark"],
group = e["group"]
}
end
end
o = s:option(ListValue, "outbound_node", translate("outbound node"))
o:value("", translate("Close"))
o:value("_socks", translate("Custom Socks"))
o:value("_http", translate("Custom HTTP"))
o:value("_iface", translate("Custom Interface"))
o.template = m:template_path("/cbi/nodes_listvalue")
o.group = {"","","",""}
for k, v in pairs(nodes_table) do
o:value(v.id, v.remarks)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
o:depends({ custom = false })
o = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)"))
o:depends({ outbound_node = "_socks"})
o:depends({ outbound_node = "_http"})
o = s:option(Value, "outbound_node_port", translate("Port"))
o.datatype = "port"
o:depends({ outbound_node = "_socks"})
o:depends({ outbound_node = "_http"})
o = s:option(Value, "outbound_node_username", translate("Username"))
o:depends({ outbound_node = "_socks"})
o:depends({ outbound_node = "_http"})
o = s:option(Value, "outbound_node_password", translate("Password"))
o.password = true
o:depends({ outbound_node = "_socks"})
o:depends({ outbound_node = "_http"})
o = s:option(Value, "outbound_node_iface", translate("Interface"))
o:depends({ outbound_node = "_iface"})
local netdev_list = api.get_network_devices()
for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
o = s:option(ListValue, "loglevel", translate("Log Level"))
o.default = "warning"
o:value("debug")
o:value("info")
o:value("warning")
o:value("error")
o:depends({ log = true })
api.luci_types(s1, s)
@@ -1,79 +1,79 @@
local m, s = ...
if not api.is_finded("ssserver") then
return
end
local type_name = "SS-Rust"
-- [[ Shadowsocks Rust ]]
s.fields["type"]:value(type_name, translate("Shadowsocks Rust"))
if s.val["type"] and s.val["type"] ~= type_name then
return
end
local option_prefix = "ssrust_"
local function _n(name)
return option_prefix .. name
end
local ssrust_encrypt_method_list = {
"plain", "none",
"aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
o = s:option(Value, _n("port"), translate("Listen Port"))
o.datatype = "port"
o:depends({ [_n("custom")] = false })
o = s:option(ListValue, _n("user"), translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ [_n("custom")] = false })
o = s:option(ListValue, _n("method"), translate("Encrypt Method"))
for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
o:depends({ [_n("custom")] = false })
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Flag, _n("log"), translate("Log"))
o.default = "1"
o.rmempty = false
api.luci_types(arg[1], m, s, type_name, option_prefix)
if not api.is_finded("ssserver") then
return
end
-- [[ Shadowsocks Rust ]]
local m, s1 = ...
local type_name = "SS-Rust"
s1.fields["type"]:value(type_name, translate("Shadowsocks Rust"))
if not s1.val["type"] then
s1.val["type"] = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "ssrust_"
local ssrust_encrypt_method_list = {
"plain", "none",
"aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
o = s:option(Flag, "custom", translate("Use Custom Config"))
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(ListValue, "user", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ custom = false })
o = s:option(ListValue, "method", translate("Encrypt Method"))
for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end
o:depends({ custom = false })
o = s:option(Value, "timeout", translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
o:depends({ custom = false })
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ custom = false })
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
api.luci_types(s1, s)
@@ -1,110 +1,110 @@
local m, s = ...
if not api.is_finded("ssr-server") then
return
end
local type_name = "SSR"
-- [[ ShadowsocksR ]]
s.fields["type"]:value(type_name, translate("ShadowsocksR"))
if s.val["type"] and s.val["type"] ~= type_name then
return
end
local option_prefix = "ssr_"
local function _n(name)
return option_prefix .. name
end
local ssr_encrypt_method_list = {
"none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb",
"aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
"cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20",
"chacha20-ietf"
}
local ssr_protocol_list = {
"origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple",
"auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5",
"auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c",
"auth_chain_d", "auth_chain_e", "auth_chain_f"
}
local ssr_obfs_list = {
"plain", "http_simple", "http_post", "random_head", "tls_simple",
"tls1.0_session_auth", "tls1.2_ticket_auth"
}
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
o = s:option(Value, _n("port"), translate("Listen Port"))
o.datatype = "port"
o:depends({ [_n("custom")] = false })
o = s:option(ListValue, _n("user"), translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ [_n("custom")] = false })
o = s:option(ListValue, _n("method"), translate("Encrypt Method"))
for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end
o:depends({ [_n("custom")] = false })
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
for a, t in ipairs(ssr_protocol_list) do o:value(t) end
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("protocol_param"), translate("Protocol_param"))
o:depends({ [_n("custom")] = false })
o = s:option(ListValue, _n("obfs"), translate("Obfs"))
for a, t in ipairs(ssr_obfs_list) do o:value(t) end
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("obfs_param"), translate("Obfs_param"))
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("timeout"), translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
o:depends({ [_n("custom")] = false })
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Flag, _n("udp_forward"), translate("UDP Forward"))
o.default = "1"
o.rmempty = false
o = s:option(Flag, _n("log"), translate("Log"))
o.default = "1"
o.rmempty = false
api.luci_types(arg[1], m, s, type_name, option_prefix)
if not api.is_finded("ssr-server") then
return
end
-- [[ ShadowsocksR ]]
local m, s1 = ...
local type_name = "SSR"
s1.fields["type"]:value(type_name, translate("ShadowsocksR"))
if not s1.val["type"] then
s1.val["type"] = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "ssr_"
local ssr_encrypt_method_list = {
"none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb",
"aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
"cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20",
"chacha20-ietf"
}
local ssr_protocol_list = {
"origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple",
"auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5",
"auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c",
"auth_chain_d", "auth_chain_e", "auth_chain_f"
}
local ssr_obfs_list = {
"plain", "http_simple", "http_post", "random_head", "tls_simple",
"tls1.0_session_auth", "tls1.2_ticket_auth"
}
o = s:option(Flag, "custom", translate("Use Custom Config"))
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(ListValue, "user", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ custom = false })
o = s:option(ListValue, "method", translate("Encrypt Method"))
for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end
o:depends({ custom = false })
o = s:option(ListValue, "protocol", translate("Protocol"))
for a, t in ipairs(ssr_protocol_list) do o:value(t) end
o:depends({ custom = false })
o = s:option(Value, "protocol_param", translate("Protocol_param"))
o:depends({ custom = false })
o = s:option(ListValue, "obfs", translate("Obfs"))
for a, t in ipairs(ssr_obfs_list) do o:value(t) end
o:depends({ custom = false })
o = s:option(Value, "obfs_param", translate("Obfs_param"))
o:depends({ custom = false })
o = s:option(Value, "timeout", translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 300
o:depends({ custom = false })
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ custom = false })
o = s:option(Flag, "udp_forward", translate("UDP Forward"))
o.default = "1"
o.rmempty = false
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
api.luci_types(s1, s)
@@ -0,0 +1,154 @@
if not api.finded_com("hysteria") then
return
end
-- [[ Hysteria2 ]]
local m, s1 = ...
local type_name = "Hysteria2"
s1.fields["type"]:value(type_name, "Hysteria2")
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
local s = NamedSection(m, arg[1], "server")
s.type_name = type_name
s.option_prefix = "hysteria2_"
local function _n(name)
return s.option_prefix .. name
end
o = s:option(Flag, "custom", translate("Use Custom Config"))
o = s:option(TextValue, "custom_config", translate("Custom Config"))
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(DynamicList, "users", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ custom = false })
o = s:option(Flag, "realms", translate("Realms"))
o.default = "0"
o.rewrite_option = _n(o.option)
o:depends({ custom = false })
o = s:option(Value, "realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o.rewrite_option = _n(o.option)
o:depends({ realms = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, "realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o.rewrite_option = _n(o.option)
o:depends({ realms = "1" })
o = s:option(ListValue, "obfs_type", translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o.rewrite_option = _n(o.option)
o:depends({ custom = false })
o = s:option(Value, "obfs_password", translate("Obfs Password"))
o.rewrite_option = _n(o.option)
o:depends({ obfs_type = "salamander" })
o:depends({ obfs_type = "gecko" })
o = s:option(Value, "obfs_MinPacketSize", translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ obfs_type = "gecko" })
o.rewrite_option = _n(o.option)
o = s:option(Value, "obfs_MaxPacketSize", translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ obfs_type = "gecko" })
o.rewrite_option = _n(o.option)
o = s:option(Flag, "udp", translate("UDP"))
o.default = "1"
o.rewrite_option = _n(o.option)
o:depends({ custom = false })
o = s:option(Value, "up_mbps", translate("Max upload Mbps"))
o.rewrite_option = _n(o.option)
o:depends({ custom = false })
o = s:option(Value, "down_mbps", translate("Max download Mbps"))
o.rewrite_option = _n(o.option)
o:depends({ custom = false })
o = s:option(Flag, "ignoreClientBandwidth", translate("ignoreClientBandwidth"))
o.default = "0"
o.rewrite_option = _n(o.option)
o:depends({ custom = false })
o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o:depends({ custom = false })
o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o:depends({ custom = false })
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
api.luci_types(s1, s)
@@ -1,156 +0,0 @@
local m, s = ...
if not api.finded_com("hysteria") then
return
end
local fs = api.fs
local type_name = "Hysteria2"
-- [[ Hysteria2 ]]
s.fields["type"]:value(type_name, "Hysteria2")
if s.val["type"] and s.val["type"] ~= type_name then
return
end
local option_prefix = "hysteria2_"
local function _n(name)
return option_prefix .. name
end
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
o = s:option(Value, _n("port"), translate("Listen Port"))
o.datatype = "port"
o:depends({ [_n("custom")] = false })
o = s:option(DynamicList, _n("users"), translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ [_n("custom")] = false })
o = s:option(Flag, _n("realms"), translate("Realms"))
o.default = "0"
o.rewrite_option = o.option
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o.rewrite_option = o.option
o:depends({ [_n("realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o.rewrite_option = o.option
o:depends({ [_n("realms")] = "1" })
o = s:option(ListValue, _n("obfs_type"), translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o.rewrite_option = o.option
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("obfs_password"), translate("Obfs Password"))
o.rewrite_option = o.option
o:depends({ [_n("obfs_type")] = "salamander" })
o:depends({ [_n("obfs_type")] = "gecko" })
o = s:option(Value, _n("obfs_MinPacketSize"), translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ [_n("obfs_type")] = "gecko" })
o.rewrite_option = o.option
o = s:option(Value, _n("obfs_MaxPacketSize"), translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ [_n("obfs_type")] = "gecko" })
o.rewrite_option = o.option
o = s:option(Flag, _n("udp"), translate("UDP"))
o.default = "1"
o.rewrite_option = o.option
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("up_mbps"), translate("Max upload Mbps"))
o.rewrite_option = o.option
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("down_mbps"), translate("Max download Mbps"))
o.rewrite_option = o.option
o:depends({ [_n("custom")] = false })
o = s:option(Flag, _n("ignoreClientBandwidth"), translate("ignoreClientBandwidth"))
o.default = "0"
o.rewrite_option = o.option
o:depends({ [_n("custom")] = false })
o = s:option(FileUpload, _n("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o:depends({ [_n("custom")] = false })
o = s:option(FileUpload, _n("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Flag, _n("log"), translate("Log"))
o.default = "1"
o.rmempty = false
api.luci_types(arg[1], m, s, type_name, option_prefix)
@@ -1,541 +0,0 @@
local m, s = ...
if not api.finded_com("xray") then
return
end
local type_name = "Xray"
-- [[ Xray ]]
s.fields["type"]:value(type_name, "Xray")
if not s.fields["type"].default then
s.fields["type"].default = type_name
end
if s.val["type"] and s.val["type"] ~= type_name then
return
end
local option_prefix = "xray_"
local function _n(name)
return option_prefix .. name
end
local x_ss_method_list = {
"aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
}
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
o:value("socks", "Socks")
o:value("http", "HTTP")
o:value("vmess", "Vmess")
o:value("vless", "VLESS")
o:value("shadowsocks", "Shadowsocks")
o:value("trojan", "Trojan")
o:value("hysteria2", "Hysteria2")
o:value("wireguard", "WireGuard")
o:value("dokodemo-door", "dokodemo-door")
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("port"), translate("Listen Port"))
o.datatype = "port"
o:depends({ [_n("custom")] = false })
o = s:option(DynamicList, _n("users"), translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(ListValue, _n("d_protocol"), translate("Destination protocol"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP")
o:depends({ [_n("protocol")] = "dokodemo-door" })
o = s:option(Value, _n("d_address"), translate("Destination address"))
o:depends({ [_n("protocol")] = "dokodemo-door" })
o = s:option(Value, _n("d_port"), translate("Destination port"))
o.datatype = "port"
o:depends({ [_n("protocol")] = "dokodemo-door" })
o = s:option(Value, _n("decryption"), translate("Encrypt Method") .. " (decryption)")
o.default = "none"
o.placeholder = "none"
o:depends({ [_n("protocol")] = "vless" })
o.validate = function(self, value)
value = api.trim(value)
return (value == "" and "none" or value)
end
o = s:option(ListValue, _n("x_ss_method"), translate("Encrypt Method"))
o.rewrite_option = "method"
for a, t in ipairs(x_ss_method_list) do o:value(t) end
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(Value, _n("ss_password"), translate("Password"))
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(ListValue, _n("ss_network"), translate("Transport"))
o.default = "tcp,udp"
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP")
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(Flag, _n("udp_forward"), translate("UDP Forward"))
o.default = "1"
o.rmempty = false
o:depends({ [_n("protocol")] = "socks" })
o = s:option(ListValue, _n("flow"), translate("flow"))
o.default = ""
o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:depends({ [_n("protocol")] = "vless" })
---- [[ hysteria2 ]]
o = s:option(Flag, _n("hysteria2_realms"), translate("Realms"))
o.default = "0"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ [_n("hysteria2_realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ [_n("hysteria2_realms")] = "1" })
o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password"))
o:depends({ [_n("hysteria2_obfs_type")] = "salamander" })
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_obfs_MinPacketSize"), translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_obfs_MaxPacketSize"), translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Flag, _n("hysteria2_ignore_client_bandwidth"), translate("Client BBR Flow Control"))
o.default = 0
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps"))
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_ignore_client_bandwidth")] = false })
o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps"))
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_ignore_client_bandwidth")] = false })
---- [[ TLS ]]
o = s:option(Flag, _n("tls"), translate("TLS"))
o.default = 0
o.validate = function(self, value, t)
if value then
local reality = s.fields[_n("reality")] and s.fields[_n("reality")]:formvalue(t) or nil
if reality and reality == "1" then return value end
if value == "1" then
local ca = s.fields[_n("tls_certificateFile")] and s.fields[_n("tls_certificateFile")]:formvalue(t) or ""
local key = s.fields[_n("tls_keyFile")] and s.fields[_n("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
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
-- [[ REALITY部分 ]] --
o = s:option(Flag, _n("reality"), translate("REALITY"))
o.default = 0
o:depends({ [_n("tls")] = true })
o = s:option(Value, _n("reality_private_key"), translate("Private Key"))
o:depends({ [_n("reality")] = true })
o = s:option(DynamicList, _n("reality_shortId"), translate("Short Id"))
o:depends({ [_n("reality")] = true })
o = s:option(Value, _n("reality_dest"), translate("Dest"))
o.default = "google.com:443"
o:depends({ [_n("reality")] = true })
o = s:option(DynamicList, _n("reality_serverNames"), translate("serverNames"))
o:depends({ [_n("reality")] = true })
o = s:option(ListValue, _n("alpn"), translate("alpn"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o = s:option(Flag, _n("use_mldsa65Seed"), translate("ML-DSA-65"))
o.default = "0"
o:depends({ [_n("reality")] = true })
o = s:option(TextValue, _n("reality_mldsa65Seed"), "ML-DSA-65 " .. translate("Private Key"))
o.default = ""
o.rows = 5
o.wrap = "soft"
o:depends({ [_n("use_mldsa65Seed")] = true })
o.validate = function(self, value)
return api.trim(value:gsub("[\r\n]", ""))
end
-- o = s:option(Value, _n("minversion"), translate("minversion"))
-- o.default = "1.3"
-- o:value("1.3")
--o:depends({ [_n("tls")] = true })
-- [[ TLS部分 ]] --
o = s:option(FileUpload, _n("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2"})
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(FileUpload, _n("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2"})
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(Flag, _n("ech"), translate("ECH"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o = s:option(TextValue, _n("ech_key"), translate("ECH Key"))
o.default = ""
o.rows = 5
o.wrap = "soft"
o:depends({ [_n("ech")] = true })
o.validate = function(self, value)
return api.trim(value:gsub("[\r\n]", ""))
end
o = s:option(ListValue, _n("transport"), translate("Transport"))
o:value("raw", "RAW")
o:value("mkcp", "mKCP")
o:value("ws", "WebSocket")
o:value("grpc", "gRPC")
o:value("httpupgrade", "HttpUpgrade")
o:value("xhttp", "XHTTP")
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
-- [[ WebSocket部分 ]]--
o = s:option(Value, _n("ws_host"), translate("WebSocket Host"))
o:depends({ [_n("transport")] = "ws" })
o = s:option(Value, _n("ws_path"), translate("WebSocket Path"))
o:depends({ [_n("transport")] = "ws" })
-- [[ HttpUpgrade部分 ]]--
o = s:option(Value, _n("httpupgrade_host"), translate("HttpUpgrade Host"))
o:depends({ [_n("transport")] = "httpupgrade" })
o = s:option(Value, _n("httpupgrade_path"), translate("HttpUpgrade Path"))
o.placeholder = "/"
o:depends({ [_n("transport")] = "httpupgrade" })
-- [[ XHTTP部分 ]]--
o = s:option(Value, _n("xhttp_host"), translate("XHTTP Host"))
o:depends({ [_n("transport")] = "xhttp" })
o = s:option(Value, _n("xhttp_path"), translate("XHTTP Path"))
o.placeholder = "/"
o:depends({ [_n("transport")] = "xhttp" })
o = s:option(Value, _n("xhttp_maxuploadsize"), translate("maxUploadSize"))
o.default = "1000000"
o:depends({ [_n("transport")] = "xhttp" })
o = s:option(Value, _n("xhttp_maxconcurrentuploads"), translate("maxConcurrentUploads"))
o.default = "10"
o:depends({ [_n("transport")] = "xhttp" })
-- [[ TCP部分 ]]--
-- TCP伪装
o = s:option(ListValue, _n("tcp_guise"), translate("Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
o:depends({ [_n("transport")] = "raw" })
-- HTTP域名
o = s:option(DynamicList, _n("tcp_guise_http_host"), translate("HTTP Host"))
o:depends({ [_n("tcp_guise")] = "http" })
-- HTTP路径
o = s:option(DynamicList, _n("tcp_guise_http_path"), translate("HTTP Path"))
o:depends({ [_n("tcp_guise")] = "http" })
-- [[ mKCP部分 ]]--
o = s:option(ListValue, _n("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)<br />dns: Disguising traffic as DNS requests.'))
for a, t in ipairs(header_type_list) do o:value(t) end
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
o:depends({ [_n("mkcp_guise")] = "dns" })
o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU"))
o.datatype = "uinteger"
o.default = 1350
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed"))
o:depends({ [_n("transport")] = "mkcp" })
-- [[ gRPC部分 ]]--
o = s:option(Value, _n("grpc_serviceName"), "ServiceName")
o:depends({ [_n("transport")] = "grpc" })
--[[FinalMask]]
o = s:option(Flag, _n("use_finalmask"), "FinalMask")
o.default = "0"
o:depends({ [_n("custom")] = false, [_n("protocol")] = "vmess" })
o:depends({ [_n("custom")] = false, [_n("protocol")] = "vless" })
o:depends({ [_n("custom")] = false, [_n("protocol")] = "trojan" })
o:depends({ [_n("custom")] = false, [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("custom")] = false, [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o = s:option(TextValue, _n("finalmask"), "FinalMask JSON")
o:depends({ [_n("use_finalmask")] = true })
o.rows = 10
o.wrap = "off"
o.datatype = "json"
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
return api.base64Decode(raw)
end
end
o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "")
end
--[[acceptProxyProtocol]]
o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
o.default = "0"
o:depends({ [_n("custom")] = false })
--[[Fast Open]]
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ [_n("protocol")] = "vmess", [_n("custom")] = false })
o:depends({ [_n("protocol")] = "vless", [_n("custom")] = false })
o:depends({ [_n("protocol")] = "shadowsocks", [_n("custom")] = false })
o:depends({ [_n("protocol")] = "trojan", [_n("custom")] = false })
-- [[ Fallback部分 ]]--
o = s:option(Flag, _n("fallback"), translate("Fallback"))
o:depends({ [_n("protocol")] = "vless", [_n("transport")] = "raw" })
o:depends({ [_n("protocol")] = "trojan", [_n("transport")] = "raw" })
--[[
o = s:option(Value, _n("fallback_alpn"), "Fallback alpn")
o:depends({ [_n("fallback")] = true })
o = s:option(Value, _n("fallback_path"), "Fallback path")
o:depends({ [_n("fallback")] = true })
o = s:option(Value, _n("fallback_dest"), "Fallback dest")
o:depends({ [_n("fallback")] = true })
o = s:option(Value, _n("fallback_xver"), "Fallback xver")
o.default = 0
o:depends({ [_n("fallback")] = true })
]]--
o = s:option(DynamicList, _n("fallback_list"), "Fallback", translate("format: dest,path,xver"))
o:depends({ [_n("fallback")] = true })
-- Not supported at present
--[[
o = s:option(Flag, _n("wireguard_system_interface"), translate("System interface"))
o.default = 0
o:depends({ [_n("protocol")] = "wireguard" })
]]--
o = s:option(Value, _n("wireguard_mtu"), "MTU")
o.default = "1420"
o:depends({ [_n("protocol")] = "wireguard" })
-- Not supported at present
--[[
o = s:option(DynamicList, _n("wireguard_local_address"), translate("Local Address"))
o:depends({ [_n("protocol")] = "wireguard" })
]]--
o = s:option(Value, _n("wireguard_private_key"), translate("Private Key"))
o.datatype = "base64"
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_public_key"), translate("Public Key"))
o.datatype = "base64"
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(DummyValue, _n("gen_wireguard_key"))
o.prefix = option_prefix
o.template = m:template_path("/server/server_wireguard")
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Flag, _n("bind_local"), translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0"
o:depends({ [_n("custom")] = false })
o = s:option(Flag, _n("accept_lan"), translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!"))
o.default = "0"
o:depends({ [_n("custom")] = false })
local nodes_table = {}
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" and e.type == type_name then
nodes_table[#nodes_table + 1] = {
id = e[".name"],
remarks = e["remark"],
group = e["group"]
}
end
end
o = s:option(ListValue, _n("outbound_node"), translate("outbound node"))
o:value("", translate("Close"))
o:value("_socks", translate("Custom Socks"))
o:value("_http", translate("Custom HTTP"))
o:value("_iface", translate("Custom Interface"))
o.template = m:template_path("/cbi/nodes_listvalue")
o.group = {"","","",""}
for k, v in pairs(nodes_table) do
o:value(v.id, v.remarks)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("outbound_node_address"), translate("Address (Support Domain Name)"))
o:depends({ [_n("outbound_node")] = "_socks"})
o:depends({ [_n("outbound_node")] = "_http"})
o = s:option(Value, _n("outbound_node_port"), translate("Port"))
o.datatype = "port"
o:depends({ [_n("outbound_node")] = "_socks"})
o:depends({ [_n("outbound_node")] = "_http"})
o = s:option(Value, _n("outbound_node_username"), translate("Username"))
o:depends({ [_n("outbound_node")] = "_socks"})
o:depends({ [_n("outbound_node")] = "_http"})
o = s:option(Value, _n("outbound_node_password"), translate("Password"))
o.password = true
o:depends({ [_n("outbound_node")] = "_socks"})
o:depends({ [_n("outbound_node")] = "_http"})
o = s:option(Value, _n("outbound_node_iface"), translate("Interface"))
o:depends({ [_n("outbound_node")] = "_iface"})
local netdev_list = api.get_network_devices()
for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Flag, _n("log"), translate("Log"))
o.default = "1"
o.rmempty = false
o = s:option(ListValue, _n("loglevel"), translate("Log Level"))
o.default = "warning"
o:value("debug")
o:value("info")
o:value("warning")
o:value("error")
o:depends({ [_n("log")] = true })
api.luci_types(arg[1], m, s, type_name, option_prefix)
@@ -1,523 +0,0 @@
local m, s = ...
local singbox_bin = api.finded_com("sing-box")
if not singbox_bin then
return
end
local type_name = "sing-box"
-- [[ Sing-Box ]]
s.fields["type"]:value(type_name, "Sing-Box")
if not s.fields["type"].default then
s.fields["type"].default = type_name
end
if s.val["type"] and s.val["type"] ~= type_name then
return
end
local option_prefix = "singbox_"
local function _n(name)
return option_prefix .. name
end
local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'")
local ss_method_list = {
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
o = s:option(Flag, _n("custom"), translate("Use Custom Config"))
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
o:value("mixed", "Mixed")
o:value("socks", "Socks")
o:value("http", "HTTP")
o:value("shadowsocks", "Shadowsocks")
o:value("vmess", "Vmess")
o:value("vless", "VLESS")
o:value("trojan", "Trojan")
o:value("naive", "Naive")
if singbox_tags:find("with_quic") then
o:value("hysteria", "Hysteria")
end
if singbox_tags:find("with_quic") then
o:value("tuic", "TUIC")
end
if singbox_tags:find("with_quic") then
o:value("hysteria2", "Hysteria2")
end
o:value("anytls", "AnyTLS")
if singbox_tags:find("with_wireguard") then
o:value("wireguard", "WireGuard")
end
o:value("direct", "Direct")
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("port"), translate("Listen Port"))
o.datatype = "port"
o:depends({ [_n("custom")] = false })
o = s:option(DynamicList, _n("users"), translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ [_n("protocol")] = "mixed" })
o:depends({ [_n("protocol")] = "socks" })
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "naive" })
o:depends({ [_n("protocol")] = "hysteria" })
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2" })
o:depends({ [_n("protocol")] = "anytls" })
o:depends({ [_n("protocol")] = "wireguard" })
if singbox_tags:find("with_quic") then
o = s:option(Value, _n("hysteria_obfs"), translate("Obfs Password"))
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_up_mbps"), translate("Max upload Mbps"))
o.default = "100"
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_down_mbps"), translate("Max download Mbps"))
o.default = "100"
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_recv_window_conn"), translate("QUIC stream receive window"))
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_recv_window_client"), translate("QUIC connection receive window"))
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Value, _n("hysteria_max_conn_client"), translate("QUIC concurrent bidirectional streams"))
o.default = "1024"
o:depends({ [_n("protocol")] = "hysteria" })
o = s:option(Flag, _n("hysteria_disable_mtu_discovery"), translate("Disable MTU detection"))
o:depends({ [_n("protocol")] = "hysteria" })
end
if singbox_tags:find("with_quic") then
o = s:option(ListValue, _n("tuic_congestion_control"), translate("Congestion control algorithm"))
o.default = "cubic"
o:value("bbr", translate("BBR"))
o:value("cubic", translate("CUBIC"))
o:value("new_reno", translate("New Reno"))
o:depends({ [_n("protocol")] = "tuic" })
o = s:option(Flag, _n("tuic_zero_rtt_handshake"), translate("Enable 0-RTT QUIC handshake"))
o.default = 0
o:depends({ [_n("protocol")] = "tuic" })
o = s:option(Value, _n("tuic_heartbeat"), translate("Heartbeat interval(second)"))
o.datatype = "uinteger"
o.default = "3"
o:depends({ [_n("protocol")] = "tuic" })
o = s:option(ListValue, _n("tuic_alpn"), translate("QUIC TLS ALPN"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:value("spdy/3.1")
o:value("h3,spdy/3.1")
o:depends({ [_n("protocol")] = "tuic" })
end
if singbox_tags:find("with_quic") then
o = s:option(Flag, _n("hysteria2_realms"), translate("Realms"))
o.default = "0"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Value, _n("hysteria2_realm_url"), translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ [_n("hysteria2_realms")] = "1" })
o.validate = function(self, value)
value = api.trim(value)
local realm = api.parse_realm_uri(value)
if realm then return value end
return nil, translate("Invalid Realm URL.")
end
o = s:option(DynamicList, _n("hysteria2_realm_stun"), translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends({ [_n("hysteria2_realms")] = "1" })
o = s:option(ListValue, _n("hysteria2_obfs_type"), translate("Obfs Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_obfs_password"), translate("Obfs Password"))
o:depends({ [_n("hysteria2_obfs_type")] = "salamander" })
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_obfs_MinPacketSize"), translate("Gecko Packet Size (min)"))
o.datatype = "uinteger"
o.placeholder = "512"
o.default = "512"
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Value, _n("hysteria2_obfs_MaxPacketSize"), translate("Gecko Packet Size (max)"))
o.datatype = "uinteger"
o.placeholder = "1200"
o.default = "1200"
o:depends({ [_n("hysteria2_obfs_type")] = "gecko" })
o = s:option(Flag, _n("hysteria2_ignore_client_bandwidth"), translate("Client BBR Flow Control"), translate("Commands the client to use the BBR flow control algorithm"))
o.default = 0
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps"))
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_ignore_client_bandwidth")] = false })
o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps"))
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_ignore_client_bandwidth")] = false })
end
o = s:option(ListValue, _n("d_protocol"), translate("Destination protocol"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP")
o:depends({ [_n("protocol")] = "direct" })
o = s:option(Value, _n("d_address"), translate("Destination address"))
o:depends({ [_n("protocol")] = "direct" })
o = s:option(Value, _n("d_port"), translate("Destination port"))
o.datatype = "port"
o:depends({ [_n("protocol")] = "direct" })
o = s:option(Value, _n("decryption"), translate("Encrypt Method"))
o.default = "none"
o:depends({ [_n("protocol")] = "vless" })
o = s:option(ListValue, _n("ss_method"), translate("Encrypt Method"))
o.rewrite_option = "method"
for a, t in ipairs(ss_method_list) do o:value(t) end
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(Value, _n("ss_password"), translate("Password"))
o:depends({ [_n("protocol")] = "shadowsocks" })
o = s:option(ListValue, _n("flow"), translate("flow"))
o.default = ""
o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:depends({ [_n("protocol")] = "vless" , [_n("tls")] = true })
o = s:option(Flag, _n("tls"), translate("TLS"))
o.default = 0
o.validate = function(self, value, t)
if value then
local reality = s.fields[_n("reality")] and s.fields[_n("reality")]:formvalue(t) or nil
if reality and reality == "1" then return value end
if value == "1" then
local ca = s.fields[_n("tls_certificateFile")] and s.fields[_n("tls_certificateFile")]:formvalue(t) or ""
local key = s.fields[_n("tls_keyFile")] and s.fields[_n("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
o:depends({ [_n("protocol")] = "http" })
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "trojan" })
o:depends({ [_n("protocol")] = "anytls" })
-- https://github.com/SagerNet/sing-box/commit/d2a04c4e41e6cef0937331cb6d10211f431caaab
if singbox_tags:find("with_utls") then
-- [[ REALITY部分 ]] --
o = s:option(Flag, _n("reality"), translate("REALITY"))
o.default = 0
o:depends({ [_n("protocol")] = "http", [_n("tls")] = true })
o:depends({ [_n("protocol")] = "vmess", [_n("tls")] = true })
o:depends({ [_n("protocol")] = "vless", [_n("tls")] = true })
o:depends({ [_n("protocol")] = "trojan", [_n("tls")] = true })
o:depends({ [_n("protocol")] = "anytls", [_n("tls")] = true })
o = s:option(Value, _n("reality_private_key"), translate("Private Key"))
o:depends({ [_n("reality")] = true })
o = s:option(Value, _n("reality_shortId"), translate("Short Id"))
o:depends({ [_n("reality")] = true })
o = s:option(Value, _n("reality_handshake_server"), translate("Handshake Server"))
o.default = "google.com"
o:depends({ [_n("reality")] = true })
o = s:option(Value, _n("reality_handshake_server_port"), translate("Handshake Server Port"))
o.datatype = "port"
o.default = "443"
o:depends({ [_n("reality")] = true })
end
o = s:option(ListValue, _n("alpn"), translate("ALPN"))
o.default = "default"
o:value("default", translate("Default"))
o:value("h3")
o:value("h2")
o:value("h3,h2")
o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria" })
-- [[ TLS部分 ]] --
o = s:option(FileUpload, _n("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "naive" })
o:depends({ [_n("protocol")] = "hysteria" })
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2" })
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(FileUpload, _n("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key"
if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "naive" })
o:depends({ [_n("protocol")] = "hysteria" })
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2" })
o.validate = function(self, value, t)
if value and value ~= "" then
if not fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
end
end
return nil
end
o = s:option(Flag, _n("ech"), translate("ECH"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("flow")] = "", [_n("reality")] = false })
o:depends({ [_n("protocol")] = "naive" })
o:depends({ [_n("protocol")] = "hysteria" })
o:depends({ [_n("protocol")] = "tuic" })
o:depends({ [_n("protocol")] = "hysteria2", [_n("hysteria2_realms")] = false })
o = s:option(TextValue, _n("ech_key"), translate("ECH Key"))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ [_n("ech")] = true })
o.validate = function(self, value)
value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n")
value = value:gsub("^%s*\n", "")
if value:sub(-1) == "\n" then
value = value:sub(1, -2)
end
return value
end
o = s:option(ListValue, _n("transport"), translate("Transport"))
o:value("tcp", "TCP")
o:value("http", "HTTP")
o:value("ws", "WebSocket")
o:value("httpupgrade", "HTTPUpgrade")
o:value("quic", "QUIC")
o:value("grpc", "gRPC")
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless" })
o:depends({ [_n("protocol")] = "trojan" })
-- [[ HTTP部分 ]]--
o = s:option(DynamicList, _n("http_host"), translate("HTTP Host"))
o:depends({ [_n("transport")] = "http" })
o = s:option(Value, _n("http_path"), translate("HTTP Path"))
o:depends({ [_n("transport")] = "http" })
-- [[ WebSocket部分 ]]--
o = s:option(Value, _n("ws_host"), translate("WebSocket Host"))
o:depends({ [_n("transport")] = "ws" })
o = s:option(Value, _n("ws_path"), translate("WebSocket Path"))
o:depends({ [_n("transport")] = "ws" })
-- [[ HTTPUpgrade部分 ]]--
o = s:option(Value, _n("httpupgrade_host"), translate("HTTPUpgrade Host"))
o:depends({ [_n("transport")] = "httpupgrade" })
o = s:option(Value, _n("httpupgrade_path"), translate("HTTPUpgrade Path"))
o:depends({ [_n("transport")] = "httpupgrade" })
-- [[ gRPC部分 ]]--
o = s:option(Value, _n("grpc_serviceName"), "ServiceName")
o:depends({ [_n("transport")] = "grpc" })
-- [[ Mux ]]--
o = s:option(Flag, _n("mux"), translate("Mux"))
o.rmempty = false
o:depends({ [_n("protocol")] = "vmess" })
o:depends({ [_n("protocol")] = "vless", [_n("flow")] = "" })
o:depends({ [_n("protocol")] = "shadowsocks" })
o:depends({ [_n("protocol")] = "trojan" })
-- [[ TCP Brutal ]]--
o = s:option(Flag, _n("tcpbrutal"), translate("TCP Brutal"))
o.default = 0
o:depends({ [_n("mux")] = true })
o = s:option(Value, _n("tcpbrutal_up_mbps"), translate("Max upload Mbps"))
o.default = "10"
o:depends({ [_n("tcpbrutal")] = true })
o = s:option(Value, _n("tcpbrutal_down_mbps"), translate("Max download Mbps"))
o.default = "50"
o:depends({ [_n("tcpbrutal")] = true })
if singbox_tags:find("with_wireguard") then
o = s:option(Flag, _n("wireguard_system_interface"), translate("System interface"))
o.default = 0
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_mtu"), "MTU")
o.default = "1408"
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(DynamicList, _n("wireguard_local_address"), translate("Local Address"))
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_private_key"), translate("Private Key"))
o.datatype = "base64"
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(Value, _n("wireguard_public_key"), translate("Public Key"))
o.datatype = "base64"
o:depends({ [_n("protocol")] = "wireguard" })
o = s:option(DummyValue, _n("gen_wireguard_key"))
o.prefix = option_prefix
o.template = m:template_path("/server/server_wireguard")
o:depends({ [_n("protocol")] = "wireguard" })
end
o = s:option(Flag, _n("bind_local"), translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0"
o:depends({ [_n("custom")] = false })
o = s:option(Flag, _n("accept_lan"), translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!"))
o.default = "0"
o:depends({ [_n("custom")] = false })
local nodes_table = {}
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" and e.type == type_name then
nodes_table[#nodes_table + 1] = {
id = e[".name"],
remarks = e["remark"],
group = e["group"]
}
end
end
o = s:option(ListValue, _n("outbound_node"), translate("outbound node"))
o:value("", translate("Close"))
o:value("_socks", translate("Custom Socks"))
o:value("_http", translate("Custom HTTP"))
o:value("_iface", translate("Custom Interface"))
o.template = m:template_path("/cbi/nodes_listvalue")
o.group = {"","","",""}
for k, v in pairs(nodes_table) do
o:value(v.id, v.remarks)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
o:depends({ [_n("custom")] = false })
o = s:option(Value, _n("outbound_node_address"), translate("Address (Support Domain Name)"))
o:depends({ [_n("outbound_node")] = "_socks" })
o:depends({ [_n("outbound_node")] = "_http" })
o = s:option(Value, _n("outbound_node_port"), translate("Port"))
o.datatype = "port"
o:depends({ [_n("outbound_node")] = "_socks" })
o:depends({ [_n("outbound_node")] = "_http" })
o = s:option(Value, _n("outbound_node_username"), translate("Username"))
o:depends({ [_n("outbound_node")] = "_socks" })
o:depends({ [_n("outbound_node")] = "_http" })
o = s:option(Value, _n("outbound_node_password"), translate("Password"))
o.password = true
o:depends({ [_n("outbound_node")] = "_socks" })
o:depends({ [_n("outbound_node")] = "_http" })
o = s:option(Value, _n("outbound_node_iface"), translate("Interface"))
o:depends({ [_n("outbound_node")] = "_iface" })
local netdev_list = api.get_network_devices()
for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Flag, _n("log"), translate("Log"))
o.default = "1"
o.rmempty = false
o = s:option(ListValue, _n("loglevel"), translate("Log Level"))
o.default = "info"
o:value("debug")
o:value("info")
o:value("warn")
o:value("error")
o:depends({ [_n("log")] = true })
api.luci_types(arg[1], m, s, type_name, option_prefix)
@@ -1,40 +0,0 @@
local m, s = ...
if not api.is_finded("microsocks") then
return
end
local type_name = "Socks"
local option_prefix = "socks_"
local function _n(name)
return option_prefix .. name
end
-- [[ microsocks ]]
s.fields["type"]:value(type_name, "Socks")
if not s.fields["type"].default then
s.fields["type"].default = type_name
end
if s.val["type"] and s.val["type"] ~= type_name then
return
end
o = s:option(Value, _n("port"), translate("Listen Port"))
o.datatype = "port"
o = s:option(Flag, _n("auth"), translate("Auth"))
o = s:option(ListValue, _n("user"), translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ [_n("auth")] = true })
o = s:option(Flag, _n("log"), translate("Log"))
o.default = "1"
api.luci_types(arg[1], m, s, type_name, option_prefix)
@@ -44,7 +44,7 @@ o = s:option(Value, "wireguard_private_key", "Wireguard " .. translate("Private
o.datatype = "base64"
o = s:option(DummyValue, "gen_wireguard_key")
o.template = m:template_path("/server/server_wireguard")
o.template = m:template_path("/server/gen_wireguard_key")
o = s:option(Value, "wireguard_pre_shared_key", "Wireguard " .. translate("Pre shared key"))
o.datatype = "base64"
+87 -79
View File
@@ -1534,102 +1534,110 @@ function return_map(map)
return map
end
function luci_types(id, m, s, type_name, option_prefix)
function luci_types(s, s2)
local cbi = require "luci.cbi"
local m = s.map
local id = s2.section
local type_name = s2.type_name
local option_prefix = s2.option_prefix
local fv_type
local field_type = s.fields["type"]
if field_type then
fv_type = field_type:formvalue(id)
end
local rewrite_option_table = {}
for key, value in pairs(s.fields) do
if key:find(option_prefix) == 1 then
if not s.fields[key].not_rewrite then
if s.fields[key].rewrite_option then
if not rewrite_option_table[s.fields[key].rewrite_option] then
rewrite_option_table[s.fields[key].rewrite_option] = 1
for i, v in ipairs(s2.children) do
local o = s2.children[i]
o.config_option = o.option
o.option_prefix = option_prefix
o.option = option_prefix .. o.option
if not o.not_rewrite then
o.cfgvalue = function(self, section)
-- Add a custom `custom_cfgvalue` attribute. If a custom `custom_cfgvalue` function exists, the custom `cfgvalue` logic will be used.
if self.custom_cfgvalue then
return self:custom_cfgvalue(section)
else
if self.rewrite_option then
return m:get(section, self.rewrite_option)
else
rewrite_option_table[s.fields[key].rewrite_option] = rewrite_option_table[s.fields[key].rewrite_option] + 1
return m:get(section, self.config_option)
end
end
s.fields[key].cfgvalue = function(self, section)
-- 添加自定义 custom_cfgvalue 属性,如果有自定义的 custom_cfgvalue 函数,则使用自定义的 cfgvalue 逻辑
if self.custom_cfgvalue then
return self:custom_cfgvalue(section)
end
o.write = function(self, section, value)
if s.fields["type"]:formvalue(id) == type_name then
-- Add a custom `custom_write` attribute; if a custom `custom_write` function exists, then use the custom write logic.
if self.custom_write then
self:custom_write(section, value)
else
local new_val = value
if util.instanceof(self, cbi.DynamicList) then
local new_t = {}
if type(value) == "table" then
new_t = table_remove_duplicates(value)
else
new_t = { value }
end
if self.cast == "string" then
new_val = table.concat(new_t, " ")
else
new_val = new_t
end
end
if self.rewrite_option then
m:set(section, self.rewrite_option, new_val)
else
m:set(section, self.config_option, new_val)
end
end
end
end
o.remove = function(self, section)
if s.fields["type"]:formvalue(id) == type_name then
-- Add a custom `custom_remove` attribute; if a custom `custom_remove` function exists, use the custom remove logic.
if self.custom_remove then
self:custom_remove(section)
else
if self.rewrite_option then
return m:get(section, self.rewrite_option)
m:del(section, self.rewrite_option)
else
if self.option:find(option_prefix) == 1 then
return m:get(section, self.option:sub(1 + #option_prefix))
end
m:del(section, self.config_option)
end
end
end
s.fields[key].write = function(self, section, value)
if s.fields["type"]:formvalue(id) == type_name then
-- 添加自定义 custom_write 属性,如果有自定义的 custom_write 函数,则使用自定义的 write 逻辑
if self.custom_write then
self:custom_write(section, value)
else
local new_val = value
if util.instanceof(self, cbi.DynamicList) then
local new_t = {}
if type(value) == "table" then
new_t = table_remove_duplicates(value)
else
new_t = { value }
end
if self.cast == "string" then
new_val = table.concat(new_t, " ")
else
new_val = new_t
end
end
if self.rewrite_option then
m:set(section, self.rewrite_option, new_val)
else
if self.option:find(option_prefix) == 1 then
m:set(section, self.option:sub(1 + #option_prefix), new_val)
end
end
end
end
end
s.fields[key].remove = function(self, section)
if s.fields["type"]:formvalue(id) == type_name then
-- 添加自定义 custom_remove 属性,如果有自定义的 custom_remove 函数,则使用自定义的 remove 逻辑
if self.custom_remove then
self:custom_remove(section)
else
if self.rewrite_option and rewrite_option_table[self.rewrite_option] == 1 then
m:del(section, self.rewrite_option)
else
if self.option:find(option_prefix) == 1 then
m:del(section, self.option:sub(1 + #option_prefix))
end
end
end
end
end
end
local deps = s.fields[key].deps
if #deps > 0 then
for index, value in ipairs(deps) do
if not deps[index]['!reverse'] then
deps[index]["type"] = type_name
end
end
else
s.fields[key]:depends({ type = type_name })
end
if fv_type and fv_type ~= type_name then
s.fields[key].rmempty = true
end
end
local deps = o.deps
if #deps > 0 then
local function process_deps(dep)
local rewrite_deps = {}
for k, v in pairs(dep) do
if k:find("!") then
rewrite_deps[k] = v
else
rewrite_deps[option_prefix .. k] = v
end
end
if not rewrite_deps['!reverse'] then
rewrite_deps["type"] = type_name
end
return rewrite_deps
end
for index, value in ipairs(deps) do
local rewrite_deps = process_deps(value)
if rewrite_deps then
deps[index] = rewrite_deps
end
end
else
o:depends({ type = type_name })
end
if fv_type and fv_type ~= type_name then
o.rmempty = true
end
s:append(o)
end
end
@@ -1,7 +1,7 @@
<%
local map = self.map
local api = map.api
local prefix = self.prefix
local prefix = self.option_prefix or ""
-%>
<div class="cbi-value" id="cbi-<%=self.config.."-"..section.."-"..self.option%>" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>">
<label class="cbi-value-title"><%:Generate key%></label>
@@ -821,6 +821,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
(_method == "xchacha20-poly1305" and "xchacha20-ietf-poly1305") or _method
result.method = method
result.ss_method = method
result.password = password
result.tcp_fast_open = params.tfo
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
@@ -1189,6 +1190,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.port = content.port
result.password = content.password
result.method = content.encryption
result.ss_method = content.encryption
result.plugin = content.plugin
result.plugin_opts = content.plugin_options
result.group = content.airport
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196
PKG_RELEASE:=58
PKG_RELEASE:=59
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -143,40 +143,54 @@ end
-- 默认的保存并应用行为
local function apply_redirect(m)
local tmp_uci_file = "/etc/config/" .. "shadowsocksr" .. "_redirect"
local tmp_uci_file = "/etc/config/shadowsocksr_redirect"
if m.redirect and m.redirect ~= "" then
-- 如果存在临时文件,读取 URL 并通过 luci.http.redirect 跳转
if nixio.fs.access(tmp_uci_file) then
local redirect
local redirect_url
for line in io.lines(tmp_uci_file) do
redirect = line:match("option%s+url%s+['\"]([^'\"]+)['\"]")
if redirect and redirect ~= "" then break end
redirect_url = line:match("option%s+url%s+['\"]([^'\"]+)['\"]")
if redirect_url and redirect_url ~= "" then break end
end
if redirect and redirect ~= "" then
luci.sys.call("/bin/rm -f " .. tmp_uci_file)
luci.http.redirect(redirect)
if redirect_url and redirect_url ~= "" then
-- 使用 nixio 安全删除文件,替代外部 Shell 命令
nixio.fs.remove(tmp_uci_file)
-- JS 版 LuCI 依赖 luci.http.redirect 向前端重定向
luci.http.redirect(redirect_url)
return
end
else
-- 创建标记文件
nixio.fs.writefile(tmp_uci_file, "config redirect\n")
end
-- 挂载保存回调
local old_on_after_save = m.on_after_save
m.on_after_save = function(self)
local redirect = self.redirect
if redirect and redirect ~= "" then
m.uci:set("shadowsocksr" .. "_redirect", "@redirect[0]", "url", redirect)
if old_on_after_save then old_on_after_save(self) end
local target = self.redirect
if target and target ~= "" then
-- 使用标准的 nixio.fs 写入文件,保持格式一致
local content = string.format("config redirect\n\toption url '%s'\n", target)
nixio.fs.writefile(tmp_uci_file, content)
end
end
else
luci.sys.call("/bin/rm -f " .. tmp_uci_file)
-- 不满足重定向条件时清理文件
if nixio.fs.access(tmp_uci_file) then
nixio.fs.remove(tmp_uci_file)
end
end
end
local function set_apply_on_parse(map)
if not map then return end
if is_js_luci() then
apply_redirect(map)
local old = map.on_after_save
map.on_after_save = function(self)
if old then old(self) end
--map:set("@global[0]", "timestamp", os.time())
local function set_apply_on_parse(m)
if not m then return end
-- is_js_luci 保护
if is_js_luci and is_js_luci() then
apply_redirect(m)
local old_on_after_save = m.on_after_save
m.on_after_save = function(self)
if old_on_after_save then old_on_after_save(self) end
-- m:set("@global[0]", "timestamp", os.time())
end
end
end
@@ -345,15 +359,12 @@ if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then
return
end
-- 保存&应用成功后跳转到节点列表
set_apply_on_parse(m)
local old_after_save = m.on_after_save
m.on_after_save = function(self)
if old_after_save then old_after_save(self) end
set_apply_on_parse(m, function(self)
local node_type = self.uci:get("shadowsocksr", sid, "type")
if node_type == "clash" then
luci.sys.call(string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1 &", sid))
end
end
end)
local server_header = Template("/shadowsocksr/server_header")
server_header.section = sid
@@ -29,42 +29,56 @@ local function is_js_luci()
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
end
-- 保存并应用行为
-- 默认的保存并应用行为
local function apply_redirect(m)
local tmp_uci_file = "/etc/config/" .. "shadowsocksr" .. "_redirect"
local tmp_uci_file = "/etc/config/shadowsocksr_redirect"
if m.redirect and m.redirect ~= "" then
-- 如果存在临时文件,读取 URL 并通过 luci.http.redirect 跳转
if nixio.fs.access(tmp_uci_file) then
local redirect
local redirect_url
for line in io.lines(tmp_uci_file) do
redirect = line:match("option%s+url%s+['\"]([^'\"]+)['\"]")
if redirect and redirect ~= "" then break end
redirect_url = line:match("option%s+url%s+['\"]([^'\"]+)['\"]")
if redirect_url and redirect_url ~= "" then break end
end
if redirect and redirect ~= "" then
luci.sys.call("/bin/rm -f " .. tmp_uci_file)
luci.http.redirect(redirect)
if redirect_url and redirect_url ~= "" then
-- 使用 nixio 安全删除文件,替代外部 Shell 命令
nixio.fs.remove(tmp_uci_file)
-- JS 版 LuCI 依赖 luci.http.redirect 向前端重定向
luci.http.redirect(redirect_url)
return
end
else
-- 创建标记文件
nixio.fs.writefile(tmp_uci_file, "config redirect\n")
end
-- 挂载保存回调
local old_on_after_save = m.on_after_save
m.on_after_save = function(self)
local redirect = self.redirect
if redirect and redirect ~= "" then
m.uci:set("shadowsocksr" .. "_redirect", "@redirect[0]", "url", redirect)
if old_on_after_save then old_on_after_save(self) end
local target = self.redirect
if target and target ~= "" then
-- 使用标准的 nixio.fs 写入文件,保持格式一致
local content = string.format("config redirect\n\toption url '%s'\n", target)
nixio.fs.writefile(tmp_uci_file, content)
end
end
else
luci.sys.call("/bin/rm -f " .. tmp_uci_file)
-- 不满足重定向条件时清理文件
if nixio.fs.access(tmp_uci_file) then
nixio.fs.remove(tmp_uci_file)
end
end
end
local function set_apply_on_parse(map)
if not map then return end
if is_js_luci() then
apply_redirect(map)
local old = map.on_after_save
map.on_after_save = function(self)
if old then old(self) end
-- map:set("@global[0]", "timestamp", os.time())
local function set_apply_on_parse(m)
if not m then return end
-- is_js_luci 保护
if is_js_luci and is_js_luci() then
apply_redirect(m)
local old_on_after_save = m.on_after_save
m.on_after_save = function(self)
if old_on_after_save then old_on_after_save(self) end
-- m:set("@global[0]", "timestamp", os.time())
end
end
end
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -10,8 +10,8 @@ LUCI_TITLE:=Antitethering module for luci-app-firewall
#LUCI_DEPENDS:= +luci-app-firewall
PKG_LICENSE:=Apache-2.0
PKG_VERSION:=0.0.6
PKG_RELEASE:=3
PKG_VERSION:=0.0.7
PKG_RELEASE:=4
include $(TOPDIR)/feeds/luci/luci.mk
@@ -12,6 +12,26 @@
var briefInfo = _('In Method proxy Proxy server must be configured in transparent mode.<br />Default on lan ipaddress interface and port 3128 tcp.<br />Disable masquerade recommended.');
function packageInstalled(name) {
return Promise.all([
fs.stat('/usr/lib/opkg/info/' + name + '.list').catch(function() {
return false;
}),
fs.stat('/lib/apk/packages/' + name + '.list').catch(function() {
return false;
})
]).then(function(installed) {
return installed[0] || installed[1];
});
}
function anyPackageInstalled(packages) {
return Promise.all(packages.map(packageInstalled))
.then(function(installed) {
return installed.some(Boolean);
});
}
return view.extend({
render: function() {
@@ -32,46 +52,50 @@ return view.extend({
o.nocreate = true;
o.optional = true;
o = s.option(form.ListValue, 'method', _('Method'),
_('TTL method outgoing interface<br />Proxy method incoming interface'));
o.value('ttl', 'TTL');
o.value('proxy', 'Proxy');
return anyPackageInstalled(['3proxy', 'squid', 'redsocks']).then(function(proxyInstalled) {
o = s.option(form.ListValue, 'method', _('Method'),
_('TTL method outgoing interface<br />Proxy method incoming interface'));
o.value('ttl', 'TTL');
o = s.option(form.Flag, 'advanced', _('Advanced Option'));
o.default = '0';
o.rmempty = false;
if (proxyInstalled)
o.value('proxy', 'Proxy');
o = s.option(form.ListValue, 'inet', _('Inet Family'));
o.value('ipv4', 'IPv4');
o.value('ipv6', 'IPv6');
o.value('ipv4v6', _('Both'));
o.rmempty = true;
o.editable = true;
o.depends('advanced', '1');
o = s.option(form.Flag, 'advanced', _('Advanced Option'));
o.default = '0';
o.rmempty = false;
o = s.option(form.Value, 'ttl', _('TTL Value'),
_('Select TTL value. Range 1 - 255'));
o.value('64','64')
o.value('128','128')
o.default = '64';
o.rmempty = true;
o.editable = true;
o.depends({advanced: '1', method: /ttl/});
o = s.option(form.ListValue, 'inet', _('Inet Family'));
o.value('ipv4', 'IPv4');
o.value('ipv6', 'IPv6');
o.value('ipv4v6', _('Both'));
o.rmempty = true;
o.editable = true;
o.depends('advanced', '1');
o = s.option(form.Value, 'ports', _('Ports'),
_('Incoming ports route to proxy-server<br />Custom ports range: 0-65535'));
o.editable = true;
o.rmempty = true;
o.value('all', _('ALL Ports'));
o.value('http', _('HTTP Ports'));
o.default = 'all';
o.depends({advanced: '1', method: /proxy/})
o = s.option(form.Value, 'ttl', _('TTL Value'),
_('Select TTL value. Range 1 - 255'));
o.value('64', '64');
o.value('128', '128');
o.default = '64';
o.rmempty = true;
o.editable = true;
o.depends({ advanced: '1', method: /ttl/ });
o = s.option(form.Value, 'proxy', _('Proxy Server Address'),
_('IP-address proxy<br/>Format: <code>ipaddress:port</code><br />If not defined: use selected interface address.<br />Default: <code>lan</code> interface ipaddress'));
o.datatype = 'ipaddrport';
o.depends({advanced: '1', method: /proxy/})
o = s.option(form.Value, 'ports', _('Ports'),
_('Incoming ports route to proxy-server<br />Custom ports range: 0-65535'));
o.editable = true;
o.rmempty = true;
o.value('all', _('ALL Ports'));
o.value('http', _('HTTP Ports'));
o.default = 'all';
o.depends({ advanced: '1', method: /proxy/ });
return m.render();
o = s.option(form.Value, 'proxy', _('Proxy Server Address'),
_('IP-address proxy<br/>Format: <code>ipaddress:port</code><br />If not defined: use selected interface address.<br />Default: <code>lan</code> interface ipaddress'));
o.datatype = 'ipaddrport';
o.depends({ advanced: '1', method: /proxy/ });
return m.render();
});
},
});
+2 -2
View File
@@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=mhz
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL=https://github.com/wtarreau/mhz.git
PKG_SOURCE_DATE:=2025-07-26
PKG_SOURCE_VERSION:=512f63021f6f0cec8e5e13be744dba0b4704cfae
PKG_SOURCE_VERSION:=null
PKG_MIRROR_HASH:=skip
PKG_MAINTAINER:=Robert Marko <robimarko@gmail.com>
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=nexttrace
PKG_VERSION:=1.7.2
PKG_RELEASE:=13
PKG_VERSION:=1.6.2
PKG_RELEASE:=14
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -8,8 +8,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ooniprobe
PKG_VERSION:=3.30.0
PKG_RELEASE:=6
PKG_VERSION:=3.16.5
PKG_RELEASE:=7
PKG_SOURCE:=probe-cli-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/ooni/probe-cli/tar.gz/v$(PKG_VERSION)?
+3 -3
View File
@@ -8,12 +8,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=torrserver
PKG_VERSION:=MatriX.142.4
PKG_RELEASE:=16
PKG_VERSION:=MatriX.143
PKG_RELEASE:=17
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/YouROK/TorrServer.git
PKG_SOURCE_VERSION:=9b611cf7022b60f4385392ce37c83793235fe5c6
PKG_SOURCE_VERSION:=5c812ec1abe4c8e0acee29d820f78205a8f0e7b7
PKG_MIRROR_HASH:=skip
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>