diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile new file mode 100644 index 000000000..2d88da13a --- /dev/null +++ b/luci-app-passwall2/Makefile @@ -0,0 +1,153 @@ +# Copyright (C) 2022-2023 xiaorouji +# +# This is free software, licensed under the GNU General Public License v3. + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-passwall2 +PKG_VERSION:=1.16-7 +PKG_RELEASE:= + +PKG_CONFIG_DEPENDS:= \ + CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ + CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Brook \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_IPv6_Nat \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin + +LUCI_TITLE:=LuCI support for PassWall 2 +LUCI_PKGARCH:=all +LUCI_DEPENDS:=+coreutils +coreutils-base64 +coreutils-nohup +curl \ + +ip-full +libuci-lua +lua +luci-compat +luci-lib-jsonc +resolveip +tcping \ + +xray-core +v2ray-geoip +v2ray-geosite \ + +unzip \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Brook:brook \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy:haproxy \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria:hysteria \ + +PACKAGE_$(PKG_NAME)_INCLUDE_IPv6_Nat:ip6tables-mod-nat \ + +PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client:shadowsocks-libev-ss-local \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client:shadowsocks-libev-ss-redir \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server:shadowsocks-libev-ss-server \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client:shadowsocks-rust-sslocal \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \ + +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-local \ + +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-redir \ + +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs:simple-obfs \ + +PACKAGE_$(PKG_NAME)_INCLUDE_V2ray:v2ray-core \ + +PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin:v2ray-plugin + +define Package/$(PKG_NAME)/config +menu "Configuration" + +config PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy + bool "Iptables Transparent Proxy" + select PACKAGE_dnsmasq-full + select PACKAGE_dnsmasq_full_ipset + select PACKAGE_ipset + select PACKAGE_iptables + select PACKAGE_iptables-nft + select PACKAGE_iptables-zz-legacy + select PACKAGE_iptables-mod-conntrack-extra + select PACKAGE_iptables-mod-iprange + select PACKAGE_iptables-mod-socket + select PACKAGE_iptables-mod-tproxy + select PACKAGE_kmod-ipt-nat + depends on PACKAGE_$(PKG_NAME) + default y if ! PACKAGE_firewall4 + +config PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy + bool "Nftables Transparent Proxy" + select PACKAGE_dnsmasq-full + select PACKAGE_dnsmasq_full_nftset + select PACKAGE_nftables + select PACKAGE_kmod-nft-socket + select PACKAGE_kmod-nft-tproxy + select PACKAGE_kmod-nft-nat + depends on PACKAGE_$(PKG_NAME) + default y if PACKAGE_firewall4 + +config PACKAGE_$(PKG_NAME)_INCLUDE_Brook + bool "Include Brook" + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy + bool "Include Haproxy" + default y if aarch64||arm||i386||x86_64 + +config PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria + bool "Include Hysteria" + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_IPv6_Nat + depends on PACKAGE_ip6tables + bool "Include IPv6 Nat" + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy + bool "Include NaiveProxy" + depends on !(arc||(arm&&TARGET_gemini)||armeb||mips||mips64||powerpc) + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client + bool "Include Shadowsocks Libev Client" + default y + +config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server + bool "Include Shadowsocks Libev Server" + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client + bool "Include Shadowsocks Rust Client" + depends on aarch64||arm||i386||mips||mipsel||x86_64 + default y if aarch64 + +config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server + bool "Include Shadowsocks Rust Server" + depends on aarch64||arm||i386||mips||mipsel||x86_64 + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client + bool "Include ShadowsocksR Libev Client" + default y + +config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server + bool "Include ShadowsocksR Libev Server" + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs + bool "Include Simple-Obfs (Shadowsocks Plugin)" + default y + +config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray + bool "Include V2ray" + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin + bool "Include V2ray-Plugin (Shadowsocks Plugin)" + default y if aarch64||arm||i386||x86_64 + +endmenu +endef + +define Package/$(PKG_NAME)/conffiles +/etc/config/passwall2 +/etc/config/passwall2_server +/usr/share/passwall2/domains_excluded +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-passwall2/luasrc/controller/passwall2.lua b/luci-app-passwall2/luasrc/controller/passwall2.lua new file mode 100644 index 000000000..f92c1358f --- /dev/null +++ b/luci-app-passwall2/luasrc/controller/passwall2.lua @@ -0,0 +1,399 @@ +-- Copyright (C) 2022-2023 xiaorouji + +module("luci.controller.passwall2", package.seeall) +local api = require "luci.passwall2.api" +local appname = api.appname +local ucic = luci.model.uci.cursor() +local http = require "luci.http" +local util = require "luci.util" +local i18n = require "luci.i18n" + +function index() + appname = require "luci.passwall2.api".appname + entry({"admin", "services", appname}).dependent = true + entry({"admin", "services", appname, "reset_config"}, call("reset_config")).leaf = true + entry({"admin", "services", appname, "show"}, call("show_menu")).leaf = true + entry({"admin", "services", appname, "hide"}, call("hide_menu")).leaf = true + if not nixio.fs.access("/etc/config/passwall2") then return end + if nixio.fs.access("/etc/config/passwall2_show") then + e = entry({"admin", "services", appname}, alias("admin", "services", appname, "settings"), _("PassWall 2"), 0) + e.dependent = true + e.acl_depends = { "luci-app-passwall2" } + end + --[[ Client ]] + entry({"admin", "services", appname, "settings"}, cbi(appname .. "/client/global"), _("Basic Settings"), 1).dependent = true + entry({"admin", "services", appname, "node_list"}, cbi(appname .. "/client/node_list"), _("Node List"), 2).dependent = true + entry({"admin", "services", appname, "node_subscribe"}, cbi(appname .. "/client/node_subscribe"), _("Node Subscribe"), 3).dependent = true + entry({"admin", "services", appname, "auto_switch"}, cbi(appname .. "/client/auto_switch"), _("Auto Switch"), 4).leaf = true + entry({"admin", "services", appname, "other"}, cbi(appname .. "/client/other", {autoapply = true}), _("Other Settings"), 92).leaf = true + if nixio.fs.access("/usr/sbin/haproxy") then + entry({"admin", "services", appname, "haproxy"}, cbi(appname .. "/client/haproxy"), _("Load Balancing"), 93).leaf = true + end + entry({"admin", "services", appname, "app_update"}, cbi(appname .. "/client/app_update"), _("App Update"), 95).leaf = true + entry({"admin", "services", appname, "rule"}, cbi(appname .. "/client/rule"), _("Rule Manage"), 96).leaf = true + entry({"admin", "services", appname, "node_subscribe_config"}, cbi(appname .. "/client/node_subscribe_config")).leaf = true + entry({"admin", "services", appname, "node_config"}, cbi(appname .. "/client/node_config")).leaf = true + entry({"admin", "services", appname, "shunt_rules"}, cbi(appname .. "/client/shunt_rules")).leaf = true + entry({"admin", "services", appname, "acl"}, cbi(appname .. "/client/acl"), _("Access control"), 98).leaf = true + entry({"admin", "services", appname, "acl_config"}, cbi(appname .. "/client/acl_config")).leaf = true + entry({"admin", "services", appname, "log"}, form(appname .. "/client/log"), _("Watch Logs"), 999).leaf = true + + --[[ Server ]] + entry({"admin", "services", appname, "server"}, cbi(appname .. "/server/index"), _("Server-Side"), 99).leaf = true + entry({"admin", "services", appname, "server_user"}, cbi(appname .. "/server/user")).leaf = true + + --[[ API ]] + entry({"admin", "services", appname, "server_user_status"}, call("server_user_status")).leaf = true + entry({"admin", "services", appname, "server_user_log"}, call("server_user_log")).leaf = true + entry({"admin", "services", appname, "server_get_log"}, call("server_get_log")).leaf = true + entry({"admin", "services", appname, "server_clear_log"}, call("server_clear_log")).leaf = true + entry({"admin", "services", appname, "link_add_node"}, call("link_add_node")).leaf = true + entry({"admin", "services", appname, "autoswitch_add_node"}, call("autoswitch_add_node")).leaf = true + entry({"admin", "services", appname, "autoswitch_remove_node"}, call("autoswitch_remove_node")).leaf = true + entry({"admin", "services", appname, "get_now_use_node"}, call("get_now_use_node")).leaf = true + entry({"admin", "services", appname, "get_redir_log"}, call("get_redir_log")).leaf = true + entry({"admin", "services", appname, "get_log"}, call("get_log")).leaf = true + entry({"admin", "services", appname, "clear_log"}, call("clear_log")).leaf = true + entry({"admin", "services", appname, "status"}, call("status")).leaf = true + entry({"admin", "services", appname, "haproxy_status"}, call("haproxy_status")).leaf = true + entry({"admin", "services", appname, "socks_status"}, call("socks_status")).leaf = true + entry({"admin", "services", appname, "connect_status"}, call("connect_status")).leaf = true + entry({"admin", "services", appname, "ping_node"}, call("ping_node")).leaf = true + entry({"admin", "services", appname, "urltest_node"}, call("urltest_node")).leaf = true + entry({"admin", "services", appname, "set_node"}, call("set_node")).leaf = true + entry({"admin", "services", appname, "copy_node"}, call("copy_node")).leaf = true + entry({"admin", "services", appname, "clear_all_nodes"}, call("clear_all_nodes")).leaf = true + entry({"admin", "services", appname, "delete_select_nodes"}, call("delete_select_nodes")).leaf = true + entry({"admin", "services", appname, "update_rules"}, call("update_rules")).leaf = true + + --[[Components update]] + local coms = require "luci.passwall2.com" + local com + for com, _ in pairs(coms) do + entry({"admin", "services", appname, "check_" .. com}, call("com_check", com)).leaf = true + entry({"admin", "services", appname, "update_" .. com}, call("com_update", com)).leaf = true + end +end + +local function http_write_json(content) + http.prepare_content("application/json") + http.write_json(content or {code = 1}) +end + +function reset_config() + luci.sys.call('/etc/init.d/passwall2 stop') + luci.sys.call('[ -f "/usr/share/passwall2/0_default_config" ] && cp -f /usr/share/passwall2/0_default_config /etc/config/passwall2') + luci.http.redirect(api.url()) +end + +function show_menu() + luci.sys.call("touch /etc/config/passwall2_show") + luci.sys.call("rm -rf /tmp/luci-*") + luci.sys.call("/etc/init.d/rpcd restart >/dev/null") + luci.http.redirect(api.url()) +end + +function hide_menu() + luci.sys.call("rm -rf /etc/config/passwall2_show") + luci.sys.call("rm -rf /tmp/luci-*") + luci.sys.call("/etc/init.d/rpcd restart >/dev/null") + luci.http.redirect(luci.dispatcher.build_url("admin", "status", "overview")) +end + +function link_add_node() + local lfile = "/tmp/links.conf" + local link = luci.http.formvalue("link") + luci.sys.call('echo \'' .. link .. '\' > ' .. lfile) + luci.sys.call("lua /usr/share/passwall2/subscribe.lua add log") +end + +function autoswitch_add_node() + local key = luci.http.formvalue("key") + if key and key ~= "" then + local new_list = ucic:get(appname, "@auto_switch[0]", "node") or {} + for i = #new_list, 1, -1 do + if (ucic:get(appname, new_list[i], "remarks") or ""):find(key) then + table.remove(new_list, i) + end + end + for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" and e["remark"]:find(key) then + table.insert(new_list, e.id) + end + end + ucic:set_list(appname, "@auto_switch[0]", "node", new_list) + ucic:commit(appname) + end + luci.http.redirect(api.url("auto_switch")) +end + +function autoswitch_remove_node() + local key = luci.http.formvalue("key") + if key and key ~= "" then + local new_list = ucic:get(appname, "@auto_switch[0]", "node") or {} + for i = #new_list, 1, -1 do + if (ucic:get(appname, new_list[i], "remarks") or ""):find(key) then + table.remove(new_list, i) + end + end + ucic:set_list(appname, "@auto_switch[0]", "node", new_list) + ucic:commit(appname) + end + luci.http.redirect(api.url("auto_switch")) +end + +function get_now_use_node() + local e = {} + local data, code, msg = nixio.fs.readfile("/tmp/etc/passwall2/id/global") + if data then + e["global"] = util.trim(data) + end + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function get_redir_log() + local id = luci.http.formvalue("id") + if nixio.fs.access("/tmp/etc/passwall2/" .. id .. ".log") then + local content = luci.sys.exec("cat /tmp/etc/passwall2/" .. id .. ".log") + content = content:gsub("\n", "
") + luci.http.write(content) + else + luci.http.write(string.format("", i18n.translate("Not enabled log"))) + end +end + +function get_log() + -- luci.sys.exec("[ -f /tmp/log/passwall2.log ] && sed '1!G;h;$!d' /tmp/log/passwall2.log > /tmp/log/passwall2_show.log") + luci.http.write(luci.sys.exec("[ -f '/tmp/log/passwall2.log' ] && cat /tmp/log/passwall2.log")) +end + +function clear_log() + luci.sys.call("echo '' > /tmp/log/passwall2.log") +end + +function status() + local e = {} + e["global_status"] = luci.sys.call(string.format("top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep -i 'global\\.json' >/dev/null", appname)) == 0 + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function haproxy_status() + local e = luci.sys.call(string.format("top -bn1 | grep -v grep | grep '%s/bin/' | grep haproxy >/dev/null", appname)) == 0 + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function socks_status() + local e = {} + local index = luci.http.formvalue("index") + local id = luci.http.formvalue("id") + e.index = index + e.socks_status = luci.sys.call(string.format("top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' | grep 'SOCKS_' > /dev/null", appname, id)) == 0 + local use_http = ucic:get(appname, id, "http_port") or 0 + e.use_http = 0 + if tonumber(use_http) > 0 then + e.use_http = 1 + e.http_status = luci.sys.call(string.format("top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' | grep -E 'HTTP_|HTTP2SOCKS' > /dev/null", appname, id)) == 0 + end + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function connect_status() + local e = {} + e.use_time = "" + local url = luci.http.formvalue("url") + local result = luci.sys.exec('curl --connect-timeout 3 -o /dev/null -I -sk -w "%{http_code}:%{time_starttransfer}" ' .. url) + local code = tonumber(luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $1}'") or "0") + if code ~= 0 then + local use_time = luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $2}'") + if use_time:find("%.") then + e.use_time = string.format("%.2f", use_time * 1000) + else + e.use_time = string.format("%.2f", use_time / 1000) + end + e.ping_type = "curl" + end + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function ping_node() + local index = luci.http.formvalue("index") + local address = luci.http.formvalue("address") + local port = luci.http.formvalue("port") + local e = {} + e.index = index + local nodes_ping = ucic:get(appname, "@global_other[0]", "nodes_ping") or "" + if nodes_ping:find("tcping") and luci.sys.exec("echo -n $(command -v tcping)") ~= "" then + if api.is_ipv6(address) then + address = api.get_ipv6_only(address) + end + e.ping = luci.sys.exec(string.format("echo -n $(tcping -q -c 1 -i 1 -t 2 -p %s %s 2>&1 | grep -o 'time=[0-9]*' | awk -F '=' '{print $2}') 2>/dev/null", port, address)) + end + if e.ping == nil or tonumber(e.ping) == 0 then + e.ping = luci.sys.exec("echo -n $(ping -c 1 -W 1 %q 2>&1 | grep -o 'time=[0-9]*' | awk -F '=' '{print $2}') 2>/dev/null" % address) + end + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function urltest_node() + local index = luci.http.formvalue("index") + local id = luci.http.formvalue("id") + local e = {} + e.index = index + local result = luci.sys.exec(string.format("/usr/share/passwall2/test.sh url_test_node %s %s", id, "urltest_node")) + local code = tonumber(luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $1}'") or "0") + if code ~= 0 then + local use_time = luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $2}'") + if use_time:find("%.") then + e.use_time = string.format("%.2f", use_time * 1000) + else + e.use_time = string.format("%.2f", use_time / 1000) + end + end + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function set_node() + local type = luci.http.formvalue("type") + local config = luci.http.formvalue("config") + local section = luci.http.formvalue("section") + ucic:set(appname, type, config, section) + ucic:commit(appname) + luci.sys.call("/etc/init.d/passwall2 restart > /dev/null 2>&1 &") + luci.http.redirect(api.url("log")) +end + +function copy_node() + local section = luci.http.formvalue("section") + local uuid = api.gen_short_uuid() + ucic:section(appname, "nodes", uuid) + for k, v in pairs(ucic:get_all(appname, section)) do + local filter = k:find("%.") + if filter and filter == 1 then + else + xpcall(function() + ucic:set(appname, uuid, k, v) + end, + function(e) + end) + end + end + ucic:delete(appname, uuid, "add_from") + ucic:set(appname, uuid, "add_mode", 1) + ucic:commit(appname) + luci.http.redirect(api.url("node_config", uuid)) +end + +function clear_all_nodes() + ucic:set(appname, '@global[0]', "enabled", "0") + ucic:set(appname, '@global[0]', "node", "nil") + ucic:set_list(appname, "@auto_switch[0]", "node", {}) + ucic:foreach(appname, "socks", function(t) + ucic:delete(appname, t[".name"]) + end) + ucic:foreach(appname, "haproxy_config", function(t) + ucic:delete(appname, t[".name"]) + end) + ucic:foreach(appname, "acl_rule", function(t) + ucic:set(appname, t[".name"], "node", "default") + end) + ucic:foreach(appname, "nodes", function(node) + ucic:delete(appname, node['.name']) + end) + + ucic:commit(appname) + luci.sys.call("/etc/init.d/" .. appname .. " stop") +end + +function delete_select_nodes() + local ids = luci.http.formvalue("ids") + local auto_switch_node_list = ucic:get(appname, "@auto_switch[0]", "node") or {} + string.gsub(ids, '[^' .. "," .. ']+', function(w) + for i = #auto_switch_node_list, 1, -1 do + if w == auto_switch_node_list[i] then + table.remove(auto_switch_node_list, i) + end + end + ucic:set_list(appname, "@auto_switch[0]", "node", auto_switch_node_list) + if (ucic:get(appname, "@global[0]", "node") or "nil") == w then + ucic:set(appname, '@global[0]', "node", "nil") + end + ucic:foreach(appname, "socks", function(t) + if t["node"] == w then + ucic:delete(appname, t[".name"]) + end + end) + ucic:foreach(appname, "haproxy_config", function(t) + if t["lbss"] == w then + ucic:delete(appname, t[".name"]) + end + end) + ucic:foreach(appname, "acl_rule", function(t) + if t["node"] == w then + ucic:set(appname, t[".name"], "node", "default") + end + end) + ucic:delete(appname, w) + end) + ucic:commit(appname) + luci.sys.call("/etc/init.d/" .. appname .. " restart > /dev/null 2>&1 &") +end + +function update_rules() + local update = luci.http.formvalue("update") + luci.sys.call("lua /usr/share/passwall2/rule_update.lua log '" .. update .. "' > /dev/null 2>&1 &") + http_write_json() +end + +function server_user_status() + local e = {} + e.index = luci.http.formvalue("index") + e.status = luci.sys.call(string.format("top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep -i '%s' >/dev/null", appname .. "_server", luci.http.formvalue("id"))) == 0 + http_write_json(e) +end + +function server_user_log() + local id = luci.http.formvalue("id") + if nixio.fs.access("/tmp/etc/passwall2_server/" .. id .. ".log") then + local content = luci.sys.exec("cat /tmp/etc/passwall2_server/" .. id .. ".log") + content = content:gsub("\n", "
") + luci.http.write(content) + else + luci.http.write(string.format("", i18n.translate("Not enabled log"))) + end +end + +function server_get_log() + luci.http.write(luci.sys.exec("[ -f '/tmp/log/passwall2_server.log' ] && cat /tmp/log/passwall2_server.log")) +end + +function server_clear_log() + luci.sys.call("echo '' > /tmp/log/passwall2_server.log") +end + +function com_check(comname) + local json = api.to_check("", comname) + http_write_json(json) +end + +function com_update(comname) + local json = nil + local task = http.formvalue("task") + if task == "extract" then + json = api.to_extract(comname, http.formvalue("file"), http.formvalue("subfix")) + elseif task == "move" then + json = api.to_move(comname, http.formvalue("file")) + else + json = api.to_download(comname, http.formvalue("url"), http.formvalue("size")) + end + + http_write_json(json) +end + + diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua new file mode 100644 index 000000000..c346ce364 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua @@ -0,0 +1,62 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local sys = api.sys +local has_chnlist = api.fs.access("/usr/share/passwall2/rules/chnlist") + +m = Map(appname) + +s = m:section(TypedSection, "global", translate("ACLs"), "" .. translate("ACLs is a tools which used to designate specific IP proxy mode.") .. "") +s.anonymous = true + +o = s:option(Flag, "acl_enable", translate("Main switch")) +o.rmempty = false +o.default = false + +-- [[ ACLs Settings ]]-- +s = m:section(TypedSection, "acl_rule") +s.template = "cbi/tblsection" +s.sortable = true +s.anonymous = true +s.addremove = true +s.extedit = api.url("acl_config", "%s") +function s.create(e, t) + t = TypedSection.create(e, t) + luci.http.redirect(e.extedit:format(t)) +end + +---- Enable +o = s:option(Flag, "enabled", translate("Enable")) +o.default = 1 +o.rmempty = false + +---- Remarks +o = s:option(Value, "remarks", translate("Remarks")) +o.rmempty = true + +local mac_t = {} +sys.net.mac_hints(function(e, t) + mac_t[e] = { + ip = t, + mac = e + } +end) + +o = s:option(DummyValue, "sources", translate("Source")) +o.rawhtml = true +o.cfgvalue = function(t, n) + local e = '' + local v = Value.cfgvalue(t, n) or '' + string.gsub(v, '[^' .. " " .. ']+', function(w) + local a = w + if mac_t[w] then + a = a .. ' (' .. mac_t[w].ip .. ')' + end + if #e > 0 then + e = e .. "
" + end + e = e .. a + end) + return e +end + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua new file mode 100644 index 000000000..e9646a04e --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua @@ -0,0 +1,294 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local sys = api.sys + +m = Map(appname) + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + nodes_table[#nodes_table + 1] = e +end + +local dynamicList_write = function(self, section, value) + local t = {} + local t2 = {} + if type(value) == "table" then + local x + for _, x in ipairs(value) do + if x and #x > 0 then + if not t2[x] then + t2[x] = x + t[#t+1] = x + end + end + end + else + t = { value } + end + t = table.concat(t, " ") + return DynamicList.write(self, section, t) +end +local doh_validate = function(self, value, t) + if value ~= "" then + local flag = 0 + local util = require "luci.util" + local val = util.split(value, ",") + local url = val[1] + val[1] = nil + for i = 1, #val do + local v = val[i] + if v then + if not datatypes.ipmask4(v) then + flag = 1 + end + end + end + if flag == 0 then + return value + end + end + return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP" +end +-- [[ ACLs Settings ]]-- +s = m:section(NamedSection, arg[1], translate("ACLs"), translate("ACLs")) +s.addremove = false +s.dynamic = false + +---- Enable +o = s:option(Flag, "enabled", translate("Enable")) +o.default = 1 +o.rmempty = false + +---- Remarks +o = s:option(Value, "remarks", translate("Remarks")) +o.default = arg[1] +o.rmempty = true + +local mac_t = {} +sys.net.mac_hints(function(e, t) + mac_t[#mac_t + 1] = { + ip = t, + mac = e + } +end) +table.sort(mac_t, function(a,b) + if #a.ip < #b.ip then + return true + elseif #a.ip == #b.ip then + if a.ip < b.ip then + return true + else + return #a.ip < #b.ip + end + end + return false +end) + +---- Source +sources = s:option(DynamicList, "sources", translate("Source")) +sources.description = "" +sources.cast = "string" +for _, key in pairs(mac_t) do + sources:value(key.mac, "%s (%s)" % {key.mac, key.ip}) +end +sources.cfgvalue = function(self, section) + local value + if self.tag_error[section] then + value = self:formvalue(section) + else + value = self.map:get(section, self.option) + if type(value) == "string" then + local value2 = {} + string.gsub(value, '[^' .. " " .. ']+', function(w) table.insert(value2, w) end) + value = value2 + end + end + return value +end +sources.validate = function(self, value, t) + local err = {} + for _, v in ipairs(value) do + local flag = false + if v:find("ipset:") and v:find("ipset:") == 1 then + local ipset = v:gsub("ipset:", "") + if ipset and ipset ~= "" then + flag = true + end + end + + if flag == false and datatypes.macaddr(v) then + flag = true + end + + if flag == false and datatypes.ip4addr(v) then + flag = true + end + + if flag == false and api.iprange(v) then + flag = true + end + + if flag == false then + err[#err + 1] = v + end + end + + if #err > 0 then + self:add_error(t, "invalid", translate("Not true format, please re-enter!")) + for _, v in ipairs(err) do + self:add_error(t, "invalid", v) + end + end + + return value +end +sources.write = dynamicList_write + +---- TCP No Redir Ports +o = s:option(Value, "tcp_no_redir_ports", translate("TCP No Redir Ports")) +o.default = "default" +o:value("disable", translate("No patterns are used")) +o:value("default", translate("Default")) +o:value("1:65535", translate("All")) + +---- UDP No Redir Ports +o = s:option(Value, "udp_no_redir_ports", translate("UDP No Redir Ports")) +o.default = "default" +o:value("disable", translate("No patterns are used")) +o:value("default", translate("Default")) +o:value("1:65535", translate("All")) + +---- TCP Redir Ports +o = s:option(Value, "tcp_redir_ports", translate("TCP Redir Ports")) +o.default = "default" +o:value("default", translate("Default")) +o:value("1:65535", translate("All")) +o:value("22,25,53,143,465,587,853,993,995,80,443", translate("Common Use")) +o:value("80,443", "80,443") + +---- UDP Redir Ports +o = s:option(Value, "udp_redir_ports", translate("UDP Redir Ports")) +o.default = "default" +o:value("default", translate("Default")) +o:value("1:65535", translate("All")) + +node = s:option(ListValue, "node", "" .. translate("Node") .. "") +node.default = "default" +node:value("default", translate("Default")) + +for k, v in pairs(nodes_table) do + node:value(v.id, v["remark"]) +end + +o = s:option(ListValue, "direct_dns_protocol", translate("Direct DNS Protocol")) +o.default = "auto" +o:value("auto", translate("Auto")) +o:value("udp", "UDP") +o:value("tcp", "TCP") +o:value("doh", "DoH") +o:depends({ node = "default", ['!reverse'] = true }) +---- DNS Forward +o = s:option(Value, "direct_dns", translate("Direct DNS")) +o.datatype = "or(ipaddr,ipaddrport)" +o.default = "119.29.29.29" +o:value("114.114.114.114", "114.114.114.114 (114DNS)") +o:value("119.29.29.29", "119.29.29.29 (DNSPod)") +o:value("223.5.5.5", "223.5.5.5 (AliDNS)") +o:depends("direct_dns_protocol", "udp") +o:depends("direct_dns_protocol", "tcp") + +---- DoH +o = s:option(Value, "direct_dns_doh", translate("Direct DNS DoH")) +o.default = "https://223.5.5.5/dns-query" +o:value("https://1.12.12.12/dns-query", "DNSPod 1") +o:value("https://120.53.53.53/dns-query", "DNSPod 2") +o:value("https://223.5.5.5/dns-query", "AliDNS") +o.validate = doh_validate +o:depends("direct_dns_protocol", "doh") + +o = s:option(Value, "direct_dns_client_ip", translate("Direct DNS EDNS Client Subnet")) +o.description = translate("Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address).") .. "
" .. + translate("This feature requires the DNS server to support the Edns Client Subnet (RFC7871).") +o.datatype = "ipaddr" +o:depends("direct_dns_protocol", "tcp") +o:depends("direct_dns_protocol", "doh") + +o = s:option(ListValue, "direct_dns_query_strategy", translate("Direct Query Strategy")) +o.default = "UseIP" +o:value("UseIP") +o:value("UseIPv4") +o:value("UseIPv6") +o:depends({ node = "default", ['!reverse'] = true }) + +o = s:option(ListValue, "remote_dns_protocol", translate("Remote DNS Protocol")) +o:value("tcp", "TCP") +o:value("doh", "DoH") +o:value("udp", "UDP") +o:depends({ node = "default", ['!reverse'] = true }) + +---- DNS Forward +o = s:option(Value, "remote_dns", translate("Remote DNS")) +o.datatype = "or(ipaddr,ipaddrport)" +o.default = "1.1.1.1" +o:value("1.1.1.1", "1.1.1.1 (CloudFlare)") +o:value("1.1.1.2", "1.1.1.2 (CloudFlare-Security)") +o:value("8.8.4.4", "8.8.4.4 (Google)") +o:value("8.8.8.8", "8.8.8.8 (Google)") +o:value("9.9.9.9", "9.9.9.9 (Quad9-Recommended)") +o:value("208.67.220.220", "208.67.220.220 (OpenDNS)") +o:value("208.67.222.222", "208.67.222.222 (OpenDNS)") +o:depends("remote_dns_protocol", "tcp") +o:depends("remote_dns_protocol", "udp") + +---- DoH +o = s:option(Value, "remote_dns_doh", translate("Remote DNS DoH")) +o:value("https://1.1.1.1/dns-query", "CloudFlare") +o:value("https://1.1.1.2/dns-query", "CloudFlare-Security") +o:value("https://8.8.4.4/dns-query", "Google 8844") +o:value("https://8.8.8.8/dns-query", "Google 8888") +o:value("https://9.9.9.9/dns-query", "Quad9-Recommended") +o:value("https://208.67.222.222/dns-query", "OpenDNS") +o:value("https://dns.adguard.com/dns-query,176.103.130.130", "AdGuard") +o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "LibreDNS") +o:value("https://doh.libredns.gr/ads,116.202.176.26", "LibreDNS (No Ads)") +o.default = "https://1.1.1.1/dns-query" +o.validate = doh_validate +o:depends("remote_dns_protocol", "doh") + +o = s:option(Value, "remote_dns_client_ip", translate("Remote DNS EDNS Client Subnet")) +o.description = translate("Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address).") .. "
" .. + translate("This feature requires the DNS server to support the Edns Client Subnet (RFC7871).") +o.datatype = "ipaddr" +o:depends("remote_dns_protocol", "tcp") +o:depends("remote_dns_protocol", "doh") + +o = s:option(Flag, "remote_fakedns", "FakeDNS", translate("Use FakeDNS work in the shunt domain that proxy.")) +o.default = "0" +o.rmempty = false +o:depends("remote_dns_protocol", "tcp") +o:depends("remote_dns_protocol", "doh") +o:depends("remote_dns_protocol", "udp") + +o = s:option(ListValue, "remote_dns_query_strategy", translate("Remote Query Strategy")) +o.default = "UseIPv4" +o:value("UseIP") +o:value("UseIPv4") +o:value("UseIPv6") +o:depends("remote_dns_protocol", "tcp") +o:depends("remote_dns_protocol", "doh") +o:depends("remote_dns_protocol", "udp") + +hosts = s:option(TextValue, "dns_hosts", translate("Domain Override")) +hosts.rows = 5 +hosts.wrap = "off" +hosts:depends("remote_dns_protocol", "tcp") +hosts:depends("remote_dns_protocol", "doh") +hosts:depends("remote_dns_protocol", "udp") + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua new file mode 100644 index 000000000..5364eaacb --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua @@ -0,0 +1,28 @@ +local api = require "luci.passwall2.api" +local appname = api.appname + +m = Map(appname) + +-- [[ App Settings ]]-- +s = m:section(TypedSection, "global_app", translate("App Update"), + "" .. + translate("Please confirm that your firmware supports FPU.") .. + "") +s.anonymous = true +s:append(Template(appname .. "/app_update/app_version")) + +local k, v +local com = require "luci.passwall2.com" +for k, v in pairs(com) do + o = s:option(Value, k:gsub("%-","_") .. "_file", translatef("%s App Path", v.name)) + o.default = v.default_path or ("/usr/bin/" .. k) + o.rmempty = false +end + +o = s:option(DummyValue, "tips", " ") +o.rawhtml = true +o.cfgvalue = function(t, n) + return string.format('%s', translate("if you want to run from memory, change the path, /tmp beginning then save the application and update it manually.")) +end + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/auto_switch.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/auto_switch.lua new file mode 100644 index 000000000..c4f0573ca --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/auto_switch.lua @@ -0,0 +1,66 @@ +local api = require "luci.passwall2.api" +local appname = api.appname + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + nodes_table[#nodes_table + 1] = e +end + +m = Map(appname) + +-- [[ Auto Switch Settings ]]-- +s = m:section(TypedSection, "auto_switch") +s.anonymous = true + +---- Enable +o = s:option(Flag, "enable", translate("Enable")) +o.default = 0 +o.rmempty = false + +o = s:option(Value, "testing_time", translate("How often to test"), translate("Units:minutes")) +o.datatype = "uinteger" +o.default = 1 + +o = s:option(Value, "connect_timeout", translate("Timeout seconds"), translate("Units:seconds")) +o.datatype = "uinteger" +o.default = 3 + +o = s:option(Value, "retry_num", translate("Timeout retry num")) +o.datatype = "uinteger" +o.default = 3 + +o = s:option(DynamicList, "node", translate("List of backup nodes")) +for k, v in pairs(nodes_table) do + if v.node_type == "normal" then + o:value(v.id, v["remark"]) + end +end +function o.write(self, section, value) + local t = {} + local t2 = {} + if type(value) == "table" then + local x + for _, x in ipairs(value) do + if x and #x > 0 then + if not t2[x] then + t2[x] = x + t[#t+1] = x + end + end + end + else + t = { value } + end + return DynamicList.write(self, section, t) +end + +o = s:option(Flag, "restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node.")) + +o = s:option(ListValue, "shunt_logic", translate("If the main node is shunt")) +o:value("0", translate("Switch it")) +o:value("1", translate("Applying to the default node")) +o:value("2", translate("Applying to the default preproxy node")) + +m:append(Template(appname .. "/auto_switch/footer")) + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua new file mode 100644 index 000000000..6625bf1ab --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua @@ -0,0 +1,425 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local uci = api.uci +local datatypes = api.datatypes +local has_v2ray = api.is_finded("v2ray") +local has_xray = api.is_finded("xray") + +m = Map(appname) + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + nodes_table[#nodes_table + 1] = e +end + +local doh_validate = function(self, value, t) + if value ~= "" then + local flag = 0 + local util = require "luci.util" + local val = util.split(value, ",") + local url = val[1] + val[1] = nil + for i = 1, #val do + local v = val[i] + if v then + if not datatypes.ipmask4(v) then + flag = 1 + end + end + end + if flag == 0 then + return value + end + end + return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP" +end + +m:append(Template(appname .. "/global/status")) + +s = m:section(TypedSection, "global") +s.anonymous = true +s.addremove = false + +s:tab("Main", translate("Main")) + +-- [[ Global Settings ]]-- +o = s:taboption("Main", Flag, "enabled", translate("Main switch")) +o.rmempty = false + +local auto_switch_tip +local shunt_remark +local current_node = luci.sys.exec(string.format("[ -f '/tmp/etc/%s/id/global' ] && echo -n $(cat /tmp/etc/%s/id/global)", appname, appname)) +if current_node and current_node ~= "" and current_node ~= "nil" then + local n = uci:get_all(appname, current_node) + if n then + if tonumber(m:get("@auto_switch[0]", "enable") or 0) == 1 then + if n.protocol == "_shunt" then + local shunt_logic = tonumber(m:get("@auto_switch[0]", "shunt_logic")) + if shunt_logic == 1 or shunt_logic == 2 then + if shunt_logic == 1 then + shunt_remark = "default" + elseif shunt_logic == 2 then + shunt_remark = "main" + end + current_node = luci.sys.exec(string.format("[ -f '/tmp/etc/%s/id/global_%s' ] && echo -n $(cat /tmp/etc/%s/id/global_%s)", appname, shunt_remark, appname, shunt_remark)) + if current_node and current_node ~= "" and current_node ~= "nil" then + n = uci:get_all(appname, current_node) + end + end + end + if n then + local remarks = api.get_node_remarks(n) + local url = api.url("node_config", n[".name"]) + auto_switch_tip = translatef("Current node: %s", string.format('%s', url, remarks)) .. "
" + end + end + end +end + +---- Node +node = s:taboption("Main", ListValue, "node", "" .. translate("Node") .. "") +node:value("nil", translate("Close")) +if not shunt_remark and auto_switch_tip then + node.description = auto_switch_tip +end + +-- 分流 +if (has_v2ray or has_xray) and #nodes_table > 0 then + local normal_list = {} + local balancing_list = {} + local shunt_list = {} + for k, v in pairs(nodes_table) do + if v.node_type == "normal" then + normal_list[#normal_list + 1] = v + end + if v.protocol and v.protocol == "_balancing" then + balancing_list[#balancing_list + 1] = v + end + if v.protocol and v.protocol == "_shunt" then + shunt_list[#shunt_list + 1] = v + end + end + + local function get_cfgvalue(shunt_node_id, option) + return function(self, section) + return m:get(shunt_node_id, option) or "nil" + end + end + local function get_write(shunt_node_id, option) + return function(self, section, value) + m:set(shunt_node_id, option, value) + end + end + if #normal_list > 0 then + for k, v in pairs(shunt_list) do + local vid = v.id + -- shunt node type, V2ray or Xray + local type = s:taboption("Main", ListValue, vid .. "-type", translate("Type")) + if has_v2ray then + type:value("V2ray", translate("V2ray")) + end + if has_xray then + type:value("Xray", translate("Xray")) + end + type.cfgvalue = get_cfgvalue(v.id, "type") + type.write = get_write(v.id, "type") + + -- pre-proxy + o = s:taboption("Main", Flag, vid .. "-preproxy_enabled", translate("Preproxy")) + o:depends("node", v.id) + o.rmempty = false + o.cfgvalue = get_cfgvalue(v.id, "preproxy_enabled") + o.write = get_write(v.id, "preproxy_enabled") + + o = s:taboption("Main", Value, vid .. "-main_node", string.format('%s', translate("Preproxy Node")), translate("Set the node to be used as a pre-proxy. Each rule (including Default) has a separate switch that controls whether this rule uses the pre-proxy or not.")) + o:depends(vid .. "-preproxy_enabled", "1") + for k1, v1 in pairs(balancing_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(normal_list) do + o:value(v1.id, v1.remark) + end + if #o.keylist > 0 then + o.default = o.keylist[1] + end + o.cfgvalue = get_cfgvalue(v.id, "main_node") + o.write = get_write(v.id, "main_node") + if shunt_remark == "main" and auto_switch_tip then + o.description = auto_switch_tip + end + + if (has_v2ray and has_xray) or (v.type == "V2ray" and not has_v2ray) or (v.type == "Xray" and not has_xray) then + type:depends("node", v.id) + else + type:depends("node", "hide") --不存在的依赖,即始终隐藏 + end + + uci:foreach(appname, "shunt_rules", function(e) + local id = e[".name"] + local node_option = vid .. "-" .. id .. "_node" + if id and e.remarks then + o = s:taboption("Main", Value, node_option, string.format('* %s', api.url("shunt_rules", id), e.remarks)) + o.cfgvalue = get_cfgvalue(v.id, id) + o.write = get_write(v.id, id) + o:depends("node", v.id) + o.default = "nil" + o:value("nil", translate("Close")) + o:value("_default", translate("Default")) + o:value("_direct", translate("Direct Connection")) + o:value("_blackhole", translate("Blackhole")) + + local pt = s:taboption("Main", ListValue, vid .. "-".. id .. "_proxy_tag", string.format('* %s', e.remarks .. " " .. translate("Preproxy"))) + pt.cfgvalue = get_cfgvalue(v.id, id .. "_proxy_tag") + pt.write = get_write(v.id, id .. "_proxy_tag") + pt:value("nil", translate("Close")) + pt:value("main", translate("Preproxy Node")) + pt.default = "nil" + for k1, v1 in pairs(balancing_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(normal_list) do + o:value(v1.id, v1.remark) + pt:depends({ [node_option] = v1.id, [vid .. "-preproxy_enabled"] = "1" }) + end + end + end) + + local id = "default_node" + o = s:taboption("Main", Value, vid .. "-" .. id, string.format('* %s', translate("Default"))) + o.cfgvalue = get_cfgvalue(v.id, id) + o.write = get_write(v.id, id) + o:depends("node", v.id) + o.default = "_direct" + o:value("_direct", translate("Direct Connection")) + o:value("_blackhole", translate("Blackhole")) + for k1, v1 in pairs(balancing_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(normal_list) do + o:value(v1.id, v1.remark) + end + if shunt_remark == "default" and auto_switch_tip then + o.description = auto_switch_tip + end + + local id = "default_proxy_tag" + o = s:taboption("Main", ListValue, vid .. "-" .. id, string.format('* %s', translate("Default Preproxy")), translate("When using, localhost will connect this node first and then use this node to connect the default node.")) + o.cfgvalue = get_cfgvalue(v.id, id) + o.write = get_write(v.id, id) + o:value("nil", translate("Close")) + o:value("main", translate("Preproxy Node")) + for k1, v1 in pairs(normal_list) do + if v1.protocol ~= "_balancing" then + o:depends({ [vid .. "-default_node"] = v1.id, [vid .. "-preproxy_enabled"] = "1" }) + end + end + end + else + local tips = s:taboption("Main", DummyValue, "tips", " ") + tips.rawhtml = true + tips.cfgvalue = function(t, n) + return string.format('%s', translate("There are no available nodes, please add or subscribe nodes first.")) + end + tips:depends({ node = "nil", ["!reverse"] = true }) + for k, v in pairs(shunt_list) do + tips:depends("node", v.id) + end + for k, v in pairs(balancing_list) do + tips:depends("node", v.id) + end + end +end + +o = s:taboption("Main", Flag, "localhost_proxy", translate("Localhost Proxy"), translate("When selected, localhost can transparent proxy.")) +o.default = "1" +o.rmempty = false + +node_socks_port = s:taboption("Main", Value, "node_socks_port", translate("Node") .. " Socks " .. translate("Listen Port")) +node_socks_port.default = 1070 +node_socks_port.datatype = "port" + +--[[ +if has_v2ray or has_xray then + node_http_port = s:taboption("Main", Value, "node_http_port", translate("Node") .. " HTTP " .. translate("Listen Port") .. " " .. translate("0 is not use")) + node_http_port.default = 0 + node_http_port.datatype = "port" +end +]]-- + +s:tab("DNS", translate("DNS")) + +o = s:taboption("DNS", ListValue, "direct_dns_protocol", translate("Direct DNS Protocol")) +o.default = "auto" +o:value("auto", translate("Auto")) +o:value("udp", "UDP") +o:value("tcp", "TCP") +o:value("doh", "DoH") + +---- DNS Forward +o = s:taboption("DNS", Value, "direct_dns", translate("Direct DNS")) +o.datatype = "or(ipaddr,ipaddrport)" +o.default = "119.29.29.29" +o:value("114.114.114.114", "114.114.114.114 (114DNS)") +o:value("119.29.29.29", "119.29.29.29 (DNSPod)") +o:value("223.5.5.5", "223.5.5.5 (AliDNS)") +o:depends("direct_dns_protocol", "udp") +o:depends("direct_dns_protocol", "tcp") + +---- DoH +o = s:taboption("DNS", Value, "direct_dns_doh", translate("Direct DNS DoH")) +o.default = "https://223.5.5.5/dns-query" +o:value("https://1.12.12.12/dns-query", "DNSPod 1") +o:value("https://120.53.53.53/dns-query", "DNSPod 2") +o:value("https://223.5.5.5/dns-query", "AliDNS") +o.validate = doh_validate +o:depends("direct_dns_protocol", "doh") + +o = s:taboption("DNS", Value, "direct_dns_client_ip", translate("Direct DNS EDNS Client Subnet")) +o.description = translate("Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address).") .. "
" .. + translate("This feature requires the DNS server to support the Edns Client Subnet (RFC7871).") +o.datatype = "ipaddr" +o:depends("direct_dns_protocol", "tcp") +o:depends("direct_dns_protocol", "doh") + +o = s:taboption("DNS", ListValue, "direct_dns_query_strategy", translate("Direct Query Strategy")) +o.default = "UseIP" +o:value("UseIP") +o:value("UseIPv4") +o:value("UseIPv6") + +o = s:taboption("DNS", ListValue, "remote_dns_protocol", translate("Remote DNS Protocol")) +o:value("tcp", "TCP") +o:value("doh", "DoH") +o:value("udp", "UDP") + +---- DNS Forward +o = s:taboption("DNS", Value, "remote_dns", translate("Remote DNS")) +o.datatype = "or(ipaddr,ipaddrport)" +o.default = "1.1.1.1" +o:value("1.1.1.1", "1.1.1.1 (CloudFlare)") +o:value("1.1.1.2", "1.1.1.2 (CloudFlare-Security)") +o:value("8.8.4.4", "8.8.4.4 (Google)") +o:value("8.8.8.8", "8.8.8.8 (Google)") +o:value("9.9.9.9", "9.9.9.9 (Quad9-Recommended)") +o:value("208.67.220.220", "208.67.220.220 (OpenDNS)") +o:value("208.67.222.222", "208.67.222.222 (OpenDNS)") +o:depends("remote_dns_protocol", "tcp") +o:depends("remote_dns_protocol", "udp") + +---- DoH +o = s:taboption("DNS", Value, "remote_dns_doh", translate("Remote DNS DoH")) +o.default = "https://1.1.1.1/dns-query" +o:value("https://1.1.1.1/dns-query", "CloudFlare") +o:value("https://1.1.1.2/dns-query", "CloudFlare-Security") +o:value("https://8.8.4.4/dns-query", "Google 8844") +o:value("https://8.8.8.8/dns-query", "Google 8888") +o:value("https://9.9.9.9/dns-query", "Quad9-Recommended") +o:value("https://208.67.222.222/dns-query", "OpenDNS") +o:value("https://dns.adguard.com/dns-query,176.103.130.130", "AdGuard") +o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "LibreDNS") +o:value("https://doh.libredns.gr/ads,116.202.176.26", "LibreDNS (No Ads)") +o.validate = doh_validate +o:depends("remote_dns_protocol", "doh") + +o = s:taboption("DNS", Value, "remote_dns_client_ip", translate("Remote DNS EDNS Client Subnet")) +o.description = translate("Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address).") .. "
" .. + translate("This feature requires the DNS server to support the Edns Client Subnet (RFC7871).") +o.datatype = "ipaddr" +o:depends("remote_dns_protocol", "tcp") +o:depends("remote_dns_protocol", "doh") + +o = s:taboption("DNS", Flag, "remote_fakedns", "FakeDNS", translate("Use FakeDNS work in the shunt domain that proxy.")) +o.default = "0" +o.rmempty = false + +o = s:taboption("DNS", ListValue, "remote_dns_query_strategy", translate("Remote Query Strategy")) +o.default = "UseIPv4" +o:value("UseIP") +o:value("UseIPv4") +o:value("UseIPv6") + +hosts = s:taboption("DNS", TextValue, "dns_hosts", translate("Domain Override")) +hosts.rows = 5 +hosts.wrap = "off" + +o = s:taboption("DNS", Button, "clear_ipset", translate("Clear IPSET"), translate("Try this feature if the rule modification does not take effect.")) +o.inputstyle = "remove" +function o.write(e, e) + luci.sys.call("[ -n \"$(nft list sets 2>/dev/null | grep \"passwall2_\")\" ] && sh /usr/share/" .. appname .. "/nftables.sh flush_nftset || sh /usr/share/" .. appname .. "/iptables.sh flush_ipset > /dev/null 2>&1 &") + luci.http.redirect(api.url("log")) +end + +s:tab("log", translate("Log")) +o = s:taboption("log", Flag, "close_log", translate("Close Node Log")) +o.rmempty = false + +loglevel = s:taboption("log", ListValue, "loglevel", translate("Log Level")) +loglevel.default = "warning" +loglevel:value("debug") +loglevel:value("info") +loglevel:value("warning") +loglevel:value("error") + +s:tab("faq", "FAQ") + +o = s:taboption("faq", DummyValue, "") +o.template = appname .. "/global/faq" + +-- [[ Socks Server ]]-- +o = s:taboption("Main", Flag, "socks_enabled", "Socks " .. translate("Main switch")) +o.rmempty = false + +s = m:section(TypedSection, "socks", translate("Socks Config")) +s.anonymous = true +s.addremove = true +s.template = "cbi/tblsection" +function s.create(e, t) + TypedSection.create(e, api.gen_short_uuid()) +end + +o = s:option(DummyValue, "status", translate("Status")) +o.rawhtml = true +o.cfgvalue = function(t, n) + return string.format('
', n) +end + +---- Enable +o = s:option(Flag, "enabled", translate("Enable")) +o.default = 1 +o.rmempty = false + +socks_node = s:option(ListValue, "node", translate("Socks Node")) + +local n = 1 +uci:foreach(appname, "socks", function(s) + if s[".name"] == section then + return false + end + n = n + 1 +end) + +o = s:option(Value, "port", "Socks " .. translate("Listen Port")) +o.default = n + 1080 +o.datatype = "port" +o.rmempty = false + +if has_v2ray or has_xray then + o = s:option(Value, "http_port", "HTTP " .. translate("Listen Port") .. " " .. translate("0 is not use")) + o.default = 0 + o.datatype = "port" +end + +for k, v in pairs(nodes_table) do + node:value(v.id, v["remark"]) + if v.type == "Socks" then + if has_v2ray or has_xray then + socks_node:value(v.id, v["remark"]) + end + else + socks_node:value(v.id, v["remark"]) + end +end + +m:append(Template(appname .. "/global/footer")) + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua new file mode 100644 index 000000000..515fa5efa --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua @@ -0,0 +1,140 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local sys = api.sys +local net = require "luci.model.network".init() +local datatypes = api.datatypes + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + obj = e, + remarks = e["remark"] + } + end +end + +m = Map(appname) + +-- [[ Haproxy Settings ]]-- +s = m:section(TypedSection, "global_haproxy") +s.anonymous = true + +s:append(Template(appname .. "/haproxy/status")) + +---- Balancing Enable +o = s:option(Flag, "balancing_enable", translate("Enable Load Balancing")) +o.rmempty = false +o.default = false + +---- Console Username +o = s:option(Value, "console_user", translate("Console Username")) +o.default = "" +o:depends("balancing_enable", true) + +---- Console Password +o = s:option(Value, "console_password", translate("Console Password")) +o.password = true +o.default = "" +o:depends("balancing_enable", true) + +---- Console Port +o = s:option(Value, "console_port", translate("Console Port"), translate( + "In the browser input routing IP plus port access, such as:192.168.1.1:1188")) +o.default = "1188" +o:depends("balancing_enable", true) + +---- Health Check Type +o = s:option(ListValue, "health_check_type", translate("Health Check Type")) +o.default = "passwall_logic" +o:value("tcp", "TCP") +o:value("passwall_logic", translate("Availability test") .. string.format("(passwall %s)", translate("Inner implement"))) +o:depends("balancing_enable", true) + +---- Health Check Inter +o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds")) +o.default = "60" +o:depends("balancing_enable", true) + +o = s:option(DummyValue, "health_check_tips", " ") +o.rawhtml = true +o.cfgvalue = function(t, n) + return string.format('%s', translate("When the availability test is used, the load balancing node will be converted into a Socks node. when node list set customizing, must be a Socks node, otherwise the health check will be invalid.")) +end +o:depends("health_check_type", "passwall_logic") + +-- [[ Balancing Settings ]]-- +s = m:section(TypedSection, "haproxy_config", "", + "" .. + translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. + "\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. + "") +s.template = "cbi/tblsection" +s.sortable = true +s.anonymous = true +s.addremove = true + +s.create = function(e, t) + TypedSection.create(e, api.gen_short_uuid()) +end + +s.remove = function(self, section) + for k, v in pairs(self.children) do + v.rmempty = true + v.validate = nil + end + TypedSection.remove(self, section) +end + +---- Enable +o = s:option(Flag, "enabled", translate("Enable")) +o.default = 1 +o.rmempty = false + +---- Node Address +o = s:option(Value, "lbss", translate("Node Address")) +for k, v in pairs(nodes_table) do o:value(v.id, v.remarks) end +o.rmempty = false +o.validate = function(self, value) + if not value then return nil end + local t = m:get(value) or nil + if t and t[".type"] == "nodes" then + return value + end + if datatypes.hostport(value) or datatypes.ip4addrport(value) then + return value + end + if api.is_ipv6addrport(value) then + return value + end + return nil, value +end + +---- Haproxy Port +o = s:option(Value, "haproxy_port", translate("Haproxy Port")) +o.datatype = "port" +o.default = 1181 +o.rmempty = false + +---- Node Weight +o = s:option(Value, "lbweight", translate("Node Weight")) +o.datatype = "uinteger" +o.default = 5 +o.rmempty = false + +---- Export +o = s:option(ListValue, "export", translate("Export Of Multi WAN")) +o:value(0, translate("Auto")) +local wa = require "luci.tools.webadmin" +wa.cbi_add_networks(o) +o.default = 0 +o.rmempty = false + +---- Mode +o = s:option(ListValue, "backup", translate("Mode")) +o:value(0, translate("Primary")) +o:value(1, translate("Standby")) +o.rmempty = false + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/log.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/log.lua new file mode 100644 index 000000000..ff7e74fa1 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/log.lua @@ -0,0 +1,8 @@ +local api = require "luci.passwall2.api" +local appname = api.appname + +f = SimpleForm(appname) +f.reset = false +f.submit = false +f:append(Template(appname .. "/log/log")) +return f diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua new file mode 100644 index 000000000..226ae562c --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua @@ -0,0 +1,904 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local uci = api.uci + +if not arg[1] or not uci:get(appname, arg[1]) then + luci.http.redirect(api.url("node_list")) +end + +local ss_encrypt_method_list = { + "rc4-md5", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", + "aes-192-ctr", "aes-256-ctr", "bf-cfb", "salsa20", "chacha20", "chacha20-ietf", + "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305" +} + +local ss_rust_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-chacha8-poly1305", "2022-blake3-chacha20-poly1305" +} + +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" +} + +local v_ss_encrypt_method_list = { + "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305" +} + +local x_ss_encrypt_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 security_list = {"none", "auto", "aes-128-gcm", "chacha20-poly1305", "zero"} + +local header_type_list = { + "none", "srtp", "utp", "wechat-video", "dtls", "wireguard" +} +local encrypt_methods_ss_aead = { + "chacha20-ietf-poly1305", + "aes-128-gcm", + "aes-256-gcm", +} + +m = Map(appname, translate("Node Config")) +m.redirect = api.url() + +s = m:section(NamedSection, arg[1], "nodes", "") +s.addremove = false +s.dynamic = false + +share = s:option(DummyValue, "passwall2", " ") +share.rawhtml = true +share.template = "passwall2/node_list/link_share_man" +share.value = arg[1] + +remarks = s:option(Value, "remarks", translate("Node Remarks")) +remarks.default = translate("Remarks") +remarks.rmempty = false + +type = s:option(ListValue, "type", translate("Type")) +if api.is_finded("ss-redir") then + type:value("SS", translate("Shadowsocks Libev")) +end +if api.is_finded("sslocal") then + type:value("SS-Rust", translate("Shadowsocks Rust")) +end +if api.is_finded("ssr-redir") then + type:value("SSR", translate("ShadowsocksR Libev")) +end +if api.is_finded("v2ray") then + type:value("V2ray", translate("V2ray")) +end +if api.is_finded("xray") then + type:value("Xray", translate("Xray")) +end +if api.is_finded("brook") then + type:value("Brook", translate("Brook")) +end +if api.is_finded("naive") then + type:value("Naiveproxy", translate("NaiveProxy")) +end +if api.is_finded("hysteria") then + type:value("Hysteria", translate("Hysteria")) +end + +protocol = s:option(ListValue, "protocol", translate("Protocol")) +protocol:value("vmess", translate("Vmess")) +protocol:value("vless", translate("VLESS")) +protocol:value("http", translate("HTTP")) +protocol:value("socks", translate("Socks")) +protocol:value("shadowsocks", translate("Shadowsocks")) +protocol:value("trojan", translate("Trojan")) +protocol:value("wireguard", translate("WireGuard")) +protocol:value("_balancing", translate("Balancing")) +protocol:value("_shunt", translate("Shunt")) +protocol:value("_iface", translate("Custom Interface") .. " (Only Support Xray)") +protocol:depends("type", "V2ray") +protocol:depends("type", "Xray") + + +iface = s:option(Value, "iface", translate("Interface")) +iface.default = "eth1" +iface:depends("protocol", "_iface") + +local nodes_table = {} +local balancers_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remarks = e["remark"] + } + end + if e.protocol == "_balancing" then + balancers_table[#balancers_table + 1] = { + id = e[".name"], + remarks = e["remark"] + } + end +end + +-- 负载均衡列表 +local balancing_node = s:option(DynamicList, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, document")) +for k, v in pairs(nodes_table) do balancing_node:value(v.id, v.remarks) end +balancing_node:depends("protocol", "_balancing") + +local balancingStrategy = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy")) +balancingStrategy:depends("protocol", "_balancing") +balancingStrategy:value("random") +balancingStrategy:value("leastPing") +balancingStrategy.default = "random" +-- 探测地址 +local useCustomProbeUrl = s:option(Flag, "useCustomProbeUrl", translate("Use Custome Probe URL"), translate("By default the built-in probe URL will be used, enable this option to use a custom probe URL.")) +useCustomProbeUrl:depends("balancingStrategy", "leastPing") +local probeUrl = s:option(Value, "probeUrl", translate("Probe URL")) +probeUrl:depends("useCustomProbeUrl", true) +probeUrl.default = "https://www.google.com/generate_204" +probeUrl.description = translate("The URL used to detect the connection status.") +-- 探测间隔 +local probeInterval = s:option(Value, "probeInterval", translate("Probe Interval")) +probeInterval:depends("balancingStrategy", "leastPing") +probeInterval.default = "1m" +probeInterval.description = translate("The interval between initiating probes. Every time this time elapses, a server status check is performed on a server. The time format is numbers + units, such as '10s', '2h45m', and the supported time units are ns, us, ms, s, m, h, which correspond to nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, respectively.") + +-- 分流 +if #nodes_table > 0 then + o = s:option(Flag, "preproxy_enabled", translate("Preproxy")) + o:depends("protocol", "_shunt") + o = s:option(Value, "main_node", string.format('%s', translate("Preproxy Node")), translate("Set the node to be used as a pre-proxy. Each rule (including Default) has a separate switch that controls whether this rule uses the pre-proxy or not.")) + o:depends("preproxy_enabled", "1") + for k, v in pairs(balancers_table) do + o:value(v.id, v.remarks) + end + for k, v in pairs(nodes_table) do + o:value(v.id, v.remarks) + end + if #o.keylist > 0 then + o.default = o.keylist[1] + end +end +uci:foreach(appname, "shunt_rules", function(e) + if e[".name"] and e.remarks then + o = s:option(Value, e[".name"], string.format('* %s', api.url("shunt_rules", e[".name"]), e.remarks)) + o.default = "nil" + o:value("nil", translate("Close")) + o:value("_default", translate("Default")) + o:value("_direct", translate("Direct Connection")) + o:value("_blackhole", translate("Blackhole")) + o:depends("protocol", "_shunt") + + if #nodes_table > 0 then + for k, v in pairs(balancers_table) do + o:value(v.id, v.remarks) + end + local pt = s:option(ListValue, e[".name"] .. "_proxy_tag", string.format('* %s', e.remarks .. " " .. translate("Preproxy"))) + pt:value("nil", translate("Close")) + pt:value("main", translate("Preproxy Node")) + pt.default = "nil" + for k, v in pairs(nodes_table) do + o:value(v.id, v.remarks) + pt:depends({ preproxy_enabled = "1", [e[".name"]] = v.id }) + end + end + end +end) + +shunt_tips = s:option(DummyValue, "shunt_tips", " ") +shunt_tips.rawhtml = true +shunt_tips.cfgvalue = function(t, n) + return string.format('%s', translate("No shunt rules? Click me to go to add.")) +end +shunt_tips:depends("protocol", "_shunt") + +local default_node = s:option(Value, "default_node", string.format('* %s', translate("Default"))) +default_node:depends("protocol", "_shunt") +default_node.default = "_direct" +default_node:value("_direct", translate("Direct Connection")) +default_node:value("_blackhole", translate("Blackhole")) + +if #nodes_table > 0 then + for k, v in pairs(balancers_table) do + default_node:value(v.id, v.remarks) + end + local dpt = s:option(ListValue, "default_proxy_tag", string.format('* %s', translate("Default Preproxy")), translate("When using, localhost will connect this node first and then use this node to connect the default node.")) + dpt:value("nil", translate("Close")) + dpt:value("main", translate("Preproxy Node")) + dpt.default = "nil" + for k, v in pairs(nodes_table) do + default_node:value(v.id, v.remarks) + dpt:depends({ preproxy_enabled = "1", default_node = v.id }) + end +end + +domainStrategy = s:option(ListValue, "domainStrategy", translate("Domain Strategy")) +domainStrategy:value("AsIs") +domainStrategy:value("IPIfNonMatch") +domainStrategy:value("IPOnDemand") +domainStrategy.default = "IPOnDemand" +domainStrategy.description = "
" +domainStrategy:depends("protocol", "_shunt") + +domainMatcher = s:option(ListValue, "domainMatcher", translate("Domain matcher")) +domainMatcher:value("hybrid") +domainMatcher:value("linear") +domainMatcher:depends("protocol", "_shunt") + + +-- Brook协议 +brook_protocol = s:option(ListValue, "brook_protocol", translate("Protocol")) +brook_protocol:value("client", translate("Brook")) +brook_protocol:value("wsclient", translate("WebSocket")) +brook_protocol:depends("type", "Brook") +function brook_protocol.cfgvalue(self, section) + return m:get(section, "protocol") +end +function brook_protocol.write(self, section, value) + m:set(section, "protocol", value) +end + +brook_tls = s:option(Flag, "brook_tls", translate("Use TLS")) +brook_tls:depends("brook_protocol", "wsclient") + +-- Naiveproxy协议 +naiveproxy_protocol = s:option(ListValue, "naiveproxy_protocol", translate("Protocol")) +naiveproxy_protocol:value("https", translate("HTTPS")) +naiveproxy_protocol:value("quic", translate("QUIC")) +naiveproxy_protocol:depends("type", "Naiveproxy") +function naiveproxy_protocol.cfgvalue(self, section) + return m:get(section, "protocol") +end +function naiveproxy_protocol.write(self, section, value) + m:set(section, "protocol", value) +end + +address = s:option(Value, "address", translate("Address (Support Domain Name)")) +address.rmempty = false +address:depends("type", "SS") +address:depends("type", "SS-Rust") +address:depends("type", "SSR") +address:depends("type", "Brook") +address:depends("type", "Naiveproxy") +address:depends("type", "Hysteria") +address:depends({ type = "V2ray", protocol = "vmess" }) +address:depends({ type = "V2ray", protocol = "vless" }) +address:depends({ type = "V2ray", protocol = "http" }) +address:depends({ type = "V2ray", protocol = "socks" }) +address:depends({ type = "V2ray", protocol = "shadowsocks" }) +address:depends({ type = "V2ray", protocol = "trojan" }) +address:depends({ type = "Xray", protocol = "vmess" }) +address:depends({ type = "Xray", protocol = "vless" }) +address:depends({ type = "Xray", protocol = "http" }) +address:depends({ type = "Xray", protocol = "socks" }) +address:depends({ type = "Xray", protocol = "shadowsocks" }) +address:depends({ type = "Xray", protocol = "trojan" }) +address:depends({ type = "Xray", protocol = "wireguard" }) + +--[[ +use_ipv6 = s:option(Flag, "use_ipv6", translate("Use IPv6")) +use_ipv6.default = 0 +use_ipv6:depends("type", "SS") +use_ipv6:depends("type", "SS-Rust") +use_ipv6:depends("type", "SSR") +use_ipv6:depends("type", "Brook") +use_ipv6:depends("type", "Hysteria") +use_ipv6:depends({ type = "V2ray", protocol = "vmess" }) +use_ipv6:depends({ type = "V2ray", protocol = "vless" }) +use_ipv6:depends({ type = "V2ray", protocol = "http" }) +use_ipv6:depends({ type = "V2ray", protocol = "socks" }) +use_ipv6:depends({ type = "V2ray", protocol = "shadowsocks" }) +use_ipv6:depends({ type = "V2ray", protocol = "trojan" }) +use_ipv6:depends({ type = "Xray", protocol = "vmess" }) +use_ipv6:depends({ type = "Xray", protocol = "vless" }) +use_ipv6:depends({ type = "Xray", protocol = "http" }) +use_ipv6:depends({ type = "Xray", protocol = "socks" }) +use_ipv6:depends({ type = "Xray", protocol = "shadowsocks" }) +use_ipv6:depends({ type = "Xray", protocol = "trojan" }) +--]] + +port = s:option(Value, "port", translate("Port")) +port.datatype = "port" +port.rmempty = false +port:depends("type", "SS") +port:depends("type", "SS-Rust") +port:depends("type", "SSR") +port:depends("type", "Brook") +port:depends("type", "Naiveproxy") +port:depends("type", "Hysteria") +port:depends({ type = "V2ray", protocol = "vmess" }) +port:depends({ type = "V2ray", protocol = "vless" }) +port:depends({ type = "V2ray", protocol = "http" }) +port:depends({ type = "V2ray", protocol = "socks" }) +port:depends({ type = "V2ray", protocol = "shadowsocks" }) +port:depends({ type = "V2ray", protocol = "trojan" }) +port:depends({ type = "Xray", protocol = "vmess" }) +port:depends({ type = "Xray", protocol = "vless" }) +port:depends({ type = "Xray", protocol = "http" }) +port:depends({ type = "Xray", protocol = "socks" }) +port:depends({ type = "Xray", protocol = "shadowsocks" }) +port:depends({ type = "Xray", protocol = "trojan" }) +port:depends({ type = "Xray", protocol = "wireguard" }) + +hysteria_hop = s:option(Value, "hysteria_hop", translate("Additional ports for hysteria hop")) +hysteria_hop:depends("type", "Hysteria") + +username = s:option(Value, "username", translate("Username")) +username:depends("type", "Naiveproxy") +username:depends({ type = "V2ray", protocol = "http" }) +username:depends({ type = "V2ray", protocol = "socks" }) +username:depends({ type = "Xray", protocol = "http" }) +username:depends({ type = "Xray", protocol = "socks" }) + +password = s:option(Value, "password", translate("Password")) +password.password = true +password:depends("type", "SS") +password:depends("type", "SS-Rust") +password:depends("type", "SSR") +password:depends("type", "Brook") +password:depends("type", "Naiveproxy") +password:depends({ type = "V2ray", protocol = "http" }) +password:depends({ type = "V2ray", protocol = "socks" }) +password:depends({ type = "V2ray", protocol = "shadowsocks" }) +password:depends({ type = "V2ray", protocol = "trojan" }) +password:depends({ type = "Xray", protocol = "http" }) +password:depends({ type = "Xray", protocol = "socks" }) +password:depends({ type = "Xray", protocol = "shadowsocks" }) +password:depends({ type = "Xray", protocol = "trojan" }) + +hysteria_protocol = s:option(ListValue, "hysteria_protocol", translate("Protocol")) +hysteria_protocol:value("udp", "UDP") +hysteria_protocol:value("faketcp", "faketcp") +hysteria_protocol:value("wechat-video", "wechat-video") +hysteria_protocol:depends("type", "Hysteria") +function hysteria_protocol.cfgvalue(self, section) + return m:get(section, "protocol") +end +function hysteria_protocol.write(self, section, value) + m:set(section, "protocol", value) +end + +hysteria_obfs = s:option(Value, "hysteria_obfs", translate("Obfs Password")) +hysteria_obfs:depends("type", "Hysteria") + +hysteria_auth_type = s:option(ListValue, "hysteria_auth_type", translate("Auth Type")) +hysteria_auth_type:value("disable", translate("Disable")) +hysteria_auth_type:value("string", translate("STRING")) +hysteria_auth_type:value("base64", translate("BASE64")) +hysteria_auth_type:depends("type", "Hysteria") + +hysteria_auth_password = s:option(Value, "hysteria_auth_password", translate("Auth Password")) +hysteria_auth_password.password = true +hysteria_auth_password:depends("hysteria_auth_type", "string") +hysteria_auth_password:depends("hysteria_auth_type", "base64") + +hysteria_alpn = s:option(Value, "hysteria_alpn", translate("QUIC TLS ALPN")) +hysteria_alpn:depends("type", "Hysteria") + +ss_encrypt_method = s:option(Value, "ss_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(ss_encrypt_method_list) do ss_encrypt_method:value(t) end +ss_encrypt_method:depends("type", "SS") +function ss_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function ss_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +ss_rust_encrypt_method = s:option(Value, "ss_rust_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(ss_rust_encrypt_method_list) do ss_rust_encrypt_method:value(t) end +ss_rust_encrypt_method:depends("type", "SS-Rust") +function ss_rust_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function ss_rust_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +ssr_encrypt_method = s:option(Value, "ssr_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(ssr_encrypt_method_list) do ssr_encrypt_method:value(t) end +ssr_encrypt_method:depends("type", "SSR") +function ssr_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function ssr_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +security = s:option(ListValue, "security", translate("Encrypt Method")) +for a, t in ipairs(security_list) do security:value(t) end +security:depends({ type = "V2ray", protocol = "vmess" }) +security:depends({ type = "Xray", protocol = "vmess" }) + +encryption = s:option(Value, "encryption", translate("Encrypt Method")) +encryption.default = "none" +encryption:value("none") +encryption:depends({ type = "V2ray", protocol = "vless" }) +encryption:depends({ type = "Xray", protocol = "vless" }) + +v_ss_encrypt_method = s:option(ListValue, "v_ss_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(v_ss_encrypt_method_list) do v_ss_encrypt_method:value(t) end +v_ss_encrypt_method:depends({ type = "V2ray", protocol = "shadowsocks" }) +function v_ss_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function v_ss_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +x_ss_encrypt_method = s:option(ListValue, "x_ss_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(x_ss_encrypt_method_list) do x_ss_encrypt_method:value(t) end +x_ss_encrypt_method:depends({ type = "Xray", protocol = "shadowsocks" }) +function x_ss_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function x_ss_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +iv_check = s:option(Flag, "iv_check", translate("IV Check")) +iv_check:depends({ type = "V2ray", protocol = "shadowsocks" }) +iv_check:depends({ type = "Xray", protocol = "shadowsocks", x_ss_encrypt_method = "aes-128-gcm" }) +iv_check:depends({ type = "Xray", protocol = "shadowsocks", x_ss_encrypt_method = "aes-256-gcm" }) +iv_check:depends({ type = "Xray", protocol = "shadowsocks", x_ss_encrypt_method = "chacha20-poly1305" }) +iv_check:depends({ type = "Xray", protocol = "shadowsocks", x_ss_encrypt_method = "xchacha20-poly1305" }) + +uot = s:option(Flag, "uot", translate("UDP over TCP"), translate("Need Xray-core or sing-box as server side.")) +uot:depends({ type = "Xray", protocol = "shadowsocks", x_ss_encrypt_method = "2022-blake3-aes-128-gcm" }) +uot:depends({ type = "Xray", protocol = "shadowsocks", x_ss_encrypt_method = "2022-blake3-aes-256-gcm" }) +uot:depends({ type = "Xray", protocol = "shadowsocks", x_ss_encrypt_method = "2022-blake3-chacha20-poly1305" }) + +ssr_protocol = s:option(Value, "ssr_protocol", translate("Protocol")) +for a, t in ipairs(ssr_protocol_list) do ssr_protocol:value(t) end +ssr_protocol:depends("type", "SSR") +function ssr_protocol.cfgvalue(self, section) + return m:get(section, "protocol") +end +function ssr_protocol.write(self, section, value) + m:set(section, "protocol", value) +end + +protocol_param = s:option(Value, "protocol_param", translate("Protocol_param")) +protocol_param:depends("type", "SSR") + +obfs = s:option(Value, "obfs", translate("Obfs")) +for a, t in ipairs(ssr_obfs_list) do obfs:value(t) end +obfs:depends("type", "SSR") + +obfs_param = s:option(Value, "obfs_param", translate("Obfs_param")) +obfs_param:depends("type", "SSR") + +timeout = s:option(Value, "timeout", translate("Connection Timeout")) +timeout.datatype = "uinteger" +timeout.default = 300 +timeout:depends("type", "SS") +timeout:depends("type", "SS-Rust") +timeout:depends("type", "SSR") + +tcp_fast_open = s:option(ListValue, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) +tcp_fast_open:value("false") +tcp_fast_open:value("true") +tcp_fast_open:depends("type", "SS") +tcp_fast_open:depends("type", "SS-Rust") +tcp_fast_open:depends("type", "SSR") + +fast_open = s:option(Flag, "fast_open", translate("Fast Open")) +fast_open.default = "0" +fast_open:depends("type", "Hysteria") + +ss_plugin = s:option(ListValue, "ss_plugin", translate("plugin")) +ss_plugin:value("none", translate("none")) +if api.is_finded("xray-plugin") then ss_plugin:value("xray-plugin") end +if api.is_finded("v2ray-plugin") then ss_plugin:value("v2ray-plugin") end +if api.is_finded("obfs-local") then ss_plugin:value("obfs-local") end +ss_plugin:depends("type", "SS") +ss_plugin:depends("type", "SS-Rust") +function ss_plugin.cfgvalue(self, section) + return m:get(section, "plugin") +end +function ss_plugin.write(self, section, value) + m:set(section, "plugin", value) +end + +ss_plugin_opts = s:option(Value, "ss_plugin_opts", translate("opts")) +ss_plugin_opts:depends("ss_plugin", "xray-plugin") +ss_plugin_opts:depends("ss_plugin", "v2ray-plugin") +ss_plugin_opts:depends("ss_plugin", "obfs-local") +function ss_plugin_opts.cfgvalue(self, section) + return m:get(section, "plugin_opts") +end +function ss_plugin_opts.write(self, section, value) + m:set(section, "plugin_opts", value) +end + +uuid = s:option(Value, "uuid", translate("ID")) +uuid.password = true +uuid:depends({ type = "V2ray", protocol = "vmess" }) +uuid:depends({ type = "V2ray", protocol = "vless" }) +uuid:depends({ type = "Xray", protocol = "vmess" }) +uuid:depends({ type = "Xray", protocol = "vless" }) + +tls = s:option(Flag, "tls", translate("TLS")) +tls.default = 0 +tls:depends({ type = "V2ray", protocol = "vmess" }) +tls:depends({ type = "V2ray", protocol = "vless" }) +tls:depends({ type = "V2ray", protocol = "socks" }) +tls:depends({ type = "V2ray", protocol = "trojan" }) +tls:depends({ type = "V2ray", protocol = "shadowsocks" }) +tls:depends({ type = "Xray", protocol = "vmess" }) +tls:depends({ type = "Xray", protocol = "vless" }) +tls:depends({ type = "Xray", protocol = "socks" }) +tls:depends({ type = "Xray", protocol = "trojan" }) +tls:depends({ type = "Xray", protocol = "shadowsocks" }) + +tlsflow = s:option(Value, "tlsflow", translate("flow")) +tlsflow.default = "" +tlsflow:value("", translate("Disable")) +tlsflow:value("xtls-rprx-vision") +tlsflow:value("xtls-rprx-vision-udp443") +tlsflow:depends({ type = "Xray", protocol = "vless", tls = true, transport = "tcp" }) + +reality = s:option(Flag, "reality", translate("REALITY"), translate("Only recommend to use with VLESS-TCP-XTLS-Vision.")) +reality.default = 0 +reality:depends({ type = "Xray", tls = true, transport = "tcp" }) +reality:depends({ type = "Xray", tls = true, transport = "h2" }) +reality:depends({ type = "Xray", tls = true, transport = "grpc" }) + +alpn = s:option(ListValue, "alpn", translate("alpn")) +alpn.default = "default" +alpn:value("default", translate("Default")) +alpn:value("h2,http/1.1") +alpn:value("h2") +alpn:value("http/1.1") +alpn:depends({ type = "V2ray", tls = true }) +alpn:depends({ type = "Xray", tls = true, reality = false }) + +tls_serverName = s:option(Value, "tls_serverName", translate("Domain")) +tls_serverName:depends("tls", true) +tls_serverName:depends("type", "Hysteria") + +tls_allowInsecure = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) +tls_allowInsecure.default = "0" +tls_allowInsecure:depends({ tls = true, reality = false }) +tls_allowInsecure:depends("type", "Hysteria") + +xray_fingerprint = s:option(Value, "xray_fingerprint", translate("Finger Print"), translate("Avoid using randomized, unless you have to.")) +xray_fingerprint:value("", translate("Disable")) +xray_fingerprint:value("chrome") +xray_fingerprint:value("firefox") +xray_fingerprint:value("safari") +xray_fingerprint:value("ios") +--xray_fingerprint:value("android") +xray_fingerprint:value("edge") +--xray_fingerprint:value("360") +xray_fingerprint:value("qq") +xray_fingerprint:value("random") +xray_fingerprint:value("randomized") +xray_fingerprint.default = "" +xray_fingerprint:depends({ type = "Xray", tls = true, reality = false }) +function xray_fingerprint.cfgvalue(self, section) + return m:get(section, "fingerprint") +end +function xray_fingerprint.write(self, section, value) + m:set(section, "fingerprint", value) +end +function xray_fingerprint.remove(self, section) + m:del(section, "fingerprint") +end + + +-- [[ REALITY部分 ]] -- +reality_publicKey = s:option(Value, "reality_publicKey", translate("Public Key")) +reality_publicKey:depends({ type = "Xray", tls = true, reality = true }) + +reality_shortId = s:option(Value, "reality_shortId", translate("Short Id")) +reality_shortId:depends({ type = "Xray", tls = true, reality = true }) + +reality_spiderX = s:option(Value, "reality_spiderX", translate("Spider X")) +reality_spiderX.placeholder = "/" +reality_spiderX:depends({ type = "Xray", tls = true, reality = true }) + +reality_fingerprint = s:option(Value, "reality_fingerprint", translate("Finger Print"), translate("Avoid using randomized, unless you have to.")) +reality_fingerprint:value("chrome") +reality_fingerprint:value("firefox") +reality_fingerprint:value("safari") +reality_fingerprint:value("ios") +--reality_fingerprint:value("android") +reality_fingerprint:value("edge") +--reality_fingerprint:value("360") +reality_fingerprint:value("qq") +reality_fingerprint:value("random") +reality_fingerprint:value("randomized") +reality_fingerprint.default = "chrome" +reality_fingerprint:depends({ type = "Xray", tls = true, reality = true }) +function reality_fingerprint.cfgvalue(self, section) + return m:get(section, "fingerprint") +end +function reality_fingerprint.write(self, section, value) + m:set(section, "fingerprint", value) +end + +transport = s:option(ListValue, "transport", translate("Transport")) +transport:value("tcp", "TCP") +transport:value("mkcp", "mKCP") +transport:value("ws", "WebSocket") +transport:value("h2", "HTTP/2") +transport:value("ds", "DomainSocket") +transport:value("quic", "QUIC") +transport:value("grpc", "gRPC") +transport:depends({ type = "V2ray", protocol = "vmess" }) +transport:depends({ type = "V2ray", protocol = "vless" }) +transport:depends({ type = "V2ray", protocol = "socks" }) +transport:depends({ type = "V2ray", protocol = "shadowsocks" }) +transport:depends({ type = "V2ray", protocol = "trojan" }) +transport:depends({ type = "Xray", protocol = "vmess" }) +transport:depends({ type = "Xray", protocol = "vless" }) +transport:depends({ type = "Xray", protocol = "socks" }) +transport:depends({ type = "Xray", protocol = "shadowsocks" }) +transport:depends({ type = "Xray", protocol = "trojan" }) + +--[[ +ss_transport = s:option(ListValue, "ss_transport", translate("Transport")) +ss_transport:value("ws", "WebSocket") +ss_transport:value("h2", "HTTP/2") +ss_transport:value("h2+ws", "HTTP/2 & WebSocket") +ss_transport:depends({ type = "V2ray", protocol = "shadowsocks" }) +ss_transport:depends({ type = "Xray", protocol = "shadowsocks" }) +]]-- + +wireguard_public_key = s:option(Value, "wireguard_public_key", translate("Public Key")) +wireguard_public_key:depends({ type = "Xray", protocol = "wireguard" }) + +wireguard_secret_key = s:option(Value, "wireguard_secret_key", translate("Private Key")) +wireguard_secret_key:depends({ type = "Xray", protocol = "wireguard" }) + +wireguard_preSharedKey = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key")) +wireguard_preSharedKey:depends({ type = "Xray", protocol = "wireguard" }) + +wireguard_local_address = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) +wireguard_local_address:depends({ type = "Xray", protocol = "wireguard" }) + +wireguard_mtu = s:option(Value, "wireguard_mtu", translate("MTU")) +wireguard_mtu.default = "1420" +wireguard_mtu:depends({ type = "Xray", protocol = "wireguard" }) + +if api.compare_versions(api.get_app_version("xray"), ">=", "1.8.0") then + wireguard_reserved = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) + wireguard_reserved:depends({ type = "Xray", protocol = "wireguard" }) +end + +wireguard_keepAlive = s:option(Value, "wireguard_keepAlive", translate("Keep Alive")) +wireguard_keepAlive.default = "0" +wireguard_keepAlive:depends({ type = "Xray", protocol = "wireguard" }) + +-- [[ TCP部分 ]]-- + +-- TCP伪装 +tcp_guise = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) +tcp_guise:value("none", "none") +tcp_guise:value("http", "http") +tcp_guise:depends("transport", "tcp") + +-- HTTP域名 +tcp_guise_http_host = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) +tcp_guise_http_host:depends("tcp_guise", "http") + +-- HTTP路径 +tcp_guise_http_path = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) +tcp_guise_http_path.placeholder = "/" +tcp_guise_http_path:depends("tcp_guise", "http") + +-- [[ mKCP部分 ]]-- + +mkcp_guise = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)')) +for a, t in ipairs(header_type_list) do mkcp_guise:value(t) end +mkcp_guise:depends("transport", "mkcp") + +mkcp_mtu = s:option(Value, "mkcp_mtu", translate("KCP MTU")) +mkcp_mtu.default = "1350" +mkcp_mtu:depends("transport", "mkcp") + +mkcp_tti = s:option(Value, "mkcp_tti", translate("KCP TTI")) +mkcp_tti.default = "20" +mkcp_tti:depends("transport", "mkcp") + +mkcp_uplinkCapacity = s:option(Value, "mkcp_uplinkCapacity", translate("KCP uplinkCapacity")) +mkcp_uplinkCapacity.default = "5" +mkcp_uplinkCapacity:depends("transport", "mkcp") + +mkcp_downlinkCapacity = s:option(Value, "mkcp_downlinkCapacity", translate("KCP downlinkCapacity")) +mkcp_downlinkCapacity.default = "20" +mkcp_downlinkCapacity:depends("transport", "mkcp") + +mkcp_congestion = s:option(Flag, "mkcp_congestion", translate("KCP Congestion")) +mkcp_congestion:depends("transport", "mkcp") + +mkcp_readBufferSize = s:option(Value, "mkcp_readBufferSize", translate("KCP readBufferSize")) +mkcp_readBufferSize.default = "1" +mkcp_readBufferSize:depends("transport", "mkcp") + +mkcp_writeBufferSize = s:option(Value, "mkcp_writeBufferSize", translate("KCP writeBufferSize")) +mkcp_writeBufferSize.default = "1" +mkcp_writeBufferSize:depends("transport", "mkcp") + +mkcp_seed = s:option(Value, "mkcp_seed", translate("KCP Seed")) +mkcp_seed:depends("transport", "mkcp") + +-- [[ WebSocket部分 ]]-- +ws_host = s:option(Value, "ws_host", translate("WebSocket Host")) +ws_host:depends("transport", "ws") +ws_host:depends("ss_transport", "ws") + +ws_path = s:option(Value, "ws_path", translate("WebSocket Path")) +ws_path.placeholder = "/" +ws_path:depends("transport", "ws") +ws_path:depends("ss_transport", "ws") +ws_path:depends({ type = "Brook", brook_protocol = "wsclient" }) + +ws_enableEarlyData = s:option(Flag, "ws_enableEarlyData", translate("Enable early data")) +ws_enableEarlyData:depends({ type = "V2ray", transport = "ws" }) + +ws_maxEarlyData = s:option(Value, "ws_maxEarlyData", translate("Early data length")) +ws_maxEarlyData.default = "1024" +ws_maxEarlyData:depends("ws_enableEarlyData", true) + +ws_earlyDataHeaderName = s:option(Value, "ws_earlyDataHeaderName", translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol")) +ws_earlyDataHeaderName:depends("ws_enableEarlyData", true) + +-- [[ HTTP/2部分 ]]-- +h2_host = s:option(Value, "h2_host", translate("HTTP/2 Host")) +h2_host:depends("transport", "h2") +h2_host:depends("ss_transport", "h2") + +h2_path = s:option(Value, "h2_path", translate("HTTP/2 Path")) +h2_path.placeholder = "/" +h2_path:depends("transport", "h2") +h2_path:depends("ss_transport", "h2") + +h2_health_check = s:option(Flag, "h2_health_check", translate("Health check")) +h2_health_check:depends({ type = "Xray", transport = "h2"}) + +h2_read_idle_timeout = s:option(Value, "h2_read_idle_timeout", translate("Idle timeout")) +h2_read_idle_timeout.default = "10" +h2_read_idle_timeout:depends("h2_health_check", true) + +h2_health_check_timeout = s:option(Value, "h2_health_check_timeout", translate("Health check timeout")) +h2_health_check_timeout.default = "15" +h2_health_check_timeout:depends("h2_health_check", true) + +-- [[ DomainSocket部分 ]]-- +ds_path = s:option(Value, "ds_path", "Path", translate("A legal file path. This file must not exist before running.")) +ds_path:depends("transport", "ds") + +-- [[ QUIC部分 ]]-- +quic_security = s:option(ListValue, "quic_security", translate("Encrypt Method")) +quic_security:value("none") +quic_security:value("aes-128-gcm") +quic_security:value("chacha20-poly1305") +quic_security:depends("transport", "quic") + +quic_key = s:option(Value, "quic_key", translate("Encrypt Method") .. translate("Key")) +quic_key:depends("transport", "quic") + +quic_guise = s:option(ListValue, "quic_guise", translate("Camouflage Type")) +for a, t in ipairs(header_type_list) do quic_guise:value(t) end +quic_guise:depends("transport", "quic") + +-- [[ gRPC部分 ]]-- +grpc_serviceName = s:option(Value, "grpc_serviceName", "ServiceName") +grpc_serviceName:depends("transport", "grpc") + +grpc_mode = s:option(ListValue, "grpc_mode", "gRPC " .. translate("Transfer mode")) +grpc_mode:value("gun") +grpc_mode:value("multi") +grpc_mode:depends({ type = "Xray", transport = "grpc"}) + +grpc_health_check = s:option(Flag, "grpc_health_check", translate("Health check")) +grpc_health_check:depends({ type = "Xray", transport = "grpc"}) + +grpc_idle_timeout = s:option(Value, "grpc_idle_timeout", translate("Idle timeout")) +grpc_idle_timeout.default = "10" +grpc_idle_timeout:depends("grpc_health_check", true) + +grpc_health_check_timeout = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout")) +grpc_health_check_timeout.default = "20" +grpc_health_check_timeout:depends("grpc_health_check", true) + +grpc_permit_without_stream = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream")) +grpc_permit_without_stream.default = "0" +grpc_permit_without_stream:depends("grpc_health_check", true) + +grpc_initial_windows_size = s:option(Value, "grpc_initial_windows_size", translate("Initial Windows Size")) +grpc_initial_windows_size.default = "0" +grpc_initial_windows_size:depends({ type = "Xray", transport = "grpc"}) + +-- [[ Mux ]]-- +mux = s:option(Flag, "mux", translate("Mux")) +mux:depends({ type = "V2ray", protocol = "vmess" }) +mux:depends({ type = "V2ray", protocol = "vless" }) +mux:depends({ type = "V2ray", protocol = "http" }) +mux:depends({ type = "V2ray", protocol = "socks" }) +mux:depends({ type = "V2ray", protocol = "shadowsocks" }) +mux:depends({ type = "V2ray", protocol = "trojan" }) +mux:depends({ type = "Xray", protocol = "vmess" }) +mux:depends({ type = "Xray", protocol = "vless", tlsflow = "" }) +mux:depends({ type = "Xray", protocol = "http" }) +mux:depends({ type = "Xray", protocol = "socks" }) +mux:depends({ type = "Xray", protocol = "shadowsocks" }) +mux:depends({ type = "Xray", protocol = "trojan" }) + +-- [[ XUDP Mux ]]-- +xmux = s:option(Flag, "xmux", translate("Mux")) +xmux.default = 1 +xmux:depends({ type = "Xray", protocol = "vless", tlsflow = "xtls-rprx-vision" }) +xmux:depends({ type = "Xray", protocol = "vless", tlsflow = "xtls-rprx-vision-udp443" }) + +mux_concurrency = s:option(Value, "mux_concurrency", translate("Mux concurrency")) +mux_concurrency.default = 8 +mux_concurrency:depends("mux", true) +mux_concurrency:depends("smux", true) + +xudp_concurrency = s:option(Value, "xudp_concurrency", translate("XUDP Mux concurrency")) +xudp_concurrency.default = 8 +xudp_concurrency:depends("xmux", true) + +smux_idle_timeout = s:option(Value, "smux_idle_timeout", translate("Mux idle timeout")) +smux_idle_timeout.default = 60 +smux_idle_timeout:depends("smux", true) + +hysteria_up_mbps = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps")) +hysteria_up_mbps.default = "10" +hysteria_up_mbps:depends("type", "Hysteria") + +hysteria_down_mbps = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps")) +hysteria_down_mbps.default = "50" +hysteria_down_mbps:depends("type", "Hysteria") + +hysteria_recv_window_conn = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window")) +hysteria_recv_window_conn:depends("type", "Hysteria") + +hysteria_recv_window = s:option(Value, "hysteria_recv_window", translate("QUIC connection receive window")) +hysteria_recv_window:depends("type", "Hysteria") + +hysteria_handshake_timeout = s:option(Value, "hysteria_handshake_timeout", translate("Handshake Timeout")) +hysteria_handshake_timeout:depends("type", "Hysteria") + +hysteria_idle_timeout = s:option(Value, "hysteria_idle_timeout", translate("Idle Timeout")) +hysteria_idle_timeout:depends("type", "Hysteria") + +hysteria_hop_interval = s:option(Value, "hysteria_hop_interval", translate("Hop Interval")) +hysteria_hop_interval:depends("type", "Hysteria") + +hysteria_disable_mtu_discovery = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection")) +hysteria_disable_mtu_discovery:depends("type", "Hysteria") + +hysteria_lazy_start = s:option(Flag, "hysteria_lazy_start", translate("Lazy Start")) +hysteria_lazy_start:depends("type", "Hysteria") + +protocol.validate = function(self, value) + if value == "_shunt" or value == "_balancing" then + address.rmempty = true + port.rmempty = true + end + return value +end + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua new file mode 100644 index 000000000..bff34094b --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua @@ -0,0 +1,140 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local sys = api.sys +local datatypes = api.datatypes + +m = Map(appname) + +-- [[ Other Settings ]]-- +s = m:section(TypedSection, "global_other") +s.anonymous = true + +o = s:option(MultiValue, "nodes_ping", " ") +o:value("auto_ping", translate("Auto Ping"), translate("This will automatically ping the node for latency")) +o:value("tcping", translate("Tcping"), translate("This will use tcping replace ping detection of node")) +o:value("info", translate("Show server address and port"), translate("Show server address and port")) + +-- [[ Add the node via the link ]]-- +s:append(Template(appname .. "/node_list/link_add_node")) + +local nodes_ping = m:get("@global_other[0]", "nodes_ping") or "" + +-- [[ Node List ]]-- +s = m:section(TypedSection, "nodes") +s.anonymous = true +s.addremove = true +s.template = "cbi/tblsection" +s.extedit = api.url("node_config", "%s") +function s.create(e, t) + local uuid = api.gen_short_uuid() + t = uuid + TypedSection.create(e, t) + luci.http.redirect(e.extedit:format(t)) +end + +function s.remove(e, t) + m.uci:foreach(appname, "socks", function(s) + if s["node"] == t then + m:del(s[".name"]) + end + end) + m.uci:foreach(appname, "acl_rule", function(s) + if s["node"] and s["node"] == t then + m:set(s[".name"], "node", "default") + end + end) + for k, v in ipairs(m:get("@auto_switch[0]", "node") or {}) do + if v and v == t then + sys.call(string.format("uci -q del_list %s.@auto_switch[0].node='%s'", appname, v)) + end + end + TypedSection.remove(e, t) + local new_node = "nil" + local node0 = m:get("@nodes[0]") or nil + if node0 then + new_node = node0[".name"] + end + if (m:get("@global[0]", "node") or "nil") == t then + m:set('@global[0]', "node", new_node) + end +end + +s.sortable = true +-- 简洁模式 +o = s:option(DummyValue, "add_from", "") +o.cfgvalue = function(t, n) + local v = Value.cfgvalue(t, n) + if v and v ~= '' then + local group = m:get(n, "group") or "" + if group ~= "" then + v = v .. " " .. group + end + return v + else + return '' + end +end +o = s:option(DummyValue, "remarks", translate("Remarks")) +o.rawhtml = true +o.cfgvalue = function(t, n) + local str = "" + local is_sub = m:get(n, "is_sub") or "" + local group = m:get(n, "group") or "" + local remarks = m:get(n, "remarks") or "" + local type = m:get(n, "type") or "" + str = str .. string.format("", appname, n, type) + if type == "V2ray" or type == "Xray" then + local protocol = m:get(n, "protocol") + if protocol == "_balancing" then + protocol = translate("Balancing") + elseif protocol == "_shunt" then + protocol = translate("Shunt") + elseif protocol == "vmess" then + protocol = "VMess" + elseif protocol == "vless" then + protocol = "VLESS" + else + protocol = protocol:gsub("^%l",string.upper) + end + type = type .. " " .. protocol + end + local address = m:get(n, "address") or "" + local port = m:get(n, "port") or "" + str = str .. translate(type) .. ":" .. remarks + if address ~= "" and port ~= "" then + if nodes_ping:find("info") then + if datatypes.ip6addr(address) then + str = str .. string.format("([%s]:%s)", address, port) + else + str = str .. string.format("(%s:%s)", address, port) + end + end + str = str .. string.format("", appname, n, address) + str = str .. string.format("", appname, n, port) + end + return str +end + +---- Ping +o = s:option(DummyValue, "ping") +o.width = "8%" +o.rawhtml = true +o.cfgvalue = function(t, n) + local result = "---" + if not nodes_ping:find("auto_ping") then + result = string.format('Ping', n) + else + result = string.format('---', n) + end + return result +end + +o = s:option(DummyValue, "_url_test") +o.rawhtml = true +o.cfgvalue = function(t, n) + return string.format(' 0 then + o = s:option(ListValue, "ss_aead_type", translate("SS AEAD Node Use Type")) + for key, value in pairs(ss_aead_type) do + o:value(value, translate(value:gsub("^%l",string.upper))) + end +end + +---- Subscribe Delete All +o = s:option(Button, "_stop", translate("Delete All Subscribe Node")) +o.inputstyle = "remove" +function o.write(e, e) + luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua truncate > /dev/null 2>&1") +end + +o = s:option(Button, "_update", translate("Manual subscription All")) +o.inputstyle = "apply" +function o.write(t, n) + luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua start > /dev/null 2>&1 &") + luci.http.redirect(api.url("log")) +end + +s = m:section(TypedSection, "subscribe_list", "", "" .. translate("Please input the subscription url first, save and submit before manual subscription.") .. "") +s.addremove = true +s.anonymous = true +s.sortable = true +s.template = "cbi/tblsection" +s.extedit = api.url("node_subscribe_config", "%s") +function s.create(e, t) + local id = TypedSection.create(e, t) + luci.http.redirect(e.extedit:format(id)) +end + +o = s:option(Value, "remark", translate("Remarks")) +o.width = "auto" +o.rmempty = false +o.validate = function(self, value, t) + if value then + local count = 0 + m.uci:foreach(appname, "subscribe_list", function(e) + if e[".name"] ~= t and e["remark"] == value then + count = count + 1 + end + end) + if count > 0 then + return nil, translate("This remark already exists, please change a new remark.") + end + return value + end +end + +o = s:option(DummyValue, "_node_count") +o.rawhtml = true +o.cfgvalue = function(t, n) + local remark = m:get(n, "remark") or "" + local num = 0 + m.uci:foreach(appname, "nodes", function(s) + if s["add_from"] ~= "" and s["add_from"] == remark then + num = num + 1 + end + end) + return string.format("%s", remark .. " " .. translate("Node num") .. ": " .. num, num) +end + +o = s:option(Value, "url", translate("Subscribe URL")) +o.width = "auto" +o.rmempty = false + +o = s:option(Button, "_remove", translate("Delete the subscribed node")) +o.inputstyle = "remove" +function o.write(t, n) + local remark = m:get(n, "remark") or "" + luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua truncate " .. remark .. " > /dev/null 2>&1") +end + +o = s:option(Button, "_update", translate("Manual subscription")) +o.inputstyle = "apply" +function o.write(t, n) + luci.sys.call("lua /usr/share/" .. appname .. "/subscribe.lua start " .. n .. " > /dev/null 2>&1 &") + luci.http.redirect(api.url("log")) +end + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua new file mode 100644 index 000000000..1a23f9f78 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua @@ -0,0 +1,90 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local sys = api.sys +local has_ss = api.is_finded("ss-redir") +local has_ss_rust = api.is_finded("sslocal") +local has_v2ray = api.is_finded("v2ray") +local has_xray = api.is_finded("xray") +local ss_aead_type = {} +if has_ss then + ss_aead_type[#ss_aead_type + 1] = "shadowsocks-libev" +end +if has_ss_rust then + ss_aead_type[#ss_aead_type + 1] = "shadowsocks-rust" +end +if has_v2ray then + ss_aead_type[#ss_aead_type + 1] = "v2ray" +end +if has_xray then + ss_aead_type[#ss_aead_type + 1] = "xray" +end + +m = Map(appname) +m.redirect = api.url("node_subscribe") + +s = m:section(NamedSection, arg[1]) +s.addremove = false +s.dynamic = false + +o = s:option(Value, "remark", translate("Subscribe Remark")) +o.rmempty = false + +o = s:option(TextValue, "url", translate("Subscribe URL")) +o.rows = 5 +o.rmempty = false + +o = s:option(Flag, "allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) +o.default = "1" +o.rmempty = false + +o = s:option(ListValue, "filter_keyword_mode", translate("Filter keyword Mode")) +o.default = "5" +o:value("0", translate("Close")) +o:value("1", translate("Discard List")) +o:value("2", translate("Keep List")) +o:value("3", translate("Discard List,But Keep List First")) +o:value("4", translate("Keep List,But Discard List First")) +o:value("5", translate("Use global config")) + +o = s:option(DynamicList, "filter_discard_list", translate("Discard List")) +o:depends("filter_keyword_mode", "1") +o:depends("filter_keyword_mode", "3") +o:depends("filter_keyword_mode", "4") + +o = s:option(DynamicList, "filter_keep_list", translate("Keep List")) +o:depends("filter_keyword_mode", "2") +o:depends("filter_keyword_mode", "3") +o:depends("filter_keyword_mode", "4") + +if #ss_aead_type > 0 then + o = s:option(ListValue, "ss_aead_type", translate("SS AEAD Node Use Type")) + o.default = "global" + o:value("global", translate("Use global config")) + for key, value in pairs(ss_aead_type) do + o:value(value, translate(value:gsub("^%l",string.upper))) + end +end + +---- Enable auto update subscribe +o = s:option(Flag, "auto_update", translate("Enable auto update subscribe")) +o.default = 0 +o.rmempty = false + +---- Week update rules +o = s:option(ListValue, "week_update", translate("Week update rules")) +o:value(7, translate("Every day")) +for e = 1, 6 do o:value(e, translate("Week") .. e) end +o:value(0, translate("Week") .. translate("day")) +o.default = 0 +o:depends("auto_update", true) + +---- Day update rules +o = s:option(ListValue, "time_update", translate("Day update rules")) +for e = 0, 23 do o:value(e, e .. translate("oclock")) end +o.default = 0 +o:depends("auto_update", true) + +o = s:option(Value, "user_agent", translate("User-Agent")) +o.default = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36" + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua new file mode 100644 index 000000000..4d235ae75 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua @@ -0,0 +1,158 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local fs = api.fs +local has_v2ray = api.is_finded("v2ray") +local has_xray = api.is_finded("xray") +local has_fw3 = api.is_finded("fw3") +local has_fw4 = api.is_finded("fw4") + +m = Map(appname) + +-- [[ Delay Settings ]]-- +s = m:section(TypedSection, "global_delay", translate("Delay Settings")) +s.anonymous = true +s.addremove = false + +---- Delay Start +o = s:option(Value, "start_delay", translate("Delay Start"), translate("Units:seconds")) +o.default = "1" +o.rmempty = true + +---- Open and close Daemon +o = s:option(Flag, "start_daemon", translate("Open and close Daemon")) +o.default = 1 +o.rmempty = false + +--[[ +---- Open and close automatically +o = s:option(Flag, "auto_on", translate("Open and close automatically")) +o.default = 0 +o.rmempty = false + +---- Automatically turn off time +o = s:option(ListValue, "time_off", translate("Automatically turn off time")) +o.default = nil +o:depends("auto_on", true) +o:value(nil, translate("Disable")) +for e = 0, 23 do o:value(e, e .. translate("oclock")) end + +---- Automatically turn on time +o = s:option(ListValue, "time_on", translate("Automatically turn on time")) +o.default = nil +o:depends("auto_on", true) +o:value(nil, translate("Disable")) +for e = 0, 23 do o:value(e, e .. translate("oclock")) end + +---- Automatically restart time +o = s:option(ListValue, "time_restart", translate("Automatically restart time")) +o.default = nil +o:depends("auto_on", true) +o:value(nil, translate("Disable")) +for e = 0, 23 do o:value(e, e .. translate("oclock")) end +--]] + +-- [[ Forwarding Settings ]]-- +s = m:section(TypedSection, "global_forwarding", translate("Forwarding Settings")) +s.anonymous = true +s.addremove = false + +---- TCP No Redir Ports +o = s:option(Value, "tcp_no_redir_ports", translate("TCP No Redir Ports")) +o.default = "disable" +o:value("disable", translate("No patterns are used")) +o:value("1:65535", translate("All")) + +---- UDP No Redir Ports +o = s:option(Value, "udp_no_redir_ports", translate("UDP No Redir Ports"), + "" .. + translate("Fill in the ports you don't want to be forwarded by the agent, with the highest priority.") .. + "") +o.default = "disable" +o:value("disable", translate("No patterns are used")) +o:value("1:65535", translate("All")) + +---- TCP Redir Ports +o = s:option(Value, "tcp_redir_ports", translate("TCP Redir Ports")) +o.default = "22,25,53,143,465,587,853,993,995,80,443" +o:value("1:65535", translate("All")) +o:value("22,25,53,143,465,587,853,993,995,80,443", translate("Common Use")) +o:value("80,443", translate("Only Web")) + +---- UDP Redir Ports +o = s:option(Value, "udp_redir_ports", translate("UDP Redir Ports")) +o.default = "1:65535" +o:value("1:65535", translate("All")) + +---- Use nftables +o = s:option(ListValue, "use_nft", translate("Firewall tools")) +o.default = "0" +if has_fw3 then + o:value("0", "IPtables") +end +if has_fw4 then + o:value("1", "NFtables") +end + +if (os.execute("lsmod | grep -i REDIRECT >/dev/null") == 0 and os.execute("lsmod | grep -i TPROXY >/dev/null") == 0) or (os.execute("lsmod | grep -i nft_redir >/dev/null") == 0 and os.execute("lsmod | grep -i nft_tproxy >/dev/null") == 0) then + o = s:option(ListValue, "tcp_proxy_way", translate("TCP Proxy Way")) + o.default = "redirect" + o:value("redirect", "REDIRECT") + o:value("tproxy", "TPROXY") + o:depends("ipv6_tproxy", false) + + o = s:option(ListValue, "_tcp_proxy_way", translate("TCP Proxy Way")) + o.default = "tproxy" + o:value("tproxy", "TPROXY") + o:depends("ipv6_tproxy", true) + o.write = function(self, section, value) + return self.map:set(section, "tcp_proxy_way", value) + end + + if os.execute("lsmod | grep -i ip6table_mangle >/dev/null") == 0 or os.execute("lsmod | grep -i nft_tproxy >/dev/null") == 0 then + ---- IPv6 TProxy + o = s:option(Flag, "ipv6_tproxy", translate("IPv6 TProxy"), + "" .. + translate("Experimental feature. Make sure that your node supports IPv6.") .. + "") + o.default = 0 + o.rmempty = false + end +end + +o = s:option(Flag, "accept_icmp", translate("Hijacking ICMP (PING)")) +o.default = 0 + +o = s:option(Flag, "accept_icmpv6", translate("Hijacking ICMPv6 (IPv6 PING)")) +o:depends("ipv6_tproxy", true) +o.default = 0 + +if has_v2ray or has_xray then + o = s:option(Flag, "sniffing", translate("Sniffing (V2Ray/Xray)"), translate("When using the V2ray/Xray shunt, must be enabled, otherwise the shunt will invalid.")) + o.default = 1 + o.rmempty = false + + if has_xray then + route_only = s:option(Flag, "route_only", translate("Sniffing Route Only (Xray)"), translate("When enabled, the server not will resolve the domain name again.")) + route_only.default = 0 + route_only:depends("sniffing", true) + + local domains_excluded = string.format("/usr/share/%s/domains_excluded", appname) + o = s:option(TextValue, "no_sniffing_hosts", translate("No Sniffing Lists"), translate("Hosts added into No Sniffing Lists will not resolve again on server (Xray only).")) + o.rows = 15 + o.wrap = "off" + o.cfgvalue = function(self, section) return fs.readfile(domains_excluded) or "" end + o.write = function(self, section, value) fs.writefile(domains_excluded, value:gsub("\r\n", "\n")) end + o.remove = function(self, section, value) + if route_only:formvalue(section) == "0" then + fs.writefile(domains_excluded, "") + end + end + o:depends({sniffing = true, route_only = false}) + + o = s:option(Value, "buffer_size", translate("Buffer Size (Xray)"), translate("Buffer size for every connection (kB)")) + o.rmempty = true + o.datatype = "uinteger" + end +end + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua new file mode 100644 index 000000000..af3768ab4 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua @@ -0,0 +1,65 @@ +local api = require "luci.passwall2.api" +local appname = api.appname + +m = Map(appname) +-- [[ Rule Settings ]]-- +s = m:section(TypedSection, "global_rules", translate("Rule status")) +s.anonymous = true + +s:append(Template(appname .. "/rule/rule_version")) + +---- Auto Update +o = s:option(Flag, "auto_update", translate("Enable auto update rules")) +o.default = 0 +o.rmempty = false + +---- Week Update +o = s:option(ListValue, "week_update", translate("Week update rules")) +o:value(7, translate("Every day")) +for e = 1, 6 do o:value(e, translate("Week") .. e) end +o:value(0, translate("Week") .. translate("day")) +o.default = 0 +o:depends("auto_update", true) + +---- Time Update +o = s:option(ListValue, "time_update", translate("Day update rules")) +for e = 0, 23 do o:value(e, e .. translate("oclock")) end +o.default = 0 +o:depends("auto_update", true) + +o = s:option(Value, "v2ray_location_asset", translate("Location of V2ray/Xray asset"), translate("This variable specifies a directory where geoip.dat and geosite.dat files are.")) +o.default = "/usr/share/v2ray/" +o.rmempty = false + +---- Custom geo file url +o = s:option(Value, "geoip_url", translate("Custom geoip URL")) +o.default = "https://api.github.com/repos/Loyalsoldier/v2ray-rules-dat/releases/latest" +o.rmempty = false + +o = s:option(Value, "geosite_url", translate("Custom geosite URL")) +o.default = "https://api.github.com/repos/Loyalsoldier/v2ray-rules-dat/releases/latest" +o.rmempty = false +---- + +s = m:section(TypedSection, "shunt_rules", "V2ray/Xray " .. translate("Shunt Rule"), "" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "") +s.template = "cbi/tblsection" +s.anonymous = false +s.addremove = true +s.sortable = true +s.extedit = api.url("shunt_rules", "%s") +function s.create(e, t) + TypedSection.create(e, t) + luci.http.redirect(e.extedit:format(t)) +end +function s.remove(e, t) + m.uci:foreach(appname, "nodes", function(s) + if s["protocol"] and s["protocol"] == "_shunt" then + m:del(s[".name"], t) + end + end) + TypedSection.remove(e, t) +end + +o = s:option(DummyValue, "remarks", translate("Remarks")) + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua new file mode 100644 index 000000000..9fb2f9aca --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua @@ -0,0 +1,158 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local datatypes = api.datatypes + +m = Map(appname, "V2ray/Xray " .. translate("Shunt Rule")) +m.redirect = api.url() + +s = m:section(NamedSection, arg[1], "shunt_rules", "") +s.addremove = false +s.dynamic = false + +remarks = s:option(Value, "remarks", translate("Remarks")) +remarks.default = arg[1] +remarks.rmempty = false + +protocol = s:option(MultiValue, "protocol", translate("Protocol")) +protocol:value("http") +protocol:value("tls") +protocol:value("bittorrent") + +network = s:option(ListValue, "network", translate("Network")) +network:value("tcp,udp", "TCP UDP") +network:value("tcp", "TCP") +network:value("udp", "UDP") + +source = s:option(DynamicList, "source", translate("Source")) +source.description = "" +source.cast = "string" +source.cfgvalue = function(self, section) + local value + if self.tag_error[section] then + value = self:formvalue(section) + else + value = self.map:get(section, self.option) + if type(value) == "string" then + local value2 = {} + string.gsub(value, '[^' .. " " .. ']+', function(w) table.insert(value2, w) end) + value = value2 + end + end + return value +end +source.validate = function(self, value, t) + local err = {} + for _, v in ipairs(value) do + local flag = false + if datatypes.ip4addr(v) then + flag = true + end + + if flag == false and v:find("geoip:") and v:find("geoip:") == 1 then + flag = true + end + + if flag == false then + err[#err + 1] = v + end + end + + if #err > 0 then + self:add_error(t, "invalid", translate("Not true format, please re-enter!")) + for _, v in ipairs(err) do + self:add_error(t, "invalid", v) + end + end + + return value +end + +local dynamicList_write = function(self, section, value) + local t = {} + local t2 = {} + if type(value) == "table" then + local x + for _, x in ipairs(value) do + if x and #x > 0 then + if not t2[x] then + t2[x] = x + t[#t+1] = x + end + end + end + else + t = { value } + end + t = table.concat(t, " ") + return DynamicList.write(self, section, t) +end + +source.write = dynamicList_write + +sourcePort = s:option(Value, "sourcePort", translate("Source port")) + +port = s:option(Value, "port", translate("port")) + +domain_list = s:option(TextValue, "domain_list", translate("Domain")) +domain_list.rows = 10 +domain_list.wrap = "off" +domain_list.validate = function(self, value) + local hosts= {} + string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(hosts, w) end) + for index, host in ipairs(hosts) do + local flag = 1 + local tmp_host = host + if host:find("regexp:") and host:find("regexp:") == 1 then + flag = 0 + elseif host:find("domain:.") and host:find("domain:.") == 1 then + tmp_host = host:gsub("domain:", "") + elseif host:find("full:.") and host:find("full:.") == 1 then + tmp_host = host:gsub("full:", "") + elseif host:find("geosite:") and host:find("geosite:") == 1 then + flag = 0 + elseif host:find("ext:") and host:find("ext:") == 1 then + flag = 0 + end + if flag == 1 then + if not datatypes.hostname(tmp_host) then + return nil, tmp_host .. " " .. translate("Not valid domain name, please re-enter!") + end + end + end + return value +end +domain_list.description = "
" +ip_list = s:option(TextValue, "ip_list", "IP") +ip_list.rows = 10 +ip_list.wrap = "off" +ip_list.validate = function(self, value) + local ipmasks= {} + string.gsub(value, '[^' .. "\r\n" .. ']+', function(w) table.insert(ipmasks, w) end) + for index, ipmask in ipairs(ipmasks) do + if ipmask:find("geoip:") and ipmask:find("geoip:") == 1 then + elseif ipmask:find("ext:") and ipmask:find("ext:") == 1 then + else + if not (datatypes.ipmask4(ipmask) or datatypes.ipmask6(ipmask)) then + return nil, ipmask .. " " .. translate("Not valid IP format, please re-enter!") + end + end + end + return value +end +ip_list.description = "
" + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua new file mode 100644 index 000000000..fc6d2e5cc --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua @@ -0,0 +1,73 @@ +local api = require "luci.passwall2.api" + +m = Map("passwall2_server", translate("Server-Side")) + +t = m:section(NamedSection, "global", "global") +t.anonymous = true +t.addremove = false + +e = t:option(Flag, "enable", translate("Enable")) +e.rmempty = false + +t = m:section(TypedSection, "user", translate("Users Manager")) +t.anonymous = true +t.addremove = true +t.sortable = true +t.template = "cbi/tblsection" +t.extedit = api.url("server_user", "%s") +function t.create(e, t) + local uuid = api.gen_uuid() + t = uuid + TypedSection.create(e, t) + luci.http.redirect(e.extedit:format(t)) +end +function t.remove(e, t) + e.map.proceed = true + e.map:del(t) + luci.http.redirect(api.url("server")) +end + +e = t:option(Flag, "enable", translate("Enable")) +e.width = "5%" +e.rmempty = false + +e = t:option(DummyValue, "status", translate("Status")) +e.rawhtml = true +e.cfgvalue = function(t, n) + return string.format('%s', translate("Collecting data...")) +end + +e = t:option(DummyValue, "remarks", translate("Remarks")) +e.width = "15%" + +---- Type +e = t:option(DummyValue, "type", translate("Type")) +e.cfgvalue = function(t, n) + local v = Value.cfgvalue(t, n) + if v then + if v == "V2ray" or v == "Xray" then + local protocol = m:get(n, "protocol") + if protocol == "vmess" then + protocol = "VMess" + elseif protocol == "vless" then + protocol = "VLESS" + else + protocol = protocol:gsub("^%l",string.upper) + end + return v .. " -> " .. protocol + end + return v + end +end + +e = t:option(DummyValue, "port", translate("Port")) + +e = t:option(Flag, "log", translate("Log")) +e.default = "1" +e.rmempty = false + +m:append(Template("passwall2/server/log")) + +m:append(Template("passwall2/server/users_list_status")) +return m + diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua new file mode 100644 index 000000000..390a2e466 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua @@ -0,0 +1,632 @@ +local api = require "luci.passwall2.api" + +local ss_encrypt_method_list = { + "rc4-md5", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", + "aes-192-ctr", "aes-256-ctr", "bf-cfb", "camellia-128-cfb", + "camellia-192-cfb", "camellia-256-cfb", "salsa20", "chacha20", + "chacha20-ietf", -- aead + "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305" +} + +local ss_rust_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-chacha8-poly1305", "2022-blake3-chacha20-poly1305" +} + +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" +} + +local v_ss_encrypt_method_list = { + "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305" +} + +local x_ss_encrypt_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" +} + +local encrypt_methods_ss_aead = { + "chacha20-ietf-poly1305", + "aes-128-gcm", + "aes-256-gcm", +} + +m = Map("passwall2_server", translate("Server Config")) +m.redirect = api.url("server") + +s = m:section(NamedSection, arg[1], "user", "") +s.addremove = false +s.dynamic = false + +enable = s:option(Flag, "enable", translate("Enable")) +enable.default = "1" +enable.rmempty = false + +remarks = s:option(Value, "remarks", translate("Remarks")) +remarks.default = translate("Remarks") +remarks.rmempty = false + +type = s:option(ListValue, "type", translate("Type")) +if api.is_finded("ss-server") then + type:value("SS", translate("Shadowsocks")) +end +if api.is_finded("ssserver") then + type:value("SS-Rust", translate("Shadowsocks Rust")) +end +if api.is_finded("ssr-server") then + type:value("SSR", translate("ShadowsocksR")) +end +if api.is_finded("v2ray") then + type:value("V2ray", translate("V2ray")) +end +if api.is_finded("xray") then + type:value("Xray", translate("Xray")) +end +if api.is_finded("brook") then + type:value("Brook", translate("Brook")) +end +if api.is_finded("hysteria") then + type:value("Hysteria", translate("Hysteria")) +end + +protocol = s:option(ListValue, "protocol", translate("Protocol")) +protocol:value("vmess", "Vmess") +protocol:value("vless", "VLESS") +protocol:value("http", "HTTP") +protocol:value("socks", "Socks") +protocol:value("shadowsocks", "Shadowsocks") +protocol:value("trojan", "Trojan") +protocol:value("mtproto", "MTProto") +protocol:value("dokodemo-door", "dokodemo-door") +protocol:depends("type", "V2ray") +protocol:depends("type", "Xray") + +-- Brook协议 +brook_protocol = s:option(ListValue, "brook_protocol", translate("Protocol")) +brook_protocol:value("server", "Brook") +brook_protocol:value("wsserver", "WebSocket") +brook_protocol:depends("type", "Brook") +function brook_protocol.cfgvalue(self, section) + return m:get(section, "protocol") +end +function brook_protocol.write(self, section, value) + m:set(section, "protocol", value) +end + +--brook_tls = s:option(Flag, "brook_tls", translate("Use TLS")) +--brook_tls:depends("brook_protocol", "wsserver") + +port = s:option(Value, "port", translate("Listen Port")) +port.datatype = "port" +port.rmempty = false + +auth = s:option(Flag, "auth", translate("Auth")) +auth.validate = function(self, value, t) + if value and value == "1" then + local user_v = username:formvalue(t) or "" + local pass_v = password:formvalue(t) or "" + if user_v == "" or pass_v == "" then + return nil, translate("Username and Password must be used together!") + end + end + return value +end +auth:depends({ type = "V2ray", protocol = "socks" }) +auth:depends({ type = "V2ray", protocol = "http" }) +auth:depends({ type = "Xray", protocol = "socks" }) +auth:depends({ type = "Xray", protocol = "http" }) + +username = s:option(Value, "username", translate("Username")) +username:depends("auth", true) + +password = s:option(Value, "password", translate("Password")) +password.password = true +password:depends("auth", true) +password:depends("type", "SS") +password:depends("type", "SS-Rust") +password:depends("type", "SSR") +password:depends("type", "Brook") +password:depends({ type = "V2ray", protocol = "shadowsocks" }) +password:depends({ type = "Xray", protocol = "shadowsocks" }) + +mtproto_password = s:option(Value, "mtproto_password", translate("Password"), translate("The MTProto protocol must be 32 characters and can only contain characters from 0 to 9 and a to f.")) +mtproto_password:depends({ type = "V2ray", protocol = "mtproto" }) +mtproto_password:depends({ type = "Xray", protocol = "mtproto" }) +mtproto_password.default = arg[1] +function mtproto_password.cfgvalue(self, section) + return m:get(section, "password") +end +function mtproto_password.write(self, section, value) + m:set(section, "password", value) +end + +d_protocol = s:option(ListValue, "d_protocol", translate("Destination protocol")) +d_protocol:value("tcp", "TCP") +d_protocol:value("udp", "UDP") +d_protocol:value("tcp,udp", "TCP,UDP") +d_protocol:depends({ type = "V2ray", protocol = "dokodemo-door" }) +d_protocol:depends({ type = "Xray", protocol = "dokodemo-door" }) + +d_address = s:option(Value, "d_address", translate("Destination address")) +d_address:depends({ type = "V2ray", protocol = "dokodemo-door" }) +d_address:depends({ type = "Xray", protocol = "dokodemo-door" }) + +d_port = s:option(Value, "d_port", translate("Destination port")) +d_port.datatype = "port" +d_port:depends({ type = "V2ray", protocol = "dokodemo-door" }) +d_port:depends({ type = "Xray", protocol = "dokodemo-door" }) + +decryption = s:option(Value, "decryption", translate("Encrypt Method")) +decryption.default = "none" +decryption:depends({ type = "V2ray", protocol = "vless" }) +decryption:depends({ type = "Xray", protocol = "vless" }) + +hysteria_protocol = s:option(ListValue, "hysteria_protocol", translate("Protocol")) +hysteria_protocol:value("udp", "UDP") +hysteria_protocol:value("faketcp", "faketcp") +hysteria_protocol:value("wechat-video", "wechat-video") +hysteria_protocol:depends("type", "Hysteria") +function hysteria_protocol.cfgvalue(self, section) + return m:get(section, "protocol") +end +function hysteria_protocol.write(self, section, value) + m:set(section, "protocol", value) +end + +hysteria_obfs = s:option(Value, "hysteria_obfs", translate("Obfs Password")) +hysteria_obfs:depends("type", "Hysteria") + +hysteria_auth_type = s:option(ListValue, "hysteria_auth_type", translate("Auth Type")) +hysteria_auth_type:value("disable", translate("Disable")) +hysteria_auth_type:value("string", translate("STRING")) +hysteria_auth_type:depends("type", "Hysteria") + +hysteria_auth_password = s:option(Value, "hysteria_auth_password", translate("Auth Password")) +hysteria_auth_password.password = true +hysteria_auth_password:depends("hysteria_auth_type", "string") + +hysteria_alpn = s:option(Value, "hysteria_alpn", translate("QUIC TLS ALPN")) +hysteria_alpn:depends("type", "Hysteria") + +hysteria_udp = s:option(Flag, "hysteria_udp", translate("UDP")) +hysteria_udp.default = "1" +hysteria_udp:depends("type", "Hysteria") + +hysteria_up_mbps = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps")) +hysteria_up_mbps.default = "10" +hysteria_up_mbps:depends("type", "Hysteria") + +hysteria_down_mbps = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps")) +hysteria_down_mbps.default = "50" +hysteria_down_mbps:depends("type", "Hysteria") + +hysteria_recv_window_conn = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window")) +hysteria_recv_window_conn:depends("type", "Hysteria") + +hysteria_recv_window = s:option(Value, "hysteria_recv_window", translate("QUIC connection receive window")) +hysteria_recv_window:depends("type", "Hysteria") + +hysteria_disable_mtu_discovery = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection")) +hysteria_disable_mtu_discovery:depends("type", "Hysteria") + +ss_encrypt_method = s:option(ListValue, "ss_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(ss_encrypt_method_list) do ss_encrypt_method:value(t) end +ss_encrypt_method:depends("type", "SS") +function ss_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function ss_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +ss_rust_encrypt_method = s:option(ListValue, "ss_rust_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(ss_rust_encrypt_method_list) do ss_rust_encrypt_method:value(t) end +ss_rust_encrypt_method:depends("type", "SS-Rust") +function ss_rust_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function ss_rust_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +ssr_encrypt_method = s:option(ListValue, "ssr_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(ssr_encrypt_method_list) do ssr_encrypt_method:value(t) end +ssr_encrypt_method:depends("type", "SSR") +function ssr_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function ssr_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +v_ss_encrypt_method = s:option(ListValue, "v_ss_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(v_ss_encrypt_method_list) do v_ss_encrypt_method:value(t) end +v_ss_encrypt_method:depends({ type = "V2ray", protocol = "shadowsocks" }) +function v_ss_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function v_ss_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +x_ss_encrypt_method = s:option(ListValue, "x_ss_encrypt_method", translate("Encrypt Method")) +for a, t in ipairs(x_ss_encrypt_method_list) do x_ss_encrypt_method:value(t) end +x_ss_encrypt_method:depends({ type = "Xray", protocol = "shadowsocks" }) +function x_ss_encrypt_method.cfgvalue(self, section) + return m:get(section, "method") +end +function x_ss_encrypt_method.write(self, section, value) + m:set(section, "method", value) +end + +iv_check = s:option(Flag, "iv_check", translate("IV Check")) +iv_check:depends({ type = "V2ray", protocol = "shadowsocks" }) +iv_check:depends({ type = "Xray", protocol = "shadowsocks" }) + +ss_network = s:option(ListValue, "ss_network", translate("Transport")) +ss_network.default = "tcp,udp" +ss_network:value("tcp", "TCP") +ss_network:value("udp", "UDP") +ss_network:value("tcp,udp", "TCP,UDP") +ss_network:depends({ type = "V2ray", protocol = "shadowsocks" }) +ss_network:depends({ type = "Xray", protocol = "shadowsocks" }) + +ssr_protocol = s:option(ListValue, "ssr_protocol", translate("Protocol")) +for a, t in ipairs(ssr_protocol_list) do ssr_protocol:value(t) end +ssr_protocol:depends("type", "SSR") +function ssr_protocol.cfgvalue(self, section) + return m:get(section, "protocol") +end +function ssr_protocol.write(self, section, value) + m:set(section, "protocol", value) +end + +protocol_param = s:option(Value, "protocol_param", translate("Protocol_param")) +protocol_param:depends("type", "SSR") + +obfs = s:option(ListValue, "obfs", translate("Obfs")) +for a, t in ipairs(ssr_obfs_list) do obfs:value(t) end +obfs:depends("type", "SSR") + +obfs_param = s:option(Value, "obfs_param", translate("Obfs_param")) +obfs_param:depends("type", "SSR") + +timeout = s:option(Value, "timeout", translate("Connection Timeout")) +timeout.datatype = "uinteger" +timeout.default = 300 +timeout:depends("type", "SS") +timeout:depends("type", "SS-Rust") +timeout:depends("type", "SSR") + +udp_forward = s:option(Flag, "udp_forward", translate("UDP Forward")) +udp_forward.default = "1" +udp_forward.rmempty = false +udp_forward:depends("type", "SSR") +udp_forward:depends({ type = "V2ray", protocol = "socks" }) +udp_forward:depends({ type = "Xray", protocol = "socks" }) + +uuid = s:option(DynamicList, "uuid", translate("ID") .. "/" .. translate("Password")) +for i = 1, 3 do + uuid:value(api.gen_uuid(1)) +end +uuid:depends({ type = "V2ray", protocol = "vmess" }) +uuid:depends({ type = "V2ray", protocol = "vless" }) +uuid:depends({ type = "V2ray", protocol = "trojan" }) +uuid:depends({ type = "Xray", protocol = "vmess" }) +uuid:depends({ type = "Xray", protocol = "vless" }) +uuid:depends({ type = "Xray", protocol = "trojan" }) + +tls = s:option(Flag, "tls", translate("TLS")) +tls.default = 0 +tls.validate = function(self, value, t) + if value then + local type = type:formvalue(t) or "" + if value == "1" then + local ca = tls_certificateFile:formvalue(t) or "" + local key = tls_keyFile:formvalue(t) or "" + if ca == "" or key == "" then + return nil, translate("Public key and Private key path can not be empty!") + end + end + return value + end +end +tls:depends({ type = "V2ray", protocol = "vmess" }) +tls:depends({ type = "V2ray", protocol = "vless" }) +tls:depends({ type = "V2ray", protocol = "socks" }) +tls:depends({ type = "V2ray", protocol = "shadowsocks" }) +tls:depends({ type = "V2ray", protocol = "trojan" }) +tls:depends({ type = "Xray", protocol = "vmess" }) +tls:depends({ type = "Xray", protocol = "vless" }) +tls:depends({ type = "Xray", protocol = "socks" }) +tls:depends({ type = "Xray", protocol = "shadowsocks" }) +tls:depends({ type = "Xray", protocol = "trojan" }) + +alpn = s:option(ListValue, "alpn", translate("alpn")) +alpn.default = "h2,http/1.1" +alpn:value("h2,http/1.1") +alpn:value("h2") +alpn:value("http/1.1") +alpn:depends({ type = "V2ray", tls = true }) +alpn:depends({ type = "Xray", tls = true }) + +-- [[ TLS部分 ]] -- + +tls_certificateFile = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +tls_certificateFile.validate = function(self, value, t) + if value and value ~= "" then + if not nixio.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end +tls_certificateFile.default = "/etc/config/ssl/" .. arg[1] .. ".pem" +tls_certificateFile:depends("tls", true) +tls_certificateFile:depends("type", "Hysteria") + +tls_keyFile = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +tls_keyFile.validate = function(self, value, t) + if value and value ~= "" then + if not nixio.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end +tls_keyFile.default = "/etc/config/ssl/" .. arg[1] .. ".key" +tls_keyFile:depends("tls", true) +tls_keyFile:depends("type", "Hysteria") + +transport = s:option(ListValue, "transport", translate("Transport")) +transport:value("tcp", "TCP") +transport:value("mkcp", "mKCP") +transport:value("ws", "WebSocket") +transport:value("h2", "HTTP/2") +transport:value("ds", "DomainSocket") +transport:value("quic", "QUIC") +transport:value("grpc", "gRPC") +transport:depends({ type = "V2ray", protocol = "vmess" }) +transport:depends({ type = "V2ray", protocol = "vless" }) +transport:depends({ type = "V2ray", protocol = "socks" }) +transport:depends({ type = "V2ray", protocol = "shadowsocks" }) +transport:depends({ type = "V2ray", protocol = "trojan" }) +transport:depends({ type = "Xray", protocol = "vmess" }) +transport:depends({ type = "Xray", protocol = "vless" }) +transport:depends({ type = "Xray", protocol = "socks" }) +transport:depends({ type = "Xray", protocol = "shadowsocks" }) +transport:depends({ type = "Xray", protocol = "trojan" }) + +-- [[ WebSocket部分 ]]-- + +ws_host = s:option(Value, "ws_host", translate("WebSocket Host")) +ws_host:depends("transport", "ws") +ws_host:depends("ss_transport", "ws") + +ws_path = s:option(Value, "ws_path", translate("WebSocket Path")) +ws_path:depends("transport", "ws") +ws_path:depends("ss_transport", "ws") +ws_path:depends({ type = "Brook", brook_protocol = "wsserver" }) + +-- [[ HTTP/2部分 ]]-- + +h2_host = s:option(Value, "h2_host", translate("HTTP/2 Host")) +h2_host:depends("transport", "h2") +h2_host:depends("ss_transport", "h2") + +h2_path = s:option(Value, "h2_path", translate("HTTP/2 Path")) +h2_path:depends("transport", "h2") +h2_path:depends("ss_transport", "h2") + +-- [[ TCP部分 ]]-- + +-- TCP伪装 +tcp_guise = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) +tcp_guise:value("none", "none") +tcp_guise:value("http", "http") +tcp_guise:depends("transport", "tcp") + +-- HTTP域名 +tcp_guise_http_host = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) +tcp_guise_http_host:depends("tcp_guise", "http") + +-- HTTP路径 +tcp_guise_http_path = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) +tcp_guise_http_path:depends("tcp_guise", "http") + +-- [[ mKCP部分 ]]-- + +mkcp_guise = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)')) +for a, t in ipairs(header_type_list) do mkcp_guise:value(t) end +mkcp_guise:depends("transport", "mkcp") + +mkcp_mtu = s:option(Value, "mkcp_mtu", translate("KCP MTU")) +mkcp_mtu.default = "1350" +mkcp_mtu:depends("transport", "mkcp") + +mkcp_tti = s:option(Value, "mkcp_tti", translate("KCP TTI")) +mkcp_tti.default = "20" +mkcp_tti:depends("transport", "mkcp") + +mkcp_uplinkCapacity = s:option(Value, "mkcp_uplinkCapacity", translate("KCP uplinkCapacity")) +mkcp_uplinkCapacity.default = "5" +mkcp_uplinkCapacity:depends("transport", "mkcp") + +mkcp_downlinkCapacity = s:option(Value, "mkcp_downlinkCapacity", translate("KCP downlinkCapacity")) +mkcp_downlinkCapacity.default = "20" +mkcp_downlinkCapacity:depends("transport", "mkcp") + +mkcp_congestion = s:option(Flag, "mkcp_congestion", translate("KCP Congestion")) +mkcp_congestion:depends("transport", "mkcp") + +mkcp_readBufferSize = s:option(Value, "mkcp_readBufferSize", translate("KCP readBufferSize")) +mkcp_readBufferSize.default = "1" +mkcp_readBufferSize:depends("transport", "mkcp") + +mkcp_writeBufferSize = s:option(Value, "mkcp_writeBufferSize", translate("KCP writeBufferSize")) +mkcp_writeBufferSize.default = "1" +mkcp_writeBufferSize:depends("transport", "mkcp") + +mkcp_seed = s:option(Value, "mkcp_seed", translate("KCP Seed")) +mkcp_seed:depends("transport", "mkcp") + +-- [[ DomainSocket部分 ]]-- + +ds_path = s:option(Value, "ds_path", "Path", translate("A legal file path. This file must not exist before running.")) +ds_path:depends("transport", "ds") + +-- [[ QUIC部分 ]]-- +quic_security = s:option(ListValue, "quic_security", translate("Encrypt Method")) +quic_security:value("none") +quic_security:value("aes-128-gcm") +quic_security:value("chacha20-poly1305") +quic_security:depends("transport", "quic") + +quic_key = s:option(Value, "quic_key", translate("Encrypt Method") .. translate("Key")) +quic_key:depends("transport", "quic") + +quic_guise = s:option(ListValue, "quic_guise", translate("Camouflage Type")) +for a, t in ipairs(header_type_list) do quic_guise:value(t) end +quic_guise:depends("transport", "quic") + +-- [[ gRPC部分 ]]-- +grpc_serviceName = s:option(Value, "grpc_serviceName", "ServiceName") +grpc_serviceName:depends("transport", "grpc") + +acceptProxyProtocol = 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.")) +acceptProxyProtocol:depends({ type = "V2ray", transport = "tcp" }) +acceptProxyProtocol:depends({ type = "V2ray", transport = "ws" }) +acceptProxyProtocol:depends({ type = "Xray", transport = "tcp" }) +acceptProxyProtocol:depends({ type = "Xray", transport = "ws" }) + +-- [[ Fallback部分 ]]-- +fallback = s:option(Flag, "fallback", translate("Fallback")) +fallback:depends({ type = "V2ray", protocol = "vless", transport = "tcp" }) +fallback:depends({ type = "V2ray", protocol = "trojan", transport = "tcp" }) +fallback:depends({ type = "Xray", protocol = "vless", transport = "tcp" }) +fallback:depends({ type = "Xray", protocol = "trojan", transport = "tcp" }) + +--[[ +fallback_alpn = s:option(Value, "fallback_alpn", "Fallback alpn") +fallback_alpn:depends("fallback", true) + +fallback_path = s:option(Value, "fallback_path", "Fallback path") +fallback_path:depends("fallback", true) + +fallback_dest = s:option(Value, "fallback_dest", "Fallback dest") +fallback_dest:depends("fallback", true) + +fallback_xver = s:option(Value, "fallback_xver", "Fallback xver") +fallback_xver.default = 0 +fallback_xver:depends("fallback", true) +]]-- + +fallback_list = s:option(DynamicList, "fallback_list", "Fallback", translate("dest,path")) +fallback_list:depends("fallback", true) + +tcp_fast_open = s:option(Flag, "tcp_fast_open", translate("TCP Fast Open")) +tcp_fast_open.default = "0" +tcp_fast_open:depends("type", "SS") +tcp_fast_open:depends("type", "SS-Rust") +tcp_fast_open:depends("type", "SSR") + +remote_address = s:option(Value, "remote_address", translate("Remote Address")) +remote_address.default = "127.0.0.1" +remote_address:depends("remote_enable", 1) + +remote_port = s:option(Value, "remote_port", translate("Remote Port")) +remote_port.datatype = "port" +remote_port.default = "80" +remote_port:depends("remote_enable", 1) + +bind_local = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed locally, It is recommended to turn on when using reverse proxies or be fallback.")) +bind_local.default = "0" +bind_local:depends("type", "V2ray") +bind_local:depends("type", "Xray") + +accept_lan = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +accept_lan.default = "0" +accept_lan:depends("type", "V2ray") +accept_lan:depends("type", "Xray") + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" and (e.type == "V2ray" or e.type == "Xray") then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remarks = e["remark"] + } + end +end + +outbound_node = s:option(ListValue, "outbound_node", translate("outbound node")) +outbound_node:value("nil", translate("Close")) +outbound_node:value("_socks", translate("Custom Socks")) +outbound_node:value("_http", translate("Custom HTTP")) +outbound_node:value("_iface", translate("Custom Interface") .. " (Only Support Xray)") +for k, v in pairs(nodes_table) do outbound_node:value(v.id, v.remarks) end +outbound_node.default = "nil" +outbound_node:depends("type", "V2ray") +outbound_node:depends("type", "Xray") + +outbound_node_address = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)")) +outbound_node_address:depends("outbound_node", "_socks") +outbound_node_address:depends("outbound_node", "_http") + +outbound_node_port = s:option(Value, "outbound_node_port", translate("Port")) +outbound_node_port.datatype = "port" +outbound_node_port:depends("outbound_node", "_socks") +outbound_node_port:depends("outbound_node", "_http") + +outbound_node_username = s:option(Value, "outbound_node_username", translate("Username")) +outbound_node_username:depends("outbound_node", "_socks") +outbound_node_username:depends("outbound_node", "_http") + +outbound_node_password = s:option(Value, "outbound_node_password", translate("Password")) +outbound_node_password.password = true +outbound_node_password:depends("outbound_node", "_socks") +outbound_node_password:depends("outbound_node", "_http") + +outbound_node_iface = s:option(Value, "outbound_node_iface", translate("Interface")) +outbound_node_iface.default = "eth1" +outbound_node_iface:depends("outbound_node", "_iface") + +log = s:option(Flag, "log", translate("Log")) +log.default = "1" +log.rmempty = false + +loglevel = s:option(ListValue, "loglevel", translate("Log Level")) +loglevel.default = "warning" +loglevel:value("debug") +loglevel:value("info") +loglevel:value("warning") +loglevel:value("error") +loglevel:depends({ type = "V2ray", log = true }) +loglevel:depends({ type = "Xray", log = true }) + +return m diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua new file mode 100644 index 000000000..45e9d47e1 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -0,0 +1,881 @@ +module("luci.passwall2.api", package.seeall) +local com = require "luci.passwall2.com" +bin = require "nixio".bin +fs = require "nixio.fs" +sys = require "luci.sys" +uci = require"luci.model.uci".cursor() +util = require "luci.util" +datatypes = require "luci.cbi.datatypes" +jsonc = require "luci.jsonc" +i18n = require "luci.i18n" + +appname = "passwall2" +curl_args = {"-skfL", "--connect-timeout 3", "--retry 3", "-m 60"} +command_timeout = 300 +OPENWRT_ARCH = nil +DISTRIB_ARCH = nil + +LOG_FILE = "/tmp/log/passwall2.log" + +function log(...) + local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ") + local f, err = io.open(LOG_FILE, "a") + if f and err == nil then + f:write(result .. "\n") + f:close() + end +end + +function exec_call(cmd) + local process = io.popen(cmd .. '; echo -e "\n$?"') + local lines = {} + local result = "" + local return_code + for line in process:lines() do + lines[#lines + 1] = line + end + process:close() + if #lines > 0 then + return_code = lines[#lines] + for i = 1, #lines - 1 do + result = result .. lines[i] .. ((i == #lines - 1) and "" or "\n") + end + end + return tonumber(return_code), trim(result) +end + +function base64Decode(text) + local raw = text + if not text then return '' end + text = text:gsub("%z", "") + text = text:gsub("%c", "") + text = text:gsub("_", "/") + text = text:gsub("-", "+") + local mod4 = #text % 4 + text = text .. string.sub('====', mod4 + 1) + local result = nixio.bin.b64decode(text) + if result then + return result:gsub("%z", "") + else + return raw + end +end + +function curl_base(url, file, args) + if not args then args = {} end + if file then + args[#args + 1] = "-o " .. file + end + local cmd = string.format('curl %s "%s"', table_join(args), url) + return exec_call(cmd) +end + +function curl_proxy(url, file, args) + --使用代理 + local socks_server = luci.sys.exec("[ -f /tmp/etc/passwall2/global_SOCKS_server ] && echo -n $(cat /tmp/etc/passwall2/global_SOCKS_server) || echo -n ''") + if socks_server ~= "" then + if not args then args = {} end + local tmp_args = clone(args) + tmp_args[#tmp_args + 1] = "-x socks5h://" .. socks_server + return curl_base(url, file, tmp_args) + end + return nil, nil +end + +function curl_logic(url, file, args) + local return_code, result = curl_proxy(url, file, args) + if not return_code or return_code ~= 0 then + return_code, result = curl_base(url, file, args) + end + return return_code, result +end + +function url(...) + local url = string.format("admin/services/%s", appname) + local args = { ... } + for i, v in pairs(args) do + if v ~= "" then + url = url .. "/" .. v + end + end + return require "luci.dispatcher".build_url(url) +end + +function trim(s) + return (s:gsub("^%s*(.-)%s*$", "%1")) +end + +function is_exist(table, value) + for index, k in ipairs(table) do + if k == value then + return true + end + end + return false +end + +function repeat_exist(table, value) + local count = 0 + for index, k in ipairs(table) do + if k:find("-") and k == value then + count = count + 1 + end + end + if count > 1 then + return true + end + return false +end + +function get_args(arg) + local var = {} + for i, arg_k in pairs(arg) do + if i > 0 then + local v = arg[i + 1] + if v then + if repeat_exist(arg, v) == false then + var[arg_k] = v + end + end + end + end + return var +end + +function get_function_args(arg) + local var = nil + if arg and #arg > 1 then + local param = {} + for i = 2, #arg do + param[#param + 1] = arg[i] + end + var = get_args(param) + end + return var +end + +function strToTable(str) + if str == nil or type(str) ~= "string" then + return {} + end + + return loadstring("return " .. str)() +end + +function is_normal_node(e) + if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface") then + return false + end + return true +end + +function is_special_node(e) + return is_normal_node(e) == false +end + +function is_ip(val) + if is_ipv6(val) then + val = get_ipv6_only(val) + end + return datatypes.ipaddr(val) +end + +function is_ipv6(val) + local str = val + local address = val:match('%[(.*)%]') + if address then + str = address + end + if datatypes.ip6addr(str) then + return true + end + return false +end + +function is_ipv6addrport(val) + if is_ipv6(val) then + local address, port = val:match('%[(.*)%]:([^:]+)$') + if port then + return datatypes.port(port) + end + end + return false +end + +function get_ipv6_only(val) + local result = "" + if is_ipv6(val) then + result = val + if val:match('%[(.*)%]') then + result = val:match('%[(.*)%]') + end + end + return result +end + +function get_ipv6_full(val) + local result = "" + if is_ipv6(val) then + result = val + if not val:match('%[(.*)%]') then + result = "[" .. result .. "]" + end + end + return result +end + +function get_ip_type(val) + if is_ipv6(val) then + return "6" + elseif datatypes.ip4addr(val) then + return "4" + end + return "" +end + +function is_mac(val) + return datatypes.macaddr(val) +end + +function ip_or_mac(val) + if val then + if get_ip_type(val) == "4" then + return "ip" + end + if is_mac(val) then + return "mac" + end + end + return "" +end + +function iprange(val) + if val then + local ipStart, ipEnd = val:match("^([^/]+)-([^/]+)$") + if (ipStart and datatypes.ip4addr(ipStart)) and (ipEnd and datatypes.ip4addr(ipEnd)) then + return true + end + end + return false +end + +function get_domain_from_url(url) + local domain = string.match(url, "//([^/]+)") + if domain then + return domain + end + return url +end + +function get_valid_nodes() + local nodes_ping = uci_get_type("global_other", "nodes_ping") or "" + local nodes = {} + uci:foreach(appname, "nodes", function(e) + e.id = e[".name"] + if e.type and e.remarks then + if e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface") then + e["remark"] = "%s:[%s] " % {i18n.translatef(e.type .. e.protocol), e.remarks} + e["node_type"] = "special" + nodes[#nodes + 1] = e + end + if e.port and e.address then + local address = e.address + if is_ip(address) or datatypes.hostname(address) then + local type = e.type + if (type == "V2ray" or type == "Xray") and e.protocol then + local protocol = e.protocol + if protocol == "vmess" then + protocol = "VMess" + elseif protocol == "vless" then + protocol = "VLESS" + else + protocol = protocol:gsub("^%l",string.upper) + end + type = type .. " " .. protocol + end + if is_ipv6(address) then address = get_ipv6_full(address) end + e["remark"] = "%s:[%s]" % {type, e.remarks} + if nodes_ping:find("info") then + e["remark"] = "%s:[%s] %s:%s" % {type, e.remarks, address, e.port} + end + e.node_type = "normal" + nodes[#nodes + 1] = e + end + end + end + end) + return nodes +end + +function get_node_remarks(n) + local remarks = "" + if n then + if n.protocol and (n.protocol == "_balancing" or n.protocol == "_shunt" or n.protocol == "_iface") then + remarks = "%s:[%s] " % {i18n.translatef(n.type .. n.protocol), n.remarks} + else + local type2 = n.type + if (n.type == "V2ray" or n.type == "Xray") and n.protocol then + local protocol = n.protocol + if protocol == "vmess" then + protocol = "VMess" + elseif protocol == "vless" then + protocol = "VLESS" + else + protocol = protocol:gsub("^%l",string.upper) + end + type2 = type2 .. " " .. protocol + end + remarks = "%s:[%s]" % {type2, n.remarks} + end + end + return remarks +end + +function get_full_node_remarks(n) + local remarks = get_node_remarks(n) + if #remarks > 0 then + if n.address and n.port then + remarks = remarks .. " " .. n.address .. ":" .. n.port + end + end + return remarks +end + +function gen_uuid(format) + local uuid = sys.exec("echo -n $(cat /proc/sys/kernel/random/uuid)") + if format == nil then + uuid = string.gsub(uuid, "-", "") + end + return uuid +end + +function gen_short_uuid() + return sys.exec("echo -n $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)") +end + +function uci_get_type(type, config, default) + local value = uci:get_first(appname, type, config, default) or sys.exec("echo -n $(uci -q get " .. appname .. ".@" .. type .."[0]." .. config .. ")") + if (value == nil or value == "") and (default and default ~= "") then + value = default + end + return value +end + +function uci_get_type_id(id, config, default) + local value = uci:get(appname, id, config, default) or sys.exec("echo -n $(uci -q get " .. appname .. "." .. id .. "." .. config .. ")") + if (value == nil or value == "") and (default and default ~= "") then + value = default + end + return value +end + +function chmod_755(file) + if file and file ~= "" then + if not fs.access(file, "rwx", "rx", "rx") then + fs.chmod(file, 755) + end + end +end + +function get_customed_path(e) + return uci_get_type("global_app", e .. "_file") +end + +function is_finded(e) + return luci.sys.exec('type -t -p "/bin/%s" -p "/usr/bin/%s" -p "%s" "%s"' % {e, e, get_customed_path(e), e}) ~= "" and true or false +end + +function clone(org) + local function copy(org, res) + for k,v in pairs(org) do + if type(v) ~= "table" then + res[k] = v; + else + res[k] = {}; + copy(v, res[k]) + end + end + end + + local res = {} + copy(org, res) + return res +end + +local function get_bin_version_cache(file, cmd) + sys.call("mkdir -p /tmp/etc/passwall2_tmp") + if fs.access(file) then + chmod_755(file) + local md5 = sys.exec("echo -n $(md5sum " .. file .. " | awk '{print $1}')") + if fs.access("/tmp/etc/passwall2_tmp/" .. md5) then + return sys.exec("echo -n $(cat /tmp/etc/passwall2_tmp/%s)" % md5) + else + local version = sys.exec(string.format("echo -n $(%s %s)", file, cmd)) + if version and version ~= "" then + sys.call("echo '" .. version .. "' > " .. "/tmp/etc/passwall2_tmp/" .. md5) + return version + end + end + end + return "" +end + +function get_app_path(app_name) + local def_path = com[app_name].default_path + local path = uci_get_type("global_app", app_name:gsub("%-","_") .. "_file") + path = path and (#path>0 and path or def_path) or def_path + return path +end + +function get_app_version(app_name, file) + if file == nil then file = get_app_path(app_name) end + return get_bin_version_cache(file, com[app_name].cmd_version) +end + +function is_file(path) + if path and #path > 1 then + if sys.exec('[ -f "%s" ] && echo -n 1' % path) == "1" then + return true + end + end + return nil +end + +function is_dir(path) + if path and #path > 1 then + if sys.exec('[ -d "%s" ] && echo -n 1' % path) == "1" then + return true + end + end + return nil +end + +function get_final_dir(path) + if is_dir(path) then + return path + else + return get_final_dir(fs.dirname(path)) + end +end + +function get_free_space(dir) + if dir == nil then dir = "/" end + if sys.call("df -k " .. dir .. " >/dev/null 2>&1") == 0 then + return tonumber(sys.exec("echo -n $(df -k " .. dir .. " | awk 'NR>1' | awk '{print $4}')")) + end + return 0 +end + +function get_file_space(file) + if file == nil then return 0 end + if fs.access(file) then + return tonumber(sys.exec("echo -n $(du -k " .. file .. " | awk '{print $1}')")) + end + return 0 +end + +function _unpack(t, i) + i = i or 1 + if t[i] ~= nil then return t[i], _unpack(t, i + 1) end +end + +function table_join(t, s) + if not s then + s = " " + end + local str = "" + for index, value in ipairs(t) do + str = str .. t[index] .. (index == #t and "" or s) + end + return str +end + +function exec(cmd, args, writer, timeout) + local os = require "os" + local nixio = require "nixio" + + local fdi, fdo = nixio.pipe() + local pid = nixio.fork() + + if pid > 0 then + fdo:close() + + if writer or timeout then + local starttime = os.time() + while true do + if timeout and os.difftime(os.time(), starttime) >= timeout then + nixio.kill(pid, nixio.const.SIGTERM) + return 1 + end + + if writer then + local buffer = fdi:read(2048) + if buffer and #buffer > 0 then + writer(buffer) + end + end + + local wpid, stat, code = nixio.waitpid(pid, "nohang") + + if wpid and stat == "exited" then return code end + + if not writer and timeout then nixio.nanosleep(1) end + end + else + local wpid, stat, code = nixio.waitpid(pid) + return wpid and stat == "exited" and code + end + elseif pid == 0 then + nixio.dup(fdo, nixio.stdout) + fdi:close() + fdo:close() + nixio.exece(cmd, args, nil) + nixio.stdout:close() + os.exit(1) + end +end + +function parseURL(url) + if not url or url == "" then + return nil + end + local pattern = "^(%w+)://" + local protocol = url:match(pattern) + + if not protocol then + --error("Invalid URL: " .. url) + return nil + end + + local auth_host_port = url:sub(#protocol + 4) + local auth_pattern = "^([^@]+)@" + local auth = auth_host_port:match(auth_pattern) + local username, password + + if auth then + username, password = auth:match("^([^:]+):([^:]+)$") + auth_host_port = auth_host_port:sub(#auth + 2) + end + + local host, port = auth_host_port:match("^([^:]+):(%d+)$") + + if not host or not port then + --error("Invalid URL: " .. url) + return nil + end + + return { + protocol = protocol, + username = username, + password = password, + host = host, + port = tonumber(port) + } +end + +function compare_versions(ver1, comp, ver2) + local table = table + + if not ver1 then ver1 = "" end + if not ver2 then ver2 = "" end + + local av1 = util.split(ver1, "[%.%-]", nil, true) + local av2 = util.split(ver2, "[%.%-]", nil, true) + + local max = table.getn(av1) + local n2 = table.getn(av2) + if (max < n2) then max = n2 end + + for i = 1, max, 1 do + local s1 = tonumber(av1[i] or 0) or 0 + local s2 = tonumber(av2[i] or 0) or 0 + + if comp == "~=" and (s1 ~= s2) then return true end + if (comp == "<" or comp == "<=") and (s1 < s2) then return true end + if (comp == ">" or comp == ">=") and (s1 > s2) then return true end + if (s1 ~= s2) then return false end + end + + return not (comp == "<" or comp == ">") +end + +local function auto_get_arch() + local arch = nixio.uname().machine or "" + if not OPENWRT_ARCH and fs.access("/usr/lib/os-release") then + OPENWRT_ARCH = sys.exec("echo -n $(grep 'OPENWRT_ARCH' /usr/lib/os-release | awk -F '[\\042\\047]' '{print $2}')") + if OPENWRT_ARCH == "" then OPENWRT_ARCH = nil end + end + if not DISTRIB_ARCH and fs.access("/etc/openwrt_release") then + DISTRIB_ARCH = sys.exec("echo -n $(grep 'DISTRIB_ARCH' /etc/openwrt_release | awk -F '[\\042\\047]' '{print $2}')") + if DISTRIB_ARCH == "" then DISTRIB_ARCH = nil end + end + + if arch:match("^i[%d]86$") then + arch = "x86" + elseif arch:match("armv5") then -- armv5l + arch = "armv5" + elseif arch:match("armv6") then + arch = "armv6" + elseif arch:match("armv7") then -- armv7l + arch = "armv7" + end + + if OPENWRT_ARCH or DISTRIB_ARCH then + if arch == "mips" then + if OPENWRT_ARCH and OPENWRT_ARCH:match("mipsel") == "mipsel" + or DISTRIB_ARCH and DISTRIB_ARCH:match("mipsel") == "mipsel" then + arch = "mipsel" + end + elseif arch == "armv7" then + if OPENWRT_ARCH and not OPENWRT_ARCH:match("vfp") and not OPENWRT_ARCH:match("neon") + or DISTRIB_ARCH and not DISTRIB_ARCH:match("vfp") and not DISTRIB_ARCH:match("neon") then + arch = "armv5" + end + end + end + + return util.trim(arch) +end + +local default_file_tree = { + x86_64 = "amd64", + x86 = "386", + aarch64 = "arm64", + mips = "mips", + mipsel = "mipsle", + armv5 = "arm.*5", + armv6 = "arm.*6[^4]*", + armv7 = "arm.*7", + armv8 = "arm64" +} + +function get_api_json(url) + local jsonc = require "luci.jsonc" + local return_code, content = curl_logic(url, nil, curl_args) + if return_code ~= 0 or content == "" then return {} end + return jsonc.parse(content) or {} +end + +local function check_path(app_name) + local path = get_app_path(app_name) or "" + if path == "" then + return { + code = 1, + error = i18n.translatef("You did not fill in the %s path. Please save and apply then update manually.", app_name) + } + end + return { + code = 0, + app_path = path + } +end + +function to_check(arch, app_name) + local result = check_path(app_name) + if result.code ~= 0 then + return result + end + + if not arch or arch == "" then arch = auto_get_arch() end + + local file_tree = com[app_name].file_tree[arch] or default_file_tree[arch] or "" + + if file_tree == "" then + return { + code = 1, + error = i18n.translate("Can't determine ARCH, or ARCH not supported.") + } + end + + local local_version = get_app_version(app_name) + local match_file_name = string.format(com[app_name].match_fmt_str, file_tree) + local json = get_api_json(com[app_name]:get_url()) + + if #json > 0 then + json = json[1] + end + + if json.tag_name == nil then + return { + code = 1, + error = i18n.translate("Get remote version info failed.") + } + end + + local remote_version = json.tag_name + local has_update = compare_versions(local_version:match("[^v]+"), "<", remote_version:match("[^v]+")) + + if not has_update then + return { + code = 0, + local_version = local_version, + remote_version = remote_version + } + end + + local asset = {} + for _, v in ipairs(json.assets) do + if v.name and v.name:match(match_file_name) then + asset = v + break + end + end + if not asset.browser_download_url then + return { + code = 1, + local_version = local_version, + remote_version = remote_version, + html_url = json.html_url, + data = asset, + error = i18n.translate("New version found, but failed to get new version download url.") + } + end + + return { + code = 0, + has_update = true, + local_version = local_version, + remote_version = remote_version, + html_url = json.html_url, + data = asset + } +end + +function to_download(app_name, url, size) + local result = check_path(app_name) + if result.code ~= 0 then + return result + end + + if not url or url == "" then + return {code = 1, error = i18n.translate("Download url is required.")} + end + + sys.call("/bin/rm -f /tmp/".. app_name .."_download.*") + + local tmp_file = util.trim(util.exec("mktemp -u -t ".. app_name .."_download.XXXXXX")) + + if size then + local kb1 = get_free_space("/tmp") + if tonumber(size) > tonumber(kb1) then + return {code = 1, error = i18n.translatef("%s not enough space.", "/tmp")} + end + end + + local return_code, result = curl_logic(url, tmp_file, curl_args) + result = return_code == 0 + + if not result then + exec("/bin/rm", {"-f", tmp_file}) + return { + code = 1, + error = i18n.translatef("File download failed or timed out: %s", url) + } + end + + return {code = 0, file = tmp_file, zip = com[app_name].zipped } +end + +function to_extract(app_name, file, subfix) + local result = check_path(app_name) + if result.code ~= 0 then + return result + end + + if not file or file == "" or not fs.access(file) then + return {code = 1, error = i18n.translate("File path required.")} + end + + if sys.exec("echo -n $(opkg list-installed | grep -c unzip)") ~= "1" then + exec("/bin/rm", {"-f", file}) + return { + code = 1, + error = i18n.translate("Not installed unzip, Can't unzip!") + } + end + + sys.call("/bin/rm -rf /tmp/".. app_name .."_extract.*") + + local new_file_size = get_file_space(file) + local tmp_free_size = get_free_space("/tmp") + if tmp_free_size <= 0 or tmp_free_size <= new_file_size then + return {code = 1, error = i18n.translatef("%s not enough space.", "/tmp")} + end + + local tmp_dir = util.trim(util.exec("mktemp -d -t ".. app_name .."_extract.XXXXXX")) + + local output = {} + exec("/usr/bin/unzip", {"-o", file, app_name, "-d", tmp_dir}, + function(chunk) output[#output + 1] = chunk end) + + local files = util.split(table.concat(output)) + + exec("/bin/rm", {"-f", file}) + + return {code = 0, file = tmp_dir} +end + +function to_move(app_name,file) + local result = check_path(app_name) + if result.code ~= 0 then + return result + end + + local app_path = result.app_path + local bin_path = file + local cmd_rm_tmp = "/bin/rm -rf /tmp/" .. app_name .. "_download.*" + if fs.stat(file, "type") == "dir" then + bin_path = file .. "/" .. app_name + cmd_rm_tmp = "/bin/rm -rf /tmp/" .. app_name .. "_extract.*" + end + + if not file or file == "" then + sys.call(cmd_rm_tmp) + return {code = 1, error = i18n.translate("Client file is required.")} + end + + local new_version = get_app_version(app_name, bin_path) + if new_version == "" then + sys.call(cmd_rm_tmp) + return { + code = 1, + error = i18n.translate("The client file is not suitable for current device.")..app_name.."__"..bin_path + } + end + + local flag = sys.call('pgrep -af "passwall2/.*'.. app_name ..'" >/dev/null') + if flag == 0 then + sys.call("/etc/init.d/passwall2 stop") + end + + local old_app_size = 0 + if fs.access(app_path) then + old_app_size = get_file_space(app_path) + end + local new_app_size = get_file_space(bin_path) + local final_dir = get_final_dir(app_path) + local final_dir_free_size = get_free_space(final_dir) + if final_dir_free_size > 0 then + final_dir_free_size = final_dir_free_size + old_app_size + if new_app_size > final_dir_free_size then + sys.call(cmd_rm_tmp) + return {code = 1, error = i18n.translatef("%s not enough space.", final_dir)} + end + end + + result = exec("/bin/mv", { "-f", bin_path, app_path }, nil, command_timeout) == 0 + + sys.call(cmd_rm_tmp) + if flag == 0 then + sys.call("/etc/init.d/passwall2 restart >/dev/null 2>&1 &") + end + + if not result or not fs.access(app_path) then + return { + code = 1, + error = i18n.translatef("Can't move new file to path: %s", app_path) + } + end + + return {code = 0} +end diff --git a/luci-app-passwall2/luasrc/passwall2/com.lua b/luci-app-passwall2/luasrc/passwall2/com.lua new file mode 100644 index 000000000..dfdf8321f --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/com.lua @@ -0,0 +1,63 @@ +local _M = {} + +local function gh_release_url(self) + return "https://api.github.com/repos/" .. self.repo .. "/releases/latest" +end + +local function gh_pre_release_url(self) + return "https://api.github.com/repos/" .. self.repo .. "/releases?per_page=1" +end + +_M.brook = { + name = "Brook", + repo = "txthinking/brook", + get_url = gh_release_url, + cmd_version = "-v | awk '{print $3}'", + zipped = false, + default_path = "/usr/bin/brook", + match_fmt_str = "linux_%s$", + file_tree = {} +} + +_M.hysteria = { + name = "Hysteria", + repo = "HyNetwork/hysteria", + get_url = gh_release_url, + cmd_version = "-v | awk '{print $3}'", + zipped = false, + default_path = "/usr/bin/hysteria", + match_fmt_str = "linux%%-%s$", + file_tree = { + armv6 = "arm", + armv7 = "arm" + } +} + +_M.v2ray = { + name = "V2ray", + repo = "v2fly/v2ray-core", + get_url = gh_pre_release_url, + cmd_version = "version | awk '{print $2}' | sed -n 1P", + zipped = true, + default_path = "/usr/bin/v2ray", + match_fmt_str = "linux%%-%s", + file_tree = { + x86_64 = "64", + x86 = "32", + mips = "mips32", + mipsel = "mips32le" + } +} + +_M.xray = { + name = "Xray", + repo = "XTLS/Xray-core", + get_url = gh_pre_release_url, + cmd_version = _M.v2ray.cmd_version, + zipped = true, + default_path = "/usr/bin/xray", + match_fmt_str = _M.v2ray.match_fmt_str, + file_tree = _M.v2ray.file_tree +} + +return _M diff --git a/luci-app-passwall2/luasrc/passwall2/server_app.lua b/luci-app-passwall2/luasrc/passwall2/server_app.lua new file mode 100644 index 000000000..aa3dca543 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/server_app.lua @@ -0,0 +1,230 @@ +#!/usr/bin/lua + +local action = arg[1] +local api = require "luci.passwall2.api" +local sys = api.sys +local uci = api.uci +local jsonc = api.jsonc + +local CONFIG = "passwall2_server" +local CONFIG_PATH = "/tmp/etc/" .. CONFIG +local NFT_INCLUDE_FILE = CONFIG_PATH .. "/" .. CONFIG .. ".nft" +local LOG_APP_FILE = "/tmp/log/" .. CONFIG .. ".log" +local TMP_BIN_PATH = CONFIG_PATH .. "/bin" +local require_dir = "luci.passwall2." + +local ipt_bin = sys.exec("echo -n $(/usr/share/passwall2/iptables.sh get_ipt_bin)") +local ip6t_bin = sys.exec("echo -n $(/usr/share/passwall2/iptables.sh get_ip6t_bin)") + +local nft_flag = api.is_finded("fw4") and "1" or "0" + +local function log(...) + local f, err = io.open(LOG_APP_FILE, "a") + if f and err == nil then + local str = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ") + f:write(str .. "\n") + f:close() + end +end + +local function cmd(cmd) + sys.call(cmd) +end + +local function ipt(arg) + if ipt_bin and #ipt_bin > 0 then + cmd(ipt_bin .. " -w " .. arg) + end +end + +local function ip6t(arg) + if ip6t_bin and #ip6t_bin > 0 then + cmd(ip6t_bin .. " -w " .. arg) + end +end + +local function ln_run(s, d, command, output) + if not output then + output = "/dev/null" + end + d = TMP_BIN_PATH .. "/" .. d + cmd(string.format('[ ! -f "%s" ] && ln -s %s %s 2>/dev/null', d, s, d)) + return string.format("%s >%s 2>&1 &", d .. " " ..command, output) +end + +local function gen_include() + cmd(string.format("echo '#!/bin/sh' > /tmp/etc/%s.include", CONFIG)) + local function extract_rules(n, a) + local _ipt = ipt_bin + if n == "6" then + _ipt = ip6t_bin + end + local result = "*" .. a + result = result .. "\n" .. sys.exec(_ipt .. '-save -t ' .. a .. ' | grep "PSW2-SERVER" | sed -e "s/^-A \\(INPUT\\)/-I \\1 1/"') + result = result .. "COMMIT" + return result + end + local f, err = io.open("/tmp/etc/" .. CONFIG .. ".include", "a") + if f and err == nil then + if nft_flag == "0" then + f:write(ipt_bin .. '-save -c | grep -v "PSW2-SERVER" | ' .. ipt_bin .. '-restore -c' .. "\n") + f:write(ipt_bin .. '-restore -n <<-EOT' .. "\n") + f:write(extract_rules("4", "filter") .. "\n") + f:write("EOT" .. "\n") + f:write(ip6t_bin .. '-save -c | grep -v "PSW2-SERVER" | ' .. ip6t_bin .. '-restore -c' .. "\n") + f:write(ip6t_bin .. '-restore -n <<-EOT' .. "\n") + f:write(extract_rules("6", "filter") .. "\n") + f:write("EOT" .. "\n") + f:close() + else + f:write("nft -f " .. NFT_INCLUDE_FILE .. "\n") + f:close() + end + end +end + +local function start() + local enabled = tonumber(uci:get(CONFIG, "@global[0]", "enable") or 0) + if enabled == nil or enabled == 0 then + return + end + cmd(string.format("mkdir -p %s %s", CONFIG_PATH, TMP_BIN_PATH)) + cmd(string.format("touch %s", LOG_APP_FILE)) + if nft_flag == "0" then + ipt("-N PSW2-SERVER") + ipt("-I INPUT -j PSW2-SERVER") + ip6t("-N PSW2-SERVER") + ip6t("-I INPUT -j PSW2-SERVER") + else + nft_file, err = io.open(NFT_INCLUDE_FILE, "w") + nft_file:write('#!/usr/sbin/nft -f\n') + nft_file:write('add chain inet fw4 PSW2-SERVER\n') + nft_file:write('flush chain inet fw4 PSW2-SERVER\n') + nft_file:write('insert rule inet fw4 input position 0 jump PSW2-SERVER comment "PSW2-SERVER"\n') + end + uci:foreach(CONFIG, "user", function(user) + local id = user[".name"] + local enable = user.enable + if enable and tonumber(enable) == 1 then + local enable_log = user.log + local log_path = nil + if enable_log and enable_log == "1" then + log_path = CONFIG_PATH .. "/" .. id .. ".log" + else + log_path = nil + end + local remarks = user.remarks + local port = tonumber(user.port) + local bin + local config = {} + local config_file = CONFIG_PATH .. "/" .. id .. ".json" + local udp_forward = 1 + local type = user.type or "" + if type == "Socks" then + local auth = "" + if user.auth and user.auth == "1" then + local username = user.username or "" + local password = user.password or "" + if username ~= "" and password ~= "" then + username = "-u " .. username + password = "-P " .. password + auth = username .. " " .. password + end + end + bin = ln_run("/usr/bin/microsocks", "microsocks_" .. id, string.format("-i :: -p %s %s", port, auth), log_path) + elseif type == "SS" or type == "SSR" then + config = require(require_dir .. "util_shadowsocks").gen_config_server(user) + local udp_param = "" + udp_forward = tonumber(user.udp_forward) or 1 + if udp_forward == 1 then + udp_param = "-u" + end + type = type:lower() + bin = ln_run("/usr/bin/" .. type .. "-server", type .. "-server", "-c " .. config_file .. " " .. udp_param, log_path) + elseif type == "SS-Rust" then + config = require(require_dir .. "util_shadowsocks").gen_config_server(user) + bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path) + elseif type == "V2ray" then + config = require(require_dir .. "util_xray").gen_config_server(user) + bin = ln_run(api.get_app_path("v2ray"), "v2ray", "run -c " .. config_file, log_path) + elseif type == "Xray" then + config = require(require_dir .. "util_xray").gen_config_server(user) + bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path) + elseif type == "Brook" then + local brook_protocol = user.protocol + local brook_password = user.password + local brook_path = user.ws_path or "/ws" + local brook_path_arg = "" + if brook_protocol == "wsserver" and brook_path then + brook_path_arg = " --path " .. brook_path + end + bin = ln_run(api.get_app_path("brook"), "brook_" .. id, string.format("--debug %s -l :%s -p %s%s", brook_protocol, port, brook_password, brook_path_arg), log_path) + elseif type == "Hysteria" then + config = require(require_dir .. "util_hysteria").gen_config_server(user) + bin = ln_run(api.get_app_path("hysteria"), "hysteria", "-c " .. config_file .. " server", log_path) + end + + if next(config) then + local f, err = io.open(config_file, "w") + if f and err == nil then + f:write(jsonc.stringify(config, 1)) + f:close() + end + log(string.format("%s %s 生成配置文件并运行 - %s", remarks, port, config_file)) + end + + if bin then + cmd(bin) + end + + local bind_local = user.bind_local or 0 + if bind_local and tonumber(bind_local) ~= 1 then + if nft_flag == "0" then + ipt(string.format('-A PSW2-SERVER -p tcp --dport %s -m comment --comment "%s" -j ACCEPT', port, remarks)) + ip6t(string.format('-A PSW2-SERVER -p tcp --dport %s -m comment --comment "%s" -j ACCEPT', port, remarks)) + if udp_forward == 1 then + ipt(string.format('-A PSW2-SERVER -p udp --dport %s -m comment --comment "%s" -j ACCEPT', port, remarks)) + ip6t(string.format('-A PSW2-SERVER -p udp --dport %s -m comment --comment "%s" -j ACCEPT', port, remarks)) + end + else + nft_file:write(string.format('add rule inet fw4 PSW2-SERVER meta l4proto tcp tcp dport {%s} counter accept comment "%s"\n', port, remarks)) + if udp_forward == 1 then + nft_file:write(string.format('add rule inet fw4 PSW2-SERVER meta l4proto udp udp dport {%s} counter accept comment "%s"\n', port, remarks)) + end + end + end + end + end) + if nft_flag == "1" then + nft_file:write("add rule inet fw4 PSW2-SERVER return\n") + nft_file:close() + cmd("nft -f " .. NFT_INCLUDE_FILE) + end + gen_include() +end + +local function stop() + cmd(string.format("top -bn1 | grep -v 'grep' | grep '%s/' | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1", CONFIG_PATH)) + if nft_flag == "0" then + ipt("-D INPUT -j PSW2-SERVER 2>/dev/null") + ipt("-F PSW2-SERVER 2>/dev/null") + ipt("-X PSW2-SERVER 2>/dev/null") + ip6t("-D INPUT -j PSW2-SERVER 2>/dev/null") + ip6t("-F PSW2-SERVER 2>/dev/null") + ip6t("-X PSW2-SERVER 2>/dev/null") + else + local nft_cmd = "handles=$(nft -a list chain inet fw4 input | grep -E \"PSW2-SERVER\" | awk -F '# handle ' '{print$2}')\n for handle in $handles; do\n nft delete rule inet fw4 input handle ${handle} 2>/dev/null\n done" + cmd(nft_cmd) + cmd("nft flush chain inet fw4 PSW2-SERVER 2>/dev/null") + cmd("nft delete chain inet fw4 PSW2-SERVER 2>/dev/null") + end + cmd(string.format("rm -rf %s %s /tmp/etc/%s.include", CONFIG_PATH, LOG_APP_FILE, CONFIG)) +end + +if action then + if action == "start" then + start() + elseif action == "stop" then + stop() + end +end diff --git a/luci-app-passwall2/luasrc/passwall2/util_hysteria.lua b/luci-app-passwall2/luasrc/passwall2/util_hysteria.lua new file mode 100644 index 000000000..e55586427 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/util_hysteria.lua @@ -0,0 +1,104 @@ +module("luci.passwall2.util_hysteria", package.seeall) +local api = require "luci.passwall2.api" +local uci = api.uci +local jsonc = api.jsonc + +function gen_config_server(node) + local config = { + listen = ":" .. node.port, + protocol = node.protocol or "udp", + obfs = node.hysteria_obfs, + cert = node.tls_certificateFile, + key = node.tls_keyFile, + auth = (node.hysteria_auth_type == "string") and { + mode = "password", + config = { + password = node.hysteria_auth_password + } + } or nil, + disable_udp = (node.hysteria_udp == "0") and true or false, + alpn = node.hysteria_alpn or nil, + up_mbps = tonumber(node.hysteria_up_mbps) or 10, + down_mbps = tonumber(node.hysteria_down_mbps) or 50, + recv_window_conn = (node.hysteria_recv_window_conn) and tonumber(node.hysteria_recv_window_conn) or nil, + recv_window = (node.hysteria_recv_window) and tonumber(node.hysteria_recv_window) or nil, + disable_mtu_discovery = (node.hysteria_disable_mtu_discovery) and true or false + } + return config +end + +function gen_config(var) + local node_id = var["-node"] + if not node_id then + print("-node 不能为空") + return + end + local node = uci:get_all("passwall2", node_id) + local local_socks_address = var["-local_socks_address"] or "0.0.0.0" + local local_socks_port = var["-local_socks_port"] + local local_socks_username = var["-local_socks_username"] + local local_socks_password = var["-local_socks_password"] + local local_http_address = var["-local_http_address"] or "0.0.0.0" + local local_http_port = var["-local_http_port"] + local local_http_username = var["-local_http_username"] + local local_http_password = var["-local_http_password"] + local server_host = var["-server_host"] or node.address + local server_port = var["-server_port"] or node.port + + if api.is_ipv6(server_host) then + server_host = api.get_ipv6_full(server_host) + end + local server = server_host .. ":" .. server_port + + if (node.hysteria_hop) then + server = server .. "," .. node.hysteria_hop + end + + local config = { + server = server, + protocol = node.protocol or "udp", + obfs = node.hysteria_obfs, + auth = (node.hysteria_auth_type == "base64") and node.hysteria_auth_password or nil, + auth_str = (node.hysteria_auth_type == "string") and node.hysteria_auth_password or nil, + alpn = node.hysteria_alpn or nil, + server_name = node.tls_serverName, + insecure = (node.tls_allowInsecure == "1") and true or false, + up_mbps = tonumber(node.hysteria_up_mbps) or 10, + down_mbps = tonumber(node.hysteria_down_mbps) or 50, + retry = -1, + retry_interval = 5, + recv_window_conn = (node.hysteria_recv_window_conn) and tonumber(node.hysteria_recv_window_conn) or nil, + recv_window = (node.hysteria_recv_window) and tonumber(node.hysteria_recv_window) or nil, + handshake_timeout = (node.hysteria_handshake_timeout) and tonumber(node.hysteria_handshake_timeout) or nil, + idle_timeout = (node.hysteria_idle_timeout) and tonumber(node.hysteria_idle_timeout) or nil, + hop_interval = (node.hysteria_hop_interval) and tonumber(node.hysteria_hop_interval) or nil, + disable_mtu_discovery = (node.hysteria_disable_mtu_discovery) and true or false, + fast_open = (node.fast_open == "1") and true or false, + lazy_start = (node.hysteria_lazy_start) and true or false, + socks5 = (local_socks_address and local_socks_port) and { + listen = local_socks_address .. ":" .. local_socks_port, + timeout = 300, + disable_udp = false, + user = (local_socks_username and local_socks_password) and local_socks_username, + password = (local_socks_username and local_socks_password) and local_socks_password, + } or nil, + http = (local_http_address and local_http_port) and { + listen = local_http_address .. ":" .. local_http_port, + timeout = 300, + disable_udp = false, + user = (local_http_username and local_http_password) and local_http_username, + password = (local_http_username and local_http_password) and local_http_password, + } or nil + } + + return jsonc.stringify(config, 1) +end + +_G.gen_config = gen_config + +if arg[1] then + local func =_G[arg[1]] + if func then + print(func(api.get_function_args(arg))) + end +end diff --git a/luci-app-passwall2/luasrc/passwall2/util_naiveproxy.lua b/luci-app-passwall2/luasrc/passwall2/util_naiveproxy.lua new file mode 100644 index 000000000..b1f8bb824 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/util_naiveproxy.lua @@ -0,0 +1,39 @@ +module("luci.passwall2.util_navieproxy", package.seeall) +local api = require "luci.passwall2.api" +local uci = api.uci +local jsonc = api.jsonc + +function gen_config(var) + local node_id = var["-node"] + if not node_id then + print("-node 不能为空") + return + end + local node = uci:get_all("passwall2", node_id) + local run_type = var["-run_type"] + local local_addr = var["-local_addr"] + local local_port = var["-local_port"] + local server_host = var["-server_host"] or node.address + local server_port = var["-server_port"] or node.port + + if api.is_ipv6(server_host) then + server_host = api.get_ipv6_full(server_host) + end + local server = server_host .. ":" .. server_port + + local config = { + listen = run_type .. "://" .. local_addr .. ":" .. local_port, + proxy = node.protocol .. "://" .. node.username .. ":" .. node.password .. "@" .. server + } + + return jsonc.stringify(config, 1) +end + +_G.gen_config = gen_config + +if arg[1] then + local func =_G[arg[1]] + if func then + print(func(api.get_function_args(arg))) + end +end diff --git a/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua b/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua new file mode 100644 index 000000000..8c5286a95 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua @@ -0,0 +1,123 @@ +module("luci.passwall2.util_shadowsocks", package.seeall) +local api = require "luci.passwall2.api" +local uci = api.uci +local jsonc = api.jsonc + +function gen_config_server(node) + local config = {} + config.server_port = tonumber(node.port) + config.password = node.password + config.timeout = tonumber(node.timeout) + config.fast_open = (node.tcp_fast_open and node.tcp_fast_open == "1") and true or false + config.method = node.method + + if node.type == "SS-Rust" then + config.server = "::" + config.mode = "tcp_and_udp" + else + config.server = {"[::0]", "0.0.0.0"} + end + + if node.type == "SSR" then + config.protocol = node.protocol + config.protocol_param = node.protocol_param + config.obfs = node.obfs + config.obfs_param = node.obfs_param + end + + return config +end + + +function gen_config(var) + local node_id = var["-node"] + if not node_id then + print("-node 不能为空") + return + end + local node = uci:get_all("passwall2", node_id) + local server_host = var["-server_host"] or node.address + local server_port = var["-server_port"] or node.port + local local_addr = var["-local_addr"] + local local_port = var["-local_port"] + local mode = var["-mode"] + local local_socks_address = var["-local_socks_address"] or "0.0.0.0" + local local_socks_port = var["-local_socks_port"] + local local_socks_username = var["-local_socks_username"] + local local_socks_password = var["-local_socks_password"] + local local_http_address = var["-local_http_address"] or "0.0.0.0" + local local_http_port = var["-local_http_port"] + local local_http_username = var["-local_http_username"] + local local_http_password = var["-local_http_password"] + + if api.is_ipv6(server_host) then + server_host = api.get_ipv6_only(server_host) + end + local server = server_host + + local config = { + server = server, + server_port = tonumber(server_port), + local_address = local_addr, + local_port = tonumber(local_port), + password = node.password, + method = node.method, + timeout = tonumber(node.timeout), + fast_open = (node.tcp_fast_open and node.tcp_fast_open == "true") and true or false, + reuse_port = true + } + + if node.type == "SS" then + if node.plugin and node.plugin ~= "none" then + config.plugin = node.plugin + config.plugin_opts = node.plugin_opts or nil + end + config.mode = mode + elseif node.type == "SSR" then + config.protocol = node.protocol + config.protocol_param = node.protocol_param + config.obfs = node.obfs + config.obfs_param = node.obfs_param + elseif node.type == "SS-Rust" then + config = { + servers = { + { + address = server, + port = tonumber(server_port), + method = node.method, + password = node.password, + timeout = tonumber(node.timeout), + plugin = (node.plugin and node.plugin ~= "none") and node.plugin or nil, + plugin_opts = (node.plugin and node.plugin ~= "none") and node.plugin_opts or nil + } + }, + locals = {}, + fast_open = (node.tcp_fast_open and node.tcp_fast_open == "true") and true or false + } + if local_socks_address and local_socks_port then + table.insert(config.locals, { + local_address = local_socks_address, + local_port = tonumber(local_socks_port), + mode = "tcp_and_udp" + }) + end + if local_http_address and local_http_port then + table.insert(config.locals, { + protocol = "http", + local_address = local_http_address, + local_port = tonumber(local_http_port) + }) + end + end + + return jsonc.stringify(config, 1) +end + +_G.gen_config = gen_config + +if arg[1] then + local func =_G[arg[1]] + if func then + print(func(api.get_function_args(arg))) + end +end diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua new file mode 100644 index 000000000..71b5cea2e --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -0,0 +1,1623 @@ +module("luci.passwall2.util_xray", package.seeall) +local api = require "luci.passwall2.api" +local uci = api.uci +local sys = api.sys +local jsonc = api.jsonc +local appname = api.appname +local fs = api.fs + +local new_port + +local function get_new_port() + if new_port then + new_port = tonumber(sys.exec(string.format("echo -n $(/usr/share/%s/app.sh get_new_port %s tcp)", appname, new_port + 1))) + else + new_port = tonumber(sys.exec(string.format("echo -n $(/usr/share/%s/app.sh get_new_port auto tcp)", appname))) + end + return new_port +end + +local function get_domain_excluded() + local path = string.format("/usr/share/%s/domains_excluded", appname) + local content = fs.readfile(path) + if not content then return nil end + local hosts = {} + string.gsub(content, '[^' .. "\n" .. ']+', function(w) + local s = w:gsub("^%s*(.-)%s*$", "%1") -- Trim + if s == "" then return end + if s:find("#") and s:find("#") == 1 then return end + if not s:find("#") or s:find("#") ~= 1 then table.insert(hosts, s) end + end) + if #hosts == 0 then hosts = nil end + return hosts +end + +function gen_outbound(flag, node, tag, proxy_table) + local result = nil + if node and node ~= "nil" then + local node_id = node[".name"] + if tag == nil then + tag = node_id + end + + local proxy = 0 + local proxy_tag = "nil" + if proxy_table ~= nil and type(proxy_table) == "table" then + proxy = proxy_table.proxy or 0 + proxy_tag = proxy_table.tag or "nil" + end + + if node.type == "V2ray" or node.type == "Xray" then + if node.type == "Xray" and node.tlsflow == "xtls-rprx-vision" then + else + proxy = 0 + if proxy_tag ~= "nil" then + node.proxySettings = { + tag = proxy_tag, + transportLayer = true + } + end + end + end + + if node.type ~= "V2ray" and node.type ~= "Xray" then + local relay_port = node.port + new_port = get_new_port() + local config_file = string.format("%s_%s_%s.json", flag, tag, new_port) + if tag and node_id and tag ~= node_id then + config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port) + end + sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null', + appname, + string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s", + new_port, --flag + node_id, --node + "127.0.0.1", --bind + new_port, --socks port + config_file, --config file + (proxy == 1 and relay_port) and tostring(relay_port) or "" --relay port + ) + ) + ) + node = {} + node.protocol = "socks" + node.transport = "tcp" + node.address = "127.0.0.1" + node.port = new_port + node.stream_security = "none" + end + + if node.type == "V2ray" or node.type == "Xray" then + if node.tls and node.tls == "1" then + node.stream_security = "tls" + if node.type == "Xray" and node.reality and node.reality == "1" then + node.stream_security = "reality" + end + end + end + + if node.protocol == "wireguard" and node.wireguard_reserved then + local bytes = {} + if not node.wireguard_reserved:match("[^%d,]+") then + node.wireguard_reserved:gsub("%d+", function(b) + bytes[#bytes + 1] = tonumber(b) + end) + else + local result = api.bin.b64decode(node.wireguard_reserved) + for i = 1, #result do + bytes[i] = result:byte(i) + end + end + node.wireguard_reserved = #bytes > 0 and bytes or nil + end + + result = { + _flag_tag = node_id, + _flag_proxy = proxy, + _flag_proxy_tag = proxy_tag, + tag = tag, + proxySettings = node.proxySettings or nil, + protocol = node.protocol, + mux = { + enabled = (node.mux == "1" or node.xmux == "1") and true or false, + concurrency = (node.mux == "1" and ((node.mux_concurrency) and tonumber(node.mux_concurrency) or 8)) or ((node.xmux == "1") and -1) or nil, + xudpConcurrency = (node.xmux == "1" and ((node.xudp_concurrency) and tonumber(node.xudp_concurrency) or 8)) or nil + } or nil, + -- 底层传输配置 + streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan") and { + sockopt = { + mark = 255 + }, + network = node.transport, + security = node.stream_security, + tlsSettings = (node.stream_security == "tls") and { + serverName = node.tls_serverName, + allowInsecure = (node.tls_allowInsecure == "1") and true or false, + fingerprint = (node.type == "Xray" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil + } or nil, + realitySettings = (node.stream_security == "reality") and { + serverName = node.tls_serverName, + publicKey = node.reality_publicKey, + shortId = node.reality_shortId or "", + spiderX = node.reality_spiderX or "/", + fingerprint = (node.type == "Xray" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or "chrome" + } or nil, + tcpSettings = (node.transport == "tcp" and node.protocol ~= "socks") and { + header = { + type = node.tcp_guise or "none", + request = (node.tcp_guise == "http") and { + path = node.tcp_guise_http_path or {"/"}, + headers = { + Host = node.tcp_guise_http_host or {} + } + } or nil + } + } or nil, + kcpSettings = (node.transport == "mkcp") and { + mtu = tonumber(node.mkcp_mtu), + tti = tonumber(node.mkcp_tti), + uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), + downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), + congestion = (node.mkcp_congestion == "1") and true or false, + readBufferSize = tonumber(node.mkcp_readBufferSize), + writeBufferSize = tonumber(node.mkcp_writeBufferSize), + seed = (node.mkcp_seed and node.mkcp_seed ~= "") and node.mkcp_seed or nil, + header = {type = node.mkcp_guise} + } or nil, + wsSettings = (node.transport == "ws") and { + path = node.ws_path or "/", + headers = (node.ws_host ~= nil) and + {Host = node.ws_host} or nil, + maxEarlyData = tonumber(node.ws_maxEarlyData) or nil, + earlyDataHeaderName = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil + } or nil, + httpSettings = (node.transport == "h2") and { + path = node.h2_path or "/", + host = node.h2_host, + read_idle_timeout = tonumber(node.h2_read_idle_timeout) or nil, + health_check_timeout = tonumber(node.h2_health_check_timeout) or nil + } or nil, + dsSettings = (node.transport == "ds") and + {path = node.ds_path} or nil, + quicSettings = (node.transport == "quic") and { + security = node.quic_security, + key = node.quic_key, + header = {type = node.quic_guise} + } or nil, + grpcSettings = (node.transport == "grpc") and { + serviceName = node.grpc_serviceName, + multiMode = (node.grpc_mode == "multi") and true or nil, + idle_timeout = tonumber(node.grpc_idle_timeout) or nil, + health_check_timeout = tonumber(node.grpc_health_check_timeout) or nil, + permit_without_stream = (node.grpc_permit_without_stream == "1") and true or nil, + initial_windows_size = tonumber(node.grpc_initial_windows_size) or nil + } or nil + } or nil, + settings = { + vnext = (node.protocol == "vmess" or node.protocol == "vless") and { + { + address = node.address, + port = tonumber(node.port), + users = { + { + id = node.uuid, + level = 0, + security = (node.protocol == "vmess") and node.security or nil, + encryption = node.encryption or "none", + flow = (node.protocol == "vless" and node.tls == '1' and node.tlsflow) and node.tlsflow or nil + } + } + } + } or nil, + servers = (node.protocol == "socks" or node.protocol == "http" or node.protocol == "shadowsocks" or node.protocol == "trojan") and { + { + address = node.address, + port = tonumber(node.port), + method = node.method or nil, + ivCheck = (node.protocol == "shadowsocks") and node.iv_check == "1" or nil, + uot = (node.protocol == "shadowsocks") and node.uot == "1" or nil, + password = node.password or "", + users = (node.username and node.password) and { + { + user = node.username, + pass = node.password + } + } or nil + } + } or nil, + address = (node.protocol == "wireguard" and node.wireguard_local_address) and node.wireguard_local_address or nil, + secretKey = (node.protocol == "wireguard") and node.wireguard_secret_key or nil, + peers = (node.protocol == "wireguard") and { + { + publicKey = node.wireguard_public_key, + endpoint = node.address .. ":" .. node.port, + preSharedKey = node.wireguard_preSharedKey, + keepAlive = node.wireguard_keepAlive and tonumber(node.wireguard_keepAlive) or nil + } + } or nil, + mtu = (node.protocol == "wireguard" and node.wireguard_mtu) and tonumber(node.wireguard_mtu) or nil, + reserved = (node.protocol == "wireguard" and node.wireguard_reserved) and node.wireguard_reserved or nil + } + } + local alpn = {} + if node.alpn and node.alpn ~= "default" then + string.gsub(node.alpn, '[^' .. "," .. ']+', function(w) + table.insert(alpn, w) + end) + end + if alpn and #alpn > 0 then + if result.streamSettings.tlsSettings then + result.streamSettings.tlsSettings.alpn = alpn + end + end + end + return result +end + +function gen_config_server(node) + local settings = nil + local routing = nil + local outbounds = { + {protocol = "freedom", tag = "direct"}, {protocol = "blackhole", tag = "blocked"} + } + + if node.protocol == "vmess" or node.protocol == "vless" then + if node.uuid then + local clients = {} + for i = 1, #node.uuid do + clients[i] = { + id = node.uuid[i], + flow = ("vless" == node.protocol and "1" == node.tls and node.tlsflow) and node.tlsflow or nil + } + end + settings = { + clients = clients, + decryption = node.decryption or "none" + } + end + elseif node.protocol == "socks" then + settings = { + udp = ("1" == node.udp_forward) and true or false, + auth = ("1" == node.auth) and "password" or "noauth", + accounts = ("1" == node.auth) and { + { + user = node.username, + pass = node.password + } + } or nil + } + elseif node.protocol == "http" then + settings = { + allowTransparent = false, + accounts = ("1" == node.auth) and { + { + user = node.username, + pass = node.password + } + } or nil + } + node.transport = "tcp" + node.tcp_guise = "none" + elseif node.protocol == "shadowsocks" then + settings = { + method = node.method, + password = node.password, + ivCheck = ("1" == node.iv_check) and true or false, + network = node.ss_network or "TCP,UDP" + } + elseif node.protocol == "trojan" then + if node.uuid then + local clients = {} + for i = 1, #node.uuid do + clients[i] = { + password = node.uuid[i] + } + end + settings = { + clients = clients + } + end + elseif node.protocol == "mtproto" then + settings = { + users = { + { + secret = (node.password == nil) and "" or node.password + } + } + } + elseif node.protocol == "dokodemo-door" then + settings = { + network = node.d_protocol, + address = node.d_address, + port = tonumber(node.d_port) + } + end + + if node.fallback and node.fallback == "1" then + local fallbacks = {} + for i = 1, #node.fallback_list do + local fallbackStr = node.fallback_list[i] + if fallbackStr then + local tmp = {} + string.gsub(fallbackStr, '[^' .. "," .. ']+', function(w) + table.insert(tmp, w) + end) + local dest = tmp[1] or "" + local path = tmp[2] + if dest:find("%.") then + else + dest = tonumber(dest) + end + fallbacks[i] = { + path = path, + dest = dest, + xver = 1 + } + end + end + settings.fallbacks = fallbacks + end + + routing = { + domainStrategy = "IPOnDemand", + rules = { + { + type = "field", + ip = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}, + outboundTag = (node.accept_lan == nil or node.accept_lan == "0") and "blocked" or "direct" + } + } + } + + if node.outbound_node and node.outbound_node ~= "nil" then + local outbound = nil + if node.outbound_node == "_iface" and node.outbound_node_iface then + outbound = { + protocol = "freedom", + tag = "outbound", + streamSettings = { + sockopt = { + interface = node.outbound_node_iface + } + } + } + else + local outbound_node_t = uci:get_all("passwall2", node.outbound_node) + if node.outbound_node == "_socks" or node.outbound_node == "_http" then + outbound_node_t = { + type = node.type, + protocol = node.outbound_node:gsub("_", ""), + transport = "tcp", + address = node.outbound_node_address, + port = node.outbound_node_port, + username = (node.outbound_node_username and node.outbound_node_username ~= "") and node.outbound_node_username or nil, + password = (node.outbound_node_password and node.outbound_node_password ~= "") and node.outbound_node_password or nil, + } + end + outbound = require("luci.passwall2.util_xray").gen_outbound(nil, outbound_node_t, "outbound") + end + if outbound then + table.insert(outbounds, 1, outbound) + end + end + + local config = { + log = { + loglevel = ("1" == node.log) and node.loglevel or "none" + }, + -- 传入连接 + inbounds = { + { + listen = (node.bind_local == "1") and "127.0.0.1" or nil, + port = tonumber(node.port), + protocol = node.protocol, + settings = settings, + streamSettings = { + network = node.transport, + security = "none", + tlsSettings = ("1" == node.tls) and { + disableSystemRoot = false, + certificates = { + { + certificateFile = node.tls_certificateFile, + keyFile = node.tls_keyFile + } + } + } or nil, + tcpSettings = (node.transport == "tcp") and { + acceptProxyProtocol = (node.acceptProxyProtocol and node.acceptProxyProtocol == "1") and true or false, + header = { + type = node.tcp_guise, + request = (node.tcp_guise == "http") and { + path = node.tcp_guise_http_path or {"/"}, + headers = { + Host = node.tcp_guise_http_host or {} + } + } or nil + } + } or nil, + kcpSettings = (node.transport == "mkcp") and { + mtu = tonumber(node.mkcp_mtu), + tti = tonumber(node.mkcp_tti), + uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), + downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), + congestion = (node.mkcp_congestion == "1") and true or false, + readBufferSize = tonumber(node.mkcp_readBufferSize), + writeBufferSize = tonumber(node.mkcp_writeBufferSize), + seed = (node.mkcp_seed and node.mkcp_seed ~= "") and node.mkcp_seed or nil, + header = {type = node.mkcp_guise} + } or nil, + wsSettings = (node.transport == "ws") and { + acceptProxyProtocol = (node.acceptProxyProtocol and node.acceptProxyProtocol == "1") and true or false, + headers = (node.ws_host) and {Host = node.ws_host} or nil, + path = node.ws_path + } or nil, + httpSettings = (node.transport == "h2") and { + path = node.h2_path, host = node.h2_host + } or nil, + dsSettings = (node.transport == "ds") and { + path = node.ds_path + } or nil, + quicSettings = (node.transport == "quic") and { + security = node.quic_security, + key = node.quic_key, + header = {type = node.quic_guise} + } or nil, + grpcSettings = (node.transport == "grpc") and { + serviceName = node.grpc_serviceName + } or nil + } + } + }, + -- 传出连接 + outbounds = outbounds, + routing = routing + } + + local alpn = {} + if node.alpn then + string.gsub(node.alpn, '[^' .. "," .. ']+', function(w) + table.insert(alpn, w) + end) + end + if alpn and #alpn > 0 then + if config.inbounds[1].streamSettings.tlsSettings then + config.inbounds[1].streamSettings.tlsSettings.alpn = alpn + end + end + + if "1" == node.tls then + config.inbounds[1].streamSettings.security = "tls" + end + + return config +end + +function gen_config(var) + local flag = var["-flag"] + local loglevel = var["-loglevel"] or "warning" + local node_id = var["-node"] + local tcp_proxy_way = var["-tcp_proxy_way"] + local redir_port = var["-redir_port"] + local sniffing = var["-sniffing"] + local route_only = var["-route_only"] + local buffer_size = var["-buffer_size"] + local local_socks_address = var["-local_socks_address"] or "0.0.0.0" + local local_socks_port = var["-local_socks_port"] + local local_socks_username = var["-local_socks_username"] + local local_socks_password = var["-local_socks_password"] + local local_http_address = var["-local_http_address"] or "0.0.0.0" + local local_http_port = var["-local_http_port"] + local local_http_username = var["-local_http_username"] + local local_http_password = var["-local_http_password"] + local dns_listen_port = var["-dns_listen_port"] + local dns_query_strategy = var["-dns_query_strategy"] + local direct_dns_port = var["-direct_dns_port"] + local direct_dns_udp_server = var["-direct_dns_udp_server"] + local remote_dns_port = var["-remote_dns_port"] + local remote_dns_udp_server = var["-remote_dns_udp_server"] + local remote_dns_fake = var["-remote_dns_fake"] + local remote_dns_fake_strategy = var["-remote_dns_fake_strategy"] + local dns_cache = var["-dns_cache"] + + local dns_direct_domains = {} + local dns_direct_expectIPs = {} + local dns_remote_domains = {} + local dns_remote_expectIPs = {} + local dns = nil + local fakedns = nil + local inbounds = {} + local outbounds = {} + local routing = nil + local observatory = nil + + local nodes = {} + if node_id then + local node = uci:get_all(appname, node_id) + if node then + nodes[node_id] = node + end + end + + if local_socks_port then + local inbound = { + listen = local_socks_address, + port = tonumber(local_socks_port), + protocol = "socks", + settings = {auth = "noauth", udp = true}, + sniffing = {enabled = true, destOverride = {"http", "tls"}} + } + if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then + inbound.settings.auth = "password" + inbound.settings.accounts = { + { + user = local_socks_username, + pass = local_socks_password + } + } + end + table.insert(inbounds, inbound) + end + + if local_http_port then + local inbound = { + listen = local_http_address, + port = tonumber(local_http_port), + protocol = "http", + settings = {allowTransparent = false} + } + if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then + inbound.settings.accounts = { + { + user = local_http_username, + pass = local_http_password + } + } + end + table.insert(inbounds, inbound) + end + + if redir_port then + local inbound = { + port = tonumber(redir_port), + protocol = "dokodemo-door", + settings = {network = "tcp,udp", followRedirect = true}, + streamSettings = {sockopt = {tproxy = "tproxy"}}, + sniffing = {enabled = sniffing and true or false, destOverride = {"http", "tls", (remote_dns_fake) and "fakedns"}, metadataOnly = false, routeOnly = route_only and true or nil, domainsExcluded = (sniffing and not route_only) and get_domain_excluded() or nil} + } + local tcp_inbound = api.clone(inbound) + tcp_inbound.tag = "tcp_redir" + tcp_inbound.settings.network = "tcp" + tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way + table.insert(inbounds, tcp_inbound) + + local udp_inbound = api.clone(inbound) + udp_inbound.tag = "udp_redir" + udp_inbound.settings.network = "udp" + table.insert(inbounds, udp_inbound) + end + + local function get_balancer_tag(_node_id) + return "balancer-" .. _node_id + end + + local function gen_balancer(_node, loopbackTag) + local blc_nodes = _node.balancing_node + local length = #blc_nodes + local valid_nodes = {} + for i = 1, length do + local blc_node_id = blc_nodes[i] + local blc_node_tag = "blc-" .. blc_node_id + local is_new_blc_node = true + for _, outbound in ipairs(outbounds) do + if outbound.tag == blc_node_tag then + is_new_blc_node = false + valid_nodes[#valid_nodes + 1] = blc_node_tag + break + end + end + if is_new_blc_node then + local blc_node = uci:get_all(appname, blc_node_id) + local outbound = gen_outbound(flag, blc_node, blc_node_tag) + if outbound then + table.insert(outbounds, outbound) + valid_nodes[#valid_nodes + 1] = blc_node_tag + end + end + end + + local balancer, rule + if #valid_nodes > 0 then + local balancerTag = get_balancer_tag(_node[".name"]) + balancer = { + tag = balancerTag, + selector = valid_nodes, + strategy = { type = _node.balancingStrategy or "random" } + } + if _node.balancingStrategy == "leastPing" then + if not observatory then + observatory = { + subjectSelector = { "blc-" }, + probeUrl = _node.useCustomProbeUrl and _node.probeUrl or nil, + probeInterval = _node.probeInterval or "1m", + enableConcurrency = (nodes[node_id] and nodes[node_id].type == "Xray") and true or nil --这里只判断顶层节点(分流总节点/单独的负载均衡节点)类型为Xray,就可以启用并发 + } + end + end + if loopbackTag and loopbackTag ~= "" then + local inboundTag = loopbackTag .. "-in" + table.insert(outbounds, { + protocol = "loopback", + tag = loopbackTag, + settings = { inboundTag = inboundTag } + }) + rule = { + type = "field", + inboundTag = { inboundTag }, + balancerTag = balancerTag + } + end + end + return balancer, rule + end + + for k, v in pairs(nodes) do + local node = v + if node.protocol == "_shunt" then + local rules = {} + local balancers = {} + + local preproxy_enabled = node.preproxy_enabled == "1" + local preproxy_tag = "main" + local preproxy_node_id = node["main_node"] + local preproxy_node = preproxy_enabled and preproxy_node_id and uci:get_all(appname, preproxy_node_id) or nil + local preproxy_is_balancer + + if not preproxy_node and preproxy_node_id and api.parseURL(preproxy_node_id) then + local parsed1 = api.parseURL(preproxy_node_id) + local _node = { + type = "Xray", + protocol = parsed1.protocol, + username = parsed1.username, + password = parsed1.password, + address = parsed1.host, + port = parsed1.port, + transport = "tcp", + stream_security = "none" + } + local preproxy_outbound = gen_outbound(flag, _node, preproxy_tag) + if preproxy_outbound then + table.insert(outbounds, preproxy_outbound) + else + preproxy_enabled = false + end + elseif preproxy_node and api.is_normal_node(preproxy_node) then + local preproxy_outbound = gen_outbound(flag, preproxy_node, preproxy_tag) + if preproxy_outbound then + table.insert(outbounds, preproxy_outbound) + else + preproxy_enabled = false + end + elseif preproxy_node and preproxy_node.protocol == "_balancing" then + preproxy_is_balancer = true + local preproxy_balancer, preproxy_rule = gen_balancer(preproxy_node, preproxy_tag) + if preproxy_balancer and preproxy_rule then + table.insert(balancers, preproxy_balancer) + table.insert(rules, preproxy_rule) + else + preproxy_enabled = false + end + end + + local function gen_shunt_node(rule_name, _node_id, as_proxy) + if not rule_name then return nil, nil end + if not _node_id then _node_id = node[rule_name] or "nil" end + local rule_outboundTag + local rule_balancerTag + if _node_id == "_direct" then + rule_outboundTag = "direct" + elseif _node_id == "_blackhole" then + rule_outboundTag = "blackhole" + elseif _node_id == "_default" and rule_name ~= "default" then + rule_outboundTag = "default" + elseif api.parseURL(_node_id) then + local parsed1 = api.parseURL(_node_id) + local _node = { + type = "Xray", + protocol = parsed1.protocol, + username = parsed1.username, + password = parsed1.password, + address = parsed1.host, + port = parsed1.port, + transport = "tcp", + stream_security = "none" + } + local _outbound = gen_outbound(flag, _node, rule_name) + if _outbound then + table.insert(outbounds, _outbound) + rule_outboundTag = rule_name + end + elseif _node_id ~= "nil" then + local _node = uci:get_all(appname, _node_id) + if not _node then return nil, nil end + + if api.is_normal_node(_node) then + local proxy = preproxy_enabled and node[rule_name .. "_proxy_tag"] == preproxy_tag and _node_id ~= preproxy_node_id + if proxy and preproxy_is_balancer then + local blc_nodes = preproxy_node.balancing_node + for _, blc_node_id in ipairs(blc_nodes) do + if _node_id == blc_node_id then + proxy = false + break + end + end + end + local copied_outbound + for index, value in ipairs(outbounds) do + if value["_flag_tag"] == _node_id and value["_flag_proxy_tag"] == preproxy_tag then + copied_outbound = api.clone(value) + break + end + end + if copied_outbound then + copied_outbound.tag = rule_name + table.insert(outbounds, copied_outbound) + rule_outboundTag = rule_name + else + if proxy then + local pre_proxy = nil + if _node.type ~= "V2ray" and _node.type ~= "Xray" then + pre_proxy = true + end + if _node.type == "Xray" and _node.tlsflow == "xtls-rprx-vision" then + pre_proxy = true + end + if pre_proxy then + new_port = get_new_port() + table.insert(inbounds, { + tag = "proxy_" .. rule_name, + listen = "127.0.0.1", + port = new_port, + protocol = "dokodemo-door", + settings = {network = "tcp,udp", address = _node.address, port = tonumber(_node.port)} + }) + if _node.tls_serverName == nil then + _node.tls_serverName = _node.address + end + _node.address = "127.0.0.1" + _node.port = new_port + table.insert(rules, 1, { + type = "field", + inboundTag = {"proxy_" .. rule_name}, + outboundTag = is_balancing_proxy and nil or preproxy_tag, + balancerTag = is_balancing_proxy and get_balancer_tag(proxy_node_id) or nil + }) + end + end + local _outbound = gen_outbound(flag, _node, rule_name, { proxy = proxy and 1 or 0, tag = proxy and preproxy_tag or nil }) + if _outbound then + table.insert(outbounds, _outbound) + if proxy then preproxy_used = true end + rule_outboundTag = rule_name + end + end + elseif _node.protocol == "_balancing" then + local is_new_balancer = true + for _, v in ipairs(balancers) do + if v["_flag_tag"] == _node_id then + is_new_balancer = false + rule_balancerTag = v.tag + break + end + end + if is_new_balancer then + local balancer = gen_balancer(_node) + if balancer then + table.insert(balancers, balancer) + rule_balancerTag = balancer.tag + end + end + end + end + return rule_outboundTag, rule_balancerTag + end + --default_node + local default_node_id = node.default_node or "_direct" + local default_outboundTag, default_balancerTag = gen_shunt_node("default", default_node_id) + --shunt rule + uci:foreach(appname, "shunt_rules", function(e) + local outboundTag, balancerTag = gen_shunt_node(e[".name"]) + if outboundTag or balancerTag and e.remarks then + if outboundTag == "default" then + outboundTag = default_outboundTag + balancerTag = default_balancerTag + end + local protocols = nil + if e["protocol"] and e["protocol"] ~= "" then + protocols = {} + string.gsub(e["protocol"], '[^' .. " " .. ']+', function(w) + table.insert(protocols, w) + end) + end + local domains = nil + if e.domain_list then + domains = {} + string.gsub(e.domain_list, '[^' .. "\r\n" .. ']+', function(w) + table.insert(domains, w) + if outboundTag == "direct" then + table.insert(dns_direct_domains, w) + else + if outboundTag ~= "nil" then + table.insert(dns_remote_domains, w) + end + end + end) + end + local ip = nil + if e.ip_list then + ip = {} + string.gsub(e.ip_list, '[^' .. "\r\n" .. ']+', function(w) + table.insert(ip, w) + if outboundTag == "direct" then + table.insert(dns_direct_expectIPs, w) + else + if outboundTag ~= "nil" then + table.insert(dns_remote_expectIPs, w) + end + end + end) + end + local source = nil + if e.source then + source = {} + string.gsub(e.source, '[^' .. " " .. ']+', function(w) + table.insert(source, w) + end) + end + local rule = { + _flag = e.remarks, + type = "field", + outboundTag = outboundTag, + balancerTag = balancerTag, + network = e["network"] or "tcp,udp", + source = source, + sourcePort = e["sourcePort"] ~= "" and e["sourcePort"] or nil, + port = e["port"] ~= "" and e["port"] or nil, + protocol = protocols + } + if domains then + local _rule = api.clone(rule) + _rule["_flag"] = _rule["_flag"] .. "_domains" + _rule.domains = domains + table.insert(rules, _rule) + end + if ip then + local _rule = api.clone(rule) + _rule["_flag"] = _rule["_flag"] .. "_ip" + _rule.ip = ip + table.insert(rules, _rule) + end + if not domains and not ip then + table.insert(rules, rule) + end + end + end) + + if default_outboundTag or default_balancerTag then + table.insert(rules, { + _flag = "default", + type = "field", + outboundTag = default_outboundTag, + balancerTag = default_balancerTag, + network = "tcp,udp" + }) + end + + routing = { + domainStrategy = node.domainStrategy or "AsIs", + domainMatcher = node.domainMatcher or "hybrid", + balancers = #balancers > 0 and balancers or nil, + rules = rules + } + elseif node.protocol == "_balancing" then + if node.balancing_node then + local balancer = gen_balancer(node) + routing = { + balancers = { balancer }, + rules = { + { type = "field", network = "tcp,udp", balancerTag = balancer.tag } + } + } + end + else + local outbound = nil + if node.protocol == "_iface" then + if node.iface then + outbound = { + protocol = "freedom", + tag = "outbound", + streamSettings = { + sockopt = { + interface = node.iface + } + } + } + end + else + outbound = gen_outbound(flag, node) + end + if outbound then table.insert(outbounds, outbound) end + routing = { + domainStrategy = "AsIs", + domainMatcher = "hybrid", + rules = {} + } + table.insert(routing.rules, { + _flag = "default", + type = "field", + outboundTag = node_id, + network = "tcp,udp" + }) + end + + end + + if remote_dns_udp_server then + local rules = {} + local _remote_dns_proto + + if not routing then + routing = { + domainStrategy = "IPOnDemand", + rules = {} + } + end + + dns = { + tag = "dns-in1", + hosts = {}, + disableCache = (dns_cache and dns_cache == "0") and true or false, + disableFallback = true, + disableFallbackIfMatch = true, + servers = {}, + queryStrategy = (dns_query_strategy and dns_query_strategy ~= "") and dns_query_strategy or "UseIP" + } + + local dns_host = "" + if flag == "global" then + dns_host = uci:get(appname, "@global[0]", "dns_hosts") or "" + else + flag = flag:gsub("acl_", "") + local dns_hosts_mode = uci:get(appname, flag, "dns_hosts_mode") or "default" + if dns_hosts_mode == "default" then + dns_host = uci:get(appname, "@global[0]", "dns_hosts") or "" + elseif dns_hosts_mode == "disable" then + dns_host = "" + elseif dns_hosts_mode == "custom" then + dns_host = uci:get(appname, flag, "dns_hosts") or "" + end + end + if #dns_host > 0 then + string.gsub(dns_host, '[^' .. "\r\n" .. ']+', function(w) + local host = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $1}')", w)) + local key = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $2}')", w)) + if host ~= "" and key ~= "" then + dns.hosts[host] = key + end + end) + end + + if true then + if remote_dns_udp_server then + local _remote_dns = { + _flag = "remote", + address = remote_dns_udp_server, + port = tonumber(remote_dns_port) or 53 + } + if not remote_dns_fake then + _remote_dns.domains = #dns_remote_domains > 0 and dns_remote_domains or nil + --_remote_dns.expectIPs = #dns_remote_expectIPs > 0 and dns_remote_expectIPs or nil + end + _remote_dns_proto = "udp" + table.insert(dns.servers, _remote_dns) + + table.insert(routing.rules, 1, { + type = "field", + ip = { + remote_dns_udp_server + }, + port = tonumber(remote_dns_port) or 53, + network = "udp", + outboundTag = "direct" + }) + end + if remote_dns_fake then + fakedns = {} + local fakedns4 = { + ipPool = "198.18.0.0/16", + poolSize = 65535 + } + local fakedns6 = { + ipPool = "fc00::/18", + poolSize = 65535 + } + if remote_dns_fake_strategy == "UseIP" then + table.insert(fakedns, fakedns4) + table.insert(fakedns, fakedns6) + elseif remote_dns_fake_strategy == "UseIPv4" then + table.insert(fakedns, fakedns4) + elseif remote_dns_fake_strategy == "UseIPv6" then + table.insert(fakedns, fakedns6) + end + local _remote_dns = { + _flag = "remote_fakedns", + address = "fakedns", + domains = #dns_remote_domains > 0 and dns_remote_domains or nil + --expectIPs = #dns_remote_expectIPs > 0 and dns_remote_expectIPs or nil + } + table.insert(dns.servers, _remote_dns) + end + end + + if true then + local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u') + string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w) + table.insert(dns_direct_domains, "full:" .. w) + end) + + local _direct_dns = { + _flag = "direct", + domains = #dns_direct_domains > 0 and dns_direct_domains or nil + --expectIPs = #dns_direct_expectIPs > 0 and dns_direct_expectIPs or nil + } + + if direct_dns_udp_server then + _direct_dns.address = direct_dns_udp_server + _direct_dns.port = tonumber(direct_dns_port) or 53 + table.insert(routing.rules, 1, { + type = "field", + ip = { + direct_dns_udp_server + }, + port = tonumber(direct_dns_port) or 53, + network = "udp", + outboundTag = "direct" + }) + end + + table.insert(dns.servers, _direct_dns) + end + + if dns_listen_port then + table.insert(inbounds, { + listen = "127.0.0.1", + port = tonumber(dns_listen_port), + protocol = "dokodemo-door", + tag = "dns-in", + settings = { + address = "1.1.1.1", + port = 53, + network = "tcp,udp" + } + }) + local direct_type_dns = { + settings = { + address = direct_dns_udp_server, + port = tonumber(direct_dns_port) or 53, + network = "udp" + }, + proxySettings = { + tag = "direct" + } + } + local remote_type_dns = { + settings = { + address = remote_dns_udp_server, + port = tonumber(remote_dns_port) or 53, + network = _remote_dns_proto or "tcp" + }, + proxySettings = { + tag = "direct" + } + } + local custom_type_dns = { + settings = { + address = "1.1.1.1", + port = 53, + network = "tcp", + } + } + local type_dns = remote_type_dns + table.insert(outbounds, { + tag = "dns-out", + protocol = "dns", + proxySettings = type_dns.proxySettings, + settings = type_dns.settings + }) + table.insert(routing.rules, 1, { + type = "field", + inboundTag = { + "dns-in" + }, + outboundTag = "dns-out" + }) + end + + local default_dns_flag = "remote" + if node_id and redir_port then + local node = uci:get_all(appname, node_id) + if node.protocol == "_shunt" then + if node.default_node == "_direct" then + default_dns_flag = "direct" + end + end + end + + if dns.servers and #dns.servers > 0 then + local dns_servers = nil + for index, value in ipairs(dns.servers) do + if not dns_servers and value["_flag"] == default_dns_flag then + if value["_flag"] == "remote" and remote_dns_fake then + value["_flag"] = "default" + break + end + dns_servers = { + _flag = "default", + address = value.address, + port = value.port + } + break + end + end + if dns_servers then + table.insert(dns.servers, 1, dns_servers) + end + + for i = #dns.servers, 1, -1 do + local v = dns.servers[i] + if v["_flag"] ~= "default" then + if not v.domains or #v.domains == 0 then + table.remove(dns.servers, i) + end + end + end + end + + local default_rule_index = #routing.rules > 0 and #routing.rules or 1 + for index, value in ipairs(routing.rules) do + if value["_flag"] == "default" then + default_rule_index = index + break + end + end + for index, value in ipairs(rules) do + local t = rules[#rules + 1 - index] + table.insert(routing.rules, default_rule_index, t) + end + + local dns_hosts_len = 0 + for key, value in pairs(dns.hosts) do + dns_hosts_len = dns_hosts_len + 1 + end + + if dns_hosts_len == 0 then + dns.hosts = nil + end + end + + if inbounds or outbounds then + local config = { + log = { + --access = string.format("/tmp/etc/%s/%s_access.log", appname, "global"), + --error = string.format("/tmp/etc/%s/%s_error.log", appname, "global"), + --dnsLog = true, + loglevel = loglevel + }, + -- DNS + dns = dns, + fakedns = fakedns, + -- 传入连接 + inbounds = inbounds, + -- 传出连接 + outbounds = outbounds, + -- 连接观测 + observatory = observatory, + -- 路由 + routing = routing, + -- 本地策略 + policy = { + levels = { + [0] = { + -- handshake = 4, + -- connIdle = 300, + -- uplinkOnly = 2, + -- downlinkOnly = 5, + bufferSize = buffer_size and tonumber(buffer_size) or nil, + statsUserUplink = false, + statsUserDownlink = false + } + }, + -- system = { + -- statsInboundUplink = false, + -- statsInboundDownlink = false + -- } + } + } + table.insert(outbounds, { + protocol = "freedom", + tag = "direct", + settings = { + domainStrategy = (dns_query_strategy and dns_query_strategy ~= "") and dns_query_strategy or "UseIPv4" + }, + streamSettings = { + sockopt = { + mark = 255 + } + } + }) + table.insert(outbounds, { + protocol = "blackhole", + tag = "blackhole" + }) + return jsonc.stringify(config, 1) + end +end + +function gen_proto_config(var) + local local_socks_address = var["-local_socks_address"] or "0.0.0.0" + local local_socks_port = var["-local_socks_port"] + local local_socks_username = var["-local_socks_username"] + local local_socks_password = var["-local_socks_password"] + local local_http_address = var["-local_http_address"] or "0.0.0.0" + local local_http_port = var["-local_http_port"] + local local_http_username = var["-local_http_username"] + local local_http_password = var["-local_http_password"] + local server_proto = var["-server_proto"] + local server_address = var["-server_address"] + local server_port = var["-server_port"] + local server_username = var["-server_username"] + local server_password = var["-server_password"] + + local inbounds = {} + local outbounds = {} + local routing = nil + + if local_socks_address and local_socks_port then + local inbound = { + listen = local_socks_address, + port = tonumber(local_socks_port), + protocol = "socks", + settings = { + udp = true, + auth = "noauth" + } + } + if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then + inbound.settings.auth = "password" + inbound.settings.accounts = { + { + user = local_socks_username, + pass = local_socks_password + } + } + end + table.insert(inbounds, inbound) + end + + if local_http_address and local_http_port then + local inbound = { + listen = local_http_address, + port = tonumber(local_http_port), + protocol = "http", + settings = { + allowTransparent = false + } + } + if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then + inbound.settings.accounts = { + { + user = local_http_username, + pass = local_http_password + } + } + end + table.insert(inbounds, inbound) + end + + if server_proto ~= "nil" and server_address ~= "nil" and server_port ~= "nil" then + local outbound = { + protocol = server_proto, + streamSettings = { + network = "tcp", + security = "none" + }, + settings = { + servers = { + { + address = server_address, + port = tonumber(server_port), + users = (server_username and server_password) and { + { + user = server_username, + pass = server_password + } + } or nil + } + } + } + } + if outbound then table.insert(outbounds, outbound) end + end + + -- 额外传出连接 + table.insert(outbounds, { + protocol = "freedom", tag = "direct", settings = {keep = ""} + }) + + local config = { + log = { + loglevel = "warning" + }, + -- 传入连接 + inbounds = inbounds, + -- 传出连接 + outbounds = outbounds, + -- 路由 + routing = routing + } + return jsonc.stringify(config, 1) +end + +function gen_dns_config(var) + local dns_listen_port = var["-dns_listen_port"] + local dns_query_strategy = var["-dns_query_strategy"] + local dns_out_tag = var["-dns_out_tag"] + local dns_client_ip = var["-dns_client_ip"] + local direct_dns_server = var["-direct_dns_server"] + local direct_dns_port = var["-direct_dns_port"] + local direct_dns_udp_server = var["-direct_dns_udp_server"] + local direct_dns_tcp_server = var["-direct_dns_tcp_server"] + local direct_dns_doh_url = var["-direct_dns_doh_url"] + local direct_dns_doh_host = var["-direct_dns_doh_host"] + local remote_dns_server = var["-remote_dns_server"] + local remote_dns_port = var["-remote_dns_port"] + local remote_dns_udp_server = var["-remote_dns_udp_server"] + local remote_dns_tcp_server = var["-remote_dns_tcp_server"] + local remote_dns_doh_url = var["-remote_dns_doh_url"] + local remote_dns_doh_host = var["-remote_dns_doh_host"] + local remote_dns_outbound_socks_address = var["-remote_dns_outbound_socks_address"] + local remote_dns_outbound_socks_port = var["-remote_dns_outbound_socks_port"] + local remote_dns_fake = var["-remote_dns_fake"] + local dns_cache = var["-dns_cache"] + local loglevel = var["-loglevel"] or "warning" + + local inbounds = {} + local outbounds = {} + local dns = nil + local fakedns = nil + local routing = nil + + if dns_listen_port then + routing = { + domainStrategy = "IPOnDemand", + rules = {} + } + + dns = { + tag = "dns-in1", + hosts = {}, + disableCache = (dns_cache == "1") and false or true, + disableFallback = true, + disableFallbackIfMatch = true, + servers = {}, + clientIp = (dns_client_ip and dns_client_ip ~= "") and dns_client_ip or nil, + queryStrategy = (dns_query_strategy and dns_query_strategy ~= "") and dns_query_strategy or "UseIPv4" + } + + local other_type_dns_proto, other_type_dns_server, other_type_dns_port + + if dns_out_tag == "remote" then + local _remote_dns = { + _flag = "remote" + } + + if remote_dns_fake then + remote_dns_server = "1.1.1.1" + fakedns = {} + fakedns[#fakedns + 1] = { + ipPool = "198.18.0.0/16", + poolSize = 65535 + } + if dns_query_strategy == "UseIP" then + fakedns[#fakedns + 1] = { + ipPool = "fc00::/18", + poolSize = 65535 + } + end + _remote_dns.address = "fakedns" + end + + other_type_dns_port = tonumber(remote_dns_port) or 53 + other_type_dns_server = remote_dns_server + + if remote_dns_udp_server then + _remote_dns.address = remote_dns_udp_server + _remote_dns.port = tonumber(remote_dns_port) or 53 + other_type_dns_proto = "udp" + end + + if remote_dns_tcp_server then + _remote_dns.address = remote_dns_tcp_server + _remote_dns.port = tonumber(remote_dns_port) or 53 + other_type_dns_proto = "tcp" + end + + if remote_dns_doh_url and remote_dns_doh_host then + if remote_dns_server and remote_dns_doh_host ~= remote_dns_server and not api.is_ip(remote_dns_doh_host) then + dns.hosts[remote_dns_doh_host] = remote_dns_server + end + _remote_dns.address = remote_dns_doh_url + _remote_dns.port = tonumber(remote_dns_port) or 443 + other_type_dns_proto = "tcp" + other_type_dns_port = 53 + end + + table.insert(dns.servers, _remote_dns) + table.insert(outbounds, 1, { + tag = "remote", + protocol = "socks", + streamSettings = { + network = "tcp", + security = "none" + }, + settings = { + servers = { + { + address = remote_dns_outbound_socks_address, + port = tonumber(remote_dns_outbound_socks_port) + } + } + } + }) + elseif dns_out_tag == "direct" then + local _direct_dns = { + _flag = "direct" + } + + other_type_dns_proto = tonumber(direct_dns_port) or 53 + other_type_dns_server = direct_dns_server + + if direct_dns_udp_server then + _direct_dns.address = direct_dns_udp_server + _direct_dns.port = tonumber(direct_dns_port) or 53 + table.insert(routing.rules, 1, { + type = "field", + ip = { + direct_dns_udp_server + }, + port = tonumber(direct_dns_port) or 53, + network = "udp", + outboundTag = "direct" + }) + end + + if direct_dns_udp_server then + _direct_dns.address = direct_dns_udp_server + _direct_dns.port = tonumber(direct_dns_port) or 53 + other_type_dns_proto = "udp" + end + + if direct_dns_tcp_server then + _direct_dns.address = direct_dns_tcp_server:gsub("tcp://", "tcp+local://") + _direct_dns.port = tonumber(direct_dns_port) or 53 + other_type_dns_proto = "tcp" + end + + if direct_dns_doh_url and direct_dns_doh_host then + if direct_dns_server and direct_dns_doh_host ~= direct_dns_server and not api.is_ip(direct_dns_doh_host) then + dns.hosts[direct_dns_doh_host] = direct_dns_server + end + _direct_dns.address = direct_dns_doh_url:gsub("https://", "https+local://") + _direct_dns.port = tonumber(direct_dns_port) or 443 + other_type_dns_proto = "tcp" + other_type_dns_port = 53 + end + + table.insert(dns.servers, _direct_dns) + + table.insert(outbounds, 1, { + protocol = "freedom", + tag = "direct", + settings = { + domainStrategy = (dns_query_strategy and dns_query_strategy ~= "") and dns_query_strategy or "UseIPv4" + }, + streamSettings = { + sockopt = { + mark = 255 + } + } + }) + end + + local dns_hosts_len = 0 + for key, value in pairs(dns.hosts) do + dns_hosts_len = dns_hosts_len + 1 + end + + if dns_hosts_len == 0 then + dns.hosts = nil + end + + table.insert(inbounds, { + listen = "127.0.0.1", + port = tonumber(dns_listen_port), + protocol = "dokodemo-door", + tag = "dns-in", + settings = { + address = other_type_dns_server or "1.1.1.1", + port = 53, + network = "tcp,udp" + } + }) + + table.insert(outbounds, { + tag = "dns-out", + protocol = "dns", + proxySettings = { + tag = dns_out_tag + }, + settings = { + address = other_type_dns_server or "1.1.1.1", + port = other_type_dns_port or 53, + network = other_type_dns_proto or "tcp", + } + }) + + table.insert(routing.rules, 1, { + type = "field", + inboundTag = { + "dns-in" + }, + outboundTag = "dns-out" + }) + + table.insert(routing.rules, { + type = "field", + inboundTag = { + "dns-in1" + }, + outboundTag = dns_out_tag + }) + end + + if inbounds or outbounds then + local config = { + log = { + --dnsLog = true, + loglevel = loglevel + }, + -- DNS + dns = dns, + fakedns = fakedns, + -- 传入连接 + inbounds = inbounds, + -- 传出连接 + outbounds = outbounds, + -- 路由 + routing = routing + } + return jsonc.stringify(config, 1) + end + +end + +_G.gen_config = gen_config +_G.gen_proto_config = gen_proto_config +_G.gen_dns_config = gen_dns_config + +if arg[1] then + local func =_G[arg[1]] + if func then + print(func(api.get_function_args(arg))) + end +end diff --git a/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm b/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm new file mode 100644 index 000000000..3be00ab23 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm @@ -0,0 +1,192 @@ +<% +local api = require "luci.passwall2.api" +local com = require "luci.passwall2.com" +local version = {} +-%> + + + +<%for k, v in pairs(com) do + version[k] = api.get_app_version(k)%> +
+ +
+
+ 【 <%=version[k] ~="" and version[k] or translate("Null") %> 】 + + +
+
+
+<%end%> diff --git a/luci-app-passwall2/luasrc/view/passwall2/auto_switch/footer.htm b/luci-app-passwall2/luasrc/view/passwall2/auto_switch/footer.htm new file mode 100644 index 000000000..ab648d2f3 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/auto_switch/footer.htm @@ -0,0 +1,22 @@ +<% +local api = require "luci.passwall2.api" +-%> + + + + \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm b/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm new file mode 100644 index 000000000..da3e0290d --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm @@ -0,0 +1,41 @@ +<% +local api = require "luci.passwall2.api" +-%> +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/view/passwall2/global/footer.htm b/luci-app-passwall2/luasrc/view/passwall2/global/footer.htm new file mode 100644 index 000000000..dd9a4b2d6 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/global/footer.htm @@ -0,0 +1,127 @@ +<% +local api = require "luci.passwall2.api" +local auto_switch = api.uci_get_type("auto_switch", "enable", 0) +-%> + \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/view/passwall2/global/status.htm b/luci-app-passwall2/luasrc/view/passwall2/global/status.htm new file mode 100644 index 000000000..e6b6d61c6 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/global/status.htm @@ -0,0 +1,203 @@ +<% +local api = require "luci.passwall2.api" +-%> + + + +
+ + <%:Running Status%> + +
+
+
+
+
+ +
+
+
+

V2ray
<%:NOT RUNNING%>

+
+
+
+
+
+
+
+ +
+
+
+

<%:Baidu Connection%>
<%:Touch Check%>

+
+
+
+
+
+
+
+ +
+
+
+

<%:Google Connection%>
<%:Touch Check%>

+
+
+
+
+
+
+
+ +
+
+
+

<%:GitHub Connection%>
<%:Touch Check%>

+
+
+
+
+ +
diff --git a/luci-app-passwall2/luasrc/view/passwall2/haproxy/status.htm b/luci-app-passwall2/luasrc/view/passwall2/haproxy/status.htm new file mode 100644 index 000000000..12d1ad61f --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/haproxy/status.htm @@ -0,0 +1,26 @@ +<% +local api = require "luci.passwall2.api" +local console_port = api.uci_get_type("global_haproxy", "console_port", "") +-%> +

+ + diff --git a/luci-app-passwall2/luasrc/view/passwall2/log/log.htm b/luci-app-passwall2/luasrc/view/passwall2/log/log.htm new file mode 100644 index 000000000..0df1e39ce --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/log/log.htm @@ -0,0 +1,30 @@ +<% +local api = require "luci.passwall2.api" +-%> + +
+ + +
diff --git a/luci-app-passwall2/luasrc/view/passwall2/node_list/link_add_node.htm b/luci-app-passwall2/luasrc/view/passwall2/node_list/link_add_node.htm new file mode 100644 index 000000000..7d1ec1d5f --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/node_list/link_add_node.htm @@ -0,0 +1,108 @@ +<% +local api = require "luci.passwall2.api" +-%> + + + + + + + +
+ +
+ + + + + + +
+
+
\ No newline at end of file diff --git a/luci-app-passwall2/luasrc/view/passwall2/node_list/link_share_man.htm b/luci-app-passwall2/luasrc/view/passwall2/node_list/link_share_man.htm new file mode 100644 index 000000000..18e93bfbf --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/node_list/link_share_man.htm @@ -0,0 +1,851 @@ +<%+cbi/valueheader%> +<% +local api = require "luci.passwall2.api" +local has_v2ray = api.is_finded("v2ray") +local has_xray = api.is_finded("xray") +-%> + + + + +<%+cbi/valuefooter%> diff --git a/luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm b/luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm new file mode 100644 index 000000000..549616d99 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm @@ -0,0 +1,499 @@ +<% +local api = require "luci.passwall2.api" +local uci = api.uci + +local default_node_type = "" +local shunt_rule_list = {} +local node = api.uci_get_type("global", "node", "nil") +if node ~= "nil" then + local node_type = api.uci_get_type_id(node, "type") + local node_protocol = api.uci_get_type_id(node, "protocol") + if (node_type == "V2ray" or node_type == "Xray") and node_protocol == "_shunt" then + default_node_type = node_protocol + uci:foreach("passwall2", "shunt_rules", function(e) + if e[".name"] and e.remarks then + shunt_rule_list[#shunt_rule_list + 1] = e + end + end) + end +end +-%> + + + + + +
+
+
<%:You choose node is:%>
+
+ <%- if default_node_type == "_shunt" then + for i, v in ipairs(shunt_rule_list) do + -%> + ')" value="<%=v.remarks%>" /> + <%- + end + -%> + <% else %> + + <% end %> + +
+
+
\ No newline at end of file diff --git a/luci-app-passwall2/luasrc/view/passwall2/rule/rule_version.htm b/luci-app-passwall2/luasrc/view/passwall2/rule/rule_version.htm new file mode 100644 index 000000000..0cb5528fa --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/rule/rule_version.htm @@ -0,0 +1,56 @@ +<% +local api = require "luci.passwall2.api" + +local geoip_update = api.uci_get_type("global_rules", "geoip_update", "1") == "1" and "checked='checked'" or "" +local geosite_update = api.uci_get_type("global_rules", "geosite_update", "1") == "1" and "checked='checked'" or "" +-%> + + +
+ +
+
+ + + +
+
+
diff --git a/luci-app-passwall2/luasrc/view/passwall2/server/log.htm b/luci-app-passwall2/luasrc/view/passwall2/server/log.htm new file mode 100644 index 000000000..4bb5c2dad --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/server/log.htm @@ -0,0 +1,34 @@ +<% +local api = require "luci.passwall2.api" +-%> + +
+ + <%:Logs%> + + + +
\ No newline at end of file diff --git a/luci-app-passwall2/luasrc/view/passwall2/server/users_list_status.htm b/luci-app-passwall2/luasrc/view/passwall2/server/users_list_status.htm new file mode 100644 index 000000000..d9473651c --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/server/users_list_status.htm @@ -0,0 +1,38 @@ +<% +local api = require "luci.passwall2.api" +-%> + \ No newline at end of file diff --git a/luci-app-passwall2/po/zh-cn/passwall2.po b/luci-app-passwall2/po/zh-cn/passwall2.po new file mode 100644 index 000000000..d7ff4d0bf --- /dev/null +++ b/luci-app-passwall2/po/zh-cn/passwall2.po @@ -0,0 +1,1316 @@ +msgid "PassWall 2" +msgstr "PassWall 2" + +msgid "Auto" +msgstr "自动" + +msgid "RUNNING" +msgstr "运行中" + +msgid "NOT RUNNING" +msgstr "未运行" + +msgid "Working..." +msgstr "连接正常" + +msgid "Problem detected!" +msgstr "连接失败" + +msgid "Touch Check" +msgstr "点我检测" + +msgid "Kernel Unsupported" +msgstr "内核不支持" + +msgid "Basic Settings" +msgstr "基本设置" + +msgid "Node List" +msgstr "节点列表" + +msgid "Other Settings" +msgstr "高级设置" + +msgid "Load Balancing" +msgstr "负载均衡" + +msgid "Enter interface" +msgstr "进入界面" + +msgid "Rule Manage" +msgstr "规则管理" + +msgid "Rule List" +msgstr "规则列表" + +msgid "Access control" +msgstr "访问控制" + +msgid "Watch Logs" +msgstr "查看日志" + +msgid "Node Config" +msgstr "节点配置" + +msgid "Running Status" +msgstr "运行状态" + +msgid "Baidu Connection" +msgstr "百度连接" + +msgid "Google Connection" +msgstr "谷歌连接" + +msgid "GitHub Connection" +msgstr "GitHub连接" + +msgid "Instagram Connection" +msgstr "Instagram连接" + +msgid "Node Check" +msgstr "节点检测" + +msgid "Check..." +msgstr "检测中..." + +msgid "Clear" +msgstr "清除" + +msgid "Main switch" +msgstr "主开关" + +msgid "Node" +msgstr "节点" + +msgid "Edit Current Node" +msgstr "编辑当前节点" + +msgid "Localhost Proxy" +msgstr "路由器本机代理" + +msgid "When selected, localhost can transparent proxy." +msgstr "当勾选时,路由器本机可以透明代理。" + +msgid "Socks Config" +msgstr "Socks配置" + +msgid "Socks Node" +msgstr "Socks节点" + +msgid "Listen Port" +msgstr "监听端口" + +msgid "0 is not use" +msgstr "0为不使用" + +msgid "Current node: %s" +msgstr "当前节点:%s" + +msgid "IP:Port mode acceptable, multi value split with english comma." +msgstr "接受 IP:Port 形式的输入,多个以英文逗号分隔。" + +msgid "Direct DNS Protocol" +msgstr "直连 DNS 协议" + +msgid "Direct DNS" +msgstr "直连 DNS" + +msgid "Direct DNS DoH" +msgstr "直连 DNS DoH" + +msgid "Direct DNS EDNS Client Subnet" +msgstr "直连 DNS EDNS Client Subnet" + +msgid "Remote DNS Protocol" +msgstr "远程 DNS 协议" + +msgid "Remote DNS" +msgstr "远程 DNS" + +msgid "Remote DNS DoH" +msgstr "远程 DNS DoH" + +msgid "Remote DNS EDNS Client Subnet" +msgstr "远程 DNS EDNS Client Subnet" + +msgid "Notify the DNS server when the DNS query is notified, the location of the client (cannot be a private IP address)." +msgstr "用于 DNS 查询时通知 DNS 服务器,客户端所在的地理位置(不能是私有 IP 地址)。" + +msgid "This feature requires the DNS server to support the Edns Client Subnet (RFC7871)." +msgstr "此功能需要 DNS 服务器支持 EDNS Client Subnet(RFC7871)。" + +msgid "Direct Query Strategy" +msgstr "直连查询策略" + +msgid "Remote Query Strategy" +msgstr "远程查询策略" + +msgid "Use FakeDNS work in the shunt domain that proxy." +msgstr "需要代理的分流规则域名使用 FakeDNS。" + +msgid "Domain Override" +msgstr "域名重写" + +msgid "Clear IPSET" +msgstr "清空 IPSET" + +msgid "Try this feature if the rule modification does not take effect." +msgstr "如果修改规则后没有生效,请尝试此功能。" + +msgid "About DNS issues:" +msgstr "关于DNS问题:" + +msgid "Some browsers may have built-in DNS, be sure to close. Example: Chrome. Settings - Security and Privacy - Security - Use secure DNS disabled." +msgstr "部分浏览器可能有内置的DNS,请务必关闭。如:chrome。 设置 - 安全和隐私设置 - 使用安全 DNS 关闭。" + +msgid "Sometimes after restart, you can not internet. At this time, close all browsers (important), Windows Client, please `ipconfig /flushdns`. Please close the WiFi on the phone, cut the flight mode and then cut back." +msgstr "有时候重启后,上不了。这时请先关闭所有浏览器(重要),Windows客户端请`ipconfig /flushdns`。手机端请关闭WIFI,切一下飞行模式再切回来。" + +msgid "The client DNS and the default gateway must point to this router." +msgstr "客户端DNS和默认网关必须指向本路由器。" + +msgid "If you have a wrong DNS process, the consequences are at your own risk!" +msgstr "如果你自行配置了错误的DNS流程,后果自负!" + +msgid "Restore the default configuration method. Input example in the address bar:" +msgstr "恢复默认配置方法,地址栏输入例:" + +msgid "Hide menu method, input example in the address bar:" +msgstr "隐藏菜单方法,地址栏输入例:" + +msgid "After the hidden to the display, input example in the address bar:" +msgstr "当你隐藏后想再次显示,地址栏输入例:" + +msgid "Are you sure to reset?" +msgstr "你确定要恢复吗?" + +msgid "Are you sure to hide?" +msgstr "你确定要隐藏吗?" + +msgid "DNS Export Of Multi WAN" +msgstr "国内DNS指定解析出口" + +msgid "Node Export Of Multi WAN" +msgstr "节点指定出口" + +msgid "Only support Multi Wan." +msgstr "只有多线接入才有效。" + +msgid "Not Specify" +msgstr "不指定" + +msgid "custom" +msgstr "自定义" + +msgid "If not available, try clearing the cache." +msgstr "如果无法使用,请尝试清除缓存。" + +msgid "Operation" +msgstr "操作" + +msgid "Add Node" +msgstr "添加节点" + +msgid "Add the node via the link" +msgstr "通过链接添加节点" + +msgid "SS/SSR/Vmess/VLESS/Trojan/Hysteria Link" +msgstr "SS/SSR/Vmess/VLESS/Trojan/Hysteria 链接" + +msgid "Please enter the correct link." +msgstr "请输入正确的链接。" + +msgid "Clear all nodes" +msgstr "清空所有节点" + +msgid "Are you sure to clear all nodes?" +msgstr "你确定要清空所有节点吗?" + +msgid "Error" +msgstr "错误" + +msgid "Delete select nodes" +msgstr "删除选择的节点" + +msgid "To Top" +msgstr "置顶" + +msgid "Select" +msgstr "选择" + +msgid "DeSelect" +msgstr "反选" + +msgid "Select all" +msgstr "全选" + +msgid "DeSelect all" +msgstr "全不选" + +msgid "Are you sure to delete select nodes?" +msgstr "你确定要删除选择的节点吗?" + +msgid "You no select nodes !" +msgstr "你没有选择任何节点!" + +msgid "Are you sure set to" +msgstr "你确定要设为" + +msgid "the server?" +msgstr "服务器吗?" + +msgid "You choose node is:" +msgstr "你选择的节点是:" + +msgid "Timeout" +msgstr "超时" + +msgid "Node Remarks" +msgstr "节点备注" + +msgid "Add Mode" +msgstr "添加方式" + +msgid "Type" +msgstr "类型" + +msgid "Balancing" +msgstr "负载均衡" + +msgid "Xray_balancing" +msgstr "Xray 负载均衡" + +msgid "V2ray_balancing" +msgstr "V2ray 负载均衡" + +msgid "Balancing Strategy" +msgstr "负载均衡策略" + +msgid "Use Custome Probe URL" +msgstr "使用自定义探测网址" + +msgid "By default the built-in probe URL will be used, enable this option to use a custom probe URL." +msgstr "默认使用内置的探测网址,启用此选项以使用自定义探测网址。" + +msgid "Probe URL" +msgstr "探测网址" + +msgid "The URL used to detect the connection status." +msgstr "用于检测连接状态的网址。" + +msgid "Probe Interval" +msgstr "探测间隔" + +msgid "The interval between initiating probes. Every time this time elapses, a server status check is performed on a server. The time format is numbers + units, such as '10s', '2h45m', and the supported time units are ns, us, ms, s, m, h, which correspond to nanoseconds, microseconds, milliseconds, seconds, minutes, and hours, respectively." +msgstr "发起探测的间隔。每经过这个时间,就会对一个服务器进行服务器状态检测。时间格式为数字+单位,比如"10s", "2h45m",支持的时间单位有 nsusmssmh,分别对应纳秒、微秒、毫秒、秒、分、时。" + +msgid "Shunt" +msgstr "分流" + +msgid "Xray_shunt" +msgstr "Xray 分流" + +msgid "V2ray_shunt" +msgstr "V2ray 分流" + +msgid "Preproxy" +msgstr "前置代理" + +msgid "Preproxy Node" +msgstr "前置代理节点" + +msgid "Set the node to be used as a pre-proxy. Each rule (including Default) has a separate switch that controls whether this rule uses the pre-proxy or not." +msgstr "设置用作前置代理的节点。每条规则(包括默认)都有独立开关控制本规则是否使用前置代理。" + +msgid "Direct Connection" +msgstr "直连" + +msgid "Blackhole" +msgstr "黑洞" + +msgid "Default Preproxy" +msgstr "默认前置代理" + +msgid "There are no available nodes, please add or subscribe nodes first." +msgstr "没有可用节点,请先添加或订阅节点。" + +msgid "No shunt rules? Click me to go to add." +msgstr "没有分流规则?点我前往去添加。" + +msgid "When using, localhost will connect this node first and then use this node to connect the default node." +msgstr "当使用时,本机将首先连接到此节点,然后再使用此节点连接到默认节点落地。" + +msgid "Domain Strategy" +msgstr "域名解析策略" + +msgid "Domain matcher" +msgstr "域名匹配算法" + +msgid "'AsIs': Only use domain for routing. Default value." +msgstr "AsIs:只使用域名进行路由选择。默认值。" + +msgid "'IPIfNonMatch': When no rule matches current domain, resolves it into IP addresses (A or AAAA records) and try all rules again." +msgstr "IPIfNonMatch:当域名没有匹配任何规则时,将域名解析成 IP(A 记录或 AAAA 记录)再次进行匹配。" + +msgid "'IPOnDemand': As long as there is a IP-based rule, resolves the domain into IP immediately." +msgstr "IPOnDemand:当匹配时碰到任何基于 IP 的规则,将域名立即解析为 IP 进行匹配。" + +msgid "Load balancing node list" +msgstr "负载均衡节点列表" + +msgid "Load balancing node list, document" +msgstr "负载均衡节点列表,文档原理" + +msgid "From Share URL" +msgstr "导入分享URL" + +msgid "Build Share URL" +msgstr "导出分享URL" + +msgid "Import Finished" +msgstr "导入完成:" + +msgid "Not a supported scheme:" +msgstr "不支持这种样式的:" + +msgid "Invalid Share URL Format" +msgstr "无效的分享URL信息" + +msgid "Paste Share URL Here" +msgstr "在此处粘贴分享信息" + +msgid "Share URL to clipboard unable." +msgstr "无法分享URL到剪贴板。" + +msgid "Share URL to clipboard successfully." +msgstr "成功复制分享URL到剪贴板。" + +msgid "Faltal on get option, please help in debug:" +msgstr "代码错误,请协助捉虫:" + +msgid "Faltal on set option, please help in debug:" +msgstr "代码错误,请协助捉虫:" + +msgid "Address" +msgstr "地址" + +msgid "Address (Support Domain Name)" +msgstr "地址(支持域名)" + +msgid "Trojan Verify Cert" +msgstr "验证证书" + +msgid "Trojan Cert Path" +msgstr "证书路径" + +msgid "Finger Print" +msgstr "指纹伪造" + +msgid "Avoid using randomized, unless you have to." +msgstr "避免使用 randomized , 除非你必须要。" + +msgid "Original" +msgstr "原版" + +msgid "Transport Plugin" +msgstr "传输层插件" + +msgid "Shadowsocks secondary encryption" +msgstr "Shadowsocks 二次加密" + +msgid "Obfs Password" +msgstr "混淆密码" + +msgid "Auth Type" +msgstr "认证类型" + +msgid "Auth Password" +msgstr "认证密码" + +msgid "Max upload Mbps" +msgstr "最大上行(Mbps)" + +msgid "Max download Mbps" +msgstr "最大下行(Mbps)" + +msgid "QUIC stream receive window" +msgstr "QUIC 流接收窗口" + +msgid "QUIC connection receive window" +msgstr "QUIC 连接接收窗口" + +msgid "Disable MTU detection" +msgstr "禁用 MTU 检测" + +msgid "Lazy Start" +msgstr "延迟启动" + +msgid "Encrypt Method" +msgstr "加密" + +msgid "Latency" +msgstr "延迟" + +msgid "Show Add Mode" +msgstr "显示添加方式" + +msgid "Show Group" +msgstr "显示组" + +msgid "Group" +msgstr "组" + +msgid "Auto Ping" +msgstr "自动Ping" + +msgid "Show server address and port" +msgstr "显示服务器地址和端口" + +msgid "Availability test" +msgstr "可用性测试" + +msgid "Node num" +msgstr "节点数量" + +msgid "Self add" +msgstr "自添" + +msgid "Apply" +msgstr "应用" + +msgid "Use" +msgstr "使用" + +msgid "Copy" +msgstr "复制" + +msgid "Delay Settings" +msgstr "定时配置" + +msgid "Open and close Daemon" +msgstr "启动守护进程" + +msgid "Delay Start" +msgstr "开机时延时启动" + +msgid "Units:seconds" +msgstr "单位:秒" + +msgid "Units:minutes" +msgstr "单位:分钟" + +msgid "Open and close automatically" +msgstr "定时自动开关" + +msgid "Automatically turn off time" +msgstr "自动关闭时间" + +msgid "Automatically turn on time" +msgstr "自动开启时间" + +msgid "Automatically restart time" +msgstr "自动重启时间" + +msgid "Forwarding Settings" +msgstr "转发配置" + +msgid "TCP No Redir Ports" +msgstr "TCP不转发端口" + +msgid "UDP No Redir Ports" +msgstr "UDP不转发端口" + +msgid "Fill in the ports you don't want to be forwarded by the agent, with the highest priority." +msgstr "填写你不希望被代理转发的端口,优先级最高。" + +msgid "TCP Proxy Drop Ports" +msgstr "TCP转发屏蔽端口" + +msgid "UDP Proxy Drop Ports" +msgstr "UDP转发屏蔽端口" + +msgid "TCP Redir Ports" +msgstr "TCP转发端口" + +msgid "UDP Redir Ports" +msgstr "UDP转发端口" + +msgid "No patterns are used" +msgstr "不使用" + +msgid "All" +msgstr "所有" + +msgid "Common Use" +msgstr "常用的" + +msgid "Only Web" +msgstr "仅网页" + +msgid "or more" +msgstr "及以上" + +msgid "or less" +msgstr "及以下" + +msgid "Default" +msgstr "默认" + +msgid "Close" +msgstr "关闭" + +msgid "Hijacking ICMP (PING)" +msgstr "劫持ICMP (PING)" + +msgid "Hijacking ICMPv6 (IPv6 PING)" +msgstr "劫持ICMPv6 (IPv6 PING)" + +msgid "Sniffing (V2Ray/Xray)" +msgstr "流量嗅探 (V2ray/Xray)" + +msgid "When using the V2ray/Xray shunt, must be enabled, otherwise the shunt will invalid." +msgstr "使用 V2Ray/Xray 分流时,必须启用,否则分流将无效。" + +msgid "Sniffing Route Only (Xray)" +msgstr "流量嗅探只供路由使用 (Xray)" + +msgid "When enabled, the server not will resolve the domain name again." +msgstr "启用后,服务器不会再次解析域名。" + +msgid "TCP Proxy Way" +msgstr "TCP代理方式" + +msgid "Auto Switch" +msgstr "自动切换" + +msgid "When there is no server, an automatic reconnect scheme is used" +msgstr "当没有服务器时,则使用自动重连方案" + +msgid "How often to test" +msgstr "多久检测一次" + +msgid "Timeout seconds" +msgstr "超时秒数" + +msgid "Timeout retry num" +msgstr "超时重试次数" + +msgid "Automatic switching cannot be used when this option is checked" +msgstr "当勾选此选项时,不能使用自动切换" + +msgid "Main node" +msgstr "主节点" + +msgid "List of backup nodes" +msgstr "备用节点的列表" + +msgid "Restore Switch" +msgstr "恢复切换" + +msgid "When detects main node is available, switch back to the main node." +msgstr "当检测到主节点可用时,切换回主节点。" + +msgid "If the main node is shunt" +msgstr "如果主节点是分流" + +msgid "Switch it" +msgstr "切掉它" + +msgid "Applying to the default node" +msgstr "应用于默认节点" + +msgid "Applying to the default preproxy node" +msgstr "应用于默认前置节点" + +msgid "Add nodes to the standby node list by keywords" +msgstr "通过关键字添加节点到备用节点列表" + +msgid "Delete nodes in the standby node list by keywords" +msgstr "通过关键字删除备用节点列表的节点" + +msgid "Please enter the node keyword, pay attention to distinguish between spaces, uppercase and lowercase." +msgstr "请输入节点关键字,注意区分空格、大写和小写。" + +msgid "Enable Load Balancing" +msgstr "开启负载均衡" + +msgid "Console Username" +msgstr "控制台账号" + +msgid "Console Password" +msgstr "控制台密码" + +msgid "Console Port" +msgstr "控制台端口" + +msgid "In the browser input routing IP plus port access, such as:192.168.1.1:1188" +msgstr "在浏览器输入路由IP加端口访问,如:192.168.1.1:1188" + +msgid "Haproxy Port" +msgstr "负载均衡端口" + +msgid "Health Check Type" +msgstr "健康检查类型" + +msgid "Inner implement" +msgstr "内置实现" + +msgid "Health Check Inter" +msgstr "健康检查节点间隔时间" + +msgid "When the availability test is used, the load balancing node will be converted into a Socks node. when node list set customizing, must be a Socks node, otherwise the health check will be invalid." +msgstr "当使用可用性测试时,负载均衡节点将转换成Socks节点。下面的节点列表自定义时必须为Socks节点,否则健康检查将无效。" + +msgid "Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group." +msgstr "添加节点,指定出口功能是为多WAN用户准备的。负载比重范围1-256。多个主服务器可以负载均衡,备用只有在主服务器离线时才会启用!可以设置多个组,负载均衡端口相同则为一组。" + +msgid "Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!" +msgstr "注意,当使用TCP健康检查时负载均衡的节点配置参数必须一致,否则无法正常使用!" + +msgid "Node Address" +msgstr "节点地址" + +msgid "Node Port" +msgstr "节点端口" + +msgid "Node Weight" +msgstr "负载比重" + +msgid "Export Of Multi WAN" +msgstr "多WAN指定出口" + +msgid "Main" +msgstr "主要" + +msgid "Mode" +msgstr "模式" + +msgid "Primary" +msgstr "主要" + +msgid "Standby" +msgstr "备用" + +msgid "Check update" +msgstr "检查更新" + +msgid "Enable custom URL" +msgstr "启用自定义规则地址" + +msgid "Rule status" +msgstr "规则版本" + +msgid "Enable auto update rules" +msgstr "开启自动更新规则" + +msgid "Week update rules" +msgstr "更新时间星期" + +msgid "Day update rules" +msgstr "更新时间小时" + +msgid "Every day" +msgstr "每天" + +msgid "day" +msgstr "日" + +msgid "Week" +msgstr "周" + +msgid "oclock" +msgstr "点" + +msgid "Location of V2ray/Xray asset" +msgstr "V2ray/Xray 资源文件目录" + +msgid "This variable specifies a directory where geoip.dat and geosite.dat files are." +msgstr "此变量指定geoip.dat和geosite.dat文件所在的目录。" + +msgid "Shunt Rule" +msgstr "分流规则" + +msgid "Please note attention to the priority, the higher the order, the higher the priority." +msgstr "请注意优先级问题,排序越上面优先级越高。" + +msgid "Update..." +msgstr "更新中" + +msgid "It is the latest version" +msgstr "已是最新版本" + +msgid "Update successful" +msgstr "更新成功" + +msgid "Click to update" +msgstr "点击更新" + +msgid "Updating..." +msgstr "更新中" + +msgid "Unexpected error" +msgstr "意外错误" + +msgid "Updating, are you sure to close?" +msgstr "正在更新,你确认要关闭吗?" + +msgid "Downloading..." +msgstr "下载中" + +msgid "Unpacking..." +msgstr "解压中" + +msgid "Moving..." +msgstr "移动中" + +msgid "App Update" +msgstr "组件更新" + +msgid "Please confirm that your firmware supports FPU." +msgstr "请确认你的固件支持FPU。" + +msgid "if you want to run from memory, change the path, /tmp beginning then save the application and update it manually." +msgstr "如果你希望从内存中运行,请更改路径,/tmp 开头,然后保存应用后,再手动更新。" + +msgid "Make sure there is enough space to install %s" +msgstr "确保有足够的空间安装 %s" + +msgid "App Path" +msgstr "程序路径" + +msgid "%s App Path" +msgstr "%s 程序路径" + +msgid "%s Client App Path" +msgstr "%s 客户端程序路径" + +msgid "Trojan-Go Version API" +msgstr "Trojan-Go 版本 API" + +msgid "alternate API URL for version checking" +msgstr "用于版本检查的 API URL" + +msgid "Node Subscribe" +msgstr "节点订阅" + +msgid "Subscribe Remark" +msgstr "订阅备注(机场)" + +msgid "Subscribe URL" +msgstr "订阅网址" + +msgid "Please input the subscription url first, save and submit before manual subscription." +msgstr "请输入订阅网址保存应用后再手动订阅。" + +msgid "Subscribe via proxy" +msgstr "通过代理订阅" + +msgid "Enable auto update subscribe" +msgstr "开启自动更新订阅" + +msgid "Manual subscription" +msgstr "手动订阅" + +msgid "Delete All Subscribe Node" +msgstr "删除所有订阅节点" + +msgid "Delete the subscribed node" +msgstr "删除已订阅的节点" + +msgid "Manual subscription All" +msgstr "手动订阅全部" + +msgid "This remark already exists, please change a new remark." +msgstr "此备注已存在,请改一个新的备注。" + +msgid "Filter keyword Mode" +msgstr "过滤关键字模式" + +msgid "Discard List" +msgstr "丢弃列表" + +msgid "Keep List" +msgstr "保留列表" + +msgid "Discard List,But Keep List First" +msgstr "丢弃列表,但保留列表优先" + +msgid "Keep List,But Discard List First" +msgstr "保留列表,但丢弃列表优先" + +msgid "Use global config" +msgstr "使用全局配置" + +msgid "User-Agent" +msgstr "用户代理(User-Agent)" + +msgid "Add" +msgstr "添加" + +msgid "ACLs" +msgstr "访问控制" + +msgid "ACLs is a tools which used to designate specific IP proxy mode." +msgstr "访问控制列表是用于指定特殊IP代理模式的工具。" + +msgid "Example:" +msgstr "例:" + +msgid "IP range" +msgstr "IP 范围" + +msgid "Remarks" +msgstr "备注" + +msgid "Direct List" +msgstr "直连列表" + +msgid "Proxy List" +msgstr "代理列表" + +msgid "Block List" +msgstr "屏蔽列表" + +msgid "Lan IP List" +msgstr "局域网IP列表" + +msgid "Route Hosts" +msgstr "路由Hosts文件" + +msgid "Join the direct hosts list of domain names will not proxy." +msgstr "加入的域名不走代理,对所有模式有效。且优先级最高。" + +msgid "These had been joined ip addresses will not proxy. Please input the ip address or ip address segment,every line can input only one ip address. For example: 192.168.0.0/24 or 223.5.5.5." +msgstr "加入的IP段不走代理,对所有模式有效。且优先级最高。可输入IP地址或地址段,如:192.168.0.0/24或223.5.5.5,每个地址段一行。" + +msgid "These had been joined websites will use proxy. Please input the domain names of websites, every line can input only one website domain. For example: google.com." +msgstr "加入的域名将走代理。输入网站域名,如:google.com,每个地址段一行。" + +msgid "These had been joined ip addresses will use proxy. Please input the ip address or ip address segment, every line can input only one ip address. For example: 35.24.0.0/24 or 8.8.4.4." +msgstr "加入的IP段将走代理。可输入IP地址或地址段,如:35.24.0.0/24或8.8.4.4,每个地址段一行。" + +msgid "These had been joined websites will be block. Please input the domain names of websites, every line can input only one website domain. For example: twitter.com." +msgstr "加入的域名将屏蔽。输入网站域名,如:twitter.com,每个地址段一行。" + +msgid "The list is the IPv4 LAN IP list, which represents the direct connection IP of the LAN. If you need the LAN IP in the proxy list, please clear it from the list. Do not modify this list by default." +msgstr "列表中为IPv4的局域网IP列表,代表局域网直连IP。如果需要代理列表中的局域网IP,请将其在该列表中清除,并将其添加到代理列表中。默认情况下不要修改这个列表。" + +msgid "The list is the IPv6 LAN IP list, which represents the direct connection IP of the LAN. If you need the LAN IP in the proxy list, please clear it from the list. Do not modify this list by default." +msgstr "列表中为IPv6的局域网IP列表,代表局域网直连IP。如果需要代理列表中的局域网IP,请将其在该列表中清除,并将其添加到代理列表中。默认情况下不要修改这个列表。" + +msgid "Configure routing etc/hosts file, if you don't know what you are doing, please don't change the content." +msgstr "配置路由etc/hosts文件,如果你不知道自己在做什么,请不要改动内容。" + +msgid "These had been joined ip addresses will be block. Please input the ip address or ip address segment, every line can input only one ip address." +msgstr "加入的IP段将屏蔽。可输入IP地址或地址段,每个地址段一行。" + +msgid "Not valid domain name, please re-enter!" +msgstr "不是有效域名,请重新输入!" + +msgid "Not valid IP format, please re-enter!" +msgstr "不是有效IP格式,请重新输入!" + +msgid "Not valid IPv4 format, please re-enter!" +msgstr "不是有效IPv4格式,请重新输入!" + +msgid "Not valid IPv6 format, please re-enter!" +msgstr "不是有效IPv6格式,请重新输入!" + +msgid "Not true format, please re-enter!" +msgstr "不是正确的格式,请重新输入!" + +msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'." +msgstr "纯字符串: 当此字符串匹配目标域名中任意部分,该规则生效。比如'sina.com'可以匹配'sina.com'、'sina.com.cn'和'www.sina.com',但不匹配'sina.cn'。" + +msgid "Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'." +msgstr "正则表达式: 由'regexp:'开始,余下部分是一个正则表达式。当此正则表达式匹配目标域名时,该规则生效。例如'regexp:\\.goo.*\\.com$'匹配'www.google.com'、'fonts.googleapis.com',但不匹配'google.com'。" + +msgid "Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'." +msgstr "子域名 (推荐): 由'domain:'开始,余下部分是一个域名。当此域名是目标域名或其子域名时,该规则生效。例如'domain:v2ray.com'匹配'www.v2ray.com'、'v2ray.com',但不匹配'xv2ray.com'。" + +msgid "Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'." +msgstr "完整匹配: 由'full:'开始,余下部分是一个域名。当此域名完整匹配目标域名时,该规则生效。例如'full:v2ray.com'匹配'v2ray.com'但不匹配'www.v2ray.com'。" + +msgid "Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn." +msgstr "预定义域名列表:由'geosite:'开头,余下部分是一个名称,如geosite:google或者geosite:cn。" + +msgid "Domains from file: Such as 'ext:file:tag'. The value must begin with ext: (lowercase), and followed by filename and tag. The file is placed in resource directory, and has the same format of geosite.dat. The tag must exist in the file." +msgstr "从文件中加载域名: 形如'ext:file:tag',必须以ext:(小写)开头,后面跟文件名和标签,文件存放在资源目录中,文件格式与geosite.dat相同,标签必须在文件中存在。" + +msgid "IP: such as '127.0.0.1'." +msgstr "IP: 形如'127.0.0.1'。" + +msgid "CIDR: such as '127.0.0.0/8'." +msgstr "CIDR: 形如'10.0.0.0/8'." + +msgid "GeoIP: such as 'geoip:cn'. It begins with geoip: (lower case) and followed by two letter of country code." +msgstr "GeoIP: 形如'geoip:cn',必须以geoip:(小写)开头,后面跟双字符国家代码,支持几乎所有可以上网的国家。" + +msgid "IPs from file: Such as 'ext:file:tag'. The value must begin with ext: (lowercase), and followed by filename and tag. The file is placed in resource directory, and has the same format of geoip.dat. The tag must exist in the file." +msgstr "从文件中加载 IP: 形如'ext:file:tag',必须以ext:(小写)开头,后面跟文件名和标签,文件存放在资源目录中,文件格式与geoip.dat相同标签必须在文件中存在。" + +msgid "Clear logs" +msgstr "清空日志" + +msgid "Only recommend to use with VLESS-TCP-XTLS-Vision." +msgstr "只推荐与 VLESS-TCP-XTLS-Vision 搭配使用。" + +msgid "Password" +msgstr "密码" + +msgid "IV Check" +msgstr "IV 检查" + +msgid "UDP over TCP" +msgstr "TCP 封装 UDP" + +msgid "Need Xray-core or sing-box as server side." +msgstr "需要 Xray-core 或者 sing-box 作为服务器端。" + +msgid "Connection Timeout" +msgstr "连接超时时间" + +msgid "Local Port" +msgstr "本地端口" + +msgid "Fast Open" +msgstr "快速打开" + +msgid "plugin" +msgstr "插件" + +msgid "opts" +msgstr "插件选项" + +msgid "Protocol" +msgstr "协议名称" + +msgid "Protocol_param" +msgstr "协议参数" + +msgid "Obfs" +msgstr "混淆" + +msgid "Obfs_param" +msgstr "混淆参数" + +msgid "Plugin Name" +msgstr "插件名称" + +msgid "Plugin Arguments" +msgstr "插件参数" + +msgid "Brook Protocol" +msgstr "Brook协议" + +msgid "Use TLS" +msgstr "使用TLS" + +msgid "Naiveproxy Protocol" +msgstr "Naiveproxy协议" + +msgid "V2ray Protocol" +msgstr "V2ray协议" + +msgid "User Level" +msgstr "用户等级(level)" + +msgid "Transport" +msgstr "传输方式" + +msgid "Public Key" +msgstr "公钥" + +msgid "Private Key" +msgstr "私钥" + +msgid "Pre shared key" +msgstr "额外的对称加密密钥" + +msgid "Local Address" +msgstr "本地地址" + +msgid "Decimal numbers separated by \",\" or Base64-encoded strings." +msgstr "用“,”隔开的十进制数字或 Base64 编码字符串。" + +msgid "Camouflage Type" +msgstr "伪装类型" + +msgid "Transport Layer Encryption" +msgstr "传输层加密" + +msgid "Whether or not transport layer encryption is enabled, \"none\" for unencrypted, \"tls\" for using TLS, \"xtls\" for using XTLS." +msgstr "是否启入传输层加密,支持的选项有 \"none\" 表示不加密,\"tls\" 表示使用 TLS,\"xtls\" 表示使用 XTLS。" + +msgid "Original Trojan only supported 'tls', please choose 'tls'." +msgstr "原版Trojan只支持'tls',请选择'tls'。" + +msgid "Transfer mode" +msgstr "传输模式" + +msgid "Domain" +msgstr "域名" + +msgid "allowInsecure" +msgstr "允许不安全连接" + +msgid "Whether unsafe connections are allowed. When checked, Certificate validation will be skipped." +msgstr "是否允许不安全连接。当勾选时,将跳过证书验证。" + +msgid "SS AEAD Node Use Type" +msgstr "SS AEAD节点使用类型" + +msgid "Trojan Node Use Type" +msgstr "Trojan节点使用类型" + +msgid "
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)" +msgstr "
none:默认值,不进行伪装,发送的数据是没有特征的数据包。
srtp:伪装成 SRTP 数据包,会被识别为视频通话数据(如 FaceTime)。
utp:伪装成 uTP 数据包,会被识别为 BT 下载数据。
wechat-video:伪装成微信视频通话的数据包。
dtls:伪装成 DTLS 1.2 数据包。
wireguard:伪装成 WireGuard 数据包。(并不是真正的 WireGuard 协议)" + +msgid "A legal file path. This file must not exist before running." +msgstr "一个合法的文件路径。在运行之前,这个文件必须不存在。" + +msgid "Auth" +msgstr "身份认证" + +msgid "Socks for authentication" +msgstr "Socks认证方式" + +msgid "Socks protocol authentication, support anonymous and password." +msgstr "Socks 协议的认证方式,支持匿名方式和账号密码方式。" + +msgid "anonymous" +msgstr "匿名" + +msgid "User Password" +msgstr "账号密码" + +msgid "Username and Password must be used together!" +msgstr "账号和密码必须同时使用!" + +msgid "Firewall tools" +msgstr "防火墙工具" + +msgid "IPv6 TProxy" +msgstr "IPv6透明代理(TProxy)" + +msgid "Experimental feature. Make sure that your node supports IPv6." +msgstr "实验特性,请确保你的节点支持IPv6" + +msgid "Status info" +msgstr "状态信息" + +msgid "Big icon" +msgstr "大图标" + +msgid "Show node check" +msgstr "显示节点检测" + +msgid "Show Show IP111" +msgstr "显示IP111" + +msgid "The MTProto protocol must be 32 characters and can only contain characters from 0 to 9 and a to f." +msgstr "MTProto 协议必须为 32 个字符,仅可包含 0 到 9 和 a 到 f 之间的字符。" + +msgid "Destination protocol" +msgstr "目标协议" + +msgid "Destination address" +msgstr "目标地址" + +msgid "Destination port" +msgstr "目标端口" + +msgid "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." +msgstr "是否接收 PROXY protocol,当该节点要被回落或被代理转发时,必须启用,否则不能使用。" + +msgid "outbound node" +msgstr "出站节点" + +msgid "Custom Socks" +msgstr "自定义 Socks" + +msgid "Custom HTTP" +msgstr "自定义 HTTP" + +msgid "Custom Interface" +msgstr "自定义接口" + +msgid "Interface" +msgstr "接口" + +msgid "Bind Local" +msgstr "本机监听" + +msgid "When selected, it can only be accessed locally, It is recommended to turn on when using reverse proxies or be fallback." +msgstr "当勾选时,只能由本机访问此端口,当想被反向代理或被回落时建议勾选此项。" + +msgid "Accept LAN Access" +msgstr "接受局域网访问" + +msgid "When selected, it can accessed lan , this will not be safe!" +msgstr "当勾选时,可以直接访问局域网,这将不安全!(非特殊情况不建议开启)" + +msgid "Enable Remote" +msgstr "启用转发" + +msgid "You can forward to Nginx/Caddy/V2ray/Xray WebSocket and more." +msgstr "您可以转发到Nginx/Caddy/V2ray/Xray WebSocket等。" + +msgid "Remote Address" +msgstr "远程地址" + +msgid "Remote Port" +msgstr "远程端口" + +msgid "as:" +msgstr "如:" + +msgid "Public key absolute path" +msgstr "公钥文件绝对路径" + +msgid "Private key absolute path" +msgstr "私钥文件绝对路径" + +msgid "Can't find this file!" +msgstr "找不到这个文件!" + +msgid "Public key and Private key path can not be empty!" +msgstr "公钥和私钥文件路径不能为空!" + +msgid "Server-Side" +msgstr "服务器端" + +msgid "Server Config" +msgstr "服务器配置" + +msgid "Users Manager" +msgstr "用户管理" + +msgid "Logs" +msgstr "日志" + +msgid "Log" +msgstr "日志" + +msgid "Close Node Log" +msgstr "关闭节点日志" + +msgid "Log Level" +msgstr "日志等级" + +msgid "Not enabled log" +msgstr "未启用日志" + +msgid "UDP Forward" +msgstr "UDP转发" + +msgid "DNS Settings" +msgstr "DNS设置" + +msgid "Null" +msgstr "无" + +msgid "You did not fill in the %s path. Please save and apply then update manually." +msgstr "您没有填写 %s 路径。请保存应用后再手动更新。" + +msgid "Not installed unzip, Can't unzip!" +msgstr "未安装unzip,无法解压。" + +msgid "Can't determine ARCH, or ARCH not supported." +msgstr "无法确认ARCH架构,或是不支持。" + +msgid "Get remote version info failed." +msgstr "获取远程版本信息失败。" + +msgid "New version found, but failed to get new version download url." +msgstr "发现新版本,但未能获得新版本的下载地址。" + +msgid "Download url is required." +msgstr "请指定下载地址。" + +msgid "File download failed or timed out: %s" +msgstr "文件下载失败或超时:%s" + +msgid "File path required." +msgstr "请指定文件路径。" + +msgid "%s not enough space." +msgstr "%s 空间不足。" + +msgid "Can't find client in file: %s" +msgstr "无法在文件中找到客户端:%s" + +msgid "Client file is required." +msgstr "请指定客户端文件。" + +msgid "The client file is not suitable for current device." +msgstr "客户端文件不适合当前设备。" + +msgid "Can't move new file to path: %s" +msgstr "无法移动新文件到:%s" + +msgid "Mux concurrency" +msgstr "最大并发连接数" + +msgid "XUDP Mux concurrency" +msgstr "XUDP 最大并发连接数" + +msgid "Mux idle timeout" +msgstr "最大闲置时间" + +msgid "Enable early data" +msgstr "启用前置数据" + +msgid "Early data length" +msgstr "前置数据最大长度" + +msgid "Early data header name" +msgstr "前置数据 HTTP 头名" + +msgid "Recommended value: Sec-WebSocket-Protocol" +msgstr "推荐值:Sec-WebSocket-Protocol" + +msgid "Health check" +msgstr "健康检查" + +msgid "Idle timeout" +msgstr "闲置时间" + +msgid "Health check timeout" +msgstr "检查超时时间" + +msgid "Permit without stream" +msgstr "无子连接时的健康检查" + +msgid "Initial Windows Size" +msgstr "初始窗口大小" + +msgid "No Sniffing Lists" +msgstr "不进行流量嗅探的域名列表" + +msgid "Hosts added into No Sniffing Lists will not resolve again on server (Xray only)." +msgstr "加入的域名不会再次在服务器解析(仅适用于Xray)。" + +msgid "Buffer Size (Xray)" +msgstr "缓冲区大小(Xray)" + +msgid "Buffer size for every connection (kB)" +msgstr "每一个连接的缓冲区大小(kB)" + +msgid "Custom geoip URL" +msgstr "自定义geoip文件更新链接" + +msgid "Custom geosite URL" +msgstr "自定义geosite文件更新链接" + +msgid "Handshake Timeout" +msgstr "握手超时 " + +msgid "Idle Timeout" +msgstr "空闲超时 " + +msgid "Hop Interval" +msgstr "端口跳跃时间 " + +msgid "Additional ports for hysteria hop" +msgstr "端口跳跃额外端口" diff --git a/luci-app-passwall2/po/zh_Hans b/luci-app-passwall2/po/zh_Hans new file mode 120000 index 000000000..41451e4a1 --- /dev/null +++ b/luci-app-passwall2/po/zh_Hans @@ -0,0 +1 @@ +zh-cn \ No newline at end of file diff --git a/luci-app-passwall2/root/etc/config/passwall2_server b/luci-app-passwall2/root/etc/config/passwall2_server new file mode 100644 index 000000000..c9526cb28 --- /dev/null +++ b/luci-app-passwall2/root/etc/config/passwall2_server @@ -0,0 +1,4 @@ + +config global 'global' + option enable '0' + diff --git a/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 b/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 new file mode 100644 index 000000000..688ccb072 --- /dev/null +++ b/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 @@ -0,0 +1,23 @@ +#!/bin/sh + +[[ "$ACTION" == "ifup" && $(uci get "passwall2.@global[0].enabled") == "1" ]] && [ -f /var/lock/passwall2_ready.lock ] && { + default_device=$(ip route | grep default | awk -F 'dev ' '{print $2}' | awk '{print $1}') + [ "$default_device" == "$DEVICE" ] && { + LOCK_FILE_DIR=/var/lock + [ ! -d ${LOCK_FILE_DIR} ] && mkdir -p ${LOCK_FILE_DIR} + LOCK_FILE="${LOCK_FILE_DIR}/passwall2_ifup.lock" + if [ -s ${LOCK_FILE} ]; then + SPID=$(cat ${LOCK_FILE}) + if [ -e /proc/${SPID}/status ]; then + exit 1 + fi + cat /dev/null > ${LOCK_FILE} + fi + echo $$ > ${LOCK_FILE} + + /etc/init.d/passwall2 restart + echo "passwall2: restart when $INTERFACE ifup" > /dev/kmsg + + rm -rf ${LOCK_FILE} + } +} diff --git a/luci-app-passwall2/root/etc/init.d/passwall2 b/luci-app-passwall2/root/etc/init.d/passwall2 new file mode 100755 index 000000000..8f07ae3cb --- /dev/null +++ b/luci-app-passwall2/root/etc/init.d/passwall2 @@ -0,0 +1,66 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=15 + +CONFIG=passwall2 +APP_FILE=/usr/share/${CONFIG}/app.sh +LOCK_FILE_DIR=/var/lock +LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}.lock + +set_lock() { + [ ! -d "$LOCK_FILE_DIR" ] && mkdir -p $LOCK_FILE_DIR + exec 999>"$LOCK_FILE" + flock -xn 999 +} + +unset_lock() { + flock -u 999 + rm -rf "$LOCK_FILE" +} + +unlock() { + failcount=1 + while [ "$failcount" -le 10 ]; do + if [ -f "$LOCK_FILE" ]; then + let "failcount++" + sleep 1s + [ "$failcount" -ge 10 ] && unset_lock + else + break + fi + done +} + +boot() { + local delay=$(uci -q get ${CONFIG}.@global_delay[0].start_delay || echo 1) + if [ "$delay" -gt 0 ]; then + $APP_FILE echolog "执行启动延时 $delay 秒后再启动!" + sleep $delay + fi + restart + touch ${LOCK_FILE_DIR}/${CONFIG}_ready.lock +} + +start() { + set_lock + [ $? == 1 ] && $APP_FILE echolog "脚本已经在运行,不重复运行,退出." && exit 0 + $APP_FILE start + unset_lock +} + +stop() { + unlock + set_lock + [ $? == 1 ] && $APP_FILE echolog "停止脚本等待超时,不重复运行,退出." && exit 0 + $APP_FILE stop + unset_lock +} + +restart() { + set_lock + [ $? == 1 ] && $APP_FILE echolog "脚本已经在运行,不重复运行,退出." && exit 0 + $APP_FILE stop + $APP_FILE start + unset_lock +} diff --git a/luci-app-passwall2/root/etc/init.d/passwall2_server b/luci-app-passwall2/root/etc/init.d/passwall2_server new file mode 100755 index 000000000..f18a73b7a --- /dev/null +++ b/luci-app-passwall2/root/etc/init.d/passwall2_server @@ -0,0 +1,16 @@ +#!/bin/sh /etc/rc.common + +START=99 + +start() { + lua /usr/lib/lua/luci/passwall2/server_app.lua start +} + +stop() { + lua /usr/lib/lua/luci/passwall2/server_app.lua stop +} + +restart() { + stop + start +} \ No newline at end of file diff --git a/luci-app-passwall2/root/etc/uci-defaults/luci-passwall2 b/luci-app-passwall2/root/etc/uci-defaults/luci-passwall2 new file mode 100755 index 000000000..1ab2d9583 --- /dev/null +++ b/luci-app-passwall2/root/etc/uci-defaults/luci-passwall2 @@ -0,0 +1,49 @@ +#!/bin/sh + +uci -q batch <<-EOF >/dev/null + set dhcp.@dnsmasq[0].localuse=1 + commit dhcp + delete ucitrack.@passwall2[-1] + add ucitrack passwall2 + set ucitrack.@passwall2[-1].init=passwall2 + commit ucitrack + delete firewall.passwall2 + set firewall.passwall2=include + set firewall.passwall2.type=script + set firewall.passwall2.path=/var/etc/passwall2.include + set firewall.passwall2.reload=1 + commit firewall + delete ucitrack.@passwall2_server[-1] + add ucitrack passwall2_server + set ucitrack.@passwall2_server[-1].init=passwall2_server + commit ucitrack + delete firewall.passwall2_server + set firewall.passwall2_server=include + set firewall.passwall2_server.type=script + set firewall.passwall2_server.path=/var/etc/passwall2_server.include + set firewall.passwall2_server.reload=1 + commit firewall + set uhttpd.main.max_requests=50 + commit uhttpd +EOF + +touch /etc/config/passwall2_show >/dev/null 2>&1 +[ ! -s "/etc/config/passwall2" ] && cp -f /usr/share/passwall2/0_default_config /etc/config/passwall2 + +use_nft=$(uci -q get passwall2.@global_forwarding[0].use_nft || echo "0") +[ "${use_nft}" = "0" ] && { + if [ -z "$(command -v iptables-legacy || command -v iptables)" ] || [ -z "$(command -v ipset)" ] || [ -z "$(dnsmasq --version | grep 'Compile time options:.* ipset')" ]; then + [ "$(opkg list-installed | grep "firewall4")" ] && [ "$(opkg list-installed | grep "nftables")" ] && { + [ "$(opkg list-installed | grep "kmod\-nft\-socket")" ] && [ "$(opkg list-installed | grep "kmod\-nft\-tproxy")" ] && [ "$(opkg list-installed | grep "kmod\-nft\-nat")" ] && { + uci -q set passwall2.@global_forwarding[0].use_nft=1 + uci -q commit passwall2 + sed -i "s#use_nft '0'#use_nft '1'#g" /usr/share/passwall2/0_default_config + } + } + fi +} + +rm -f /tmp/luci-indexcache +rm -rf /tmp/luci-modulecache/ +killall -HUP rpcd 2>/dev/null +exit 0 diff --git a/luci-app-passwall2/root/usr/share/passwall2/0_default_config b/luci-app-passwall2/root/usr/share/passwall2/0_default_config new file mode 100644 index 000000000..5e5adf111 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/0_default_config @@ -0,0 +1,244 @@ + +config global + option enabled '0' + option node_socks_port '1070' + option localhost_proxy '1' + option socks_enabled '0' + option acl_enable '0' + option node 'myshunt' + option direct_dns_protocol 'auto' + option direct_dns_query_strategy 'UseIP' + option remote_dns_protocol 'tcp' + option remote_dns '1.1.1.1' + option remote_dns_query_strategy 'UseIPv4' + option dns_hosts 'cloudflare-dns.com 1.1.1.1 +dns.google.com 8.8.8.8' + option close_log '0' + option loglevel 'error' + +config global_haproxy + option balancing_enable '0' + +config global_delay + option auto_on '0' + option start_daemon '1' + option start_delay '60' + +config global_forwarding + option tcp_no_redir_ports 'disable' + option udp_no_redir_ports 'disable' + option tcp_redir_ports '22,25,53,143,465,587,853,993,995,80,443' + option udp_redir_ports '1:65535' + option accept_icmp '0' + option use_nft '0' + option tcp_proxy_way 'redirect' + option ipv6_tproxy '0' + option sniffing '1' + option route_only '0' + +config global_other + option nodes_ping 'auto_ping tcping' + +config global_rules + option auto_update '0' + option geosite_update '1' + option geoip_update '1' + option v2ray_location_asset '/usr/share/v2ray/' + +config global_app + option v2ray_file '/usr/bin/v2ray' + option xray_file '/usr/bin/xray' + option brook_file '/usr/bin/brook' + option hysteria_file '/usr/bin/hysteria' + +config global_subscribe + option filter_keyword_mode '1' + list filter_discard_list '过期时间' + list filter_discard_list '剩余流量' + list filter_discard_list 'QQ群' + list filter_discard_list '官网' + +config auto_switch + option enable '0' + option testing_time '1' + option connect_timeout '3' + option retry_num '3' + option shunt_logic '1' + +config nodes 'myshunt' + option remarks '分流总节点' + option type 'Xray' + option protocol '_shunt' + option DirectGame '_direct' + option ProxyGame '_default' + option Direct '_direct' + option AD 'nil' + option BT '_direct' + option Netflix 'nil' + option OpenAI 'nil' + option TVB 'nil' + option Proxy '_default' + option China '_direct' + option QUIC '_blackhole' + option UDP 'nil' + option default_node 'nil' + option domainStrategy 'IPOnDemand' + option domainMatcher 'hybrid' + +config shunt_rules 'DirectGame' + option remarks 'DirectGame' + option network 'tcp,udp' + option domain_list 'api.steampowered.com +regexp:\.cm.steampowered.com$ +regexp:\.steamserver.net$ +geosite:category-games@cn +' + + option ip_list '103.10.124.0/24 +103.10.125.0/24 +103.28.54.0/24 +146.66.152.0/24 +146.66.155.0/24 +153.254.86.0/24 +155.133.224.0/23 +155.133.226.0/24 +155.133.227.0/24 +155.133.230.0/24 +155.133.232.0/24 +155.133.233.0/24 +155.133.234.0/24 +155.133.236.0/23 +155.133.238.0/24 +155.133.239.0/24 +155.133.240.0/23 +155.133.245.0/24 +155.133.246.0/24 +155.133.248.0/24 +155.133.249.0/24 +155.133.250.0/24 +155.133.251.0/24 +155.133.252.0/24 +155.133.253.0/24 +155.133.254.0/24 +155.133.255.0/24 +162.254.192.0/24 +162.254.193.0/24 +162.254.194.0/23 +162.254.195.0/24 +162.254.196.0/24 +162.254.197.0/24 +162.254.198.0/24 +162.254.199.0/24 +185.25.182.0/24 +185.25.183.0/24 +190.217.33.0/24 +192.69.96.0/22 +205.185.194.0/24 +205.196.6.0/24 +208.64.200.0/24 +208.64.201.0/24 +208.64.202.0/24 +208.64.203.0/24 +208.78.164.0/22' + +config shunt_rules 'ProxyGame' + option remarks 'ProxyGame' + option domain_list 'geosite:category-games@!cn +domain:store.steampowered.com +' + +config shunt_rules 'Direct' + option network 'tcp,udp' + option remarks 'Direct' + option ip_list 'geoip:private +114.114.114.114 +114.114.115.115 +223.5.5.5 +223.6.6.6 +119.29.29.29 +180.76.76.76 +' + option domain_list 'apple.com +microsoft.com +dyndns.com +steamcontent.com +dl.steam.clngaa.com +dl.steam.ksyna.com +st.dl.bscstorage.net +st.dl.eccdnx.com +st.dl.pinyuncloud.com +cdn.mileweb.cs.steampowered.com.8686c.com +cdn-ws.content.steamchina.com +cdn-qc.content.steamchina.com +cdn-ali.content.steamchina.com +epicgames-download1-1251447533.file.myqcloud.com' + +config shunt_rules 'AD' + option remarks 'AD' + option domain_list 'geosite:category-ads-all' + option network 'tcp,udp' + +config shunt_rules 'BT' + option remarks 'BT' + option protocol 'bittorrent' + option network 'tcp,udp' + +config shunt_rules 'Netflix' + option remarks 'Netflix' + option network 'tcp,udp' + option domain_list 'geosite:netflix' + +config shunt_rules 'OpenAI' + option remarks 'OpenAI' + option domain_list 'geosite:openai' + +config shunt_rules 'TVB' + option remarks 'TVB' + option network 'tcp,udp' + option domain_list 'geosite:tvb +geosite:mytvsuper +' + +config shunt_rules 'Proxy' + option network 'tcp,udp' + option remarks 'Proxy' + option domain_list 'geosite:geolocation-!cn' + option ip_list '149.154.160.0/20 +91.108.4.0/22 +91.108.56.0/24 +109.239.140.0/24 +67.198.55.0/24 +8.8.4.4 +8.8.8.8 +208.67.222.222 +208.67.220.220 +1.1.1.1 +1.1.1.2 +1.0.0.1 +9.9.9.9 +149.112.112.112 +2001:67c:4e8::/48 +2001:b28:f23c::/48 +2001:b28:f23d::/48 +2001:b28:f23f::/48 +2001:b28:f242::/48 +2001:4860:4860::8888 +2001:4860:4860::8844 +2606:4700:4700::1111 +2606:4700:4700::1001 +' + +config shunt_rules 'China' + option remarks 'China' + option network 'tcp,udp' + option ip_list 'geoip:cn' + option domain_list 'geosite:cn' + +config shunt_rules 'QUIC' + option remarks 'QUIC' + option port '80,443' + option network 'udp' + +config shunt_rules 'UDP' + option remarks 'UDP' + option network 'udp' diff --git a/luci-app-passwall2/root/usr/share/passwall2/app.sh b/luci-app-passwall2/root/usr/share/passwall2/app.sh new file mode 100755 index 000000000..35635cb4c --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -0,0 +1,1087 @@ +#!/bin/sh +# Copyright (C) 2022-2023 xiaorouji + +. $IPKG_INSTROOT/lib/functions.sh +. $IPKG_INSTROOT/lib/functions/service.sh + +CONFIG=passwall2 +TMP_PATH=/tmp/etc/$CONFIG +TMP_BIN_PATH=$TMP_PATH/bin +TMP_SCRIPT_FUNC_PATH=$TMP_PATH/script_func +TMP_ID_PATH=$TMP_PATH/id +TMP_PORT_PATH=$TMP_PATH/port +TMP_ROUTE_PATH=$TMP_PATH/route +TMP_ACL_PATH=$TMP_PATH/acl +TMP_PATH2=/tmp/etc/${CONFIG}_tmp +DNSMASQ_PATH=/etc/dnsmasq.d +TMP_DNSMASQ_PATH=/tmp/dnsmasq.d/passwall2 +LOG_FILE=/tmp/log/$CONFIG.log +APP_PATH=/usr/share/$CONFIG +RULES_PATH=/usr/share/${CONFIG}/rules +TUN_DNS_PORT=15353 +TUN_DNS="127.0.0.1#${TUN_DNS_PORT}" +DEFAULT_DNS= +IFACES= +ENABLED_DEFAULT_ACL=0 +ENABLED_ACLS=0 +PROXY_IPV6=0 +PROXY_IPV6_UDP=0 +LUA_UTIL_PATH=/usr/lib/lua/luci/passwall2 +UTIL_SS=$LUA_UTIL_PATH/util_shadowsocks.lua +UTIL_XRAY=$LUA_UTIL_PATH/util_xray.lua +UTIL_NAIVE=$LUA_UTIL_PATH/util_naiveproxy.lua +UTIL_HYSTERIA=$LUA_UTIL_PATH/util_hysteria.lua +V2RAY_ARGS="" +V2RAY_CONFIG="" + +echolog() { + local d="$(date "+%Y-%m-%d %H:%M:%S")" + echo -e "$d: $*" >>$LOG_FILE +} + +config_get_type() { + local ret=$(uci -q get "${CONFIG}.${1}" 2>/dev/null) + echo "${ret:=$2}" +} + +config_n_get() { + local ret=$(uci -q get "${CONFIG}.${1}.${2}" 2>/dev/null) + echo "${ret:=$3}" +} + +config_t_get() { + local index=${4:-0} + local ret=$(uci -q get "${CONFIG}.@${1}[${index}].${2}" 2>/dev/null) + echo "${ret:=${3}}" +} + +get_enabled_anonymous_secs() { + uci -q show "${CONFIG}" | grep "${1}\[.*\.enabled='1'" | cut -d '.' -sf2 +} + +get_host_ip() { + local host=$2 + local count=$3 + [ -z "$count" ] && count=3 + local isip="" + local ip=$host + if [ "$1" == "ipv6" ]; then + isip=$(echo $host | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}") + if [ -n "$isip" ]; then + isip=$(echo $host | cut -d '[' -f2 | cut -d ']' -f1) + else + isip=$(echo $host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}") + fi + else + isip=$(echo $host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}") + fi + [ -z "$isip" ] && { + local t=4 + [ "$1" == "ipv6" ] && t=6 + local vpsrip=$(resolveip -$t -t $count $host | awk 'NR==1{print}') + ip=$vpsrip + } + echo $ip +} + +get_node_host_ip() { + local ip + local address=$(config_n_get $1 address) + [ -n "$address" ] && { + local use_ipv6=$(config_n_get $1 use_ipv6) + local network_type="ipv4" + [ "$use_ipv6" == "1" ] && network_type="ipv6" + ip=$(get_host_ip $network_type $address) + } + echo $ip +} + +get_ip_port_from() { + local __host=${1}; shift 1 + local __ipv=${1}; shift 1 + local __portv=${1}; shift 1 + local __ucipriority=${1}; shift 1 + + local val1 val2 + if [ -n "${__ucipriority}" ]; then + val2=$(config_n_get ${__host} port $(echo $__host | sed -n 's/^.*[:#]\([0-9]*\)$/\1/p')) + val1=$(config_n_get ${__host} address "${__host%%${val2:+[:#]${val2}*}}") + else + val2=$(echo $__host | sed -n 's/^.*[:#]\([0-9]*\)$/\1/p') + val1="${__host%%${val2:+[:#]${val2}*}}" + fi + eval "${__ipv}=\"$val1\"; ${__portv}=\"$val2\"" +} + +host_from_url(){ + local f=${1} + + ## Remove protocol part of url ## + f="${f##http://}" + f="${f##https://}" + f="${f##ftp://}" + f="${f##sftp://}" + + ## Remove username and/or username:password part of URL ## + f="${f##*:*@}" + f="${f##*@}" + + ## Remove rest of urls ## + f="${f%%/*}" + echo "${f%%:*}" +} + +hosts_foreach() { + local __hosts + eval "__hosts=\$${1}"; shift 1 + local __func=${1}; shift 1 + local __default_port=${1}; shift 1 + local __ret=1 + + [ -z "${__hosts}" ] && return 0 + local __ip __port + for __host in $(echo $__hosts | sed 's/[ ,]/\n/g'); do + get_ip_port_from "$__host" "__ip" "__port" + eval "$__func \"${__host}\" \"\${__ip}\" \"\${__port:-${__default_port}}\" \"$@\"" + __ret=$? + [ ${__ret} -ge ${ERROR_NO_CATCH:-1} ] && return ${__ret} + done +} + +check_host() { + local f=${1} + a=$(echo $f | grep "\/") + [ -n "$a" ] && return 1 + # 判断是否包含汉字~ + local tmp=$(echo -n $f | awk '{print gensub(/[!-~]/,"","g",$0)}') + [ -n "$tmp" ] && return 1 + return 0 +} + +get_first_dns() { + local __hosts_val=${1}; shift 1 + __first() { + [ -z "${2}" ] && return 0 + echo "${2}#${3}" + return 1 + } + eval "hosts_foreach \"${__hosts_val}\" __first \"$@\"" +} + +get_last_dns() { + local __hosts_val=${1}; shift 1 + local __first __last + __every() { + [ -z "${2}" ] && return 0 + __last="${2}#${3}" + __first=${__first:-${__last}} + } + eval "hosts_foreach \"${__hosts_val}\" __every \"$@\"" + [ "${__first}" == "${__last}" ] || echo "${__last}" +} + +check_port_exists() { + port=$1 + protocol=$2 + [ -n "$protocol" ] || protocol="tcp,udp" + result= + if [ "$protocol" = "tcp" ]; then + result=$(netstat -tln | grep -c ":$port ") + elif [ "$protocol" = "udp" ]; then + result=$(netstat -uln | grep -c ":$port ") + elif [ "$protocol" = "tcp,udp" ]; then + result=$(netstat -tuln | grep -c ":$port ") + fi + echo "${result}" +} + +get_new_port() { + port=$1 + [ "$port" == "auto" ] && port=2082 + protocol=$(echo $2 | tr 'A-Z' 'a-z') + result=$(check_port_exists $port $protocol) + if [ "$result" != 0 ]; then + temp= + if [ "$port" -lt 65535 ]; then + temp=$(expr $port + 1) + elif [ "$port" -gt 1 ]; then + temp=$(expr $port - 1) + fi + get_new_port $temp $protocol + else + echo $port + fi +} + +first_type() { + local path_name=${1} + type -t -p "/bin/${path_name}" -p "${TMP_BIN_PATH}/${path_name}" -p "${path_name}" "$@" | head -n1 +} + +eval_set_val() { + for i in $@; do + for j in $i; do + eval $j + done + done +} + +eval_unset_val() { + for i in $@; do + for j in $i; do + eval unset j + done + done +} + +ln_run() { + local file_func=${1} + local ln_name=${2} + local output=${3} + + shift 3; + if [ "${file_func%%/*}" != "${file_func}" ]; then + [ ! -L "${file_func}" ] && { + ln -s "${file_func}" "${TMP_BIN_PATH}/${ln_name}" >/dev/null 2>&1 + file_func="${TMP_BIN_PATH}/${ln_name}" + } + [ -x "${file_func}" ] || echolog " - $(readlink ${file_func}) 没有执行权限,无法启动:${file_func} $*" + fi + #echo "${file_func} $*" >&2 + [ -n "${file_func}" ] || echolog " - 找不到 ${ln_name},无法启动..." + ${file_func:-echolog " - ${ln_name}"} "$@" >${output} 2>&1 & + process_count=$(ls $TMP_SCRIPT_FUNC_PATH | grep -v "^_" | wc -l) + process_count=$((process_count + 1)) + echo "${file_func:-echolog " - ${ln_name}"} $@ >${output}" > $TMP_SCRIPT_FUNC_PATH/$process_count +} + +lua_api() { + local func=${1} + [ -z "${func}" ] && { + echo "nil" + return + } + echo $(lua -e "local api = require 'luci.passwall2.api' print(api.${func})") +} + +run_v2ray() { + local flag node redir_port socks_address socks_port socks_username socks_password http_address http_port http_username http_password + local dns_listen_port direct_dns_protocol direct_dns_udp_server direct_dns_tcp_server direct_dns_doh direct_dns_client_ip direct_dns_query_strategy remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache + local loglevel log_file config_file + local _extra_param="" + eval_set_val $@ + local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + if [ "$type" != "v2ray" ] && [ "$type" != "xray" ]; then + local bin=$(first_type $(config_t_get global_app xray_file) xray) + if [ -n "$bin" ]; then + type="xray" + else + bin=$(first_type $(config_t_get global_app v2ray_file) v2ray) + [ -n "$bin" ] && type="v2ray" + fi + fi + [ -z "$type" ] && return 1 + [ -n "$log_file" ] || local log_file="/dev/null" + [ -z "$loglevel" ] && local loglevel=$(config_t_get global loglevel "warning") + [ -n "$flag" ] && pgrep -af "$TMP_BIN_PATH" | awk -v P1="${flag}" 'BEGIN{IGNORECASE=1}$0~P1{print $1}' | xargs kill -9 >/dev/null 2>&1 + [ -n "$flag" ] && _extra_param="${_extra_param} -flag $flag" + [ -n "$socks_address" ] && _extra_param="${_extra_param} -local_socks_address $socks_address" + [ -n "$socks_port" ] && _extra_param="${_extra_param} -local_socks_port $socks_port" + [ -n "$socks_username" ] && [ -n "$socks_password" ] && _extra_param="${_extra_param} -local_socks_username $socks_username -local_socks_password $socks_password" + [ -n "$http_address" ] && _extra_param="${_extra_param} -local_http_address $http_address" + [ -n "$http_port" ] && _extra_param="${_extra_param} -local_http_port $http_port" + [ -n "$http_username" ] && [ -n "$http_password" ] && _extra_param="${_extra_param} -local_http_username $http_username -local_http_password $http_password" + local sniffing=$(config_t_get global_forwarding sniffing 1) + [ "${sniffing}" = "1" ] && { + _extra_param="${_extra_param} -sniffing 1" + local route_only=$(config_t_get global_forwarding route_only 0) + [ "${route_only}" = "1" ] && _extra_param="${_extra_param} -route_only 1" + } + local buffer_size=$(config_t_get global_forwarding buffer_size) + [ -n "${buffer_size}" ] && _extra_param="${_extra_param} -buffer_size ${buffer_size}" + + local protocol=$(config_n_get $node protocol) + [ "$protocol" == "_iface" ] && { + IFACES="$IFACES $(config_n_get $node iface)" + } + + [ -n "$dns_listen_port" ] && { + V2RAY_DNS_DIRECT_CONFIG="${TMP_PATH}/${flag}_dns_direct.json" + V2RAY_DNS_DIRECT_LOG="${TMP_PATH}/${flag}_dns_direct.log" + V2RAY_DNS_DIRECT_LOG="/dev/null" + V2RAY_DNS_DIRECT_ARGS="-dns_out_tag direct" + dns_direct_listen_port=$(get_new_port $(expr $dns_listen_port + 1) udp) + V2RAY_DNS_DIRECT_ARGS="${V2RAY_DNS_DIRECT_ARGS} -dns_listen_port ${dns_direct_listen_port}" + [ "$direct_dns_protocol" = "auto" ] && { + direct_dns_protocol="udp" + direct_dns_udp_server=${AUTO_DNS} + } + case "$direct_dns_protocol" in + udp) + local _dns=$(get_first_dns direct_dns_udp_server 53 | sed 's/#/:/g') + local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') + local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') + V2RAY_DNS_DIRECT_ARGS="${V2RAY_DNS_DIRECT_ARGS} -direct_dns_server ${_dns_address} -direct_dns_port ${_dns_port} -direct_dns_udp_server ${_dns_address}" + ;; + tcp) + local _dns=$(get_first_dns direct_dns_tcp_server 53 | sed 's/#/:/g') + local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') + local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') + V2RAY_DNS_DIRECT_ARGS="${V2RAY_DNS_DIRECT_ARGS} -direct_dns_server ${_dns_address} -direct_dns_port ${_dns_port} -direct_dns_tcp_server tcp://${_dns}" + ;; + doh) + local _doh_url=$(echo $direct_dns_doh | awk -F ',' '{print $1}') + local _doh_host_port=$(lua_api "get_domain_from_url(\"${_doh_url}\")") + #local _doh_host_port=$(echo $_doh_url | sed "s/https:\/\///g" | awk -F '/' '{print $1}') + local _doh_host=$(echo $_doh_host_port | awk -F ':' '{print $1}') + local is_ip=$(lua_api "is_ip(\"${_doh_host}\")") + local _doh_port=$(echo $_doh_host_port | awk -F ':' '{print $2}') + [ -z "${_doh_port}" ] && _doh_port=443 + local _doh_bootstrap=$(echo $direct_dns_doh | cut -d ',' -sf 2-) + [ "${is_ip}" = "true" ] && _doh_bootstrap=${_doh_host} + [ -n "$_doh_bootstrap" ] && V2RAY_DNS_DIRECT_ARGS="${V2RAY_DNS_DIRECT_ARGS} -direct_dns_server ${_doh_bootstrap}" + V2RAY_DNS_DIRECT_ARGS="${V2RAY_DNS_DIRECT_ARGS} -direct_dns_port ${_doh_port} -direct_dns_doh_url ${_doh_url} -direct_dns_doh_host ${_doh_host}" + ;; + esac + [ -n "$direct_dns_query_strategy" ] && V2RAY_DNS_DIRECT_ARGS="${V2RAY_DNS_DIRECT_ARGS} -dns_query_strategy ${direct_dns_query_strategy}" + [ -n "$direct_dns_client_ip" ] && V2RAY_DNS_DIRECT_ARGS="${V2RAY_DNS_DIRECT_ARGS} -dns_client_ip ${direct_dns_client_ip}" + + lua $UTIL_XRAY gen_dns_config ${V2RAY_DNS_DIRECT_ARGS} > $V2RAY_DNS_DIRECT_CONFIG + ln_run "$(first_type $(config_t_get global_app ${type}_file) ${type})" ${type} $V2RAY_DNS_DIRECT_LOG run -c "$V2RAY_DNS_DIRECT_CONFIG" + + direct_dnsmasq_listen_port=$(get_new_port $(expr $dns_direct_listen_port + 1) udp) + if [ "${nftflag}" = "1" ]; then + local direct_nftset="4#inet#fw4#passwall2_whitelist,6#inet#fw4#passwall2_whitelist6" + else + local direct_ipset="passwall2_whitelist,passwall2_whitelist6" + fi + run_ipset_dnsmasq listen_port=${direct_dnsmasq_listen_port} server_dns=127.0.0.1#${dns_direct_listen_port} ipset="${direct_ipset}" nftset="${direct_nftset}" config_file=$TMP_PATH/dnsmasq_${flag}_direct.conf + + V2RAY_DNS_REMOTE_CONFIG="${TMP_PATH}/${flag}_dns_remote.json" + V2RAY_DNS_REMOTE_LOG="${TMP_PATH}/${flag}_dns_remote.log" + V2RAY_DNS_REMOTE_LOG="/dev/null" + V2RAY_DNS_REMOTE_ARGS="-dns_out_tag remote" + dns_remote_listen_port=$(get_new_port $(expr $direct_dnsmasq_listen_port + 1) udp) + V2RAY_DNS_REMOTE_ARGS="${V2RAY_DNS_REMOTE_ARGS} -dns_listen_port ${dns_remote_listen_port}" + case "$remote_dns_protocol" in + udp) + local _dns=$(get_first_dns remote_dns_udp_server 53 | sed 's/#/:/g') + local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') + local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') + V2RAY_DNS_REMOTE_ARGS="${V2RAY_DNS_REMOTE_ARGS} -remote_dns_server ${_dns_address} -remote_dns_port ${_dns_port} -remote_dns_udp_server ${_dns_address}" + ;; + tcp) + local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g') + local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') + local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') + V2RAY_DNS_REMOTE_ARGS="${V2RAY_DNS_REMOTE_ARGS} -remote_dns_server ${_dns_address} -remote_dns_port ${_dns_port} -remote_dns_tcp_server tcp://${_dns}" + ;; + doh) + local _doh_url=$(echo $remote_dns_doh | awk -F ',' '{print $1}') + local _doh_host_port=$(lua_api "get_domain_from_url(\"${_doh_url}\")") + #local _doh_host_port=$(echo $_doh_url | sed "s/https:\/\///g" | awk -F '/' '{print $1}') + local _doh_host=$(echo $_doh_host_port | awk -F ':' '{print $1}') + local is_ip=$(lua_api "is_ip(\"${_doh_host}\")") + local _doh_port=$(echo $_doh_host_port | awk -F ':' '{print $2}') + [ -z "${_doh_port}" ] && _doh_port=443 + local _doh_bootstrap=$(echo $remote_dns_doh | cut -d ',' -sf 2-) + [ "${is_ip}" = "true" ] && _doh_bootstrap=${_doh_host} + [ -n "$_doh_bootstrap" ] && V2RAY_DNS_REMOTE_ARGS="${V2RAY_DNS_REMOTE_ARGS} -remote_dns_server ${_doh_bootstrap}" + V2RAY_DNS_REMOTE_ARGS="${V2RAY_DNS_REMOTE_ARGS} -remote_dns_port ${_doh_port} -remote_dns_doh_url ${_doh_url} -remote_dns_doh_host ${_doh_host}" + ;; + esac + + [ -n "$remote_dns_query_strategy" ] && V2RAY_DNS_REMOTE_ARGS="${V2RAY_DNS_REMOTE_ARGS} -dns_query_strategy ${remote_dns_query_strategy}" + [ -n "$remote_dns_client_ip" ] && V2RAY_DNS_REMOTE_ARGS="${V2RAY_DNS_REMOTE_ARGS} -dns_client_ip ${remote_dns_client_ip}" + + V2RAY_DNS_REMOTE_ARGS="${V2RAY_DNS_REMOTE_ARGS} -remote_dns_outbound_socks_address 127.0.0.1 -remote_dns_outbound_socks_port ${socks_port}" + lua $UTIL_XRAY gen_dns_config ${V2RAY_DNS_REMOTE_ARGS} > $V2RAY_DNS_REMOTE_CONFIG + ln_run "$(first_type $(config_t_get global_app ${type}_file) ${type})" ${type} $V2RAY_DNS_REMOTE_LOG run -c "$V2RAY_DNS_REMOTE_CONFIG" + + [ -n "$dns_listen_port" ] && _extra_param="${_extra_param} -dns_listen_port ${dns_listen_port}" + [ -n "$dns_cache" ] && _extra_param="${_extra_param} -dns_cache ${dns_cache}" + _extra_param="${_extra_param} -dns_query_strategy UseIP" + _extra_param="${_extra_param} -direct_dns_port ${direct_dnsmasq_listen_port} -direct_dns_udp_server 127.0.0.1" + _extra_param="${_extra_param} -remote_dns_port ${dns_remote_listen_port} -remote_dns_udp_server 127.0.0.1" + [ "$remote_fakedns" = "1" ] && _extra_param="${_extra_param} -remote_dns_fake 1 -remote_dns_fake_strategy ${remote_dns_query_strategy}" + } + + lua $UTIL_XRAY gen_config -node $node -redir_port $redir_port -tcp_proxy_way $tcp_proxy_way -loglevel $loglevel ${_extra_param} > $config_file + ln_run "$(first_type $(config_t_get global_app ${type}_file) ${type})" ${type} $log_file run -c "$config_file" +} + +run_socks() { + local flag node bind socks_port config_file http_port http_config_file relay_port log_file + eval_set_val $@ + [ -n "$config_file" ] && [ -z "$(echo ${config_file} | grep $TMP_PATH)" ] && config_file=$TMP_PATH/$config_file + [ -n "$http_port" ] || http_port=0 + [ -n "$http_config_file" ] && [ -z "$(echo ${http_config_file} | grep $TMP_PATH)" ] && http_config_file=$TMP_PATH/$http_config_file + if [ -n "$log_file" ] && [ -z "$(echo ${log_file} | grep $TMP_PATH)" ]; then + log_file=$TMP_PATH/$log_file + else + log_file="/dev/null" + fi + local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + local remarks=$(config_n_get $node remarks) + local server_host=$(config_n_get $node address) + local port=$(config_n_get $node port) + [ -n "$relay_port" ] && { + server_host="127.0.0.1" + port=$relay_port + } + local error_msg tmp + + if [ -n "$server_host" ] && [ -n "$port" ]; then + check_host $server_host + [ $? != 0 ] && { + echolog " - Socks节点:[$remarks]${server_host} 是非法的服务器地址,无法启动!" + return 1 + } + tmp="${server_host}:${port}" + else + error_msg="某种原因,此 Socks 服务的相关配置已失联,启动中止!" + fi + + if [ "$type" == "v2ray" ] || [ "$type" == "xray" ]; then + local protocol=$(config_n_get $node protocol) + if [ "$protocol" == "_balancing" ] || [ "$protocol" == "_shunt" ] || [ "$protocol" == "_iface" ]; then + unset error_msg + fi + fi + + [ -n "${error_msg}" ] && { + [ "$bind" != "127.0.0.1" ] && echolog " - Socks节点:[$remarks]${tmp},启动中止 ${bind}:${socks_port} ${error_msg}" + return 1 + } + [ "$bind" != "127.0.0.1" ] && echolog " - Socks节点:[$remarks]${tmp},启动 ${bind}:${socks_port}" + + case "$type" in + v2ray|\ + xray) + [ "$http_port" != "0" ] && { + http_flag=1 + config_file=$(echo $config_file | sed "s/SOCKS/HTTP_SOCKS/g") + local _extra_param="-local_http_port $http_port" + } + lua $UTIL_XRAY gen_config -flag SOCKS_$flag -node $node -local_socks_port $socks_port ${_extra_param} > $config_file + ln_run "$(first_type $(config_t_get global_app ${type}_file) ${type})" ${type} $log_file run -c "$config_file" + ;; + naiveproxy) + lua $UTIL_NAIVE gen_config -node $node -run_type socks -local_addr $bind -local_port $socks_port -server_host $server_host -server_port $port > $config_file + ln_run "$(first_type naive)" naive $log_file "$config_file" + ;; + brook) + local protocol=$(config_n_get $node protocol client) + local prefix="" + [ "$protocol" == "wsclient" ] && { + prefix="ws://" + local brook_tls=$(config_n_get $node brook_tls 0) + [ "$brook_tls" == "1" ] && { + prefix="wss://" + protocol="wssclient" + } + local ws_path=$(config_n_get $node ws_path "/ws") + } + server_host=${prefix}${server_host} + ln_run "$(first_type $(config_t_get global_app brook_file) brook)" "brook_SOCKS_${flag}" $log_file "$protocol" --socks5 "$bind:$socks_port" -s "${server_host}:${port}${ws_path}" -p "$(config_n_get $node password)" + ;; + ssr) + lua $UTIL_SS gen_config -node $node -local_addr "0.0.0.0" -local_port $socks_port -server_host $server_host -server_port $port > $config_file + ln_run "$(first_type ssr-local)" "ssr-local" $log_file -c "$config_file" -v -u + ;; + ss) + lua $UTIL_SS gen_config -node $node -local_addr "0.0.0.0" -local_port $socks_port -server_host $server_host -server_port $port -mode tcp_and_udp > $config_file + ln_run "$(first_type ss-local)" "ss-local" $log_file -c "$config_file" -v + ;; + ss-rust) + [ "$http_port" != "0" ] && { + http_flag=1 + config_file=$(echo $config_file | sed "s/SOCKS/HTTP_SOCKS/g") + local _extra_param="-local_http_port $http_port" + } + lua $UTIL_SS gen_config -node $node -local_socks_port $socks_port -server_host $server_host -server_port $port ${_extra_param} > $config_file + ln_run "$(first_type sslocal)" "sslocal" $log_file -c "$config_file" -v + ;; + hysteria) + [ "$http_port" != "0" ] && { + http_flag=1 + config_file=$(echo $config_file | sed "s/SOCKS/HTTP_SOCKS/g") + local _extra_param="-local_http_port $http_port" + } + lua $UTIL_HYSTERIA gen_config -node $node -local_socks_port $socks_port -server_host $server_host -server_port $port ${_extra_param} > $config_file + ln_run "$(first_type $(config_t_get global_app hysteria_file))" "hysteria" $log_file -c "$config_file" client + ;; + esac + + # http to socks + [ -z "$http_flag" ] && [ "$http_port" != "0" ] && [ -n "$http_config_file" ] && [ "$type" != "v2ray" ] && [ "$type" != "xray" ] && [ "$type" != "socks" ] && { + local bin=$(first_type $(config_t_get global_app v2ray_file) v2ray) + if [ -n "$bin" ]; then + type="v2ray" + else + bin=$(first_type $(config_t_get global_app xray_file) xray) + [ -n "$bin" ] && type="xray" + fi + [ -z "$type" ] && return 1 + lua $UTIL_XRAY gen_proto_config -local_http_port $http_port -server_proto socks -server_address "127.0.0.1" -server_port $socks_port -server_username $_username -server_password $_password > $http_config_file + ln_run "$bin" ${type} /dev/null run -c "$http_config_file" + } + unset http_flag +} + +node_switch() { + local flag new_node shunt_logic + eval_set_val $@ + [ -n "$flag" ] && [ -n "$new_node" ] && { + pgrep -af "$TMP_BIN_PATH" | awk -v P1="${flag}" 'BEGIN{IGNORECASE=1}$0~P1 && !/acl\/|acl_/{print $1}' | xargs kill -9 >/dev/null 2>&1 + rm -rf $TMP_PATH/${flag}* + [ "$shunt_logic" != "0" ] && { + local node=$(config_t_get global node nil) + [ "$(config_n_get $node protocol nil)" = "_shunt" ] && { + if [ "$shunt_logic" = "1" ]; then + uci set $CONFIG.$node.default_node="$new_node" + elif [ "$shunt_logic" = "2" ]; then + uci set $CONFIG.$node.main_node="$new_node" + fi + uci commit $CONFIG + } + new_node=$node + } + + [ -s "$TMP_SCRIPT_FUNC_PATH/_${flag}" ] && { + for filename in $(ls ${TMP_SCRIPT_FUNC_PATH} | grep -v "^_"); do + cmd=$(cat ${TMP_SCRIPT_FUNC_PATH}/${filename}) + [ -n "$(echo $cmd | grep "${flag}")" ] && rm -f ${TMP_SCRIPT_FUNC_PATH}/${filename} + done + local script_func=$(cat $TMP_SCRIPT_FUNC_PATH/_${flag}) + local now_node_arg=$(echo $script_func | grep -o -E "node=.*" | awk -F ' ' '{print $1}') + new_script_func=$(echo $script_func | sed "s#${now_node_arg}#node=${new_node}#g") + ${new_script_func} + echo $new_node > $TMP_ID_PATH/${flag} + + [ "$shunt_logic" != "0" ] && [ "$(config_n_get $new_node protocol nil)" = "_shunt" ] && { + echo $(config_n_get $new_node default_node nil) > $TMP_ID_PATH/${flag}_default + echo $(config_n_get $new_node main_node nil) > $TMP_ID_PATH/${flag}_main + uci commit $CONFIG + } + + #uci set $CONFIG.@global[0].node=$node + #uci commit $CONFIG + source $APP_PATH/helper_dnsmasq.sh logic_restart no_log=1 + } + } +} + +run_global() { + [ "$NODE" = "nil" ] && return 1 + TYPE=$(echo $(config_n_get $NODE type nil) | tr 'A-Z' 'a-z') + [ "$TYPE" = "nil" ] && return 1 + echo $REDIR_PORT > $TMP_PORT_PATH/global + echo $NODE > $TMP_ID_PATH/global + [ "$(config_n_get $NODE protocol nil)" = "_shunt" ] && { + local default_node=$(config_n_get $NODE default_node nil) + local main_node=$(config_n_get $NODE main_node nil) + echo $default_node > $TMP_ID_PATH/global_default + echo $main_node > $TMP_ID_PATH/global_main + } + + if [ $PROXY_IPV6 == "1" ]; then + echolog "开启实验性IPv6透明代理(TProxy),请确认您的节点及类型支持IPv6!" + PROXY_IPV6_UDP=1 + fi + V2RAY_ARGS="flag=global node=$NODE redir_port=$REDIR_PORT" + V2RAY_ARGS="${V2RAY_ARGS} dns_listen_port=${TUN_DNS_PORT} direct_dns_query_strategy=${DIRECT_DNS_QUERY_STRATEGY} remote_dns_query_strategy=${REMOTE_DNS_QUERY_STRATEGY} dns_cache=${DNS_CACHE}" + local msg="${TUN_DNS} (" + [ -n "$DIRECT_DNS_PROTOCOL" ] && { + V2RAY_ARGS="${V2RAY_ARGS} direct_dns_protocol=${DIRECT_DNS_PROTOCOL}" + case "$DIRECT_DNS_PROTOCOL" in + auto) + msg="${msg} 直连DNS:${AUTO_DNS}" + ;; + udp) + LOCAL_DNS=${DIRECT_DNS} + V2RAY_ARGS="${V2RAY_ARGS} direct_dns_udp_server=${DIRECT_DNS}" + msg="${msg} 直连DNS:${DIRECT_DNS}" + ;; + tcp) + V2RAY_ARGS="${V2RAY_ARGS} direct_dns_tcp_server=${DIRECT_DNS}" + msg="${msg} 直连DNS:${DIRECT_DNS}" + ;; + doh) + DIRECT_DNS_DOH=$(config_t_get global direct_dns_doh "https://223.5.5.5/dns-query") + V2RAY_ARGS="${V2RAY_ARGS} direct_dns_doh=${DIRECT_DNS_DOH}" + msg="${msg} 直连DNS:${DIRECT_DNS_DOH}" + ;; + esac + local _direct_dns_client_ip=$(config_t_get global direct_dns_client_ip) + [ -n "${_direct_dns_client_ip}" ] && V2RAY_ARGS="${V2RAY_ARGS} direct_dns_client_ip=${_direct_dns_client_ip}" + } + + [ -n "$REMOTE_DNS_PROTOCOL" ] && { + V2RAY_ARGS="${V2RAY_ARGS} remote_dns_protocol=${REMOTE_DNS_PROTOCOL}" + case "$REMOTE_DNS_PROTOCOL" in + udp*) + V2RAY_ARGS="${V2RAY_ARGS} remote_dns_udp_server=${REMOTE_DNS}" + msg="${msg} 远程DNS:${REMOTE_DNS}" + ;; + tcp) + V2RAY_ARGS="${V2RAY_ARGS} remote_dns_tcp_server=${REMOTE_DNS}" + msg="${msg} 远程DNS:${REMOTE_DNS}" + ;; + doh) + REMOTE_DNS_DOH=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") + V2RAY_ARGS="${V2RAY_ARGS} remote_dns_doh=${REMOTE_DNS_DOH}" + msg="${msg} 远程DNS:${REMOTE_DNS_DOH}" + ;; + esac + [ "$REMOTE_FAKEDNS" = "1" ] && { + V2RAY_ARGS="${V2RAY_ARGS} remote_fakedns=1" + msg="${msg} + FakeDNS " + } + + local _remote_dns_client_ip=$(config_t_get global remote_dns_client_ip) + [ -n "${_remote_dns_client_ip}" ] && V2RAY_ARGS="${V2RAY_ARGS} remote_dns_client_ip=${_remote_dns_client_ip}" + } + msg="${msg})" + echolog ${msg} + + source $APP_PATH/helper_dnsmasq.sh stretch + source $APP_PATH/helper_dnsmasq.sh add TMP_DNSMASQ_PATH=$TMP_DNSMASQ_PATH DNSMASQ_CONF_FILE=/tmp/dnsmasq.d/dnsmasq-passwall2.conf DEFAULT_DNS=$AUTO_DNS LOCAL_DNS=$LOCAL_DNS TUN_DNS=$TUN_DNS NFTFLAG=${nftflag:-0} + + V2RAY_CONFIG=$TMP_PATH/global.json + V2RAY_LOG=$TMP_PATH/global.log + [ "$(config_t_get global close_log 1)" = "1" ] && V2RAY_LOG="/dev/null" + V2RAY_ARGS="${V2RAY_ARGS} log_file=${V2RAY_LOG} config_file=${V2RAY_CONFIG}" + + node_socks_port=$(config_t_get global node_socks_port 1070) + V2RAY_ARGS="${V2RAY_ARGS} socks_port=${node_socks_port}" + echo "127.0.0.1:$node_socks_port" > $TMP_PATH/global_SOCKS_server + + node_http_port=$(config_t_get global node_http_port 0) + [ "$node_http_port" != "0" ] && V2RAY_ARGS="${V2RAY_ARGS} http_port=${node_http_port}" + + run_v2ray $V2RAY_ARGS + echo "run_v2ray $V2RAY_ARGS" > $TMP_SCRIPT_FUNC_PATH/_global +} + +start_socks() { + [ "$SOCKS_ENABLED" = "1" ] && { + local ids=$(uci show $CONFIG | grep "=socks" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}') + [ -n "$ids" ] && { + echolog "分析 Socks 服务的节点配置..." + for id in $ids; do + local enabled=$(config_n_get $id enabled 0) + [ "$enabled" == "0" ] && continue + local node=$(config_n_get $id node nil) + [ "$node" == "nil" ] && continue + local port=$(config_n_get $id port) + local config_file="SOCKS_${id}.json" + local log_file="SOCKS_${id}.log" + local http_port=$(config_n_get $id http_port 0) + local http_config_file="HTTP2SOCKS_${id}.json" + run_socks flag=$id node=$node bind=0.0.0.0 socks_port=$port config_file=$config_file http_port=$http_port http_config_file=$http_config_file + echo $node > $TMP_ID_PATH/SOCKS_${id} + done + } + } +} + +clean_log() { + logsnum=$(cat $LOG_FILE 2>/dev/null | wc -l) + [ "$logsnum" -gt 1000 ] && { + echo "" > $LOG_FILE + echolog "日志文件过长,清空处理!" + } +} + +clean_crontab() { + touch /etc/crontabs/root + #sed -i "/${CONFIG}/d" /etc/crontabs/root >/dev/null 2>&1 + sed -i "/$(echo "/etc/init.d/${CONFIG}" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1 + sed -i "/$(echo "lua ${APP_PATH}/rule_update.lua log" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1 + sed -i "/$(echo "lua ${APP_PATH}/subscribe.lua start" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1 +} + +start_crontab() { + clean_crontab + [ "$ENABLED" != 1 ] && { + /etc/init.d/cron restart + return + } + auto_on=$(config_t_get global_delay auto_on 0) + if [ "$auto_on" = "1" ]; then + time_off=$(config_t_get global_delay time_off) + time_on=$(config_t_get global_delay time_on) + time_restart=$(config_t_get global_delay time_restart) + [ -z "$time_off" -o "$time_off" != "nil" ] && { + echo "0 $time_off * * * /etc/init.d/$CONFIG stop" >>/etc/crontabs/root + echolog "配置定时任务:每天 $time_off 点关闭服务。" + } + [ -z "$time_on" -o "$time_on" != "nil" ] && { + echo "0 $time_on * * * /etc/init.d/$CONFIG start" >>/etc/crontabs/root + echolog "配置定时任务:每天 $time_on 点开启服务。" + } + [ -z "$time_restart" -o "$time_restart" != "nil" ] && { + echo "0 $time_restart * * * /etc/init.d/$CONFIG restart" >>/etc/crontabs/root + echolog "配置定时任务:每天 $time_restart 点重启服务。" + } + fi + + autoupdate=$(config_t_get global_rules auto_update) + weekupdate=$(config_t_get global_rules week_update) + dayupdate=$(config_t_get global_rules time_update) + if [ "$autoupdate" = "1" ]; then + local t="0 $dayupdate * * $weekupdate" + [ "$weekupdate" = "7" ] && t="0 $dayupdate * * *" + echo "$t lua $APP_PATH/rule_update.lua log > /dev/null 2>&1 &" >>/etc/crontabs/root + echolog "配置定时任务:自动更新规则。" + fi + + TMP_SUB_PATH=$TMP_PATH/sub_crontabs + mkdir -p $TMP_SUB_PATH + for item in $(uci show ${CONFIG} | grep "=subscribe_list" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do + if [ "$(config_n_get $item auto_update 0)" = "1" ]; then + cfgid=$(uci show ${CONFIG}.$item | head -n 1 | cut -d '.' -sf 2 | cut -d '=' -sf 1) + remark=$(config_n_get $item remark) + week_update=$(config_n_get $item week_update) + time_update=$(config_n_get $item time_update) + echo "$cfgid" >> $TMP_SUB_PATH/${week_update}_${time_update} + echolog "配置定时任务:自动更新【$remark】订阅。" + fi + done + + [ -d "${TMP_SUB_PATH}" ] && { + for name in $(ls ${TMP_SUB_PATH}); do + week_update=$(echo $name | awk -F '_' '{print $1}') + time_update=$(echo $name | awk -F '_' '{print $2}') + local t="0 $time_update * * $week_update" + [ "$week_update" = "7" ] && t="0 $time_update * * *" + cfgids=$(echo -n $(cat ${TMP_SUB_PATH}/${name}) | sed 's# #,#g') + echo "$t lua $APP_PATH/subscribe.lua start $cfgids > /dev/null 2>&1 &" >>/etc/crontabs/root + done + rm -rf $TMP_SUB_PATH + } + + if [ "$ENABLED_DEFAULT_ACL" == 1 ] || [ "$ENABLED_ACLS" == 1 ]; then + start_daemon=$(config_t_get global_delay start_daemon 0) + [ "$start_daemon" = "1" ] && $APP_PATH/monitor.sh > /dev/null 2>&1 & + + AUTO_SWITCH_ENABLE=$(config_t_get auto_switch enable 0) + [ "$AUTO_SWITCH_ENABLE" = "1" ] && $APP_PATH/test.sh > /dev/null 2>&1 & + else + echolog "运行于非代理模式,仅允许服务启停的定时任务。" + fi + + /etc/init.d/cron restart +} + +stop_crontab() { + clean_crontab + /etc/init.d/cron restart + #echolog "清除定时执行命令。" +} + +add_ip2route() { + local ip=$(get_host_ip "ipv4" $1) + [ -z "$ip" ] && { + echolog " - 无法解析[${1}],路由表添加失败!" + return 1 + } + local remarks="${1}" + [ "$remarks" != "$ip" ] && remarks="${1}(${ip})" + + . /lib/functions/network.sh + local gateway device + network_get_gateway gateway "$2" + network_get_device device "$2" + [ -z "${device}" ] && device="$2" + + if [ -n "${gateway}" ]; then + route add -host ${ip} gw ${gateway} dev ${device} >/dev/null 2>&1 + echo "$ip" >> $TMP_ROUTE_PATH/${device} + echolog " - [${remarks}]添加到接口[${device}]路由表成功!" + else + echolog " - [${remarks}]添加到接口[${device}]路由表失功!原因是找不到[${device}]网关。" + fi +} + +delete_ip2route() { + [ -d "${TMP_ROUTE_PATH}" ] && { + for interface in $(ls ${TMP_ROUTE_PATH}); do + for ip in $(cat ${TMP_ROUTE_PATH}/${interface}); do + route del -host ${ip} dev ${interface} >/dev/null 2>&1 + done + done + } +} + +start_haproxy() { + [ "$(config_t_get global_haproxy balancing_enable 0)" != "1" ] && return + haproxy_path=${TMP_PATH}/haproxy + haproxy_conf="config.cfg" + lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} -dns ${LOCAL_DNS} + ln_run "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}" +} + +run_ipset_dnsmasq() { + local listen_port server_dns ipset nftset cache_size dns_forward_max config_file + eval_set_val $@ + cat <<-EOF > $config_file + port=${listen_port} + server=${server_dns} + no-poll + no-resolv + cache-size=${cache_size:-0} + dns-forward-max=${dns_forward_max:-1000} + EOF + [ -n "${ipset}" ] && echo "ipset=${ipset}" >> $config_file + [ -n "${nftset}" ] && echo "nftset=${nftset}" >> $config_file + ln_run "$(first_type dnsmasq)" "dnsmasq" "/dev/null" -C $config_file +} + +kill_all() { + kill -9 $(pidof "$@") >/dev/null 2>&1 +} + +acl_app() { + local items=$(uci show ${CONFIG} | grep "=acl_rule" | cut -d '.' -sf 2 | cut -d '=' -sf 1) + [ -n "$items" ] && { + local index=0 + local item + local redir_port dns_port dnsmasq_port + local ipt_tmp msg msg2 + redir_port=11200 + dns_port=11300 + dnsmasq_port=11400 + echolog "访问控制:" + for item in $items; do + index=$(expr $index + 1) + local enabled sid remarks sources node direct_dns_protocol direct_dns direct_dns_doh direct_dns_client_ip direct_dns_query_strategy remote_dns_protocol remote_dns remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy + local _ip _mac _iprange _ipset _ip_or_mac rule_list config_file + sid=$(uci -q show "${CONFIG}.${item}" | grep "=acl_rule" | awk -F '=' '{print $1}' | awk -F '.' '{print $2}') + eval $(uci -q show "${CONFIG}.${item}" | cut -d'.' -sf 3-) + [ "$enabled" = "1" ] || continue + + [ -z "${sources}" ] && continue + for s in $sources; do + is_iprange=$(lua_api "iprange(\"${s}\")") + if [ "${is_iprange}" = "true" ]; then + rule_list="${rule_list}\niprange:${s}" + elif [ -n "$(echo ${s} | grep '^ipset:')" ]; then + rule_list="${rule_list}\nipset:${s}" + else + _ip_or_mac=$(lua_api "ip_or_mac(\"${s}\")") + if [ "${_ip_or_mac}" = "ip" ]; then + rule_list="${rule_list}\nip:${s}" + elif [ "${_ip_or_mac}" = "mac" ]; then + rule_list="${rule_list}\nmac:${s}" + fi + fi + done + [ -z "${rule_list}" ] && continue + mkdir -p $TMP_ACL_PATH/$sid + echo -e "${rule_list}" | sed '/^$/d' > $TMP_ACL_PATH/$sid/rule_list + + tcp_proxy_mode="global" + udp_proxy_mode="global" + node=${node:-default} + direct_dns_protocol=${direct_dns_protocol:-auto} + direct_dns=${direct_dns:-119.29.29.29} + [ "$direct_dns_protocol" = "doh" ] && direct_dns=${direct_dns_doh:-https://223.5.5.5/dns-query} + direct_dns_query_strategy=${direct_dns_query_strategy:-UseIP} + remote_dns_protocol=${remote_dns_protocol:-tcp} + remote_dns=${remote_dns:-1.1.1.1} + [ "$remote_dns_protocol" = "doh" ] && remote_dns=${remote_dns_doh:-https://1.1.1.1/dns-query} + remote_fakedns=${remote_fakedns:-0} + remote_dns_query_strategy=${remote_dns_query_strategy:-UseIPv4} + + [ "$node" != "nil" ] && { + if [ "$node" = "default" ]; then + node=$NODE + redir_port=$REDIR_PORT + else + [ "$(config_get_type $node nil)" = "nodes" ] && { + if [ "$node" = "$NODE" ]; then + redir_port=$REDIR_PORT + else + redir_port=$(get_new_port $(expr $redir_port + 1)) + eval node_${node}_redir_port=$redir_port + + local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + if [ -n "${type}" ]; then + config_file=$TMP_ACL_PATH/${node}_TCP_UDP_DNS_${redir_port}.json + dns_port=$(get_new_port $(expr $dns_port + 1)) + local acl_socks_port=$(get_new_port $(expr $redir_port + $index)) + run_v2ray flag=acl_$sid node=$node redir_port=$redir_port socks_address=127.0.0.1 socks_port=$acl_socks_port dns_listen_port=${dns_port} direct_dns_protocol=${direct_dns_protocol} direct_dns_udp_server=${direct_dns} direct_dns_tcp_server=${direct_dns} direct_dns_doh="${direct_dns}" direct_dns_client_ip=${direct_dns_client_ip} direct_dns_query_strategy=${direct_dns_query_strategy} remote_dns_protocol=${remote_dns_protocol} remote_dns_tcp_server=${remote_dns} remote_dns_udp_server=${remote_dns} remote_dns_doh="${remote_dns}" remote_dns_client_ip=${remote_dns_client_ip} remote_fakedns=${remote_fakedns} remote_dns_query_strategy=${remote_dns_query_strategy} config_file=${config_file} + fi + dnsmasq_port=$(get_new_port $(expr $dnsmasq_port + 1)) + redirect_dns_port=$dnsmasq_port + mkdir -p $TMP_ACL_PATH/$sid/dnsmasq.d + default_dnsmasq_cfgid=$(uci show dhcp.@dnsmasq[0] | awk -F '.' '{print $2}' | awk -F '=' '{print $1}'| head -1) + [ -s "/tmp/etc/dnsmasq.conf.${default_dnsmasq_cfgid}" ] && { + cp -r /tmp/etc/dnsmasq.conf.${default_dnsmasq_cfgid} $TMP_ACL_PATH/$sid/dnsmasq.conf + sed -i "/ubus/d" $TMP_ACL_PATH/$sid/dnsmasq.conf + sed -i "/dhcp/d" $TMP_ACL_PATH/$sid/dnsmasq.conf + sed -i "/port=/d" $TMP_ACL_PATH/$sid/dnsmasq.conf + sed -i "/conf-dir/d" $TMP_ACL_PATH/$sid/dnsmasq.conf + sed -i "/no-poll/d" $TMP_ACL_PATH/$sid/dnsmasq.conf + sed -i "/no-resolv/d" $TMP_ACL_PATH/$sid/dnsmasq.conf + } + echo "port=${dnsmasq_port}" >> $TMP_ACL_PATH/$sid/dnsmasq.conf + echo "conf-dir=${TMP_ACL_PATH}/${sid}/dnsmasq.d" >> $TMP_ACL_PATH/$sid/dnsmasq.conf + echo "server=127.0.0.1#${dns_port}" >> $TMP_ACL_PATH/$sid/dnsmasq.conf + echo "no-poll" >> $TMP_ACL_PATH/$sid/dnsmasq.conf + echo "no-resolv" >> $TMP_ACL_PATH/$sid/dnsmasq.conf + #source $APP_PATH/helper_dnsmasq.sh add TMP_DNSMASQ_PATH=$TMP_ACL_PATH/$sid/dnsmasq.d DNSMASQ_CONF_FILE=/dev/null DEFAULT_DNS=$AUTO_DNS TUN_DNS=127.0.0.1#${dns_port} NFTFLAG=${nftflag:-0} NO_LOGIC_LOG=1 + ln_run "$(first_type dnsmasq)" "dnsmasq_${sid}" "/dev/null" -C $TMP_ACL_PATH/$sid/dnsmasq.conf -x $TMP_ACL_PATH/$sid/dnsmasq.pid + eval node_${node}_$(echo -n "${tcp_proxy_mode}${remote_dns}" | md5sum | cut -d " " -f1)=${dnsmasq_port} + filter_node $node TCP > /dev/null 2>&1 & + filter_node $node UDP > /dev/null 2>&1 & + fi + echo "${node}" > $TMP_ACL_PATH/$sid/var_node + } + fi + echo "${redir_port}" > $TMP_ACL_PATH/$sid/var_port + } + [ -n "$redirect_dns_port" ] && echo "${redirect_dns_port}" > $TMP_ACL_PATH/$sid/var_redirect_dns_port + unset enabled sid remarks sources node direct_dns_protocol direct_dns direct_dns_doh direct_dns_client_ip direct_dns_query_strategy remote_dns_protocol remote_dns remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy + unset _ip _mac _iprange _ipset _ip_or_mac rule_list config_file + unset redirect_dns_port + done + unset redir_port dns_port dnsmasq_port + } +} + +start() { + pgrep -f /tmp/etc/passwall2/bin > /dev/null 2>&1 && { + echolog "程序已启动,先停止再重新启动!" + stop + } + + ulimit -n 65535 + start_haproxy + start_socks + nftflag=0 + local use_nft=$(config_t_get global_forwarding use_nft 0) + local USE_TABLES + if [ "$use_nft" == 1 ] && [ -z "$(dnsmasq --version | grep 'Compile time options:.* nftset')" ]; then + echolog "Dnsmasq软件包不满足nftables透明代理要求,如需使用请确保dnsmasq版本在2.87以上并开启nftset支持。" + elif [ "$use_nft" == 1 ] && [ -n "$(dnsmasq --version | grep 'Compile time options:.* nftset')" ]; then + USE_TABLES="nftables" + nftflag=1 + elif [ -z "$(command -v iptables-legacy || command -v iptables)" ] || [ -z "$(command -v ipset)" ] || [ -z "$(dnsmasq --version | grep 'Compile time options:.* ipset')" ]; then + echolog "系统未安装iptables或ipset或Dnsmasq没有开启ipset支持,无法透明代理!" + else + USE_TABLES="iptables" + fi + + [ "$ENABLED_DEFAULT_ACL" == 1 ] && run_global + [ -n "$USE_TABLES" ] && source $APP_PATH/${USE_TABLES}.sh start + [ "$ENABLED_DEFAULT_ACL" == 1 ] && source $APP_PATH/helper_dnsmasq.sh logic_restart + if [ "$ENABLED_DEFAULT_ACL" == 1 ] || [ "$ENABLED_ACLS" == 1 ]; then + bridge_nf_ipt=$(sysctl -e -n net.bridge.bridge-nf-call-iptables) + echo -n $bridge_nf_ipt > $TMP_PATH/bridge_nf_ipt + sysctl -w net.bridge.bridge-nf-call-iptables=0 >/dev/null 2>&1 + [ "$PROXY_IPV6" == "1" ] && { + bridge_nf_ip6t=$(sysctl -e -n net.bridge.bridge-nf-call-ip6tables) + echo -n $bridge_nf_ip6t > $TMP_PATH/bridge_nf_ip6t + sysctl -w net.bridge.bridge-nf-call-ip6tables=0 >/dev/null 2>&1 + } + fi + start_crontab + echolog "运行完成!\n" +} + +stop() { + clean_log + [ -n "$($(source $APP_PATH/iptables.sh get_ipt_bin) -t mangle -t nat -L -nv 2>/dev/null | grep "PSW2")" ] && source $APP_PATH/iptables.sh stop + [ -n "$(nft list sets 2>/dev/null | grep "${CONFIG}_")" ] && source $APP_PATH/nftables.sh stop + delete_ip2route + kill_all v2ray-plugin obfs-local + pgrep -f "sleep.*(6s|9s|58s)" | xargs kill -9 >/dev/null 2>&1 + pgrep -af "${CONFIG}/" | awk '! /app\.sh|subscribe\.lua|rule_update\.lua/{print $1}' | xargs kill -9 >/dev/null 2>&1 + unset V2RAY_LOCATION_ASSET + unset XRAY_LOCATION_ASSET + stop_crontab + source $APP_PATH/helper_dnsmasq.sh del + source $APP_PATH/helper_dnsmasq.sh restart no_log=1 + [ -s "$TMP_PATH/bridge_nf_ipt" ] && sysctl -w net.bridge.bridge-nf-call-iptables=$(cat $TMP_PATH/bridge_nf_ipt) >/dev/null 2>&1 + [ -s "$TMP_PATH/bridge_nf_ip6t" ] && sysctl -w net.bridge.bridge-nf-call-ip6tables=$(cat $TMP_PATH/bridge_nf_ip6t) >/dev/null 2>&1 + rm -rf ${TMP_PATH} + rm -rf /tmp/lock/${CONFIG}_script.lock + echolog "清空并关闭相关程序和缓存完成。" + exit 0 +} + +ENABLED=$(config_t_get global enabled 0) +NODE=$(config_t_get global node nil) +[ "$ENABLED" == 1 ] && { + [ "$NODE" != "nil" ] && [ "$(config_get_type $NODE nil)" != "nil" ] && ENABLED_DEFAULT_ACL=1 +} +ENABLED_ACLS=$(config_t_get global acl_enable 0) +[ "$ENABLED_ACLS" == 1 ] && { + [ "$(uci show ${CONFIG} | grep "@acl_rule" | grep "enabled='1'" | wc -l)" == 0 ] && ENABLED_ACLS=0 +} +SOCKS_ENABLED=$(config_t_get global socks_enabled 0) +REDIR_PORT=$(echo $(get_new_port 1041 tcp,udp)) +tcp_proxy_way=$(config_t_get global_forwarding tcp_proxy_way redirect) +TCP_NO_REDIR_PORTS=$(config_t_get global_forwarding tcp_no_redir_ports 'disable') +UDP_NO_REDIR_PORTS=$(config_t_get global_forwarding udp_no_redir_ports 'disable') +TCP_REDIR_PORTS=$(config_t_get global_forwarding tcp_redir_ports '22,25,53,143,465,587,853,993,995,80,443') +UDP_REDIR_PORTS=$(config_t_get global_forwarding udp_redir_ports '1:65535') +TCP_PROXY_MODE="global" +UDP_PROXY_MODE="global" +LOCALHOST_PROXY=$(config_t_get global localhost_proxy '1') +DIRECT_DNS_PROTOCOL=$(config_t_get global direct_dns_protocol tcp) +DIRECT_DNS=$(config_t_get global direct_dns 119.29.29.29:53 | sed 's/#/:/g' | sed -E 's/\:([^:]+)$/#\1/g') +DIRECT_DNS_QUERY_STRATEGY=$(config_t_get global direct_dns_query_strategy UseIP) +REMOTE_DNS_PROTOCOL=$(config_t_get global remote_dns_protocol tcp) +REMOTE_DNS=$(config_t_get global remote_dns 1.1.1.1:53 | sed 's/#/:/g' | sed -E 's/\:([^:]+)$/#\1/g') +REMOTE_FAKEDNS=$(config_t_get global remote_fakedns '0') +REMOTE_DNS_QUERY_STRATEGY=$(config_t_get global remote_dns_query_strategy UseIPv4) +DNS_CACHE=$(config_t_get global dns_cache 1) + +RESOLVFILE=/tmp/resolv.conf.d/resolv.conf.auto +[ -f "${RESOLVFILE}" ] && [ -s "${RESOLVFILE}" ] || RESOLVFILE=/tmp/resolv.conf.auto + +ISP_DNS=$(cat $RESOLVFILE 2>/dev/null | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | sort -u | grep -v 0.0.0.0 | grep -v 127.0.0.1) +ISP_DNS6=$(cat $RESOLVFILE 2>/dev/null | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | awk -F % '{print $1}' | awk -F " " '{print $2}'| sort -u | grep -v -Fx ::1 | grep -v -Fx ::) + +DEFAULT_DNS=$(uci show dhcp | grep "@dnsmasq" | grep "\.server=" | awk -F '=' '{print $2}' | sed "s/'//g" | tr ' ' '\n' | grep -v "\/" | head -2 | sed ':label;N;s/\n/,/;b label') +[ -z "${DEFAULT_DNS}" ] && DEFAULT_DNS=$(echo -n $ISP_DNS | tr ' ' '\n' | head -2 | tr '\n' ',') +AUTO_DNS=${DEFAULT_DNS:-119.29.29.29} + +PROXY_IPV6=$(config_t_get global_forwarding ipv6_tproxy 0) + +export V2RAY_LOCATION_ASSET=$(config_t_get global_rules v2ray_location_asset "/usr/share/v2ray/") +export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET +mkdir -p /tmp/etc $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ID_PATH $TMP_PORT_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2 + +arg1=$1 +shift +case $arg1 in +add_ip2route) + add_ip2route $@ + ;; +get_new_port) + get_new_port $@ + ;; +run_v2ray) + run_v2ray $@ + ;; +run_socks) + run_socks $@ + ;; +node_switch) + node_switch $@ + ;; +echolog) + echolog $@ + ;; +stop) + stop + ;; +start) + start + ;; +esac diff --git a/luci-app-passwall2/root/usr/share/passwall2/domains_excluded b/luci-app-passwall2/root/usr/share/passwall2/domains_excluded new file mode 100644 index 000000000..1346a4f9f --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/domains_excluded @@ -0,0 +1,24 @@ +courier.push.apple.com +rbsxbxp-mim.vivox.com +rbsxbxp.www.vivox.com +rbsxbxp-ws.vivox.com +rbspsxp.www.vivox.com +rbspsxp-mim.vivox.com +rbspsxp-ws.vivox.com +rbswxp.www.vivox.com +rbswxp-mim.vivox.com +disp-rbspsp-5-1.vivox.com +disp-rbsxbp-5-1.vivox.com +proxy.rbsxbp.vivox.com +proxy.rbspsp.vivox.com +proxy.rbswp.vivox.com +rbswp.vivox.com +rbsxbp.vivox.com +rbspsp.vivox.com +rbspsp.www.vivox.com +rbswp.www.vivox.com +rbsxbp.www.vivox.com +rbsxbxp.vivox.com +rbspsxp.vivox.com +rbswxp.vivox.com +Mijia Cloud diff --git a/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua new file mode 100644 index 000000000..59fb75202 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua @@ -0,0 +1,219 @@ +#!/usr/bin/lua + +local api = require ("luci.passwall2.api") +local appname = api.appname +local fs = api.fs +local jsonc = api.jsonc +local uci = api.uci +local sys = api.sys + +local log = function(...) + api.log(...) +end + +function get_ip_port_from(str) + local result_port = sys.exec("echo -n " .. str .. " | sed -n 's/^.*[:#]\\([0-9]*\\)$/\\1/p'") + local result_ip = sys.exec(string.format("__host=%s;__varport=%s;", str, result_port) .. "echo -n ${__host%%${__varport:+[:#]${__varport}*}}") + return result_ip, result_port +end + +local new_port +local function get_new_port() + if new_port then + new_port = tonumber(sys.exec(string.format("echo -n $(/usr/share/%s/app.sh get_new_port %s tcp)", appname, new_port + 1))) + else + new_port = tonumber(sys.exec(string.format("echo -n $(/usr/share/%s/app.sh get_new_port auto tcp)", appname))) + end + return new_port +end + +local var = api.get_args(arg) +local haproxy_path = var["-path"] +local haproxy_conf = var["-conf"] +local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53" + +local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1" +local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp" +local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "10" + +log("HAPROXY 负载均衡...") +fs.mkdir(haproxy_path) +local haproxy_file = haproxy_path .. "/" .. haproxy_conf + +local f_out = io.open(haproxy_file, "a") + +local haproxy_config = [[ +global + daemon + log 127.0.0.1 local2 + maxconn 60000 + stats socket {{path}}/haproxy.sock + nbthread {{nbthread}} + external-check + insecure-fork-wanted + +defaults + mode tcp + log global + option tcplog + option dontlognull + option http-server-close + #option forwardfor except 127.0.0.0/8 + option redispatch + retries 2 + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 1m + timeout server 1m + timeout http-keep-alive 10s + timeout check 10s + maxconn 3000 + +resolvers mydns + resolve_retries 1 + timeout resolve 5s + hold valid 600s +{{dns}} +]] + +haproxy_config = haproxy_config:gsub("{{path}}", haproxy_path) +haproxy_config = haproxy_config:gsub("{{nbthread}}", cpu_thread) + +local mydns = "" +local index = 0 +string.gsub(haproxy_dns, '[^' .. "," .. ']+', function(w) + index = index + 1 + local s = w:gsub("#", ":") + if not s:find(":") then + s = s .. ":53" + end + mydns = mydns .. (index > 1 and "\n" or "") .. " " .. string.format("nameserver dns%s %s", index, s) +end) +haproxy_config = haproxy_config:gsub("{{dns}}", mydns) + +f_out:write(haproxy_config) + +local listens = {} + +uci:foreach(appname, "haproxy_config", function(t) + if t.enabled == "1" then + local server_remark + local server_address + local server_port + local lbss = t.lbss + local listen_port = tonumber(t.haproxy_port) or 0 + local server_node = uci:get_all(appname, lbss) + if server_node and server_node.address and server_node.port then + server_remark = server_node.address .. ":" .. server_node.port + server_address = server_node.address + server_port = server_node.port + t.origin_address = server_address + t.origin_port = server_port + if health_check_type == "passwall_logic" then + if server_node.type ~= "Socks" then + local relay_port = server_node.port + new_port = get_new_port() + local config_file = string.format("haproxy_%s_%s.json", t[".name"], new_port) + sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null', + appname, + string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s", + new_port, --flag + server_node[".name"], --node + "127.0.0.1", --bind + new_port, --socks port + config_file --config file + ) + ) + ) + server_address = "127.0.0.1" + server_port = new_port + end + end + else + server_address, server_port = get_ip_port_from(lbss) + server_remark = server_address .. ":" .. server_port + t.origin_address = server_address + t.origin_port = server_port + end + if server_address and server_port and listen_port > 0 then + if not listens[listen_port] then + listens[listen_port] = {} + end + t.server_remark = server_remark + t.server_address = server_address + t.server_port = server_port + table.insert(listens[listen_port], t) + else + log(" - 丢弃1个明显无效的节点") + end + end +end) + +local sortTable = {} +for i in pairs(listens) do + if i ~= nil then + table.insert(sortTable, i) + end +end +table.sort(sortTable, function(a,b) return (a < b) end) + +for i, port in pairs(sortTable) do + log(" + 入口 0.0.0.0:%s..." % port) + + f_out:write("\n" .. string.format([[ +listen %s + bind 0.0.0.0:%s + mode tcp + balance roundrobin +]], port, port)) + + if health_check_type == "passwall_logic" then + f_out:write(string.format([[ + option external-check + external-check command "/usr/share/passwall2/haproxy_check.sh" +]], port, port)) + end + + for i, o in ipairs(listens[port]) do + local remark = o.server_remark + local server = o.server_address .. ":" .. o.server_port + local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check inter {{inter}} rise 1 fall 3 {{backup}}" + server_conf = server_conf:gsub("{{remark}}", remark) + server_conf = server_conf:gsub("{{server}}", server) + server_conf = server_conf:gsub("{{weight}}", o.lbweight) + local resolvers = "resolvers mydns" + if api.is_ip(o.server_address) then + resolvers = "" + end + server_conf = server_conf:gsub("{{resolvers}}", resolvers) + server_conf = server_conf:gsub("{{inter}}", tonumber(health_check_inter) .. "s") + server_conf = server_conf:gsub("{{backup}}", o.backup == "1" and "backup" or "") + + f_out:write(" " .. server_conf .. "\n") + + if o.export ~= "0" then + sys.call(string.format("/usr/share/passwall2/app.sh add_ip2route %s %s", o.origin_address, o.export)) + end + + log(string.format(" | - 出口节点:%s:%s,权重:%s", o.origin_address, o.origin_port, o.lbweight)) + end +end + +--控制台配置 +local console_port = uci:get(appname, "@global_haproxy[0]", "console_port") +local console_user = uci:get(appname, "@global_haproxy[0]", "console_user") +local console_password = uci:get(appname, "@global_haproxy[0]", "console_password") +local str = [[ +listen console + bind 0.0.0.0:%s + mode http + stats refresh 30s + stats uri / + stats admin if TRUE + %s +]] +f_out:write("\n" .. string.format(str, console_port, (console_user and console_user ~= "" and console_password and console_password ~= "") and "stats auth " .. console_user .. ":" .. console_password or "")) +log(string.format(" * 控制台端口:%s", console_port)) + +f_out:close() diff --git a/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh b/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh new file mode 100755 index 000000000..870ffb575 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +listen_address=$1 +listen_port=$2 +server_address=$3 +server_port=$4 +status=$(/usr/bin/curl -I -o /dev/null -skL -x socks5h://${server_address}:${server_port} --connect-timeout 3 --retry 3 -w %{http_code} "https://www.google.com/generate_204") +case "$status" in + 204|\ + 200) + status=200 + ;; +esac +return_code=1 +if [ "$status" = "200" ]; then + return_code=0 +fi +exit ${return_code} diff --git a/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.sh b/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.sh new file mode 100755 index 000000000..f10453b04 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.sh @@ -0,0 +1,146 @@ +#!/bin/sh + +stretch() { + #zhenduiluanshezhiDNSderen + local dnsmasq_server=$(uci -q get dhcp.@dnsmasq[0].server) + local dnsmasq_noresolv=$(uci -q get dhcp.@dnsmasq[0].noresolv) + local _flag + for server in $dnsmasq_server; do + [ -z "$(echo $server | grep '\/')" ] && _flag=1 + done + [ -z "$_flag" ] && [ "$dnsmasq_noresolv" = "1" ] && { + uci -q delete dhcp.@dnsmasq[0].noresolv + uci -q set dhcp.@dnsmasq[0].resolvfile="$RESOLVFILE" + uci commit dhcp + } +} + +backup_servers() { + DNSMASQ_DNS=$(uci show dhcp | grep "@dnsmasq" | grep ".server=" | awk -F '=' '{print $2}' | sed "s/'//g" | tr ' ' ',') + if [ -n "${DNSMASQ_DNS}" ]; then + uci -q set $CONFIG.@global[0].dnsmasq_servers="${DNSMASQ_DNS}" + uci commit $CONFIG + fi +} + +restore_servers() { + OLD_SERVER=$(uci -q get $CONFIG.@global[0].dnsmasq_servers | tr "," " ") + for server in $OLD_SERVER; do + uci -q del_list dhcp.@dnsmasq[0].server=$server + uci -q add_list dhcp.@dnsmasq[0].server=$server + done + uci commit dhcp + uci -q delete $CONFIG.@global[0].dnsmasq_servers + uci commit $CONFIG +} + +logic_restart() { + local no_log + eval_set_val $@ + _LOG_FILE=$LOG_FILE + [ -n "$no_log" ] && LOG_FILE="/dev/null" + if [ -f "$TMP_PATH/default_DNS" ]; then + backup_servers + #sed -i "/list server/d" /etc/config/dhcp >/dev/null 2>&1 + for server in $(uci -q get dhcp.@dnsmasq[0].server); do + [ -n "$(echo $server | grep '\/')" ] || uci -q del_list dhcp.@dnsmasq[0].server="$server" + done + /etc/init.d/dnsmasq restart >/dev/null 2>&1 + restore_servers + else + /etc/init.d/dnsmasq restart >/dev/null 2>&1 + fi + echolog "重启 dnsmasq 服务" + LOG_FILE=${_LOG_FILE} +} + +restart() { + local no_log + eval_set_val $@ + _LOG_FILE=$LOG_FILE + [ -n "$no_log" ] && LOG_FILE="/dev/null" + /etc/init.d/dnsmasq restart >/dev/null 2>&1 + echolog "重启 dnsmasq 服务" + LOG_FILE=${_LOG_FILE} +} + +gen_items() { + local dnss settype setnames outf ipsetoutf + eval_set_val $@ + + awk -v dnss="${dnss}" -v settype="${settype}" -v setnames="${setnames}" -v outf="${outf}" -v ipsetoutf="${ipsetoutf}" ' + BEGIN { + if(outf == "") outf="/dev/stdout"; + if(ipsetoutf == "") ipsetoutf=outf; + split(dnss, dns, ","); setdns=length(dns)>0; setlist=length(setnames)>0; + if(setdns) for(i in dns) if(length(dns[i])==0) delete dns[i]; + fail=1; + } + ! /^$/&&!/^#/ { + fail=0 + if(setdns) for(i in dns) printf("server=/.%s/%s\n", $0, dns[i]) >>outf; + if(setlist) printf("%s=/.%s/%s\n", settype, $0, setnames) >>ipsetoutf; + } + END {fflush(outf); close(outf); fflush(ipsetoutf); close(ipsetoutf); exit(fail);} + ' +} + +add() { + local TMP_DNSMASQ_PATH DNSMASQ_CONF_FILE DEFAULT_DNS LOCAL_DNS TUN_DNS NFTFLAG NO_LOGIC_LOG + eval_set_val $@ + _LOG_FILE=$LOG_FILE + [ -n "$NO_LOGIC_LOG" ] && LOG_FILE="/dev/null" + mkdir -p "${TMP_DNSMASQ_PATH}" "${DNSMASQ_PATH}" "/tmp/dnsmasq.d" + + local set_type="ipset" + [ "${NFTFLAG}" = "1" ] && { + set_type="nftset" + local setflag_4="4#inet#fw4#" + local setflag_6="6#inet#fw4#" + } + + #始终用国内DNS解析节点域名 + servers=$(uci show "${CONFIG}" | grep ".address=" | cut -d "'" -f 2) + hosts_foreach "servers" host_from_url | grep '[a-zA-Z]$' | sort -u | gen_items settype="${set_type}" setnames="${setflag_4}passwall2_vpslist,${setflag_6}passwall2_vpslist6" dnss="${LOCAL_DNS:-${DEFAULT_DNS}}" outf="${TMP_DNSMASQ_PATH}/10-vpslist_host.conf" ipsetoutf="${TMP_DNSMASQ_PATH}/ipset.conf" + echolog " - [$?]节点列表中的域名(vpslist):${DEFAULT_DNS:-默认}" + + echo "conf-dir=${TMP_DNSMASQ_PATH}" > $DNSMASQ_CONF_FILE + [ -n "${TUN_DNS}" ] && { + echo "${DEFAULT_DNS}" > $TMP_PATH/default_DNS + cat <<-EOF >> $DNSMASQ_CONF_FILE + server=${TUN_DNS} + all-servers + no-poll + no-resolv + EOF + echolog " - [$?]默认:${TUN_DNS}" + } + LOG_FILE=${_LOG_FILE} +} + +del() { + rm -rf /tmp/dnsmasq.d/dnsmasq-$CONFIG.conf + rm -rf $DNSMASQ_PATH/dnsmasq-$CONFIG.conf + rm -rf $TMP_DNSMASQ_PATH +} + +arg1=$1 +shift +case $arg1 in +stretch) + stretch $@ + ;; +add) + add $@ + ;; +del) + del $@ + ;; +restart) + restart $@ + ;; +logic_restart) + logic_restart $@ + ;; +*) ;; +esac diff --git a/luci-app-passwall2/root/usr/share/passwall2/iptables.sh b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh new file mode 100755 index 000000000..540e7cb4c --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh @@ -0,0 +1,952 @@ +#!/bin/sh + +DIR="$(cd "$(dirname "$0")" && pwd)" +MY_PATH=$DIR/iptables.sh +IPSET_LANLIST="passwall2_lanlist" +IPSET_VPSLIST="passwall2_vpslist" +IPSET_WHITELIST="passwall2_whitelist" + +IPSET_LANLIST6="passwall2_lanlist6" +IPSET_VPSLIST6="passwall2_vpslist6" +IPSET_WHITELIST6="passwall2_whitelist6" + +FORCE_INDEX=2 + +. /lib/functions/network.sh + +ipt=$(command -v iptables-legacy || command -v iptables) +ip6t=$(command -v ip6tables-legacy || command -v ip6tables) + +ipt_n="$ipt -t nat -w" +ipt_m="$ipt -t mangle -w" +ip6t_n="$ip6t -t nat -w" +ip6t_m="$ip6t -t mangle -w" +[ -z "$ip6t" -o -z "$(lsmod | grep 'ip6table_nat')" ] && ip6t_n="eval #$ip6t_n" +[ -z "$ip6t" -o -z "$(lsmod | grep 'ip6table_mangle')" ] && ip6t_m="eval #$ip6t_m" +FWI=$(uci -q get firewall.passwall2.path 2>/dev/null) +FAKE_IP="198.18.0.0/16" +FAKE_IP_6="fc00::/18" + +factor() { + if [ -z "$1" ] || [ -z "$2" ]; then + echo "" + elif [ "$1" == "1:65535" ]; then + echo "" + else + echo "$2 $1" + fi +} + +dst() { + echo "-m set $2 --match-set $1 dst" +} + +comment() { + local name=$(echo $1 | sed 's/ /_/g') + echo "-m comment --comment '$name'" +} + +destroy_ipset() { + for i in "$@"; do + ipset -q -F $i + ipset -q -X $i + done +} + +insert_rule_before() { + [ $# -ge 3 ] || { + return 1 + } + local ipt_tmp="${1}"; shift + local chain="${1}"; shift + local keyword="${1}"; shift + local rule="${1}"; shift + local _index=$($ipt_tmp -n -L $chain --line-numbers 2>/dev/null | grep "$keyword" | head -n 1 | awk '{print $1}') + $ipt_tmp -I $chain $_index $rule +} + +insert_rule_after() { + [ $# -ge 3 ] || { + return 1 + } + local ipt_tmp="${1}"; shift + local chain="${1}"; shift + local keyword="${1}"; shift + local rule="${1}"; shift + local _index=$($ipt_tmp -n -L $chain --line-numbers 2>/dev/null | grep "$keyword" | awk 'END {print}' | awk '{print $1}') + _index=${_index:-0} + _index=$((_index + 1)) + $ipt_tmp -I $chain $_index $rule +} + +RULE_LAST_INDEX() { + [ $# -ge 3 ] || { + echolog "索引列举方式不正确(iptables),终止执行!" + return 1 + } + local ipt_tmp="${1}"; shift + local chain="${1}"; shift + local list="${1}"; shift + local default="${1:-0}"; shift + local _index=$($ipt_tmp -n -L $chain --line-numbers 2>/dev/null | grep "$list" | head -n 1 | awk '{print $1}') + echo "${_index:-${default}}" +} + +REDIRECT() { + local s="-j REDIRECT" + [ -n "$1" ] && { + local s="$s --to-ports $1" + [ "$2" == "MARK" ] && s="-j MARK --set-mark $1" + [ "$2" == "TPROXY" ] && { + local mark="-m mark --mark 1" + s="${mark} -j TPROXY --tproxy-mark 0x1/0x1 --on-port $1" + } + } + echo $s +} + +get_redirect_ipt() { + echo "$(REDIRECT $2 $3)" +} + +get_redirect_ip6t() { + echo "$(REDIRECT $2 $3)" +} + +get_action_chain_name() { + echo "全局代理" +} + +gen_lanlist() { + cat <<-EOF + 0.0.0.0/8 + 10.0.0.0/8 + 100.64.0.0/10 + 127.0.0.0/8 + 169.254.0.0/16 + 172.16.0.0/12 + 192.168.0.0/16 + 224.0.0.0/4 + 240.0.0.0/4 + EOF +} + +gen_lanlist_6() { + cat <<-EOF + ::/128 + ::1/128 + ::ffff:0:0/96 + ::ffff:0:0:0/96 + 64:ff9b::/96 + 100::/64 + 2001::/32 + 2001:20::/28 + 2001:db8::/32 + 2002::/16 + fc00::/7 + fe80::/10 + ff00::/8 + EOF +} + +get_wan_ip() { + local NET_IF + local NET_ADDR + + network_flush_cache + network_find_wan NET_IF + network_get_ipaddr NET_ADDR "${NET_IF}" + + echo $NET_ADDR +} + +get_wan6_ip() { + local NET_IF + local NET_ADDR + + network_flush_cache + network_find_wan6 NET_IF + network_get_ipaddr6 NET_ADDR "${NET_IF}" + + echo $NET_ADDR +} + +load_acl() { + [ "$ENABLED_ACLS" == 1 ] && { + acl_app + echolog "访问控制:" + for sid in $(ls -F ${TMP_ACL_PATH} | grep '/$' | awk -F '/' '{print $1}'); do + eval $(uci -q show "${CONFIG}.${sid}" | cut -d'.' -sf 3-) + + tcp_no_redir_ports=${tcp_no_redir_ports:-default} + udp_no_redir_ports=${udp_no_redir_ports:-default} + tcp_proxy_mode="global" + udp_proxy_mode="global" + node=${node:-default} + [ "$tcp_no_redir_ports" = "default" ] && tcp_no_redir_ports=$TCP_NO_REDIR_PORTS + [ "$udp_no_redir_ports" = "default" ] && udp_no_redir_ports=$UDP_NO_REDIR_PORTS + [ "$tcp_redir_ports" = "default" ] && tcp_redir_ports=$TCP_REDIR_PORTS + [ "$udp_redir_ports" = "default" ] && udp_redir_ports=$UDP_REDIR_PORTS + + node_remark=$(config_n_get $NODE remarks) + [ -s "${TMP_ACL_PATH}/${sid}/var_node" ] && node=$(cat ${TMP_ACL_PATH}/${sid}/var_node) + [ -s "${TMP_ACL_PATH}/${sid}/var_port" ] && redir_port=$(cat ${TMP_ACL_PATH}/${sid}/var_port) + [ -n "$node" ] && [ "$node" != "default" ] && node_remark=$(config_n_get $node remarks) + + for i in $(cat ${TMP_ACL_PATH}/${sid}/rule_list); do + if [ -n "$(echo ${i} | grep '^iprange:')" ]; then + _iprange=$(echo ${i} | sed 's#iprange:##g') + _ipt_source=$(factor ${_iprange} "-m iprange --src-range") + msg="备注【$remarks】,IP range【${_iprange}】," + elif [ -n "$(echo ${i} | grep '^ipset:')" ]; then + _ipset=$(echo ${i} | sed 's#ipset:##g') + _ipt_source="-m set --match-set ${_ipset} src" + msg="备注【$remarks】,IPset【${_ipset}】," + elif [ -n "$(echo ${i} | grep '^ip:')" ]; then + _ip=$(echo ${i} | sed 's#ip:##g') + _ipt_source=$(factor ${_ip} "-s") + msg="备注【$remarks】,IP【${_ip}】," + elif [ -n "$(echo ${i} | grep '^mac:')" ]; then + _mac=$(echo ${i} | sed 's#mac:##g') + _ipt_source=$(factor ${_mac} "-m mac --mac-source") + msg="备注【$remarks】,MAC【${_mac}】," + else + continue + fi + + ipt_tmp=$ipt_n + [ -n "${is_tproxy}" ] && ipt_tmp=$ipt_m + + [ -n "$redir_port" ] && { + if [ "$tcp_proxy_mode" != "disable" ]; then + [ -s "${TMP_ACL_PATH}/${sid}/var_redirect_dns_port" ] && $ipt_n -A PSW2_REDIRECT $(comment "$remarks") -p udp ${_ipt_source} --dport 53 -j REDIRECT --to-ports $(cat ${TMP_ACL_PATH}/${sid}/var_redirect_dns_port) + msg2="${msg}使用TCP节点[$node_remark] [$(get_action_chain_name $tcp_proxy_mode)]" + if [ -n "${is_tproxy}" ]; then + msg2="${msg2}(TPROXY:${redir_port})代理" + ipt_tmp=$ipt_m + else + msg2="${msg2}(REDIRECT:${redir_port})代理" + fi + + [ "$accept_icmp" = "1" ] && { + $ipt_n -A PSW2 $(comment "$remarks") -p icmp ${_ipt_source} -d $FAKE_IP $(REDIRECT) + $ipt_n -A PSW2 $(comment "$remarks") -p icmp ${_ipt_source} $(REDIRECT) + } + + [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + $ip6t_n -A PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source} -d $FAKE_IP_6 $(REDIRECT) 2>/dev/null + $ip6t_n -A PSW2 $(comment "$remarks") -p ipv6-icmp ${_ipt_source} $(REDIRECT) 2>/dev/null + } + + [ "$tcp_no_redir_ports" != "disable" ] && { + $ipt_tmp -A PSW2 $(comment "$remarks") ${_ipt_source} -p tcp -m multiport --dport $tcp_no_redir_ports -j RETURN + $ip6t_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p tcp -m multiport --dport $tcp_no_redir_ports -j RETURN 2>/dev/null + msg2="${msg2}[$?]除${tcp_no_redir_ports}外的" + } + msg2="${msg2}所有端口" + + if [ "${ipt_tmp}" = "${ipt_n}" ]; then + $ipt_n -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} -d $FAKE_IP $(REDIRECT $redir_port) + $ipt_n -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(factor $tcp_redir_ports "-m multiport --dport") $(REDIRECT $redir_port) + else + $ipt_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} -d $FAKE_IP -j PSW2_RULE + $ipt_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(factor $tcp_redir_ports "-m multiport --dport") -j PSW2_RULE + $ipt_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY) + fi + [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} -d $FAKE_IP_6 -j PSW2_RULE 2>/dev/null + $ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(factor $tcp_redir_ports "-m multiport --dport") -j PSW2_RULE 2>/dev/null + $ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY) 2>/dev/null + } + else + msg2="${msg}不代理TCP" + fi + echolog " - ${msg2}" + } + + $ipt_tmp -A PSW2 $(comment "$remarks") ${_ipt_source} -p tcp -j RETURN + $ip6t_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p tcp -j RETURN 2>/dev/null + + [ -n "$redir_port" ] && { + if [ "$udp_proxy_mode" != "disable" ]; then + msg2="${msg}使用UDP节点[$node_remark] [$(get_action_chain_name $udp_proxy_mode)]" + msg2="${msg2}(TPROXY:${redir_port})代理" + [ "$udp_no_redir_ports" != "disable" ] && { + $ipt_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p udp -m multiport --dport $udp_no_redir_ports -j RETURN + $ip6t_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p udp -m multiport --dport $udp_no_redir_ports -j RETURN 2>/dev/null + msg2="${msg2}[$?]除${udp_no_redir_ports}外的" + } + msg2="${msg2}所有端口" + + $ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} -d $FAKE_IP -j PSW2_RULE + $ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(factor $udp_redir_ports "-m multiport --dport") -j PSW2_RULE + $ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(REDIRECT $redir_port TPROXY) + + [ "$PROXY_IPV6" == "1" ] && [ "$PROXY_IPV6_UDP" == "1" ] && { + $ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} -d $FAKE_IP_6 -j PSW2_RULE 2>/dev/null + $ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(factor $udp_redir_ports "-m multiport --dport") -j PSW2_RULE 2>/dev/null + $ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(REDIRECT $redir_port TPROXY) 2>/dev/null + } + else + msg2="${msg}不代理UDP" + fi + echolog " - ${msg2}" + } + $ipt_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p udp -j RETURN + $ip6t_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p udp -j RETURN 2>/dev/null + done + unset enabled sid remarks sources tcp_no_redir_ports udp_no_redir_ports tcp_redir_ports udp_redir_ports node + unset _ip _mac _iprange _ipset _ip_or_mac rule_list node_remark + unset ipt_tmp msg msg2 + done + } + + [ "$ENABLED_DEFAULT_ACL" == 1 ] && { + # 加载默认代理模式 + if [ "$TCP_PROXY_MODE" != "disable" ]; then + local ipt_tmp=$ipt_n + [ -n "${is_tproxy}" ] && ipt_tmp=$ipt_m + [ "$TCP_NO_REDIR_PORTS" != "disable" ] && { + $ipt_tmp -A PSW2 $(comment "默认") -p tcp -m multiport --dport $TCP_NO_REDIR_PORTS -j RETURN + $ip6t_m -A PSW2 $(comment "默认") -p tcp -m multiport --dport $TCP_NO_REDIR_PORTS -j RETURN + msg="${msg}除${TCP_NO_REDIR_PORTS}外的" + } + [ "$NODE" != "nil" ] && { + msg="TCP默认代理:使用节点[$(config_n_get $NODE remarks)] [$(get_action_chain_name $TCP_PROXY_MODE)]" + if [ -n "${is_tproxy}" ]; then + msg="${msg}(TPROXY:${REDIR_PORT})代理" + else + msg="${msg}(REDIRECT:${REDIR_PORT})代理" + fi + + [ "$TCP_NO_REDIR_PORTS" != "disable" ] && msg="${msg}除${TCP_NO_REDIR_PORTS}外的" + msg="${msg}所有端口" + + [ "$accept_icmp" = "1" ] && { + $ipt_n -A PSW2 $(comment "默认") -p icmp -d $FAKE_IP $(REDIRECT) + $ipt_n -A PSW2 $(comment "默认") -p icmp $(REDIRECT) + } + + [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + $ip6t_n -A PSW2 $(comment "默认") -p ipv6-icmp -d $FAKE_IP_6 $(REDIRECT) + $ip6t_n -A PSW2 $(comment "默认") -p ipv6-icmp $(REDIRECT) + } + + if [ "${ipt_tmp}" = "${ipt_n}" ]; then + $ipt_n -A PSW2 $(comment "默认") -p tcp -d $FAKE_IP $(REDIRECT $REDIR_PORT) + $ipt_n -A PSW2 $(comment "默认") -p tcp $(factor $TCP_REDIR_PORTS "-m multiport --dport") $(REDIRECT $REDIR_PORT) + else + $ipt_m -A PSW2 $(comment "默认") -p tcp -d $FAKE_IP -j PSW2_RULE + $ipt_m -A PSW2 $(comment "默认") -p tcp $(factor $TCP_REDIR_PORTS "-m multiport --dport") -j PSW2_RULE + $ipt_m -A PSW2 $(comment "默认") -p tcp $(REDIRECT $REDIR_PORT TPROXY) + fi + + [ "$PROXY_IPV6" == "1" ] && { + $ip6t_m -A PSW2 $(comment "默认") -p tcp -d $FAKE_IP_6 -j PSW2_RULE + $ip6t_m -A PSW2 $(comment "默认") -p tcp $(factor $TCP_REDIR_PORTS "-m multiport --dport") -j PSW2_RULE + $ip6t_m -A PSW2 $(comment "默认") -p tcp $(REDIRECT $REDIR_PORT TPROXY) + } + + echolog "${msg}" + } + fi + $ipt_n -A PSW2 $(comment "默认") -p tcp -j RETURN + $ipt_m -A PSW2 $(comment "默认") -p tcp -j RETURN + $ip6t_m -A PSW2 $(comment "默认") -p tcp -j RETURN + + # 加载UDP默认代理模式 + if [ "$UDP_PROXY_MODE" != "disable" ]; then + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && { + $ipt_m -A PSW2 $(comment "默认") -p udp -m multiport --dport $UDP_NO_REDIR_PORTS -j RETURN + $ip6t_m -A PSW2 $(comment "默认") -p udp -m multiport --dport $UDP_NO_REDIR_PORTS -j RETURN + } + + [ -n "1" ] && { + msg="UDP默认代理:使用节点[$(config_n_get $NODE remarks)] [$(get_action_chain_name $UDP_PROXY_MODE)](TPROXY:${REDIR_PORT})代理" + + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && msg="${msg}除${UDP_NO_REDIR_PORTS}外的" + msg="${msg}所有端口" + + $ipt_m -A PSW2 $(comment "默认") -p udp -d $FAKE_IP -j PSW2_RULE + $ipt_m -A PSW2 $(comment "默认") -p udp $(factor $UDP_REDIR_PORTS "-m multiport --dport") -j PSW2_RULE + $ipt_m -A PSW2 $(comment "默认") -p udp $(REDIRECT $REDIR_PORT TPROXY) + + if [ "$PROXY_IPV6_UDP" == "1" ]; then + $ip6t_m -A PSW2 $(comment "默认") -p udp -d $FAKE_IP_6 -j PSW2_RULE + $ip6t_m -A PSW2 $(comment "默认") -p udp $(factor $UDP_REDIR_PORTS "-m multiport --dport") -j PSW2_RULE + $ip6t_m -A PSW2 $(comment "默认") -p udp $(REDIRECT $REDIR_PORT TPROXY) + fi + + echolog "${msg}" + } + fi + $ipt_m -A PSW2 $(comment "默认") -p udp -j RETURN + $ip6t_m -A PSW2 $(comment "默认") -p udp -j RETURN + } +} + +filter_haproxy() { + for item in $(uci show $CONFIG | grep ".lbss=" | cut -d "'" -f 2); do + local ip=$(get_host_ip ipv4 $(echo $item | awk -F ":" '{print $1}') 1) + [ -n "$ip" ] && ipset -q add $IPSET_VPSLIST $ip + done + echolog "加入负载均衡的节点到ipset[$IPSET_VPSLIST]直连完成" +} + +filter_vpsip() { + uci show $CONFIG | grep ".address=" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | sed -e "/^$/d" | sed -e "s/^/add $IPSET_VPSLIST &/g" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R + uci show $CONFIG | grep ".address=" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d" | sed -e "s/^/add $IPSET_VPSLIST6 &/g" | awk '{print $0} END{print "COMMIT"}' | ipset -! -R + echolog "加入所有节点到ipset[$IPSET_VPSLIST]直连完成" +} + +filter_node() { + local proxy_node=${1} + local stream=$(echo ${2} | tr 'A-Z' 'a-z') + local proxy_port=${3} + + filter_rules() { + local node=${1} + local stream=${2} + local _proxy=${3} + local _port=${4} + local _is_tproxy ipt_tmp msg msg2 + + if [ -n "$node" ] && [ "$node" != "nil" ]; then + local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + local address=$(config_n_get $node address) + local port=$(config_n_get $node port) + ipt_tmp=$ipt_n + _is_tproxy=${is_tproxy} + [ "$stream" == "udp" ] && _is_tproxy="TPROXY" + if [ -n "${_is_tproxy}" ]; then + ipt_tmp=$ipt_m + msg="TPROXY" + else + msg="REDIRECT" + fi + else + echolog " - 节点配置不正常,略过" + return 0 + fi + + local ADD_INDEX=$FORCE_INDEX + for _ipt in 4 6; do + [ "$_ipt" == "4" ] && _ipt=$ipt_tmp + [ "$_ipt" == "6" ] && _ipt=$ip6t_m + $_ipt -n -L PSW2_OUTPUT | grep -q "${address}:${port}" + if [ $? -ne 0 ]; then + unset dst_rule + local dst_rule="-j PSW2_RULE" + msg2="按规则路由(${msg})" + [ "$_ipt" == "$ipt_m" -o "$_ipt" == "$ip6t_m" ] || { + dst_rule=$(REDIRECT $_port) + msg2="套娃使用(${msg}:${port} -> ${_port})" + } + [ -n "$_proxy" ] && [ "$_proxy" == "1" ] && [ -n "$_port" ] || { + ADD_INDEX=$(RULE_LAST_INDEX "$_ipt" PSW2_OUTPUT "$IPSET_VPSLIST" $FORCE_INDEX) + dst_rule=" -j RETURN" + msg2="直连代理" + } + $_ipt -I PSW2_OUTPUT $ADD_INDEX $(comment "${address}:${port}") -p $stream -d $address --dport $port $dst_rule 2>/dev/null + else + msg2="已配置过的节点," + fi + done + msg="[$?]$(echo ${2} | tr 'a-z' 'A-Z')${msg2}使用链${ADD_INDEX},节点(${type}):${address}:${port}" + #echolog " - ${msg}" + } + + local proxy_protocol=$(config_n_get $proxy_node protocol) + local proxy_type=$(echo $(config_n_get $proxy_node type nil) | tr 'A-Z' 'a-z') + [ "$proxy_type" == "nil" ] && echolog " - 节点配置不正常,略过!:${proxy_node}" && return 0 + if [ "$proxy_protocol" == "_balancing" ]; then + #echolog " - 多节点负载均衡(${proxy_type})..." + proxy_node=$(config_n_get $proxy_node balancing_node) + for _node in $proxy_node; do + filter_rules "$_node" "$stream" + done + elif [ "$proxy_protocol" == "_shunt" ]; then + #echolog " - 按请求目的地址分流(${proxy_type})..." + local default_node=$(config_n_get $proxy_node default_node _direct) + local main_node=$(config_n_get $proxy_node main_node nil) + if [ "$main_node" != "nil" ]; then + filter_rules $main_node $stream + else + if [ "$default_node" != "_direct" ] && [ "$default_node" != "_blackhole" ]; then + filter_rules $default_node $stream + fi + fi +:</dev/null 2>&1 & + #echolog " - 追加到白名单:${ispip}" + done + } + + [ -n "$ISP_DNS6" ] && { + #echolog "处理 ISP IPv6 DNS 例外..." + for ispip6 in $ISP_DNS6; do + ipset -! add $IPSET_LANLIST6 $ispip6 >/dev/null 2>&1 & + #echolog " - 追加到白名单:${ispip6}" + done + } + + # 过滤所有节点IP + filter_vpsip > /dev/null 2>&1 & + filter_haproxy > /dev/null 2>&1 & + + accept_icmp=$(config_t_get global_forwarding accept_icmp 0) + accept_icmpv6=$(config_t_get global_forwarding accept_icmpv6 0) + + local tcp_proxy_way=$(config_t_get global_forwarding tcp_proxy_way redirect) + if [ "$tcp_proxy_way" = "redirect" ]; then + unset is_tproxy + elif [ "$tcp_proxy_way" = "tproxy" ]; then + is_tproxy="TPROXY" + fi + + $ipt_n -N PSW2 + $ipt_n -A PSW2 $(dst $IPSET_LANLIST) -j RETURN + $ipt_n -A PSW2 $(dst $IPSET_VPSLIST) -j RETURN + $ipt_n -A PSW2 $(dst $IPSET_WHITELIST) ! -d $FAKE_IP -j RETURN + + WAN_IP=$(get_wan_ip) + [ ! -z "${WAN_IP}" ] && $ipt_n -A PSW2 $(comment "WAN_IP_RETURN") -d "${WAN_IP}" -j RETURN + + [ "$accept_icmp" = "1" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2" + [ -z "${is_tproxy}" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2" + + $ipt_n -N PSW2_OUTPUT + $ipt_n -A PSW2_OUTPUT $(dst $IPSET_LANLIST) -j RETURN + $ipt_n -A PSW2_OUTPUT $(dst $IPSET_VPSLIST) -j RETURN + $ipt_n -A PSW2_OUTPUT $(dst $IPSET_WHITELIST) ! -d $FAKE_IP -j RETURN + $ipt_n -A PSW2_OUTPUT -m mark --mark 0xff -j RETURN + + $ipt_n -N PSW2_REDIRECT + $ipt_n -I PREROUTING 1 -j PSW2_REDIRECT + + $ipt_m -N PSW2_DIVERT + $ipt_m -A PSW2_DIVERT -j MARK --set-mark 1 + $ipt_m -A PSW2_DIVERT -j ACCEPT + + $ipt_m -N PSW2_RULE + $ipt_m -A PSW2_RULE -j CONNMARK --restore-mark + $ipt_m -A PSW2_RULE -m mark --mark 0x1 -j RETURN + $ipt_m -A PSW2_RULE -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -j MARK --set-xmark 1 + $ipt_m -A PSW2_RULE -p udp -m conntrack --ctstate NEW -j MARK --set-xmark 1 + $ipt_m -A PSW2_RULE -j CONNMARK --save-mark + + $ipt_m -N PSW2 + $ipt_m -A PSW2 $(dst $IPSET_LANLIST) -j RETURN + $ipt_m -A PSW2 $(dst $IPSET_VPSLIST) -j RETURN + $ipt_m -A PSW2 $(dst $IPSET_WHITELIST) ! -d $FAKE_IP -j RETURN + + [ ! -z "${WAN_IP}" ] && $ipt_m -A PSW2 $(comment "WAN_IP_RETURN") -d "${WAN_IP}" -j RETURN + unset WAN_IP + + insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2" + insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT" + + $ipt_m -N PSW2_OUTPUT + $ipt_m -A PSW2_OUTPUT -m mark --mark 0xff -j RETURN + $ipt_m -A PSW2_OUTPUT $(dst $IPSET_LANLIST) -j RETURN + $ipt_m -A PSW2_OUTPUT $(dst $IPSET_VPSLIST) -j RETURN + $ipt_m -A PSW2_OUTPUT $(dst $IPSET_WHITELIST) ! -d $FAKE_IP -j RETURN + + ip rule add fwmark 1 lookup 100 + ip route add local 0.0.0.0/0 dev lo table 100 + + [ "$accept_icmpv6" = "1" ] && { + $ip6t_n -N PSW2 + $ip6t_n -A PSW2 $(dst $IPSET_LANLIST6) -j RETURN + $ip6t_n -A PSW2 $(dst $IPSET_VPSLIST6) -j RETURN + $ip6t_n -A PSW2 $(dst $IPSET_WHITELIST6) ! -d $FAKE_IP_6 -j RETURN + $ip6t_n -A PREROUTING -p ipv6-icmp -j PSW2 + + $ip6t_n -N PSW2_OUTPUT + $ip6t_n -A PSW2_OUTPUT $(dst $IPSET_LANLIST6) -j RETURN + $ip6t_n -A PSW2_OUTPUT $(dst $IPSET_VPSLIST6) -j RETURN + $ip6t_n -A PSW2_OUTPUT $(dst $IPSET_WHITELIST6) ! -d $FAKE_IP_6 -j RETURN + $ip6t_n -A PSW2_OUTPUT -m mark --mark 0xff -j RETURN + } + + $ip6t_m -N PSW2_DIVERT + $ip6t_m -A PSW2_DIVERT -j MARK --set-mark 1 + $ip6t_m -A PSW2_DIVERT -j ACCEPT + + $ip6t_m -N PSW2_RULE + $ip6t_m -A PSW2_RULE -j CONNMARK --restore-mark + $ip6t_m -A PSW2_RULE -m mark --mark 0x1 -j RETURN + $ip6t_m -A PSW2_RULE -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -j MARK --set-xmark 1 + $ip6t_m -A PSW2_RULE -p udp -m conntrack --ctstate NEW -j MARK --set-xmark 1 + $ip6t_m -A PSW2_RULE -j CONNMARK --save-mark + + $ip6t_m -N PSW2 + $ip6t_m -A PSW2 $(dst $IPSET_LANLIST6) -j RETURN + $ip6t_m -A PSW2 $(dst $IPSET_VPSLIST6) -j RETURN + $ip6t_m -A PSW2 $(dst $IPSET_WHITELIST6) ! -d $FAKE_IP_6 -j RETURN + + WAN6_IP=$(get_wan6_ip) + [ ! -z "${WAN6_IP}" ] && $ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") -d ${WAN6_IP} -j RETURN + unset WAN6_IP + + insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2" + insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT" + + $ip6t_m -N PSW2_OUTPUT + $ip6t_m -A PSW2_OUTPUT -m mark --mark 0xff -j RETURN + $ip6t_m -A PSW2_OUTPUT $(dst $IPSET_LANLIST6) -j RETURN + $ip6t_m -A PSW2_OUTPUT $(dst $IPSET_VPSLIST6) -j RETURN + $ip6t_m -A PSW2_OUTPUT $(dst $IPSET_WHITELIST6) ! -d $FAKE_IP_6 -j RETURN + + ip -6 rule add fwmark 1 table 100 + ip -6 route add local ::/0 dev lo table 100 + + # 过滤Socks节点 + [ "$SOCKS_ENABLED" = "1" ] && { + local ids=$(uci show $CONFIG | grep "=socks" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}') + #echolog "分析 Socks 服务所使用节点..." + local id enabled node port msg num + for id in $ids; do + enabled=$(config_n_get $id enabled 0) + [ "$enabled" == "1" ] || continue + node=$(config_n_get $id node nil) + port=$(config_n_get $id port 0) + msg="Socks 服务 [:${port}]" + if [ "$node" == "nil" ] || [ "$port" == "0" ]; then + msg="${msg} 未配置完全,略过" + else + filter_node $node TCP > /dev/null 2>&1 & + filter_node $node UDP > /dev/null 2>&1 & + fi + #echolog " - ${msg}" + done + } + + [ "$ENABLED_DEFAULT_ACL" == 1 ] && { + # 加载路由器自身代理 TCP + if [ "$NODE" != "nil" ] && [ "$LOCALHOST_PROXY" = "1" ]; then + echolog "加载路由器自身 TCP 代理..." + + [ "$accept_icmp" = "1" ] && { + $ipt_n -A OUTPUT -p icmp -j PSW2_OUTPUT + $ipt_n -A PSW2_OUTPUT -p icmp -d $FAKE_IP $(REDIRECT) + $ipt_n -A PSW2_OUTPUT -p icmp $(REDIRECT) + } + + [ "$accept_icmpv6" = "1" ] && { + $ip6t_n -A OUTPUT -p ipv6-icmp -j PSW2_OUTPUT + $ip6t_n -A PSW2_OUTPUT -p ipv6-icmp -d $FAKE_IP_6 $(REDIRECT) + $ip6t_n -A PSW2_OUTPUT -p ipv6-icmp $(REDIRECT) + } + + local ipt_tmp=$ipt_n + [ -n "${is_tproxy}" ] && { + echolog " - 启用 TPROXY 模式" + ipt_tmp=$ipt_m + } + + [ "$TCP_NO_REDIR_PORTS" != "disable" ] && { + $ipt_tmp -A PSW2_OUTPUT -p tcp -m multiport --dport $TCP_NO_REDIR_PORTS -j RETURN + $ip6t_m -A PSW2_OUTPUT -p tcp -m multiport --dport $TCP_NO_REDIR_PORTS -j RETURN + echolog " - [$?]不代理TCP 端口:$TCP_NO_REDIR_PORTS" + } + + if [ "${ipt_tmp}" = "${ipt_n}" ]; then + $ipt_n -A PSW2_OUTPUT -p tcp -d $FAKE_IP $(REDIRECT $REDIR_PORT) + $ipt_n -A PSW2_OUTPUT -p tcp $(factor $TCP_REDIR_PORTS "-m multiport --dport") $(REDIRECT $REDIR_PORT) + $ipt_n -A OUTPUT -p tcp -j PSW2_OUTPUT + else + $ipt_m -A PSW2_OUTPUT -p tcp -d $FAKE_IP -j PSW2_RULE + $ipt_m -A PSW2_OUTPUT -p tcp $(factor $TCP_REDIR_PORTS "-m multiport --dport") -j PSW2_RULE + $ipt_m -A PSW2 $(comment "本机") -p tcp -i lo $(REDIRECT $REDIR_PORT TPROXY) + $ipt_m -A PSW2 $(comment "本机") -p tcp -i lo -j RETURN + $ipt_m -A OUTPUT -p tcp -j PSW2_OUTPUT + fi + + if [ "$PROXY_IPV6" == "1" ]; then + $ip6t_m -A PSW2_OUTPUT -p tcp -d $FAKE_IP_6 -j PSW2_RULE + $ip6t_m -A PSW2_OUTPUT -p tcp $(factor $TCP_REDIR_PORTS "-m multiport --dport") -j PSW2_RULE + $ip6t_m -A PSW2 $(comment "本机") -p tcp -i lo $(REDIRECT $REDIR_PORT TPROXY) + $ip6t_m -A PSW2 $(comment "本机") -p tcp -i lo -j RETURN + $ip6t_m -A OUTPUT -p tcp -j PSW2_OUTPUT + fi + + for iface in $IFACES; do + $ipt_n -I PSW2_OUTPUT -o $iface -p tcp -j RETURN + $ipt_m -I PSW2_OUTPUT -o $iface -p tcp -j RETURN + done + fi + + # 处理轮换节点的分流或套娃 + filter_node $NODE TCP > /dev/null 2>&1 & + filter_node $NODE UDP > /dev/null 2>&1 & + + # 加载路由器自身代理 UDP + if [ "$NODE" != "nil" ] && [ "$LOCALHOST_PROXY" = "1" ]; then + echolog "加载路由器自身 UDP 代理..." + + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && { + $ipt_m -A PSW2_OUTPUT -p udp -m multiport --dport $UDP_NO_REDIR_PORTS -j RETURN + $ip6t_m -A PSW2_OUTPUT -p udp -m multiport --dport $UDP_NO_REDIR_PORTS -j RETURN + echolog " - [$?]不代理 UDP 端口:$UDP_NO_REDIR_PORTS" + } + + $ipt_m -A PSW2_OUTPUT -p udp -d $FAKE_IP -j PSW2_RULE + $ipt_m -A PSW2_OUTPUT -p udp $(factor $UDP_REDIR_PORTS "-m multiport --dport") -j PSW2_RULE + $ipt_m -A PSW2 $(comment "本机") -p udp -i lo $(REDIRECT $REDIR_PORT TPROXY) + $ipt_m -A PSW2 $(comment "本机") -p udp -i lo -j RETURN + $ipt_m -A OUTPUT -p udp -j PSW2_OUTPUT + + if [ "$PROXY_IPV6_UDP" == "1" ]; then + $ip6t_m -A PSW2_OUTPUT -p udp -d $FAKE_IP_6 -j PSW2_RULE + $ip6t_m -A PSW2_OUTPUT -p udp $(factor $UDP_REDIR_PORTS "-m multiport --dport") -j PSW2_RULE + $ip6t_m -A PSW2 $(comment "本机") -p udp -i lo $(REDIRECT $REDIR_PORT TPROXY) + $ip6t_m -A PSW2 $(comment "本机") -p udp -i lo -j RETURN + $ip6t_m -A OUTPUT -p udp -j PSW2_OUTPUT + fi + + for iface in $IFACES; do + $ipt_n -I PSW2_OUTPUT -o $iface -p udp -j RETURN + $ipt_m -I PSW2_OUTPUT -o $iface -p udp -j RETURN + done + fi + + $ipt_m -A PSW2 -p udp --dport 53 -j RETURN + $ip6t_m -A PSW2 -p udp --dport 53 -j RETURN + } + + # 加载ACLS + load_acl + + echolog "防火墙规则加载完成!" +} + +del_firewall_rule() { + for ipt in "$ipt_n" "$ipt_m" "$ip6t_n" "$ip6t_m"; do + for chain in "PREROUTING" "OUTPUT"; do + for i in $(seq 1 $($ipt -nL $chain | grep -c PSW2)); do + local index=$($ipt --line-number -nL $chain | grep PSW2 | head -1 | awk '{print $1}') + $ipt -D $chain $index 2>/dev/null + done + done + for chain in "PSW2" "PSW2_OUTPUT" "PSW2_DIVERT" "PSW2_REDIRECT" "PSW2_RULE"; do + $ipt -F $chain 2>/dev/null + $ipt -X $chain 2>/dev/null + done + done + + ip rule del fwmark 1 lookup 100 2>/dev/null + ip route del local 0.0.0.0/0 dev lo table 100 2>/dev/null + + ip -6 rule del fwmark 1 table 100 2>/dev/null + ip -6 route del local ::/0 dev lo table 100 2>/dev/null + + $DIR/app.sh echolog "删除相关防火墙规则完成。" +} + +flush_ipset() { + del_firewall_rule + for _name in $(ipset list | grep "Name: " | grep "passwall2_" | awk '{print $2}'); do + destroy_ipset ${_name} + done + /etc/init.d/passwall2 reload +} + +flush_include() { + echo '#!/bin/sh' >$FWI +} + +gen_include() { + flush_include + extract_rules() { + local _ipt="${ipt}" + [ "$1" == "6" ] && _ipt="${ip6t}" + [ -z "${_ipt}" ] && return + + echo "*$2" + ${_ipt}-save -t $2 | grep "PSW2" | grep -v "\-j PSW2$" | grep -v "socket \-j PSW2_DIVERT$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/" + echo 'COMMIT' + } + local __ipt="" + [ -n "${ipt}" ] && { + __ipt=$(cat <<- EOF + $ipt-save -c | grep -v "PSW2" | $ipt-restore -c + $ipt-restore -n <<-EOT + $(extract_rules 4 nat) + $(extract_rules 4 mangle) + EOT + + [ "$accept_icmp" = "1" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2") + [ -z "${is_tproxy}" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2") + + \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2") + \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT") + + WAN_IP=\$(${MY_PATH} get_wan_ip) + + PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ipt_n" PSW2 WAN_IP_RETURN -1) + if [ \$PR_INDEX -ge 0 ]; then + [ ! -z "\${WAN_IP}" ] && $ipt_n -R PSW2 \$PR_INDEX $(comment "WAN_IP_RETURN") -d "\${WAN_IP}" -j RETURN + fi + + PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ipt_m" PSW2 WAN_IP_RETURN -1) + if [ \$PR_INDEX -ge 0 ]; then + [ ! -z "\${WAN_IP}" ] && $ipt_m -R PSW2 \$PR_INDEX $(comment "WAN_IP_RETURN") -d "\${WAN_IP}" -j RETURN + fi + EOF + ) + } + local __ip6t="" + [ -n "${ip6t}" ] && { + __ip6t=$(cat <<- EOF + $ip6t-save -c | grep -v "PSW2" | $ip6t-restore -c + $ip6t-restore -n <<-EOT + $(extract_rules 6 nat) + $(extract_rules 6 mangle) + EOT + + [ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING -p ipv6-icmp -j PSW2 + + \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2") + \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT") + + PR_INDEX=\$(${MY_PATH} RULE_LAST_INDEX "$ip6t_m" PSW2 WAN6_IP_RETURN -1) + if [ \$PR_INDEX -ge 0 ]; then + WAN6_IP=\$(${MY_PATH} get_wan6_ip) + [ ! -z "\${WAN6_IP}" ] && $ip6t_m -R PSW2 \$PR_INDEX $(comment "WAN6_IP_RETURN") -d "\${WAN6_IP}" -j RETURN + fi + EOF + ) + } + cat <<-EOF >> $FWI + ${__ipt} + + ${__ip6t} + EOF + return 0 +} + +get_ipt_bin() { + echo $ipt +} + +get_ip6t_bin() { + echo $ip6t +} + +start() { + [ "$ENABLED_DEFAULT_ACL" == 0 -a "$ENABLED_ACLS" == 0 ] && return + add_firewall_rule + gen_include +} + +stop() { + del_firewall_rule + flush_include +} + +arg1=$1 +shift +case $arg1 in +RULE_LAST_INDEX) + RULE_LAST_INDEX "$@" + ;; +insert_rule_before) + insert_rule_before "$@" + ;; +insert_rule_after) + insert_rule_after "$@" + ;; +flush_ipset) + flush_ipset + ;; +get_ipt_bin) + get_ipt_bin + ;; +get_ip6t_bin) + get_ip6t_bin + ;; +get_wan_ip) + get_wan_ip + ;; +get_wan6_ip) + get_wan6_ip + ;; +stop) + stop + ;; +start) + start + ;; +*) ;; +esac diff --git a/luci-app-passwall2/root/usr/share/passwall2/monitor.sh b/luci-app-passwall2/root/usr/share/passwall2/monitor.sh new file mode 100755 index 000000000..2ef774ee1 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/monitor.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +CONFIG=passwall2 +TMP_PATH=/tmp/etc/$CONFIG +TMP_SCRIPT_FUNC_PATH=$TMP_PATH/script_func +LOCK_FILE_DIR=/tmp/lock +LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}_script.lock + +config_n_get() { + local ret=$(uci -q get $CONFIG.$1.$2 2>/dev/null) + echo ${ret:=$3} +} + +config_t_get() { + local index=0 + [ -n "$4" ] && index=$4 + local ret=$(uci -q get $CONFIG.@$1[$index].$2 2>/dev/null) + echo ${ret:=$3} +} + +ENABLED=$(config_t_get global enabled 0) +[ "$ENABLED" != 1 ] && return 1 +ENABLED=$(config_t_get global_delay start_daemon 0) +[ "$ENABLED" != 1 ] && return 1 +sleep 58s +while [ "$ENABLED" -eq 1 ]; do + [ -f "$LOCK_FILE" ] && { + sleep 6s + continue + } + touch $LOCK_FILE + [ -d ${TMP_SCRIPT_FUNC_PATH} ] && { + for filename in $(ls ${TMP_SCRIPT_FUNC_PATH} | grep -v "^_"); do + cmd=$(cat ${TMP_SCRIPT_FUNC_PATH}/${filename}) + cmd_check=$(echo $cmd | awk -F '>' '{print $1}') + [ -n "$(echo $cmd_check | grep "dns2socks")" ] && cmd_check=$(echo $cmd_check | sed "s#:# #g") + icount=$(pgrep -f "$(echo $cmd_check)" | wc -l) + if [ $icount = 0 ]; then + #echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall2.log + eval $(echo "nohup ${cmd} 2>&1 &") >/dev/null 2>&1 & + fi + done + } + + rm -f $LOCK_FILE + sleep 58s +done diff --git a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh new file mode 100755 index 000000000..35c262e68 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh @@ -0,0 +1,984 @@ +#!/bin/bash + +DIR="$(cd "$(dirname "$0")" && pwd)" +MY_PATH=$DIR/nftables.sh +NFTSET_LANLIST="passwall2_lanlist" +NFTSET_VPSLIST="passwall2_vpslist" +NFTSET_WHITELIST="passwall2_whitelist" + +NFTSET_LANLIST6="passwall2_lanlist6" +NFTSET_VPSLIST6="passwall2_vpslist6" +NFTSET_WHITELIST6="passwall2_whitelist6" + +FORCE_INDEX=0 + +. /lib/functions/network.sh + +FWI=$(uci -q get firewall.passwall2.path 2>/dev/null) +FAKE_IP="198.18.0.0/16" +FAKE_IP_6="fc00::/18" + +factor() { + if [ -z "$1" ] || [ -z "$2" ]; then + echo "" + elif [ "$1" == "1:65535" ]; then + echo "" + # acl mac address + elif [ -n "$(echo $1 | grep -E '([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}')" ]; then + echo "$2 {$1}" + else + echo "$2 {$(echo $1 | sed 's/:/-/g')}" + fi +} + +insert_rule_before() { + [ $# -ge 4 ] || { + return 1 + } + local table_name="${1}"; shift + local chain_name="${1}"; shift + local keyword="${1}"; shift + local rule="${1}"; shift + local default_index="${1}"; shift + default_index=${default_index:-0} + local _index=$(nft -a list chain $table_name $chain_name 2>/dev/null | grep "$keyword" | awk -F '# handle ' '{print$2}' | head -n 1 | awk '{print $1}') + if [ -z "${_index}" ] && [ "${default_index}" = "0" ]; then + nft "add rule $table_name $chain_name $rule" + else + if [ -z "${_index}" ]; then + _index=${default_index} + fi + nft "insert rule $table_name $chain_name position $_index $rule" + fi +} + +insert_rule_after() { + [ $# -ge 4 ] || { + return 1 + } + local table_name="${1}"; shift + local chain_name="${1}"; shift + local keyword="${1}"; shift + local rule="${1}"; shift + local default_index="${1}"; shift + default_index=${default_index:-0} + local _index=$(nft -a list chain $table_name $chain_name 2>/dev/null | grep "$keyword" | awk -F '# handle ' '{print$2}' | head -n 1 | awk '{print $1}') + if [ -z "${_index}" ] && [ "${default_index}" = "0" ]; then + nft "add rule $table_name $chain_name $rule" + else + if [ -n "${_index}" ]; then + _index=$((_index + 1)) + else + _index=${default_index} + fi + nft "insert rule $table_name $chain_name position $_index $rule" + fi +} + +RULE_LAST_INDEX() { + [ $# -ge 3 ] || { + echolog "索引列举方式不正确(nftables),终止执行!" + return 1 + } + local table_name="${1}"; shift + local chain_name="${1}"; shift + local keyword="${1}"; shift + local default="${1:-0}"; shift + local _index=$(nft -a list chain $table_name $chain_name 2>/dev/null | grep "$keyword" | awk -F '# handle ' '{print$2}' | head -n 1 | awk '{print $1}') + echo "${_index:-${default}}" +} + +REDIRECT() { + local s="counter redirect" + [ -n "$1" ] && { + local s="$s to :$1" + [ "$2" == "MARK" ] && s="counter meta mark set $1" + [ "$2" == "TPROXY" ] && { + s="counter meta mark 1 tproxy to :$1" + } + [ "$2" == "TPROXY4" ] && { + s="counter meta mark 1 tproxy ip to :$1" + } + [ "$2" == "TPROXY6" ] && { + s="counter meta mark 1 tproxy ip6 to :$1" + } + + } + echo $s +} + +destroy_nftset() { + for i in "$@"; do + nft flush set inet fw4 $i 2>/dev/null + nft delete set inet fw4 $i 2>/dev/null + done +} + +insert_nftset() { + local nftset_name="${1}"; shift + local nftset_elements + + nftset_elements=$(echo -e $@ | sed 's/\s/, /g') + [ -n "${nftset_elements}" ] && { + mkdir -p $TMP_PATH2/nftset + + cat > "$TMP_PATH2/nftset/$nftset_name" <<-EOF + define $nftset_name = {$nftset_elements} + add element inet fw4 $nftset_name \$$nftset_name + EOF + nft -f "$TMP_PATH2/nftset/$nftset_name" + rm -rf "$TMP_PATH2/nftset" + } +} + +gen_nftset() { + local nftset_name="${1}"; shift + local ip_type="${1}"; shift + + nft "list set inet fw4 $nftset_name" &>/dev/null + if [ $? -ne 0 ]; then + nft "add set inet fw4 $nftset_name { type $ip_type; flags interval; auto-merge; }" + fi + [ -n "${1}" ] && insert_nftset $nftset_name $@ +} + +get_action_chain_name() { + echo "全局代理" +} + +gen_lanlist() { + cat <<-EOF + 0.0.0.0/8 + 10.0.0.0/8 + 100.64.0.0/10 + 127.0.0.0/8 + 169.254.0.0/16 + 172.16.0.0/12 + 192.168.0.0/16 + 224.0.0.0/4 + 240.0.0.0/4 + EOF +} + +gen_lanlist_6() { + cat <<-EOF + ::/128 + ::1/128 + ::ffff:0:0/96 + ::ffff:0:0:0/96 + 64:ff9b::/96 + 100::/64 + 2001::/32 + 2001:20::/28 + 2001:db8::/32 + 2002::/16 + fc00::/7 + fe80::/10 + ff00::/8 + EOF +} + +get_wan_ip() { + local NET_IF + local NET_ADDR + + network_flush_cache + network_find_wan NET_IF + network_get_ipaddr NET_ADDR "${NET_IF}" + + echo $NET_ADDR +} + +get_wan6_ip() { + local NET_IF + local NET_ADDR + + network_flush_cache + network_find_wan6 NET_IF + network_get_ipaddr6 NET_ADDR "${NET_IF}" + + echo $NET_ADDR +} + +load_acl() { + [ "$ENABLED_ACLS" == 1 ] && { + acl_app + echolog "访问控制:" + for sid in $(ls -F ${TMP_ACL_PATH} | grep '/$' | awk -F '/' '{print $1}'); do + eval $(uci -q show "${CONFIG}.${sid}" | cut -d'.' -sf 3-) + + tcp_no_redir_ports=${tcp_no_redir_ports:-default} + udp_no_redir_ports=${udp_no_redir_ports:-default} + tcp_proxy_mode="global" + udp_proxy_mode="global" + node=${node:-default} + [ "$tcp_no_redir_ports" = "default" ] && tcp_no_redir_ports=$TCP_NO_REDIR_PORTS + [ "$udp_no_redir_ports" = "default" ] && udp_no_redir_ports=$UDP_NO_REDIR_PORTS + [ "$tcp_redir_ports" = "default" ] && tcp_redir_ports=$TCP_REDIR_PORTS + [ "$udp_redir_ports" = "default" ] && udp_redir_ports=$UDP_REDIR_PORTS + + node_remark=$(config_n_get $NODE remarks) + [ -s "${TMP_ACL_PATH}/${sid}/var_node" ] && node=$(cat ${TMP_ACL_PATH}/${sid}/var_node) + [ -s "${TMP_ACL_PATH}/${sid}/var_port" ] && redir_port=$(cat ${TMP_ACL_PATH}/${sid}/var_port) + [ -n "$node" ] && [ "$node" != "default" ] && node_remark=$(config_n_get $node remarks) + + for i in $(cat ${TMP_ACL_PATH}/${sid}/rule_list); do + if [ -n "$(echo ${i} | grep '^iprange:')" ]; then + _iprange=$(echo ${i} | sed 's#iprange:##g') + _ipt_source=$(factor ${_iprange} "ip saddr") + msg="备注【$remarks】,IP range【${_iprange}】," + elif [ -n "$(echo ${i} | grep '^ipset:')" ]; then + _ipset=$(echo ${i} | sed 's#ipset:##g') + _ipt_source="ip daddr @${_ipset}" + msg="备注【$remarks】,NFTset【${_ipset}】," + elif [ -n "$(echo ${i} | grep '^ip:')" ]; then + _ip=$(echo ${i} | sed 's#ip:##g') + _ipt_source=$(factor ${_ip} "ip saddr") + msg="备注【$remarks】,IP【${_ip}】," + elif [ -n "$(echo ${i} | grep '^mac:')" ]; then + _mac=$(echo ${i} | sed 's#mac:##g') + _ipt_source=$(factor ${_mac} "ether saddr") + msg="备注【$remarks】,MAC【${_mac}】," + else + continue + fi + + [ -n "$redir_port" ] && { + if [ "$tcp_proxy_mode" != "disable" ]; then + [ -s "${TMP_ACL_PATH}/${sid}/var_redirect_dns_port" ] && nft "add rule inet fw4 PSW2_REDIRECT ip protocol udp ${_ipt_source} udp dport 53 counter redirect to $(cat ${TMP_ACL_PATH}/${sid}/var_redirect_dns_port) comment \"$remarks\"" + msg2="${msg}使用TCP节点[$node_remark] [$(get_action_chain_name $tcp_proxy_mode)]" + if [ -n "${is_tproxy}" ]; then + msg2="${msg2}(TPROXY:${redir_port})代理" + else + msg2="${msg2}(REDIRECT:${redir_port})代理" + fi + + [ "$accept_icmp" = "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} ip daddr $FAKE_IP $(REDIRECT) comment \"$remarks\"" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} $(REDIRECT) comment \"$remarks\"" + } + + [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} ip6 daddr $FAKE_IP_6 $(REDIRECT) comment \"$remarks\"" 2>/dev/null + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} $(REDIRECT) comment \"$remarks\"" 2>/dev/null + } + + [ "$tcp_no_redir_ports" != "disable" ] && { + nft "add rule inet fw4 $nft_prerouting_chain ${_ipt_source} ip protocol tcp tcp dport {$tcp_no_redir_ports} counter return comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 comment ${_ipt_source} meta l4proto tcp tcp dport {$tcp_no_redir_ports} counter return comment \"$remarks\"" + msg2="${msg2}[$?]除${tcp_no_redir_ports}外的" + } + msg2="${msg2}所有端口" + + if [ -z "${is_tproxy}" ]; then + nft "add rule inet fw4 PSW2_NAT ${_ipt_source} ip daddr $FAKE_IP $(REDIRECT $redir_port) comment \"$remarks\"" + nft "add rule inet fw4 PSW2_NAT ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") $(REDIRECT $redir_port) comment \"$remarks\"" + else + nft "add rule inet fw4 PSW2_MANGLE ip protocol tcp ${_ipt_source} ip daddr $FAKE_IP counter jump PSW2_RULE comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") counter jump PSW2_RULE comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE meta nfproto {ipv4} meta l4proto tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY4) comment \"$remarks\"" + fi + + [ "$PROXY_IPV6" == "1" ] && { + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} ip6 daddr $FAKE_IP_6 counter jump PSW2_RULE comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} $(factor $tcp_redir_ports "tcp dport") jump PSW2_RULE comment \"$remarks\"" 2>/dev/null + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY) comment \"$remarks\"" 2>/dev/null + } + else + msg2="${msg}不代理TCP" + fi + echolog " - ${msg2}" + } + + nft "add rule inet fw4 $nft_prerouting_chain ip protocol tcp ${_ipt_source} counter return comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} counter return comment \"$remarks\"" 2>/dev/null + + [ -n "$redir_port" ] && { + if [ "$udp_proxy_mode" != "disable" ]; then + msg2="${msg}使用UDP节点[$node_remark] [$(get_action_chain_name $udp_proxy_mode)]" + msg2="${msg2}(TPROXY:${redir_port})代理" + [ "$udp_no_redir_ports" != "disable" ] && { + nft add rule inet fw4 PSW2_MANGLE meta l4proto udp ${_ipt_source} $(factor $udp_no_redir_ports "udp dport") counter return + nft add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} $(factor $udp_no_redir_ports "udp dport") counter return 2>/dev/null + msg2="${msg2}[$?]除${udp_no_redir_ports}外的" + } + msg2="${msg2}所有端口" + + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp ${_ipt_source} ip daddr $FAKE_IP counter jump PSW2_RULE comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") jump PSW2_RULE comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp ${_ipt_source} $(REDIRECT $redir_port TPROXY4) comment \"$remarks\"" + + [ "$PROXY_IPV6" == "1" ] && [ "$PROXY_IPV6_UDP" == "1" ] && { + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} ip6 daddr $FAKE_IP_6 counter jump PSW2_RULE comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} $(factor $udp_redir_ports "udp dport") counter jump PSW2_RULE comment \"$remarks\"" 2>/dev/null + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} $(REDIRECT $redir_port TPROXY) comment \"$remarks\"" 2>/dev/null + } + else + msg2="${msg}不代理UDP" + fi + echolog " - ${msg2}" + } + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp ${_ipt_source} counter return comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} counter return comment \"$remarks\"" 2>/dev/null + done + unset enabled sid remarks sources tcp_proxy_mode udp_proxy_mode tcp_no_redir_ports udp_no_redir_ports tcp_redir_ports udp_redir_ports node + unset _ip _mac _iprange _ipset _ip_or_mac rule_list redir_port node_remark + unset msg msg2 + done + } + + [ "$ENABLED_DEFAULT_ACL" == 1 ] && { + # 加载默认代理模式 + if [ "$TCP_PROXY_MODE" != "disable" ]; then + [ "$TCP_NO_REDIR_PORTS" != "disable" ] && { + nft add rule inet fw4 $nft_prerouting_chain ip protocol tcp $(factor $TCP_NO_REDIR_PORTS "tcp dport") counter return comment \"默认\" + nft add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp $(factor $TCP_NO_REDIR_PORTS "tcp dport") counter return comment \"默认\" + } + [ "$NODE" != "nil" ] && { + msg="TCP默认代理:使用TCP节点[$(config_n_get $NODE remarks)] [$(get_action_chain_name $TCP_PROXY_MODE)]" + if [ -n "${is_tproxy}" ]; then + msg="${msg}(TPROXY:${REDIR_PORT})代理" + else + msg="${msg}(REDIRECT:${REDIR_PORT})代理" + fi + + [ "$TCP_NO_REDIR_PORTS" != "disable" ] && msg="${msg}除${TCP_NO_REDIR_PORTS}外的" + msg="${msg}所有端口" + + [ "$accept_icmp" = "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip protocol icmp ip daddr $FAKE_IP $(REDIRECT) comment \"默认\"" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip protocol icmp $(REDIRECT) comment \"默认\"" + } + + [ "$accept_icmpv6" = "1" ] && [ "$PROXY_IPV6" == "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmpv6 ip6 daddr $FAKE_IP_6 $(REDIRECT) comment \"默认\"" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmpv6 $(REDIRECT) comment \"默认\"" + } + + if [ -z "${is_tproxy}" ]; then + nft "add rule inet fw4 PSW2_NAT ip protocol tcp ip daddr $FAKE_IP $(REDIRECT $REDIR_PORT) comment \"默认\"" + nft "add rule inet fw4 PSW2_NAT ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") $(REDIRECT $REDIR_PORT) comment \"默认\"" + else + nft "add rule inet fw4 PSW2_MANGLE ip protocol tcp ip daddr $FAKE_IP counter jump PSW2_RULE comment \"默认\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") jump PSW2_RULE comment \"默认\"" + nft "add rule inet fw4 PSW2_MANGLE meta l4proto tcp $(REDIRECT $REDIR_PORT TPROXY) comment \"默认\"" + fi + + [ "$PROXY_IPV6" == "1" ] && { + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp ip6 daddr $FAKE_IP_6 jump PSW2_RULE comment \"默认\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") jump PSW2_RULE comment \"默认\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp $(REDIRECT $REDIR_PORT TPROXY) comment \"默认\"" + } + + echolog "${msg}" + } + fi + + # 加载UDP默认代理模式 + if [ "$UDP_PROXY_MODE" != "disable" ]; then + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && { + nft "add inet fw4 PSW2_MANGLE ip protocol udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return comment \"默认\"" + nft "add inet fw4 PSW2_MANGLE_V6 counter meta l4proto udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return comment \"默认\"" + } + + [ -n "1" ] && { + msg="UDP默认代理:使用UDP节点[$(config_n_get $NODE remarks)] [$(get_action_chain_name $UDP_PROXY_MODE)](TPROXY:${REDIR_PORT})代理" + + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && msg="${msg}除${UDP_NO_REDIR_PORTS}外的" + msg="${msg}所有端口" + + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp ip daddr $FAKE_IP counter jump PSW2_RULE comment \"默认\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") jump PSW2_RULE comment \"默认\"" + nft "add rule inet fw4 PSW2_MANGLE meta l4proto udp $(REDIRECT $REDIR_PORT TPROXY) comment \"默认\"" + + [ "$PROXY_IPV6" == "1" ] && [ "$PROXY_IPV6_UDP" == "1" ] && { + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ip6 daddr $FAKE_IP_6 jump PSW2_RULE comment \"默认\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") jump PSW2_RULE comment \"默认\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp $(REDIRECT $REDIR_PORT TPROXY) comment \"默认\"" + } + + echolog "${msg}" + udp_flag=1 + } + fi + } +} + +filter_haproxy() { + for item in $(uci show $CONFIG | grep ".lbss=" | cut -d "'" -f 2); do + local ip=$(get_host_ip ipv4 $(echo $item | awk -F ":" '{print $1}') 1) + [ -n "$ip" ] && insert_nftset $NFTSET_VPSLIST $ip + done + echolog "加入负载均衡的节点到nftset[$NFTSET_VPSLIST]直连完成" +} + +filter_vps_addr() { + for server_host in $@; do + local vps_ip4=$(get_host_ip "ipv4" ${server_host}) + local vps_ip6=$(get_host_ip "ipv6" ${server_host}) + [ -n "$vps_ip4" ] && insert_nftset $NFTSET_VPSLIST $vps_ip4 + [ -n "$vps_ip6" ] && insert_nftset $NFTSET_VPSLIST6 $vps_ip6 + done +} + +filter_vpsip() { + insert_nftset $NFTSET_VPSLIST $(uci show $CONFIG | grep ".address=" | cut -d "'" -f 2 | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | sed -e "/^$/d") + insert_nftset $NFTSET_VPSLIST6 $(uci show $CONFIG | grep ".address=" | cut -d "'" -f 2 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "/^$/d") + echolog "加入所有节点到nftset[$NFTSET_VPSLIST]直连完成" +} + +filter_node() { + local proxy_node=${1} + local stream=$(echo ${2} | tr 'A-Z' 'a-z') + local proxy_port=${3} + + filter_rules() { + local node=${1} + local stream=${2} + local _proxy=${3} + local _port=${4} + local _is_tproxy msg msg2 + + if [ -n "$node" ] && [ "$node" != "nil" ]; then + local type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z') + local address=$(config_n_get $node address) + local port=$(config_n_get $node port) + _is_tproxy=${is_tproxy} + [ "$stream" == "udp" ] && _is_tproxy="TPROXY" + if [ -n "${_is_tproxy}" ]; then + msg="TPROXY" + else + msg="REDIRECT" + fi + else + echolog " - 节点配置不正常,略过" + return 0 + fi + + local ADD_INDEX=$FORCE_INDEX + for _ipt in 4 6; do + [ "$_ipt" == "4" ] && _ip_type=ip && _set_name=$NFTSET_VPSLIST + [ "$_ipt" == "6" ] && _ip_type=ip6 && _set_name=$NFTSET_VPSLIST6 + nft "list chain inet fw4 $nft_output_chain" 2>/dev/null | grep -q "${address}:${port}" + if [ $? -ne 0 ]; then + unset dst_rule + local dst_rule="jump PSW2_RULE" + msg2="按规则路由(${msg})" + [ -n "${is_tproxy}" ] || { + dst_rule=$(REDIRECT $_port) + msg2="套娃使用(${msg}:${port} -> ${_port})" + } + [ -n "$_proxy" ] && [ "$_proxy" == "1" ] && [ -n "$_port" ] || { + ADD_INDEX=$(RULE_LAST_INDEX "inet fw4" $nft_output_chain $_set_name $FORCE_INDEX) + dst_rule="return" + msg2="直连代理" + } + nft "insert rule inet fw4 $nft_output_chain position $ADD_INDEX meta l4proto $stream $_ip_type daddr $address $stream dport $port $dst_rule comment \"${address}:${port}\"" 2>/dev/null + else + msg2="已配置过的节点," + fi + done + msg="[$?]$(echo ${2} | tr 'a-z' 'A-Z')${msg2}使用链${ADD_INDEX},节点(${type}):${address}:${port}" + #echolog " - ${msg}" + } + + local proxy_protocol=$(config_n_get $proxy_node protocol) + local proxy_type=$(echo $(config_n_get $proxy_node type nil) | tr 'A-Z' 'a-z') + [ "$proxy_type" == "nil" ] && echolog " - 节点配置不正常,略过!:${proxy_node}" && return 0 + if [ "$proxy_protocol" == "_balancing" ]; then + #echolog " - 多节点负载均衡(${proxy_type})..." + proxy_node=$(config_n_get $proxy_node balancing_node) + for _node in $proxy_node; do + filter_rules "$_node" "$stream" + done + elif [ "$proxy_protocol" == "_shunt" ]; then + #echolog " - 按请求目的地址分流(${proxy_type})..." + local default_node=$(config_n_get $proxy_node default_node _direct) + local main_node=$(config_n_get $proxy_node main_node nil) + if [ "$main_node" != "nil" ]; then + filter_rules $main_node $stream + else + if [ "$default_node" != "_direct" ] && [ "$default_node" != "_blackhole" ]; then + filter_rules $default_node $stream + fi + fi +:</dev/null 2>&1 & + #echolog " - 追加到白名单:${ispip}" + done + } + + [ -n "$ISP_DNS6" ] && { + #echolog "处理 ISP IPv6 DNS 例外..." + for ispip6 in $ISP_DNS6; do + insert_nftset $NFTSET_WHITELIST6 $ispip6 >/dev/null 2>&1 & + #echolog " - 追加到白名单:${ispip6}" + done + } + + # 过滤所有节点IP + filter_vpsip > /dev/null 2>&1 & + filter_haproxy > /dev/null 2>&1 & + # Prevent some conditions + filter_vps_addr $(config_n_get $NODE address) > /dev/null 2>&1 & + + accept_icmp=$(config_t_get global_forwarding accept_icmp 0) + accept_icmpv6=$(config_t_get global_forwarding accept_icmpv6 0) + + local tcp_proxy_way=$(config_t_get global_forwarding tcp_proxy_way redirect) + if [ "$tcp_proxy_way" = "redirect" ]; then + unset is_tproxy + nft_prerouting_chain="PSW2_NAT" + nft_output_chain="PSW2_OUTPUT_NAT" + elif [ "$tcp_proxy_way" = "tproxy" ]; then + is_tproxy="TPROXY" + nft_prerouting_chain="PSW2_MANGLE" + nft_output_chain="PSW2_OUTPUT_MANGLE" + fi + + nft "add chain inet fw4 nat_output { type nat hook output priority -1; }" + + nft "add chain inet fw4 PSW2_DIVERT" + nft "flush chain inet fw4 PSW2_DIVERT" + nft "add rule inet fw4 PSW2_DIVERT meta l4proto tcp socket transparent 1 mark set 1 counter accept" + + nft "add chain inet fw4 PSW2_REDIRECT" + nft "flush chain inet fw4 PSW2_REDIRECT" + nft "add rule inet fw4 dstnat jump PSW2_REDIRECT" + + # for ipv4 ipv6 tproxy mark + nft "add chain inet fw4 PSW2_RULE" + nft "flush chain inet fw4 PSW2_RULE" + nft "add rule inet fw4 PSW2_RULE meta mark set ct mark counter" + nft "add rule inet fw4 PSW2_RULE meta mark 1 counter return" + nft "add rule inet fw4 PSW2_RULE tcp flags &(fin|syn|rst|ack) == syn meta mark set mark and 0x0 xor 0x1 counter" + nft "add rule inet fw4 PSW2_RULE meta l4proto udp ct state new meta mark set mark and 0x0 xor 0x1 counter" + nft "add rule inet fw4 PSW2_RULE ct mark set mark counter" + + #ipv4 tproxy mode and udp + nft "add chain inet fw4 PSW2_MANGLE" + nft "flush chain inet fw4 PSW2_MANGLE" + nft "add rule inet fw4 PSW2_MANGLE ip daddr @$NFTSET_LANLIST counter return" + nft "add rule inet fw4 PSW2_MANGLE ip daddr @$NFTSET_VPSLIST counter return" + nft "add rule inet fw4 PSW2_MANGLE ip daddr @$NFTSET_WHITELIST counter return" + + nft "add chain inet fw4 PSW2_OUTPUT_MANGLE" + nft "flush chain inet fw4 PSW2_OUTPUT_MANGLE" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip daddr @$NFTSET_LANLIST counter return" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip daddr @$NFTSET_VPSLIST counter return" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip daddr @$NFTSET_WHITELIST counter return" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE meta mark 0xff counter return" + + # jump chains + nft "add rule inet fw4 mangle_prerouting meta nfproto {ipv4} counter jump PSW2_MANGLE" + insert_rule_before "inet fw4" "mangle_prerouting" "PSW2_MANGLE" "counter jump PSW2_DIVERT" + + #ipv4 tcp redirect mode + [ -z "${is_tproxy}" ] && { + nft "add chain inet fw4 PSW2_NAT" + nft "flush chain inet fw4 PSW2_NAT" + nft "add rule inet fw4 PSW2_NAT ip daddr @$NFTSET_LANLIST counter return" + nft "add rule inet fw4 PSW2_NAT ip daddr @$NFTSET_VPSLIST counter return" + nft "add rule inet fw4 PSW2_NAT ip daddr @$NFTSET_WHITELIST counter return" + nft "add rule inet fw4 dstnat ip protocol tcp counter jump PSW2_NAT" + + nft "add chain inet fw4 PSW2_OUTPUT_NAT" + nft "flush chain inet fw4 PSW2_OUTPUT_NAT" + nft "add rule inet fw4 PSW2_OUTPUT_NAT ip daddr @$NFTSET_LANLIST counter return" + nft "add rule inet fw4 PSW2_OUTPUT_NAT ip daddr @$NFTSET_VPSLIST counter return" + nft "add rule inet fw4 PSW2_OUTPUT_NAT ip daddr @$NFTSET_WHITELIST counter return" + nft "add rule inet fw4 PSW2_OUTPUT_NAT meta mark 0xff counter return" + } + + #icmp ipv6-icmp redirect + if [ "$accept_icmp" = "1" ]; then + nft "add chain inet fw4 PSW2_ICMP_REDIRECT" + nft "flush chain inet fw4 PSW2_ICMP_REDIRECT" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip daddr @$NFTSET_LANLIST counter return" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip daddr @$NFTSET_VPSLIST counter return" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip daddr @$NFTSET_WHITELIST counter return" + + [ "$accept_icmpv6" = "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip6 daddr @$NFTSET_LANLIST6 counter return" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip6 daddr @$NFTSET_VPSLIST6 counter return" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip6 daddr @$NFTSET_WHITELIST6 counter return" + } + + nft "add rule inet fw4 dstnat meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT" + nft "add rule inet fw4 nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT" + fi + + WAN_IP=$(get_wan_ip) + if [ -n "${WAN_IP}" ]; then + [ -n "${is_tproxy}" ] && nft "add rule inet fw4 PSW2_MANGLE ip daddr ${WAN_IP} counter return comment \"WAN_IP_RETURN\"" || nft "add rule inet fw4 PSW2_NAT ip daddr ${WAN_IP} counter return comment \"WAN_IP_RETURN\"" + fi + unset WAN_IP + + ip rule add fwmark 1 lookup 100 + ip route add local 0.0.0.0/0 dev lo table 100 + + #ipv6 tproxy mode and udp + nft "add chain inet fw4 PSW2_MANGLE_V6" + nft "flush chain inet fw4 PSW2_MANGLE_V6" + nft "add rule inet fw4 PSW2_MANGLE_V6 ip6 daddr @$NFTSET_LANLIST6 counter return" + nft "add rule inet fw4 PSW2_MANGLE_V6 ip6 daddr @$NFTSET_VPSLIST6 counter return" + nft "add rule inet fw4 PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WHITELIST6 counter return" + + nft "add chain inet fw4 PSW2_OUTPUT_MANGLE_V6" + nft "flush chain inet fw4 PSW2_OUTPUT_MANGLE_V6" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 ip6 daddr @$NFTSET_LANLIST6 counter return" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 ip6 daddr @$NFTSET_VPSLIST6 counter return" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 ip6 daddr @$NFTSET_WHITELIST6 counter return" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 meta mark 0xff counter return" + + # jump chains + [ "$PROXY_IPV6" == "1" ] && { + nft "add rule inet fw4 mangle_prerouting meta nfproto {ipv6} counter jump PSW2_MANGLE_V6" + nft "add rule inet fw4 mangle_output meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\"" + + WAN6_IP=$(get_wan6_ip) + [ -n "${WAN6_IP}" ] && nft "add rule inet fw4 PSW2_MANGLE_V6 ip6 daddr ${WAN6_IP} counter return comment \"WAN6_IP_RETURN\"" + unset WAN6_IP + + ip -6 rule add fwmark 1 table 100 + ip -6 route add local ::/0 dev lo table 100 + } + + # 过滤Socks节点 + [ "$SOCKS_ENABLED" = "1" ] && { + local ids=$(uci show $CONFIG | grep "=socks" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}') + #echolog "分析 Socks 服务所使用节点..." + local id enabled node port msg num + for id in $ids; do + enabled=$(config_n_get $id enabled 0) + [ "$enabled" == "1" ] || continue + node=$(config_n_get $id node nil) + port=$(config_n_get $id port 0) + msg="Socks 服务 [:${port}]" + if [ "$node" == "nil" ] || [ "$port" == "0" ]; then + msg="${msg} 未配置完全,略过" + else + filter_node $node TCP > /dev/null 2>&1 & + filter_node $node UDP > /dev/null 2>&1 & + fi + #echolog " - ${msg}" + done + } + + [ "$ENABLED_DEFAULT_ACL" == 1 ] && { + # 加载路由器自身代理 TCP + if [ "$NODE" != "nil" ] && [ "$LOCALHOST_PROXY" = "1" ]; then + echolog "加载路由器自身 TCP 代理..." + + [ "$accept_icmp" = "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmp ip daddr $FAKE_IP counter redirect" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmp counter redirect" + } + + [ "$accept_icmpv6" = "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmpv6 ip6 daddr $FAKE_IP_6 counter redirect" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmpv6 counter redirect" + } + + [ -n "${is_tproxy}" ] && { + echolog " - 启用 TPROXY 模式" + } + + [ "$TCP_NO_REDIR_PORTS" != "disable" ] && { + nft "add rule inet fw4 $nft_output_chain ip protocol tcp $(factor $TCP_NO_REDIR_PORTS "tcp dport") counter return" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp $(factor $TCP_NO_REDIR_PORTS "tcp dport") counter return" + echolog " - [$?]不代理TCP 端口:$TCP_NO_REDIR_PORTS" + } + + if [ -z "${is_tproxy}" ]; then + nft "add rule inet fw4 PSW2_OUTPUT_NAT ip protocol tcp ip daddr $FAKE_IP $(REDIRECT $REDIR_PORT)" + nft "add rule inet fw4 PSW2_OUTPUT_NAT ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") $(REDIRECT $REDIR_PORT)" + nft "add rule inet fw4 nat_output ip protocol tcp counter jump PSW2_OUTPUT_NAT" + else + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip protocol tcp ip daddr $FAKE_IP counter jump PSW2_RULE" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") jump PSW2_RULE" + nft "add rule inet fw4 PSW2_MANGLE meta l4proto tcp iif lo $(REDIRECT $REDIR_PORT TPROXY) comment \"本机\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol tcp iif lo counter return comment \"本机\"" + nft "add rule inet fw4 mangle_output meta nfproto {ipv4} meta l4proto tcp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\"" + fi + + [ "$PROXY_IPV6" == "1" ] && { + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp ip6 daddr $FAKE_IP_6 jump PSW2_RULE" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp $(factor $TCP_REDIR_PORTS "tcp dport") jump PSW2_RULE" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp iif lo $(REDIRECT $REDIR_PORT TPROXY) comment \"本机\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp iif lo counter return comment \"本机\"" + } + + for iface in $IFACES; do + nft "insert rule inet fw4 $nft_output_chain ip protocol tcp oif $iface counter return" + nft "insert rule inet fw4 PSW2_OUTPUT_MANGLE_V6 ip protocol tcp oif $iface counter return" + done + fi + + # 处理轮换节点的分流或套娃 + filter_node $NODE TCP > /dev/null 2>&1 & + filter_node $NODE UDP > /dev/null 2>&1 & + + # 加载路由器自身代理 UDP + if [ "$NODE" != "nil" ] && [ "$LOCALHOST_PROXY" = "1" ]; then + echolog "加载路由器自身 UDP 代理..." + + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && { + nft add rule inet fw4 PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return + nft add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return + echolog " - [$?]不代理 UDP 端口:$UDP_NO_REDIR_PORTS" + } + + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip protocol udp ip daddr $FAKE_IP counter jump PSW2_RULE" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") jump PSW2_RULE" + nft "add rule inet fw4 PSW2_MANGLE meta l4proto udp iif lo $(REDIRECT $REDIR_PORT TPROXY) comment \"本机\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp iif lo counter return comment \"本机\"" + nft "add rule inet fw4 mangle_output meta nfproto {ipv4} meta l4proto udp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\"" + + if [ "$PROXY_IPV6_UDP" == "1" ]; then + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr $FAKE_IP_6 jump PSW2_RULE" + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_REDIR_PORTS "udp dport") jump PSW2_RULE" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp iif lo $(REDIRECT $REDIR_PORT TPROXY) comment \"本机\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp iif lo counter return comment \"本机\"" + fi + + for iface in $IFACES; do + nft "insert rule inet fw4 $nft_output_chain ip protocol udp oif $iface counter return" + nft "insert rule inet fw4 PSW2_OUTPUT_MANGLE_V6 ip protocol udp oif $iface counter return" + done + fi + + nft "add rule inet fw4 mangle_output oif lo counter return comment \"PSW2_OUTPUT_MANGLE\"" + nft "add rule inet fw4 mangle_output meta mark 1 counter return comment \"PSW2_OUTPUT_MANGLE\"" + + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp udp dport 53 counter return" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp udp dport 53 counter return" + } + + # 加载ACLS + load_acl + + [ -n "${is_tproxy}" -o -n "${udp_flag}" ] && { + bridge_nf_ipt=$(sysctl -e -n net.bridge.bridge-nf-call-iptables) + echo -n $bridge_nf_ipt > $TMP_PATH/bridge_nf_ipt + sysctl -w net.bridge.bridge-nf-call-iptables=0 >/dev/null 2>&1 + [ "$PROXY_IPV6" == "1" ] && { + bridge_nf_ip6t=$(sysctl -e -n net.bridge.bridge-nf-call-ip6tables) + echo -n $bridge_nf_ip6t > $TMP_PATH/bridge_nf_ip6t + sysctl -w net.bridge.bridge-nf-call-ip6tables=0 >/dev/null 2>&1 + } + } + echolog "防火墙规则加载完成!" +} + +del_firewall_rule() { + for nft in "forward" "dstnat" "srcnat" "nat_output" "mangle_prerouting" "mangle_output"; do + local handles=$(nft -a list chain inet fw4 ${nft} 2>/dev/null | grep -E "PSW2_" | awk -F '# handle ' '{print$2}') + for handle in $handles; do + nft delete rule inet fw4 ${nft} handle ${handle} 2>/dev/null + done + done + + for handle in $(nft -a list chains | grep -E "chain PSW2_" | grep -v "PSW2_RULE" | awk -F '# handle ' '{print$2}'); do + nft delete chain inet fw4 handle ${handle} 2>/dev/null + done + + # Need to be removed at the end, otherwise it will show "Resource busy" + nft delete chain inet fw4 handle $(nft -a list chains | grep -E "PSW2_RULE" | awk -F '# handle ' '{print$2}') 2>/dev/null + + ip rule del fwmark 1 lookup 100 2>/dev/null + ip route del local 0.0.0.0/0 dev lo table 100 2>/dev/null + + ip -6 rule del fwmark 1 table 100 2>/dev/null + ip -6 route del local ::/0 dev lo table 100 2>/dev/null + + destroy_nftset $NFTSET_LANLIST + destroy_nftset $NFTSET_VPSLIST + destroy_nftset $NFTSET_WHITELIST + + destroy_nftset $NFTSET_LANLIST6 + destroy_nftset $NFTSET_VPSLIST6 + destroy_nftset $NFTSET_WHITELIST6 + + $DIR/app.sh echolog "删除相关防火墙规则完成。" +} + +flush_nftset() { + del_firewall_rule + destroy_nftset $NFTSET_VPSLIST $NFTSET_WHITELIST $NFTSET_LANLIST + destroy_nftset $NFTSET_VPSLIST6 $NFTSET_WHITELIST6 $NFTSET_LANLIST6 + /etc/init.d/passwall2 reload +} + +flush_include() { + echo '#!/bin/sh' >$FWI +} + +gen_include() { + local nft_chain_file=$TMP_PATH/PSW2_RULE.nft + local nft_set_file=$TMP_PATH/PSW2_SETS.nft + echo "#!/usr/sbin/nft -f" > $nft_chain_file + echo "#!/usr/sbin/nft -f" > $nft_set_file + for chain in $(nft -a list chains | grep -E "chain PSW2_" | awk -F ' ' '{print$2}'); do + nft list chain inet fw4 ${chain} >> $nft_chain_file + done + + for set_name in $(nft -a list sets | grep -E "set passwall2_" | awk -F ' ' '{print$2}'); do + nft list set inet fw4 ${set_name} >> $nft_set_file + done + + local __nft=" " + __nft=$(cat <<- EOF + + [ -z "\$(nft list sets 2>/dev/null | grep "passwall2_")" ] && nft -f ${nft_set_file} + [ -z "\$(nft list chain inet fw4 nat_output 2>/dev/null)" ] && nft "add chain inet fw4 nat_output { type nat hook output priority -1; }" + nft -f ${nft_chain_file} + + nft "add rule inet fw4 dstnat jump PSW2_REDIRECT" + + [ "$accept_icmp" == "1" ] && { + nft "add rule inet fw4 dstnat meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT" + nft "add rule inet fw4 nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT" + } + + [ -z "${is_tproxy}" ] && { + PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "inet fw4" PSW2_NAT WAN_IP_RETURN -1) + if [ \$PR_INDEX -ge 0 ]; then + WAN_IP=\$(sh ${MY_PATH} get_wan_ip) + [ ! -z "\${WAN_IP}" ] && nft "replace rule inet fw4 PSW2_NAT handle \$PR_INDEX ip daddr "\${WAN_IP}" counter return comment \"WAN_IP_RETURN\"" + fi + nft "add rule inet fw4 dstnat ip protocol tcp counter jump PSW2_NAT" + nft "add rule inet fw4 nat_output ip protocol tcp counter jump PSW2_OUTPUT_NAT" + } + + [ -n "${is_tproxy}" ] && { + PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "inet fw4" PSW2_MANGLE WAN_IP_RETURN -1) + if [ \$PR_INDEX -ge 0 ]; then + WAN_IP=\$(sh ${MY_PATH} get_wan_ip) + [ ! -z "\${WAN_IP}" ] && nft "replace rule inet fw4 PSW2_MANGLE handle \$PR_INDEX ip daddr "\${WAN_IP}" counter return comment \"WAN_IP_RETURN\"" + fi + nft "add rule inet fw4 mangle_prerouting meta nfproto {ipv4} counter jump PSW2_MANGLE" + nft "add rule inet fw4 mangle_output meta nfproto {ipv4} meta l4proto tcp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\"" + } + \$(sh ${MY_PATH} insert_rule_before "inet fw4" "mangle_prerouting" "PSW2_MANGLE" "counter jump PSW2_DIVERT") + + [ "$UDP_NODE" != "nil" -o "$TCP_UDP" = "1" ] && nft "add rule inet fw4 mangle_output meta nfproto {ipv4} meta l4proto udp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\"" + + [ "$PROXY_IPV6" == "1" ] && { + PR_INDEX=\$(sh ${MY_PATH} RULE_LAST_INDEX "inet fw4" PSW2_MANGLE_V6 WAN6_IP_RETURN -1) + if [ \$PR_INDEX -ge 0 ]; then + WAN6_IP=\$(sh ${MY_PATH} get_wan6_ip) + [ ! -z "\${WAN_IP}" ] && nft "replace rule inet fw4 PSW2_MANGLE_V6 handle \$PR_INDEX ip6 daddr "\${WAN6_IP}" counter return comment \"WAN6_IP_RETURN\"" + fi + nft "add rule inet fw4 mangle_prerouting meta nfproto {ipv6} counter jump PSW2_MANGLE_V6" + nft "add rule inet fw4 mangle_output meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\"" + } + + nft "add rule inet fw4 mangle_output oif lo counter return comment \"PSW2_OUTPUT_MANGLE\"" + nft "add rule inet fw4 mangle_output meta mark 1 counter return comment \"PSW2_OUTPUT_MANGLE\"" + EOF + ) + + cat <<-EOF >> $FWI + ${__nft} + EOF + return 0 +} + +start() { + [ "$ENABLED_DEFAULT_ACL" == 0 -a "$ENABLED_ACLS" == 0 ] && return + add_firewall_rule + gen_include +} + +stop() { + del_firewall_rule + flush_include +} + +arg1=$1 +shift +case $arg1 in +RULE_LAST_INDEX) + RULE_LAST_INDEX "$@" + ;; +insert_rule_before) + insert_rule_before "$@" + ;; +insert_rule_after) + insert_rule_after "$@" + ;; +flush_nftset) + flush_nftset + ;; +get_wan_ip) + get_wan_ip + ;; +get_wan6_ip) + get_wan6_ip + ;; +stop) + stop + ;; +start) + start + ;; +*) ;; +esac diff --git a/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua b/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua new file mode 100755 index 000000000..2cb609726 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua @@ -0,0 +1,194 @@ +#!/usr/bin/lua + +require 'nixio' +require 'luci.sys' +local luci = luci +local ucic = luci.model.uci.cursor() +local jsonc = require "luci.jsonc" +local name = 'passwall2' +local api = require "luci.passwall2.api" +local arg1 = arg[1] + +local reboot = 0 +local geoip_update = 0 +local geosite_update = 0 +local v2ray_asset_location = ucic:get_first(name, 'global_rules', "v2ray_location_asset", "/usr/share/v2ray/") + +-- Custom geo file +local geoip_api = ucic:get_first(name, 'global_rules', "geoip_url", "https://api.github.com/repos/Loyalsoldier/v2ray-rules-dat/releases/latest") +local geosite_api = ucic:get_first(name, 'global_rules', "geosite_url", "https://api.github.com/repos/Loyalsoldier/v2ray-rules-dat/releases/latest") +-- +local use_nft = ucic:get(name, "@global_forwarding[0]", "use_nft") or "0" + +local log = function(...) + if arg1 then + if arg1 == "log" then + api.log(...) + elseif arg1 == "print" then + local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ") + print(result) + end + end +end + +-- curl +local function curl(url, file) + local args = { + "-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3" + } + if file then + args[#args + 1] = "-o " .. file + end + local return_code, result = api.curl_logic(url, nil, args) + return tonumber(result) +end + +--获取geoip +local function fetch_geoip() + --请求geoip + xpcall(function() + local return_code, content = api.curl_logic(geoip_api) + local json = jsonc.parse(content) + if json.tag_name and json.assets then + for _, v in ipairs(json.assets) do + if v.name and v.name == "geoip.dat.sha256sum" then + local sret = curl(v.browser_download_url, "/tmp/geoip.dat.sha256sum") + if sret == 200 then + local f = io.open("/tmp/geoip.dat.sha256sum", "r") + local content = f:read() + f:close() + f = io.open("/tmp/geoip.dat.sha256sum", "w") + f:write(content:gsub("geoip.dat", "/tmp/geoip.dat"), "") + f:close() + + if nixio.fs.access(v2ray_asset_location .. "geoip.dat") then + luci.sys.call(string.format("cp -f %s %s", v2ray_asset_location .. "geoip.dat", "/tmp/geoip.dat")) + if luci.sys.call('sha256sum -c /tmp/geoip.dat.sha256sum > /dev/null 2>&1') == 0 then + log("geoip 版本一致,无需更新。") + return 1 + end + end + for _2, v2 in ipairs(json.assets) do + if v2.name and v2.name == "geoip.dat" then + sret = curl(v2.browser_download_url, "/tmp/geoip.dat") + if luci.sys.call('sha256sum -c /tmp/geoip.dat.sha256sum > /dev/null 2>&1') == 0 then + luci.sys.call(string.format("mkdir -p %s && cp -f %s %s", v2ray_asset_location, "/tmp/geoip.dat", v2ray_asset_location .. "geoip.dat")) + reboot = 1 + log("geoip 更新成功。") + return 1 + else + log("geoip 更新失败,请稍后再试。") + end + break + end + end + end + break + end + end + end + end, + function(e) + end) + + return 0 +end + +--获取geosite +local function fetch_geosite() + --请求geosite + xpcall(function() + local return_code, content = api.curl_logic(geosite_api) + local json = jsonc.parse(content) + if json.tag_name and json.assets then + for _, v in ipairs(json.assets) do + if v.name and v.name == "geosite.dat.sha256sum" then + local sret = curl(v.browser_download_url, "/tmp/geosite.dat.sha256sum") + if sret == 200 then + local f = io.open("/tmp/geosite.dat.sha256sum", "r") + local content = f:read() + f:close() + f = io.open("/tmp/geosite.dat.sha256sum", "w") + f:write(content:gsub("geosite.dat", "/tmp/geosite.dat"), "") + f:close() + + if nixio.fs.access(v2ray_asset_location .. "geosite.dat") then + luci.sys.call(string.format("cp -f %s %s", v2ray_asset_location .. "geosite.dat", "/tmp/geosite.dat")) + if luci.sys.call('sha256sum -c /tmp/geosite.dat.sha256sum > /dev/null 2>&1') == 0 then + log("geosite 版本一致,无需更新。") + return 1 + end + end + for _2, v2 in ipairs(json.assets) do + if v2.name and v2.name == "geosite.dat" then + sret = curl(v2.browser_download_url, "/tmp/geosite.dat") + if luci.sys.call('sha256sum -c /tmp/geosite.dat.sha256sum > /dev/null 2>&1') == 0 then + luci.sys.call(string.format("mkdir -p %s && cp -f %s %s", v2ray_asset_location, "/tmp/geosite.dat", v2ray_asset_location .. "geosite.dat")) + reboot = 1 + log("geosite 更新成功。") + return 1 + else + log("geosite 更新失败,请稍后再试。") + end + break + end + end + end + break + end + end + end + end, + function(e) + end) + + return 0 +end + +if arg[2] then + string.gsub(arg[2], '[^' .. "," .. ']+', function(w) + if w == "geoip" then + geoip_update = 1 + end + if w == "geosite" then + geosite_update = 1 + end + end) +else + geoip_update = ucic:get_first(name, 'global_rules', "geoip_update", 1) + geosite_update = ucic:get_first(name, 'global_rules', "geosite_update", 1) +end +if geoip_update == 0 and geosite_update == 0 then + os.exit(0) +end + +log("开始更新规则...") + +if tonumber(geoip_update) == 1 then + log("geoip 开始更新...") + local status = fetch_geoip() + os.remove("/tmp/geoip.dat") + os.remove("/tmp/geoip.dat.sha256sum") +end + +if tonumber(geosite_update) == 1 then + log("geosite 开始更新...") + local status = fetch_geosite() + os.remove("/tmp/geosite.dat") + os.remove("/tmp/geosite.dat.sha256sum") +end + +ucic:set(name, ucic:get_first(name, 'global_rules'), "geoip_update", geoip_update) +ucic:set(name, ucic:get_first(name, 'global_rules'), "geosite_update", geosite_update) +ucic:save(name) +luci.sys.call("uci commit " .. name) + +if reboot == 1 then + log("重启服务,应用新的规则。") + if use_nft == "1" then + luci.sys.call("sh /usr/share/" .. name .. "/nftables.sh flush_nftset > /dev/null 2>&1 &") + else + luci.sys.call("sh /usr/share/" .. name .. "/iptables.sh flush_ipset > /dev/null 2>&1 &") + end +end +log("规则更新完毕...") diff --git a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua new file mode 100755 index 000000000..dd9b32976 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -0,0 +1,1173 @@ +#!/usr/bin/lua + +------------------------------------------------ +-- @author William Chan +------------------------------------------------ +require 'nixio' +require 'luci.model.uci' +require 'luci.util' +require 'luci.jsonc' +require 'luci.sys' +local appname = 'passwall2' +local api = require ("luci.passwall2.api") +local datatypes = require "luci.cbi.datatypes" + +-- these global functions are accessed all the time by the event handler +-- so caching them is worth the effort +local tinsert = table.insert +local ssub, slen, schar, sbyte, sformat, sgsub = string.sub, string.len, string.char, string.byte, string.format, string.gsub +local jsonParse, jsonStringify = luci.jsonc.parse, luci.jsonc.stringify +local base64Decode = api.base64Decode +local uci = luci.model.uci.cursor() +uci:revert(appname) + +local has_ss = api.is_finded("ss-redir") +local has_ss_rust = api.is_finded("sslocal") +local has_v2ray = api.is_finded("v2ray") +local has_xray = api.is_finded("xray") +local allowInsecure_default = true +local ss_aead_type_default = uci:get(appname, "@global_subscribe[0]", "ss_aead_type") or "shadowsocks-libev" +-- 判断是否过滤节点关键字 +local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" +local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} +local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} +local function is_filter_keyword(value) + if filter_keyword_mode_default == "1" then + for k,v in ipairs(filter_keyword_discard_list_default) do + if value:find(v, 1, true) then + return true + end + end + elseif filter_keyword_mode_default == "2" then + local result = true + for k,v in ipairs(filter_keyword_keep_list_default) do + if value:find(v, 1, true) then + result = false + end + end + return result + elseif filter_keyword_mode_default == "3" then + local result = false + for k,v in ipairs(filter_keyword_discard_list_default) do + if value:find(v, 1, true) then + result = true + end + end + for k,v in ipairs(filter_keyword_keep_list_default) do + if value:find(v, 1, true) then + result = false + end + end + return result + elseif filter_keyword_mode_default == "4" then + local result = true + for k,v in ipairs(filter_keyword_keep_list_default) do + if value:find(v, 1, true) then + result = false + end + end + for k,v in ipairs(filter_keyword_discard_list_default) do + if value:find(v, 1, true) then + result = true + end + end + return result + end + return false +end + +local nodeResult = {} -- update result +local debug = false + +local log = function(...) + if debug == true then + local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ") + print(result) + else + api.log(...) + end +end + +-- 获取各项动态配置的当前服务器,可以用 get 和 set, get必须要获取到节点表 +local CONFIG = {} +do + if true then + local szType = "@global[0]" + local option = "node" + + local node_id = uci:get(appname, szType, option) + CONFIG[#CONFIG + 1] = { + log = true, + remarks = "节点", + currentNode = node_id and uci:get_all(appname, node_id) or nil, + set = function(o, server) + uci:set(appname, szType, option, server) + o.newNodeId = server + end + } + end + + if true then + local i = 0 + local option = "node" + uci:foreach(appname, "socks", function(t) + i = i + 1 + local node_id = t[option] + CONFIG[#CONFIG + 1] = { + log = true, + id = t[".name"], + remarks = "Socks节点列表[" .. i .. "]", + currentNode = node_id and uci:get_all(appname, node_id) or nil, + set = function(o, server) + uci:set(appname, t[".name"], option, server) + o.newNodeId = server + end + } + end) + end + + if true then + local i = 0 + local option = "lbss" + uci:foreach(appname, "haproxy_config", function(t) + i = i + 1 + local node_id = t[option] + CONFIG[#CONFIG + 1] = { + log = true, + id = t[".name"], + remarks = "HAProxy负载均衡节点列表[" .. i .. "]", + currentNode = node_id and uci:get_all(appname, node_id) or nil, + set = function(o, server) + uci:set(appname, t[".name"], option, server) + o.newNodeId = server + end + } + end) + end + + if true then + local i = 0 + uci:foreach(appname, "acl_rule", function(t) + i = i + 1 + local option = "node" + local node_id = t[option] + CONFIG[#CONFIG + 1] = { + log = true, + id = t[".name"], + remarks = "访问控制列表[" .. i .. "]", + currentNode = node_id and uci:get_all(appname, node_id) or nil, + set = function(o, server) + uci:set(appname, t[".name"], option, server) + o.newNodeId = server + end + } + end) + end + + local node_table = uci:get(appname, "@auto_switch[0]", "node") + if node_table then + local nodes = {} + local new_nodes = {} + for k,node_id in ipairs(node_table) do + if node_id then + local currentNode = uci:get_all(appname, node_id) or nil + if currentNode then + if currentNode.protocol and (currentNode.protocol == "_balancing" or currentNode.protocol == "_shunt") then + currentNode = nil + end + nodes[#nodes + 1] = { + log = true, + remarks = "备用节点的列表[" .. k .. "]", + currentNode = currentNode, + set = function(o, server) + for kk, vv in pairs(CONFIG) do + if (vv.remarks == "备用节点的列表") then + table.insert(vv.new_nodes, server) + end + end + end + } + end + end + end + CONFIG[#CONFIG + 1] = { + remarks = "备用节点的列表", + nodes = nodes, + new_nodes = new_nodes, + set = function(o) + for kk, vv in pairs(CONFIG) do + if (vv.remarks == "备用节点的列表") then + --log("刷新自动切换的备用节点的列表") + uci:set_list(appname, "@auto_switch[0]", "node", vv.new_nodes) + end + end + end + } + end + + uci:foreach(appname, "nodes", function(node) + if node.protocol and node.protocol == '_shunt' then + local node_id = node[".name"] + + local rules = {} + uci:foreach(appname, "shunt_rules", function(e) + if e[".name"] and e.remarks then + table.insert(rules, e) + end + end) + table.insert(rules, { + [".name"] = "default_node", + remarks = "默认" + }) + table.insert(rules, { + [".name"] = "main_node", + remarks = "默认前置" + }) + + for k, e in pairs(rules) do + local _node_id = node[e[".name"]] or nil + CONFIG[#CONFIG + 1] = { + log = false, + currentNode = _node_id and uci:get_all(appname, _node_id) or nil, + remarks = "分流" .. e.remarks .. "节点", + set = function(o, server) + uci:set(appname, node_id, e[".name"], server) + o.newNodeId = server + end + } + end + elseif node.protocol and node.protocol == '_balancing' then + local node_id = node[".name"] + local nodes = {} + local new_nodes = {} + if node.balancing_node then + for k, node in pairs(node.balancing_node) do + nodes[#nodes + 1] = { + log = false, + node = node, + currentNode = node and uci:get_all(appname, node) or nil, + remarks = node, + set = function(o, server) + for kk, vv in pairs(CONFIG) do + if (vv.remarks == "负载均衡节点列表" .. node_id) then + table.insert(vv.new_nodes, server) + end + end + end + } + end + end + CONFIG[#CONFIG + 1] = { + remarks = "负载均衡节点列表" .. node_id, + nodes = nodes, + new_nodes = new_nodes, + set = function(o) + for kk, vv in pairs(CONFIG) do + if (vv.remarks == "负载均衡节点列表" .. node_id) then + --log("刷新负载均衡节点列表") + uci:foreach(appname, "nodes", function(node2) + if node2[".name"] == node[".name"] then + local index = node2[".index"] + uci:set_list(appname, "@nodes[" .. index .. "]", "balancing_node", vv.new_nodes) + end + end) + end + end + end + } + end + end) + + for k, v in pairs(CONFIG) do + if v.nodes and type(v.nodes) == "table" then + for kk, vv in pairs(v.nodes) do + if vv.currentNode == nil then + CONFIG[k].nodes[kk] = nil + end + end + else + if v.currentNode == nil then + CONFIG[k] = nil + end + end + end +end + +-- 分割字符串 +local function split(full, sep) + if full then + full = full:gsub("%z", "") -- 这里不是很清楚 有时候结尾带个\0 + local off, result = 1, {} + while true do + local nStart, nEnd = full:find(sep, off) + if not nEnd then + local res = ssub(full, off, slen(full)) + if #res > 0 then -- 过滤掉 \0 + tinsert(result, res) + end + break + else + tinsert(result, ssub(full, off, nStart - 1)) + off = nEnd + 1 + end + end + return result + end + return {} +end +-- urlencode +-- local function get_urlencode(c) return sformat("%%%02X", sbyte(c)) end + +-- local function urlEncode(szText) +-- local str = szText:gsub("([^0-9a-zA-Z ])", get_urlencode) +-- str = str:gsub(" ", "+") +-- return str +-- end + +local function get_urldecode(h) return schar(tonumber(h, 16)) end +local function UrlDecode(szText) + return (szText and szText:gsub("+", " "):gsub("%%(%x%x)", get_urldecode)) or nil +end + +-- trim +local function trim(text) + if not text or text == "" then return "" end + return (sgsub(text, "^%s*(.-)%s*$", "%1")) +end + +-- 处理数据 +local function processData(szType, content, add_mode, add_from) + --log(content, add_mode, add_from) + local result = { + timeout = 60, + add_mode = add_mode, --0为手动配置,1为导入,2为订阅 + add_from = add_from + } + --ssr://base64(host:port:protocol:method:obfs:base64pass/?obfsparam=base64param&protoparam=base64param&remarks=base64remarks&group=base64group&udpport=0&uot=0) + if szType == 'ssr' then + result.type = "SSR" + + local dat = split(content, "/%?") + local hostInfo = split(dat[1], ':') + if dat[1]:match('%[(.*)%]') then + result.address = dat[1]:match('%[(.*)%]') + else + result.address = hostInfo[#hostInfo-5] + end + result.port = hostInfo[#hostInfo-4] + result.protocol = hostInfo[#hostInfo-3] + result.method = hostInfo[#hostInfo-2] + result.obfs = hostInfo[#hostInfo-1] + result.password = base64Decode(hostInfo[#hostInfo]) + local params = {} + for _, v in pairs(split(dat[2], '&')) do + local t = split(v, '=') + params[t[1]] = t[2] + end + result.obfs_param = base64Decode(params.obfsparam) + result.protocol_param = base64Decode(params.protoparam) + local group = base64Decode(params.group) + if group then result.group = group end + result.remarks = base64Decode(params.remarks) + elseif szType == 'vmess' then + local info = jsonParse(content) + result.type = 'V2ray' + if has_xray then + result.type = 'Xray' + end + result.address = info.add + result.port = info.port + result.protocol = 'vmess' + result.uuid = info.id + result.remarks = info.ps + -- result.mux = 1 + -- result.mux_concurrency = 8 + info.net = string.lower(info.net) + if info.net == 'ws' then + result.ws_host = info.host + result.ws_path = info.path + end + if info.net == 'h2' then + result.h2_host = info.host + result.h2_path = info.path + end + if info.net == 'tcp' then + if info.type and info.type ~= "http" then + info.type = "none" + end + result.tcp_guise = info.type + result.tcp_guise_http_host = info.host + result.tcp_guise_http_path = info.path + end + if info.net == 'kcp' or info.net == 'mkcp' then + info.net = "mkcp" + result.mkcp_guise = info.type + result.mkcp_mtu = 1350 + result.mkcp_tti = 50 + result.mkcp_uplinkCapacity = 5 + result.mkcp_downlinkCapacity = 20 + result.mkcp_readBufferSize = 2 + result.mkcp_writeBufferSize = 2 + end + if info.net == 'quic' then + result.quic_guise = info.type + result.quic_key = info.key + result.quic_security = info.securty + end + if info.net == 'grpc' then + result.grpc_serviceName = info.path + end + result.transport = info.net + if not info.security then result.security = "auto" end + if info.tls == "tls" or info.tls == "1" then + result.tls = "1" + result.tls_serverName = (info.sni and info.sni ~= "") and info.sni or info.host + result.tls_allowInsecure = allowInsecure_default and "1" or "0" + else + result.tls = "0" + end + elseif szType == "ss" then + result.type = "SS" + + --SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ] + --userinfo = websafe-base64-encode-utf8(method ":" password) + --ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1 + --ss://cmM0LW1kNTpwYXNzd2Q@192.168.100.1:8888/?plugin=obfs-local%3Bobfs%3Dhttp#Example2 + --ss://2022-blake3-aes-256-gcm:YctPZ6U7xPPcU%2Bgp3u%2B0tx%2FtRizJN9K8y%2BuKlW2qjlI%3D@192.168.100.1:8888#Example3 + --ss://2022-blake3-aes-256-gcm:YctPZ6U7xPPcU%2Bgp3u%2B0tx%2FtRizJN9K8y%2BuKlW2qjlI%3D@192.168.100.1:8888/?plugin=v2ray-plugin%3Bserver#Example3 + + local idx_sp = 0 + local alias = "" + if content:find("#") then + idx_sp = content:find("#") + alias = content:sub(idx_sp + 1, -1) + end + result.remarks = UrlDecode(alias) + local info = content:sub(1, idx_sp - 1) + if info:find("/%?") then + local find_index = info:find("/%?") + local query = split(info, "/%?") + local params = {} + for _, v in pairs(split(query[2], '&')) do + local t = split(v, '=') + params[t[1]] = t[2] + end + if params.plugin then + local plugin_info = UrlDecode(params.plugin) + local idx_pn = plugin_info:find(";") + if idx_pn then + result.plugin = plugin_info:sub(1, idx_pn - 1) + result.plugin_opts = + plugin_info:sub(idx_pn + 1, #plugin_info) + else + result.plugin = plugin_info + end + end + if result.plugin and result.plugin == "simple-obfs" then + result.plugin = "obfs-local" + end + info = info:sub(1, find_index - 1) + end + + local hostInfo = split(base64Decode(info), "@") + if hostInfo and #hostInfo > 0 then + local host_port = hostInfo[#hostInfo] + -- [2001:4860:4860::8888]:443 + -- 8.8.8.8:443 + if host_port:find(":") then + local sp = split(host_port, ":") + result.port = sp[#sp] + if api.is_ipv6addrport(host_port) then + result.address = api.get_ipv6_only(host_port) + else + result.address = sp[1] + end + else + result.address = host_port + end + + local userinfo = nil + if #hostInfo > 2 then + userinfo = {} + for i = 1, #hostInfo - 1 do + tinsert(userinfo, hostInfo[i]) + end + userinfo = table.concat(userinfo, '@') + else + userinfo = base64Decode(hostInfo[1]) + end + + local method = userinfo:sub(1, userinfo:find(":") - 1) + local password = userinfo:sub(userinfo:find(":") + 1, #userinfo) + result.method = method + result.password = password + + local aead = false + for k, v in ipairs({"aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305"}) do + if method:lower() == v:lower() then + aead = true + end + end + if aead then + if ss_aead_type_default == "shadowsocks-libev" and has_ss then + result.type = "SS" + elseif ss_aead_type_default == "shadowsocks-rust" and has_ss_rust then + result.type = 'SS-Rust' + if method:lower() == "chacha20-poly1305" then + result.method = "chacha20-ietf-poly1305" + end + elseif ss_aead_type_default == "v2ray" and has_v2ray and not result.plugin then + result.type = 'V2ray' + result.protocol = 'shadowsocks' + result.transport = 'tcp' + if method:lower() == "chacha20-ietf-poly1305" then + result.method = "chacha20-poly1305" + end + elseif ss_aead_type_default == "xray" and has_xray and not result.plugin then + result.type = 'Xray' + result.protocol = 'shadowsocks' + result.transport = 'tcp' + if method:lower() == "chacha20-ietf-poly1305" then + result.method = "chacha20-poly1305" + end + end + end + end + elseif szType == "trojan" then + local alias = "" + if content:find("#") then + local idx_sp = content:find("#") + alias = content:sub(idx_sp + 1, -1) + content = content:sub(0, idx_sp - 1) + end + result.remarks = UrlDecode(alias) + result.type = 'V2ray' + if has_xray then + result.type = 'Xray' + end + result.protocol = 'trojan' + if content:find("@") then + local Info = split(content, "@") + result.password = UrlDecode(Info[1]) + local port = "443" + Info[2] = (Info[2] or ""):gsub("/%?", "?") + local query = split(Info[2], "?") + local host_port = query[1] + local params = {} + for _, v in pairs(split(query[2], '&')) do + local t = split(v, '=') + params[string.lower(t[1])] = UrlDecode(t[2]) + end + -- [2001:4860:4860::8888]:443 + -- 8.8.8.8:443 + if host_port:find(":") then + local sp = split(host_port, ":") + port = sp[#sp] + if api.is_ipv6addrport(host_port) then + result.address = api.get_ipv6_only(host_port) + else + result.address = sp[1] + end + else + result.address = host_port + end + + local peer, sni = nil, "" + if params.peer then peer = params.peer end + sni = params.sni and params.sni or "" + if params.ws and params.ws == "1" then + result.trojan_transport = "ws" + if params.wshost then result.ws_host = params.wshost end + if params.wspath then result.ws_path = params.wspath end + if sni == "" and params.wshost then sni = params.wshost end + end + result.port = port + result.tls = '1' + result.tls_serverName = peer and peer or sni + if params.allowinsecure then + if params.allowinsecure == "1" or params.allowinsecure == "0" then + result.tls_allowInsecure = params.allowinsecure + else + result.tls_allowInsecure = string.lower(params.allowinsecure) == "true" and "1" or "0" + end + --log(result.remarks .. ' 使用节点AllowInsecure设定: '.. result.tls_allowInsecure) + else + result.tls_allowInsecure = allowInsecure_default and "1" or "0" + end + end + elseif szType == "ssd" then + result.type = "SS" + result.address = content.server + result.port = content.port + result.password = content.password + result.method = content.encryption + result.plugin = content.plugin + result.plugin_opts = content.plugin_options + result.group = content.airport + result.remarks = content.remarks + elseif szType == "vless" then + result.type = 'V2ray' + if has_xray then + result.type = 'Xray' + end + result.protocol = "vless" + local alias = "" + if content:find("#") then + local idx_sp = content:find("#") + alias = content:sub(idx_sp + 1, -1) + content = content:sub(0, idx_sp - 1) + end + result.remarks = UrlDecode(alias) + if content:find("@") then + local Info = split(content, "@") + result.uuid = UrlDecode(Info[1]) + local port = "443" + Info[2] = (Info[2] or ""):gsub("/%?", "?") + local query = split(Info[2], "?") + local host_port = query[1] + local params = {} + for _, v in pairs(split(query[2], '&')) do + local t = split(v, '=') + params[t[1]] = UrlDecode(t[2]) + end + -- [2001:4860:4860::8888]:443 + -- 8.8.8.8:443 + if host_port:find(":") then + local sp = split(host_port, ":") + port = sp[#sp] + if api.is_ipv6addrport(host_port) then + result.address = api.get_ipv6_only(host_port) + else + result.address = sp[1] + end + else + result.address = host_port + end + + params.type = string.lower(params.type) + if params.type == 'ws' then + result.ws_host = params.host + result.ws_path = params.path + end + if params.type == 'h2' or params.type == 'http' then + params.type = "h2" + result.h2_host = params.host + result.h2_path = params.path + end + if params.type == 'tcp' then + result.tcp_guise = params.headerType or "none" + result.tcp_guise_http_host = params.host + result.tcp_guise_http_path = params.path + end + if params.type == 'kcp' or params.type == 'mkcp' then + params.type = "mkcp" + result.mkcp_guise = params.headerType or "none" + result.mkcp_mtu = 1350 + result.mkcp_tti = 50 + result.mkcp_uplinkCapacity = 5 + result.mkcp_downlinkCapacity = 20 + result.mkcp_readBufferSize = 2 + result.mkcp_writeBufferSize = 2 + end + if params.type == 'quic' then + result.quic_guise = params.headerType or "none" + result.quic_key = params.key + result.quic_security = params.quicSecurity or "none" + end + if params.type == 'grpc' then + if params.path then result.grpc_serviceName = params.path end + if params.serviceName then result.grpc_serviceName = params.serviceName end + result.grpc_mode = params.mode + end + result.transport = params.type + + result.encryption = params.encryption or "none" + + result.tls = "0" + if params.security == "tls" or params.security == "reality" then + result.tls = "1" + result.tlsflow = params.flow or nil + result.tls_serverName = (params.sni and params.sni ~= "") and params.sni or params.host + result.fingerprint = (params.fp and params.fp ~= "") and params.fp or "chrome" + if params.security == "reality" then + result.reality = "1" + result.reality_publicKey = params.pbk or nil + result.reality_shortId = params.sid or nil + result.reality_spiderX = params.spx or nil + end + end + + result.port = port + result.tls_allowInsecure = allowInsecure_default and "1" or "0" + end + elseif szType == 'hysteria' then + local alias = "" + if content:find("#") then + local idx_sp = content:find("#") + alias = content:sub(idx_sp + 1, -1) + content = content:sub(0, idx_sp - 1) + end + result.remarks = UrlDecode(alias) + result.type = "Hysteria" + + local dat = split(content, '%?') + local host_port = dat[1] + local params = {} + for _, v in pairs(split(dat[2], '&')) do + local t = split(v, '=') + if #t > 0 then + params[t[1]] = t[2] + end + end + -- [2001:4860:4860::8888]:443 + -- 8.8.8.8:443 + if host_port:find(":") then + local sp = split(host_port, ":") + result.port = sp[#sp] + if api.is_ipv6addrport(host_port) then + result.address = api.get_ipv6_only(host_port) + else + result.address = sp[1] + end + else + result.address = host_port + end + result.protocol = params.protocol + result.hysteria_obfs = params.obfsParam + result.hysteria_auth_type = "string" + result.hysteria_auth_password = params.auth + result.tls_serverName = params.peer + if params.insecure and params.insecure == "1" then + result.tls_allowInsecure = "1" + end + result.hysteria_alpn = params.alpn + result.hysteria_up_mbps = params.upmbps + result.hysteria_down_mbps = params.downmbps + else + log('暂时不支持' .. szType .. "类型的节点订阅,跳过此节点。") + return nil + end + if not result.remarks or result.remarks == "" then + if result.address and result.port then + result.remarks = result.address .. ':' .. result.port + else + result.remarks = "NULL" + end + end + return result +end + +local function curl(url, file, ua) + if not ua or ua == "" then + ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36" + end + local args = { + "-skL", "--retry 3", "--connect-timeout 3", '--user-agent "' .. ua .. '"' + } + local return_code, result = api.curl_logic(url, file, args) + return return_code +end + +local function truncate_nodes(add_from) + for _, config in pairs(CONFIG) do + if config.nodes and type(config.nodes) == "table" then + for kk, vv in pairs(config.nodes) do + if vv.currentNode.add_mode == "2" then + else + vv.set(vv, vv.currentNode[".name"]) + end + end + config.set(config) + else + if config.currentNode.add_mode == "2" then + if add_from then + if config.currentNode.add_from and config.currentNode.add_from == add_from then + config.set(config, "nil") + end + else + config.set(config, "nil") + end + if config.id then + uci:delete(appname, config.id) + end + end + end + end + uci:foreach(appname, "nodes", function(node) + if node.add_mode == "2" then + if add_from then + if node.add_from and node.add_from == add_from then + uci:delete(appname, node['.name']) + end + else + uci:delete(appname, node['.name']) + end + end + end) + uci:commit(appname) +end + +local function select_node(nodes, config) + local server + if config.currentNode then + -- 特别优先级 分流 + 备注 + if config.currentNode.protocol and config.currentNode.protocol == '_shunt' then + for index, node in pairs(nodes) do + if node.remarks == config.currentNode.remarks then + log('更新【' .. config.remarks .. '】分流匹配节点:' .. node.remarks) + server = node[".name"] + break + end + end + end + -- 特别优先级 负载均衡 + 备注 + if config.currentNode.protocol and config.currentNode.protocol == '_balancing' then + for index, node in pairs(nodes) do + if node.remarks == config.currentNode.remarks then + log('更新【' .. config.remarks .. '】负载均衡匹配节点:' .. node.remarks) + server = node[".name"] + break + end + end + end + -- 第一优先级 类型 + 备注 + IP + 端口 + if not server then + for index, node in pairs(nodes) do + if config.currentNode.type and config.currentNode.remarks and config.currentNode.address and config.currentNode.port then + if node.type and node.remarks and node.address and node.port then + if node.type == config.currentNode.type and node.remarks == config.currentNode.remarks and (node.address .. ':' .. node.port == config.currentNode.address .. ':' .. config.currentNode.port) then + if config.log == nil or config.log == true then + log('更新【' .. config.remarks .. '】第一匹配节点:' .. node.remarks) + end + server = node[".name"] + break + end + end + end + end + end + -- 第二优先级 类型 + IP + 端口 + if not server then + for index, node in pairs(nodes) do + if config.currentNode.type and config.currentNode.address and config.currentNode.port then + if node.type and node.address and node.port then + if node.type == config.currentNode.type and (node.address .. ':' .. node.port == config.currentNode.address .. ':' .. config.currentNode.port) then + if config.log == nil or config.log == true then + log('更新【' .. config.remarks .. '】第二匹配节点:' .. node.remarks) + end + server = node[".name"] + break + end + end + end + end + end + -- 第三优先级 IP + 端口 + if not server then + for index, node in pairs(nodes) do + if config.currentNode.address and config.currentNode.port then + if node.address and node.port then + if node.address .. ':' .. node.port == config.currentNode.address .. ':' .. config.currentNode.port then + if config.log == nil or config.log == true then + log('更新【' .. config.remarks .. '】第三匹配节点:' .. node.remarks) + end + server = node[".name"] + break + end + end + end + end + end + -- 第四优先级 IP + if not server then + for index, node in pairs(nodes) do + if config.currentNode.address then + if node.address then + if node.address == config.currentNode.address then + if config.log == nil or config.log == true then + log('更新【' .. config.remarks .. '】第四匹配节点:' .. node.remarks) + end + server = node[".name"] + break + end + end + end + end + end + -- 第五优先级备注 + if not server then + for index, node in pairs(nodes) do + if config.currentNode.remarks then + if node.remarks then + if node.remarks == config.currentNode.remarks then + if config.log == nil or config.log == true then + log('更新【' .. config.remarks .. '】第五匹配节点:' .. node.remarks) + end + server = node[".name"] + break + end + end + end + end + end + end + -- 还不行 随便找一个 + if not server then + local nodes_table = {} + for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" then + nodes_table[#nodes_table + 1] = e + end + end + if #nodes_table > 0 then + if config.log == nil or config.log == true then + log('【' .. config.remarks .. '】' .. '无法找到最匹配的节点,当前已更换为:' .. nodes_table[1].remarks) + end + server = nodes_table[1][".name"] + end + end + if server then + config.set(config, server) + end +end + +local function update_node(manual) + if next(nodeResult) == nil then + log("更新失败,没有可用的节点信息") + return + end + + local group = {} + for _, v in ipairs(nodeResult) do + group[v["remark"]] = true + end + + if manual == 0 and next(group) then + uci:foreach(appname, "nodes", function(node) + -- 如果未发现新节点或手动导入的节点就不要删除了... + if node.add_mode == "2" and (node.add_from and group[node.add_from] == true) then + uci:delete(appname, node['.name']) + end + end) + end + for _, v in ipairs(nodeResult) do + local remark = v["remark"] + local list = v["list"] + for _, vv in ipairs(list) do + local cfgid = uci:section(appname, "nodes", api.gen_short_uuid()) + for kkk, vvv in pairs(vv) do + uci:set(appname, cfgid, kkk, vvv) + end + end + end + uci:commit(appname) + + if next(CONFIG) then + local nodes = {} + uci:foreach(appname, "nodes", function(node) + nodes[#nodes + 1] = node + end) + + for _, config in pairs(CONFIG) do + if config.nodes and type(config.nodes) == "table" then + for kk, vv in pairs(config.nodes) do + select_node(nodes, vv) + end + config.set(config) + else + select_node(nodes, config) + end + end + + --[[ + for k, v in pairs(CONFIG) do + if type(v.new_nodes) == "table" and #v.new_nodes > 0 then + local new_node_list = "" + for kk, vv in pairs(v.new_nodes) do + new_node_list = new_node_list .. vv .. " " + end + if new_node_list ~= "" then + print(v.remarks, new_node_list) + end + else + print(v.remarks, v.newNodeId) + end + end + ]]-- + + uci:commit(appname) + end + luci.sys.call("/etc/init.d/" .. appname .. " restart > /dev/null 2>&1 &") +end + +local function parse_link(raw, add_mode, add_from) + if raw and #raw > 0 then + local nodes, szType + local node_list = {} + -- SSD 似乎是这种格式 ssd:// 开头的 + if raw:find('ssd://') then + szType = 'ssd' + local nEnd = select(2, raw:find('ssd://')) + nodes = base64Decode(raw:sub(nEnd + 1, #raw)) + nodes = jsonParse(nodes) + local extra = { + airport = nodes.airport, + port = nodes.port, + encryption = nodes.encryption, + password = nodes.password + } + local servers = {} + -- SS里面包着 干脆直接这样 + for _, server in ipairs(nodes.servers) do + tinsert(servers, setmetatable(server, { __index = extra })) + end + nodes = servers + else + -- ssd 外的格式 + if add_mode == "1" then + nodes = split(raw:gsub(" ", "\n"), "\n") + else + nodes = split(base64Decode(raw):gsub(" ", "\n"), "\n") + end + end + + for _, v in ipairs(nodes) do + if v then + local result + if szType == 'ssd' then + result = processData(szType, v, add_mode, add_from) + elseif not szType then + local node = trim(v) + local dat = split(node, "://") + if dat and dat[1] and dat[2] then + if dat[1] == 'ss' or dat[1] == 'trojan' then + result = processData(dat[1], dat[2], add_mode, add_from) + else + result = processData(dat[1], base64Decode(dat[2]), add_mode, add_from) + end + end + else + log('跳过未知类型: ' .. szType) + end + -- log(result) + if result then + if not result.type then + log('丢弃节点:' .. result.remarks .. ",找不到可使用二进制.") + elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or + (not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then + log('丢弃过滤节点: ' .. result.type .. ' 节点, ' .. result.remarks) + else + tinsert(node_list, result) + end + end + end + end + if #node_list > 0 then + nodeResult[#nodeResult + 1] = { + remark = add_from, + list = node_list + } + end + log('成功解析【' .. add_from .. '】节点数量: ' .. #node_list) + else + if add_mode == "2" then + log('获取到的【' .. add_from .. '】订阅内容为空,可能是订阅地址失效,或是网络问题,请请检测。') + end + end +end + +local execute = function() + do + local subscribe_list = {} + local fail_list = {} + if arg[2] then + string.gsub(arg[2], '[^' .. "," .. ']+', function(w) + subscribe_list[#subscribe_list + 1] = uci:get_all(appname, w) or {} + end) + else + uci:foreach(appname, "subscribe_list", function(o) + subscribe_list[#subscribe_list + 1] = o + end) + end + + for index, value in ipairs(subscribe_list) do + local cfgid = value[".name"] + local remark = value.remark + local url = value.url + if value.allowInsecure and value.allowInsecure ~= "1" then + allowInsecure_default = nil + end + local filter_keyword_mode = value.filter_keyword_mode or "5" + if filter_keyword_mode == "0" then + filter_keyword_mode_default = "0" + elseif filter_keyword_mode == "1" then + filter_keyword_mode_default = "1" + filter_keyword_discard_list_default = value.filter_discard_list or {} + elseif filter_keyword_mode == "2" then + filter_keyword_mode_default = "2" + filter_keyword_keep_list_default = value.filter_keep_list or {} + elseif filter_keyword_mode == "3" then + filter_keyword_mode_default = "3" + filter_keyword_keep_list_default = value.filter_keep_list or {} + filter_keyword_discard_list_default = value.filter_discard_list or {} + elseif filter_keyword_mode == "4" then + filter_keyword_mode_default = "4" + filter_keyword_keep_list_default = value.filter_keep_list or {} + filter_keyword_discard_list_default = value.filter_discard_list or {} + end + local ss_aead_type = value.ss_aead_type or "global" + if ss_aead_type ~= "global" then + ss_aead_type_default = ss_aead_type + end + local ua = value.user_agent + log('正在订阅:【' .. remark .. '】' .. url) + local raw = curl(url, "/tmp/" .. cfgid, ua) + if raw == 0 then + local f = io.open("/tmp/" .. cfgid, "r") + local stdout = f:read("*all") + f:close() + raw = trim(stdout) + os.remove("/tmp/" .. cfgid) + parse_link(raw, "2", remark) + else + fail_list[#fail_list + 1] = value + end + allowInsecure_default = true + filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" + filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} + filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} + ss_aead_type_default = uci:get(appname, "@global_subscribe[0]", "ss_aead_type") or "shadowsocks-libev" + end + + if #fail_list > 0 then + for index, value in ipairs(fail_list) do + log(string.format('【%s】订阅失败,可能是订阅地址失效,或是网络问题,请诊断!', value.remark)) + end + end + update_node(0) + end +end + +if arg[1] then + if arg[1] == "start" then + log('开始订阅...') + xpcall(execute, function(e) + log(e) + log(debug.traceback()) + log('发生错误, 正在恢复服务') + end) + log('订阅完毕...') + elseif arg[1] == "add" then + local f = assert(io.open("/tmp/links.conf", 'r')) + local content = f:read('*all') + f:close() + local nodes = split(content:gsub(" ", "\n"), "\n") + for _, raw in ipairs(nodes) do + parse_link(raw, "1", "导入") + end + update_node(1) + luci.sys.call("rm -f /tmp/links.conf") + elseif arg[1] == "truncate" then + truncate_nodes(arg[2]) + end +end diff --git a/luci-app-passwall2/root/usr/share/passwall2/test.sh b/luci-app-passwall2/root/usr/share/passwall2/test.sh new file mode 100755 index 000000000..81c1f799c --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/test.sh @@ -0,0 +1,253 @@ +#!/bin/sh + +CONFIG=passwall2 +LOG_FILE=/tmp/log/$CONFIG.log +LOCK_FILE_DIR=/tmp/lock +LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}_script.lock + +echolog() { + local d="$(date "+%Y-%m-%d %H:%M:%S")" + #echo -e "$d: $1" + echo -e "$d: $1" >> $LOG_FILE +} + +config_n_get() { + local ret=$(uci -q get "${CONFIG}.${1}.${2}" 2>/dev/null) + echo "${ret:=$3}" +} + +config_t_get() { + local index=0 + [ -n "$4" ] && index=$4 + local ret=$(uci -q get $CONFIG.@$1[$index].$2 2>/dev/null) + echo ${ret:=$3} +} + +test_url() { + local url=$1 + local try=1 + [ -n "$2" ] && try=$2 + local timeout=2 + [ -n "$3" ] && timeout=$3 + local extra_params=$4 + curl --help all | grep "\-\-retry-all-errors" > /dev/null + [ $? == 0 ] && extra_params="--retry-all-errors ${extra_params}" + status=$(/usr/bin/curl -I -o /dev/null -skL $extra_params --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url") + case "$status" in + 204|\ + 200) + status=200 + ;; + esac + echo $status +} + +test_proxy() { + result=0 + status=$(test_url "https://www.google.com/generate_204" ${retry_num} ${connect_timeout}) + if [ "$status" = "200" ]; then + result=0 + else + status2=$(test_url "https://www.baidu.com" ${retry_num} ${connect_timeout}) + if [ "$status2" = "200" ]; then + result=1 + else + result=2 + ping -c 3 -W 1 223.5.5.5 > /dev/null 2>&1 + [ $? -eq 0 ] && { + result=1 + } + fi + fi + echo $result +} + +url_test_node() { + result=0 + local node_id=$1 + local _type=$(echo $(config_n_get ${node_id} type nil) | tr 'A-Z' 'a-z') + [ "${_type}" != "nil" ] && { + local _tmp_port=$(/usr/share/${CONFIG}/app.sh get_new_port 61080 tcp,udp) + /usr/share/${CONFIG}/app.sh run_socks flag="url_test_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=url_test_${node_id}.json + local curlx="socks5h://127.0.0.1:${_tmp_port}" + sleep 1s + result=$(curl --connect-timeout 3 -o /dev/null -I -skL -w "%{http_code}:%{time_starttransfer}" -x $curlx "https://www.google.com/generate_204") + pgrep -af "url_test_${node_id}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 + rm -rf "/tmp/etc/${CONFIG}/url_test_${node_id}.json" + } + echo $result +} + +test_node() { + local node_id=$1 + local _type=$(echo $(config_n_get ${node_id} type nil) | tr 'A-Z' 'a-z') + [ "${_type}" != "nil" ] && { + local _tmp_port=$(/usr/share/${CONFIG}/app.sh get_new_port 61080 tcp,udp) + /usr/share/${CONFIG}/app.sh run_socks flag="test_node_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=test_node_${node_id}.json + local curlx="socks5h://127.0.0.1:${_tmp_port}" + sleep 1s + _proxy_status=$(test_url "https://www.google.com/generate_204" ${retry_num} ${connect_timeout} "-x $curlx") + pgrep -af "test_node_${node_id}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 + rm -rf "/tmp/etc/${CONFIG}/test_node_${node_id}.json" + if [ "${_proxy_status}" -eq 200 ]; then + return 0 + fi + } + return 1 +} + +flag=0 +main_node=$(config_t_get global node nil) + +test_auto_switch() { + flag=$(expr $flag + 1) + local TYPE=$1 + local b_nodes=$2 + local now_node=$3 + [ -z "$now_node" ] && { + local f="/tmp/etc/$CONFIG/id/global" + if [ -f "${f}" ]; then + now_node=$(cat ${f}) + if [ "$(config_n_get $now_node protocol nil)" = "_shunt" ]; then + if [ "$shunt_logic" == "1" ] && [ -f "${f}_default" ]; then + now_node=$(cat ${f}_default) + elif [ "$shunt_logic" == "2" ] && [ -f "${f}_main" ]; then + now_node=$(cat ${f}_main) + else + shunt_logic=0 + fi + else + shunt_logic=0 + fi + else + #echolog "自动切换检测:未知错误" + return 1 + fi + } + + [ $flag -le 1 ] && { + main_node=$now_node + } + + status=$(test_proxy) + if [ "$status" == 2 ]; then + echolog "自动切换检测:无法连接到网络,请检查网络是否正常!" + return 2 + fi + + #检测主节点是否能使用 + if [ "$restore_switch" == "1" ] && [ "$main_node" != "nil" ] && [ "$now_node" != "$main_node" ]; then + test_node ${main_node} + [ $? -eq 0 ] && { + #主节点正常,切换到主节点 + echolog "自动切换检测:${TYPE}主节点【$(config_n_get $main_node type):[$(config_n_get $main_node remarks)]】正常,切换到主节点!" + /usr/share/${CONFIG}/app.sh node_switch flag=global new_node=${main_node} shunt_logic=${shunt_logic} + [ $? -eq 0 ] && { + echolog "自动切换检测:${TYPE}节点切换完毕!" + [ "$shunt_logic" != "0" ] && { + local node=$(config_t_get global node nil) + [ "$(config_n_get $node protocol nil)" = "_shunt" ] && { + if [ "$shunt_logic" == "1" ]; then + uci set $CONFIG.$node.default_node="$main_node" + elif [ "$shunt_logic" == "2" ]; then + uci set $CONFIG.$node.main_node="$main_node" + fi + uci commit $CONFIG + } + } + } + return 0 + } + fi + + if [ "$status" == 0 ]; then + #echolog "自动切换检测:${TYPE}节点【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】正常。" + return 0 + elif [ "$status" == 1 ]; then + echolog "自动切换检测:${TYPE}节点【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】异常,切换到下一个备用节点检测!" + local new_node + in_backup_nodes=$(echo $b_nodes | grep $now_node) + # 判断当前节点是否存在于备用节点列表里 + if [ -z "$in_backup_nodes" ]; then + # 如果不存在,设置第一个节点为新的节点 + new_node=$(echo $b_nodes | awk -F ' ' '{print $1}') + else + # 如果存在,设置下一个备用节点为新的节点 + #local count=$(expr $(echo $b_nodes | grep -o ' ' | wc -l) + 1) + local next_node=$(echo $b_nodes | awk -F "$now_node" '{print $2}' | awk -F " " '{print $1}') + if [ -z "$next_node" ]; then + new_node=$(echo $b_nodes | awk -F ' ' '{print $1}') + else + new_node=$next_node + fi + fi + test_node ${new_node} + if [ $? -eq 0 ]; then + [ "$restore_switch" == "0" ] && { + [ "$shunt_logic" == "0" ] && uci set $CONFIG.@global[0].node=$new_node + [ -z "$(echo $b_nodes | grep $main_node)" ] && uci add_list $CONFIG.@auto_switch[0].node=$main_node + uci commit $CONFIG + } + echolog "自动切换检测:${TYPE}节点【$(config_n_get $new_node type):[$(config_n_get $new_node remarks)]】正常,切换到此节点!" + /usr/share/${CONFIG}/app.sh node_switch flag=global new_node=${new_node} shunt_logic=${shunt_logic} + [ $? -eq 0 ] && { + [ "$restore_switch" == "1" ] && [ "$shunt_logic" != "0" ] && { + local node=$(config_t_get global node nil) + [ "$(config_n_get $node protocol nil)" = "_shunt" ] && { + if [ "$shunt_logic" == "1" ]; then + uci set $CONFIG.$node.default_node="$main_node" + elif [ "$shunt_logic" == "2" ]; then + uci set $CONFIG.$node.main_node="$main_node" + fi + uci commit $CONFIG + } + } + echolog "自动切换检测:${TYPE}节点切换完毕!" + } + return 0 + else + test_auto_switch ${TYPE} "${b_nodes}" ${new_node} + fi + fi +} + +start() { + ENABLED=$(config_t_get global enabled 0) + [ "$ENABLED" != 1 ] && return 1 + ENABLED=$(config_t_get auto_switch enable 0) + [ "$ENABLED" != 1 ] && return 1 + delay=$(config_t_get auto_switch testing_time 1) + #sleep 9s + connect_timeout=$(config_t_get auto_switch connect_timeout 3) + retry_num=$(config_t_get auto_switch retry_num 3) + restore_switch=$(config_t_get auto_switch restore_switch 0) + shunt_logic=$(config_t_get auto_switch shunt_logic 0) + while [ "$ENABLED" -eq 1 ]; do + [ -f "$LOCK_FILE" ] && { + sleep 6s + continue + } + touch $LOCK_FILE + NODE=$(config_t_get auto_switch node nil) + [ -n "$NODE" -a "$NODE" != "nil" ] && { + NODE=$(echo $NODE | tr -s ' ' '\n' | uniq | tr -s '\n' ' ') + test_auto_switch TCP "$NODE" + } + rm -f $LOCK_FILE + sleep ${delay}m + done +} + +arg1=$1 +shift +case $arg1 in +test_url) + test_url $@ + ;; +url_test_node) + url_test_node $@ + ;; +*) + start + ;; +esac diff --git a/luci-app-passwall2/root/usr/share/rpcd/acl.d/luci-app-passwall2.json b/luci-app-passwall2/root/usr/share/rpcd/acl.d/luci-app-passwall2.json new file mode 100644 index 000000000..ec872412a --- /dev/null +++ b/luci-app-passwall2/root/usr/share/rpcd/acl.d/luci-app-passwall2.json @@ -0,0 +1,11 @@ +{ + "luci-app-passwall2": { + "description": "Grant UCI access for luci-app-passwall2", + "read": { + "uci": [ "passwall2", "passwall2_server" ] + }, + "write": { + "uci": [ "passwall2", "passwall2_server" ] + } + } +}