From 946bea39986ee52cb219db8344934c53ca48ac7c Mon Sep 17 00:00:00 2001 From: actions Date: Fri, 2 Aug 2024 10:30:09 +0800 Subject: [PATCH] luci-app-passwall2: sync upstream last commit: https://github.com/xiaorouji/openwrt-passwall2/commit/da49da8f53a75dbc01b902a2eadf590adb168919 --- luci-app-passwall2/Makefile | 156 ++ .../luci-static/resources/qrcode.min.js | 1 + .../luasrc/controller/passwall2.lua | 425 ++++ .../luasrc/model/cbi/passwall2/client/acl.lua | 63 + .../model/cbi/passwall2/client/acl_config.lua | 298 +++ .../model/cbi/passwall2/client/app_update.lua | 29 + .../model/cbi/passwall2/client/global.lua | 415 ++++ .../model/cbi/passwall2/client/haproxy.lua | 145 ++ .../luasrc/model/cbi/passwall2/client/log.lua | 8 + .../cbi/passwall2/client/node_config.lua | 41 + .../model/cbi/passwall2/client/node_list.lua | 160 ++ .../cbi/passwall2/client/node_subscribe.lua | 177 ++ .../client/node_subscribe_config.lua | 173 ++ .../model/cbi/passwall2/client/other.lua | 224 ++ .../model/cbi/passwall2/client/rule.lua | 89 + .../cbi/passwall2/client/shunt_rules.lua | 169 ++ .../cbi/passwall2/client/socks_config.lua | 125 ++ .../cbi/passwall2/client/type/hysteria2.lua | 76 + .../model/cbi/passwall2/client/type/naive.lua | 35 + .../model/cbi/passwall2/client/type/ray.lua | 624 ++++++ .../cbi/passwall2/client/type/sing-box.lua | 665 ++++++ .../cbi/passwall2/client/type/ss-rust.lua | 57 + .../model/cbi/passwall2/client/type/ss.lua | 58 + .../model/cbi/passwall2/client/type/ssr.lua | 69 + .../model/cbi/passwall2/client/type/tuic.lua | 133 ++ .../model/cbi/passwall2/server/index.lua | 67 + .../cbi/passwall2/server/type/hysteria2.lua | 75 + .../model/cbi/passwall2/server/type/ray.lua | 410 ++++ .../cbi/passwall2/server/type/sing-box.lua | 426 ++++ .../cbi/passwall2/server/type/ss-rust.lua | 47 + .../model/cbi/passwall2/server/type/ss.lua | 50 + .../model/cbi/passwall2/server/type/ssr.lua | 74 + .../model/cbi/passwall2/server/user.lua | 34 + luci-app-passwall2/luasrc/passwall2/api.lua | 1047 ++++++++++ luci-app-passwall2/luasrc/passwall2/com.lua | 56 + .../luasrc/passwall2/server_app.lua | 221 ++ .../luasrc/passwall2/util_hysteria2.lua | 115 ++ .../luasrc/passwall2/util_naiveproxy.lua | 39 + .../luasrc/passwall2/util_shadowsocks.lua | 123 ++ .../luasrc/passwall2/util_sing-box.lua | 1733 ++++++++++++++++ .../luasrc/passwall2/util_tuic.lua | 57 + .../luasrc/passwall2/util_xray.lua | 1824 +++++++++++++++++ .../view/passwall2/app_update/app_version.htm | 192 ++ .../luasrc/view/passwall2/global/faq.htm | 74 + .../luasrc/view/passwall2/global/footer.htm | 142 ++ .../luasrc/view/passwall2/global/status.htm | 197 ++ .../luasrc/view/passwall2/haproxy/status.htm | 26 + .../luasrc/view/passwall2/log/log.htm | 30 + .../passwall2/node_list/link_add_node.htm | 110 + .../passwall2/node_list/link_share_man.htm | 940 +++++++++ .../view/passwall2/node_list/node_list.htm | 462 +++++ .../view/passwall2/rule/rule_version.htm | 56 + .../luasrc/view/passwall2/server/log.htm | 34 + .../passwall2/server/users_list_status.htm | 38 + .../passwall2/socks_auto_switch/footer.htm | 23 + luci-app-passwall2/po/zh-cn/passwall2.po | 1514 ++++++++++++++ luci-app-passwall2/po/zh_Hans | 1 + .../root/etc/config/passwall2_server | 4 + .../root/etc/hotplug.d/iface/98-passwall2 | 23 + luci-app-passwall2/root/etc/init.d/passwall2 | 70 + .../root/etc/init.d/passwall2_server | 16 + .../root/etc/uci-defaults/luci-passwall2 | 49 + .../root/usr/share/passwall2/0_default_config | 235 +++ .../root/usr/share/passwall2/app.sh | 1202 +++++++++++ .../root/usr/share/passwall2/domains_excluded | 24 + .../root/usr/share/passwall2/haproxy.lua | 222 ++ .../root/usr/share/passwall2/haproxy_check.sh | 18 + .../usr/share/passwall2/helper_dnsmasq.sh | 146 ++ .../root/usr/share/passwall2/iptables.sh | 1018 +++++++++ .../root/usr/share/passwall2/monitor.sh | 47 + .../root/usr/share/passwall2/nftables.sh | 1058 ++++++++++ .../root/usr/share/passwall2/rule_update.lua | 206 ++ .../usr/share/passwall2/socks_auto_switch.sh | 180 ++ .../root/usr/share/passwall2/subscribe.lua | 1368 +++++++++++++ .../root/usr/share/passwall2/tasks.sh | 75 + .../root/usr/share/passwall2/test.sh | 106 + .../share/rpcd/acl.d/luci-app-passwall2.json | 11 + .../ucitrack/luci-app-passwall2-server.json | 4 + .../share/ucitrack/luci-app-passwall2.json | 4 + 79 files changed, 20738 insertions(+) create mode 100644 luci-app-passwall2/Makefile create mode 100644 luci-app-passwall2/htdocs/luci-static/resources/qrcode.min.js create mode 100644 luci-app-passwall2/luasrc/controller/passwall2.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/log.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/tuic.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua create mode 100644 luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/api.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/com.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/server_app.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/util_naiveproxy.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/util_sing-box.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/util_tuic.lua create mode 100644 luci-app-passwall2/luasrc/passwall2/util_xray.lua create mode 100644 luci-app-passwall2/luasrc/view/passwall2/app_update/app_version.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/global/faq.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/global/footer.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/global/status.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/haproxy/status.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/log/log.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/node_list/link_add_node.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/node_list/link_share_man.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/rule/rule_version.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/server/log.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/server/users_list_status.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/socks_auto_switch/footer.htm create mode 100644 luci-app-passwall2/po/zh-cn/passwall2.po create mode 120000 luci-app-passwall2/po/zh_Hans create mode 100644 luci-app-passwall2/root/etc/config/passwall2_server create mode 100644 luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 create mode 100755 luci-app-passwall2/root/etc/init.d/passwall2 create mode 100755 luci-app-passwall2/root/etc/init.d/passwall2_server create mode 100755 luci-app-passwall2/root/etc/uci-defaults/luci-passwall2 create mode 100644 luci-app-passwall2/root/usr/share/passwall2/0_default_config create mode 100644 luci-app-passwall2/root/usr/share/passwall2/app.sh create mode 100644 luci-app-passwall2/root/usr/share/passwall2/domains_excluded create mode 100644 luci-app-passwall2/root/usr/share/passwall2/haproxy.lua create mode 100755 luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh create mode 100755 luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.sh create mode 100755 luci-app-passwall2/root/usr/share/passwall2/iptables.sh create mode 100755 luci-app-passwall2/root/usr/share/passwall2/monitor.sh create mode 100755 luci-app-passwall2/root/usr/share/passwall2/nftables.sh create mode 100755 luci-app-passwall2/root/usr/share/passwall2/rule_update.lua create mode 100755 luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh create mode 100755 luci-app-passwall2/root/usr/share/passwall2/subscribe.lua create mode 100755 luci-app-passwall2/root/usr/share/passwall2/tasks.sh create mode 100755 luci-app-passwall2/root/usr/share/passwall2/test.sh create mode 100644 luci-app-passwall2/root/usr/share/rpcd/acl.d/luci-app-passwall2.json create mode 100644 luci-app-passwall2/root/usr/share/ucitrack/luci-app-passwall2-server.json create mode 100644 luci-app-passwall2/root/usr/share/ucitrack/luci-app-passwall2.json diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile new file mode 100644 index 000000000..2f24ea6aa --- /dev/null +++ b/luci-app-passwall2/Makefile @@ -0,0 +1,156 @@ +# 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.30-3 +PKG_RELEASE:= + +PKG_CONFIG_DEPENDS:= \ + CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ + CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ + 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_SingBox \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \ + 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_IPv6_Nat:ip6tables-mod-nat + +define Package/$(PKG_NAME)/config +menu "Configuration" + +config PACKAGE_$(PKG_NAME)_INCLUDE_IPv6_Nat + depends on PACKAGE_ip6tables + bool "Include IPv6 Nat" + default n + +if PACKAGE_$(PKG_NAME) + +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 + 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 + default y if PACKAGE_firewall4 + +config PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy + bool "Include Haproxy" + select PACKAGE_haproxy + default y if aarch64||arm||i386||x86_64 + +config PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria + bool "Include Hysteria" + select PACKAGE_hysteria + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy + bool "Include NaiveProxy" + depends on !(arc||(arm&&TARGET_gemini)||armeb||mips||mips64||powerpc) + select PACKAGE_naiveproxy + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client + bool "Include Shadowsocks Libev Client" + select PACKAGE_shadowsocks-libev-ss-local + select PACKAGE_shadowsocks-libev-ss-redir + default y + +config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server + bool "Include Shadowsocks Libev Server" + select PACKAGE_shadowsocks-libev-ss-server + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client + bool "Include Shadowsocks Rust Client" + depends on aarch64||arm||i386||mips||mipsel||x86_64 + select PACKAGE_shadowsocks-rust-sslocal + 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 + select PACKAGE_shadowsocks-rust-ssserver + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client + bool "Include ShadowsocksR Libev Client" + select PACKAGE_shadowsocksr-libev-ssr-local + select PACKAGE_shadowsocksr-libev-ssr-redir + default y + +config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server + bool "Include ShadowsocksR Libev Server" + select PACKAGE_shadowsocksr-libev-ssr-server + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs + bool "Include Simple-Obfs (Shadowsocks Plugin)" + select PACKAGE_simple-obfs + default y + +config PACKAGE_$(PKG_NAME)_INCLUDE_SingBox + bool "Include Sing-Box" + select PACKAGE_sing-box + default y if aarch64||arm||i386||x86_64 + +config PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client + bool "Include tuic-client" + depends on aarch64||arm||i386||x86_64 + select PACKAGE_tuic-client + default n + +config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin + bool "Include V2ray-Plugin (Shadowsocks Plugin)" + select PACKAGE_v2ray-plugin + default y if aarch64||arm||i386||x86_64 + +endif +endmenu +endef + +define Package/$(PKG_NAME)/conffiles +/etc/config/passwall2 +/etc/config/passwall2_server +/usr/share/passwall2/domains_excluded +/www/luci-static/resources/qrcode.min.js +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-passwall2/htdocs/luci-static/resources/qrcode.min.js b/luci-app-passwall2/htdocs/luci-static/resources/qrcode.min.js new file mode 100644 index 000000000..993e88f39 --- /dev/null +++ b/luci-app-passwall2/htdocs/luci-static/resources/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/controller/passwall2.lua b/luci-app-passwall2/luasrc/controller/passwall2.lua new file mode 100644 index 000000000..ed5d250d0 --- /dev/null +++ b/luci-app-passwall2/luasrc/controller/passwall2.lua @@ -0,0 +1,425 @@ +-- Copyright (C) 2022-2023 xiaorouji + +module("luci.controller.passwall2", package.seeall) +local api = require "luci.passwall2.api" +local appname = api.appname -- not available +local uci = luci.model.uci.cursor() -- in funtion index() +local http = require "luci.http" +local util = require "luci.util" +local i18n = require "luci.i18n" + +function index() + if not nixio.fs.access("/etc/config/passwall2") then + if nixio.fs.access("/usr/share/passwall2/0_default_config") then + luci.sys.call('cp -f /usr/share/passwall2/0_default_config /etc/config/passwall2') + else return end + end + local appname = "passwall2" -- global definitions not available + local uci = luci.model.uci.cursor() -- in function index() + 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 + local e + if uci:get(appname, "@global[0]", "hide_from_luci") ~= "1" then + e = entry({"admin", "services", appname}, alias("admin", "services", appname, "settings"), _("PassWall 2"), -1) + else + e = entry({"admin", "services", appname}, alias("admin", "services", appname, "settings"), nil, -1) + end + e.dependent = true + e.acl_depends = { "luci-app-passwall2" } + --[[ 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, "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, "socks_config"}, cbi(appname .. "/client/socks_config")).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, "socks_autoswitch_add_node"}, call("socks_autoswitch_add_node")).leaf = true + entry({"admin", "services", appname, "socks_autoswitch_remove_node"}, call("socks_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_socks_log"}, call("get_socks_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, "index_status"}, call("index_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() + uci:delete(appname, "@global[0]", "hide_from_luci") + uci:commit(appname) + 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() + uci:set(appname, "@global[0]", "hide_from_luci","1") + uci:commit(appname) + 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 socks_autoswitch_add_node() + local id = luci.http.formvalue("id") + local key = luci.http.formvalue("key") + if id and id ~= "" and key and key ~= "" then + local new_list = uci:get(appname, id, "autoswitch_backup_node") or {} + for i = #new_list, 1, -1 do + if (uci: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 + uci:set_list(appname, id, "autoswitch_backup_node", new_list) + uci:commit(appname) + end + luci.http.redirect(api.url("socks_config", id)) +end + +function socks_autoswitch_remove_node() + local id = luci.http.formvalue("id") + local key = luci.http.formvalue("key") + if id and id ~= "" and key and key ~= "" then + local new_list = uci:get(appname, id, "autoswitch_backup_node") or {} + for i = #new_list, 1, -1 do + if (uci:get(appname, new_list[i], "remarks") or ""):find(key) then + table.remove(new_list, i) + end + end + uci:set_list(appname, id, "autoswitch_backup_node", new_list) + uci:commit(appname) + end + luci.http.redirect(api.url("socks_config", id)) +end + +function get_now_use_node() + local e = {} + local data, code, msg = nixio.fs.readfile("/tmp/etc/passwall2/acl/default/global.id") + 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") + local name = luci.http.formvalue("name") + local file_path = "/tmp/etc/passwall2/acl/" .. id .. "/" .. name .. ".log" + if nixio.fs.access(file_path) then + local content = luci.sys.exec("cat '" .. file_path .. "'") + content = content:gsub("\n", "
") + luci.http.write(content) + else + luci.http.write(string.format("", i18n.translate("Not enabled log"))) + end +end + +function get_socks_log() + local name = luci.http.formvalue("name") + local path = "/tmp/etc/passwall2/SOCKS_" .. name .. ".log" + if nixio.fs.access(path) then + local content = luci.sys.exec("cat ".. path) + 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 index_status() + local e = {} + e["global_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '/tmp/etc/passwall2/bin/' | grep 'default' | grep 'global' >/dev/null") == 0 + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end + +function haproxy_status() + local e = luci.sys.call(string.format("/bin/busybox 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("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' | grep 'SOCKS_' > /dev/null", appname, id)) == 0 + local use_http = uci: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("/bin/busybox 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_appconnect}" ' .. 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 type = luci.http.formvalue("type") or "icmp" + local e = {} + e.index = index + if type == "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)) + else + 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") + uci:set(appname, type, config, section) + uci: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() + uci:section(appname, "nodes", uuid) + for k, v in pairs(uci:get_all(appname, section)) do + local filter = k:find("%.") + if filter and filter == 1 then + else + xpcall(function() + uci:set(appname, uuid, k, v) + end, + function(e) + end) + end + end + uci:delete(appname, uuid, "add_from") + uci:set(appname, uuid, "add_mode", 1) + uci:commit(appname) + luci.http.redirect(api.url("node_config", uuid)) +end + +function clear_all_nodes() + uci:set(appname, '@global[0]', "enabled", "0") + uci:set(appname, '@global[0]', "node", "nil") + uci:foreach(appname, "socks", function(t) + uci:delete(appname, t[".name"]) + uci:set_list(appname, t[".name"], "autoswitch_backup_node", {}) + end) + uci:foreach(appname, "haproxy_config", function(t) + uci:delete(appname, t[".name"]) + end) + uci:foreach(appname, "acl_rule", function(t) + uci:set(appname, t[".name"], "node", "default") + end) + uci:foreach(appname, "nodes", function(node) + uci:delete(appname, node['.name']) + end) + + uci:commit(appname) + luci.sys.call("/etc/init.d/" .. appname .. " stop") +end + +function delete_select_nodes() + local ids = luci.http.formvalue("ids") + string.gsub(ids, '[^' .. "," .. ']+', function(w) + if (uci:get(appname, "@global[0]", "node") or "nil") == w then + uci:set(appname, '@global[0]', "node", "nil") + end + uci:foreach(appname, "socks", function(t) + if t["node"] == w then + uci:delete(appname, t[".name"]) + end + local auto_switch_node_list = uci:get(appname, t[".name"], "autoswitch_backup_node") or {} + 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 + uci:set_list(appname, t[".name"], "autoswitch_backup_node", auto_switch_node_list) + end) + uci:foreach(appname, "haproxy_config", function(t) + if t["lbss"] == w then + uci:delete(appname, t[".name"]) + end + end) + uci:foreach(appname, "acl_rule", function(t) + if t["node"] == w then + uci:set(appname, t[".name"], "node", "default") + end + end) + uci:delete(appname, w) + end) + uci: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("/bin/busybox 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..26a07ac3b --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl.lua @@ -0,0 +1,63 @@ +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) +api.set_apply_on_parse(m) + +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..12ec9960b --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/acl_config.lua @@ -0,0 +1,298 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local uci = api.uci +local sys = api.sys + +local port_validate = function(self, value, t) + return value:gsub("-", ":") +end + +m = Map(appname) +api.set_apply_on_parse(m) + +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 = "
  • " .. translate("Example:") +.. "
  • " .. translate("MAC") .. ": 00:00:00:FF:FF:FF" +.. "
  • " .. translate("IP") .. ": 192.168.1.100" +.. "
  • " .. translate("IP CIDR") .. ": 192.168.1.0/24" +.. "
  • " .. translate("IP range") .. ": 192.168.1.100-192.168.1.200" +.. "
  • " .. translate("IPSet") .. ": ipset:lanlist" +.. "
" +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 +local TCP_NO_REDIR_PORTS = uci:get(appname, "@global_forwarding[0]", "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("Use global config") .. "(" .. TCP_NO_REDIR_PORTS .. ")") +o:value("1:65535", translate("All")) +o.validate = port_validate + +---- UDP No Redir Ports +local UDP_NO_REDIR_PORTS = uci:get(appname, "@global_forwarding[0]", "udp_no_redir_ports") +o = s:option(Value, "udp_no_redir_ports", translate("UDP No Redir Ports"), + "" .. + translate("If you don't want to let the device in the list to go proxy, please choose all.") .. + "") +o.default = "default" +o:value("disable", translate("No patterns are used")) +o:value("default", translate("Use global config") .. "(" .. UDP_NO_REDIR_PORTS .. ")") +o:value("1:65535", translate("All")) +o.validate = port_validate + +node = s:option(ListValue, "node", "" .. translate("Node") .. "") +node.default = "default" +node:value("default", translate("Use global config")) +for k, v in pairs(nodes_table) do + node:value(v.id, v["remark"]) +end + +---- TCP Redir Ports +local TCP_REDIR_PORTS = uci:get(appname, "@global_forwarding[0]", "tcp_redir_ports") +o = s:option(Value, "tcp_redir_ports", translate("TCP Redir Ports")) +o.default = "default" +o:value("default", translate("Use global config") .. "(" .. TCP_REDIR_PORTS .. ")") +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") +o.validate = port_validate + +---- UDP Redir Ports +local UDP_REDIR_PORTS = uci:get(appname, "@global_forwarding[0]", "udp_redir_ports") +o = s:option(Value, "udp_redir_ports", translate("UDP Redir Ports")) +o.default = "default" +o:value("default", translate("Use global config") .. "(" .. UDP_REDIR_PORTS .. ")") +o:value("1:65535", translate("All")) +o.validate = port_validate + +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("149.112.112.112", "149.112.112.112 (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 9.9.9.9") +o:value("https://149.112.112.112/dns-query", "Quad9-Recommended 149.112.112.112") +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({ __hide = true }) + +o = s:option(ListValue, "remote_dns_detour", translate("Remote DNS Outbound")) +o.default = "remote" +o:value("remote", translate("Remote")) +o:value("direct", translate("Direct")) +o:depends("remote_dns_protocol", "tcp") +o:depends("remote_dns_protocol", "doh") +o:depends("remote_dns_protocol", "udp") + +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") + +o = s:option(TextValue, "dns_hosts", translate("Domain Override")) +o.rows = 5 +o.wrap = "off" +o:depends({ __hide = true }) +o.remove = function(self, section) + local node_value = node:formvalue(arg[1]) + if node_value ~= "nil" then + local node_t = m:get(node_value) or {} + if node_t.type == "Xray" then + AbstractValue.remove(self, section) + end + end +end + +for k, v in pairs(nodes_table) do + if v.type == "Xray" then + s.fields["remote_dns_client_ip"]:depends({ node = v.id, remote_dns_protocol = "tcp" }) + s.fields["remote_dns_client_ip"]:depends({ node = v.id, remote_dns_protocol = "doh" }) + s.fields["dns_hosts"]:depends({ node = v.id }) + end +end + +o = s:option(Flag, "write_ipset_direct", translate("Direct DNS result write to IPSet"), translate("Perform the matching direct domain name rules into IP to IPSet/NFTSet, and then connect directly (not entering the core). Maybe conflict with some special circumstances.")) +o.default = "1" +o:depends({ node = "default", ['!reverse'] = true }) + +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..5649b016c --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/app_update.lua @@ -0,0 +1,29 @@ +local api = require "luci.passwall2.api" +local appname = api.appname + +m = Map(appname) +api.set_apply_on_parse(m) + +-- [[ 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/global.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua new file mode 100644 index 000000000..07b3dfd83 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua @@ -0,0 +1,415 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local uci = api.uci +local datatypes = api.datatypes +local has_singbox = api.finded_com("singbox") +local has_xray = api.finded_com("xray") + +m = Map(appname) +api.set_apply_on_parse(m) + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + nodes_table[#nodes_table + 1] = e +end + +local normal_list = {} +local balancing_list = {} +local shunt_list = {} +local iface_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 + if v.protocol and v.protocol == "_iface" then + iface_list[#iface_list + 1] = v + end +end + +local socks_list = {} +uci:foreach(appname, "socks", function(s) + if s.enabled == "1" and s.node then + socks_list[#socks_list + 1] = { + id = "Socks_" .. s[".name"], + remark = translate("Socks Config") .. " [" .. s.port .. "端口]" + } + end +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) and not datatypes.ipmask6(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")) + +local global_cfgid = uci:get_all(appname, "@global[0]")[".name"] + +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 + +---- Node +node = s:taboption("Main", ListValue, "node", "" .. translate("Node") .. "") +node:value("nil", translate("Close")) + +-- 分流 +if (has_singbox or has_xray) and #nodes_table > 0 then + 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, Sing-Box or Xray + local type = s:taboption("Main", ListValue, vid .. "-type", translate("Type")) + if has_singbox then + type:value("sing-box", translate("Sing-Box")) + 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", ListValue, 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(socks_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(balancing_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(iface_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 (has_singbox and has_xray) or (v.type == "sing-box" and not has_singbox) 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", ListValue, 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(socks_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(balancing_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(iface_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", ListValue, 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(socks_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(balancing_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(iface_list) do + o:value(v1.id, v1.remark) + end + for k1, v1 in pairs(normal_list) do + o:value(v1.id, v1.remark) + 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 + +o = s:taboption("Main", Flag, "client_proxy", translate("Client Proxy"), translate("When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to 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" + +node_socks_bind_local = s:taboption("Main", Flag, "node_socks_bind_local", translate("Node") .. " Socks " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) +node_socks_bind_local.default = "1" +node_socks_bind_local:depends({ node = "nil", ["!reverse"] = true }) + +s:tab("DNS", translate("DNS")) + +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("149.112.112.112", "149.112.112.112 (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 9.9.9.9") +o:value("https://149.112.112.112/dns-query", "Quad9-Recommended 149.112.112.112") +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({ __hide = true }) + +o = s:taboption("DNS", ListValue, "remote_dns_detour", translate("Remote DNS Outbound")) +o.default = "remote" +o:value("remote", translate("Remote")) +o:value("direct", translate("Direct")) + +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") + +o = s:taboption("DNS", TextValue, "dns_hosts", translate("Domain Override")) +o.rows = 5 +o.wrap = "off" +o:depends({ __hide = true }) +o.remove = function(self, section) + local node_value = node:formvalue(global_cfgid) + if node_value ~= "nil" then + local node_t = m:get(node_value) or {} + if node_t.type == "Xray" then + AbstractValue.remove(self, section) + end + end +end + +o = s:taboption("DNS", Flag, "write_ipset_direct", translate("Direct DNS result write to IPSet"), translate("Perform the matching direct domain name rules into IP to IPSet/NFTSet, and then connect directly (not entering the core). Maybe conflict with some special circumstances.")) +o.default = "1" +o.rmempty = false + +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/passwall2/nftables.sh flush_nftset_reload || sh /usr/share/passwall2/iptables.sh flush_ipset_reload > /dev/null 2>&1 &') + luci.http.redirect(api.url("log")) +end + +for k, v in pairs(nodes_table) do + if v.type == "Xray" then + s.fields["remote_dns_client_ip"]:depends({ node = v.id, remote_dns_protocol = "tcp" }) + s.fields["remote_dns_client_ip"]:depends({ node = v.id, remote_dns_protocol = "doh" }) + s.fields["dns_hosts"]:depends({ node = v.id }) + end +end + +s:tab("log", translate("Log")) +o = s:taboption("log", Flag, "log_node", translate("Enable Node Log")) +o.default = "1" +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.template = "cbi/tblsection" +s.anonymous = true +s.addremove = true +s.extedit = api.url("socks_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 + +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_singbox 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_singbox 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..be9cfbc07 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua @@ -0,0 +1,145 @@ +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) +api.set_apply_on_parse(m) + +-- [[ 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) + +o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) +o.default = "0" +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("URL 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 URL 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..611765bc5 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_config.lua @@ -0,0 +1,41 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local uci = api.uci +local fs = require "nixio.fs" +local types_dir = "/usr/lib/lua/luci/model/cbi/passwall2/client/type/" + +if not arg[1] or not uci:get(appname, arg[1]) then + luci.http.redirect(api.url("node_list")) +end + +m = Map(appname, translate("Node Config")) +m.redirect = api.url() +api.set_apply_on_parse(m) + +s = m:section(NamedSection, arg[1], "nodes", "") +s.addremove = false +s.dynamic = false + +o = s:option(DummyValue, "passwall2", " ") +o.rawhtml = true +o.template = "passwall2/node_list/link_share_man" +o.value = arg[1] + +o = s:option(Value, "remarks", translate("Node Remarks")) +o.default = translate("Remarks") +o.rmempty = false + +o = s:option(ListValue, "type", translate("Type")) + +local type_table = {} +for filename in fs.dir(types_dir) do + table.insert(type_table, filename) +end +table.sort(type_table) + +for index, value in ipairs(type_table) do + local p_func = loadfile(types_dir .. value) + setfenv(p_func, getfenv(1))(m, s) +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..647dd28b8 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua @@ -0,0 +1,160 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local sys = api.sys +local datatypes = api.datatypes + +m = Map(appname) +api.set_apply_on_parse(m) + +-- [[ Other Settings ]]-- +s = m:section(TypedSection, "global_other") +s.anonymous = true + +o = s:option(ListValue, "auto_detection_time", translate("Automatic detection delay")) +o:value("0", translate("Close")) +o:value("icmp", "Ping") +o:value("tcping", "TCP Ping") + +o = s:option(Flag, "show_node_info", translate("Show server address and port")) +o.default = "0" + +-- [[ Add the node via the link ]]-- +s:append(Template(appname .. "/node_list/link_add_node")) + +local auto_detection_time = m:get("@global_other[0]", "auto_detection_time") or "0" +local show_node_info = m:get("@global_other[0]", "show_node_info") or "0" + +-- [[ 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 + for k, v in ipairs(m:get(s[".name"], "autoswitch_backup_node") or {}) do + if v and v == t then + sys.call(string.format("uci -q del_list %s.%s.autoswitch_backup_node='%s'", appname, s[".name"], v)) + end + 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) + 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 == "sing-box" 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 show_node_info == "1" 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", "Ping") +o.width = "8%" +o.rawhtml = true +o.cfgvalue = function(t, n) + local result = "---" + if auto_detection_time ~= "icmp" then + result = string.format('%s', n, translate("Test")) + else + result = string.format('---', n) + end + return result +end + +---- TCP Ping +o = s:option(DummyValue, "tcping", "TCPing") +o.width = "8%" +o.rawhtml = true +o.cfgvalue = function(t, n) + local result = "---" + if auto_detection_time ~= "tcping" then + result = string.format('%s', n, translate("Test")) + else + result = string.format('---', n) + end + return result +end + +o = s:option(DummyValue, "_url_test", translate("URL Test")) +o.width = "8%" +o.rawhtml = true +o.cfgvalue = function(t, n) + return string.format('%s', n, translate("Test")) +end + +m:append(Template(appname .. "/node_list/node_list")) + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua new file mode 100644 index 000000000..222ff7d13 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua @@ -0,0 +1,177 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local has_ss = api.is_finded("ss-redir") +local has_ss_rust = api.is_finded("sslocal") +local has_singbox = api.finded_com("singbox") +local has_xray = api.finded_com("xray") +local has_hysteria2 = api.finded_com("hysteria") +local ss_type = {} +local trojan_type = {} +local vmess_type = {} +local vless_type = {} +local hysteria2_type = {} +if has_ss then + local s = "shadowsocks-libev" + table.insert(ss_type, s) +end +if has_ss_rust then + local s = "shadowsocks-rust" + table.insert(ss_type, s) +end +if has_singbox then + local s = "sing-box" + table.insert(trojan_type, s) + table.insert(ss_type, s) + table.insert(vmess_type, s) + table.insert(vless_type, s) + table.insert(hysteria2_type, s) +end +if has_xray then + local s = "xray" + table.insert(trojan_type, s) + table.insert(ss_type, s) + table.insert(vmess_type, s) + table.insert(vless_type, s) +end +if has_hysteria2 then + local s = "hysteria2" + table.insert(hysteria2_type, s) +end + +m = Map(appname) +api.set_apply_on_parse(m) + +-- [[ Subscribe Settings ]]-- +s = m:section(TypedSection, "global_subscribe", "") +s.anonymous = true + +o = s:option(ListValue, "filter_keyword_mode", translate("Filter keyword Mode")) +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 = s:option(DynamicList, "filter_discard_list", translate("Discard List")) + +o = s:option(DynamicList, "filter_keep_list", translate("Keep List")) + +if #ss_type > 0 then + o = s:option(ListValue, "ss_type", translatef("%s Node Use Type", "Shadowsocks")) + for key, value in pairs(ss_type) do + o:value(value) + end +end + +if #trojan_type > 0 then + o = s:option(ListValue, "trojan_type", translatef("%s Node Use Type", "Trojan")) + for key, value in pairs(trojan_type) do + o:value(value) + end +end + +if #vmess_type > 0 then + o = s:option(ListValue, "vmess_type", translatef("%s Node Use Type", "VMess")) + for key, value in pairs(vmess_type) do + o:value(value) + end + if has_xray then + o.default = "xray" + end +end + +if #vless_type > 0 then + o = s:option(ListValue, "vless_type", translatef("%s Node Use Type", "VLESS")) + for key, value in pairs(vless_type) do + o:value(value) + end + if has_xray then + o.default = "xray" + end +end + +if #hysteria2_type > 0 then + o = s:option(ListValue, "hysteria2_type", translatef("%s Node Use Type", "Hysteria2")) + for key, value in pairs(hysteria2_type) do + o:value(value) + end + if has_hysteria2 then + o.default = "hysteria2" + 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..9f88ff268 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua @@ -0,0 +1,173 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local has_ss = api.is_finded("ss-redir") +local has_ss_rust = api.is_finded("sslocal") +local has_singbox = api.finded_com("singbox") +local has_xray = api.finded_com("xray") +local has_hysteria2 = api.finded_com("hysteria") +local ss_type = {} +local trojan_type = {} +local vmess_type = {} +local vless_type = {} +local hysteria2_type = {} +if has_ss then + local s = "shadowsocks-libev" + table.insert(ss_type, s) +end +if has_ss_rust then + local s = "shadowsocks-rust" + table.insert(ss_type, s) +end +if has_singbox then + local s = "sing-box" + table.insert(trojan_type, s) + table.insert(ss_type, s) + table.insert(vmess_type, s) + table.insert(vless_type, s) + table.insert(hysteria2_type, s) +end +if has_xray then + local s = "xray" + table.insert(trojan_type, s) + table.insert(ss_type, s) + table.insert(vmess_type, s) + table.insert(vless_type, s) +end +if has_hysteria2 then + local s = "hysteria2" + table.insert(hysteria2_type, s) +end + +m = Map(appname) +m.redirect = api.url("node_subscribe") +api.set_apply_on_parse(m) + +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_type > 0 then + o = s:option(ListValue, "ss_type", translatef("%s Node Use Type", "Shadowsocks")) + o.default = "global" + o:value("global", translate("Use global config")) + for key, value in pairs(ss_type) do + o:value(value) + end +end + +if #trojan_type > 0 then + o = s:option(ListValue, "trojan_type", translatef("%s Node Use Type", "Trojan")) + o.default = "global" + o:value("global", translate("Use global config")) + for key, value in pairs(trojan_type) do + o:value(value) + end +end + +if #vmess_type > 0 then + o = s:option(ListValue, "vmess_type", translatef("%s Node Use Type", "VMess")) + o.default = "global" + o:value("global", translate("Use global config")) + for key, value in pairs(vmess_type) do + o:value(value) + end +end + +if #vless_type > 0 then + o = s:option(ListValue, "vless_type", translatef("%s Node Use Type", "VLESS")) + o.default = "global" + o:value("global", translate("Use global config")) + for key, value in pairs(vless_type) do + o:value(value) + end +end + +if #hysteria2_type > 0 then + o = s:option(ListValue, "hysteria2_type", translatef("%s Node Use Type", "Hysteria2")) + o.default = "global" + o:value("global", translate("Use global config")) + for key, value in pairs(hysteria2_type) do + o:value(value) + 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 +o = s:option(ListValue, "week_update", translate("Update Mode")) +o:value(8, translate("Loop Mode")) +o:value(7, translate("Every day")) +o:value(1, translate("Every Monday")) +o:value(2, translate("Every Tuesday")) +o:value(3, translate("Every Wednesday")) +o:value(4, translate("Every Thursday")) +o:value(5, translate("Every Friday")) +o:value(6, translate("Every Saturday")) +o:value(0, translate("Every Sunday")) +o.default = 7 +o:depends("auto_update", true) +o.rmempty = true + +---- Time Update +o = s:option(ListValue, "time_update", translate("Update Time(every day)")) +for t = 0, 23 do o:value(t, t .. ":00") end +o.default = 0 +o:depends("week_update", "0") +o:depends("week_update", "1") +o:depends("week_update", "2") +o:depends("week_update", "3") +o:depends("week_update", "4") +o:depends("week_update", "5") +o:depends("week_update", "6") +o:depends("week_update", "7") +o.rmempty = true + +---- Interval Update +o = s:option(ListValue, "interval_update", translate("Update Interval(hour)")) +for t = 1, 24 do o:value(t, t .. " " .. translate("hour")) end +o.default = 2 +o:depends("week_update", "8") +o.rmempty = true + +o = s:option(Value, "user_agent", translate("User-Agent")) +o.default = "sing-box/9.9.9" +o:value("curl", "Curl Default") +o:value("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Linux") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Windows") +o:value("Passwall2/OpenWrt", "PassWall2") +o:value("sing-box/9.9.9", "Xboard(V2board)") + +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..16545f0dd --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/other.lua @@ -0,0 +1,224 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local fs = api.fs +local uci = api.uci +local has_singbox = api.finded_com("singbox") +local has_xray = api.finded_com("xray") +local has_fw3 = api.is_finded("fw3") +local has_fw4 = api.is_finded("fw4") + +local port_validate = function(self, value, t) + return value:gsub("-", ":") +end + +m = Map(appname) +api.set_apply_on_parse(m) + +-- [[ 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")) +o.validate = port_validate + +---- 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")) +o.validate = port_validate + +---- 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")) +o.validate = port_validate + +---- UDP Redir Ports +o = s:option(Value, "udp_redir_ports", translate("UDP Redir Ports")) +o.default = "1:65535" +o:value("1:65535", translate("All")) +o.validate = port_validate + +---- 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_xray then + s_xray = m:section(TypedSection, "global_xray", "Xray " .. translate("Settings")) + s_xray.anonymous = true + s_xray.addremove = false + + o = s_xray:option(Flag, "fragment", translate("Fragment"), translate("TCP fragments, which can deceive the censorship system in some cases, such as bypassing SNI blacklists.")) + o.default = 0 + + o = s_xray:option(ListValue, "fragment_packets", translate("Fragment Packets"), translate(" \"1-3\" is for segmentation at TCP layer, applying to the beginning 1 to 3 data writes by the client. \"tlshello\" is for TLS client hello packet fragmentation.")) + o.default = "tlshello" + o:value("tlshello", "tlshello") + o:value("1-2", "1-2") + o:value("1-3", "1-3") + o:value("1-5", "1-5") + o:depends("fragment", true) + + o = s_xray:option(Value, "fragment_length", translate("Fragment Length"), translate("Fragmented packet length (byte)")) + o.default = "100-200" + o:depends("fragment", true) + + o = s_xray:option(Value, "fragment_interval", translate("Fragment Interval"), translate("Fragmentation interval (ms)")) + o.default = "10-20" + o:depends("fragment", true) + + o = s_xray:option(Flag, "sniffing_override_dest", translate("Override the connection destination address"), translate("Override the connection destination address with the sniffed domain.")) + o.default = 0 + + o = s_xray:option(Flag, "route_only", translate("Sniffing Route Only")) + o.default = 0 + o:depends("sniffing", true) + + local domains_excluded = string.format("/usr/share/%s/domains_excluded", appname) + o = s_xray:option(TextValue, "excluded_domains", translate("Excluded Domains"), translate("If the traffic sniffing result is in this list, the destination address will not be overridden.")) + 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:depends({sniffing_override_dest = true}) + + o = s_xray:option(Value, "buffer_size", translate("Buffer Size"), translate("Buffer size for every connection (kB)")) + o.datatype = "uinteger" +end + +if has_singbox then + s = m:section(TypedSection, "global_singbox", "Sing-Box " .. translate("Settings")) + s.anonymous = true + s.addremove = false + + o = s:option(Flag, "sniff_override_destination", translate("Override the connection destination address"), translate("Override the connection destination address with the sniffed domain.")) + o.default = 0 + o.rmempty = false + + o = s:option(Value, "geoip_path", translate("Custom geoip Path")) + o.default = "/usr/share/singbox/geoip.db" + o.rmempty = false + + o = s:option(Value, "geoip_url", translate("Custom geoip URL")) + o.default = "https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db" + o:value("https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db") + o.rmempty = false + + o = s:option(Value, "geosite_path", translate("Custom geosite Path")) + o.default = "/usr/share/singbox/geosite.db" + o.rmempty = false + + o = s:option(Value, "geosite_url", translate("Custom geosite URL")) + o.default = "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db" + o:value("https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db") + o.rmempty = false + + o = s:option(Button, "_remove_resource", translate("Remove resource files")) + o.description = translate("Sing-Box will automatically download resource files when starting, you can use this feature achieve upgrade resource files.") + o.inputstyle = "remove" + function o.write(self, section, value) + local geoip_path = s.fields["geoip_path"] and s.fields["geoip_path"]:formvalue(section) or nil + if geoip_path then + os.remove(geoip_path) + end + local geosite_path = s.fields["geosite_path"] and s.fields["geosite_path"]:formvalue(section) or nil + if geosite_path then + os.remove(geosite_path) + end + 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..e8aeba77b --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/rule.lua @@ -0,0 +1,89 @@ +local api = require "luci.passwall2.api" +local appname = api.appname + +m = Map(appname) +api.set_apply_on_parse(m) + +-- [[ Rule Settings ]]-- +s = m:section(TypedSection, "global_rules", translate("Rule status")) +s.anonymous = 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: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("Update Mode")) +o:value(8, translate("Loop Mode")) +o:value(7, translate("Every day")) +o:value(1, translate("Every Monday")) +o:value(2, translate("Every Tuesday")) +o:value(3, translate("Every Wednesday")) +o:value(4, translate("Every Thursday")) +o:value(5, translate("Every Friday")) +o:value(6, translate("Every Saturday")) +o:value(0, translate("Every Sunday")) +o.default = 7 +o:depends("auto_update", true) +o.rmempty = true + +---- Time Update +o = s:option(ListValue, "time_update", translate("Update Time(every day)")) +for t = 0, 23 do o:value(t, t .. ":00") end +o.default = 0 +o:depends("week_update", "0") +o:depends("week_update", "1") +o:depends("week_update", "2") +o:depends("week_update", "3") +o:depends("week_update", "4") +o:depends("week_update", "5") +o:depends("week_update", "6") +o:depends("week_update", "7") +o.rmempty = true + +---- Interval Update +o = s:option(ListValue, "interval_update", translate("Update Interval(hour)")) +for t = 1, 24 do o:value(t, t .. " " .. translate("hour")) end +o.default = 2 +o:depends("week_update", "8") +o.rmempty = true + +s = m:section(TypedSection, "shunt_rules", "Sing-Box/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..b5ce3b73f --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua @@ -0,0 +1,169 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local datatypes = api.datatypes + +m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule")) +m.redirect = api.url() +api.set_apply_on_parse(m) + +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") + +o = s:option(MultiValue, "inbound", translate("Inbound Tag")) +o:value("tproxy", translate("Transparent proxy")) +o:value("socks", "Socks") + +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 = "
  • " .. translate("Example:") +.. "
  • " .. translate("IP") .. ": 192.168.1.100" +.. "
  • " .. translate("IP CIDR") .. ": 192.168.1.0/24" +.. "
  • " .. translate("GeoIP") .. ": geoip:private" +.. "
" +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= {} + value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n") + 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 not host:find("#") and host:find("%s") then + elseif 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 + elseif host:find("#") and host:find("#") == 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 = "
  • " .. translate("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'.") +.. "
  • " .. translate("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'.") +.. "
  • " .. translate("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'.") +.. "
  • " .. translate("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'.") +.. "
  • " .. translate("Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") +.. "
  • " .. translate("Annotation: Begining with #") +.. "
" +ip_list = s:option(TextValue, "ip_list", "IP") +ip_list.rows = 10 +ip_list.wrap = "off" +ip_list.validate = function(self, value) + local ipmasks= {} + value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n") + 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 and not ipmask:find("%s") then + elseif ipmask:find("ext:") and ipmask:find("ext:") == 1 and not ipmask:find("%s") then + elseif ipmask:find("#") and ipmask:find("#") == 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 = "
  • " .. translate("IP: such as '127.0.0.1'.") +.. "
  • " .. translate("CIDR: such as '127.0.0.0/8'.") +.. "
  • " .. translate("GeoIP: such as 'geoip:cn'. It begins with geoip: (lower case) and followed by two letter of country code.") +.. "
  • " .. translate("Annotation: Begining with #") +.. "
" + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua new file mode 100644 index 000000000..09352ed14 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua @@ -0,0 +1,125 @@ +local api = require "luci.passwall2.api" +local appname = api.appname +local uci = api.uci +local has_singbox = api.finded_com("singbox") +local has_xray = api.finded_com("xray") + +m = Map(appname) +api.set_apply_on_parse(m) + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + nodes_table[#nodes_table + 1] = e +end + +s = m:section(NamedSection, arg[1], translate("Socks Config"), translate("Socks Config")) +s.addremove = false +s.dynamic = false + +---- Enable +o = s:option(Flag, "enabled", translate("Enable")) +o.default = 1 +o.rmempty = false + +local auto_switch_tip +local current_node_file = string.format("/tmp/etc/%s/id/socks_%s", appname, arg[1]) +local current_node = luci.sys.exec(string.format("[ -f '%s' ] && echo -n $(cat %s)", current_node_file, current_node_file)) +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(arg[1], "enable_autoswitch") or 0) == 1 then + 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 + +socks_node = s:option(ListValue, "node", translate("Node")) +if auto_switch_tip then + socks_node.description = auto_switch_tip +end + +o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) +o.default = "0" + +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_singbox 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 + +o = s:option(Flag, "log", translate("Enable") .. " " .. translate("Log")) +o.default = 1 +o.rmempty = false + +o = s:option(Flag, "enable_autoswitch", translate("Auto Switch")) +o.default = 0 +o.rmempty = false + +o = s:option(Value, "autoswitch_testing_time", translate("How often to test"), translate("Units:seconds")) +o.datatype = "min(10)" +o.default = 30 +o:depends("enable_autoswitch", true) + +o = s:option(Value, "autoswitch_connect_timeout", translate("Timeout seconds"), translate("Units:seconds")) +o.datatype = "min(1)" +o.default = 3 +o:depends("enable_autoswitch", true) + +o = s:option(Value, "autoswitch_retry_num", translate("Timeout retry num")) +o.datatype = "min(1)" +o.default = 1 +o:depends("enable_autoswitch", true) + +autoswitch_backup_node = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes")) +autoswitch_backup_node:depends("enable_autoswitch", true) +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, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node.")) +o:depends("enable_autoswitch", true) + +o = s:option(Value, "autoswitch_probe_url", translate("Probe URL"), translate("The URL used to detect the connection status.")) +o.default = "https://www.google.com/generate_204" +o:depends("enable_autoswitch", true) + +for k, v in pairs(nodes_table) do + autoswitch_backup_node:value(v.id, v["remark"]) + socks_node:value(v.id, v["remark"]) +end + +m:append(Template(appname .. "/socks_auto_switch/footer")) + +return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua new file mode 100644 index 000000000..c612af3c6 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua @@ -0,0 +1,76 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.finded_com("hysteria") then + return +end + +local type_name = "Hysteria2" + +local option_prefix = "hysteria2_" + +local function option_name(name) + return option_prefix .. name +end + +-- [[ Hysteria2 ]] + +s.fields["type"]:value(type_name, "Hysteria2") + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +o:value("udp", "UDP") + +o = s:option(Value, option_name("address"), translate("Address (Support Domain Name)")) + +o = s:option(Value, option_name("port"), translate("Port")) +o.datatype = "port" + +o = s:option(Value, option_name("hop"), translate("Additional ports for hysteria hop")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("obfs"), translate("Obfs Password")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("auth_password"), translate("Auth Password")) +o.password = true +o.rewrite_option = o.option + +o = s:option(Flag, option_name("fast_open"), translate("Fast Open")) +o.default = "0" + +o = s:option(Value, option_name("tls_serverName"), translate("Domain")) + +o = s:option(Flag, option_name("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) +o.default = "0" + +o = s:option(Value, option_name("tls_pinSHA256"), translate("PinSHA256"),translate("Certificate fingerprint")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("up_mbps"), translate("Max upload Mbps")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("down_mbps"), translate("Max download Mbps")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("hop_interval"), translate("Hop Interval")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("recv_window"), translate("QUIC stream receive window")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("recv_window_conn"), translate("QUIC connection receive window")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("idle_timeout"), translate("Idle Timeout")) +o.rewrite_option = o.option + +o = s:option(Flag, option_name("disable_mtu_discovery"), translate("Disable MTU detection")) +o.default = "0" +o.rewrite_option = o.option + +o = s:option(Flag, option_name("lazy_start"), translate("Lazy Start")) +o.default = "0" +o.rewrite_option = o.option + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua new file mode 100644 index 000000000..ff73ffc42 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua @@ -0,0 +1,35 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.is_finded("naive") then + return +end + +local type_name = "Naiveproxy" + +local option_prefix = "naive_" + +local function option_name(name) + return option_prefix .. name +end + +-- [[ Naive ]] + +s.fields["type"]:value(type_name, translate("NaiveProxy")) + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +o:value("https", translate("HTTPS")) +o:value("quic", translate("QUIC")) + +o = s:option(Value, option_name("address"), translate("Address (Support Domain Name)")) + +o = s:option(Value, option_name("port"), translate("Port")) +o.datatype = "port" + +o = s:option(Value, option_name("username"), translate("Username")) + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua new file mode 100644 index 000000000..3fcad8cce --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua @@ -0,0 +1,624 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.finded_com("xray") then + return +end + +local appname = api.appname +local uci = api.uci + +local type_name = "Xray" + +local option_prefix = "xray_" + +local function option_name(name) + return option_prefix .. name +end + +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 xray_version = api.get_app_version("xray") +-- [[ Xray ]] + +s.fields["type"]:value(type_name, "Xray") + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +o:value("vmess", translate("Vmess")) +o:value("vless", translate("VLESS")) +o:value("http", translate("HTTP")) +o:value("socks", translate("Socks")) +o:value("shadowsocks", translate("Shadowsocks")) +o:value("trojan", translate("Trojan")) +o:value("wireguard", translate("WireGuard")) +o:value("_balancing", translate("Balancing")) +o:value("_shunt", translate("Shunt")) +o:value("_iface", translate("Custom Interface")) + +o = s:option(Value, option_name("iface"), translate("Interface")) +o.default = "eth1" +o:depends({ [option_name("protocol")] = "_iface" }) + +local nodes_table = {} +local balancers_table = {} +local fallback_table = {} +local iface_table = {} +local is_balancer = nil +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remark = e["remark"], + type = e["type"] + } + end + if e.protocol == "_balancing" then + balancers_table[#balancers_table + 1] = { + id = e[".name"], + remark = e["remark"] + } + if e[".name"] ~= arg[1] then + fallback_table[#fallback_table + 1] = { + id = e[".name"], + remark = e["remark"], + fallback = e["fallback_node"] + } + else + is_balancer = true + end + end + if e.protocol == "_iface" then + iface_table[#iface_table + 1] = { + id = e[".name"], + remark = e["remark"] + } + end +end + +local socks_list = {} +uci:foreach(appname, "socks", function(s) + if s.enabled == "1" and s.node then + socks_list[#socks_list + 1] = { + id = "Socks_" .. s[".name"], + remark = translate("Socks Config") .. " [" .. s.port .. "端口]" + } + end +end) + +-- 负载均衡列表 +local o = s:option(DynamicList, option_name("balancing_node"), translate("Load balancing node list"), translate("Load balancing node list, document")) +o:depends({ [option_name("protocol")] = "_balancing" }) +for k, v in pairs(nodes_table) do o:value(v.id, v.remark) end + +local o = s:option(ListValue, option_name("balancingStrategy"), translate("Balancing Strategy")) +o:depends({ [option_name("protocol")] = "_balancing" }) +o:value("random") +o:value("roundRobin") +o:value("leastPing") +o.default = "leastPing" + +-- Fallback Node +if api.compare_versions(xray_version, ">=", "1.8.10") then + local o = s:option(ListValue, option_name("fallback_node"), translate("Fallback Node")) + if api.compare_versions(xray_version, ">=", "1.8.12") then + o:depends({ [option_name("protocol")] = "_balancing" }) + else + o:depends({ [option_name("balancingStrategy")] = "leastPing" }) + end + local function check_fallback_chain(fb) + for k, v in pairs(fallback_table) do + if v.fallback == fb then + fallback_table[k] = nil + check_fallback_chain(v.id) + end + end + end + -- 检查fallback链,去掉会形成闭环的balancer节点 + if is_balancer then + check_fallback_chain(arg[1]) + end + for k, v in pairs(fallback_table) do o:value(v.id, v.remark) end + for k, v in pairs(nodes_table) do o:value(v.id, v.remark) end +end + +-- 探测地址 +local ucpu = s:option(Flag, option_name("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.")) +ucpu:depends({ [option_name("balancingStrategy")] = "leastPing" }) + +local pu = s:option(Value, option_name("probeUrl"), translate("Probe URL")) +pu:depends({ [option_name("useCustomProbeUrl")] = true }) +pu:value("https://cp.cloudflare.com/", "Cloudflare") +pu:value("https://www.gstatic.com/generate_204", "Gstatic") +pu:value("https://www.google.com/generate_204", "Google") +pu:value("https://www.youtube.com/generate_204", "YouTube") +pu:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)") +pu:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)") +pu.default = "https://www.google.com/generate_204" +pu.description = translate("The URL used to detect the connection status.") + +-- 探测间隔 +local pi = s:option(Value, option_name("probeInterval"), translate("Probe Interval")) +pi:depends({ [option_name("balancingStrategy")] = "leastPing" }) +pi.default = "1m" +pi.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 api.compare_versions(xray_version, ">=", "1.8.12") then + ucpu:depends({ [option_name("protocol")] = "_balancing" }) + pi:depends({ [option_name("protocol")] = "_balancing" }) +else + ucpu:depends({ [option_name("balancingStrategy")] = "leastPing" }) + pi:depends({ [option_name("balancingStrategy")] = "leastPing" }) +end + + +-- [[ 分流模块 ]] +if #nodes_table > 0 then + o = s:option(Flag, option_name("preproxy_enabled"), translate("Preproxy")) + o:depends({ [option_name("protocol")] = "_shunt" }) + + o = s:option(ListValue, option_name("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({ [option_name("protocol")] = "_shunt", [option_name("preproxy_enabled")] = true }) + for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + end + for k, v in pairs(balancers_table) do + o:value(v.id, v.remark) + end + for k, v in pairs(iface_table) do + o:value(v.id, v.remark) + end + for k, v in pairs(nodes_table) do + o:value(v.id, v.remark) + end + o.default = "nil" +end +uci:foreach(appname, "shunt_rules", function(e) + if e[".name"] and e.remarks then + o = s:option(ListValue, option_name(e[".name"]), string.format('* %s', api.url("shunt_rules", e[".name"]), e.remarks)) + o:value("nil", translate("Close")) + o:value("_default", translate("Default")) + o:value("_direct", translate("Direct Connection")) + o:value("_blackhole", translate("Blackhole")) + o:depends({ [option_name("protocol")] = "_shunt" }) + + if #nodes_table > 0 then + for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + end + for k, v in pairs(balancers_table) do + o:value(v.id, v.remark) + end + for k, v in pairs(iface_table) do + o:value(v.id, v.remark) + end + local pt = s:option(ListValue, option_name(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.remark) + pt:depends({ [option_name("protocol")] = "_shunt", [option_name("preproxy_enabled")] = true, [option_name(e[".name"])] = v.id }) + end + end + end +end) + +o = s:option(DummyValue, option_name("shunt_tips"), " ") +o.not_rewrite = true +o.rawhtml = true +o.cfgvalue = function(t, n) + return string.format('%s', translate("No shunt rules? Click me to go to add.")) +end +o:depends({ [option_name("protocol")] = "_shunt" }) + +local o = s:option(ListValue, option_name("default_node"), string.format('* %s', translate("Default"))) +o:depends({ [option_name("protocol")] = "_shunt" }) +o:value("_direct", translate("Direct Connection")) +o:value("_blackhole", translate("Blackhole")) + +if #nodes_table > 0 then + for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + end + for k, v in pairs(balancers_table) do + o:value(v.id, v.remark) + end + for k, v in pairs(iface_table) do + o:value(v.id, v.remark) + end + local dpt = s:option(ListValue, option_name("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 + o:value(v.id, v.remark) + dpt:depends({ [option_name("protocol")] = "_shunt", [option_name("preproxy_enabled")] = true, [option_name("default_node")] = v.id }) + end +end + +o = s:option(ListValue, option_name("domainStrategy"), translate("Domain Strategy")) +o:value("AsIs") +o:value("IPIfNonMatch") +o:value("IPOnDemand") +o.default = "IPOnDemand" +o.description = "
  • " .. translate("'AsIs': Only use domain for routing. Default value.") + .. "
  • " .. translate("'IPIfNonMatch': When no rule matches current domain, resolves it into IP addresses (A or AAAA records) and try all rules again.") + .. "
  • " .. translate("'IPOnDemand': As long as there is a IP-based rule, resolves the domain into IP immediately.") + .. "
" +o:depends({ [option_name("protocol")] = "_shunt" }) + +o = s:option(ListValue, option_name("domainMatcher"), translate("Domain matcher")) +o:value("hybrid") +o:value("linear") +o:depends({ [option_name("protocol")] = "_shunt" }) + +-- [[ 分流模块 End ]] + +o = s:option(Value, option_name("address"), translate("Address (Support Domain Name)")) + +o = s:option(Value, option_name("port"), translate("Port")) +o.datatype = "port" + +local protocols = s.fields[option_name("protocol")].keylist +if #protocols > 0 then + for index, value in ipairs(protocols) do + if not value:find("_") then + s.fields[option_name("address")]:depends({ [option_name("protocol")] = value }) + s.fields[option_name("port")]:depends({ [option_name("protocol")] = value }) + end + end +end + +o = s:option(Value, option_name("username"), translate("Username")) +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "socks" }) + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +o = s:option(ListValue, option_name("security"), translate("Encrypt Method")) +for a, t in ipairs(security_list) do o:value(t) end +o:depends({ [option_name("protocol")] = "vmess" }) + +o = s:option(Value, option_name("encryption"), translate("Encrypt Method")) +o.default = "none" +o:value("none") +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(ListValue, option_name("x_ss_encrypt_method"), translate("Encrypt Method")) +o.rewrite_option = "method" +for a, t in ipairs(x_ss_encrypt_method_list) do o:value(t) end +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(Flag, option_name("iv_check"), translate("IV Check")) +o:depends({ [option_name("protocol")] = "shadowsocks", [option_name("x_ss_encrypt_method")] = "aes-128-gcm" }) +o:depends({ [option_name("protocol")] = "shadowsocks", [option_name("x_ss_encrypt_method")] = "aes-256-gcm" }) +o:depends({ [option_name("protocol")] = "shadowsocks", [option_name("x_ss_encrypt_method")] = "chacha20-poly1305" }) +o:depends({ [option_name("protocol")] = "shadowsocks", [option_name("x_ss_encrypt_method")] = "xchacha20-poly1305" }) + +o = s:option(Flag, option_name("uot"), translate("UDP over TCP")) +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(Value, option_name("uuid"), translate("ID")) +o.password = true +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(ListValue, option_name("flow"), translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(Flag, option_name("tls"), translate("TLS")) +o.default = 0 +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "trojan" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(Flag, option_name("reality"), translate("REALITY")) +o.default = 0 +o:depends({ [option_name("tls")] = true, [option_name("transport")] = "tcp" }) +o:depends({ [option_name("tls")] = true, [option_name("transport")] = "h2" }) +o:depends({ [option_name("tls")] = true, [option_name("transport")] = "grpc" }) + +o = s:option(ListValue, option_name("alpn"), translate("alpn")) +o.default = "default" +o:value("default", translate("Default")) +o:value("h3,h2,http/1.1") +o:value("h3,h2") +o:value("h2,http/1.1") +o:value("h3") +o:value("h2") +o:value("http/1.1") +o:depends({ [option_name("tls")] = true, [option_name("reality")] = false }) + +-- o = s:option(Value, option_name("minversion"), translate("minversion")) +-- o.default = "1.3" +-- o:value("1.3") +-- o:depends({ [option_name("tls")] = true }) + +o = s:option(Value, option_name("tls_serverName"), translate("Domain")) +o:depends({ [option_name("tls")] = true }) + +o = s:option(Flag, option_name("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) +o.default = "0" +o:depends({ [option_name("tls")] = true, [option_name("reality")] = false }) + +-- [[ REALITY部分 ]] -- +o = s:option(Value, option_name("reality_publicKey"), translate("Public Key")) +o:depends({ [option_name("tls")] = true, [option_name("reality")] = true }) + +o = s:option(Value, option_name("reality_shortId"), translate("Short Id")) +o:depends({ [option_name("tls")] = true, [option_name("reality")] = true }) + +o = s:option(Value, option_name("reality_spiderX"), translate("Spider X")) +o.placeholder = "/" +o:depends({ [option_name("tls")] = true, [option_name("reality")] = true }) + +o = s:option(Flag, option_name("utls"), translate("uTLS")) +o.default = "0" +o:depends({ [option_name("tls")] = true, [option_name("reality")] = false }) + +o = s:option(ListValue, option_name("fingerprint"), translate("Finger Print")) +o:value("chrome") +o:value("firefox") +o:value("edge") +o:value("safari") +o:value("360") +o:value("qq") +o:value("ios") +o:value("android") +o:value("random") +o:value("randomized") +o.default = "chrome" +o:depends({ [option_name("tls")] = true, [option_name("utls")] = true }) +o:depends({ [option_name("tls")] = true, [option_name("reality")] = true }) + +o = s:option(ListValue, option_name("transport"), translate("Transport")) +o:value("tcp", "TCP") +o:value("mkcp", "mKCP") +o:value("ws", "WebSocket") +o:value("h2", "HTTP/2") +o:value("ds", "DomainSocket") +o:value("quic", "QUIC") +o:value("grpc", "gRPC") +o:value("httpupgrade", "HttpUpgrade") +o:value("splithttp", "SplitHTTP") +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +o = s:option(Value, option_name("wireguard_public_key"), translate("Public Key")) +o:depends({ [option_name("protocol")] = "wireguard" }) + +o = s:option(Value, option_name("wireguard_secret_key"), translate("Private Key")) +o:depends({ [option_name("protocol")] = "wireguard" }) + +o = s:option(Value, option_name("wireguard_preSharedKey"), translate("Pre shared key")) +o:depends({ [option_name("protocol")] = "wireguard" }) + +o = s:option(DynamicList, option_name("wireguard_local_address"), translate("Local Address")) +o:depends({ [option_name("protocol")] = "wireguard" }) + +o = s:option(Value, option_name("wireguard_mtu"), translate("MTU")) +o.default = "1420" +o:depends({ [option_name("protocol")] = "wireguard" }) + +if api.compare_versions(xray_version, ">=", "1.8.0") then + o = s:option(Value, option_name("wireguard_reserved"), translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) + o:depends({ [option_name("protocol")] = "wireguard" }) +end + +o = s:option(Value, option_name("wireguard_keepAlive"), translate("Keep Alive")) +o.default = "0" +o:depends({ [option_name("protocol")] = "wireguard" }) + +-- [[ TCP部分 ]]-- + +-- TCP伪装 +o = s:option(ListValue, option_name("tcp_guise"), translate("Camouflage Type")) +o:value("none", "none") +o:value("http", "http") +o:depends({ [option_name("transport")] = "tcp" }) + +-- HTTP域名 +o = s:option(DynamicList, option_name("tcp_guise_http_host"), translate("HTTP Host")) +o:depends({ [option_name("tcp_guise")] = "http" }) + +-- HTTP路径 +o = s:option(DynamicList, option_name("tcp_guise_http_path"), translate("HTTP Path")) +o.placeholder = "/" +o:depends({ [option_name("tcp_guise")] = "http" }) + +-- [[ mKCP部分 ]]-- + +o = s:option(ListValue, option_name("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 o:value(t) end +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_mtu"), translate("KCP MTU")) +o.default = "1350" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_tti"), translate("KCP TTI")) +o.default = "20" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity")) +o.default = "5" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity")) +o.default = "20" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Flag, option_name("mkcp_congestion"), translate("KCP Congestion")) +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_readBufferSize"), translate("KCP readBufferSize")) +o.default = "1" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_writeBufferSize"), translate("KCP writeBufferSize")) +o.default = "1" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_seed"), translate("KCP Seed")) +o:depends({ [option_name("transport")] = "mkcp" }) + +-- [[ WebSocket部分 ]]-- +o = s:option(Value, option_name("ws_host"), translate("WebSocket Host")) +o:depends({ [option_name("transport")] = "ws" }) + +o = s:option(Value, option_name("ws_path"), translate("WebSocket Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "ws" }) + +-- [[ HTTP/2部分 ]]-- +o = s:option(Value, option_name("h2_host"), translate("HTTP/2 Host")) +o:depends({ [option_name("transport")] = "h2" }) + +o = s:option(Value, option_name("h2_path"), translate("HTTP/2 Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "h2" }) + +o = s:option(Flag, option_name("h2_health_check"), translate("Health check")) +o:depends({ [option_name("transport")] = "h2" }) + +o = s:option(Value, option_name("h2_read_idle_timeout"), translate("Idle timeout")) +o.default = "10" +o:depends({ [option_name("h2_health_check")] = true }) + +o = s:option(Value, option_name("h2_health_check_timeout"), translate("Health check timeout")) +o.default = "15" +o:depends({ [option_name("h2_health_check")] = true }) + +-- [[ DomainSocket部分 ]]-- +o = s:option(Value, option_name("ds_path"), "Path", translate("A legal file path. This file must not exist before running.")) +o:depends({ [option_name("transport")] = "ds" }) + +-- [[ QUIC部分 ]]-- +o = s:option(ListValue, option_name("quic_security"), translate("Encrypt Method")) +o:value("none") +o:value("aes-128-gcm") +o:value("chacha20-poly1305") +o:depends({ [option_name("transport")] = "quic" }) + +o = s:option(Value, option_name("quic_key"), translate("Encrypt Method") .. translate("Key")) +o:depends({ [option_name("transport")] = "quic" }) + +o = s:option(ListValue, option_name("quic_guise"), translate("Camouflage Type")) +for a, t in ipairs(header_type_list) do o:value(t) end +o:depends({ [option_name("transport")] = "quic" }) + +-- [[ gRPC部分 ]]-- +o = s:option(Value, option_name("grpc_serviceName"), "ServiceName") +o:depends({ [option_name("transport")] = "grpc" }) + +o = s:option(ListValue, option_name("grpc_mode"), "gRPC " .. translate("Transfer mode")) +o:value("gun") +o:value("multi") +o:depends({ [option_name("transport")] = "grpc" }) + +o = s:option(Flag, option_name("grpc_health_check"), translate("Health check")) +o:depends({ [option_name("transport")] = "grpc" }) + +o = s:option(Value, option_name("grpc_idle_timeout"), translate("Idle timeout")) +o.default = "10" +o:depends({ [option_name("grpc_health_check")] = true }) + +o = s:option(Value, option_name("grpc_health_check_timeout"), translate("Health check timeout")) +o.default = "20" +o:depends({ [option_name("grpc_health_check")] = true }) + +o = s:option(Flag, option_name("grpc_permit_without_stream"), translate("Permit without stream")) +o.default = "0" +o:depends({ [option_name("grpc_health_check")] = true }) + +o = s:option(Value, option_name("grpc_initial_windows_size"), translate("Initial Windows Size")) +o.default = "0" +o:depends({ [option_name("transport")] = "grpc" }) + +-- [[ HttpUpgrade部分 ]]-- +o = s:option(Value, option_name("httpupgrade_host"), translate("HttpUpgrade Host")) +o:depends({ [option_name("transport")] = "httpupgrade" }) + +o = s:option(Value, option_name("httpupgrade_path"), translate("HttpUpgrade Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "httpupgrade" }) + +-- [[ SplitHTTP部分 ]]-- +o = s:option(Value, option_name("splithttp_host"), translate("SplitHTTP Host")) +o:depends({ [option_name("transport")] = "splithttp" }) + +o = s:option(Value, option_name("splithttp_path"), translate("SplitHTTP Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "splithttp" }) + +-- [[ Mux ]]-- +o = s:option(Flag, option_name("mux"), translate("Mux")) +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless", [option_name("flow")] = "" }) +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +o = s:option(Value, option_name("mux_concurrency"), translate("Mux concurrency")) +o.default = 8 +o:depends({ [option_name("mux")] = true }) + +-- [[ XUDP Mux ]]-- +o = s:option(Flag, option_name("xmux"), translate("xMux")) +o.default = 1 +o:depends({ [option_name("protocol")] = "vless", [option_name("flow")] = "xtls-rprx-vision" }) +o:depends({ [option_name("protocol")] = "vless", [option_name("flow")] = "xtls-rprx-vision-udp443" }) + +o = s:option(Value, option_name("xudp_concurrency"), translate("XUDP Mux concurrency")) +o.default = 8 +o:depends({ [option_name("xmux")] = true }) + +--[[tcpMptcp]] +o = s:option(Flag, option_name("tcpMptcp"), "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration.")) +o.default = 0 + +o = s:option(Flag, option_name("tcpNoDelay"), "tcpNoDelay") +o.default = 0 + +o = s:option(ListValue, option_name("to_node"), translate("Landing node"), translate("Only support a layer of proxy.")) +o.default = "" +o:value("", translate("Close(Not use)")) +for k, v in pairs(nodes_table) do + if v.type == "Xray" then + o:value(v.id, v.remark) + end +end + +for i, v in ipairs(s.fields[option_name("protocol")].keylist) do + if not v:find("_") then + s.fields[option_name("tcpMptcp")]:depends({ [option_name("protocol")] = v }) + s.fields[option_name("tcpNoDelay")]:depends({ [option_name("protocol")] = v }) + s.fields[option_name("to_node")]:depends({ [option_name("protocol")] = v }) + end +end + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua new file mode 100644 index 000000000..887857e9b --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua @@ -0,0 +1,665 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +local singbox_bin = api.finded_com("singbox") + +if not singbox_bin then + return +end + +local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'") + +local appname = api.appname +local uci = api.uci + +local type_name = "sing-box" + +local option_prefix = "singbox_" + +local function option_name(name) + return option_prefix .. name +end + +local ss_method_new_list = { + "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" +} + +local ss_method_old_list = { + "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5", "chacha20-ietf", "xchacha20", +} + +local security_list = { "none", "auto", "aes-128-gcm", "chacha20-poly1305", "zero" } + +-- [[ sing-box ]] + +s.fields["type"]:value(type_name, translate("Sing-Box")) + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +o:value("socks", "Socks") +o:value("http", "HTTP") +o:value("shadowsocks", "Shadowsocks") +if singbox_tags:find("with_shadowsocksr") then + o:value("shadowsocksr", "ShadowsocksR") +end +o:value("vmess", "Vmess") +o:value("trojan", "Trojan") +if singbox_tags:find("with_wireguard") then + o:value("wireguard", "WireGuard") +end +if singbox_tags:find("with_quic") then + o:value("hysteria", "Hysteria") +end +o:value("vless", "VLESS") +if singbox_tags:find("with_quic") then + o:value("tuic", "TUIC") +end +if singbox_tags:find("with_quic") then + o:value("hysteria2", "Hysteria2") +end +o:value("_shunt", translate("Shunt")) +o:value("_iface", translate("Custom Interface")) + +o = s:option(Value, option_name("iface"), translate("Interface")) +o.default = "eth1" +o:depends({ [option_name("protocol")] = "_iface" }) + +local nodes_table = {} +local iface_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"], + remark = e["remark"], + type = e["type"] + } + end + if e.protocol == "_iface" then + iface_table[#iface_table + 1] = { + id = e[".name"], + remark = e["remark"] + } + end +end + +local socks_list = {} +uci:foreach(appname, "socks", function(s) + if s.enabled == "1" and s.node then + socks_list[#socks_list + 1] = { + id = "Socks_" .. s[".name"], + remark = translate("Socks Config") .. " [" .. s.port .. "端口]" + } + end +end) + +-- [[ 分流模块 ]] +if #nodes_table > 0 then + o = s:option(Flag, option_name("preproxy_enabled"), translate("Preproxy")) + o:depends({ [option_name("protocol")] = "_shunt" }) + + o = s:option(ListValue, option_name("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({ [option_name("protocol")] = "_shunt", [option_name("preproxy_enabled")] = true }) + for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + end + for k, v in pairs(iface_table) do + o:value(v.id, v.remark) + end + for k, v in pairs(nodes_table) do + o:value(v.id, v.remark) + end + o.default = "nil" +end +uci:foreach(appname, "shunt_rules", function(e) + if e[".name"] and e.remarks then + o = s:option(ListValue, option_name(e[".name"]), string.format('* %s', api.url("shunt_rules", e[".name"]), e.remarks)) + o:value("nil", translate("Close")) + o:value("_default", translate("Default")) + o:value("_direct", translate("Direct Connection")) + o:value("_blackhole", translate("Blackhole")) + o:depends({ [option_name("protocol")] = "_shunt" }) + + if #nodes_table > 0 then + for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + end + for k, v in pairs(iface_table) do + o:value(v.id, v.remark) + end + local pt = s:option(ListValue, option_name(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.remark) + pt:depends({ [option_name("protocol")] = "_shunt", [option_name("preproxy_enabled")] = true, [option_name(e[".name"])] = v.id }) + end + end + end +end) + +o = s:option(DummyValue, option_name("shunt_tips"), " ") +o.not_rewrite = true +o.rawhtml = true +o.cfgvalue = function(t, n) + return string.format('%s', translate("No shunt rules? Click me to go to add.")) +end +o:depends({ [option_name("protocol")] = "_shunt" }) + +local o = s:option(ListValue, option_name("default_node"), string.format('* %s', translate("Default"))) +o:depends({ [option_name("protocol")] = "_shunt" }) +o:value("_direct", translate("Direct Connection")) +o:value("_blackhole", translate("Blackhole")) + +if #nodes_table > 0 then + for k, v in pairs(socks_list) do + o:value(v.id, v.remark) + end + for k, v in pairs(iface_table) do + o:value(v.id, v.remark) + end + local dpt = s:option(ListValue, option_name("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 + o:value(v.id, v.remark) + dpt:depends({ [option_name("protocol")] = "_shunt", [option_name("preproxy_enabled")] = true, [option_name("default_node")] = v.id }) + end +end + +-- [[ 分流模块 End ]] + +o = s:option(Value, option_name("address"), translate("Address (Support Domain Name)")) + +o = s:option(Value, option_name("port"), translate("Port")) +o.datatype = "port" + +local protocols = s.fields[option_name("protocol")].keylist +if #protocols > 0 then + for index, value in ipairs(protocols) do + if not value:find("_") then + s.fields[option_name("address")]:depends({ [option_name("protocol")] = value }) + s.fields[option_name("port")]:depends({ [option_name("protocol")] = value }) + end + end +end + +o = s:option(Value, option_name("username"), translate("Username")) +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "socks" }) + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "shadowsocksr" }) +o:depends({ [option_name("protocol")] = "trojan" }) +o:depends({ [option_name("protocol")] = "tuic" }) + +o = s:option(ListValue, option_name("security"), translate("Encrypt Method")) +for a, t in ipairs(security_list) do o:value(t) end +o:depends({ [option_name("protocol")] = "vmess" }) + +o = s:option(ListValue, option_name("ss_method"), translate("Encrypt Method")) +o.rewrite_option = "method" +for a, t in ipairs(ss_method_new_list) do o:value(t) end +for a, t in ipairs(ss_method_old_list) do o:value(t) end +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +if singbox_tags:find("with_shadowsocksr") then + o = s:option(ListValue, option_name("ssr_method"), translate("Encrypt Method")) + o.rewrite_option = "method" + for a, t in ipairs(ss_method_old_list) do o:value(t) end + o:depends({ [option_name("protocol")] = "shadowsocksr" }) + + 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" + } + + o = s:option(ListValue, option_name("ssr_protocol"), translate("Protocol")) + for a, t in ipairs(ssr_protocol_list) do o:value(t) end + o:depends({ [option_name("protocol")] = "shadowsocksr" }) + + o = s:option(Value, option_name("ssr_protocol_param"), translate("Protocol_param")) + o:depends({ [option_name("protocol")] = "shadowsocksr" }) + + local ssr_obfs_list = { + "plain", "http_simple", "http_post", "random_head", "tls_simple", + "tls1.0_session_auth", "tls1.2_ticket_auth" + } + + o = s:option(ListValue, option_name("ssr_obfs"), translate("Obfs")) + for a, t in ipairs(ssr_obfs_list) do o:value(t) end + o:depends({ [option_name("protocol")] = "shadowsocksr" }) + + o = s:option(Value, option_name("ssr_obfs_param"), translate("Obfs_param")) + o:depends({ [option_name("protocol")] = "shadowsocksr" }) +end + +o = s:option(Flag, option_name("uot"), translate("UDP over TCP")) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(Value, option_name("uuid"), translate("ID")) +o.password = true +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "tuic" }) + +o = s:option(Value, option_name("alter_id"), "Alter ID") +o.datatype = "uinteger" +o.default = "0" +o:depends({ [option_name("protocol")] = "vmess" }) + +o = s:option(Flag, option_name("global_padding"), "global_padding", translate("Protocol parameter. Will waste traffic randomly if enabled.")) +o.default = "0" +o:depends({ [option_name("protocol")] = "vmess" }) + +o = s:option(Flag, option_name("authenticated_length"), "authenticated_length", translate("Protocol parameter. Enable length block encryption.")) +o.default = "0" +o:depends({ [option_name("protocol")] = "vmess" }) + +o = s:option(ListValue, option_name("flow"), translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:depends({ [option_name("protocol")] = "vless", [option_name("tls")] = true }) + +if singbox_tags:find("with_quic") then + o = s:option(Value, option_name("hysteria_obfs"), translate("Obfs Password")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(ListValue, option_name("hysteria_auth_type"), translate("Auth Type")) + o:value("disable", translate("Disable")) + o:value("string", translate("STRING")) + o:value("base64", translate("BASE64")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_auth_password"), translate("Auth Password")) + o.password = true + o:depends({ [option_name("protocol")] = "hysteria", [option_name("hysteria_auth_type")] = "string"}) + o:depends({ [option_name("protocol")] = "hysteria", [option_name("hysteria_auth_type")] = "base64"}) + + o = s:option(Value, option_name("hysteria_up_mbps"), translate("Max upload Mbps")) + o.default = "10" + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_down_mbps"), translate("Max download Mbps")) + o.default = "50" + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_recv_window_conn"), translate("QUIC stream receive window")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_recv_window"), translate("QUIC connection receive window")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Flag, option_name("hysteria_disable_mtu_discovery"), translate("Disable MTU detection")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_alpn"), translate("QUIC TLS ALPN")) + o:depends({ [option_name("protocol")] = "hysteria" }) +end + +if singbox_tags:find("with_quic") then + o = s:option(ListValue, option_name("tuic_congestion_control"), translate("Congestion control algorithm")) + o.default = "cubic" + o:value("bbr", translate("BBR")) + o:value("cubic", translate("CUBIC")) + o:value("new_reno", translate("New Reno")) + o:depends({ [option_name("protocol")] = "tuic" }) + + o = s:option(ListValue, option_name("tuic_udp_relay_mode"), translate("UDP relay mode")) + o.default = "native" + o:value("native", translate("native")) + o:value("quic", translate("QUIC")) + o:depends({ [option_name("protocol")] = "tuic" }) + + --[[ + o = s:option(Flag, option_name("tuic_udp_over_stream"), translate("UDP over stream")) + o:depends({ [option_name("protocol")] = "tuic" }) + ]]-- + + o = s:option(Flag, option_name("tuic_zero_rtt_handshake"), translate("Enable 0-RTT QUIC handshake")) + o.default = 0 + o:depends({ [option_name("protocol")] = "tuic" }) + + o = s:option(Value, option_name("tuic_heartbeat"), translate("Heartbeat interval(second)")) + o.datatype = "uinteger" + o.default = "3" + o:depends({ [option_name("protocol")] = "tuic" }) + + o = s:option(Value, option_name("tuic_alpn"), translate("QUIC TLS ALPN")) + o:depends({ [option_name("protocol")] = "tuic" }) +end + +if singbox_tags:find("with_quic") then + o = s:option(Value, option_name("hysteria2_up_mbps"), translate("Max upload Mbps")) + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(Value, option_name("hysteria2_down_mbps"), translate("Max download Mbps")) + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(ListValue, option_name("hysteria2_obfs_type"), translate("Obfs Type")) + o:value("", translate("Disable")) + o:value("salamander") + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(Value, option_name("hysteria2_obfs_password"), translate("Obfs Password")) + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(Value, option_name("hysteria2_auth_password"), translate("Auth Password")) + o.password = true + o:depends({ [option_name("protocol")] = "hysteria2"}) +end + +o = s:option(Flag, option_name("tls"), translate("TLS")) +o.default = 0 +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +o = s:option(ListValue, option_name("alpn"), translate("alpn")) +o.default = "default" +o:value("default", translate("Default")) +o:value("h2,http/1.1") +o:value("h2") +o:value("http/1.1") +o:depends({ [option_name("tls")] = true }) + +o = s:option(Value, option_name("tls_serverName"), translate("Domain")) +o:depends({ [option_name("tls")] = true }) +o:depends({ [option_name("protocol")] = "hysteria"}) +o:depends({ [option_name("protocol")] = "tuic" }) +o:depends({ [option_name("protocol")] = "hysteria2" }) + +o = s:option(Flag, option_name("tls_allowInsecure"), translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) +o.default = "0" +o:depends({ [option_name("tls")] = true }) +o:depends({ [option_name("protocol")] = "hysteria"}) +o:depends({ [option_name("protocol")] = "tuic" }) +o:depends({ [option_name("protocol")] = "hysteria2" }) + +if singbox_tags:find("with_ech") then + o = s:option(Flag, option_name("ech"), translate("ECH")) + o.default = "0" + o:depends({ [option_name("tls")] = true, [option_name("flow")] = "", [option_name("reality")] = false }) + o:depends({ [option_name("protocol")] = "tuic" }) + o:depends({ [option_name("protocol")] = "hysteria" }) + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(Value, option_name("ech_config"), translate("ECH Config")) + o.default = "" + o:depends({ [option_name("ech")] = true }) + + o = s:option(Flag, option_name("pq_signature_schemes_enabled"), translate("PQ signature schemes")) + o.default = "0" + o:depends({ [option_name("ech")] = true }) + + o = s:option(Flag, option_name("dynamic_record_sizing_disabled"), translate("Disable adaptive sizing of TLS records")) + o.default = "0" + o:depends({ [option_name("ech")] = true }) +end + +if singbox_tags:find("with_utls") then + o = s:option(Flag, option_name("utls"), translate("uTLS")) + o.default = "0" + o:depends({ [option_name("tls")] = true }) + + o = s:option(ListValue, option_name("fingerprint"), translate("Finger Print")) + o:value("chrome") + o:value("firefox") + o:value("edge") + o:value("safari") + -- o:value("360") + o:value("qq") + o:value("ios") + -- o:value("android") + o:value("random") + -- o:value("randomized") + o.default = "chrome" + o:depends({ [option_name("tls")] = true, [option_name("utls")] = true }) + + -- [[ REALITY部分 ]] -- + o = s:option(Flag, option_name("reality"), translate("REALITY")) + o.default = 0 + o:depends({ [option_name("protocol")] = "vless", [option_name("utls")] = true }) + o:depends({ [option_name("protocol")] = "vmess", [option_name("utls")] = true }) + o:depends({ [option_name("protocol")] = "shadowsocks", [option_name("utls")] = true }) + o:depends({ [option_name("protocol")] = "socks", [option_name("utls")] = true }) + o:depends({ [option_name("protocol")] = "trojan", [option_name("utls")] = true }) + + o = s:option(Value, option_name("reality_publicKey"), translate("Public Key")) + o:depends({ [option_name("utls")] = true, [option_name("reality")] = true }) + + o = s:option(Value, option_name("reality_shortId"), translate("Short Id")) + o:depends({ [option_name("utls")] = true, [option_name("reality")] = true }) +end + +o = s:option(ListValue, option_name("transport"), translate("Transport")) +o:value("tcp", "TCP") +o:value("http", "HTTP") +o:value("ws", "WebSocket") +o:value("httpupgrade", "HTTPUpgrade") +if singbox_tags:find("with_quic") then + o:value("quic", "QUIC") +end +if singbox_tags:find("with_grpc") then + o:value("grpc", "gRPC") +else o:value("grpc", "gRPC-lite") +end +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +if singbox_tags:find("with_wireguard") then + o = s:option(Value, option_name("wireguard_public_key"), translate("Public Key")) + o:depends({ [option_name("protocol")] = "wireguard" }) + + o = s:option(Value, option_name("wireguard_secret_key"), translate("Private Key")) + o:depends({ [option_name("protocol")] = "wireguard" }) + + o = s:option(Value, option_name("wireguard_preSharedKey"), translate("Pre shared key")) + o:depends({ [option_name("protocol")] = "wireguard" }) + + o = s:option(DynamicList, option_name("wireguard_local_address"), translate("Local Address")) + o:depends({ [option_name("protocol")] = "wireguard" }) + + o = s:option(Value, option_name("wireguard_mtu"), translate("MTU")) + o.default = "1420" + o:depends({ [option_name("protocol")] = "wireguard" }) + + o = s:option(Flag, option_name("wireguard_system_interface"), translate("System interface")) + o.default = 0 + o:depends({ [option_name("protocol")] = "wireguard" }) + + o = s:option(Value, option_name("wireguard_interface_name"), translate("System interface name")) + o:depends({ [option_name("protocol")] = "wireguard" }) + + o = s:option(Value, option_name("wireguard_reserved"), translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) + o:depends({ [option_name("protocol")] = "wireguard" }) +end + +-- [[ HTTP部分 ]]-- +o = s:option(Value, option_name("http_host"), translate("HTTP Host")) +o:depends({ [option_name("transport")] = "http" }) + +o = s:option(Value, option_name("http_path"), translate("HTTP Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "http" }) + +o = s:option(Flag, option_name("http_h2_health_check"), translate("Health check")) +o:depends({ [option_name("tls")] = true, [option_name("transport")] = "http" }) + +o = s:option(Value, option_name("http_h2_read_idle_timeout"), translate("Idle timeout")) +o.default = "10" +o:depends({ [option_name("tls")] = true, [option_name("transport")] = "http", [option_name("http_h2_health_check")] = true }) + +o = s:option(Value, option_name("http_h2_health_check_timeout"), translate("Health check timeout")) +o.default = "15" +o:depends({ [option_name("tls")] = true, [option_name("transport")] = "http", [option_name("http_h2_health_check")] = true }) + +-- [[ WebSocket部分 ]]-- +o = s:option(Value, option_name("ws_host"), translate("WebSocket Host")) +o:depends({ [option_name("transport")] = "ws" }) + +o = s:option(Value, option_name("ws_path"), translate("WebSocket Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "ws" }) + +o = s:option(Flag, option_name("ws_enableEarlyData"), translate("Enable early data")) +o:depends({ [option_name("transport")] = "ws" }) + +o = s:option(Value, option_name("ws_maxEarlyData"), translate("Early data length")) +o.default = "1024" +o:depends({ [option_name("ws_enableEarlyData")] = true }) + +o = s:option(Value, option_name("ws_earlyDataHeaderName"), translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol")) +o:depends({ [option_name("ws_enableEarlyData")] = true }) + +-- [[ HTTPUpgrade部分 ]]-- +o = s:option(Value, option_name("httpupgrade_host"), translate("HTTPUpgrade Host")) +o:depends({ [option_name("transport")] = "httpupgrade" }) + +o = s:option(Value, option_name("httpupgrade_path"), translate("HTTPUpgrade Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "httpupgrade" }) + +-- [[ gRPC部分 ]]-- +o = s:option(Value, option_name("grpc_serviceName"), "ServiceName") +o:depends({ [option_name("transport")] = "grpc" }) + +o = s:option(Flag, option_name("grpc_health_check"), translate("Health check")) +o:depends({ [option_name("transport")] = "grpc" }) + +o = s:option(Value, option_name("grpc_idle_timeout"), translate("Idle timeout")) +o.default = "10" +o:depends({ [option_name("grpc_health_check")] = true }) + +o = s:option(Value, option_name("grpc_health_check_timeout"), translate("Health check timeout")) +o.default = "20" +o:depends({ [option_name("grpc_health_check")] = true }) + +o = s:option(Flag, option_name("grpc_permit_without_stream"), translate("Permit without stream")) +o.default = "0" +o:depends({ [option_name("grpc_health_check")] = true }) + +-- [[ Mux ]]-- +o = s:option(Flag, option_name("mux"), translate("Mux")) +o.rmempty = false +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless", [option_name("flow")] = "" }) +o:depends({ [option_name("protocol")] = "shadowsocks", [option_name("uot")] = "" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +o = s:option(ListValue, option_name("mux_type"), translate("Mux")) +o:value("smux") +o:value("yamux") +o:value("h2mux") +o:depends({ [option_name("mux")] = true }) + +o = s:option(Value, option_name("mux_concurrency"), translate("Mux concurrency")) +o.default = 4 +o:depends({ [option_name("mux")] = true, [option_name("tcpbrutal")] = false }) + +o = s:option(Flag, option_name("mux_padding"), translate("Padding")) +o.default = 0 +o:depends({ [option_name("mux")] = true }) + +-- [[ TCP Brutal ]]-- +o = s:option(Flag, option_name("tcpbrutal"), translate("TCP Brutal")) +o.default = 0 +o:depends({ [option_name("mux")] = true }) + +o = s:option(Value, option_name("tcpbrutal_up_mbps"), translate("Max upload Mbps")) +o.default = "10" +o:depends({ [option_name("tcpbrutal")] = true }) + +o = s:option(Value, option_name("tcpbrutal_down_mbps"), translate("Max download Mbps")) +o.default = "50" +o:depends({ [option_name("tcpbrutal")] = true }) + +o = s:option(Flag, option_name("shadowtls"), "ShadowTLS") +o.default = 0 +o:depends({ [option_name("protocol")] = "vmess", [option_name("tls")] = false }) +o:depends({ [option_name("protocol")] = "shadowsocks", [option_name("tls")] = false }) + +o = s:option(ListValue, option_name("shadowtls_version"), "ShadowTLS " .. translate("Version")) +o.default = "1" +o:value("1", "ShadowTLS v1") +o:value("2", "ShadowTLS v2") +o:value("3", "ShadowTLS v3") +o:depends({ [option_name("shadowtls")] = true }) + +o = s:option(Value, option_name("shadowtls_password"), "ShadowTLS " .. translate("Password")) +o.password = true +o:depends({ [option_name("shadowtls")] = true, [option_name("shadowtls_version")] = "2" }) +o:depends({ [option_name("shadowtls")] = true, [option_name("shadowtls_version")] = "3" }) + +o = s:option(Value, option_name("shadowtls_serverName"), "ShadowTLS " .. translate("Domain")) +o:depends({ [option_name("shadowtls")] = true }) + +if singbox_tags:find("with_utls") then + o = s:option(Flag, option_name("shadowtls_utls"), "ShadowTLS " .. translate("uTLS")) + o.default = "0" + o:depends({ [option_name("shadowtls")] = true }) + + o = s:option(ListValue, option_name("shadowtls_fingerprint"), "ShadowTLS " .. translate("Finger Print")) + o:value("chrome") + o:value("firefox") + o:value("edge") + o:value("safari") + -- o:value("360") + o:value("qq") + o:value("ios") + -- o:value("android") + o:value("random") + -- o:value("randomized") + o.default = "chrome" + o:depends({ [option_name("shadowtls")] = true, [option_name("shadowtls_utls")] = true }) +end + +-- [[ SIP003 plugin ]]-- +o = s:option(Flag, option_name("plugin_enabled"), translate("plugin")) +o.default = 0 +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(ListValue, option_name("plugin"), "SIP003 " .. translate("plugin")) +o.default = "obfs-local" +o:depends({ [option_name("plugin_enabled")] = true }) +o:value("obfs-local") +o:value("v2ray-plugin") + +o = s:option(Value, option_name("plugin_opts"), translate("opts")) +o:depends({ [option_name("plugin_enabled")] = true }) + +o = s:option(ListValue, option_name("domain_strategy"), translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) +o.default = "prefer_ipv6" +o:value("prefer_ipv4") +o:value("prefer_ipv6") +o:value("ipv4_only") +o:value("ipv6_only") + +o = s:option(ListValue, option_name("to_node"), translate("Landing node"), translate("Only support a layer of proxy.")) +o.default = "" +o:value("", translate("Close(Not use)")) +for k, v in pairs(nodes_table) do + if v.type == "sing-box" then + o:value(v.id, v.remark) + end +end +for i, v in ipairs(s.fields[option_name("protocol")].keylist) do + if not v:find("_") then + o:depends({ [option_name("protocol")] = v }) + end +end + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua new file mode 100644 index 000000000..c381d5e99 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua @@ -0,0 +1,57 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.is_finded("sslocal") then + return +end + +local type_name = "SS-Rust" + +local option_prefix = "ssrust_" + +local function option_name(name) + return option_prefix .. name +end + +local ssrust_encrypt_method_list = { + "plain", "none", + "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", + "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305" +} + +-- [[ Shadowsocks Rust ]] + +s.fields["type"]:value(type_name, translate("Shadowsocks Rust")) + +o = s:option(Value, option_name("address"), translate("Address (Support Domain Name)")) + +o = s:option(Value, option_name("port"), translate("Port")) +o.datatype = "port" + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true + +o = s:option(Value, option_name("method"), translate("Encrypt Method")) +for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end + +o = s:option(Value, option_name("timeout"), translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 + +o = s:option(ListValue, option_name("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required")) +o:value("false") +o:value("true") + +o = s:option(ListValue, option_name("plugin"), translate("plugin")) +o:value("none", translate("none")) +if api.is_finded("xray-plugin") then o:value("xray-plugin") end +if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end +if api.is_finded("obfs-local") then o:value("obfs-local") end + +o = s:option(Value, option_name("plugin_opts"), translate("opts")) +o:depends({ [option_name("plugin")] = "xray-plugin"}) +o:depends({ [option_name("plugin")] = "v2ray-plugin"}) +o:depends({ [option_name("plugin")] = "obfs-local"}) + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss.lua new file mode 100644 index 000000000..3984a5115 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss.lua @@ -0,0 +1,58 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.is_finded("ss-local") then + return +end + +local type_name = "SS" + +local option_prefix = "ss_" + +local function option_name(name) + return option_prefix .. name +end + +local ss_encrypt_method_list = { + "rc4-md5", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", + "aes-192-ctr", "aes-256-ctr", "bf-cfb", "salsa20", "chacha20", "chacha20-ietf", + "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305" +} + +-- [[ Shadowsocks Libev ]] + +s.fields["type"]:value(type_name, translate("Shadowsocks Libev")) + +o = s:option(Value, option_name("address"), translate("Address (Support Domain Name)")) + +o = s:option(Value, option_name("port"), translate("Port")) +o.datatype = "port" + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true + +o = s:option(Value, option_name("method"), translate("Encrypt Method")) +for a, t in ipairs(ss_encrypt_method_list) do o:value(t) end + +o = s:option(Value, option_name("timeout"), translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 + +o = s:option(ListValue, option_name("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required")) +o:value("false") +o:value("true") + +o = s:option(ListValue, option_name("plugin"), translate("plugin")) +o:value("none", translate("none")) +if api.is_finded("xray-plugin") then o:value("xray-plugin") end +if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end +if api.is_finded("obfs-local") then o:value("obfs-local") end + +o = s:option(Value, option_name("plugin_opts"), translate("opts")) +o:depends({ [option_name("plugin")] = "xray-plugin"}) +o:depends({ [option_name("plugin")] = "v2ray-plugin"}) +o:depends({ [option_name("plugin")] = "obfs-local"}) + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua new file mode 100644 index 000000000..9e8d40020 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua @@ -0,0 +1,69 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.is_finded("ssr-local") then + return +end + +local type_name = "SSR" + +local option_prefix = "ssr_" + +local function option_name(name) + return option_prefix .. name +end + +local ssr_encrypt_method_list = { + "none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb", + "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", + "bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", + "cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20", + "chacha20-ietf" +} + +local ssr_protocol_list = { + "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", + "auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5", + "auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c", + "auth_chain_d", "auth_chain_e", "auth_chain_f" +} +local ssr_obfs_list = { + "plain", "http_simple", "http_post", "random_head", "tls_simple", + "tls1.0_session_auth", "tls1.2_ticket_auth" +} + +-- [[ ShadowsocksR Libev ]] + +s.fields["type"]:value(type_name, translate("ShadowsocksR Libev")) + +o = s:option(Value, option_name("address"), translate("Address (Support Domain Name)")) + +o = s:option(Value, option_name("port"), translate("Port")) +o.datatype = "port" + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true + +o = s:option(ListValue, option_name("method"), translate("Encrypt Method")) +for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +for a, t in ipairs(ssr_protocol_list) do o:value(t) end + +o = s:option(Value, option_name("protocol_param"), translate("Protocol_param")) + +o = s:option(ListValue, option_name("obfs"), translate("Obfs")) +for a, t in ipairs(ssr_obfs_list) do o:value(t) end + +o = s:option(Value, option_name("obfs_param"), translate("Obfs_param")) + +o = s:option(Value, option_name("timeout"), translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 + +o = s:option(ListValue, option_name("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required")) +o:value("false") +o:value("true") + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/tuic.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/tuic.lua new file mode 100644 index 000000000..bfa1a7dcb --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/tuic.lua @@ -0,0 +1,133 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.is_finded("tuic-client") then + return +end + +local type_name = "TUIC" + +local option_prefix = "tuic_" + +local function option_name(name) + return option_prefix .. name +end + +-- [[ TUIC ]] + +s.fields["type"]:value(type_name, translate("TUIC")) + +o = s:option(Value, option_name("address"), translate("Address (Support Domain Name)")) + +o = s:option(Value, option_name("port"), translate("Port")) +o.datatype = "port" + +o = s:option(Value, option_name("uuid"), translate("ID")) +o.password = true + +-- Tuic Password for remote server connect +o = s:option(Value, option_name("password"), translate("TUIC User Password For Connect Remote Server")) +o.password = true +o.rmempty = true +o.default = "" +o.rewrite_option = o.option + +--[[ +-- Tuic username for local socks connect +o = s:option(Value, option_name("socks_username"), translate("TUIC UserName For Local Socks")) +o.rmempty = true +o.default = "" +o.rewrite_option = o.option + +-- Tuic Password for local socks connect +o = s:option(Value, option_name("socks_password"), translate("TUIC Password For Local Socks")) +o.password = true +o.rmempty = true +o.default = "" +o.rewrite_option = o.option +--]] + +o = s:option(Value, option_name("ip"), translate("Set the TUIC proxy server ip address")) +o.datatype = "ipaddr" +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(ListValue, option_name("udp_relay_mode"), translate("UDP relay mode")) +o:value("native", translate("native")) +o:value("quic", translate("QUIC")) +o.default = "native" +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(ListValue, option_name("congestion_control"), translate("Congestion control algorithm")) +o:value("bbr", translate("BBR")) +o:value("cubic", translate("CUBIC")) +o:value("new_reno", translate("New Reno")) +o.default = "cubic" +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Value, option_name("heartbeat"), translate("Heartbeat interval(second)")) +o.datatype = "uinteger" +o.default = "3" +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Value, option_name("timeout"), translate("Timeout for establishing a connection to server(second)")) +o.datatype = "uinteger" +o.default = "8" +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Value, option_name("gc_interval"), translate("Garbage collection interval(second)")) +o.datatype = "uinteger" +o.default = "3" +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Value, option_name("gc_lifetime"), translate("Garbage collection lifetime(second)")) +o.datatype = "uinteger" +o.default = "15" +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Value, option_name("send_window"), translate("TUIC send window")) +o.datatype = "uinteger" +o.default = 20971520 +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Value, option_name("receive_window"), translate("TUIC receive window")) +o.datatype = "uinteger" +o.default = 10485760 +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Value, option_name("max_package_size"), translate("TUIC Maximum packet size the socks5 server can receive from external, in bytes")) +o.datatype = "uinteger" +o.default = 1500 +o.rmempty = true +o.rewrite_option = o.option + +--Tuic settings for the local inbound socks5 server +o = s:option(Flag, option_name("dual_stack"), translate("Set if the listening socket should be dual-stack")) +o.default = 0 +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Flag, option_name("disable_sni"), translate("Disable SNI")) +o.default = 0 +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(Flag, option_name("zero_rtt_handshake"), translate("Enable 0-RTT QUIC handshake")) +o.default = 0 +o.rmempty = true +o.rewrite_option = o.option + +o = s:option(DynamicList, option_name("tls_alpn"), translate("TLS ALPN")) +o.rmempty = true +o.rewrite_option = o.option + +api.luci_types(arg[1], m, s, type_name, option_prefix) 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..1fb77ea66 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/index.lua @@ -0,0 +1,67 @@ +local api = require "luci.passwall2.api" + +m = Map("passwall2_server", translate("Server-Side")) +api.set_apply_on_parse(m) + +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 == "sing-box" or v == "Xray" then + local protocol = m:get(n, "protocol") + 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/type/hysteria2.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua new file mode 100644 index 000000000..61c1c857f --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua @@ -0,0 +1,75 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.finded_com("hysteria") then + return +end + +local type_name = "Hysteria2" + +local option_prefix = "hysteria2_" + +local function option_name(name) + return option_prefix .. name +end + +-- [[ Hysteria2 ]] + +s.fields["type"]:value(type_name, "Hysteria2") + +o = s:option(Value, option_name("port"), translate("Listen Port")) +o.datatype = "port" + +o = s:option(Value, option_name("obfs"), translate("Obfs Password")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("auth_password"), translate("Auth Password")) +o.password = true +o.rewrite_option = o.option + +o = s:option(Flag, option_name("udp"), translate("UDP")) +o.default = "1" +o.rewrite_option = o.option + +o = s:option(Value, option_name("up_mbps"), translate("Max upload Mbps")) +o.rewrite_option = o.option + +o = s:option(Value, option_name("down_mbps"), translate("Max download Mbps")) +o.rewrite_option = o.option + +o = s:option(Flag, option_name("ignoreClientBandwidth"), translate("ignoreClientBandwidth")) +o.default = "0" +o.rewrite_option = o.option + +o = s:option(FileUpload, option_name("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" +o.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 + +o = s:option(FileUpload, option_name("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" +o.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 + +o = s:option(Flag, option_name("log"), translate("Log")) +o.default = "1" +o.rmempty = false + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua new file mode 100644 index 000000000..bf11e18ec --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua @@ -0,0 +1,410 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.finded_com("xray") then + return +end + +local type_name = "Xray" + +local option_prefix = "xray_" + +local function option_name(name) + return option_prefix .. name +end + +local x_ss_method_list = { + "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" +} + +local header_type_list = { + "none", "srtp", "utp", "wechat-video", "dtls", "wireguard" +} + +-- [[ Xray ]] + +s.fields["type"]:value(type_name, "Xray") + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +o:value("vmess", "Vmess") +o:value("vless", "VLESS") +o:value("http", "HTTP") +o:value("socks", "Socks") +o:value("shadowsocks", "Shadowsocks") +o:value("trojan", "Trojan") +o:value("dokodemo-door", "dokodemo-door") + +o = s:option(Value, option_name("port"), translate("Listen Port")) +o.datatype = "port" + +o = s:option(Flag, option_name("auth"), translate("Auth")) +o.validate = function(self, value, t) + if value and value == "1" then + local user_v = s.fields[option_name("username")] and s.fields[option_name("username")]:formvalue(t) or "" + local pass_v = s.fields[option_name("password")] and s.fields[option_name("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 +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "http" }) + +o = s:option(Value, option_name("username"), translate("Username")) +o:depends({ [option_name("auth")] = true }) + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true +o:depends({ [option_name("auth")] = true }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(ListValue, option_name("d_protocol"), translate("Destination protocol")) +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("tcp,udp", "TCP,UDP") +o:depends({ [option_name("protocol")] = "dokodemo-door" }) + +o = s:option(Value, option_name("d_address"), translate("Destination address")) +o:depends({ [option_name("protocol")] = "dokodemo-door" }) + +o = s:option(Value, option_name("d_port"), translate("Destination port")) +o.datatype = "port" +o:depends({ [option_name("protocol")] = "dokodemo-door" }) + +o = s:option(Value, option_name("decryption"), translate("Encrypt Method")) +o.default = "none" +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(ListValue, option_name("x_ss_method"), translate("Encrypt Method")) +o.rewrite_option = "method" +for a, t in ipairs(x_ss_method_list) do o:value(t) end +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(Flag, option_name("iv_check"), translate("IV Check")) +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(ListValue, option_name("ss_network"), translate("Transport")) +o.default = "tcp,udp" +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("tcp,udp", "TCP,UDP") +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(Flag, option_name("udp_forward"), translate("UDP Forward")) +o.default = "1" +o.rmempty = false +o:depends({ [option_name("protocol")] = "socks" }) + +o = s:option(DynamicList, option_name("uuid"), translate("ID") .. "/" .. translate("Password")) +for i = 1, 3 do + o:value(api.gen_uuid(1)) +end +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +o = s:option(ListValue, option_name("flow"), translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(Flag, option_name("tls"), translate("TLS")) +o.default = 0 +o.validate = function(self, value, t) + if value then + local reality = s.fields[option_name("reality")] and s.fields[option_name("reality")]:formvalue(t) or nil + if reality and reality == "1" then return value end + if value == "1" then + local ca = s.fields[option_name("tls_certificateFile")] and s.fields[option_name("tls_certificateFile")]:formvalue(t) or "" + local key = s.fields[option_name("tls_keyFile")] and s.fields[option_name("tls_keyFile")]:formvalue(t) or "" + if ca == "" or key == "" then + return nil, translate("Public key and Private key path can not be empty!") + end + end + return value + end +end +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +-- [[ REALITY部分 ]] -- +o = s:option(Flag, option_name("reality"), translate("REALITY")) +o.default = 0 +o:depends({ [option_name("tls")] = true }) + +o = s:option(Value, option_name("reality_private_key"), translate("Private Key")) +o:depends({ [option_name("reality")] = true }) + +o = s:option(DynamicList, option_name("reality_shortId"), translate("Short Id")) +o:depends({ [option_name("reality")] = true }) + +o = s:option(Value, option_name("reality_dest"), translate("Dest")) +o.default = "google.com:443" +o:depends({ [option_name("reality")] = true }) + +o = s:option(Value, option_name("reality_serverNames"), translate("serverNames")) +o:depends({ [option_name("reality")] = true }) + +o = s:option(ListValue, option_name("alpn"), translate("alpn")) +o.default = "h2,http/1.1" +o:value("h3,h2,http/1.1") +o:value("h3,h2") +o:value("h2,http/1.1") +o:value("h3") +o:value("h2") +o:value("http/1.1") +o:depends({ [option_name("tls")] = true }) + +-- o = s:option(Value, option_name("minversion"), translate("minversion")) +-- o.default = "1.3" +-- o:value("1.3") +--o:depends({ [option_name("tls")] = true }) + +-- [[ TLS部分 ]] -- +o = s:option(FileUpload, option_name("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" +o:depends({ [option_name("tls")] = true, [option_name("reality")] = false }) +o.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 + +o = s:option(FileUpload, option_name("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" +o:depends({ [option_name("tls")] = true, [option_name("reality")] = false }) +o.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 + +o = s:option(ListValue, option_name("transport"), translate("Transport")) +o:value("tcp", "TCP") +o:value("mkcp", "mKCP") +o:value("ws", "WebSocket") +o:value("h2", "HTTP/2") +o:value("ds", "DomainSocket") +o:value("quic", "QUIC") +o:value("grpc", "gRPC") +o:value("httpupgrade", "HttpUpgrade") +o:value("splithttp", "SplitHTTP") +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +-- [[ WebSocket部分 ]]-- +o = s:option(Value, option_name("ws_host"), translate("WebSocket Host")) +o:depends({ [option_name("transport")] = "ws" }) + +o = s:option(Value, option_name("ws_path"), translate("WebSocket Path")) +o:depends({ [option_name("transport")] = "ws" }) + +-- [[ HttpUpgrade部分 ]]-- +o = s:option(Value, option_name("httpupgrade_host"), translate("HttpUpgrade Host")) +o:depends({ [option_name("transport")] = "httpupgrade" }) + +o = s:option(Value, option_name("httpupgrade_path"), translate("HttpUpgrade Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "httpupgrade" }) + +-- [[ HTTP/2部分 ]]-- +o = s:option(Value, option_name("h2_host"), translate("HTTP/2 Host")) +o:depends({ [option_name("transport")] = "h2" }) + +o = s:option(Value, option_name("h2_path"), translate("HTTP/2 Path")) +o:depends({ [option_name("transport")] = "h2" }) + +-- [[ SplitHTTP部分 ]]-- +o = s:option(Value, option_name("splithttp_host"), translate("SplitHTTP Host")) +o:depends({ [option_name("transport")] = "splithttp" }) + +o = s:option(Value, option_name("splithttp_path"), translate("SplitHTTP Path")) +o.placeholder = "/" +o:depends({ [option_name("transport")] = "splithttp" }) + +o = s:option(Value, option_name("splithttp_maxuploadsize"), translate("maxUploadSize")) +o.default = "1000000" +o:depends({ [option_name("transport")] = "splithttp" }) + +o = s:option(Value, option_name("splithttp_maxconcurrentuploads"), translate("maxConcurrentUploads")) +o.default = "10" +o:depends({ [option_name("transport")] = "splithttp" }) + +-- [[ TCP部分 ]]-- + +-- TCP伪装 +o = s:option(ListValue, option_name("tcp_guise"), translate("Camouflage Type")) +o:value("none", "none") +o:value("http", "http") +o:depends({ [option_name("transport")] = "tcp" }) + +-- HTTP域名 +o = s:option(DynamicList, option_name("tcp_guise_http_host"), translate("HTTP Host")) +o:depends({ [option_name("tcp_guise")] = "http" }) + +-- HTTP路径 +o = s:option(DynamicList, option_name("tcp_guise_http_path"), translate("HTTP Path")) +o:depends({ [option_name("tcp_guise")] = "http" }) + +-- [[ mKCP部分 ]]-- +o = s:option(ListValue, option_name("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 o:value(t) end +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_mtu"), translate("KCP MTU")) +o.default = "1350" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_tti"), translate("KCP TTI")) +o.default = "20" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity")) +o.default = "5" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity")) +o.default = "20" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Flag, option_name("mkcp_congestion"), translate("KCP Congestion")) +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_readBufferSize"), translate("KCP readBufferSize")) +o.default = "1" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_writeBufferSize"), translate("KCP writeBufferSize")) +o.default = "1" +o:depends({ [option_name("transport")] = "mkcp" }) + +o = s:option(Value, option_name("mkcp_seed"), translate("KCP Seed")) +o:depends({ [option_name("transport")] = "mkcp" }) + +-- [[ DomainSocket部分 ]]-- +o = s:option(Value, option_name("ds_path"), "Path", translate("A legal file path. This file must not exist before running.")) +o:depends({ [option_name("transport")] = "ds" }) + +-- [[ QUIC部分 ]]-- +o = s:option(ListValue, option_name("quic_security"), translate("Encrypt Method")) +o:value("none") +o:value("aes-128-gcm") +o:value("chacha20-poly1305") +o:depends({ [option_name("transport")] = "quic" }) + +o = s:option(Value, option_name("quic_key"), translate("Encrypt Method") .. translate("Key")) +o:depends({ [option_name("transport")] = "quic" }) + +o = s:option(ListValue, option_name("quic_guise"), translate("Camouflage Type")) +for a, t in ipairs(header_type_list) do o:value(t) end +o:depends({ [option_name("transport")] = "quic" }) + +-- [[ gRPC部分 ]]-- +o = s:option(Value, option_name("grpc_serviceName"), "ServiceName") +o:depends({ [option_name("transport")] = "grpc" }) + +o = s:option(Flag, option_name("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) +o.default = "0" + +-- [[ Fallback部分 ]]-- +o = s:option(Flag, option_name("fallback"), translate("Fallback")) +o:depends({ [option_name("protocol")] = "vless", [option_name("transport")] = "tcp" }) +o:depends({ [option_name("protocol")] = "trojan", [option_name("transport")] = "tcp" }) + +--[[ +o = s:option(Value, option_name("fallback_alpn"), "Fallback alpn") +o:depends({ [option_name("fallback")] = true }) + +o = s:option(Value, option_name("fallback_path"), "Fallback path") +o:depends({ [option_name("fallback")] = true }) + +o = s:option(Value, option_name("fallback_dest"), "Fallback dest") +o:depends({ [option_name("fallback")] = true }) + +o = s:option(Value, option_name("fallback_xver"), "Fallback xver") +o.default = 0 +o:depends({ [option_name("fallback")] = true }) +]]-- + +o = s:option(DynamicList, option_name("fallback_list"), "Fallback", translate("dest,path")) +o:depends({ [option_name("fallback")] = true }) + +o = s:option(Flag, option_name("bind_local"), translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) +o.default = "0" + +o = s:option(Flag, option_name("accept_lan"), translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +o.default = "0" + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" and e.type == type_name then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remarks = e["remark"] + } + end +end + +o = s:option(ListValue, option_name("outbound_node"), translate("outbound node")) +o:value("nil", translate("Close")) +o:value("_socks", translate("Custom Socks")) +o:value("_http", translate("Custom HTTP")) +o:value("_iface", translate("Custom Interface")) +for k, v in pairs(nodes_table) do o:value(v.id, v.remarks) end +o.default = "nil" + +o = s:option(Value, option_name("outbound_node_address"), translate("Address (Support Domain Name)")) +o:depends({ [option_name("outbound_node")] = "_socks"}) +o:depends({ [option_name("outbound_node")] = "_http"}) + +o = s:option(Value, option_name("outbound_node_port"), translate("Port")) +o.datatype = "port" +o:depends({ [option_name("outbound_node")] = "_socks"}) +o:depends({ [option_name("outbound_node")] = "_http"}) + +o = s:option(Value, option_name("outbound_node_username"), translate("Username")) +o:depends({ [option_name("outbound_node")] = "_socks"}) +o:depends({ [option_name("outbound_node")] = "_http"}) + +o = s:option(Value, option_name("outbound_node_password"), translate("Password")) +o.password = true +o:depends({ [option_name("outbound_node")] = "_socks"}) +o:depends({ [option_name("outbound_node")] = "_http"}) + +o = s:option(Value, option_name("outbound_node_iface"), translate("Interface")) +o.default = "eth1" +o:depends({ [option_name("outbound_node")] = "_iface"}) + +o = s:option(Flag, option_name("log"), translate("Log")) +o.default = "1" +o.rmempty = false + +o = s:option(ListValue, option_name("loglevel"), translate("Log Level")) +o.default = "warning" +o:value("debug") +o:value("info") +o:value("warning") +o:value("error") +o:depends({ [option_name("log")] = true }) + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua new file mode 100644 index 000000000..2169a99c8 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua @@ -0,0 +1,426 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +local singbox_bin = api.finded_com("singbox") + +if not singbox_bin then + return +end + +local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'") + +local type_name = "sing-box" + +local option_prefix = "singbox_" + +local function option_name(name) + return option_prefix .. name +end + +local ss_method_list = { + "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", + "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" +} + +-- [[ Sing-Box ]] + +s.fields["type"]:value(type_name, "Sing-Box") + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +o:value("mixed", "Mixed") +o:value("socks", "Socks") +o:value("http", "HTTP") +o:value("shadowsocks", "Shadowsocks") +o:value("vmess", "Vmess") +o:value("vless", "VLESS") +o:value("trojan", "Trojan") +o:value("naive", "Naive") +if singbox_tags:find("with_quic") then + o:value("hysteria", "Hysteria") +end +if singbox_tags:find("with_quic") then + o:value("tuic", "TUIC") +end +if singbox_tags:find("with_quic") then + o:value("hysteria2", "Hysteria2") +end +o:value("direct", "Direct") + +o = s:option(Value, option_name("port"), translate("Listen Port")) +o.datatype = "port" + +o = s:option(Flag, option_name("auth"), translate("Auth")) +o.validate = function(self, value, t) + if value and value == "1" then + local user_v = s.fields[option_name("username")] and s.fields[option_name("username")]:formvalue(t) or "" + local pass_v = s.fields[option_name("password")] and s.fields[option_name("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 +o:depends({ [option_name("protocol")] = "mixed" }) +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "http" }) + +o = s:option(Value, option_name("username"), translate("Username")) +o:depends({ [option_name("auth")] = true }) +o:depends({ [option_name("protocol")] = "naive" }) + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true +o:depends({ [option_name("auth")] = true }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "naive" }) +o:depends({ [option_name("protocol")] = "tuic" }) + +if singbox_tags:find("with_quic") then + o = s:option(Value, option_name("hysteria_up_mbps"), translate("Max upload Mbps")) + o.default = "100" + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_down_mbps"), translate("Max download Mbps")) + o.default = "100" + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_obfs"), translate("Obfs Password")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(ListValue, option_name("hysteria_auth_type"), translate("Auth Type")) + o:value("disable", translate("Disable")) + o:value("string", translate("STRING")) + o:value("base64", translate("BASE64")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_auth_password"), translate("Auth Password")) + o.password = true + o:depends({ [option_name("protocol")] = "hysteria", [option_name("hysteria_auth_type")] = "string"}) + o:depends({ [option_name("protocol")] = "hysteria", [option_name("hysteria_auth_type")] = "base64"}) + + o = s:option(Value, option_name("hysteria_recv_window_conn"), translate("QUIC stream receive window")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_recv_window_client"), translate("QUIC connection receive window")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_max_conn_client"), translate("QUIC concurrent bidirectional streams")) + o.default = "1024" + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Flag, option_name("hysteria_disable_mtu_discovery"), translate("Disable MTU detection")) + o:depends({ [option_name("protocol")] = "hysteria" }) + + o = s:option(Value, option_name("hysteria_alpn"), translate("QUIC TLS ALPN")) + o:depends({ [option_name("protocol")] = "hysteria" }) +end + +if singbox_tags:find("with_quic") then + o = s:option(ListValue, option_name("tuic_congestion_control"), translate("Congestion control algorithm")) + o.default = "cubic" + o:value("bbr", translate("BBR")) + o:value("cubic", translate("CUBIC")) + o:value("new_reno", translate("New Reno")) + o:depends({ [option_name("protocol")] = "tuic" }) + + o = s:option(Flag, option_name("tuic_zero_rtt_handshake"), translate("Enable 0-RTT QUIC handshake")) + o.default = 0 + o:depends({ [option_name("protocol")] = "tuic" }) + + o = s:option(Value, option_name("tuic_heartbeat"), translate("Heartbeat interval(second)")) + o.datatype = "uinteger" + o.default = "3" + o:depends({ [option_name("protocol")] = "tuic" }) + + o = s:option(Value, option_name("tuic_alpn"), translate("QUIC TLS ALPN")) + o:depends({ [option_name("protocol")] = "tuic" }) +end + +if singbox_tags:find("with_quic") then + o = s:option(Flag, option_name("hysteria2_ignore_client_bandwidth"), translate("Commands the client to use the BBR flow control algorithm")) + o.default = 0 + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(Value, option_name("hysteria2_up_mbps"), translate("Max upload Mbps")) + o:depends({ [option_name("protocol")] = "hysteria2", [option_name("hysteria2_ignore_client_bandwidth")] = false }) + + o = s:option(Value, option_name("hysteria2_down_mbps"), translate("Max download Mbps")) + o:depends({ [option_name("protocol")] = "hysteria2", [option_name("hysteria2_ignore_client_bandwidth")] = false }) + + o = s:option(ListValue, option_name("hysteria2_obfs_type"), translate("Obfs Type")) + o:value("", translate("Disable")) + o:value("salamander") + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(Value, option_name("hysteria2_obfs_password"), translate("Obfs Password")) + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(Value, option_name("hysteria2_auth_password"), translate("Auth Password")) + o.password = true + o:depends({ [option_name("protocol")] = "hysteria2"}) +end + +o = s:option(ListValue, option_name("d_protocol"), translate("Destination protocol")) +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("tcp,udp", "TCP,UDP") +o:depends({ [option_name("protocol")] = "direct" }) + +o = s:option(Value, option_name("d_address"), translate("Destination address")) +o:depends({ [option_name("protocol")] = "direct" }) + +o = s:option(Value, option_name("d_port"), translate("Destination port")) +o.datatype = "port" +o:depends({ [option_name("protocol")] = "direct" }) + +o = s:option(Value, option_name("decryption"), translate("Encrypt Method")) +o.default = "none" +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(ListValue, option_name("ss_method"), translate("Encrypt Method")) +o.rewrite_option = "method" +for a, t in ipairs(ss_method_list) do o:value(t) end +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(DynamicList, option_name("uuid"), translate("ID") .. "/" .. translate("Password")) +for i = 1, 3 do + o:value(api.gen_uuid(1)) +end +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "trojan" }) +o:depends({ [option_name("protocol")] = "tuic" }) + +o = s:option(ListValue, option_name("flow"), translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(Flag, option_name("tls"), translate("TLS")) +o.default = 0 +o.validate = function(self, value, t) + if value then + local reality = s.fields[option_name("reality")] and s.fields[option_name("reality")]:formvalue(t) or nil + if reality and reality == "1" then return value end + if value == "1" then + local ca = s.fields[option_name("tls_certificateFile")] and s.fields[option_name("tls_certificateFile")]:formvalue(t) or "" + local key = s.fields[option_name("tls_keyFile")] and s.fields[option_name("tls_keyFile")]:formvalue(t) or "" + if ca == "" or key == "" then + return nil, translate("Public key and Private key path can not be empty!") + end + end + return value + end +end +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +if singbox_tags:find("with_reality_server") then + -- [[ REALITY部分 ]] -- + o = s:option(Flag, option_name("reality"), translate("REALITY")) + o.default = 0 + o:depends({ [option_name("protocol")] = "http", [option_name("tls")] = true }) + o:depends({ [option_name("protocol")] = "vmess", [option_name("tls")] = true }) + o:depends({ [option_name("protocol")] = "vless", [option_name("tls")] = true }) + o:depends({ [option_name("protocol")] = "trojan", [option_name("tls")] = true }) + + o = s:option(Value, option_name("reality_private_key"), translate("Private Key")) + o:depends({ [option_name("reality")] = true }) + + o = s:option(Value, option_name("reality_shortId"), translate("Short Id")) + o:depends({ [option_name("reality")] = true }) + + o = s:option(Value, option_name("reality_handshake_server"), translate("Handshake Server")) + o.default = "google.com" + o:depends({ [option_name("reality")] = true }) + + o = s:option(Value, option_name("reality_handshake_server_port"), translate("Handshake Server Port")) + o.datatype = "port" + o.default = "443" + o:depends({ [option_name("reality")] = true }) +end + +-- [[ TLS部分 ]] -- + +o = s:option(FileUpload, option_name("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" +o:depends({ [option_name("tls")] = true, [option_name("reality")] = false }) +o:depends({ [option_name("protocol")] = "naive" }) +o:depends({ [option_name("protocol")] = "hysteria" }) +o:depends({ [option_name("protocol")] = "tuic" }) +o:depends({ [option_name("protocol")] = "hysteria2" }) +o.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 + +o = s:option(FileUpload, option_name("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" +o:depends({ [option_name("tls")] = true, [option_name("reality")] = false }) +o:depends({ [option_name("protocol")] = "naive" }) +o:depends({ [option_name("protocol")] = "hysteria" }) +o:depends({ [option_name("protocol")] = "tuic" }) +o:depends({ [option_name("protocol")] = "hysteria2" }) +o.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 + +if singbox_tags:find("with_ech") then + o = s:option(Flag, option_name("ech"), translate("ECH")) + o.default = "0" + o:depends({ [option_name("tls")] = true, [option_name("flow")] = "", [option_name("reality")] = false }) + o:depends({ [option_name("protocol")] = "naive" }) + o:depends({ [option_name("protocol")] = "hysteria" }) + o:depends({ [option_name("protocol")] = "tuic" }) + o:depends({ [option_name("protocol")] = "hysteria2" }) + + o = s:option(Value, option_name("ech_key"), translate("ECH Key")) + o.default = "" + o:depends({ [option_name("ech")] = true }) + + o = s:option(Flag, option_name("pq_signature_schemes_enabled"), translate("PQ signature schemes")) + o.default = "0" + o:depends({ [option_name("ech")] = true }) + + o = s:option(Flag, option_name("dynamic_record_sizing_disabled"), translate("Disable adaptive sizing of TLS records")) + o.default = "0" + o:depends({ [option_name("ech")] = true }) +end + +o = s:option(ListValue, option_name("transport"), translate("Transport")) +o:value("tcp", "TCP") +o:value("http", "HTTP") +o:value("ws", "WebSocket") +o:value("httpupgrade", "HTTPUpgrade") +o:value("quic", "QUIC") +o:value("grpc", "gRPC") +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +-- [[ HTTP部分 ]]-- + +o = s:option(Value, option_name("http_host"), translate("HTTP Host")) +o:depends({ [option_name("transport")] = "http" }) + +o = s:option(Value, option_name("http_path"), translate("HTTP Path")) +o:depends({ [option_name("transport")] = "http" }) + +-- [[ WebSocket部分 ]]-- + +o = s:option(Value, option_name("ws_host"), translate("WebSocket Host")) +o:depends({ [option_name("transport")] = "ws" }) + +o = s:option(Value, option_name("ws_path"), translate("WebSocket Path")) +o:depends({ [option_name("transport")] = "ws" }) + +-- [[ HTTPUpgrade部分 ]]-- + +o = s:option(Value, option_name("httpupgrade_host"), translate("HTTPUpgrade Host")) +o:depends({ [option_name("transport")] = "httpupgrade" }) + +o = s:option(Value, option_name("httpupgrade_path"), translate("HTTPUpgrade Path")) +o:depends({ [option_name("transport")] = "httpupgrade" }) + +-- [[ gRPC部分 ]]-- +o = s:option(Value, option_name("grpc_serviceName"), "ServiceName") +o:depends({ [option_name("transport")] = "grpc" }) + +-- [[ Mux ]]-- +o = s:option(Flag, option_name("mux"), translate("Mux")) +o.rmempty = false +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless", [option_name("flow")] = "" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +-- [[ TCP Brutal ]]-- +o = s:option(Flag, option_name("tcpbrutal"), translate("TCP Brutal")) +o.default = 0 +o:depends({ [option_name("mux")] = true }) + +o = s:option(Value, option_name("tcpbrutal_up_mbps"), translate("Max upload Mbps")) +o.default = "10" +o:depends({ [option_name("tcpbrutal")] = true }) + +o = s:option(Value, option_name("tcpbrutal_down_mbps"), translate("Max download Mbps")) +o.default = "50" +o:depends({ [option_name("tcpbrutal")] = true }) + +o = s:option(Flag, option_name("bind_local"), translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) +o.default = "0" + +o = s:option(Flag, option_name("accept_lan"), translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +o.default = "0" + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" and e.type == type_name then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remarks = e["remark"] + } + end +end + +o = s:option(ListValue, option_name("outbound_node"), translate("outbound node")) +o:value("nil", translate("Close")) +o:value("_socks", translate("Custom Socks")) +o:value("_http", translate("Custom HTTP")) +o:value("_iface", translate("Custom Interface")) +for k, v in pairs(nodes_table) do o:value(v.id, v.remarks) end +o.default = "nil" + +o = s:option(Value, option_name("outbound_node_address"), translate("Address (Support Domain Name)")) +o:depends({ [option_name("outbound_node")] = "_socks" }) +o:depends({ [option_name("outbound_node")] = "_http" }) + +o = s:option(Value, option_name("outbound_node_port"), translate("Port")) +o.datatype = "port" +o:depends({ [option_name("outbound_node")] = "_socks" }) +o:depends({ [option_name("outbound_node")] = "_http" }) + +o = s:option(Value, option_name("outbound_node_username"), translate("Username")) +o:depends({ [option_name("outbound_node")] = "_socks" }) +o:depends({ [option_name("outbound_node")] = "_http" }) + +o = s:option(Value, option_name("outbound_node_password"), translate("Password")) +o.password = true +o:depends({ [option_name("outbound_node")] = "_socks" }) +o:depends({ [option_name("outbound_node")] = "_http" }) + +o = s:option(Value, option_name("outbound_node_iface"), translate("Interface")) +o.default = "eth1" +o:depends({ [option_name("outbound_node")] = "_iface" }) + +o = s:option(Flag, option_name("log"), translate("Log")) +o.default = "1" +o.rmempty = false + +o = s:option(ListValue, option_name("loglevel"), translate("Log Level")) +o.default = "info" +o:value("debug") +o:value("info") +o:value("warn") +o:value("error") +o:depends({ [option_name("log")] = true }) + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua new file mode 100644 index 000000000..cec758aa6 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua @@ -0,0 +1,47 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.is_finded("ssserver") then + return +end + +local type_name = "SS-Rust" + +local option_prefix = "ssrust_" + +local function option_name(name) + return option_prefix .. name +end + +local ssrust_encrypt_method_list = { + "plain", "none", + "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", + "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305" +} + +-- [[ Shadowsocks Rust ]] + +s.fields["type"]:value(type_name, translate("Shadowsocks Rust")) + +o = s:option(Value, option_name("port"), translate("Listen Port")) +o.datatype = "port" + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true + +o = s:option(ListValue, option_name("method"), translate("Encrypt Method")) +for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end + +o = s:option(Value, option_name("timeout"), translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 + +o = s:option(Flag, option_name("tcp_fast_open"), "TCP " .. translate("Fast Open")) +o.default = "0" + +o = s:option(Flag, option_name("log"), translate("Log")) +o.default = "1" +o.rmempty = false + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss.lua new file mode 100644 index 000000000..7d2ce351b --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss.lua @@ -0,0 +1,50 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.is_finded("ss-server") then + return +end + +local type_name = "SS" + +local option_prefix = "ss_" + +local function option_name(name) + return option_prefix .. name +end + +local ss_encrypt_method_list = { + "rc4-md5", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", + "aes-192-ctr", "aes-256-ctr", "bf-cfb", "camellia-128-cfb", + "camellia-192-cfb", "camellia-256-cfb", "salsa20", "chacha20", + "chacha20-ietf", -- aead + "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", + "xchacha20-ietf-poly1305" +} + +-- [[ Shadowsocks ]] + +s.fields["type"]:value(type_name, translate("Shadowsocks")) + +o = s:option(Value, option_name("port"), translate("Listen Port")) +o.datatype = "port" + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true + +o = s:option(ListValue, option_name("method"), translate("Encrypt Method")) +for a, t in ipairs(ss_encrypt_method_list) do o:value(t) end + +o = s:option(Value, option_name("timeout"), translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 + +o = s:option(Flag, option_name("tcp_fast_open"), "TCP " .. translate("Fast Open")) +o.default = "0" + +o = s:option(Flag, option_name("log"), translate("Log")) +o.default = "1" +o.rmempty = false + +api.luci_types(arg[1], m, s, type_name, option_prefix) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua new file mode 100644 index 000000000..e87e19fee --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua @@ -0,0 +1,74 @@ +local m, s = ... + +local api = require "luci.passwall2.api" + +if not api.is_finded("ssr-server") then + return +end + +local type_name = "SSR" + +local option_prefix = "ssr_" + +local function option_name(name) + return option_prefix .. name +end + +local ssr_encrypt_method_list = { + "none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb", + "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", + "bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", + "cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20", + "chacha20-ietf" +} + +local ssr_protocol_list = { + "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", + "auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5", + "auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c", + "auth_chain_d", "auth_chain_e", "auth_chain_f" +} +local ssr_obfs_list = { + "plain", "http_simple", "http_post", "random_head", "tls_simple", + "tls1.0_session_auth", "tls1.2_ticket_auth" +} + +-- [[ ShadowsocksR ]] + +s.fields["type"]:value(type_name, translate("ShadowsocksR")) + +o = s:option(Value, option_name("port"), translate("Listen Port")) +o.datatype = "port" + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true + +o = s:option(ListValue, option_name("method"), translate("Encrypt Method")) +for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +for a, t in ipairs(ssr_protocol_list) do o:value(t) end + +o = s:option(Value, option_name("protocol_param"), translate("Protocol_param")) + +o = s:option(ListValue, option_name("obfs"), translate("Obfs")) +for a, t in ipairs(ssr_obfs_list) do o:value(t) end + +o = s:option(Value, option_name("obfs_param"), translate("Obfs_param")) + +o = s:option(Value, option_name("timeout"), translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 + +o = s:option(Flag, option_name("tcp_fast_open"), "TCP " .. translate("Fast Open")) +o.default = "0" + +o = s:option(Flag, option_name("udp_forward"), translate("UDP Forward")) +o.default = "1" +o.rmempty = false + +o = s:option(Flag, option_name("log"), translate("Log")) +o.default = "1" +o.rmempty = false + +api.luci_types(arg[1], m, s, type_name, option_prefix) 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..47d6d4700 --- /dev/null +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/user.lua @@ -0,0 +1,34 @@ +local api = require "luci.passwall2.api" +local fs = require "nixio.fs" +local types_dir = "/usr/lib/lua/luci/model/cbi/passwall2/server/type/" + +m = Map("passwall2_server", translate("Server Config")) +m.redirect = api.url("server") +api.set_apply_on_parse(m) + +s = m:section(NamedSection, arg[1], "user", "") +s.addremove = false +s.dynamic = false + +o = s:option(Flag, "enable", translate("Enable")) +o.default = "1" +o.rmempty = false + +o = s:option(Value, "remarks", translate("Remarks")) +o.default = translate("Remarks") +o.rmempty = false + +o = s:option(ListValue, "type", translate("Type")) + +local type_table = {} +for filename in fs.dir(types_dir) do + table.insert(type_table, filename) +end +table.sort(type_table) + +for index, value in ipairs(type_table) do + local p_func = loadfile(types_dir .. value) + setfenv(p_func, getfenv(1))(m, s) +end + +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..e50bd5ea0 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -0,0 +1,1047 @@ +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" } +command_timeout = 300 +OPENWRT_ARCH = nil +DISTRIB_ARCH = nil + +LOG_FILE = "/tmp/log/passwall2.log" +CACHE_PATH = "/tmp/etc/passwall2_tmp" + +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/acl/default/SOCKS_server ] && echo -n $(cat /tmp/etc/passwall2/acl/default/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 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 = string.sub(full, off, string.len(full)) + if #res > 0 then -- 过滤掉 \0 + table.insert(result, res) + end + break + else + table.insert(result, string.sub(full, off, nStart - 1)) + off = nEnd + 1 + end + end + return result + end + return {} +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 show_node_info = uci_get_type("global_other", "show_node_info") or "0" + 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] " % {e.type .. " " .. i18n.translatef(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 == "sing-box" 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 show_node_info == "1" 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] " % {n.type .. " " .. i18n.translatef(n.protocol), n.remarks} + else + local type2 = n.type + if (n.type == "sing-box" 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 finded_com(e) + local bin = get_app_path(e) + if not bin then return end + local s = luci.sys.exec('echo -n $(type -t -p "%s" | head -n1)' % { bin }) + if s == "" then + s = nil + end + return s +end + +function finded(e) + return luci.sys.exec('echo -n $(type -t -p "/bin/%s" -p "/usr/bin/%s" "%s" | head -n1)' % {e, e, e}) +end + +function is_finded(e) + return finded(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) + if com[app_name] then + 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 +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 + if com[app_name].remote_version_str_replace then + remote_version = remote_version:gsub(com[app_name].remote_version_str_replace, "") + end + 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 _curl_args = clone(curl_args) + table.insert(_curl_args, "-m 60") + + 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 + + local tools_name + if com[app_name].zipped then + if not com[app_name].zipped_suffix or com[app_name].zipped_suffix == "zip" then + tools_name = "unzip" + end + if com[app_name].zipped_suffix and com[app_name].zipped_suffix == "tar.gz" then + tools_name = "tar" + end + if tools_name then + if sys.exec("echo -n $(command -v %s)" % { tools_name }) == "" then + exec("/bin/rm", {"-f", file}) + return { + code = 1, + error = i18n.translate("Not installed %s, Can't unzip!" % { tools_name }) + } + end + end + 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 = {} + + if tools_name then + if tools_name == "unzip" then + local bin = sys.exec("echo -n $(command -v unzip)") + exec(bin, {"-o", file, app_name, "-d", tmp_dir}, function(chunk) output[#output + 1] = chunk end) + elseif tools_name == "tar" then + local bin = sys.exec("echo -n $(command -v tar)") + if com[app_name].zipped_suffix == "tar.gz" then + exec(bin, {"-zxf", file, "-C", tmp_dir}, function(chunk) output[#output + 1] = chunk end) + sys.call("/bin/mv -f " .. tmp_dir .. "/*/" .. com[app_name].name:lower() .. " " .. tmp_dir) + end + end + 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 .. "/" .. com[app_name].name:lower() + 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 + +function cacheFileCompareToLogic(file, str) + local result = nil + if file and str then + local file_str = "" + if fs.access(file) then + file_str = sys.exec("cat " .. file) + end + + if file_str ~= str then + sys.call("rm -f " .. file) + result = false + else + result = true + end + + local f_out = io.open(file, "w") + if f_out then + f_out:write(str) + f_out:close() + end + end + return result +end + +function is_js_luci() + return sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 +end + +function set_apply_on_parse(map) + if is_js_luci() == true then + map.apply_on_parse = false + map.on_after_apply = function(self) + if self.redirect then + os.execute("sleep 1") + luci.http.redirect(self.redirect) + end + end + end +end + +function luci_types(id, m, s, type_name, option_prefix) + local rewrite_option_table = {} + for key, value in pairs(s.fields) do + if key:find(option_prefix) == 1 then + if not s.fields[key].not_rewrite then + if s.fields[key].rewrite_option then + if not rewrite_option_table[s.fields[key].rewrite_option] then + rewrite_option_table[s.fields[key].rewrite_option] = 1 + else + rewrite_option_table[s.fields[key].rewrite_option] = rewrite_option_table[s.fields[key].rewrite_option] + 1 + end + end + + s.fields[key].cfgvalue = function(self, section) + if self.rewrite_option then + return m:get(section, self.rewrite_option) + else + if self.option:find(option_prefix) == 1 then + return m:get(section, self.option:sub(1 + #option_prefix)) + end + end + end + s.fields[key].write = function(self, section, value) + if s.fields["type"]:formvalue(id) == type_name then + if self.rewrite_option then + m:set(section, self.rewrite_option, value) + else + if self.option:find(option_prefix) == 1 then + m:set(section, self.option:sub(1 + #option_prefix), value) + end + end + end + end + s.fields[key].remove = function(self, section) + if s.fields["type"]:formvalue(id) == type_name then + if self.rewrite_option and rewrite_option_table[self.rewrite_option] == 1 then + m:del(section, self.rewrite_option) + else + if self.option:find(option_prefix) == 1 then + m:del(section, self.option:sub(1 + #option_prefix)) + end + end + end + end + end + + local deps = s.fields[key].deps + if #deps > 0 then + for index, value in ipairs(deps) do + deps[index]["type"] = type_name + end + else + s.fields[key]:depends({ type = type_name }) + end + end + end +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..f3e24e0d1 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/com.lua @@ -0,0 +1,56 @@ +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.hysteria = { + name = "Hysteria", + repo = "HyNetwork/hysteria", + get_url = gh_release_url, + cmd_version = "version | awk '/^Version:/ {print $2}'", + remote_version_str_replace = "app/", + zipped = false, + default_path = "/usr/bin/hysteria", + match_fmt_str = "linux%%-%s$", + file_tree = { + armv6 = "arm", + armv7 = "arm" + } +} + +_M.singbox = { + name = "Sing-Box", + repo = "SagerNet/sing-box", + get_url = gh_release_url, + cmd_version = "version | awk '{print $3}' | sed -n 1P", + zipped = true, + zipped_suffix = "tar.gz", + default_path = "/usr/bin/sing-box", + match_fmt_str = "linux%%-%s", + file_tree = { + x86_64 = "amd64" + } +} + +_M.xray = { + name = "Xray", + repo = "XTLS/Xray-core", + get_url = gh_pre_release_url, + cmd_version = "version | awk '{print $2}' | sed -n 1P", + zipped = true, + default_path = "/usr/bin/xray", + match_fmt_str = "linux%%-%s", + file_tree = { + x86_64 = "64", + x86 = "32", + mips = "mips32", + mipsel = "mips32le" + } +} + +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..e9b905cd7 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/server_app.lua @@ -0,0 +1,221 @@ +#!/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 == "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 == "sing-box" then + config = require(require_dir .. "util_sing-box").gen_config_server(user) + bin = ln_run(api.get_app_path("singbox"), "sing-box", "run -c " .. config_file, log_path) + elseif type == "Hysteria2" then + config = require(require_dir .. "util_hysteria2").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("/bin/busybox 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_hysteria2.lua b/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua new file mode 100644 index 000000000..8d069ca6c --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua @@ -0,0 +1,115 @@ +module("luci.passwall2.util_hysteria2", 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, + tls = { + cert = node.tls_certificateFile, + key = node.tls_keyFile, + }, + obfs = (node.hysteria2_obfs) and { + type = "salamander", + salamander = { + password = node.hysteria2_obfs + } + } or nil, + auth = { + type = "password", + password = node.hysteria2_auth_password + }, + bandwidth = (node.hysteria2_up_mbps or node.hysteria2_down_mbps) and { + up = node.hysteria2_up_mbps and node.hysteria2_up_mbps .. " mbps" or nil, + down = node.hysteria2_down_mbps and node.hysteria2_down_mbps .. " mbps" or nil + } or nil, + ignoreClientBandwidth = (node.hysteria2_ignoreClientBandwidth == "1") and true or false, + disableUDP = (node.hysteria2_udp == "0") 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.hysteria2_hop) then + server = server .. "," .. node.hysteria2_hop + end + + local config = { + server = server, + transport = { + type = node.protocol or "udp", + udp = { + hopInterval = node.hysteria2_hop_interval and node.hysteria2_hop_interval .. "s" or "30s" + } + }, + obfs = (node.hysteria2_obfs) and { + type = "salamander", + salamander = { + password = node.hysteria2_obfs + } + } or nil, + auth = node.hysteria2_auth_password, + tls = { + sni = node.tls_serverName, + insecure = (node.tls_allowInsecure == "1") and true or false, + pinSHA256 = (node.hysteria2_tls_pinSHA256) and node.hysteria2_tls_pinSHA256 or nil, + }, + quic = { + initStreamReceiveWindow = (node.hysteria2_recv_window) and tonumber(node.hysteria2_recv_window) or nil, + initConnReceiveWindow = (node.hysteria2_recv_window_conn) and tonumber(node.hysteria2_recv_window_conn) or nil, + maxIdleTimeout = (node.hysteria2_idle_timeout) and tonumber(node.hysteria2_idle_timeout) or nil, + disablePathMTUDiscovery = (node.hysteria2_disable_mtu_discovery) and true or false, + }, + bandwidth = (node.hysteria2_up_mbps or node.hysteria2_down_mbps) and { + up = node.hysteria2_up_mbps and node.hysteria2_up_mbps .. " mbps" or nil, + down = node.hysteria2_down_mbps and node.hysteria2_down_mbps .. " mbps" or nil + } or nil, + fast_open = (node.fast_open == "1") and true or false, + lazy = (node.hysteria2_lazy_start == "1") and true or false, + socks5 = (local_socks_address and local_socks_port) and { + listen = local_socks_address .. ":" .. local_socks_port, + username = (local_socks_username and local_socks_password) and local_socks_username or nil, + password = (local_socks_username and local_socks_password) and local_socks_password or nil, + disableUDP = false, + } or nil, + http = (local_http_address and local_http_port) and { + listen = local_http_address .. ":" .. local_http_port, + username = (local_http_username and local_http_password) and local_http_username or nil, + password = (local_http_username and local_http_password) and local_http_password or nil, + } 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_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua new file mode 100644 index 000000000..9e4984392 --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -0,0 +1,1733 @@ +module("luci.passwall2.util_sing-box", 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 CACHE_PATH = api.CACHE_PATH + +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 + +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 == "sing-box" then + proxy = 0 + if proxy_tag ~= "nil" then + node.detour = proxy_tag + end + end + + if node.type ~= "sing-box" 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 = { + protocol = "socks", + address = "127.0.0.1", + port = new_port + } + end + + result = { + _flag_tag = node_id, + _flag_proxy = proxy, + _flag_proxy_tag = proxy_tag, + tag = tag, + type = node.protocol, + server = node.address, + server_port = tonumber(node.port), + domain_strategy = node.domain_strategy, + detour = node.detour, + } + + local tls = nil + if node.tls == "1" then + local alpn = nil + if node.alpn and node.alpn ~= "default" then + alpn = {} + string.gsub(node.alpn, '[^' .. "," .. ']+', function(w) + table.insert(alpn, w) + end) + end + tls = { + enabled = true, + disable_sni = false, --不要在 ClientHello 中发送服务器名称. + server_name = node.tls_serverName, --用于验证返回证书上的主机名,除非设置不安全。它还包含在 ClientHello 中以支持虚拟主机,除非它是 IP 地址。 + insecure = (node.tls_allowInsecure == "1") and true or false, --接受任何服务器证书。 + alpn = alpn, --支持的应用层协议协商列表,按优先顺序排列。如果两个对等点都支持 ALPN,则选择的协议将是此列表中的一个,如果没有相互支持的协议则连接将失败。 + --min_version = "1.2", + --max_version = "1.3", + ech = { + enabled = (node.ech == "1") and true or false, + config = (node.ech_config and node.ech_config:gsub("\\n","\n")) and node.ech_config:gsub("\\n","\n") or nil, + pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false, + dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false + }, + utls = { + enabled = (node.utls == "1" or node.reality == "1") and true or false, + fingerprint = node.fingerprint or "chrome" + }, + reality = { + enabled = (node.reality == "1") and true or false, + public_key = node.reality_publicKey, + short_id = node.reality_shortId + } + } + end + + local mux = nil + if node.mux == "1" then + mux = { + enabled = true, + protocol = node.mux_type or "h2mux", + max_connections = ( (node.tcpbrutal == "1") and 1 ) or tonumber(node.mux_concurrency) or 4, + padding = (node.mux_padding == "1") and true or false, + --min_streams = 4, + --max_streams = 0, + brutal = { + enabled = (node.tcpbrutal == "1") and true or false, + up_mbps = tonumber(node.tcpbrutal_up_mbps) or 10, + down_mbps = tonumber(node.tcpbrutal_down_mbps) or 50, + }, + } + end + + local v2ray_transport = nil + + if node.transport == "http" then + v2ray_transport = { + type = "http", + host = { node.http_host }, + path = node.http_path or "/", + idle_timeout = (node.http_h2_health_check == "1") and node.http_h2_read_idle_timeout or nil, + ping_timeout = (node.http_h2_health_check == "1") and node.http_h2_health_check_timeout or nil, + } + --不强制执行 TLS。如果未配置 TLS,将使用纯 HTTP 1.1。 + end + + if node.transport == "ws" then + v2ray_transport = { + type = "ws", + path = node.ws_path or "/", + headers = (node.ws_host ~= nil) and { Host = node.ws_host } or nil, + max_early_data = tonumber(node.ws_maxEarlyData) or nil, + early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil --要与 Xray-core 兼容,请将其设置为 Sec-WebSocket-Protocol。它需要与服务器保持一致。 + } + end + + if node.transport == "httpupgrade" then + v2ray_transport = { + type = "httpupgrade", + host = node.httpupgrade_host, + path = node.httpupgrade_path or "/", + } + end + + if node.transport == "quic" then + v2ray_transport = { + type = "quic" + } + --没有额外的加密支持: 它基本上是重复加密。 并且 Xray-core 在这里与 v2ray-core 不兼容。 + end + + if node.transport == "grpc" then + v2ray_transport = { + type = "grpc", + service_name = node.grpc_serviceName, + idle_timeout = tonumber(node.grpc_idle_timeout) or nil, + ping_timeout = tonumber(node.grpc_health_check_timeout) or nil, + permit_without_stream = (node.grpc_permit_without_stream == "1") and true or nil, + } + end + + local protocol_table = nil + + if node.protocol == "socks" then + protocol_table = { + version = "5", + username = (node.username and node.password) and node.username or nil, + password = (node.username and node.password) and node.password or nil, + udp_over_tcp = node.uot == "1" and { + enabled = true, + version = 2 + } or nil, + } + end + + if node.protocol == "http" then + protocol_table = { + username = (node.username and node.password) and node.username or nil, + password = (node.username and node.password) and node.password or nil, + path = nil, + headers = nil, + tls = tls + } + end + + if node.protocol == "shadowsocks" then + protocol_table = { + method = node.method or nil, + password = node.password or "", + plugin = (node.plugin_enabled and node.plugin) or nil, + plugin_opts = (node.plugin_enabled and node.plugin_opts) or nil, + udp_over_tcp = node.uot == "1" and { + enabled = true, + version = 2 + } or nil, + multiplex = mux, + } + end + + if node.protocol == "shadowsocksr" then + protocol_table = { + method = node.method or nil, + password = node.password or "", + obfs = node.ssr_obfs, + obfs_param = node.ssr_obfs_param, + protocol = node.ssr_protocol, + protocol_param = node.ssr_protocol_param, + } + end + + if node.protocol == "trojan" then + protocol_table = { + password = node.password, + tls = tls, + multiplex = mux, + transport = v2ray_transport + } + end + + if node.protocol == "vmess" then + protocol_table = { + uuid = node.uuid, + security = node.security, + alter_id = (node.alter_id) and tonumber(node.alter_id) or 0, + global_padding = (node.global_padding == "1") and true or false, + authenticated_length = (node.authenticated_length == "1") and true or false, + tls = tls, + packet_encoding = "", --UDP 包编码。(空):禁用 packetaddr:由 v2ray 5+ 支持 xudp:由 xray 支持 + multiplex = mux, + transport = v2ray_transport, + } + end + + if node.protocol == "vless" then + protocol_table = { + uuid = node.uuid, + flow = (node.tls == '1' and node.flow) and node.flow or nil, + tls = tls, + packet_encoding = "xudp", --UDP 包编码。(空):禁用 packetaddr:由 v2ray 5+ 支持 xudp:由 xray 支持 + multiplex = mux, + transport = v2ray_transport, + } + end + + if node.protocol == "wireguard" then + if 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 + protocol_table = { + system_interface = (node.wireguard_system_interface == "1") and true or false, + interface_name = node.wireguard_interface_name, + local_address = node.wireguard_local_address, + private_key = node.wireguard_secret_key, + peer_public_key = node.wireguard_public_key, + pre_shared_key = node.wireguard_preSharedKey, + reserved = node.wireguard_reserved, + mtu = tonumber(node.wireguard_mtu), + } + end + + if node.protocol == "hysteria" then + protocol_table = { + up_mbps = tonumber(node.hysteria_up_mbps), + down_mbps = tonumber(node.hysteria_down_mbps), + 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, + recv_window_conn = tonumber(node.hysteria_recv_window_conn), + recv_window = tonumber(node.hysteria_recv_window), + disable_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false, + tls = { + enabled = true, + server_name = node.tls_serverName, + insecure = (node.tls_allowInsecure == "1") and true or false, + alpn = (node.hysteria_alpn and node.hysteria_alpn ~= "") and { + node.hysteria_alpn + } or nil, + ech = { + enabled = (node.ech == "1") and true or false, + config = (node.ech_config and node.ech_config:gsub("\\n","\n")) and node.ech_config:gsub("\\n","\n") or nil, + pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false, + dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false + } + } + } + end + + if node.protocol == "shadowtls" then + protocol_table = { + version = tonumber(node.shadowtls_version), + password = (node.shadowtls_version == "2" or node.shadowtls_version == "3") and node.password or nil, + tls = tls, + } + end + + if node.protocol == "tuic" then + protocol_table = { + uuid = node.uuid, + password = node.password, + congestion_control = node.tuic_congestion_control or "cubic", + udp_relay_mode = node.tuic_udp_relay_mode or "native", + udp_over_stream = false, + zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1") and true or false, + heartbeat = node.tuic_heartbeat .. "s", + tls = { + enabled = true, + server_name = node.tls_serverName, + insecure = (node.tls_allowInsecure == "1") and true or false, + alpn = (node.tuic_alpn and node.tuic_alpn ~= "") and { + node.tuic_alpn + } or nil, + ech = { + enabled = (node.ech == "1") and true or false, + config = (node.ech_config and node.ech_config:gsub("\\n","\n")) and node.ech_config:gsub("\\n","\n") or nil, + pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false, + dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false + } + } + } + end + + if node.protocol == "hysteria2" then + protocol_table = { + up_mbps = (node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) or nil, + down_mbps = (node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) or nil, + obfs = { + type = node.hysteria2_obfs_type, + password = node.hysteria2_obfs_password + }, + password = node.hysteria2_auth_password or nil, + tls = { + enabled = true, + server_name = node.tls_serverName, + insecure = (node.tls_allowInsecure == "1") and true or false, + ech = { + enabled = (node.ech == "1") and true or false, + config = (node.ech_config and node.ech_config:gsub("\\n","\n")) and node.ech_config:gsub("\\n","\n") or nil, + pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false, + dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false + } + } + } + end + + if protocol_table then + for key, value in pairs(protocol_table) do + result[key] = value + end + end + end + return result +end + +function gen_config_server(node) + local outbounds = { + { type = "direct", tag = "direct" }, + { type = "block", tag = "block" } + } + + local tls = { + enabled = true, + certificate_path = node.tls_certificateFile, + key_path = node.tls_keyFile, + } + + if node.tls == "1" and node.reality == "1" then + tls.certificate_path = nil + tls.key_path = nil + tls.reality = { + enabled = true, + private_key = node.reality_private_key, + short_id = { + node.reality_shortId + }, + handshake = { + server = node.reality_handshake_server, + server_port = tonumber(node.reality_handshake_server_port) + } + } + end + + if node.tls == "1" and node.ech == "1" then + tls.ech = { + enabled = true, + key = (node.ech_key and node.ech_key:gsub("\\n","\n")) and node.ech_key:gsub("\\n","\n") or nil, + pq_signature_schemes_enabled = (node.pq_signature_schemes_enabled == "1") and true or false, + dynamic_record_sizing_disabled = (node.dynamic_record_sizing_disabled == "1") and true or false, + } + end + + local mux = nil + if node.mux == "1" then + mux = { + enabled = true, + padding = (node.mux_padding == "1") and true or false, + brutal = { + enabled = (node.tcpbrutal == "1") and true or false, + up_mbps = tonumber(node.tcpbrutal_up_mbps) or 10, + down_mbps = tonumber(node.tcpbrutal_down_mbps) or 50, + }, + } + end + + local v2ray_transport = nil + + if node.transport == "http" then + v2ray_transport = { + type = "http", + host = node.http_host, + path = node.http_path or "/", + } + end + + if node.transport == "ws" then + v2ray_transport = { + type = "ws", + path = node.ws_path or "/", + headers = (node.ws_host ~= nil) and { Host = node.ws_host } or nil, + early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil --要与 Xray-core 兼容,请将其设置为 Sec-WebSocket-Protocol。它需要与服务器保持一致。 + } + end + + if node.transport == "httpupgrade" then + v2ray_transport = { + type = "httpupgrade", + host = node.httpupgrade_host, + path = node.httpupgrade_path or "/", + } + end + + if node.transport == "quic" then + v2ray_transport = { + type = "quic" + } + --没有额外的加密支持: 它基本上是重复加密。 并且 Xray-core 在这里与 v2ray-core 不兼容。 + end + + if node.transport == "grpc" then + v2ray_transport = { + type = "grpc", + service_name = node.grpc_serviceName, + } + end + + local inbound = { + type = node.protocol, + tag = "inbound", + listen = (node.bind_local == "1") and "127.0.0.1" or "::", + listen_port = tonumber(node.port), + } + + local protocol_table = nil + + if node.protocol == "mixed" then + protocol_table = { + users = (node.auth == "1") and { + { + username = node.username, + password = node.password + } + } or nil, + set_system_proxy = false + } + end + + if node.protocol == "socks" then + protocol_table = { + users = (node.auth == "1") and { + { + username = node.username, + password = node.password + } + } or nil + } + end + + if node.protocol == "http" then + protocol_table = { + users = (node.auth == "1") and { + { + username = node.username, + password = node.password + } + } or nil, + tls = (node.tls == "1") and tls or nil, + } + end + + if node.protocol == "shadowsocks" then + protocol_table = { + method = node.method, + password = node.password, + multiplex = mux, + } + end + + if node.protocol == "vmess" then + if node.uuid then + local users = {} + for i = 1, #node.uuid do + users[i] = { + name = node.uuid[i], + uuid = node.uuid[i], + alterId = 0, + } + end + protocol_table = { + users = users, + tls = (node.tls == "1") and tls or nil, + multiplex = mux, + transport = v2ray_transport, + } + end + end + + if node.protocol == "vless" then + if node.uuid then + local users = {} + for i = 1, #node.uuid do + users[i] = { + name = node.uuid[i], + uuid = node.uuid[i], + flow = node.flow, + } + end + protocol_table = { + users = users, + tls = (node.tls == "1") and tls or nil, + multiplex = mux, + transport = v2ray_transport, + } + end + end + + if node.protocol == "trojan" then + if node.uuid then + local users = {} + for i = 1, #node.uuid do + users[i] = { + name = node.uuid[i], + password = node.uuid[i], + } + end + protocol_table = { + users = users, + tls = (node.tls == "1") and tls or nil, + fallback = nil, + fallback_for_alpn = nil, + multiplex = mux, + transport = v2ray_transport, + } + end + end + + if node.protocol == "naive" then + protocol_table = { + users = { + { + username = node.username, + password = node.password + } + }, + tls = tls + } + end + + if node.protocol == "hysteria" then + tls.alpn = (node.hysteria_alpn and node.hysteria_alpn ~= "") and { + node.hysteria_alpn + } or nil + protocol_table = { + up = node.hysteria_up_mbps .. " Mbps", + down = node.hysteria_down_mbps .. " Mbps", + up_mbps = tonumber(node.hysteria_up_mbps), + down_mbps = tonumber(node.hysteria_down_mbps), + obfs = node.hysteria_obfs, + users = { + { + name = "user1", + 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, + } + }, + recv_window_conn = node.hysteria_recv_window_conn and tonumber(node.hysteria_recv_window_conn) or nil, + recv_window_client = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil, + max_conn_client = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil, + disable_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false, + tls = tls + } + end + + if node.protocol == "tuic" then + tls.alpn = (node.tuic_alpn and node.tuic_alpn ~= "") and { + node.tuic_alpn + } or nil + protocol_table = { + users = { + { + name = "user1", + uuid = node.uuid, + password = node.password + } + }, + congestion_control = node.tuic_congestion_control or "cubic", + zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1") and true or false, + heartbeat = node.tuic_heartbeat .. "s", + tls = tls + } + end + + if node.protocol == "hysteria2" then + protocol_table = { + up_mbps = (node.hysteria2_ignore_client_bandwidth ~= "1" and node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) or nil, + down_mbps = (node.hysteria2_ignore_client_bandwidth ~= "1" and node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) or nil, + obfs = { + type = node.hysteria2_obfs_type, + password = node.hysteria2_obfs_password + }, + users = { + { + name = "user1", + password = node.hysteria2_auth_password or nil, + } + }, + ignore_client_bandwidth = (node.hysteria2_ignore_client_bandwidth == "1") and true or false, + tls = tls + } + end + + if node.protocol == "direct" then + protocol_table = { + network = (node.d_protocol ~= "TCP,UDP") and node.d_protocol or nil, + override_address = node.d_address, + override_port = tonumber(node.d_port) + } + end + + if protocol_table then + for key, value in pairs(protocol_table) do + inbound[key] = value + end + end + + local route = { + rules = { + { + ip_cidr = { "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" }, + outbound = (node.accept_lan == nil or node.accept_lan == "0") and "block" 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 = { + type = "direct", + tag = "outbound", + bind_interface = node.outbound_node_iface, + routing_mark = 255, + } + sys.call("mkdir -p /tmp/etc/passwall2/iface && touch /tmp/etc/passwall2/iface/" .. 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("_", ""), + address = node.outbound_node_address, + port = tonumber(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_sing-box").gen_outbound(nil, outbound_node_t, "outbound") + end + if outbound then + route.final = "outbound" + table.insert(outbounds, 1, outbound) + end + end + + local config = { + log = { + disabled = (not node or node.log == "0") and true or false, + level = node.loglevel or "info", + timestamp = true, + --output = logfile, + }, + inbounds = { inbound }, + outbounds = outbounds, + route = route + } + + for index, value in ipairs(config.outbounds) do + for k, v in pairs(config.outbounds[index]) do + if k:find("_") == 1 then + config.outbounds[index][k] = nil + end + end + end + + return config +end + +function gen_config(var) + local flag = var["-flag"] + local log = var["-log"] or "0" + local loglevel = var["-loglevel"] or "warn" + local logfile = var["-logfile"] or "/dev/null" + local node_id = var["-node"] + local server_host = var["-server_host"] + local server_port = var["-server_port"] + local tcp_proxy_way = var["-tcp_proxy_way"] + local redir_port = var["-redir_port"] + 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 direct_dns_udp_server = var["-direct_dns_udp_server"] + local direct_dns_udp_port = var["-direct_dns_udp_port"] + local direct_dns_query_strategy = var["-direct_dns_query_strategy"] + local direct_ipset = var["-direct_ipset"] + local direct_nftset = var["-direct_nftset"] + local remote_dns_udp_server = var["-remote_dns_udp_server"] + local remote_dns_udp_port = var["-remote_dns_udp_port"] + local remote_dns_tcp_server = var["-remote_dns_tcp_server"] + local remote_dns_tcp_port = var["-remote_dns_tcp_port"] + local remote_dns_doh_url = var["-remote_dns_doh_url"] + local remote_dns_doh_host = var["-remote_dns_doh_host"] + local remote_dns_doh_ip = var["-remote_dns_doh_ip"] + local remote_dns_doh_port = var["-remote_dns_doh_port"] + local remote_dns_detour = var["-remote_dns_detour"] + local remote_dns_query_strategy = var["-remote_dns_query_strategy"] + local remote_dns_fake = var["-remote_dns_fake"] + local dns_cache = var["-dns_cache"] + local tags = var["-tags"] + + local dns_domain_rules = {} + local dns = nil + local inbounds = {} + local outbounds = {} + + local CACHE_TEXT_FILE = CACHE_PATH .. "/cache_" .. flag .. ".txt" + + local singbox_settings = uci:get_all(appname, "@global_singbox[0]") or {} + + local route = { + rules = {}, + geoip = { + path = singbox_settings.geoip_path or "/usr/share/singbox/geoip.db", + download_url = singbox_settings.geoip_url or nil, + download_detour = nil, + }, + geosite = { + path = singbox_settings.geosite_path or "/usr/share/singbox/geosite.db", + download_url = singbox_settings.geosite_url or nil, + download_detour = nil, + }, + } + + local experimental = nil + + local node = nil + if node_id then + node = uci:get_all(appname, node_id) + end + + if local_socks_port then + local inbound = { + type = "socks", + tag = "socks-in", + listen = local_socks_address, + listen_port = tonumber(local_socks_port), + sniff = true + } + if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then + inbound.users = { + { + username = local_socks_username, + password = local_socks_password + } + } + end + table.insert(inbounds, inbound) + end + + if local_http_port then + local inbound = { + type = "http", + tag = "http-in", + listen = local_http_address, + listen_port = tonumber(local_http_port) + } + if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then + inbound.users = { + { + username = local_http_username, + password = local_http_password + } + } + end + table.insert(inbounds, inbound) + end + + if redir_port then + local inbound_tproxy = { + type = "tproxy", + tag = "tproxy", + listen = "::", + listen_port = tonumber(redir_port), + sniff = true, + sniff_override_destination = (singbox_settings.sniff_override_destination == "1") and true or false + } + if tcp_proxy_way ~= "tproxy" then + local inbound = { + type = "redirect", + tag = "redirect_tcp", + listen = "::", + listen_port = tonumber(redir_port), + sniff = true, + sniff_override_destination = (singbox_settings.sniff_override_destination == "1") and true or false, + } + table.insert(inbounds, inbound) + + inbound_tproxy.tag = "tproxy_udp" + inbound_tproxy.network = "udp" + end + + table.insert(inbounds, inbound_tproxy) + end + + local default_outTag = nil + + if node then + if server_host and server_port then + node.address = server_host + node.port = server_port + end + + local function set_outbound_detour(node, outbound, outbounds_table, shunt_rule_name) + if not node or not outbound or not outbounds_table then return nil end + local default_outTag = outbound.tag + + if node.shadowtls == "1" then + local _node = { + type = "sing-box", + protocol = "shadowtls", + shadowtls_version = node.shadowtls_version, + password = (node.shadowtls_version == "2" or node.shadowtls_version == "3") and node.shadowtls_password or nil, + address = node.address, + port = node.port, + tls = "1", + tls_serverName = node.shadowtls_serverName, + utls = node.shadowtls_utls, + fingerprint = node.shadowtls_fingerprint + } + local shadowtls_outbound = gen_outbound(nil, _node, outbound.tag .. "_shadowtls") + if shadowtls_outbound then + table.insert(outbounds_table, shadowtls_outbound) + outbound.detour = outbound.tag .. "_shadowtls" + outbound.server = nil + outbound.server_port = nil + end + end + + if node.to_node then + local to_node = uci:get_all(appname, node.to_node) + if to_node then + local to_outbound = gen_outbound(nil, to_node) + if to_outbound then + if shunt_rule_name then + to_outbound.tag = outbound.tag + outbound.tag = node[".name"] + else + to_outbound.tag = outbound.tag .. " -> " .. to_outbound.tag + end + + to_outbound.detour = outbound.tag + table.insert(outbounds_table, to_outbound) + default_outTag = to_outbound.tag + end + end + end + return default_outTag + end + + if node.protocol == "_shunt" then + local rules = {} + + 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 + + if preproxy_node_id and preproxy_node_id:find("Socks_") then + local socks_id = preproxy_node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + local _node = { + type = "sing-box", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.port, + uot = "1", + } + local preproxy_outbound = gen_outbound(flag, _node, preproxy_tag) + if preproxy_outbound then + table.insert(outbounds, preproxy_outbound) + else + preproxy_enabled = false + end + 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 + set_outbound_detour(preproxy_node, preproxy_outbound, outbounds, preproxy_tag) + table.insert(outbounds, preproxy_outbound) + else + preproxy_enabled = false + end + end + + local function gen_shunt_node(rule_name, _node_id) + 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 + if _node_id == "_direct" then + rule_outboundTag = "direct" + elseif _node_id == "_blackhole" then + rule_outboundTag = "block" + elseif _node_id == "_default" and rule_name ~= "default" then + rule_outboundTag = "default" + elseif _node_id:find("Socks_") then + local socks_id = _node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + local _node = { + type = "sing-box", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.port, + uot = "1", + } + local _outbound = gen_outbound(flag, _node, rule_name) + if _outbound then + table.insert(outbounds, _outbound) + rule_outboundTag = rule_name + end + 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 + 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 ~= "sing-box" then + pre_proxy = true + end + if pre_proxy then + new_port = get_new_port() + table.insert(inbounds, { + type = "direct", + tag = "proxy_" .. rule_name, + listen = "127.0.0.1", + listen_port = new_port, + override_address = _node.address, + override_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, { + inbound = {"proxy_" .. rule_name}, + outbound = preproxy_tag, + }) + 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 + set_outbound_detour(_node, _outbound, outbounds, rule_name) + table.insert(outbounds, _outbound) + rule_outboundTag = rule_name + end + end + elseif _node.protocol == "_iface" then + if _node.iface then + local _outbound = { + type = "direct", + tag = rule_name, + bind_interface = _node.iface, + routing_mark = 255, + } + table.insert(outbounds, _outbound) + rule_outboundTag = rule_name + sys.call("touch /tmp/etc/passwall2/iface/" .. _node.iface) + end + end + end + return rule_outboundTag + end + --default_node + local default_node_id = node.default_node or "_direct" + local default_outboundTag = gen_shunt_node("default", default_node_id) + --shunt rule + uci:foreach(appname, "shunt_rules", function(e) + local outboundTag = gen_shunt_node(e[".name"]) + if outboundTag and e.remarks then + if outboundTag == "default" then + outboundTag = default_outboundTag + 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 inboundTag = nil + if e["inbound"] and e["inbound"] ~= "" then + inboundTag = {} + if e["inbound"]:find("tproxy") then + if redir_port then + if tcp_proxy_way == "tproxy" then + table.insert(inboundTag, "tproxy") + else + table.insert(inboundTag, "redirect_tcp") + table.insert(inboundTag, "tproxy_udp") + end + end + end + if e["inbound"]:find("socks") then + if local_socks_port then + table.insert(inboundTag, "socks-in") + end + end + end + + local rule = { + inbound = inboundTag, + outbound = outboundTag, + invert = false, --匹配反选 + protocol = protocols + } + + if e.network then + local network = {} + string.gsub(e.network, '[^' .. "," .. ']+', function(w) + table.insert(network, w) + end) + rule.network = network + end + + if e.source then + local source_geoip = {} + local source_ip_cidr = {} + string.gsub(e.source, '[^' .. " " .. ']+', function(w) + if w:find("geoip") == 1 then + table.insert(source_geoip, w) + else + table.insert(source_ip_cidr, w) + end + end) + rule.source_geoip = #source_geoip > 0 and source_geoip or nil + rule.source_ip_cidr = #source_ip_cidr > 0 and source_ip_cidr or nil + end + + if e.sourcePort then + local source_port = {} + local source_port_range = {} + string.gsub(e.sourcePort, '[^' .. "," .. ']+', function(w) + if tonumber(w) and tonumber(w) >= 1 and tonumber(w) <= 65535 then + table.insert(source_port, tonumber(w)) + else + table.insert(source_port_range, w) + end + end) + rule.source_port = #source_port > 0 and source_port or nil + rule.source_port_range = #source_port_range > 0 and source_port_range or nil + end + + if e.port then + local port = {} + local port_range = {} + string.gsub(e.port, '[^' .. "," .. ']+', function(w) + if tonumber(w) and tonumber(w) >= 1 and tonumber(w) <= 65535 then + table.insert(port, tonumber(w)) + else + table.insert(port_range, w) + end + end) + rule.port = #port > 0 and port or nil + rule.port_range = #port_range > 0 and port_range or nil + end + + if e.domain_list then + local domain_table = { + outboundTag = outboundTag, + domain = {}, + domain_suffix = {}, + domain_keyword = {}, + domain_regex = {}, + geosite = {}, + } + string.gsub(e.domain_list, '[^' .. "\r\n" .. ']+', function(w) + if w:find("#") == 1 then return end + if w:find("geosite:") == 1 then + table.insert(domain_table.geosite, w:sub(1 + #"geosite:")) + elseif w:find("regexp:") == 1 then + table.insert(domain_table.domain_regex, w:sub(1 + #"regexp:")) + elseif w:find("full:") == 1 then + table.insert(domain_table.domain, w:sub(1 + #"full:")) + elseif w:find("domain:") == 1 then + table.insert(domain_table.domain_suffix, w:sub(1 + #"domain:")) + else + table.insert(domain_table.domain_keyword, w) + end + end) + rule.domain = #domain_table.domain > 0 and domain_table.domain or nil + rule.domain_suffix = #domain_table.domain_suffix > 0 and domain_table.domain_suffix or nil + rule.domain_keyword = #domain_table.domain_keyword > 0 and domain_table.domain_keyword or nil + rule.domain_regex = #domain_table.domain_regex > 0 and domain_table.domain_regex or nil + rule.geosite = #domain_table.geosite > 0 and domain_table.geosite or nil + + if outboundTag and outboundTag ~= "nil" then + table.insert(dns_domain_rules, api.clone(domain_table)) + end + end + + if e.ip_list then + local ip_cidr = {} + local geoip = {} + string.gsub(e.ip_list, '[^' .. "\r\n" .. ']+', function(w) + if w:find("#") == 1 then return end + if w:find("geoip:") == 1 then + table.insert(geoip, w:sub(1 + #"geoip:")) + else + table.insert(ip_cidr, w) + end + end) + + rule.ip_cidr = #ip_cidr > 0 and ip_cidr or nil + rule.geoip = #geoip > 0 and geoip or nil + end + + table.insert(rules, rule) + end + end) + + if default_outboundTag then + route.final = default_outboundTag + default_outTag = default_outboundTag + end + + for index, value in ipairs(rules) do + table.insert(route.rules, rules[index]) + end + elseif node.protocol == "_iface" then + if node.iface then + local outbound = { + type = "direct", + tag = node_id, + bind_interface = node.iface, + routing_mark = 255, + } + table.insert(outbounds, outbound) + default_outTag = outbound.tag + route.final = default_outTag + sys.call("touch /tmp/etc/passwall2/iface/" .. node.iface) + end + else + local outbound = gen_outbound(flag, node) + if outbound then + default_outTag = set_outbound_detour(node, outbound, outbounds) + table.insert(outbounds, outbound) + route.final = default_outTag + end + end + end + + if dns_listen_port then + dns = { + servers = {}, + rules = {}, + disable_cache = (dns_cache and dns_cache == "0") and true or false, + disable_expire = false, --禁用 DNS 缓存过期。 + independent_cache = false, --使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 + reverse_mapping = true, --在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 + fakeip = nil, + } + + table.insert(dns.servers, { + tag = "block", + address = "rcode://success", + }) + + local remote_strategy = "prefer_ipv6" + if remote_dns_query_strategy == "UseIPv4" then + remote_strategy = "ipv4_only" + elseif remote_dns_query_strategy == "UseIPv6" then + remote_strategy = "ipv6_only" + end + + local remote_server = { + tag = "remote", + address_strategy = "prefer_ipv4", + strategy = remote_strategy, + address_resolver = "direct", + detour = default_outTag, + } + + if remote_dns_detour == "direct" then + remote_server.detour = "direct" + end + + if remote_dns_udp_server then + local server_port = tonumber(remote_dns_udp_port) or 53 + remote_server.address = "udp://" .. remote_dns_udp_server .. ":" .. server_port + end + + if remote_dns_tcp_server then + local server_port = tonumber(remote_dns_tcp_port) or 53 + remote_server.address = "tcp://" .. remote_dns_tcp_server .. ":" .. server_port + end + + if remote_dns_doh_url then + remote_server.address = remote_dns_doh_url + end + + if remote_server.address then + table.insert(dns.servers, remote_server) + end + + local fakedns_tag = "remote_fakeip" + if remote_dns_fake then + dns.fakeip = { + enabled = true, + inet4_range = "198.18.0.0/16", + inet6_range = "fc00::/18", + } + + table.insert(dns.servers, { + tag = fakedns_tag, + address = "fakeip", + strategy = remote_strategy, + }) + + if not experimental then + experimental = {} + end + experimental.cache_file = { + enabled = true, + store_fakeip = true, + path = "/tmp/singbox_passwall2_" .. flag .. ".db" + } + end + + if direct_dns_udp_server then + local domain = {} + 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(domain, w) + end) + if #domain > 0 then + table.insert(dns_domain_rules, 1, { + outboundTag = "direct", + domain = domain + }) + end + + local direct_strategy = "prefer_ipv6" + if direct_dns_query_strategy == "UseIPv4" then + direct_strategy = "ipv4_only" + elseif direct_dns_query_strategy == "UseIPv6" then + direct_strategy = "ipv6_only" + end + + local port = tonumber(direct_dns_udp_port) or 53 + + table.insert(dns.servers, { + tag = "direct", + address = "udp://" .. direct_dns_udp_server .. ":" .. port, + address_strategy = "prefer_ipv6", + strategy = direct_strategy, + detour = "direct", + }) + 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 + else default_dns_flag = "direct" + end + dns.final = default_dns_flag + + --按分流顺序DNS + if dns_domain_rules and #dns_domain_rules > 0 then + for index, value in ipairs(dns_domain_rules) do + if value.outboundTag and (value.domain or value.domain_suffix or value.domain_keyword or value.domain_regex or value.geosite) then + local dns_rule = { + server = value.outboundTag, + domain = (value.domain and #value.domain > 0) and value.domain or nil, + domain_suffix = (value.domain_suffix and #value.domain_suffix > 0) and value.domain_suffix or nil, + domain_keyword = (value.domain_keyword and #value.domain_keyword > 0) and value.domain_keyword or nil, + domain_regex = (value.domain_regex and #value.domain_regex > 0) and value.domain_regex or nil, + geosite = (value.geosite and #value.geosite > 0) and value.geosite or nil, + disable_cache = false, + } + if value.outboundTag ~= "block" and value.outboundTag ~= "direct" then + dns_rule.server = "remote" + dns_rule.rewrite_ttl = 30 + if value.outboundTag ~= "default" and remote_server.address and remote_dns_detour ~= "direct" then + local remote_dns_server = api.clone(remote_server) + remote_dns_server.tag = value.outboundTag + remote_dns_server.detour = value.outboundTag + table.insert(dns.servers, remote_dns_server) + dns_rule.server = remote_dns_server.tag + end + if remote_dns_fake then + local fakedns_dns_rule = api.clone(dns_rule) + fakedns_dns_rule.query_type = { + "A", "AAAA" + } + fakedns_dns_rule.server = fakedns_tag + fakedns_dns_rule.disable_cache = true + table.insert(dns.rules, fakedns_dns_rule) + end + end + table.insert(dns.rules, dns_rule) + end + end + end + + table.insert(inbounds, { + type = "direct", + tag = "dns-in", + listen = "127.0.0.1", + listen_port = tonumber(dns_listen_port), + sniff = true, + }) + table.insert(outbounds, { + type = "dns", + tag = "dns-out", + }) + table.insert(route.rules, 1, { + protocol = "dns", + inbound = { + "dns-in" + }, + outbound = "dns-out" + }) + + local content = flag .. node_id .. jsonc.stringify(route.rules) + if api.cacheFileCompareToLogic(CACHE_TEXT_FILE, content) == false then + --clear ipset/nftset + if direct_ipset then + string.gsub(direct_ipset, '[^' .. "," .. ']+', function(w) + sys.call("ipset -q -F " .. w) + end) + end + if direct_nftset then + string.gsub(direct_nftset, '[^' .. "," .. ']+', function(w) + local split = api.split(w, "#") + if #split > 3 then + local ip_type = split[1] + local family = split[2] + local table_name = split[3] + local set_name = split[4] + sys.call(string.format("nft flush set %s %s %s 2>/dev/null", family, table_name, set_name)) + end + end) + end + end + end + + if inbounds or outbounds then + local config = { + log = { + disabled = log == "0" and true or false, + level = loglevel, + timestamp = true, + output = logfile, + }, + -- DNS + dns = dns, + -- 传入连接 + inbounds = inbounds, + -- 传出连接 + outbounds = outbounds, + -- 路由 + route = route, + --实验性 + experimental = experimental, + } + table.insert(outbounds, { + type = "direct", + tag = "direct", + routing_mark = 255, + domain_strategy = "prefer_ipv6", + }) + table.insert(outbounds, { + type = "block", + tag = "block" + }) + for index, value in ipairs(config.outbounds) do + for k, v in pairs(config.outbounds[index]) do + if k:find("_") == 1 then + config.outbounds[index][k] = nil + end + end + end + 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 = {} + + if local_socks_address and local_socks_port then + local inbound = { + type = "socks", + tag = "socks-in", + listen = local_socks_address, + listen_port = tonumber(local_socks_port), + } + if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then + inbound.users = { + username = local_socks_username, + password = local_socks_password + } + end + table.insert(inbounds, inbound) + end + + if local_http_address and local_http_port then + local inbound = { + type = "http", + tag = "http-in", + tls = nil, + listen = local_http_address, + listen_port = tonumber(local_http_port), + } + if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then + inbound.users = { + { + username = local_http_username, + password = local_http_password + } + } + end + table.insert(inbounds, inbound) + end + + if server_proto ~= "nil" and server_address ~= "nil" and server_port ~= "nil" then + local outbound = { + type = server_proto, + tag = "out", + server = server_address, + server_port = tonumber(server_port), + username = (server_username and server_password) and server_username or nil, + password = (server_username and server_password) and server_password or nil, + } + if outbound then table.insert(outbounds, outbound) end + end + + local config = { + log = { + disabled = true, + level = "warn", + timestamp = true, + }, + -- 传入连接 + inbounds = inbounds, + -- 传出连接 + outbounds = outbounds, + } + 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 direct_dns_udp_server = var["-direct_dns_udp_server"] + local direct_dns_udp_port = var["-direct_dns_udp_port"] + local direct_dns_tcp_server = var["-direct_dns_tcp_server"] + local direct_dns_tcp_port = var["-direct_dns_tcp_port"] + local direct_dns_doh_url = var["-direct_dns_doh_url"] + local direct_dns_doh_host = var["-direct_dns_doh_host"] + local direct_dns_doh_ip = var["-direct_dns_doh_ip"] + local direct_dns_doh_port = var["-direct_dns_doh_port"] + local remote_dns_udp_server = var["-remote_dns_udp_server"] + local remote_dns_udp_port = var["-remote_dns_udp_port"] + local remote_dns_tcp_server = var["-remote_dns_tcp_server"] + local remote_dns_tcp_port = var["-remote_dns_tcp_port"] + local remote_dns_doh_url = var["-remote_dns_doh_url"] + local remote_dns_doh_host = var["-remote_dns_doh_host"] + local remote_dns_doh_ip = var["-remote_dns_doh_ip"] + local remote_dns_doh_port = var["-remote_dns_doh_port"] + local remote_dns_detour = var["-remote_dns_detour"] + 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 dns_cache = var["-dns_cache"] + local log = var["-log"] or "0" + local loglevel = var["-loglevel"] or "warn" + local logfile = var["-logfile"] or "/dev/null" + + local inbounds = {} + local outbounds = {} + local dns = nil + local route = nil + + if dns_listen_port then + route = { + rules = {} + } + + dns = { + servers = {}, + rules = {}, + disable_cache = (dns_cache and dns_cache == "0") and true or false, + disable_expire = false, --禁用 DNS 缓存过期。 + independent_cache = false, --使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 + reverse_mapping = true, --在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 + } + + if dns_out_tag == "remote" then + local out_tag = nil + if remote_dns_detour == "direct" then + out_tag = "direct-out" + table.insert(outbounds, 1, { + type = "direct", + tag = out_tag, + routing_mark = 255, + domain_strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6", + }) + else + if remote_dns_outbound_socks_address and remote_dns_outbound_socks_port then + out_tag = "remote-out" + table.insert(outbounds, 1, { + type = "socks", + tag = out_tag, + server = remote_dns_outbound_socks_address, + server_port = tonumber(remote_dns_outbound_socks_port), + }) + end + end + + local server = { + tag = dns_out_tag, + address_strategy = "prefer_ipv4", + strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6", + detour = out_tag, + } + + if remote_dns_udp_server then + local server_port = tonumber(remote_dns_udp_port) or 53 + server.address = "udp://" .. remote_dns_udp_server .. ":" .. server_port + end + + if remote_dns_tcp_server then + local server_port = tonumber(remote_dns_tcp_port) or 53 + server.address = "tcp://" .. remote_dns_tcp_server .. ":" .. server_port + end + + if remote_dns_doh_url then + server.address = remote_dns_doh_url + end + + table.insert(dns.servers, server) + + route.final = out_tag + elseif dns_out_tag == "direct" then + local out_tag = "direct-out" + table.insert(outbounds, 1, { + type = "direct", + tag = out_tag, + routing_mark = 255, + domain_strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6", + }) + + local server = { + tag = dns_out_tag, + address_strategy = "prefer_ipv6", + strategy = (dns_query_strategy and dns_query_strategy ~= "UseIP") and "ipv4_only" or "prefer_ipv6", + detour = out_tag, + } + + if direct_dns_udp_server then + local server_port = tonumber(direct_dns_udp_port) or 53 + server.address = "udp://" .. direct_dns_udp_server .. ":" .. server_port + end + + if direct_dns_tcp_server then + local server_port = tonumber(direct_dns_tcp_port) or 53 + server.address = "tcp://" .. direct_dns_tcp_server .. ":" .. server_port + end + + if direct_dns_doh_url then + server.address = direct_dns_doh_url + end + + table.insert(dns.servers, server) + + route.final = out_tag + end + + table.insert(inbounds, { + type = "direct", + tag = "dns-in", + listen = "127.0.0.1", + listen_port = tonumber(dns_listen_port), + sniff = true, + }) + + table.insert(outbounds, { + type = "dns", + tag = "dns-out", + }) + + table.insert(route.rules, 1, { + protocol = "dns", + inbound = { + "dns-in" + }, + outbound = "dns-out" + }) + end + + if inbounds or outbounds then + local config = { + log = { + disabled = log == "0" and true or false, + level = loglevel, + timestamp = true, + output = logfile, + }, + -- DNS + dns = dns, + -- 传入连接 + inbounds = inbounds, + -- 传出连接 + outbounds = outbounds, + -- 路由 + route = route + } + 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/passwall2/util_tuic.lua b/luci-app-passwall2/luasrc/passwall2/util_tuic.lua new file mode 100644 index 000000000..b37027c4f --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/util_tuic.lua @@ -0,0 +1,57 @@ +module("luci.passwall2.util_tuic", package.seeall) +local api = require "luci.passwall2.api" +local uci = api.uci +local json = 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 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 + local loglevel = var["-loglevel"] or "warn" + + local tuic= { + relay = { + server = server_host .. ":" .. server_port, + ip = node.tuic_ip, + uuid = node.uuid, + password = node.tuic_password, + -- certificates = node.tuic_certificate and { node.tuic_certpath } or nil, + udp_relay_mode = node.tuic_udp_relay_mode, + congestion_control = node.tuic_congestion_control, + heartbeat = node.tuic_heartbeat .. "s", + timeout = node.tuic_timeout .. "s", + gc_interval = node.tuic_gc_interval .. "s", + gc_lifetime = node.tuic_gc_lifetime .. "s", + alpn = node.tuic_tls_alpn, + disable_sni = (node.tuic_disable_sni == "1"), + zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1"), + send_window = tonumber(node.tuic_send_window), + receive_window = tonumber(node.tuic_receive_window) + }, + ["local"] = { + server = "[::]:" .. local_port, + username = node.tuic_socks_username, + password = node.tuic_socks_password, + dual_stack = (node.tuic_dual_stack == "1") and true or false, + max_packet_size = tonumber(node.tuic_max_package_size) + }, + log_level = loglevel + } + return json.stringify(tuic, 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..20349b44d --- /dev/null +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -0,0 +1,1824 @@ +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 CACHE_PATH = api.CACHE_PATH + +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" + local fragment = nil + if proxy_table ~= nil and type(proxy_table) == "table" then + proxy = proxy_table.proxy or 0 + proxy_tag = proxy_table.tag or "nil" + fragment = proxy_table.fragment or nil + end + + if node.type == "Xray" then + if node.flow == "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 ~= "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 == "Xray" then + if node.tls and node.tls == "1" then + node.stream_security = "tls" + if 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, + tcpMptcp = (node.tcpMptcp == "1") and true or nil, + tcpNoDelay = (node.tcpNoDelay == "1") and true or nil, + dialerProxy = fragment and "fragment" or nil + }, + 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.utls == "1" 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, + httpupgradeSettings = (node.transport == "httpupgrade") and { + path = node.httpupgrade_path or "/", + host = node.httpupgrade_host + } or nil, + splithttpSettings = (node.transport == "splithttp") and { + path = node.splithttp_path or "/", + host = node.splithttp_host + } 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.flow) and node.flow 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 + } + } + + if node.protocol == "wireguard" then + result.settings.kernelMode = false + end + + 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.flow) and node.flow 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 == "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 = { + mark = 255, + interface = node.outbound_node_iface + } + } + } + sys.call("mkdir -p /tmp/etc/passwall2/iface && touch /tmp/etc/passwall2/iface/" .. 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 { + 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 { + 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, + httpupgradeSettings = (node.transport == "httpupgrade") and { + path = node.httpupgrade_path or "/", + host = node.httpupgrade_host + } or nil, + splithttpSettings = (node.transport == "splithttp") and { + path = node.splithttp_path or "/", + host = node.splithttp_host, + maxUploadSize = node.splithttp_maxuploadsize, + maxConcurrentUploads = node.splithttp_maxconcurrentuploads + } or nil, + sockopt = { + acceptProxyProtocol = (node.acceptProxyProtocol and node.acceptProxyProtocol == "1") and true or false + } + } + } + }, + -- 传出连接 + 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" + if "1" == node.reality then + config.inbounds[1].streamSettings.tlsSettings = nil + config.inbounds[1].streamSettings.security = "reality" + config.inbounds[1].streamSettings.realitySettings = { + show = false, + dest = node.reality_dest, + serverNames = { + node.reality_serverNames + }, + privateKey = node.reality_private_key, + shortIds = node.reality_shortId or "" + } or nil + end + end + + return config +end + +function gen_config(var) + local flag = var["-flag"] + local loglevel = var["-loglevel"] or "warning" + local node_id = var["-node"] + local server_host = var["-server_host"] + local server_port = var["-server_port"] + local tcp_proxy_way = var["-tcp_proxy_way"] + local redir_port = var["-redir_port"] + 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 direct_dns_udp_server = var["-direct_dns_udp_server"] + local direct_dns_udp_port = var["-direct_dns_udp_port"] + local direct_dns_query_strategy = var["-direct_dns_query_strategy"] + local direct_ipset = var["-direct_ipset"] + local direct_nftset = var["-direct_nftset"] + local remote_dns_udp_server = var["-remote_dns_udp_server"] + local remote_dns_udp_port = var["-remote_dns_udp_port"] + local remote_dns_tcp_server = var["-remote_dns_tcp_server"] + local remote_dns_tcp_port = var["-remote_dns_tcp_port"] + local remote_dns_doh_url = var["-remote_dns_doh_url"] + local remote_dns_doh_host = var["-remote_dns_doh_host"] + local remote_dns_doh_ip = var["-remote_dns_doh_ip"] + local remote_dns_doh_port = var["-remote_dns_doh_port"] + local remote_dns_fake = var["-remote_dns_fake"] + local remote_dns_query_strategy = var["-remote_dns_query_strategy"] + local remote_dns_detour = var["-remote_dns_detour"] + local dns_cache = var["-dns_cache"] + + local dns_domain_rules = {} + local dns = nil + local fakedns = nil + local inbounds = {} + local outbounds = {} + local routing = nil + local observatory = nil + + local CACHE_TEXT_FILE = CACHE_PATH .. "/cache_" .. flag .. ".txt" + + local xray_settings = uci:get_all(appname, "@global_xray[0]") or {} + + local node = node_id and uci:get_all(appname, node_id) or nil + local balancers = {} + local rules = {} + + if local_socks_port then + local inbound = { + tag = "socks-in", + listen = local_socks_address, + port = tonumber(local_socks_port), + protocol = "socks", + settings = {auth = "noauth", udp = true}, + sniffing = {enabled = true, destOverride = {"http", "tls", "quic"}} + } + 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 = xray_settings.sniffing_override_dest == "1" or node.protocol == "_shunt", + destOverride = {"http", "tls", "quic", (remote_dns_fake) and "fakedns"}, + metadataOnly = false, + routeOnly = node.protocol == "_shunt" and xray_settings.sniffing_override_dest ~= "1" or nil, + domainsExcluded = xray_settings.sniffing_override_dest == "1" 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_loopback(outboundTag, dst_node_id) + if not outboundTag then return nil end + local inboundTag = dst_node_id and "loop-in-" .. dst_node_id or outboundTag .. "-lo" + table.insert(outbounds, { + protocol = "loopback", + tag = outboundTag, + settings = { inboundTag = inboundTag } + }) + return inboundTag + end + + local function gen_balancer(_node, loopbackTag) + local blc_nodes = _node.balancing_node + local fallback_node_id = _node.fallback_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, { fragment = xray_settings.fragment == "1" or nil }) + if outbound then + table.insert(outbounds, outbound) + valid_nodes[#valid_nodes + 1] = blc_node_tag + end + end + end + if fallback_node_id == "" then fallback_node_id = nil end + if fallback_node_id then + local is_new_node = true + for _, outbound in ipairs(outbounds) do + if outbound.tag == fallback_node_id then + is_new_node = false + break + end + end + if is_new_node then + local fallback_node = uci:get_all(appname, fallback_node_id) + if fallback_node.protocol ~= "_balancing" then + local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil }) + if outbound then + table.insert(outbounds, outbound) + else + fallback_node_id = nil + end + else + local valid = gen_balancer(fallback_node) + if not valid then + fallback_node_id = nil + end + end + end + end + + local valid = nil + if #valid_nodes > 0 then + local balancerTag = get_balancer_tag(_node[".name"]) + table.insert(balancers, { + tag = balancerTag, + selector = valid_nodes, + fallbackTag = fallback_node_id, + strategy = { type = _node.balancingStrategy or "random" } + }) + if _node.balancingStrategy == "leastPing" or fallback_node_id then + if not observatory then + observatory = { + subjectSelector = { "blc-" }, + probeUrl = _node.useCustomProbeUrl and _node.probeUrl or nil, + probeInterval = _node.probeInterval or "1m", + enableConcurrency = true + } + end + end + if loopbackTag == nil or loopbackTag =="" then loopbackTag = _node[".name"] end + local inboundTag = gen_loopback(loopbackTag, _node[".name"]) + table.insert(rules, { type = "field", inboundTag = { inboundTag }, balancerTag = balancerTag }) + valid = true + end + return valid + end + + local function set_outbound_detour(node, outbound, outbounds_table, shunt_rule_name) + if not node or not outbound or not outbounds_table then return nil end + local default_outTag = outbound.tag + + if node.to_node then + local to_node = uci:get_all(appname, node.to_node) + if to_node then + local to_outbound = gen_outbound(nil, to_node) + if to_outbound then + if shunt_rule_name then + to_outbound.tag = outbound.tag + outbound.tag = node[".name"] + else + to_outbound.tag = outbound.tag .. " -> " .. to_outbound.tag + end + + to_outbound.proxySettings = { + tag = outbound.tag, + transportLayer = true + } + table.insert(outbounds_table, to_outbound) + default_outTag = to_outbound.tag + end + end + end + return default_outTag + end + + if node then + if server_host and server_port then + node.address = server_host + node.port = server_port + end + if node.protocol == "_shunt" then + local proxy_tag = "main" + local proxy_node_id = node["main_node"] + local proxy_node = node.preproxy_enabled == "1" and proxy_node_id or nil + local proxy_outboundTag, proxy_balancerTag + + local function gen_shunt_node(rule_name, _node_id) + 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" then + rule_outboundTag = "default" + elseif _node_id:find("Socks_") then + local socks_id = _node_id:sub(1 + #"Socks_") + local socks_node = uci:get_all(appname, socks_id) or nil + if socks_node then + local _node = { + type = "Xray", + protocol = "socks", + address = "127.0.0.1", + port = socks_node.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 + 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_node = (proxy_node and proxy_node_id) and uci:get_all(appname, proxy_node_id) or nil + local use_proxy = _proxy_node and node[rule_name .. "_proxy_tag"] == proxy_tag and _node_id ~= proxy_node_id + if use_proxy and proxy_balancerTag then + for _, blc_node_id in ipairs(_proxy_node.balancing_node) do + if _node_id == blc_node_id then + use_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"] == proxy_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 use_proxy and (_node.type ~= "Xray" or _node.flow == "xtls-rprx-vision") 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 = proxy_outboundTag, + balancerTag = proxy_balancerTag + }) + end + local proxy_table = { + proxy = use_proxy and 1 or 0, + tag = use_proxy and proxy_tag or nil + } + if xray_settings.fragment == "1" and not proxy_table.tag then + proxy_table.fragment = true + end + local outbound = gen_outbound(flag, _node, rule_name, proxy_table) + if outbound then + set_outbound_detour(_node, outbound, outbounds, rule_name) + table.insert(outbounds, outbound) + rule_outboundTag = rule_name + end + end + elseif _node.protocol == "_balancing" then + local is_new_balancer = true + rule_balancerTag = get_balancer_tag(_node_id) + for _, v in ipairs(balancers) do + if v.tag == rule_balancerTag then + is_new_balancer = false + gen_loopback(rule_name, _node_id) + break + end + end + if is_new_balancer then + local valid = gen_balancer(_node, rule_name) + if not valid then + rule_balancerTag = nil + end + end + elseif _node.protocol == "_iface" then + if _node.iface then + local outbound = { + protocol = "freedom", + tag = rule_name, + streamSettings = { + sockopt = { + mark = 255, + interface = _node.iface + } + } + } + table.insert(outbounds, outbound) + rule_outboundTag = rule_name + sys.call("touch /tmp/etc/passwall2/iface/" .. _node.iface) + end + end + end + return rule_outboundTag, rule_balancerTag + end + + --proxy_node + if proxy_node then + proxy_outboundTag, proxy_balancerTag = gen_shunt_node(proxy_tag, proxy_node_id) + if not proxy_outboundTag and not proxy_balancerTag then + proxy_node = nil + end + 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 inboundTag = nil + if e["inbound"] and e["inbound"] ~= "" then + inboundTag = {} + if e["inbound"]:find("tproxy") then + if redir_port then + table.insert(inboundTag, "tcp_redir") + table.insert(inboundTag, "udp_redir") + end + end + if e["inbound"]:find("socks") then + if local_socks_port then + table.insert(inboundTag, "socks-in") + end + end + end + local domains = nil + if e.domain_list then + local domain_table = { + shunt_rule_name = e[".name"], + outboundTag = outboundTag, + domain = {}, + } + domains = {} + string.gsub(e.domain_list, '[^' .. "\r\n" .. ']+', function(w) + if w:find("#") == 1 then return end + table.insert(domains, w) + table.insert(domain_table.domain, w) + end) + if outboundTag and outboundTag ~= "nil" then + table.insert(dns_domain_rules, api.clone(domain_table)) + end + if #domains == 0 then domains = nil end + end + local ip = nil + if e.ip_list then + ip = {} + string.gsub(e.ip_list, '[^' .. "\r\n" .. ']+', function(w) + if w:find("#") == 1 then return end + table.insert(ip, w) + end) + if #ip == 0 then ip = nil 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", + inboundTag = inboundTag, + 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 valid = gen_balancer(node) + if valid then + table.insert(rules, { type = "field", network = "tcp,udp", balancerTag = get_balancer_tag(node_id) }) + end + routing = { + balancers = balancers, + rules = rules + } + end + elseif node.protocol == "_iface" then + if node.iface then + local outbound = { + protocol = "freedom", + tag = "outbound", + streamSettings = { + sockopt = { + mark = 255, + interface = node.iface + } + } + } + table.insert(outbounds, outbound) + sys.call("touch /tmp/etc/passwall2/iface/" .. node.iface) + end + else + local outbound = gen_outbound(flag, node, nil, { fragment = xray_settings.fragment == "1" or nil }) + if outbound then + local default_outTag = set_outbound_detour(node, outbound, outbounds) + table.insert(outbounds, outbound) + routing = { + domainStrategy = "AsIs", + domainMatcher = "hybrid", + rules = {} + } + table.insert(routing.rules, { + _flag = "default", + type = "field", + outboundTag = default_outTag, + network = "tcp,udp" + }) + end + end + end + + if dns_listen_port then + local rules = {} + local _remote_dns_proto = "tcp" + + 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 = "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 + + local _remote_dns_ip = nil + + local _remote_dns = { + _flag = "remote", + queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4" + } + + if remote_dns_udp_server then + _remote_dns.address = remote_dns_udp_server + _remote_dns.port = tonumber(remote_dns_udp_port) or 53 + _remote_dns_proto = "udp" + _remote_dns_ip = remote_dns_udp_server + end + + if remote_dns_tcp_server then + _remote_dns.address = "tcp://" .. remote_dns_tcp_server + _remote_dns.port = tonumber(remote_dns_tcp_port) or 53 + _remote_dns_proto = "tcp" + _remote_dns_ip = remote_dns_tcp_server + end + + if remote_dns_doh_url and remote_dns_doh_host then + if remote_dns_doh_ip and remote_dns_doh_host ~= remote_dns_doh_ip and not api.is_ip(remote_dns_doh_host) then + dns.hosts[remote_dns_doh_host] = remote_dns_doh_ip + end + _remote_dns.address = remote_dns_doh_url + _remote_dns.port = tonumber(remote_dns_doh_port) or 443 + _remote_dns_ip = remote_dns_doh_ip + end + + if _remote_dns.address then + table.insert(dns.servers, _remote_dns) + if remote_dns_detour == "direct" then + table.insert(routing.rules, 1, { + type = "field", + ip = { + _remote_dns_ip + }, + port = _remote_dns.port, + network = _remote_dns_proto, + outboundTag = "direct" + }) + end + end + + local _remote_fakedns = nil + 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_query_strategy == "UseIP" then + table.insert(fakedns, fakedns4) + table.insert(fakedns, fakedns6) + elseif remote_dns_query_strategy == "UseIPv4" then + table.insert(fakedns, fakedns4) + elseif remote_dns_query_strategy == "UseIPv6" then + table.insert(fakedns, fakedns6) + end + _remote_fakedns = { + _flag = "remote_fakedns", + address = "fakedns", + } + table.insert(dns.servers, _remote_fakedns) + end + + local _direct_dns = nil + if direct_dns_udp_server then + local domain = {} + 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(domain, "full:" .. w) + end) + if #domain > 0 then + table.insert(dns_domain_rules, 1, { + outboundTag = "direct", + domain = domain + }) + end + + _direct_dns = { + _flag = "direct", + address = direct_dns_udp_server, + port = tonumber(direct_dns_udp_port) or 53, + queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP", + } + table.insert(routing.rules, 1, { + type = "field", + ip = { + direct_dns_udp_server + }, + port = tonumber(direct_dns_udp_port) or 53, + network = "udp", + outboundTag = "direct" + }) + + 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 = "0.0.0.0", + network = "tcp,udp" + } + }) + local direct_type_dns = { + settings = { + address = direct_dns_udp_server, + port = tonumber(direct_dns_udp_port) or 53, + network = "udp", + nonIPQuery = "skip" + }, + proxySettings = { + tag = "direct" + } + } + local remote_type_dns = { + settings = { + address = remote_dns_udp_server, + port = tonumber(remote_dns_udp_port) or 53, + network = _remote_dns_proto or "tcp", + nonIPQuery = "drop" + } + } + local type_dns = direct_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, + queryStrategy = value.queryStrategy + } + break + end + end + if dns_servers then + table.insert(dns.servers, 1, dns_servers) + end + + --按分流顺序DNS + if dns_domain_rules and #dns_domain_rules > 0 then + for index, value in ipairs(dns_domain_rules) do + if value.outboundTag and value.domain then + local dns_server = nil + if value.outboundTag == "direct" then + dns_server = api.clone(_direct_dns) + else + if remote_dns_fake then + dns_server = api.clone(_remote_fakedns) + else + dns_server = api.clone(_remote_dns) + end + end + dns_server.domains = value.domain + if value.shunt_rule_name then + dns_server["_flag"] = value.shunt_rule_name + end + + if dns_server then + table.insert(dns.servers, dns_server) + end + end + end + 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 + + local content = flag .. node_id .. jsonc.stringify(routing.rules) + if api.cacheFileCompareToLogic(CACHE_TEXT_FILE, content) == false then + --clear ipset/nftset + if direct_ipset then + string.gsub(direct_ipset, '[^' .. "," .. ']+', function(w) + sys.call("ipset -q -F " .. w) + end) + end + if direct_nftset then + string.gsub(direct_nftset, '[^' .. "," .. ']+', function(w) + local split = api.split(w, "#") + if #split > 3 then + local ip_type = split[1] + local family = split[2] + local table_name = split[3] + local set_name = split[4] + sys.call(string.format("nft flush set %s %s %s 2>/dev/null", family, table_name, set_name)) + end + end) + end + 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 = xray_settings.buffer_size and tonumber(xray_settings.buffer_size) or nil, + statsUserUplink = false, + statsUserDownlink = false + } + }, + -- system = { + -- statsInboundUplink = false, + -- statsInboundDownlink = false + -- } + } + } + + if xray_settings.fragment == "1" then + table.insert(outbounds, { + protocol = "freedom", + tag = "fragment", + settings = { + domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP", + fragment = { + packets = (xray_settings.fragment_packets and xray_settings.fragment_packets ~= "") and xray_settings.fragment_packets, + length = (xray_settings.fragment_length and xray_settings.fragment_length ~= "") and xray_settings.fragment_length, + interval = (xray_settings.fragment_interval and xray_settings.fragment_interval ~= "") and xray_settings.fragment_interval + } + }, + streamSettings = { + sockopt = { + mark = 255, + tcpNoDelay = true + } + } + }) + end + + table.insert(outbounds, { + protocol = "freedom", + tag = "direct", + settings = { + domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP" + }, + 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_out_tag = var["-dns_out_tag"] + local dns_client_ip = var["-dns_client_ip"] + local direct_dns_udp_server = var["-direct_dns_udp_server"] + local direct_dns_udp_port = var["-direct_dns_udp_port"] + local direct_dns_tcp_server = var["-direct_dns_tcp_server"] + local direct_dns_tcp_port = var["-direct_dns_tcp_port"] + local direct_dns_doh_url = var["-direct_dns_doh_url"] + local direct_dns_doh_host = var["-direct_dns_doh_host"] + local direct_dns_doh_ip = var["-direct_dns_doh_ip"] + local direct_dns_doh_port = var["-direct_dns_doh_port"] + local direct_dns_query_strategy = var["-direct_dns_query_strategy"] + local remote_dns_udp_server = var["-remote_dns_udp_server"] + local remote_dns_udp_port = var["-remote_dns_udp_port"] + local remote_dns_tcp_server = var["-remote_dns_tcp_server"] + local remote_dns_tcp_port = var["-remote_dns_tcp_port"] + local remote_dns_doh_url = var["-remote_dns_doh_url"] + local remote_dns_doh_host = var["-remote_dns_doh_host"] + local remote_dns_doh_ip = var["-remote_dns_doh_ip"] + local remote_dns_doh_port = var["-remote_dns_doh_port"] + local remote_dns_query_strategy = var["-remote_dns_query_strategy"] + local remote_dns_detour = var["-remote_dns_detour"] + 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 dns_cache = var["-dns_cache"] + local loglevel = var["-loglevel"] or "warning" + + local inbounds = {} + local outbounds = {} + local dns = 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, + } + + local other_type_dns_proto, other_type_dns_server, other_type_dns_port + + if dns_out_tag == "remote" then + dns.queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4" + if remote_dns_detour == "direct" then + dns_out_tag = "direct" + table.insert(outbounds, 1, { + tag = dns_out_tag, + protocol = "freedom", + settings = { + domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP" + }, + streamSettings = { + sockopt = { + mark = 255 + } + } + }) + else + if remote_dns_outbound_socks_address and remote_dns_outbound_socks_port then + table.insert(outbounds, 1, { + tag = dns_out_tag, + protocol = "socks", + streamSettings = { + network = "tcp", + security = "none" + }, + settings = { + servers = { + { + address = remote_dns_outbound_socks_address, + port = tonumber(remote_dns_outbound_socks_port) + } + } + } + }) + end + end + + local _remote_dns = { + _flag = "remote" + } + + if remote_dns_udp_server then + _remote_dns.address = remote_dns_udp_server + _remote_dns.port = tonumber(remote_dns_udp_port) or 53 + + other_type_dns_proto = "udp" + other_type_dns_server = remote_dns_udp_server + other_type_dns_port = _remote_dns.port + end + + if remote_dns_tcp_server then + _remote_dns.address = "tcp://" .. remote_dns_tcp_server + _remote_dns.port = tonumber(remote_dns_tcp_port) or 53 + + other_type_dns_proto = "tcp" + other_type_dns_server = remote_dns_tcp_server + other_type_dns_port = _remote_dns.port + end + + if remote_dns_doh_url and remote_dns_doh_host then + if remote_dns_doh_ip and remote_dns_doh_host ~= remote_dns_doh_ip and not api.is_ip(remote_dns_doh_host) then + dns.hosts[remote_dns_doh_host] = remote_dns_doh_ip + end + _remote_dns.address = remote_dns_doh_url + _remote_dns.port = tonumber(remote_dns_doh_port) or 443 + end + + table.insert(dns.servers, _remote_dns) + elseif dns_out_tag == "direct" then + dns.queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP" + table.insert(outbounds, 1, { + tag = dns_out_tag, + protocol = "freedom", + settings = { + domainStrategy = dns.queryStrategy + }, + streamSettings = { + sockopt = { + mark = 255 + } + } + }) + + local _direct_dns = { + _flag = "direct" + } + + if direct_dns_udp_server then + _direct_dns.address = direct_dns_udp_server + _direct_dns.port = tonumber(direct_dns_udp_port) or 53 + table.insert(routing.rules, 1, { + type = "field", + ip = { + direct_dns_udp_server + }, + port = tonumber(direct_dns_udp_port) or 53, + network = "udp", + outboundTag = "direct" + }) + + other_type_dns_proto = "udp" + other_type_dns_server = direct_dns_udp_server + other_type_dns_port = _direct_dns.port + end + + if direct_dns_tcp_server then + _direct_dns.address = "tcp+local://" .. direct_dns_tcp_server + _direct_dns.port = tonumber(direct_dns_tcp_port) or 53 + + other_type_dns_proto = "tcp" + other_type_dns_server = direct_dns_tcp_server + other_type_dns_port = _direct_dns.port + end + + if direct_dns_doh_url and direct_dns_doh_host then + if direct_dns_doh_ip and direct_dns_doh_host ~= direct_dns_doh_ip and not api.is_ip(direct_dns_doh_host) then + dns.hosts[direct_dns_doh_host] = direct_dns_doh_ip + end + _direct_dns.address = direct_dns_doh_url:gsub("https://", "https+local://") + _direct_dns.port = tonumber(direct_dns_doh_port) or 443 + end + + table.insert(dns.servers, _direct_dns) + 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 = other_type_dns_port or 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", + nonIPQuery = "drop" + } + }) + + 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, + -- 传入连接 + 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/global/faq.htm b/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm new file mode 100644 index 000000000..08ead501f --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm @@ -0,0 +1,74 @@ +<% +local api = require "luci.passwall2.api" +-%> + +
+
+
    + <%:DNS related issues:%> +
  • 1. <%:Certain browsers such as Chrome have built-in DNS service, which may affect DNS resolution settings. You can go to 'Settings -> Privacy and security -> Use secure DNS' menu to turn it off.%>
  • +
  • 2. <%:If you are unable to access the internet after reboot, please try clearing the cache of your terminal devices (make sure to close all open browser application windows first, this step is especially important):%> +
    • <%:For Windows systems, open Command Prompt and run the command 'ipconfig /flushdns'.%>
    • +
    • <%:For Mac systems, open Terminal and run the command 'sudo killall -HUP mDNSResponder'.%>
    • +
    • <%:For mobile devices, you can clear it by reconnecting to the network, such as toggling Airplane Mode and reconnecting to WiFi.%>
    • +
    +
  • +
  • 3. <%:Please make sure your device's network settings point both the DNS server and default gateway to this router, to ensure DNS queries are properly routed.%>
  • +
+
+
+
+ + 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..5b7220ad6 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/global/footer.htm @@ -0,0 +1,142 @@ +<% +local api = require "luci.passwall2.api" +-%> + \ 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..03b2e56d0 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/global/status.htm @@ -0,0 +1,197 @@ +<% +local api = require "luci.passwall2.api" +-%> + + + +
+
+
+
+
+
+ +
+
+
+

Core
<%: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..6a1bec7e7 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/node_list/link_add_node.htm @@ -0,0 +1,110 @@ +<% +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..626cf5ceb --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/node_list/link_share_man.htm @@ -0,0 +1,940 @@ +<%+cbi/valueheader%> +<% +local api = require "luci.passwall2.api" +-%> + + + + + + + +<%+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..5f222ecfe --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/node_list/node_list.htm @@ -0,0 +1,462 @@ +<% +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 == "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/luasrc/view/passwall2/socks_auto_switch/footer.htm b/luci-app-passwall2/luasrc/view/passwall2/socks_auto_switch/footer.htm new file mode 100644 index 000000000..b4f629d41 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/socks_auto_switch/footer.htm @@ -0,0 +1,23 @@ +<% +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..47d50158e --- /dev/null +++ b/luci-app-passwall2/po/zh-cn/passwall2.po @@ -0,0 +1,1514 @@ +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 "Client Proxy" +msgstr "客户端代理" + +msgid "When selected, devices in LAN can transparent proxy. Otherwise, it will not be proxy. But you can still use access control to allow the designated device to 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 "Remote DNS Outbound" +msgstr "远程 DNS 出站" + +msgid "Remote" +msgstr "远程" + +msgid "Direct" +msgstr "直连" + +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 "Direct DNS result write to IPSet" +msgstr "直连 DNS 解析结果写入到 IPSet" + +msgid "Perform the matching direct domain name rules into IP to IPSet/NFTSet, and then connect directly (not entering the core). Maybe conflict with some special circumstances." +msgstr "将匹配到的直连规则的域名解析IP写入到 IPSet/NFTSet,然后直连(不进入内核)。可能和某些特殊情况冲突。" + +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 related issues:" +msgstr "DNS相关问题:" + +msgid "Certain browsers such as Chrome have built-in DNS service, which may affect DNS resolution settings. You can go to 'Settings -> Privacy and security -> Use secure DNS' menu to turn it off." +msgstr "某些浏览器如Chrome等内置此功能,这会影响设置的DNS解析。您可以进入 '设置->私隐和安全->使用安全DNS' 菜单,将其关闭。" + +msgid "If you are unable to access the internet after reboot, please try clearing the cache of your terminal devices (make sure to close all open browser application windows first, this step is especially important):" +msgstr "如果在重启后无法上网,请尝试清除终端设备的缓存(先关闭所有正在使用的浏览器应用程序,这一步尤其重要):" + +msgid "For Windows systems, open Command Prompt and run the command 'ipconfig /flushdns'." +msgstr "对于Windows系统,请在命令提示符中运行命令 'ipconfig /flushdns'." + +msgid "For Mac systems, open Terminal and run the command 'sudo killall -HUP mDNSResponder'." +msgstr "对于Mac系统,在终端中运行命令 'sudo killall -HUP mDNSResponder'." + +msgid "For mobile devices, you can clear it by reconnecting to the network, such as toggling Airplane Mode and reconnecting to WiFi." +msgstr "对于移动设备,可通过重新接入网络的方式清除。比如开关一次飞行模式,重新连接WiFi。" + +msgid "Please make sure your device's network settings point both the DNS server and default gateway to this router, to ensure DNS queries are properly routed." +msgstr "请确认您设备的网络设置,客户端DNS服务器和默认网关应均指向本路由器,以确保DNS查询正确路由。" + +msgid "Restore to default configuration:" +msgstr "恢复默认配置:" + +msgid "Browser access:" +msgstr "浏览器访问:" + +msgid "Hide in main menu:" +msgstr "在主菜单中隐藏:" + +msgid "Show in main menu:" +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 "Share Link" +msgstr "分享链接" + +msgid "Not a subscription link!!!" +msgstr "不是订阅链接!!!" + +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 "_shunt" +msgstr "分流" + +msgid "Balancing" +msgstr "负载均衡" + +msgid "Balancing Strategy" +msgstr "负载均衡策略" + +msgid "Fallback Node" +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 "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 "Generate QRCode" +msgstr "生成二维码" + +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 Type" +msgstr "混淆类型" + +msgid "Obfs Password" +msgstr "混淆密码" + +msgid "Auth Type" +msgstr "认证类型" + +msgid "Auth Password" +msgstr "认证密码" + +msgid "Commands the client to use the BBR flow control algorithm" +msgstr "命令客户端使用 BBR 流量控制算法" + +msgid "PinSHA256" +msgstr "PinSHA256" + +msgid "Certificate fingerprint" +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 "QUIC concurrent bidirectional streams" +msgstr "QUIC 并发双向流的最大数量" + +msgid "Disable MTU detection" +msgstr "禁用 MTU 检测" + +msgid "ignoreClientBandwidth" +msgstr "忽略客户端带宽设置" + +msgid "Lazy Start" +msgstr "延迟启动" + +msgid "Encrypt Method" +msgstr "加密" + +msgid "Latency" +msgstr "延迟" + +msgid "Automatic detection delay" +msgstr "自动检测延迟" + +msgid "Show server address and port" +msgstr "显示服务器地址和端口" + +msgid "URL Test" +msgstr "URL 测试" + +msgid "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 "If you don't want to let the device in the list to go proxy, please choose all." +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" +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 URL 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 "当使用URL测试时,负载均衡节点将转换成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 "Manually update" +msgstr "手动更新" + +msgid "Enable auto update rules" +msgstr "开启自动更新规则" + +msgid "Update Time(every day)" +msgstr "更新时间(每天)" + +msgid "Update Interval(hour)" +msgstr "更新间隔(小时)" + +msgid "Update Mode" +msgstr "更新模式" + +msgid "Loop Mode" +msgstr "循环" + +msgid "Every day" +msgstr "每天" + +msgid "Every Monday" +msgstr "每周一" + +msgid "Every Tuesday" +msgstr "每周二" + +msgid "Every Wednesday" +msgstr "每周三" + +msgid "Every Thursday" +msgstr "每周四" + +msgid "Every Friday" +msgstr "每周五" + +msgid "Every Saturday" +msgstr "每周六" + +msgid "Every Sunday" +msgstr "每周日" + +msgid "hour" +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 "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 "Inbound Tag" +msgstr "入站标签" + +msgid "Transparent proxy" +msgstr "透明代理" + +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 "Annotation: Begining with #" +msgstr "注释: 由 # 开头" + +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 "Clear logs" +msgstr "清空日志" + +msgid "Password" +msgstr "密码" + +msgid "IV Check" +msgstr "IV 检查" + +msgid "UDP over TCP" +msgstr "TCP 封装 UDP" + +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 "Naiveproxy Protocol" +msgstr "Naiveproxy协议" + +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 "System interface" +msgstr "系统接口" + +msgid "System interface name" +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 "%s Node Use Type" +msgstr "%s 节点使用类型" + +msgid "Set the TUIC proxy server ip address" +msgstr "指定远程TUIC服务器IP" + +msgid "TUIC User Password For Connect Remote Server" +msgstr "用于远程TUIC服务器连接的密码" + +msgid "TUIC UserName For Local Socks" +msgstr "用于本地Socks服务器连接的用户名" + +msgid "TUIC Password For Local Socks" +msgstr "用于本地Socks服务器连接的密码" + +msgid "UDP relay mode" +msgstr "UDP中继模式" + +msgid "Congestion control algorithm" +msgstr "拥塞控制算法" + +msgid "Heartbeat interval(second)" +msgstr "保活心跳包发送间隔(单位:秒)" + +msgid "Timeout for establishing a connection to server(second)" +msgstr "连接超时时间(单位:秒)" + +msgid "Garbage collection interval(second)" +msgstr "UDP数据包片残片清理间隔(单位:秒)" + +msgid "Garbage collection lifetime(second)" +msgstr "UDP数据包残片在服务器的保留时间(单位:秒)" + +msgid "Disable SNI" +msgstr "关闭SNI服务器名称指示" + +msgid "Enable 0-RTT QUIC handshake" +msgstr "客户端启用 0-RTT QUIC 连接握手" + +msgid "TUIC send window" +msgstr "发送窗口(无需确认即可发送的最大字节数:默认8Mb*2)" + +msgid "TUIC receive window" +msgstr "接收窗口(无需确认即可接收的最大字节数:默认8Mb)" + +msgid "TUIC Maximum packet size the socks5 server can receive from external, in bytes" +msgstr "TUIC socks5 服务器可以从外部接收的最大数据包大小(以字节为单位)" + +msgid "Set if the listening socket should be dual-stack" +msgstr "设置监听套接字为双栈" + +msgid "
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 "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 localhost." +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 "Enable 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 %s, Can't unzip!" +msgstr "未安装 %s,无法解压。" + +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 "Padding" +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 "Excluded Domains" +msgstr "排除域名" + +msgid "If the traffic sniffing result is in this list, the destination address will not be overridden." +msgstr "如果流量嗅探结果在此列表中,则不会覆盖目标地址。" + +msgid "Buffer Size" +msgstr "缓冲区大小" + +msgid "Buffer size for every connection (kB)" +msgstr "每一个连接的缓冲区大小(kB)" + +msgid "Custom geoip Path" +msgstr "自定义geoip文件路径" + +msgid "Custom geoip URL" +msgstr "自定义geoip文件更新链接" + +msgid "Custom geosite Path" +msgstr "自定义geosite文件路径" + +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 "端口跳跃额外端口" + +msgid "Remove resource files" +msgstr "删除资源文件" + +msgid "Sing-Box will automatically download resource files when starting, you can use this feature achieve upgrade resource files." +msgstr "Sing-Box 会在启动时自动下载资源文件,您可以使用此功能实现升级资源文件。" + +msgid "Override the connection destination address" +msgstr "覆盖连接目标地址" + +msgid "Override the connection destination address with the sniffed domain." +msgstr "用探测出的域名覆盖连接目标地址。" + +msgid "Handshake Server" +msgstr "握手服务器" + +msgid "Handshake Server Port" +msgstr "握手服务器端口" + +msgid "Protocol parameter. Will waste traffic randomly if enabled." +msgstr "协议参数。 如果启用会随机浪费流量。" + +msgid "Protocol parameter. Enable length block encryption." +msgstr "协议参数。启用长度块加密。" + +msgid "ECH Config" +msgstr "ECH 密钥" + +msgid "ECH Key" +msgstr "ECH 配置" + +msgid "PQ signature schemes" +msgstr "后量子对等证书签名方案" + +msgid "Disable adaptive sizing of TLS records" +msgstr "禁用 TLS 记录的自适应大小调整" + +msgid "Enable Multipath TCP, need to be enabled in both server and client configuration." +msgstr "启用 Multipath TCP,需在服务端和客户端配置中同时启用。" + +msgid "Fragment" +msgstr "分片" + +msgid "TCP fragments, which can deceive the censorship system in some cases, such as bypassing SNI blacklists." +msgstr "TCP 分片,在某些情况下可以欺骗审查系统,比如绕过 SNI 黑名单。" + +msgid "Fragment Packets" +msgstr "分片方式" + +msgid " \"1-3\" is for segmentation at TCP layer, applying to the beginning 1 to 3 data writes by the client. \"tlshello\" is for TLS client hello packet fragmentation." +msgstr " \"1-3\" 是 TCP 的流切片,应用于客户端第 1 至第 3 次写数据。\"tlshello\" 是 TLS 握手包切片。" + +msgid "Fragment Length" +msgstr "分片包长" + +msgid "Fragmented packet length (byte)" +msgstr "分片包长 (byte)" + +msgid "Fragment Interval" +msgstr "分片间隔" + +msgid "Fragmentation interval (ms)" +msgstr "分片间隔(ms)" + +msgid "If is domain name, The requested domain name will be resolved to IP before connect." +msgstr "如果是域名,域名将在请求发出之前解析为 IP。" + +msgid "Landing node" +msgstr "落地节点" + +msgid "Only support a layer of proxy." +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..b47c6adb8 --- /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 >/dev/null 2>&1 & + logger -p notice -t network -s "passwall2: restart when $INTERFACE ifup" + + 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..5a3c49829 --- /dev/null +++ b/luci-app-passwall2/root/etc/init.d/passwall2 @@ -0,0 +1,70 @@ +#!/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_func() { + 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 +} + +boot() { + boot_func >/dev/null 2>&1 & +} + +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..cb52b9cc3 --- /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 + [ -e "/etc/config/ucitrack" ] && { + 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 + [ -e "/etc/config/ucitrack" ] && { + 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 + +[ ! -s "/etc/config/passwall2" ] && cp -f /usr/share/passwall2/0_default_config /etc/config/passwall2 + +chmod +x /usr/share/passwall2/*.sh + +[ -e "/etc/config/passwall2_show" ] && rm -rf /etc/config/passwall2_show + +[ "$(uci -q get passwall2.@global_xray[0].sniffing)" == "1" ] && [ "$(uci -q get passwall2.@global_xray[0].route_only)" != "1" ] && uci -q set passwall2.@global_xray[0].sniffing_override_dest=1 +uci -q delete passwall2.@global_xray[0].sniffing +uci -q delete passwall2.@global_xray[0].route_only +uci -q commit passwall2 + +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..5f5b8d049 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/0_default_config @@ -0,0 +1,235 @@ + +config global + option enabled '0' + option node_socks_port '1070' + option localhost_proxy '1' + option client_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 log_node '1' + 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' + +config global_xray + option sniffing_override_dest '0' + +config global_other + option auto_detection_time 'tcping' + option show_node_info '0' + +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 xray_file '/usr/bin/xray' + option hysteria_file '/usr/bin/hysteria' + option singbox_file '/usr/bin/sing-box' + +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 global_singbox + option sniff_override_destination '0' + option geoip_path '/usr/share/singbox/geoip.db' + option geoip_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.db' + option geosite_path '/usr/share/singbox/geosite.db' + option geosite_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.db' + +config nodes 'myshunt' + option remarks '分流总节点' + option type 'Xray' + option protocol '_shunt' + option DirectGame '_direct' + option ProxyGame '_default' + option Direct '_direct' + option GooglePlay '_default' + option Netflix 'nil' + option OpenAI '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 +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 'GooglePlay' + option remarks 'GooglePlay' + option network 'tcp,udp' + option domain_list 'domain:googleapis.cn +domain:googleapis.com +domain:xn--ngstr-lra8j.com' + +config shunt_rules 'Netflix' + option remarks 'Netflix' + option network 'tcp,udp' + option domain_list 'geosite:netflix' + +config shunt_rules 'OpenAI' + option remarks 'OpenAI' + option network 'tcp,udp' + option domain_list 'geosite:openai' + +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 '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 100644 index 000000000..92c98d128 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -0,0 +1,1202 @@ +#!/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_ROUTE_PATH=$TMP_PATH/route +TMP_ACL_PATH=$TMP_PATH/acl +TMP_IFACE_PATH=$TMP_PATH/iface +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= +ENABLED_DEFAULT_ACL=0 +ENABLED_ACLS=0 +PROXY_IPV6=0 +PROXY_IPV6_UDP=0 +LUA_UTIL_PATH=/usr/lib/lua/luci/passwall2 +UTIL_SINGBOX=$LUA_UTIL_PATH/util_sing-box.lua +UTIL_SS=$LUA_UTIL_PATH/util_shadowsocks.lua +UTIL_XRAY=$LUA_UTIL_PATH/util_xray.lua +UTIL_NAIVE=$LUA_UTIL_PATH/util_naiveproxy.lua +UTIL_HYSTERIA2=$LUA_UTIL_PATH/util_hysteria2.lua +UTIL_TUIC=$LUA_UTIL_PATH/util_tuic.lua +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}}" +} + +config_t_set() { + local index=${4:-0} + local ret=$(uci -q set "${CONFIG}.@${1}[${index}].${2}=${3}" 2>/dev/null) +} + +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}" +} + +check_depends() { + local depends + local tables=${1} + if [ "$tables" == "iptables" ]; then + for depends in "iptables-mod-tproxy" "iptables-mod-socket" "iptables-mod-iprange" "iptables-mod-conntrack-extra" "kmod-ipt-nat"; do + [ -s "/usr/lib/opkg/info/${depends}.control" ] || echolog "$tables透明代理基础依赖 $depends 未安装..." + done + else + for depends in "kmod-nft-socket" "kmod-nft-tproxy" "kmod-nft-nat"; do + [ -s "/usr/lib/opkg/info/${depends}.control" ] || echolog "$tables透明代理基础依赖 $depends 未安装..." + done + fi +} + +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_xray() { + 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 remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_dns_detour remote_fakedns remote_dns_query_strategy dns_cache write_ipset_direct + 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" != "xray" ]; then + local bin=$(first_type $(config_t_get global_app xray_file) xray) + [ -n "$bin" ] && type="xray" + 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" + + [ -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}" + + local _dns=$(get_first_dns AUTO_DNS 53 | sed 's/#/:/g') + local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') + local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') + + DIRECT_DNS_UDP_SERVER=${_dns_address} + DIRECT_DNS_UDP_PORT=${_dns_port} + + [ "${write_ipset_direct}" = "1" ] && { + direct_dnsmasq_listen_port=$(get_new_port $(expr $dns_listen_port + 1) udp) + local set_flag="${flag}" + local direct_ipset_conf=$TMP_PATH/dnsmasq_${flag}_direct.conf + [ -n "$(echo ${flag} | grep '^acl')" ] && { + direct_ipset_conf=${TMP_ACL_PATH}/${sid}/dnsmasq_${flag}_direct.conf + set_flag=$(echo ${flag} | awk -F '_' '{print $2}') + } + if [ "${nftflag}" = "1" ]; then + local direct_nftset="4#inet#fw4#passwall2_${set_flag}_whitelist,6#inet#fw4#passwall2_${set_flag}_whitelist6" + else + local direct_ipset="passwall2_${set_flag}_whitelist,passwall2_${set_flag}_whitelist6" + fi + run_ipset_dnsmasq listen_port=${direct_dnsmasq_listen_port} server_dns=${AUTO_DNS} ipset="${direct_ipset}" nftset="${direct_nftset}" config_file=${direct_ipset_conf} + DIRECT_DNS_UDP_PORT=${direct_dnsmasq_listen_port} + DIRECT_DNS_UDP_SERVER="127.0.0.1" + [ -n "${direct_ipset}" ] && _extra_param="${_extra_param} -direct_ipset ${direct_ipset}" + [ -n "${direct_nftset}" ] && _extra_param="${_extra_param} -direct_nftset ${direct_nftset}" + } + _extra_param="${_extra_param} -direct_dns_udp_port ${DIRECT_DNS_UDP_PORT} -direct_dns_udp_server ${DIRECT_DNS_UDP_SERVER} -direct_dns_query_strategy UseIP" + + DNS_REMOTE_ARGS="" + 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}') + DNS_REMOTE_ARGS="-remote_dns_udp_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}') + DNS_REMOTE_ARGS="-remote_dns_tcp_port ${_dns_port} -remote_dns_tcp_server ${_dns_address}" + ;; + 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} + DNS_REMOTE_ARGS="-remote_dns_doh_port ${_doh_port} -remote_dns_doh_url ${_doh_url} -remote_dns_doh_host ${_doh_host}" + [ -n "$_doh_bootstrap" ] && DNS_REMOTE_ARGS="${DNS_REMOTE_ARGS} -remote_dns_doh_ip ${_doh_bootstrap}" + ;; + esac + [ -n "$remote_dns_detour" ] && DNS_REMOTE_ARGS="${DNS_REMOTE_ARGS} -remote_dns_detour ${remote_dns_detour}" + [ -n "$remote_dns_query_strategy" ] && DNS_REMOTE_ARGS="${DNS_REMOTE_ARGS} -remote_dns_query_strategy ${remote_dns_query_strategy}" + [ -n "$remote_dns_client_ip" ] && DNS_REMOTE_ARGS="${DNS_REMOTE_ARGS} -dns_client_ip ${remote_dns_client_ip}" + [ "$remote_fakedns" = "1" ] && _extra_param="${_extra_param} -remote_dns_fake 1 -remote_dns_fake_strategy ${remote_dns_query_strategy}" + + local independent_dns + if [ -z "${independent_dns}" ]; then + _extra_param="${_extra_param} ${DNS_REMOTE_ARGS}" + else + dns_remote_listen_port=$(get_new_port $(expr ${direct_dnsmasq_listen_port:-${dns_listen_port}} + 1) udp) + 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" + DNS_REMOTE_ARGS="${DNS_REMOTE_ARGS} -dns_out_tag remote -dns_listen_port ${dns_remote_listen_port} -remote_dns_outbound_socks_address 127.0.0.1 -remote_dns_outbound_socks_port ${socks_port}" + + lua $UTIL_XRAY gen_dns_config ${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" + _extra_param="${_extra_param} -remote_dns_udp_port ${dns_remote_listen_port} -remote_dns_udp_server 127.0.0.1 -remote_dns_query_strategy ${remote_dns_query_strategy}" + fi + } + + 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_singbox() { + 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 remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_detour remote_fakedns remote_dns_query_strategy dns_cache write_ipset_direct + 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') + [ -z "$type" ] && return 1 + [ -n "$log_file" ] || local log_file="/dev/null" + _extra_param="${_extra_param} -log 1 -logfile ${log_file}" + if [ "$log_file" = "/dev/null" ]; then + _extra_param="${_extra_param} -log 0" + else + _extra_param="${_extra_param} -log 1 -logfile ${log_file}" + fi + [ -z "$loglevel" ] && local loglevel=$(config_t_get global loglevel "warn") + [ "$loglevel" = "warning" ] && loglevel="warn" + _extra_param="${_extra_param} -loglevel $loglevel" + + _extra_param="${_extra_param} -tags $($(first_type $(config_t_get global_app singbox_file) sing-box) version | grep 'Tags:' | awk '{print $2}')" + + [ -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" + + [ -n "$dns_listen_port" ] && { + local _dns=$(get_first_dns AUTO_DNS 53 | sed 's/#/:/g') + local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') + local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') + + DIRECT_DNS_UDP_SERVER=${_dns_address} + DIRECT_DNS_UDP_PORT=${_dns_port} + + [ "${write_ipset_direct}" = "1" ] && { + direct_dnsmasq_listen_port=$(get_new_port $(expr $dns_listen_port + 1) udp) + local set_flag="${flag}" + local direct_ipset_conf=$TMP_PATH/dnsmasq_${flag}_direct.conf + [ -n "$(echo ${flag} | grep '^acl')" ] && { + direct_ipset_conf=${TMP_ACL_PATH}/${sid}/dnsmasq_${flag}_direct.conf + set_flag=$(echo ${flag} | awk -F '_' '{print $2}') + } + if [ "${nftflag}" = "1" ]; then + local direct_nftset="4#inet#fw4#passwall2_${set_flag}_whitelist,6#inet#fw4#passwall2_${set_flag}_whitelist6" + else + local direct_ipset="passwall2_${set_flag}_whitelist,passwall2_${set_flag}_whitelist6" + fi + run_ipset_dnsmasq listen_port=${direct_dnsmasq_listen_port} server_dns=${AUTO_DNS} ipset="${direct_ipset}" nftset="${direct_nftset}" config_file=${direct_ipset_conf} + DIRECT_DNS_UDP_PORT=${direct_dnsmasq_listen_port} + DIRECT_DNS_UDP_SERVER="127.0.0.1" + [ -n "${direct_ipset}" ] && _extra_param="${_extra_param} -direct_ipset ${direct_ipset}" + [ -n "${direct_nftset}" ] && _extra_param="${_extra_param} -direct_nftset ${direct_nftset}" + } + _extra_param="${_extra_param} -direct_dns_udp_port ${DIRECT_DNS_UDP_PORT} -direct_dns_udp_server ${DIRECT_DNS_UDP_SERVER} -direct_dns_query_strategy UseIP" + + 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}') + _extra_param="${_extra_param} -remote_dns_udp_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}') + _extra_param="${_extra_param} -remote_dns_tcp_port ${_dns_port} -remote_dns_tcp_server ${_dns_address}" + ;; + 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" ] && _extra_param="${_extra_param} -remote_dns_doh_ip ${_doh_bootstrap}" + _extra_param="${_extra_param} -remote_dns_doh_port ${_doh_port} -remote_dns_doh_url ${_doh_url} -remote_dns_doh_host ${_doh_host}" + ;; + esac + + [ -n "$remote_dns_detour" ] && _extra_param="${_extra_param} -remote_dns_detour ${remote_dns_detour}" + [ -n "$remote_dns_query_strategy" ] && _extra_param="${_extra_param} -remote_dns_query_strategy ${remote_dns_query_strategy}" + + [ -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}" + [ "$remote_fakedns" = "1" ] && _extra_param="${_extra_param} -remote_dns_fake 1" + } + + lua $UTIL_SINGBOX gen_config -node $node -redir_port $redir_port -tcp_proxy_way $tcp_proxy_way ${_extra_param} > $config_file + ln_run "$(first_type $(config_t_get global_app singbox_file) sing-box)" "sing-box" "${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" == "sing-box" ] || [ "$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 + sing-box) + [ "$http_port" != "0" ] && { + http_flag=1 + config_file=$(echo $config_file | sed "s/SOCKS/HTTP_SOCKS/g") + local _extra_param="-local_http_address $bind -local_http_port $http_port" + } + [ -n "$relay_port" ] && _extra_param="${_extra_param} -server_host $server_host -server_port $port" + [ "${log_file}" != "/dev/null" ] && { + local loglevel=$(config_t_get global loglevel "warn") + [ "$loglevel" = "warning" ] && loglevel="warn" + _extra_param="${_extra_param} -log 1 -loglevel $loglevel -logfile $log_file" + } + lua $UTIL_SINGBOX gen_config -flag SOCKS_$flag -node $node -local_socks_address $bind -local_socks_port $socks_port ${_extra_param} > $config_file + ln_run "$(first_type $(config_t_get global_app singbox_file) sing-box)" "sing-box" /dev/null run -c "$config_file" + ;; + xray) + [ "$http_port" != "0" ] && { + http_flag=1 + config_file=$(echo $config_file | sed "s/SOCKS/HTTP_SOCKS/g") + local _extra_param="-local_http_address $bind -local_http_port $http_port" + } + [ -n "$relay_port" ] && _extra_param="${_extra_param} -server_host $server_host -server_port $port" + lua $UTIL_XRAY gen_config -flag SOCKS_$flag -node $node -local_socks_address $bind -local_socks_port $socks_port ${_extra_param} > $config_file + ln_run "$(first_type $(config_t_get global_app xray_file) xray)" "xray" $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" + ;; + ssr) + lua $UTIL_SS gen_config -node $node -local_addr $bind -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 $bind -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_address $bind -local_http_port $http_port" + } + lua $UTIL_SS gen_config -node $node -local_socks_address $bind -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 + ;; + hysteria2) + [ "$http_port" != "0" ] && { + http_flag=1 + config_file=$(echo $config_file | sed "s/SOCKS/HTTP_SOCKS/g") + local _extra_param="-local_http_address $bind -local_http_port $http_port" + } + lua $UTIL_HYSTERIA2 gen_config -node $node -local_socks_address $bind -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 + ;; + tuic) + lua $UTIL_TUIC gen_config -node $node -local_addr $bind -local_port $socks_port -server_host $server_host -server_port $port > $config_file + ln_run "$(first_type tuic-client)" "tuic-client" $log_file -c "$config_file" + ;; + esac + + # http to socks + [ -z "$http_flag" ] && [ "$http_port" != "0" ] && [ -n "$http_config_file" ] && [ "$type" != "sing-box" ] && [ "$type" != "xray" ] && [ "$type" != "socks" ] && { + local bin=$(first_type $(config_t_get global_app singbox_file) sing-box) + if [ -n "$bin" ]; then + type="sing-box" + lua $UTIL_SINGBOX 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" + else + bin=$(first_type $(config_t_get global_app xray_file) xray) + [ -n "$bin" ] && type="xray" + [ -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" + fi + } + unset http_flag +} + +socks_node_switch() { + local flag new_node + 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/SOCKS_${flag}* + rm -rf $TMP_PATH/HTTP2SOCKS_${flag}* + + for filename in $(ls ${TMP_SCRIPT_FUNC_PATH}); do + cmd=$(cat ${TMP_SCRIPT_FUNC_PATH}/${filename}) + [ -n "$(echo $cmd | grep "${flag}")" ] && rm -f ${TMP_SCRIPT_FUNC_PATH}/${filename} + done + local bind_local=$(config_n_get $flag bind_local 0) + local bind="0.0.0.0" + [ "$bind_local" = "1" ] && bind="127.0.0.1" + local port=$(config_n_get $flag port) + local config_file="SOCKS_${flag}.json" + local log_file="SOCKS_${flag}.log" + local log=$(config_n_get $flag log 1) + [ "$log" == "0" ] && log_file="" + local http_port=$(config_n_get $flag http_port 0) + local http_config_file="HTTP2SOCKS_${flag}.json" + LOG_FILE="/dev/null" + run_socks flag=$flag node=$new_node bind=$bind socks_port=$port config_file=$config_file http_port=$http_port http_config_file=$http_config_file log_file=$log_file + echo $new_node > $TMP_ID_PATH/socks_${flag} + } +} + +run_global() { + [ "$NODE" = "nil" ] && return 1 + TYPE=$(echo $(config_n_get $NODE type nil) | tr 'A-Z' 'a-z') + [ "$TYPE" = "nil" ] && return 1 + mkdir -p $TMP_ACL_PATH/default + echo $NODE > $TMP_ACL_PATH/default/global.id + + 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} remote_dns_query_strategy=${REMOTE_DNS_QUERY_STRATEGY} dns_cache=${DNS_CACHE}" + local msg="${TUN_DNS} (直连DNS:${AUTO_DNS}" + + [ -n "$REMOTE_DNS_PROTOCOL" ] && { + V2RAY_ARGS="${V2RAY_ARGS} remote_dns_protocol=${REMOTE_DNS_PROTOCOL} remote_dns_detour=${REMOTE_DNS_DETOUR}" + 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_ACL_PATH/default/global.json + V2RAY_LOG=$TMP_ACL_PATH/default/global.log + [ "$(config_t_get global log_node 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) + node_socks_bind_local=$(config_t_get global node_socks_bind_local 1) + node_socks_bind="127.0.0.1" + [ "${node_socks_bind_local}" != "1" ] && node_socks_bind="0.0.0.0" + V2RAY_ARGS="${V2RAY_ARGS} socks_address=${node_socks_bind} socks_port=${node_socks_port}" + echo "127.0.0.1:$node_socks_port" > $TMP_ACL_PATH/default/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}" + + V2RAY_ARGS="${V2RAY_ARGS} write_ipset_direct=${WRITE_IPSET_DIRECT}" + + local run_func + [ -n "${XRAY_BIN}" ] && run_func="run_xray" + [ -n "${SINGBOX_BIN}" ] && run_func="run_singbox" + if [ "${TYPE}" = "xray" ] && [ -n "${XRAY_BIN}" ]; then + run_func="run_xray" + elif [ "${TYPE}" = "sing-box" ] && [ -n "${SINGBOX_BIN}" ]; then + run_func="run_singbox" + fi + + ${run_func} $V2RAY_ARGS +} + +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 bind_local=$(config_n_get $id bind_local 0) + local bind="0.0.0.0" + [ "$bind_local" = "1" ] && bind="127.0.0.1" + local port=$(config_n_get $id port) + local config_file="SOCKS_${id}.json" + local log_file="SOCKS_${id}.log" + local log=$(config_n_get $id log 1) + [ "$log" == "0" ] && log_file="" + local http_port=$(config_n_get $id http_port 0) + local http_config_file="HTTP2SOCKS_${id}.json" + run_socks flag=$id node=$node bind=$bind socks_port=$port config_file=$config_file http_port=$http_port http_config_file=$http_config_file log_file=$log_file + echo $node > $TMP_ID_PATH/socks_${id} + + #自动切换逻辑 + local enable_autoswitch=$(config_n_get $id enable_autoswitch 0) + [ "$enable_autoswitch" = "1" ] && $APP_PATH/socks_auto_switch.sh ${id} > /dev/null 2>&1 & + done + } + } +} + +clean_log() { + logsnum=$(cat $LOG_FILE 2>/dev/null | wc -l) + [ "$logsnum" -gt 1000 ] && { + echo "" > $LOG_FILE + echolog "日志文件过长,清空处理!" + } +} + +clean_crontab() { + [ -f "/tmp/lock/${CONFIG}_cron.lock" ] && return + 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 + + pgrep -af "${CONFIG}/" | awk '/tasks\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 + rm -rf /tmp/lock/${CONFIG}_tasks.lock +} + +start_crontab() { + 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 & + fi + + [ -f "/tmp/lock/${CONFIG}_cron.lock" ] && { + rm -rf "/tmp/lock/${CONFIG}_cron.lock" + echolog "当前为计划任务自动运行,不重新配置定时任务。" + return + } + + 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 * * *" + if [ "$weekupdate" = "8" ]; then + update_loop=1 + else + echo "$t lua $APP_PATH/rule_update.lua log all cron > /dev/null 2>&1 &" >>/etc/crontabs/root + fi + 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}') + cfgids=$(echo -n $(cat ${TMP_SUB_PATH}/${name}) | sed 's# #,#g') + local t="0 $time_update * * $week_update" + [ "$week_update" = "7" ] && t="0 $time_update * * *" + if [ "$week_update" = "8" ]; then + update_loop=1 + else + echo "$t lua $APP_PATH/subscribe.lua start $cfgids cron > /dev/null 2>&1 &" >>/etc/crontabs/root + fi + done + rm -rf $TMP_SUB_PATH + } + + if [ "$ENABLED_DEFAULT_ACL" == 1 ] || [ "$ENABLED_ACLS" == 1 ]; then + [ "$update_loop" = "1" ] && { + $APP_PATH/tasks.sh > /dev/null 2>&1 & + echolog "自动更新:启动循环更新进程。" + } + else + echolog "运行于非代理模式,仅允许服务启停的定时任务。" + fi + + /etc/init.d/cron restart +} + +stop_crontab() { + [ -f "/tmp/lock/${CONFIG}_cron.lock" ] && return + 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} + no-poll + no-resolv + cache-size=${cache_size:-0} + dns-forward-max=${dns_forward_max:-1000} + EOF + for i in $(echo ${server_dns} | sed "s#,# #g"); do + echo "server=${i}" >> $config_file + done + [ -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 + for item in $items; do + index=$(expr $index + 1) + local enabled sid remarks sources node remote_dns_protocol remote_dns remote_dns_doh remote_dns_client_ip remote_dns_detour 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} + 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_dns_detour=${remote_dns_detour:-remote} + remote_fakedns=${remote_fakedns:-0} + remote_dns_query_strategy=${remote_dns_query_strategy:-UseIPv4} + + write_ipset_direct=${write_ipset_direct:-1} + + [ "$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)) + local run_func + [ -n "${XRAY_BIN}" ] && run_func="run_xray" + [ -n "${SINGBOX_BIN}" ] && run_func="run_singbox" + if [ "${type}" = "xray" ] && [ -n "${XRAY_BIN}" ]; then + run_func="run_xray" + elif [ "${type}" = "sing-box" ] && [ -n "${SINGBOX_BIN}" ]; then + run_func="run_singbox" + fi + ${run_func} 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_query_strategy=UseIP 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_dns_detour=${remote_dns_detour} remote_fakedns=${remote_fakedns} remote_dns_query_strategy=${remote_dns_query_strategy} write_ipset_direct=${write_ipset_direct} 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 + [ -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 remote_dns_protocol remote_dns remote_dns_doh remote_dns_client_ip remote_dns_detour 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" == 0 ]; then + if [ -n "$(command -v iptables-legacy || command -v iptables)" ] && [ -n "$(command -v ipset)" ] && [ -n "$(dnsmasq --version | grep 'Compile time options:.* ipset')" ]; then + USE_TABLES="iptables" + else + echolog "系统未安装iptables或ipset或Dnsmasq没有开启ipset支持,无法使用iptables+ipset透明代理!" + if [ -n "$(command -v fw4)" ] && [ -n "$(command -v nft)" ] && [ -n "$(dnsmasq --version | grep 'Compile time options:.* nftset')" ]; then + echolog "检测到fw4,使用nftables进行透明代理。" + USE_TABLES="nftables" + nftflag=1 + config_t_set global_forwarding use_nft 1 + uci commit ${CONFIG} + fi + fi + else + if [ -n "$(dnsmasq --version | grep 'Compile time options:.* nftset')" ]; then + USE_TABLES="nftables" + nftflag=1 + else + echolog "Dnsmasq软件包不满足nftables透明代理要求,如需使用请确保dnsmasq版本在2.87以上并开启nftset支持。" + fi + fi + + check_depends $USE_TABLES + + [ "$USE_TABLES" = "nftables" ] && { + dnsmasq_version=$(dnsmasq -v | grep -i "Dnsmasq version " | awk '{print $3}') + [ "$(expr $dnsmasq_version \>= 2.90)" == 0 ] && echolog "Dnsmasq版本低于2.90,建议升级至2.90及以上版本以避免部分情况下Dnsmasq崩溃问题!" + } + + [ "$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|tasks\.sh/{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}_socks_auto_switch* + 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') +CLIENT_PROXY=$(config_t_get global client_proxy '1') +REMOTE_DNS_PROTOCOL=$(config_t_get global remote_dns_protocol tcp) +REMOTE_DNS_DETOUR=$(config_t_get global remote_dns_detour remote) +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) +WRITE_IPSET_DIRECT=$(config_t_get global write_ipset_direct 1) +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_DNSMASQ_CFGID=$(uci show dhcp.@dnsmasq[0] | awk -F '.' '{print $2}' | awk -F '=' '{print $1}'| head -1) +DEFAULT_DNS=$(uci show dhcp.@dnsmasq[0] | 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) + +XRAY_BIN=$(first_type $(config_t_get global_app xray_file) xray) +SINGBOX_BIN=$(first_type $(config_t_get global_app singbox_file) sing-box) + +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_ROUTE_PATH $TMP_ACL_PATH $TMP_IFACE_PATH $TMP_PATH2 + +arg1=$1 +shift +case $arg1 in +add_ip2route) + add_ip2route $@ + ;; +get_new_port) + get_new_port $@ + ;; +run_socks) + run_socks $@ + ;; +socks_node_switch) + socks_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..6eb3fd673 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua @@ -0,0 +1,222 @@ +#!/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" +local bind_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0" +local bind_address = "0.0.0.0" +if bind_local == "1" then bind_address = "127.0.0.1" end + +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(" + 入口 %s:%s" % {bind_address, port}) + + f_out:write("\n" .. string.format([[ +listen %s + bind %s:%s + mode tcp + balance roundrobin +]], port, bind_address, 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..6a4e6be1b --- /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.@dnsmasq[0] | 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 | grep -v "engage.cloudflareclient.com" | 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..638d07599 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh @@ -0,0 +1,1018 @@ +#!/bin/sh + +DIR="$(cd "$(dirname "$0")" && pwd)" +MY_PATH=$DIR/iptables.sh +IPSET_LANLIST="passwall2_lanlist" +IPSET_VPSLIST="passwall2_vpslist" + +IPSET_LANLIST6="passwall2_lanlist6" +IPSET_VPSLIST6="passwall2_vpslist6" + +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 default_index="${1}"; shift + default_index=${default_index:-0} + local _index=$($ipt_tmp -n -L $chain --line-numbers 2>/dev/null | grep "$keyword" | head -n 1 | awk '{print $1}') + if [ -z "${_index}" ] && [ "${default_index}" = "0" ]; then + $ipt_tmp -A $chain $rule + else + if [ -z "${_index}" ]; then + _index=${default_index} + fi + $ipt_tmp -I $chain $_index $rule + fi +} + +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 default_index="${1}"; shift + default_index=${default_index:-0} + local _index=$($ipt_tmp -n -L $chain --line-numbers 2>/dev/null | grep "$keyword" | awk 'END {print}' | awk '{print $1}') + if [ -z "${_index}" ] && [ "${default_index}" = "0" ]; then + $ipt_tmp -A $chain $rule + else + if [ -n "${_index}" ]; then + _index=$((_index + 1)) + else + _index=${default_index} + fi + $ipt_tmp -I $chain $_index $rule + fi +} + +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}' | grep -v 'default'); 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) + + write_ipset_direct=${write_ipset_direct:-1} + [ "${write_ipset_direct}" = "1" ] && { + if [ "$node" = "default" ]; then + local ipset_whitelist=${ipset_global_whitelist} + local ipset_whitelist6=${ipset_global_whitelist6} + else + local ipset_whitelist="passwall2_${sid}_whitelist" + local ipset_whitelist6="passwall2_${sid}_whitelist6" + ipset -! create $ipset_whitelist nethash maxelem 1048576 + ipset -! create $ipset_whitelist6 nethash family inet6 maxelem 1048576 + fi + } + + 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 + + [ "$tcp_no_redir_ports" != "disable" ] && { + if [ "$tcp_no_redir_ports" != "1:65535" ]; then + $ip6t_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p tcp -m multiport --dport $tcp_no_redir_ports -j RETURN 2>/dev/null + $ipt_tmp -A PSW2 $(comment "$remarks") ${_ipt_source} -p tcp -m multiport --dport $tcp_no_redir_ports -j RETURN + echolog " - ${msg}不代理 TCP 端口[${tcp_no_redir_ports}]" + else + #结束时会return,无需加多余的规则。 + tcp_proxy_mode="disable" + echolog " - ${msg}不代理所有 TCP" + fi + } + + [ "$udp_no_redir_ports" != "disable" ] && { + if [ "$udp_no_redir_ports" != "1:65535" ]; then + $ip6t_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p udp -m multiport --dport $udp_no_redir_ports -j RETURN 2>/dev/null + $ipt_m -A PSW2 $(comment "$remarks") ${_ipt_source} -p udp -m multiport --dport $udp_no_redir_ports -j RETURN + echolog " - ${msg}不代理 UDP 端口[${udp_no_redir_ports}]" + else + #结束时会return,无需加多余的规则。 + udp_proxy_mode="disable" + echolog " - ${msg}不代理所有 UDP" + fi + } + + [ "$tcp_proxy_mode" != "disable" ] && [ -n "$redir_port" ] && { + [ -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]" + if [ -n "${is_tproxy}" ]; then + msg2="${msg2}(TPROXY:${redir_port})" + else + msg2="${msg2}(REDIRECT:${redir_port})" + fi + + [ "${write_ipset_direct}" = "1" ] && $ipt_tmp -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(dst $ipset_whitelist) ! -d $FAKE_IP -j RETURN + + [ "$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 + } + + 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" ] && { + [ "${write_ipset_direct}" = "1" ] && $ip6t_m -A PSW2 $(comment "$remarks") -p tcp ${_ipt_source} $(dst $ipset_whitelist6) ! -d $FAKE_IP_6 -j RETURN + $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 + } + 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 + + [ "$udp_proxy_mode" != "disable" ] && [ -n "$redir_port" ] && { + msg2="${msg}使用 UDP 节点[$node_remark](TPROXY:${redir_port})" + + [ "${write_ipset_direct}" = "1" ] && $ipt_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(dst $ipset_whitelist) ! -d $FAKE_IP -j RETURN + $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" ] && { + [ "${write_ipset_direct}" = "1" ] && $ip6t_m -A PSW2 $(comment "$remarks") -p udp ${_ipt_source} $(dst $ipset_whitelist6) ! -d $FAKE_IP_6 -j RETURN + $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 + } + 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 ] && [ "$CLIENT_PROXY" == 1 ] && { + # 加载默认代理模式 + msg="【默认】," + local ipt_tmp=$ipt_n + [ -n "${is_tproxy}" ] && ipt_tmp=$ipt_m + + [ "$TCP_NO_REDIR_PORTS" != "disable" ] && { + $ip6t_m -A PSW2 $(comment "默认") -p tcp -m multiport --dport $TCP_NO_REDIR_PORTS -j RETURN + $ipt_tmp -A PSW2 $(comment "默认") -p tcp -m multiport --dport $TCP_NO_REDIR_PORTS -j RETURN + if [ "$TCP_NO_REDIR_PORTS" != "1:65535" ]; then + echolog " - ${msg}不代理 TCP 端口[${TCP_NO_REDIR_PORTS}]" + else + TCP_PROXY_MODE="disable" + echolog " - ${msg}不代理所有 TCP 端口" + fi + } + + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && { + $ip6t_m -A PSW2 $(comment "默认") -p udp -m multiport --dport $UDP_NO_REDIR_PORTS -j RETURN + $ipt_m -A PSW2 $(comment "默认") -p udp -m multiport --dport $UDP_NO_REDIR_PORTS -j RETURN + if [ "$UDP_NO_REDIR_PORTS" != "1:65535" ]; then + echolog " - ${msg}不代理 UDP 端口[${UDP_NO_REDIR_PORTS}]" + else + UDP_PROXY_MODE="disable" + echolog " - ${msg}不代理所有 UDP 端口" + fi + } + + if [ "$TCP_PROXY_MODE" != "disable" ] && [ "$NODE" != "nil" ]; then + msg2="${msg}使用 TCP 节点[$(config_n_get $NODE remarks)]" + if [ -n "${is_tproxy}" ]; then + msg2="${msg2}(TPROXY:${REDIR_PORT})" + else + msg2="${msg2}(REDIRECT:${REDIR_PORT})" + fi + + [ "${WRITE_IPSET_DIRECT}" = "1" ] && $ipt_tmp -A PSW2 $(comment "默认") -p tcp $(dst $ipset_global_whitelist) ! -d $FAKE_IP -j RETURN + + [ "$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 [ -z "${is_tproxy}" ]; 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" ] && { + [ "${WRITE_IPSET_DIRECT}" = "1" ] && $ip6t_m -A PSW2 $(comment "默认") -p tcp $(dst $ipset_global_whitelist6) ! -d $FAKE_IP_6 -j RETURN + $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 "${msg2}" + fi + + if [ "$UDP_PROXY_MODE" != "disable" ] && [ "$NODE" != "nil" ]; then + msg2="${msg}使用 UDP 节点[$(config_n_get $NODE remarks)](TPROXY:${REDIR_PORT})" + + [ "${WRITE_IPSET_DIRECT}" = "1" ] && $ipt_m -A PSW2 $(comment "默认") -p udp $(dst $ipset_global_whitelist) ! -d $FAKE_IP -j RETURN + $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 + [ "${WRITE_IPSET_DIRECT}" = "1" ] && $ip6t_m -A PSW2 $(comment "默认") -p udp $(dst $ipset_global_whitelist6) ! -d $FAKE_IP_6 -j RETURN + $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 "${msg2}" + 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" ] && 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 & + 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 + + 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 + [ "${WRITE_IPSET_DIRECT}" = "1" ] && $ipt_n -A PSW2_OUTPUT $(dst $ipset_global_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 + + [ ! -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 $(dst $IPSET_LANLIST) -j RETURN + $ipt_m -A PSW2_OUTPUT $(dst $IPSET_VPSLIST) -j RETURN + [ -n "$AUTO_DNS" ] && { + for auto_dns in $(echo $AUTO_DNS | tr ',' ' '); do + local dns_address=$(echo $auto_dns | awk -F '#' '{print $1}') + local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}') + $ipt_m -A PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN + echolog " - [$?]追加直连DNS到iptables:${dns_address}:${dns_port:-53}" + done + } + [ "${WRITE_IPSET_DIRECT}" = "1" ] && $ipt_m -A PSW2_OUTPUT $(dst $ipset_global_whitelist) ! -d $FAKE_IP -j RETURN + $ipt_m -A PSW2_OUTPUT -m mark --mark 0xff -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 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 + [ "${WRITE_IPSET_DIRECT}" = "1" ] && $ip6t_n -A PSW2_OUTPUT $(dst $ipset_global_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 + + 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 + [ "${WRITE_IPSET_DIRECT}" = "1" ] && $ip6t_m -A PSW2_OUTPUT $(dst $ipset_global_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 ] && { + local ipt_tmp=$ipt_n + [ -n "${is_tproxy}" ] && ipt_tmp=$ipt_m + + # 过滤节点 + filter_node $NODE TCP > /dev/null 2>&1 & + filter_node $NODE UDP > /dev/null 2>&1 & + + TCP_LOCALHOST_PROXY=$LOCALHOST_PROXY + UDP_LOCALHOST_PROXY=$LOCALHOST_PROXY + + msg="【路由器本机】," + [ "$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 + if [ "$TCP_NO_REDIR_PORTS" != "1:65535" ]; then + echolog " - ${msg}不代理 TCP 端口[${TCP_NO_REDIR_PORTS}]" + else + unset TCP_LOCALHOST_PROXY + echolog " - ${msg}不代理所有 TCP" + fi + } + + [ "$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 + if [ "$UDP_NO_REDIR_PORTS" != "1:65535" ]; then + echolog " - ${msg}不代理 UDP 端口[${UDP_NO_REDIR_PORTS}]" + else + unset UDP_LOCALHOST_PROXY + echolog " - ${msg}不代理所有 UDP" + fi + } + + # 加载路由器自身代理 TCP + if [ "$NODE" != "nil" ] && [ "$TCP_LOCALHOST_PROXY" = "1" ]; then + [ "$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) + } + + if [ -z "${is_tproxy}" ]; 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 + insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -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 + insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p tcp -j PSW2_OUTPUT" + fi + + for iface in $(ls ${TMP_IFACE_PATH}); 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 + + # 加载路由器自身代理 UDP + if [ "$NODE" != "nil" ] && [ "$UDP_LOCALHOST_PROXY" = "1" ]; then + $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 + insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -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 + insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p udp -j PSW2_OUTPUT" + fi + + for iface in $(ls ${TMP_IFACE_PATH}); 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 -I OUTPUT $(comment "mangle-OUTPUT-PSW2") -o lo -j RETURN + insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -m mark --mark 1 -j RETURN" + + $ip6t_m -I OUTPUT $(comment "mangle-OUTPUT-PSW2") -o lo -j RETURN + insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -m mark --mark 1 -j RETURN" + + $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 "删除iptables防火墙规则完成。" +} + +flush_ipset() { + $DIR/app.sh echolog "清空 IPSET。" + for _name in $(ipset list | grep "Name: " | grep "passwall2_" | awk '{print $2}'); do + destroy_ipset ${_name} + done +} + +flush_ipset_reload() { + del_firewall_rule + flush_ipset + rm -rf /tmp/singbox_passwall2_* + /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 + ;; +flush_ipset_reload) + flush_ipset_reload + ;; +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..18e518938 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh @@ -0,0 +1,1058 @@ +#!/bin/bash + +DIR="$(cd "$(dirname "$0")" && pwd)" +MY_PATH=$DIR/nftables.sh +NFTSET_LANLIST="passwall2_lanlist" +NFTSET_VPSLIST="passwall2_vpslist" + +NFTSET_LANLIST6="passwall2_lanlist6" +NFTSET_VPSLIST6="passwall2_vpslist6" + +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 timeout_argument="${1}"; shift + local defalut_timeout_argument="3650d" + local nftset_elements + + [ -n "${1}" ] && { + if [ "$timeout_argument" == "-1" ]; then + nftset_elements=$(echo -e $@ | sed 's/\s/, /g') + elif [ "$timeout_argument" == "0" ]; then + nftset_elements=$(echo -e $@ | sed "s/\s/ timeout $defalut_timeout_argument, /g" | sed "s/$/ timeout $defalut_timeout_argument/") + else + nftset_elements=$(echo -e $@ | sed "s/\s/ timeout $timeout_argument, /g" | sed "s/$/ timeout $timeout_argument/") + fi + 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 + # 0 - don't set defalut timeout + local timeout_argument_set="${1}"; shift + # 0 - don't let element timeout(3650 days) when set's timeout parameters be seted + # -1 - follow the set's timeout parameters + local timeout_argument_element="${1}"; shift + + nft "list set inet fw4 $nftset_name" &>/dev/null + if [ $? -ne 0 ]; then + if [ "$timeout_argument_set" == "0" ]; then + nft "add set inet fw4 $nftset_name { type $ip_type; flags interval, timeout; auto-merge; }" + else + nft "add set inet fw4 $nftset_name { type $ip_type; flags interval, timeout; timeout $timeout_argument_set; gc-interval $timeout_argument_set; auto-merge; }" + fi + fi + [ -n "${1}" ] && insert_nftset $nftset_name $timeout_argument_element $@ +} + +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}' | grep -v 'default'); 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) + + write_ipset_direct=${write_ipset_direct:-1} + [ "${write_ipset_direct}" = "1" ] && { + if [ "$node" = "default" ]; then + local nftset_whitelist=${nftset_global_whitelist} + local nftset_whitelist6=${nftset_global_whitelist6} + else + local nftset_whitelist="passwall2_${sid}_whitelist" + local nftset_whitelist6="passwall2_${sid}_whitelist6" + gen_nftset $nftset_whitelist ipv4_addr 0 0 + gen_nftset $nftset_whitelist6 ipv6_addr 0 0 + fi + } + + 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 + + [ "$tcp_no_redir_ports" != "disable" ] && { + if [ "$tcp_no_redir_ports" != "1:65535" ]; then + nft "add rule inet fw4 $nft_prerouting_chain ${_ipt_source} ip protocol tcp $(factor $tcp_no_redir_ports "tcp dport") counter return comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 ${_ipt_source} meta l4proto tcp $(factor $tcp_no_redir_ports "tcp dport") counter return comment \"$remarks\"" + echolog " - ${msg}不代理 TCP 端口[${tcp_no_redir_ports}]" + else + #结束时会return,无需加多余的规则。 + tcp_proxy_mode="disable" + echolog " - ${msg}不代理所有 TCP" + fi + } + + [ "$udp_no_redir_ports" != "disable" ] && { + if [ "$udp_no_redir_ports" != "1:65535" ]; then + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp ${_ipt_source} $(factor $udp_no_redir_ports "udp dport") counter return comment \"$remarks\"" + nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} $(factor $udp_no_redir_ports "udp dport") counter return comment \"$remarks\"" 2>/dev/null + echolog " - ${msg}不代理 UDP 端口[${udp_no_redir_ports}]" + else + #结束时会return,无需加多余的规则。 + udp_proxy_mode="disable" + echolog " - ${msg}不代理所有 UDP" + fi + } + + [ "$tcp_proxy_mode" != "disable" ] && [ -n "$redir_port" ] && { + [ -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]" + if [ -n "${is_tproxy}" ]; then + msg2="${msg2}(TPROXY:${redir_port})" + else + msg2="${msg2}(REDIRECT:${redir_port})" + fi + + [ "${write_ipset_direct}" = "1" ] && [ -z "${is_tproxy}" ] && nft "add rule inet fw4 PSW2_NAT ip protocol tcp ${_ipt_source} ip daddr @$nftset_whitelist counter return comment \"$remarks\"" + [ "${write_ipset_direct}" = "1" ] && [ -n "${is_tproxy}" ] && nft "add rule inet fw4 PSW2_MANGLE ip protocol tcp ${_ipt_source} ip daddr @$nftset_whitelist counter return comment \"$remarks\"" + + [ "$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\"" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip protocol icmp ${_ipt_source} return 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 + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmpv6 ${_ipt_source} return comment \"$remarks\"" 2>/dev/null + } + + if [ -z "${is_tproxy}" ]; then + nft "add rule inet fw4 PSW2_NAT ip protocol tcp ${_ipt_source} ip daddr $FAKE_IP $(REDIRECT $redir_port) comment \"$remarks\"" + nft "add rule inet fw4 PSW2_NAT ip protocol tcp ${_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 ip protocol tcp ${_ipt_source} $(REDIRECT $redir_port TPROXY4) comment \"$remarks\"" + fi + + [ "$PROXY_IPV6" == "1" ] && { + [ "${write_ipset_direct}" = "1" ] && nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} ip6 daddr @$nftset_whitelist6 counter return comment \"$remarks\"" + 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 + } + 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 + + [ "$udp_proxy_mode" != "disable" ] && [ -n "$redir_port" ] && { + msg2="${msg}使用 UDP 节点[$node_remark](TPROXY:${redir_port})" + + [ "${write_ipset_direct}" = "1" ] && nft "add rule inet fw4 PSW2_MANGLE ip protocol udp ${_ipt_source} ip daddr @$nftset_whitelist counter return comment \"$remarks\"" + 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" ] && { + [ "${write_ipset_direct}" = "1" ] && nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} ip6 daddr @$nftset_whitelist6 counter return comment \"$remarks\"" + 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 + } + 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 ] && [ "$CLIENT_PROXY" == 1 ] && { + # 加载默认代理模式 + msg="【默认】," + + [ "$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 \"默认\" + if [ "$TCP_NO_REDIR_PORTS" != "1:65535" ]; then + echolog " - ${msg}不代理 TCP 端口[${TCP_NO_REDIR_PORTS}]" + else + TCP_PROXY_MODE="disable" + echolog " - ${msg}不代理所有 TCP 端口" + fi + } + + [ "$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 \"默认\"" + if [ "$UDP_NO_REDIR_PORTS" != "1:65535" ]; then + echolog " - ${msg}不代理 UDP 端口[${UDP_NO_REDIR_PORTS}]" + else + UDP_PROXY_MODE="disable" + echolog " - ${msg}不代理所有 UDP 端口" + fi + } + + if [ "$TCP_PROXY_MODE" != "disable" ] && [ "$NODE" != "nil" ]; then + msg2="${msg}使用 TCP 节点[$(config_n_get $NODE remarks)]" + if [ -n "${is_tproxy}" ]; then + msg2="${msg2}(TPROXY:${REDIR_PORT})" + else + msg2="${msg2}(REDIRECT:${REDIR_PORT})" + fi + + [ "${WRITE_IPSET_DIRECT}" = "1" ] && [ -z "${is_tproxy}" ] && nft "add rule inet fw4 PSW2_NAT ip protocol tcp ip daddr @$nftset_global_whitelist counter return comment \"$remarks\"" + [ "${WRITE_IPSET_DIRECT}" = "1" ] && [ -n "${is_tproxy}" ] && nft "add rule inet fw4 PSW2_MANGLE ip protocol tcp ip daddr @$nftset_global_whitelist counter return comment \"$remarks\"" + + [ "$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 \"默认\"" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip protocol icmp return 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 \"默认\"" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT meta l4proto icmpv6 return 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 ip protocol tcp $(REDIRECT $REDIR_PORT TPROXY4) comment \"默认\"" + fi + + [ "$PROXY_IPV6" == "1" ] && { + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto tcp ip6 daddr @$nftset_global_whitelist6 counter return comment \"$remarks\"" + 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 "${msg2}" + fi + + if [ "$UDP_PROXY_MODE" != "disable" ] && [ "$NODE" != "nil" ]; then + msg2="${msg}使用 UDP 节点[$(config_n_get $NODE remarks)](TPROXY:${REDIR_PORT})" + + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_MANGLE ip protocol udp ip daddr @$nftset_global_whitelist counter return comment \"$remarks\"" + 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 ip protocol udp $(REDIRECT $REDIR_PORT TPROXY4) comment \"默认\"" + + [ "$PROXY_IPV6" == "1" ] && [ "$PROXY_IPV6_UDP" == "1" ] && { + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_MANGLE_V6 meta l4proto udp ip6 daddr @$nftset_global_whitelist6 counter return comment \"$remarks\"" + 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 "${msg2}" + 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 "-1" $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 "-1" $vps_ip4 + [ -n "$vps_ip6" ] && insert_nftset $NFTSET_VPSLIST6 "-1" $vps_ip6 + done +} + +filter_vpsip() { + insert_nftset $NFTSET_VPSLIST "-1" $(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 "-1" $(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 & + 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 + + [ -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 "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 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" + [ -n "$AUTO_DNS" ] && { + for auto_dns in $(echo $AUTO_DNS | tr ',' ' '); do + local dns_address=$(echo $auto_dns | awk -F '#' '{print $1}') + local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}') + nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip protocol udp ip daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return" + echolog " - [$?]追加直连DNS到nftables:${dns_address}:${dns_port:-53}" + done + } + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_OUTPUT_MANGLE ip daddr @$nftset_global_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 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" + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_OUTPUT_NAT ip daddr @$nftset_global_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" + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip daddr @$nftset_global_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" + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_ICMP_REDIRECT ip6 daddr @$nftset_global_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 + nft "add rule inet fw4 PSW2_MANGLE ip daddr ${WAN_IP} counter return comment \"WAN_IP_RETURN\"" + [ -z "${is_tproxy}" ] && 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" + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_MANGLE_V6 ip6 daddr @$nftset_global_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" + [ "${WRITE_IPSET_DIRECT}" = "1" ] && nft "add rule inet fw4 PSW2_OUTPUT_MANGLE_V6 ip6 daddr @$nftset_global_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 ] && { + # 过滤节点 + filter_node $NODE TCP > /dev/null 2>&1 & + filter_node $NODE UDP > /dev/null 2>&1 & + + TCP_LOCALHOST_PROXY=$LOCALHOST_PROXY + UDP_LOCALHOST_PROXY=$LOCALHOST_PROXY + + msg="【路由器本机】," + [ "$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" + if [ "$TCP_NO_REDIR_PORTS" != "1:65535" ]; then + echolog " - ${msg}不代理 TCP 端口[${TCP_NO_REDIR_PORTS}]" + else + unset TCP_LOCALHOST_PROXY + echolog " - ${msg}不代理所有 TCP" + fi + } + + [ "$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 + if [ "$UDP_NO_REDIR_PORTS" != "1:65535" ]; then + echolog " - ${msg}不代理 UDP 端口[${UDP_NO_REDIR_PORTS}]" + else + unset UDP_LOCALHOST_PROXY + echolog " - ${msg}不代理所有 UDP" + fi + } + + # 加载路由器自身代理 TCP + if [ "$NODE" != "nil" ] && [ "$TCP_LOCALHOST_PROXY" = "1" ]; then + [ "$accept_icmp" = "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT oif lo ip protocol icmp ip daddr $FAKE_IP counter redirect" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT oif lo ip protocol icmp counter redirect" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT oif lo ip protocol icmp counter return" + } + + [ "$accept_icmpv6" = "1" ] && { + nft "add rule inet fw4 PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 ip6 daddr $FAKE_IP_6 counter redirect" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 counter redirect" + nft "add rule inet fw4 PSW2_ICMP_REDIRECT oif lo meta l4proto icmpv6 counter return" + } + + 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 ip protocol tcp iif lo $(REDIRECT $REDIR_PORT TPROXY4) comment \"本机\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol tcp iif lo counter return comment \"本机\"" + nft "add rule inet fw4 mangle_output ip protocol 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 $(ls ${TMP_IFACE_PATH}); 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 + + # 加载路由器自身代理 UDP + if [ "$NODE" != "nil" ] && [ "$UDP_LOCALHOST_PROXY" = "1" ]; then + 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 ip protocol udp iif lo $(REDIRECT $REDIR_PORT TPROXY4) comment \"本机\"" + nft "add rule inet fw4 PSW2_MANGLE ip protocol udp iif lo counter return comment \"本机\"" + nft "add rule inet fw4 mangle_output ip protocol 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 $(ls ${TMP_IFACE_PATH}); 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_LANLIST6 + destroy_nftset $NFTSET_VPSLIST6 + + $DIR/app.sh echolog "删除nftables防火墙规则完成。" +} + +flush_nftset() { + $DIR/app.sh echolog "清空 NFTSET。" + for _name in $(nft -a list sets | grep -E "passwall2" | awk -F 'set ' '{print $2}' | awk '{print $1}'); do + destroy_nftset ${_name} + done +} + +flush_nftset_reload() { + del_firewall_rule + flush_nftset + rm -rf /tmp/singbox_passwall2_* + /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 ip protocol 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 ip protocol 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 + ;; +flush_nftset_reload) + flush_nftset_reload + ;; +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..60e55fdc1 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua @@ -0,0 +1,206 @@ +#!/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 arg2 = arg[2] +local arg3 = arg[3] + +local reboot = 0 +local geoip_update = 0 +local geosite_update = 0 +local 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" + +if arg3 == "cron" then + arg2 = nil +end + +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(asset_location .. "geoip.dat") then + luci.sys.call(string.format("cp -f %s %s", 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", asset_location, "/tmp/geoip.dat", 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(asset_location .. "geosite.dat") then + luci.sys.call(string.format("cp -f %s %s", 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", asset_location, "/tmp/geosite.dat", 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 arg2 then + string.gsub(arg2, '[^' .. "," .. ']+', 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 + if arg3 == "cron" then + if not nixio.fs.access("/var/lock/" .. name .. ".lock") then + luci.sys.call("touch /tmp/lock/" .. name .. "_cron.lock") + end + end + + log("重启服务,应用新的规则。") + if use_nft == "1" then + luci.sys.call("sh /usr/share/" .. name .. "/nftables.sh flush_nftset_reload > /dev/null 2>&1 &") + else + luci.sys.call("sh /usr/share/" .. name .. "/iptables.sh flush_ipset_reload > /dev/null 2>&1 &") + end +end +log("规则更新完毕...") diff --git a/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh b/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh new file mode 100755 index 000000000..563bf57ea --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/socks_auto_switch.sh @@ -0,0 +1,180 @@ +#!/bin/sh + +CONFIG=passwall2 +LOG_FILE=/tmp/log/$CONFIG.log +LOCK_FILE_DIR=/tmp/lock + +flag=0 + +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}" +} + +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 --user-agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url") + case "$status" in + 204) + status=200 + ;; + esac + echo $status +} + +test_proxy() { + result=0 + status=$(test_url "${probe_url}" ${retry_num} ${connect_timeout} "-x socks5h://127.0.0.1:${socks_port}") + 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 +} + +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 "${probe_url}" ${retry_num} ${connect_timeout} "-x $curlx") + pgrep -af "test_node_${node_id}" | awk '! /socks_auto_switch\.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 +} + +test_auto_switch() { + flag=$(expr $flag + 1) + local b_nodes=$1 + local now_node=$2 + [ -z "$now_node" ] && { + local f="/tmp/etc/$CONFIG/id/socks_${id}" + if [ -f "${f}" ]; then + now_node=$(cat ${f}) + 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 "自动切换检测:${id}主节点【$(config_n_get $main_node type):[$(config_n_get $main_node remarks)]】正常,切换到主节点!" + /usr/share/${CONFIG}/app.sh socks_node_switch flag=${id} new_node=${main_node} + [ $? -eq 0 ] && { + echolog "自动切换检测:${id}节点切换完毕!" + } + return 0 + } + fi + + if [ "$status" == 0 ]; then + #echolog "自动切换检测:${id}【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】正常。" + return 0 + elif [ "$status" == 1 ]; then + echolog "自动切换检测:${id}【$(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" ] && { + uci set $CONFIG.${id}.node=$new_node + [ -z "$(echo $b_nodes | grep $main_node)" ] && uci add_list $CONFIG.${id}.autoswitch_backup_node=$main_node + uci commit $CONFIG + } + echolog "自动切换检测:${id}【$(config_n_get $new_node type):[$(config_n_get $new_node remarks)]】正常,切换到此节点!" + /usr/share/${CONFIG}/app.sh socks_node_switch flag=${id} new_node=${new_node} + [ $? -eq 0 ] && { + echolog "自动切换检测:${id}节点切换完毕!" + } + return 0 + else + test_auto_switch "${b_nodes}" ${new_node} + fi + fi +} + +start() { + id=$1 + LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}_socks_auto_switch_${id}.lock + main_node=$(config_n_get $id node nil) + socks_port=$(config_n_get $id port 0) + delay=$(config_n_get $id autoswitch_testing_time 30) + sleep 5s + connect_timeout=$(config_n_get $id autoswitch_connect_timeout 3) + retry_num=$(config_n_get $id autoswitch_retry_num 1) + restore_switch=$(config_n_get $id autoswitch_restore_switch 0) + probe_url=$(config_n_get $id autoswitch_probe_url "https://www.google.com/generate_204") + backup_node=$(config_n_get $id autoswitch_backup_node nil) + while [ -n "$backup_node" -a "$backup_node" != "nil" ]; do + [ -f "$LOCK_FILE" ] && { + sleep 6s + continue + } + touch $LOCK_FILE + backup_node=$(echo $backup_node | tr -s ' ' '\n' | uniq | tr -s '\n' ' ') + test_auto_switch "$backup_node" + rm -f $LOCK_FILE + sleep ${delay} + done +} + +start $@ + 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..1fd64fa08 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -0,0 +1,1368 @@ +#!/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 split = api.split +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_singbox = api.finded_com("singbox") +local has_xray = api.finded_com("xray") +local has_hysteria2 = api.finded_com("hysteria") +local allowInsecure_default = true +local ss_type_default = uci:get(appname, "@global_subscribe[0]", "ss_type") or "shadowsocks-libev" +local trojan_type_default = uci:get(appname, "@global_subscribe[0]", "trojan_type") or "sing-box" +local vmess_type_default = uci:get(appname, "@global_subscribe[0]", "vmess_type") or "xray" +local vless_type_default = uci:get(appname, "@global_subscribe[0]", "vless_type") or "xray" +local hysteria2_type_default = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "hysteria2" +-- 判断是否过滤节点关键字 +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, + delete = function(o) + uci:delete(appname, t[".name"]) + 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, + delete = function(o) + uci:delete(appname, t[".name"]) + 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 + + uci:foreach(appname, "socks", function(o) + local id = o[".name"] + local node_table = uci:get(appname, id, "autoswitch_backup_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 = "Socks[" .. id .. "]备用节点的列表[" .. k .. "]", + currentNode = currentNode, + set = function(o, server) + for kk, vv in pairs(CONFIG) do + if (vv.remarks == id .. "备用节点的列表") then + table.insert(vv.new_nodes, server) + end + end + end + } + end + end + end + CONFIG[#CONFIG + 1] = { + remarks = id .. "备用节点的列表", + nodes = nodes, + new_nodes = new_nodes, + set = function(o) + for kk, vv in pairs(CONFIG) do + if (vv.remarks == id .. "备用节点的列表") then + uci:set_list(appname, id, "autoswitch_backup_node", vv.new_nodes) + end + end + end + } + end + end) + + uci:foreach(appname, "nodes", function(node) + local node_id = node[".name"] + if node.protocol and node.protocol == '_shunt' then + 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 + if _node_id and api.parseURL(_node_id) then + else + CONFIG[#CONFIG + 1] = { + log = false, + currentNode = _node_id and uci:get_all(appname, _node_id) or nil, + remarks = "分流" .. e.remarks .. "节点", + set = function(o, server) + if not server then server = "nil" end + uci:set(appname, node_id, e[".name"], server) + o.newNodeId = server + end + } + end + + end + elseif node.protocol and node.protocol == '_balancing' then + 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 == "Xray负载均衡节点[" .. node_id .. "]列表") then + table.insert(vv.new_nodes, server) + end + end + end + } + end + end + CONFIG[#CONFIG + 1] = { + remarks = "Xray负载均衡节点[" .. node_id .. "]列表", + nodes = nodes, + new_nodes = new_nodes, + set = function(o) + for kk, vv in pairs(CONFIG) do + if (vv.remarks == "Xray负载均衡节点[" .. node_id .. "]列表") then + uci:foreach(appname, "nodes", function(node2) + if node2[".name"] == node[".name"] then + local section = uci:section(appname, "nodes", node_id) + uci:set_list(appname, section, "balancing_node", vv.new_nodes) + end + end) + end + end + end + } + + --后备节点 + local currentNode = uci:get_all(appname, node_id) or nil + if currentNode and currentNode.fallback_node then + CONFIG[#CONFIG + 1] = { + log = true, + id = node_id, + remarks = "Xray负载均衡节点[" .. node_id .. "]后备节点", + currentNode = uci:get_all(appname, currentNode.fallback_node) or nil, + set = function(o, server) + uci:set(appname, node_id, "fallback_node", server) + o.newNodeId = server + end, + delete = function(o) + uci:delete(appname, node_id, "fallback_node") + end + } + end + else + --落地节点 + local currentNode = uci:get_all(appname, node_id) or nil + if currentNode and currentNode.to_node then + CONFIG[#CONFIG + 1] = { + log = true, + id = node_id, + remarks = "节点[" .. node_id .. "]落地节点", + currentNode = uci:get_all(appname, currentNode.to_node) or nil, + set = function(o, server) + uci:set(appname, node_id, "to_node", server) + o.newNodeId = server + end, + delete = function(o) + uci:delete(appname, node_id, "to_node") + 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 + if v.delete then + v.delete() + end + CONFIG[k] = nil + end + end + end +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) + if has_singbox then + result.type = 'sing-box' + end + if has_xray then + result.type = 'Xray' + end + if vmess_type_default == "sing-box" and has_singbox then + result.type = 'sing-box' + end + if vmess_type_default == "xray" and has_xray then + result.type = "Xray" + end + result.alter_id = info.aid + result.address = info.add + result.port = info.port + result.protocol = 'vmess' + result.alter_id = info.aid + result.uuid = info.id + result.remarks = info.ps + -- result.mux = 1 + -- result.mux_concurrency = 8 + + if not info.net then + info.net = "tcp" + end + info.net = string.lower(info.net) + result.transport = info.net + if info.net == 'ws' then + result.ws_host = info.host + result.ws_path = info.path + if result.type == "sing-box" and info.path then + local ws_path_dat = split(info.path, "?") + local ws_path = ws_path_dat[1] + local ws_path_params = {} + for _, v in pairs(split(ws_path_dat[2], '&')) do + local t = split(v, '=') + ws_path_params[t[1]] = t[2] + end + if ws_path_params.ed and tonumber(ws_path_params.ed) then + result.ws_path = ws_path + result.ws_enableEarlyData = "1" + result.ws_maxEarlyData = tonumber(ws_path_params.ed) + result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol" + end + end + end + 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 + 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 + + if ss_type_default == "shadowsocks-rust" and has_ss_rust then + result.type = 'SS-Rust' + end + if ss_type_default == "xray" and has_xray then + result.type = 'Xray' + result.protocol = 'shadowsocks' + result.transport = 'tcp' + end + if ss_type_default == "sing-box" and has_singbox then + result.type = 'sing-box' + result.protocol = 'shadowsocks' + end + + if result.type == "SS-Rust" and method:lower() == "chacha20-poly1305" then + result.method = "chacha20-ietf-poly1305" + end + + if result.type == "Xray" and method:lower() == "chacha20-ietf-poly1305" then + result.method = "chacha20-poly1305" + end + + if result.plugin then + if result.type == 'Xray' then + --不支持插件 + result.error_msg = "Xray不支持插件." + end + if result.type == "sing-box" then + result.plugin_enabled = "1" + end + end + + if result.type == "SS" then + local aead2022_methods = { "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" } + local aead2022 = false + for k, v in ipairs(aead2022_methods) do + if method:lower() == v:lower() then + aead2022 = true + end + end + if aead2022 then + -- shadowsocks-libev 不支持2022加密 + result.error_msg = "shadowsocks-libev 不支持2022加密." + 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) + 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, '=') + if #t > 1 then + params[string.lower(t[1])] = UrlDecode(t[2]) + end + 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 + + if trojan_type_default == "sing-box" and has_singbox then + result.type = 'sing-box' + elseif trojan_type_default == "xray" and has_xray then + result.type = 'Xray' + end + result.protocol = 'trojan' + 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 + if has_singbox then + result.type = 'sing-box' + end + if has_xray then + result.type = 'Xray' + end + if vless_type_default == "sing-box" and has_singbox then + result.type = 'sing-box' + end + if vless_type_default == "xray" and 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 + + if not params.type then + params.type = "tcp" + end + params.type = string.lower(params.type) + result.transport = params.type + if params.type == 'ws' then + result.ws_host = params.host + result.ws_path = params.path + if result.type == "sing-box" and params.path then + local ws_path_dat = split(params.path, "?") + local ws_path = ws_path_dat[1] + local ws_path_params = {} + for _, v in pairs(split(ws_path_dat[2], '&')) do + local t = split(v, '=') + ws_path_params[t[1]] = t[2] + end + if ws_path_params.ed and tonumber(ws_path_params.ed) then + result.ws_path = ws_path + result.ws_enableEarlyData = "1" + result.ws_maxEarlyData = tonumber(ws_path_params.ed) + result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol" + end + end + end + 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.encryption = params.encryption or "none" + + result.flow = params.flow or nil + + result.tls = "0" + if params.security == "tls" or params.security == "reality" then + result.tls = "1" + 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) + + local dat = split(content:gsub("/%?", "?"), '%?') + 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 + + if has_singbox then + result.type = 'sing-box' + result.protocol = "hysteria" + end + elseif szType == 'hysteria2' or szType == 'hy2' 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) + local Info = content + if content:find("@") then + local contents = split(content, "@") + result.hysteria2_auth_password = UrlDecode(contents[1]) + Info = (contents[2] or ""):gsub("/%?", "?") + end + local query = split(Info, "?") + local host_port = query[1] + local params = {} + for _, v in pairs(split(query[2], '&')) do + local t = split(v, '=') + if #t > 1 then + params[string.lower(t[1])] = UrlDecode(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.tls_serverName = params.sni + if params.insecure and (params.insecure == "1" or params.insecure == "0") then + result.tls_allowInsecure = params.insecure + --log(result.remarks ..' 使用节点AllowInsecure设定: '.. result.tls_allowInsecure) + else + result.tls_allowInsecure = allowInsecure_default and "1" or "0" + end + result.hysteria2_tls_pinSHA256 = params.pinSHA256 + + if has_hysteria2 then + result.type = "Hysteria2" + if params["obfs-password"] then + result.hysteria2_obfs = params["obfs-password"] + end + end + if hysteria2_type_default == "sing-box" and has_singbox then + result.type = 'sing-box' + result.protocol = "hysteria2" + if params["obfs-password"] then + result.hysteria2_obfs_type = "salamander" + result.hysteria2_obfs_password = params["obfs-password"] + end + end + 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) + local curl_args = api.clone(api.curl_args) + if ua and ua ~= "" and ua ~= "curl" then + table.insert(curl_args, '--user-agent "' .. ua .. '"') + end + local return_code, result = api.curl_logic(url, file, curl_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 and 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) + if config.currentNode then + local server + -- 特别优先级 cfgid + if config.currentNode[".name"] then + for index, node in pairs(nodes) do + if node[".name"] == config.currentNode[".name"] 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 + -- 还不行 随便找一个 + 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 + else + config.set(config, "nil") + 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 + + if arg[3] == "cron" then + if not nixio.fs.access("/var/lock/" .. appname .. ".lock") then + luci.sys.call("touch /tmp/lock/" .. appname .. "_cron.lock") + end + 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 + xpcall(function () + 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 result.error_msg then + log('丢弃节点: ' .. result.remarks .. ", 原因:" .. result.error_msg) + elseif 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, function (err) + --log(err) + log(v, "解析错误,跳过此节点。") + 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_type = value.ss_type or "global" + if ss_type ~= "global" then + ss_type_default = ss_type + end + local trojan_type = value.trojan_type or "global" + if trojan_type ~= "global" then + trojan_type_default = trojan_type + end + local vmess_type = value.vmess_type or "global" + if vmess_type ~= "global" then + vmess_type_default = vmess_type + end + local vless_type = value.vless_type or "global" + if vless_type ~= "global" then + vless_type_default = vless_type + end + local hysteria2_type = value.hysteria2_type or "global" + if hysteria2_type ~= "global" then + hysteria2_type_default = hysteria2_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_type_default = uci:get(appname, "@global_subscribe[0]", "ss_type") or "shadowsocks-libev" + trojan_type_default = uci:get(appname, "@global_subscribe[0]", "trojan_type") or "sing-box" + vmess_type_default = uci:get(appname, "@global_subscribe[0]", "vmess_type") or "xray" + vless_type_default = uci:get(appname, "@global_subscribe[0]", "vless_type") or "xray" + hysteria2_type_default = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "hysteria2" + 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/tasks.sh b/luci-app-passwall2/root/usr/share/passwall2/tasks.sh new file mode 100755 index 000000000..c8d915abe --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/tasks.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +## 循环更新脚本 + +CONFIG=passwall2 +APP_PATH=/usr/share/$CONFIG +TMP_PATH=/tmp/etc/$CONFIG +LOCK_FILE=/tmp/lock/${CONFIG}_tasks.lock +CFG_UPDATE_INT=0 + +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}}" +} + +exec 99>"$LOCK_FILE" +flock -n 99 +if [ "$?" != 0 ]; then + exit 0 +fi + +while true +do + + if [ "$CFG_UPDATE_INT" -ne 0 ]; then + + autoupdate=$(config_t_get global_rules auto_update) + weekupdate=$(config_t_get global_rules week_update) + hourupdate=$(config_t_get global_rules interval_update) + hourupdate=$(expr "$hourupdate" \* 60) + if [ "$autoupdate" = "1" ]; then + [ "$weekupdate" = "8" ] && { + [ "$(expr "$CFG_UPDATE_INT" % "$hourupdate")" -eq 0 ] && lua $APP_PATH/rule_update.lua log all cron > /dev/null 2>&1 & + } + fi + + TMP_SUB_PATH=$TMP_PATH/sub_tasks + 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) + hour_update=$(config_n_get $item interval_update) + echo "$cfgid" >> $TMP_SUB_PATH/${week_update}_${hour_update} + fi + done + + [ -d "${TMP_SUB_PATH}" ] && { + for name in $(ls ${TMP_SUB_PATH}); do + week_update=$(echo $name | awk -F '_' '{print $1}') + hour_update=$(echo $name | awk -F '_' '{print $2}') + hour_update=$(expr "$hour_update" \* 60) + cfgids=$(echo -n $(cat ${TMP_SUB_PATH}/${name}) | sed 's# #,#g') + [ "$week_update" = "8" ] && { + [ "$(expr "$CFG_UPDATE_INT" % "$hour_update")" -eq 0 ] && lua $APP_PATH/subscribe.lua start $cfgids cron > /dev/null 2>&1 & + } + + done + rm -rf $TMP_SUB_PATH + } + + fi + + CFG_UPDATE_INT=$(expr "$CFG_UPDATE_INT" + 10) + + sleep 600 + +done 2>/dev/null 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..77fee55cf --- /dev/null +++ b/luci-app-passwall2/root/usr/share/passwall2/test.sh @@ -0,0 +1,106 @@ +#!/bin/sh + +CONFIG=passwall2 +LOG_FILE=/tmp/log/$CONFIG.log + +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 +} + +arg1=$1 +shift +case $arg1 in +test_url) + test_url $@ + ;; +url_test_node) + url_test_node $@ + ;; +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" ] + } + } +} diff --git a/luci-app-passwall2/root/usr/share/ucitrack/luci-app-passwall2-server.json b/luci-app-passwall2/root/usr/share/ucitrack/luci-app-passwall2-server.json new file mode 100644 index 000000000..d73b5d109 --- /dev/null +++ b/luci-app-passwall2/root/usr/share/ucitrack/luci-app-passwall2-server.json @@ -0,0 +1,4 @@ +{ + "config": "passwall2_server", + "init": "passwall2_server" +} diff --git a/luci-app-passwall2/root/usr/share/ucitrack/luci-app-passwall2.json b/luci-app-passwall2/root/usr/share/ucitrack/luci-app-passwall2.json new file mode 100644 index 000000000..2ff857b0d --- /dev/null +++ b/luci-app-passwall2/root/usr/share/ucitrack/luci-app-passwall2.json @@ -0,0 +1,4 @@ +{ + "config": "passwall2", + "init": "passwall2" +}