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

This commit is contained in:
github-actions[bot] committed 2026-03-05 23:51:42 +08:00
1 parent 0d155c70be
commit 32600acc60
14258 files changed
+3954570

No files matched your search

Binary file not shown.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,17 @@
{
"admin/services/xclient/overview": {
"title": "XClient",
"action": {
"type": "view",
"path": "xclient"
},
"depends": {
"acl": [
"luci-app-xclient"
],
"uci": {
"xclient": true
}
}
}
}
@@ -0,0 +1,11 @@
{
"luci-app-xclient": {
"description": "Grant UCI access for luci-app-xclient",
"read": {
"uci": ["xclient"]
},
"write": {
"uci": ["xclient"]
}
}
}
@@ -0,0 +1,4 @@
{
"config": "xclient",
"init": "xclient"
}
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
cur_core_version=$(/usr/bin/xray -version | awk '{print $2}' | sed -n 1P 2>&1 & >/dev/null)
if [ "$?" -eq "0" ]; then
rm -rf /usr/share/xclient/core_version
if [ $cur_core_version ]; then
echo $cur_core_version > /usr/share/xclient/core_version 2>&1 & >/dev/null
elif [ $cur_core_version == "" ]; then
echo "--" > /usr/share/xclient/core_version 2>&1 & >/dev/null
fi
fi
new_core_version=`curl -sL "https://github.com/XTLS/Xray-core/tags"| grep "/XTLS/Xray-core/releases/"| head -n 1| awk -F "/tag/" '{print $2}'| sed 's/\">//'`
if [ "$?" -eq "0" ]; then
rm -rf /usr/share/xclient/new_core
if [ $new_core_version ]; then
echo $new_core_version > /usr/share/xclient/new_core 2>&1 & >/dev/null
elif [ $new_core_version == "" ]; then
echo 0 > /usr/share/xclient/new_core 2>&1 & >/dev/null
fi
fi
@@ -0,0 +1,626 @@
#!/usr/bin/lua
--local ucursor = require "luci.model.uci"
local ucursor = require "luci.model.uci".cursor()
local json = require "luci.jsonc"
local nixiofs = require "nixio.fs"
local proxy_section = ucursor:get_first("xclient", "global")
local proxy = ucursor:get_all("xclient", proxy_section)
local tcp_server_section = proxy.global_server or nil
local tcp_server = ucursor:get_all("xclient", tcp_server_section)
local udp_server_section = proxy.udp_relay_server or nil
local udp_server = ucursor:get_all("xclient", udp_server_section)
local socks5_server_section = proxy.socks5_server or nil
local socks5_server = ucursor:get_all("xclient", socks5_server_section)
local server_section = arg[1] or nil
local node = ucursor:get_all("xclient", server_section)
local geoip_existence = false
local geosite_existence = false
local xray_data_file_iterator = nixiofs.dir("/usr/share/xclient")
local proto = arg[2]
local local_port = arg[3] or "1234"
local socks_port = arg[4] or "1080"
local _protocol = arg[5] or "redir"
local _local = arg[6] or "0"
repeat
local fn = xray_data_file_iterator()
if fn == "geoip.dat" then
geoip_existence = true
end
if fn == "geosite.dat" then
geosite_existence = true
end
until fn == nil
local function direct_outbound()
return {
protocol = "freedom",
settings = {
domainStrategy = "UseIP"
},
tag = "direct"
}
end
local function block_outbound()
return {
protocol = "blackhole",
tag = "block",
settings = {
response = {
type = "http"
}
}
}
end
local function dns_outbound()
return {
protocol = "dns",
address = "1.1.1.1",
network = "tcp,udp",
port = 53,
tag = "dns_outbound"
}
end
local function stream_tcp_fake_http_request(server)
if server.tcp_guise == "http" then
return {
version = "1.1",
method = "GET",
path = server.http_path,
headers = {
Host = server.http_host,
User_Agent = {
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"
},
Accept_Encoding = {"gzip, deflate"},
Connection = {"keep-alive"},
Pragma = "no-cache"
}
}
else
return nil
end
end
local function stream_tcp_fake_http_response(server)
if server.tcp_guise == "http" then
return {
version = "1.1",
status = "200",
reason = "OK",
headers = {
Content_Type = {"application/octet-stream", "video/mpeg"},
Transfer_Encoding = {"chunked"},
Connection = {"keep-alive"},
Pragma = "no-cache"
}
}
else
return nil
end
end
local function stream_tcp(server)
if server.transport == "tcp" then
return {
header = {
type = server.tcp_guise,
request = stream_tcp_fake_http_request(server),
response = stream_tcp_fake_http_response(server)
}
}
else
return nil
end
end
local function stream_h2(server)
if (server.transport == "h2") then
return {
path = server.h2_path,
host = server.h2_host,
read_idle_timeout = tonumber(server.read_idle_timeout) or nil,
health_check_timeout = tonumber(server.health_check_timeout) or nil
}
else
return nil
end
end
local function stream_grpc(server)
if (server.transport == "grpc") then
return {
serviceName = server.serviceName or "",
multiMode = server.grpc_multi_mode == "1",
initial_windows_size = tonumber(server.initial_windows_size) or nil,
idle_timeout = tonumber(server.idle_timeout) or nil,
health_check_timeout = tonumber(server.health_check_timeout) or nil,
permit_without_stream = (server.permit_without_stream == "1") and true or nil
}
else
return nil
end
end
local function stream_kcp(server)
if server.transport == "mkcp" then
local mkcp_seed = nil
if server.seed ~= "" then
mkcp_seed = server.seed
end
return {
mtu = tonumber(server.mtu),
tti = tonumber(server.tti),
uplinkCapacity = tonumber(server.uplink_capacity),
downlinkCapacity = tonumber(server.downlink_capacity),
congestion = (server.congestion == "1") and true or false,
readBufferSize = tonumber(server.read_buffer_size),
writeBufferSize = tonumber(server.write_buffer_size),
seed = mkcp_seed,
header = {
type = server.kcp_guise
}
}
else
return nil
end
end
local function stream_quic(server)
if server.transport == "quic" then
return {
security = server.quic_security,
key = server.quic_key,
header = {type = server.quic_guise}
}
else
return nil
end
end
local function stream_ws(server)
if server.transport == "ws" then
local headers = nil
if (server.ws_host ~= nil) then
headers = {
Host = server.ws_host or server.tls_host
}
end
return {
path = server.ws_path,
headers = headers
}
else
return nil
end
end
local function tls_settings(server)
local result = {
fingerprint = server.fingerprint,
rejectUnknownSni = (server.rejectUnknownSni == "1") and true or false,
allowInsecure = (server.insecure == "1") and true or false,
serverName = server.tls_host,
}
if server.alpn ~= nil then
local alpn = {}
for _, x in ipairs(server.alpn) do
table.insert(alpn, x)
end
result["alpn"] = alpn
end
return result
end
local function reality_settings(server)
local result = {
fingerprint = server.fingerprint,
shortId = server.shortId,
publicKey = server.publicKey,
serverName = server.tls_host,
spiderX = server.spiderX,
}
return result
end
local function shadowsocks_outbound(server, tag)
return {
protocol = "shadowsocks",
tag = tag,
settings = {
servers = {
{
address = server.server,
port = tonumber(server.server_port),
password = server.password,
method = server.encrypt_method_v2ray_ss
}
}
},
streamSettings = {
network = server.transport,
security = (server.security == 'tls') and "tls" or "none",
tlsSettings = (server.security == 'tls') and "tls" and tls_settings(server) or nil,
quicSettings = stream_quic(server),
kcpSettings = stream_kcp(server),
tcpSettings = stream_tcp(server) and server.protocol ~= "shadowsocks" or nil,
wsSettings = stream_ws(server),
grpcSettings = stream_grpc(server),
httpSettings = stream_h2(server)
},
mux = (server.mux == "1" and server.security == 'tls' or server.security == 'none') and {
enabled = true,
concurrency = tonumber(server.concurrency)
} or nil
}
end
local function vmess_outbound(server, tag)
return {
protocol = "vmess",
tag = tag,
settings = {
vnext = {
{
address = server.server,
port = tonumber(server.server_port),
users = {
{
id = server.vmess_id,
alterId = 0,
security = server.vmess_encryption
}
}
}
}
},
streamSettings = {
network = server.transport,
security = (server.security == 'tls') and "tls" or "none",
tlsSettings = (server.security == 'tls') and "tls" and tls_settings(server) or nil,
tcpSettings = stream_tcp(server),
wsSettings = stream_ws(server),
grpcSettings = stream_grpc(server),
httpSettings = stream_h2(server),
quicSettings = stream_quic(server),
kcpSettings = stream_kcp(server),
}
}
end
local function vless_outbound(server, tag)
local flow = server.flow
if server.flow == "none" then
flow = nil
end
return {
protocol = "vless",
tag = tag,
settings = {
vnext = {
{
address = server.server,
port = tonumber(server.server_port),
users = {
{
id = server.vmess_id,
flow = flow,
encryption = server.vless_encryption
}
}
}
}
},
streamSettings = {
network = server.transport,
security = (server.security == 'reality') and "reality" or (server.security == 'tls') and "tls" or "none",
tlsSettings = (server.security == 'tls') and "tls" and tls_settings(server) or nil,
realitySettings = (server.security == 'reality') and "reality" and reality_settings(server) or nil,
tcpSettings = stream_tcp(server),
wsSettings = stream_ws(server),
grpcSettings = stream_grpc(server),
httpSettings = stream_h2(server),
quicSettings = stream_quic(server),
kcpSettings = stream_kcp(server),
}
}
end
local function trojan_outbound(server, tag)
local flow = server.flow
if server.flow == "none" then
flow = nil
end
return {
protocol = "trojan",
tag = tag,
settings = {
servers = {
{
address = server.server,
port = tonumber(server.server_port),
password = server.password,
flow = flow,
}
}
},
streamSettings = {
network = server.transport,
security = (server.security == 'tls') and "tls" or "none",
tlsSettings = (server.security == 'tls') and "tls" and tls_settings(server) or nil,
tcpSettings = stream_tcp(server) and server.protocol ~= "trojan" or nil,
wsSettings = stream_ws(server),
grpcSettings = stream_grpc(server),
httpSettings = stream_h2(server),
quicSettings = stream_quic(server),
kcpSettings = stream_kcp(server),
},
mux = (server.mux == "1" and server.security == 'tls' or server.security == 'none') and {
enabled = true,
concurrency = tonumber(server.concurrency)
} or nil
}
end
local function server_outbound(server, tag)
if server.protocol == "vmess" then
return vmess_outbound(server, tag)
end
if server.protocol == "vless" then
return vless_outbound(server, tag)
end
if server.protocol == "shadowsocks" then
return shadowsocks_outbound(server, tag)
end
if server.protocol == "trojan" then
return trojan_outbound(server, tag)
end
return nil
end
local function proxy_inbound()
if local_port == "0" then
return nil
end
return {
port = local_port,
protocol = "dokodemo-door",
tag = "proxy_inbound",
sniffing = {
enabled = true,
destOverride = {"http", "tls"}
} ,
settings = {
network = proto,
followRedirect = true
}
}
end
local function socks_inbound()
if socks_port == "0" then
return nil
end
return {
port = socks_port,
protocol = "socks",
tag = "socks_inbound",
settings = {
auth = "noauth",
udp = true
}
}
end
local function dns_inbounds()
local servers = {}
if proxy["dns_servers"] ~= nil then
for _, x in ipairs(proxy["dns_servers"]) do
table.insert(servers, x)
end
end
local i = 0
ucursor:foreach("xclient", "dns", function(v)
i = i + 1
table.insert(servers, {
address = v.address or nil,
port = v.port,
domains = v.domains or nil,
expectIPs = v.expectIPs or nil,
skipFallback = v.skipFallback == "1" and true or false
})
end)
return servers
end
local function dns()
local result = {}
for _, v in ipairs(dns_inbounds()) do
table.insert(result, v)
end
return {
server = result,
clientIp = proxy.clientIp or nil,
queryStrategy = proxy.queryStrategy,
disableCache = proxy.disableCache == "1" and true or false,
disableFallback = proxy.disableFallback == "1" and true or false,
disableFallbackIfMatch = proxy.disableFallbackIfMatch == "1" and true or false,
tag = "dns_inbound"
}
end
local function inbounds()
local i = {}
if proxy.global_server ~= "nil" or proxy.udp_server ~= "nil" then
i = {
proxy_inbound(),
}
if proxy.socks5_server == "same" or _local == "1" then
table.insert(i, socks_inbound())
end
table.insert(i, {
listen = "127.0.0.1",
port = 8888,
protocol = "dokodemo-door",
settings = {
address = "127.0.0.1"
},
tag = "api"
})
return i
end
end
local function api()
return {
tag = "api",
services = {
"StatsService"
}
}
end
local function routing()
local rule = {}
local i = 0
ucursor:foreach("xclient", "rule", function(v)
i = i + 1
table.insert(rule, {
type = "field",
domainMatcher = v.domainMatcher or nil,
domain = v.domain or nil,
ip = v.ip or nil,
port = v.port or nil,
sourcePort = v.sourcePort or nil,
network = v.network or nil,
source = v.source or nil,
inboundTag = v.inboundTag or nil,
protocol = v.protocol or nil,
outboundTag = v.outboundTag or nil,
})
end)
table.insert(rule, {
type = "field",
inboundTag = {
"api"
},
outboundTag = "api",
enabled = true
})
return rule
end
local function routing_rules()
local result = {}
for _, v in ipairs(routing()) do
table.insert(result, v)
end
return {
domainMatcher = proxy.domainMatcher,
domainStrategy = proxy.routing_strategy or 'AsIs',
rules = result
}
end
local function outbounds()
local result = {}
if arg[7] == "none" then
result = {
server_outbound(socks5_server, "proxy_outbound"),
dns_outbound(),
direct_outbound(),
block_outbound()
}
elseif proto == "tcp" then
result = {
server_outbound(tcp_server, "proxy_outbound"),
dns_outbound(),
direct_outbound(),
block_outbound()
}
elseif proto == "udp" then
result = {
server_outbound(udp_server, "proxy_outbound"),
dns_outbound(),
direct_outbound(),
block_outbound()
}
elseif proto == "tcp,udp" then
result = {
server_outbound(tcp_server, "proxy_outbound"),
dns_outbound(),
direct_outbound(),
block_outbound()
}
end
return result
end
local function logging()
return {
loglevel = proxy.log_level or "warning",
}
end
local function policy()
return {
system = {
statsOutboundUplink = true,
statsOutboundDownlink = true
}
}
end
local xclient = {
log = logging(),
dns = dns(),
routing = routing_rules(),
dns = dns(),
api = api(),
policy = policy(),
routing = routing_rules(),
inbounds = inbounds(),
outbounds = outbounds()
}
local shadowsocks_plugin = {
server = node.server,
server_port = tonumber(node.server_port),
local_address = "0.0.0.0",
local_port = tonumber(local_port),
mode = (proto == "tcp,udp") and "tcp_and_udp" or proto .. "_only",
password = node.password,
method = node.encrypt_method_v2ray_ss,
reuse_port = true,
protocol = _protocol,
plugin = node.plugin,
plugin_opts = node.plugin_opts or nil
}
if node.protocol == "shadowsocks-plugin" then
print(json.stringify(shadowsocks_plugin, true))
else
print(json.stringify(xclient, true))
end
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
echo '' >/var/lock/xclient.lock 2>/dev/null
echo '' >/tmp/geo_ip_update.txt 2>/dev/null
echo 'Checking for latest version' >/tmp/geo_ip_update.txt 2>/dev/null
new_version=`wget -qO- "https://github.com/Loyalsoldier/v2ray-rules-dat/tags"| grep "/Loyalsoldier/v2ray-rules-dat/releases/"| head -n 1| awk -F "/tag/" '{print $2}'| sed 's/\">//'`
rm -rf /tmp/geoip.dat >/dev/null 2>&1
echo 'Latest version: ' $new_version >/tmp/geo_ip_update.txt 2>/dev/null
if [ -f /var/run/geo_ip_down_complete ];then
rm -rf /var/run/geo_ip_down_complete 2>/dev/null
fi
echo 'Starting download...' >/tmp/geo_ip_update.txt 2>/dev/null
wget -c4 --no-check-certificate --timeout=60 --user-agent="Mozilla" https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/"$new_version"/geoip.dat -O /tmp/geoip.dat
sleep 2
if [ "$?" -eq "0" ] && [ "$(ls -l /tmp/geoip.dat |awk '{print int($5)}')" -ne 0 ]; then
touch /var/run/geo_ip_down_complete >/dev/null 2>&1
rm -rf /var/run/geo_ip_update >/dev/null 2>&1
rm -rf /usr/bin/geoip.dat && mv /tmp/geoip.dat /usr/bin
echo $new_version > /usr/share/xclient/geoip_version 2>/dev/null
echo "" > /tmp/geo_ip_update.txt >/dev/null 2>&1
rm -rf /var/lock/xclient.lock 2>/dev/null
if pidof xclient >/dev/null; then
/etc/init.d/xclient boot 2>/dev/null
fi
fi
@@ -0,0 +1 @@
202201242210
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
echo '' >/var/lock/xclient.lock 2>/dev/null
echo '' >/tmp/geo_site_update.txt 2>/dev/null
echo 'Checking for latest version' >/tmp/geo_site_update.txt 2>/dev/null
new_version=`wget -qO- "https://github.com/Loyalsoldier/v2ray-rules-dat/tags"| grep "/Loyalsoldier/v2ray-rules-dat/releases/"| head -n 1| awk -F "/tag/" '{print $2}'| sed 's/\">//'`
rm -rf /tmp/geosite.dat >/dev/null 2>&1
echo 'Latest version: ' $new_version >/tmp/geo_site_update.txt 2>/dev/null
if [ -f /var/run/geo_site_down_complete ];then
rm -rf /var/run/geo_site_down_complete 2>/dev/null
fi
echo 'Starting download...' >/tmp/geo_ip_update.txt 2>/dev/null
wget -c4 --no-check-certificate --timeout=60 --user-agent="Mozilla" https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/"$new_version"/geosite.dat -O /tmp/geosite.dat
if [ "$?" -eq "0" ] && [ "$(ls -l /tmp/geosite.dat |awk '{print int($5)}')" -ne 0 ]; then
touch /var/run/geo_site_down_complete >/dev/null 2>&1
rm -rf /var/run/geo_site_update >/dev/null 2>&1
rm -rf /usr/bin/geosite.dat && mv /tmp/geosite.dat /usr/bin
echo $new_version > /usr/share/xclient/geosite_version 2>/dev/null
echo "" > /tmp/geo_site_update.txt >/dev/null 2>&1
rm -rf /var/lock/xclient.lock 2>/dev/null
if pidof xclient >/dev/null; then
/etc/init.d/xclient boot 2>/dev/null
fi
fi
@@ -0,0 +1 @@
202201242210
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
TAG="_XCLIENT_SPEC_RULE_" # comment tag
IPT="iptables -t nat" # alias of iptables
FWI=$(uci get firewall.xclient.path 2>/dev/null) # firewall include file
usage() {
cat <<-EOF
Usage: xrules [options]
Valid options are:
-s <server_ip> ip address of xclient remote server
-l <local_port> port number of xclient local server
-S <server_ip> ip address of xclient remote UDP server
-L <local_port> port number of xclient local UDP server
define access control mode
-b <wan_ips> wan ip of will be bypassed
-B <bp_lan_ips> lan ip of will be bypassed proxy
-p <fp_lan_ips> lan ip of will be global proxy
-m <Interface> Interface name
-e <extra_options> extra options for iptables
-u enable udprelay mode, TPROXY is required
-U enable udprelay mode, using different IP
and ports for TCP and UDP
-f flush the rules
-h show this help message and exit
EOF
exit $1
}
loger() {
# 1.alert 2.crit 3.err 4.warn 5.notice 6.info 7.debug
logger -st xrules[$$] -p$1 $2
}
flush_r() {
flush_iptables() {
local ipt="iptables -t $1"
local DAT=$(iptables-save -t $1)
eval $(echo "$DAT" | grep "$TAG" | sed -e 's/^-A/$ipt -D/' -e 's/$/;/')
for chain in $(echo "$DAT" | awk '/^:XCLIENT_SPEC/{print $1}'); do
$ipt -F ${chain:1} 2>/dev/null && $ipt -X ${chain:1}
done
}
flush_iptables nat
flush_iptables mangle
ip rule del fwmark 0x01/0x01 table 100 2>/dev/null
ip route del local 0.0.0.0/0 dev lo table 100 2>/dev/null
ipset -X xclient_spec_wan_ac 2>/dev/null
ipset -X fplan 2>/dev/null
ipset -X bplan 2>/dev/null
ipset -X blacklist 2>/dev/null
[ -n "$FWI" ] && echo '#!/bin/sh' >$FWI
return 0
}
ipset_r() {
ipset -! -R <<-EOF || return 1
create xclient_spec_wan_ac hash:net
$(gen_spec_iplist | sed -e "s/^/add xclient_spec_wan_ac /")
EOF
$IPT -N XCLIENT_SPEC_WAN_AC
$IPT -I XCLIENT_SPEC_WAN_AC -p tcp ! --dport 53 -d $server -j RETURN
$IPT -A XCLIENT_SPEC_WAN_AC -m set --match-set xclient_spec_wan_ac dst -j RETURN
$IPT -A XCLIENT_SPEC_WAN_AC -j XCLIENT_SPEC_WAN_FW
ipset -N fplan hash:net 2>/dev/null
for ip in $LAN_FP_IP; do ipset -! add fplan $ip; done
$IPT -I XCLIENT_SPEC_WAN_AC -m set --match-set fplan src -j XCLIENT_SPEC_WAN_FW
ipset -N bplan hash:net 2>/dev/null
for ip in $LAN_BP_IP; do ipset -! add bplan $ip; done
$IPT -I XCLIENT_SPEC_WAN_AC -m set --match-set bplan src -j RETURN
ipset -N blacklist hash:net 2>/dev/null
$IPT -I XCLIENT_SPEC_WAN_AC -m set --match-set blacklist dst -j RETURN
if [ $(ipset list music -name -quiet | grep music) ]; then
$IPT -I XCLIENT_SPEC_WAN_AC -m set --match-set music dst -j RETURN 2>/dev/null
fi
for ip in $WAN_BP_IP; do ipset -! add blacklist $ip; done
return $?
}
fw_rule() {
$IPT -N XCLIENT_SPEC_WAN_FW
$IPT -A XCLIENT_SPEC_WAN_FW -d 0.0.0.0/8 -j RETURN
$IPT -A XCLIENT_SPEC_WAN_FW -d 10.0.0.0/8 -j RETURN
$IPT -A XCLIENT_SPEC_WAN_FW -d 127.0.0.0/8 -j RETURN
$IPT -A XCLIENT_SPEC_WAN_FW -d 169.254.0.0/16 -j RETURN
$IPT -A XCLIENT_SPEC_WAN_FW -d 172.16.0.0/12 -j RETURN
$IPT -A XCLIENT_SPEC_WAN_FW -d 192.168.0.0/16 -j RETURN
$IPT -A XCLIENT_SPEC_WAN_FW -d 224.0.0.0/4 -j RETURN
$IPT -A XCLIENT_SPEC_WAN_FW -d 240.0.0.0/4 -j RETURN
$IPT -A XCLIENT_SPEC_WAN_FW -p tcp -j REDIRECT --to-ports $local_port 2>/dev/null || {
loger 3 "Can't redirect, please check the iptables."
exit 1
}
return $?
}
ac_rule() {
if [ -z "$Interface" ]; then
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j XCLIENT_SPEC_WAN_AC
else
for name in $Interface; do
local IFNAME=$(uci -P /var/state get network.$name.device 2>/dev/null)
[ -n "$IFNAME" ] && $IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS -m comment --comment "$TAG" -j XCLIENT_SPEC_WAN_AC
done
fi
$IPT -I OUTPUT 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j XCLIENT_SPEC_WAN_AC
return $?
}
tp_rule() {
[ -n "$TPROXY" ] || return 0
ip rule add fwmark 0x01/0x01 table 100
ip route add local 0.0.0.0/0 dev lo table 100
local ipt="iptables -t mangle"
$ipt -N XCLIENT_SPEC_TPROXY
$ipt -A XCLIENT_SPEC_TPROXY -p udp --dport 53 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -d 0.0.0.0/8 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -d 10.0.0.0/8 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -d 127.0.0.0/8 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -d 169.254.0.0/16 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -d 172.16.0.0/12 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -d 192.168.0.0/16 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -d 224.0.0.0/4 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -d 240.0.0.0/4 -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp ! --dport 53 -d $SERVER -j RETURN
[ "$server" != "$SERVER" ] && ipset -! add blacklist $SERVER
$ipt -A XCLIENT_SPEC_TPROXY -p udp -m set --match-set bplan src -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -m set --match-set fplan src -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark 0x01/0x01
$ipt -A XCLIENT_SPEC_TPROXY -p udp -m set --match-set xclient_spec_wan_ac dst -j RETURN
$ipt -A XCLIENT_SPEC_TPROXY -p udp -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark 0x01/0x01
if [ -z "$Interface" ]; then
$ipt -I PREROUTING 1 -p udp $EXT_ARGS -m comment --comment "$TAG" -j XCLIENT_SPEC_TPROXY
else
for name in $Interface; do
local IFNAME=$(uci -P /var/state get network.$name.device 2>/dev/null)
[ -n "$IFNAME" ] && $ipt -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p udp $EXT_ARGS -m comment --comment "$TAG" -j XCLIENT_SPEC_TPROXY
done
fi
return $?
}
get_wan_ip() {
cat <<-EOF | grep -E "^([0-9]{1,3}\.){3}[0-9]{1,3}"
$server
$SERVER
$WAN_BP_IP
EOF
}
gen_spec_iplist() {
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.0.0.0/24
192.0.2.0/24
192.88.99.0/24
192.168.0.0/16
198.18.0.0/15
198.51.100.0/24
203.0.113.0/24
224.0.0.0/4
240.0.0.0/4
255.255.255.255
$(get_wan_ip)
EOF
}
gen_include() {
[ -n "$FWI" ] || return 0
extract_rules() {
echo "*$1"
iptables-save -t $1 | grep XCLIENT_SPEC_ | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/"
echo 'COMMIT'
}
cat <<-EOF >>$FWI
iptables-save -c | grep -v "XCLIENT_SPEC" | iptables-restore -c
iptables-restore -n <<-EOT
$(extract_rules nat)
$(extract_rules mangle)
EOT
EOF
return 0
}
while getopts ":m:s:l:S:L:e:B:b:p:D:uUfh" arg; do
case "$arg" in
m)
Interface=$OPTARG
;;
s)
server=$OPTARG
;;
l)
local_port=$OPTARG
;;
S)
SERVER=$OPTARG
;;
L)
LOCAL_PORT=$OPTARG
;;
e)
EXT_ARGS=$OPTARG
;;
B)
LAN_BP_IP=$OPTARG
;;
b)
WAN_BP_IP=$(for ip in $OPTARG; do echo $ip; done)
;;
p)
LAN_FP_IP=$OPTARG
;;
u)
TPROXY=1
;;
U)
TPROXY=2
;;
f)
flush_r
exit 0
;;
h) usage 0 ;;
esac
done
if [ -z "$server" -o -z "$local_port" ]; then
usage 2
fi
case "$TPROXY" in
1)
SERVER=$server
LOCAL_PORT=$local_port
;;
2)
: ${SERVER:?"You must assign an ip for the udp relay server."}
: ${LOCAL_PORT:?"You must assign a port for the udp relay server."}
;;
esac
flush_r && fw_rule && ipset_r && ac_rule && tp_rule && gen_include
RET=$?
[ "$RET" = 0 ] || loger 3 "Start failed!"
exit $RET
@@ -0,0 +1 @@
1
+70
View File
@@ -0,0 +1,70 @@
#!/bin/sh
. $IPKG_INSTROOT/etc/init.d/xclient
LOCK_FILE="/var/lock/monitor.lock"
[ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
redir_tcp_process=$1
redir_udp_process=$2
pdnsd_process=$3
if [ -z "$pdnsd_process" ]; then
pdnsd_process=0
fi
i=0
GLOBAL_SERVER=$(uci_get_by_type global global_server)
server=$(uci_get_by_name $GLOBAL_SERVER server)
server_port=$(uci_get_by_name $GLOBAL_SERVER server_port)
while [ "1" == "1" ]; do
sleep 1
#redir tcp
if [ "$redir_tcp_process" -gt 0 ]; then
icount=$(busybox ps -w | grep xclient-retcp | grep -v grep | wc -l)
if [ "$icount" == 0 ]; then
logger -t "$NAME" "xclient redir tcp error.restart!"
echolog "xclient redir tcp error.restart!"
/etc/init.d/xclient reload
exit 0
fi
fi
#redir udp
if [ "$redir_udp_process" -gt 0 ]; then
icount=$(busybox ps -w | grep xclient-reudp | grep -v grep | wc -l)
if [ "$icount" == 0 ]; then
logger -t "$NAME" "xclient redir udp error.restart!"
echolog "xclient redir udp error.restart!"
/etc/init.d/xclient reload
exit 0
fi
fi
#pdnsd
if [ "$pdnsd_process" -eq 1 ]; then
icount=$(busybox ps -w | grep $TMP_BIN_PATH/pdnsd | grep -v grep | wc -l)
if [ "$icount" -lt "$pdnsd_process" ]; then
logger -t "$NAME" "pdnsd tunnel error.restart!"
echolog "pdnsd tunnel error.restart!"
if [ -f /var/run/pdnsd.pid ]; then
kill $(cat /var/run/pdnsd.pid) >/dev/null 2>&1
else
kill -9 $(ps | grep $TMP_PATH/pdnsd.conf | grep -v grep | awk '{print $1}') >/dev/null 2>&1
fi
ln_start_bin $(first_type pdnsd) pdnsd -c $TMP_PATH/pdnsd.conf
fi
fi
#dns2socks
if [ "$pdnsd_process" -eq 2 ]; then
icount=$(busybox ps -w | grep -e xclient-dns -e "dns2socks 127.0.0.1 $tmp_dns_port" | grep -v grep | wc -l)
if [ "$icount" -lt 2 ]; then
logger -t "$NAME" "dns2socks $dnsstr tunnel error.restart!"
echolog "dns2socks $dnsstr tunnel error.restart!"
dnsstr=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
dnsserver=$(echo "$dnsstr" | awk -F ':' '{print $1}')
dnsport=$(echo "$dnsstr" | awk -F ':' '{print $2}')
kill -9 $(busybox ps -w | grep xclient-dns | grep -v grep | awk '{print $1}') >/dev/null 2>&1
kill -9 $(busybox ps -w | grep "dns2socks 127.0.0.1 $tmp_dns_port" | grep -v grep | awk '{print $1}') >/dev/null 2>&1
ln_start_bin $(first_type microsocks) microsocks -i 127.0.0.1 -p $tmp_dns_port xclient-dns
ln_start_bin $(first_type dns2socks) dns2socks 127.0.0.1:$tmp_dns_port $dnsserver:$dnsport 127.0.0.1:$dns_port -q
fi
fi
done
@@ -0,0 +1,544 @@
#!/usr/bin/lua
require "luci.model.uci"
require "nixio"
require "luci.util"
require "luci.sys"
require "luci.jsonc"
-- these global functions are accessed all the time by the event handler
-- so caching them is worth the effort
local tinsert = table.insert
local ssub, slen, schar, sbyte, sformat, sgsub = string.sub, string.len, string.char, string.byte, string.format, string.gsub
local jsonParse, jsonStringify = luci.jsonc.parse, luci.jsonc.stringify
local b64decode = nixio.bin.b64decode
local cache = {}
local nodeResult = setmetatable({}, {__index = cache}) -- update result
local name = 'xclient'
local uciType = 'servers'
local ucic = luci.model.uci.cursor()
local proxy = '0'
local switch = '0'
local subscribe_url = ucic:get(name, 'config', 'subscribe_url')
local filter_words = '过期时间/剩余流量'
local save_words = ''
local v2_ss = "xray"
local v2_tj = "xray"
local log = function(...)
print(os.date("%Y-%m-%d %H:%M:%S ") .. table.concat({...}, " "))
end
local encrypt_methods_ss = {
"aes-128-gcm",
"aes-192-gcm",
"aes-256-gcm",
"chacha20-ietf-poly1305",
"xchacha20-ietf-poly1305"
}
local function split(full, sep)
full = full:gsub("%z", "")
local off, result = 1, {}
while true do
local nStart, nEnd = full:find(sep, off)
if not nEnd then
local res = ssub(full, off, slen(full))
if #res > 0 then
tinsert(result, res)
end
break
else
tinsert(result, ssub(full, off, nStart - 1))
off = nEnd + 1
end
end
return result
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:gsub("+", " "):gsub("%%(%x%x)", get_urldecode)
end
-- trim
local function trim(text)
if not text or text == "" then
return ""
end
return (sgsub(text, "^%s*(.-)%s*$", "%1"))
end
-- md5
local function md5(content)
local stdout = luci.sys.exec('echo \"' .. urlEncode(content) .. '\" | md5sum | cut -d \" \" -f1')
-- assert(nixio.errno() == 0)
return trim(stdout)
end
-- base64
local function base64Decode(text)
local raw = text
if not text then
return ''
end
text = text:gsub("%z", "")
text = text:gsub("_", "/")
text = text:gsub("-", "+")
local mod4 = #text % 4
text = text .. string.sub('====', mod4 + 1)
local result = b64decode(text)
if result then
return result:gsub("%z", "")
else
return raw
end
end
local function checkTabValue(tab)
local revtab = {}
for k,v in pairs(tab) do
revtab[v] = true
end
return revtab
end
local function processData(szType, content)
local result = {type = szType}
if szType == 'vmess' then
local info = jsonParse(content)
result.type = 'xray'
result.protocol = 'vmess'
result.server = info.add
result.server_port = info.port
result.transport = info.net
result.vmess_id = info.id
result.alias = info.ps
-- result.mux = 1
-- result.concurrency = 8
if info.net == 'ws' then
result.ws_host = info.host
result.ws_path = info.path
end
if info.net == 'h2' then
result.h2_host = info.host
result.h2_path = info.path
end
if info.net == 'quic' then
result.quic_guise = "none"
result.quic_key = ""
result.quic_security = "none"
end
if info.net == 'grpc' then
result.serviceName = info.serviceName
end
if info.net == 'tcp' then
if info.type and info.type ~= "http" then
info.type = "none"
end
result.tcp_guise = info.type
result.http_host = info.host
result.http_path = info.path
end
if info.net == 'kcp' then
result.kcp_guise = info.type
result.mtu = 1350
result.tti = 50
result.uplink_capacity = 5
result.downlink_capacity = 20
result.read_buffer_size = 2
result.write_buffer_size = 2
end
if info.security then
result.security = info.security
end
if info.tls == "tls" or info.tls == "1" then
result.security = "tls"
result.tls_host = info.sni
result.insecure = 1
else
result.security = "none"
end
elseif szType == "ss" then
local idx_sp = 0
local alias = ""
if content:find("#") then
idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
end
local info = content:sub(1, idx_sp - 1)
local hostInfo = split(base64Decode(info), "@")
local host = split(hostInfo[2], ":")
local userinfo = base64Decode(hostInfo[1])
local method = userinfo:sub(1, userinfo:find(":") - 1)
local password = userinfo:sub(userinfo:find(":") + 1, #userinfo)
result.alias = UrlDecode(alias)
result.type = v2_ss
result.password = password
result.server = host[1]
result.transport = "tcp"
result.protocol = "shadowsocks"
if host[2]:find("/%?") then
local query = split(host[2], "/%?")
result.server_port = query[1]
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
result.protocol = "shadowsocks-plugin"
end
else
result.server_port = host[2]:gsub("/","")
end
result.encrypt_method_v2ray_ss = method
elseif szType == "trojan" then
local idx_sp = 0
local alias = ""
if content:find("#") then
idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
end
local info = content:sub(1, idx_sp - 1)
local hostInfo = split(info, "@")
local host = split(hostInfo[2], ":")
local userinfo = hostInfo[1]
local password = userinfo
result.alias = UrlDecode(alias)
result.type = v2_tj
result.protocol = "trojan"
result.server = host[1]
if host[2]:find("?") then
local query = split(host[2], "?")
result.server_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local t = split(v, '=')
params[t[1]] = t[2]
end
if params.obfs == "grpc" and params.security == "none" then
result.security = "none"
else
result.security = "tls"
end
if params.sni then
result.tls_host = params.sni
end
if params.allowInsecure then
result.insecure = params.allowInsecure
end
if params.obfs then
if params.obfs == "grpc" then
result.transport = params.obfs
result.serviceName = params.serviceName
end
if params.obfs == "websocket" then
result.transport = "ws"
result.ws_host = params.obfsParam
result.ws_path = params.path
end
else
result.transport = "tcp"
end
else
result.server_port = host[2]
end
result.password = password
elseif szType == "vless" then
local idx_sp = 0
local alias = ""
if content:find("#") then
idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
end
local info = content:sub(1, idx_sp - 1)
local hostInfo = split(info, "@")
local host = split(hostInfo[2], ":")
local uuid = hostInfo[1]
if host[2]:find("?") then
local query = split(host[2], "?")
local params = {}
for _, v in pairs(split(UrlDecode(query[2]), '&')) do
local t = split(v, '=')
params[t[1]] = t[2]
end
result.alias = UrlDecode(alias)
result.type = 'v2ray'
result.protocol = 'vless'
result.server = host[1]
result.server_port = query[1]
result.vmess_id = uuid
result.vless_encryption = params.encryption or "none"
result.transport = params.type and (params.type == 'http' and 'h2' or params.type) or "tcp"
if not params.type or params.type == "tcp" then
result.vless_flow = params.flow
end
if params.type == 'ws' then
result.ws_host = params.host
result.ws_path = params.path or "/"
end
if params.type == 'http' then
result.h2_host = params.host
result.h2_path = params.path or "/"
end
if params.type == 'kcp' then
result.kcp_guise = params.headerType or "none"
result.mtu = 1350
result.tti = 50
result.uplink_capacity = 5
result.downlink_capacity = 20
result.read_buffer_size = 2
result.write_buffer_size = 2
result.seed = params.seed or ""
end
if info.net == 'quic' then
result.quic_guise = "none"
result.quic_key = ""
result.quic_security = "none"
end
if params.type == 'grpc' then
result.serviceName = params.serviceName
end
if params.security == "tls" then
result.security = "tls"
result.tls_host = params.sni
elseif params.security == "reality" then
result.security = "reality"
result.tls_host = params.sni
result.publicKey = params.publicKey
result.shortId = params.shortId
result.fingerprint = params.fingerprint
result.spiderX = params.spiderX
else
result.security = "none"
end
else
result.server_port = host[2]
end
end
if not result.alias then
if result.server and result.server_port then
result.alias = result.server .. ':' .. result.server_port
else
result.alias = "NULL"
end
end
local alias = result.alias
result.alias = nil
result.hashkey = md5(jsonStringify(result))
result.alias = alias
return result
end
-- wget
local function wget(url)
local stdout = luci.sys.exec('uclient-fetch -q --user-agent="Luci-app-xclient/OpenWRT" --no-check-certificate -O- "' .. url .. '"')
return trim(stdout)
end
local function check_filer(result)
do
local filter_word = split(filter_words, "/")
local check_save = false
if save_words ~= nil and save_words ~= "" and save_words ~= "NULL" then
check_save = true
end
local save_word = split(save_words, "/")
local filter_result = false
local save_result = true
for i, v in pairs(filter_word) do
if tostring(result.alias):find(v) then
filter_result = true
end
end
if check_save == true then
for i, v in pairs(save_word) do
if tostring(result.alias):find(v) then
save_result = false
end
end
else
save_result = false
end
if filter_result == true or save_result == true then
return true
else
return false
end
end
end
local execute = function()
-- exec
do
if proxy == '0' then
log('service is pausing..')
luci.sys.init.stop(name)
end
if subscribe_url then
local raw = wget(subscribe_url)
if #raw > 0 then
local nodes, szType
local groupHash = md5(subscribe_url)
cache[groupHash] = {}
tinsert(nodeResult, {})
local index = #nodeResult
if jsonParse(raw) then
nodes = jsonParse(raw).servers or jsonParse(raw)
if nodes[1].server and nodes[1].method then
szType = 'sip008'
end
else
nodes = split(base64Decode(raw):gsub(" ", "_"), "\n")
end
for _, v in ipairs(nodes) do
if v then
local result
if szType then
result = processData(szType, v)
elseif not szType then
local node = trim(v)
local dat = split(node, "://")
if dat and dat[1] and dat[2] then
local dat3 = ""
if dat[3] then
dat3 = "://" .. dat[3]
end
if dat[1] == 'ss' or dat[1] == 'trojan' then
result = processData(dat[1], dat[2] .. dat3)
else
result = processData(dat[1], base64Decode(dat[2]))
end
end
else
log('skip unknown types: ' .. szType)
end
-- log(result)
if result then
if not result.server or not result.server_port or result.alias == "NULL" or check_filer(result) or result.server:match("[^0-9a-zA-Z%-%.%s]") or cache[groupHash][result.hashkey] then
log('Discard invalid nodes: 【' .. result.type .. ' 】, ' .. result.alias)
else
result.grouphashkey = groupHash
tinsert(nodeResult[index], result)
cache[groupHash][result.hashkey] = nodeResult[index][#nodeResult[index]]
end
end
end
end
log('Number of nodes successfully resolved: ' .. #nodes)
else
log(subscribe_url .. ': get content is empty')
end
end
end
-- diff
do
if next(nodeResult) == nil then
log("Update failed, no node info available")
return
end
local add, del = 0, 0
ucic:foreach(name, uciType, function(old)
if old.grouphashkey or old.hashkey then
if not nodeResult[old.grouphashkey] or not nodeResult[old.grouphashkey][old.hashkey] then
ucic:delete(name, old['.name'])
del = del + 1
else
local dat = nodeResult[old.grouphashkey][old.hashkey]
ucic:tset(name, old['.name'], dat)
setmetatable(nodeResult[old.grouphashkey][old.hashkey], {__index = {_ignore = true}})
end
else
if not old.alias then
if old.server or old.server_port then
old.alias = old.server .. ':' .. old.server_port
log('Ignore manually added nodes: ' .. old.alias)
else
ucic:delete(name, old['.name'])
end
else
log('Ignore manually added nodes: ' .. old.alias)
end
end
end)
for k, v in ipairs(nodeResult) do
for kk, vv in ipairs(v) do
if not vv._ignore then
local section = ucic:add(name, uciType)
ucic:tset(name, section, vv)
add = add + 1
end
end
end
ucic:commit(name)
local globalServer = ucic:get_first(name, 'global', 'global_server', '')
if globalServer ~= "nil" then
local firstServer = ucic:get_first(name, uciType)
if firstServer then
if not ucic:get(name, globalServer) then
luci.sys.call("/etc/init.d/" .. name .. " stop > /dev/null 2>&1 &")
ucic:commit(name)
ucic:set(name, ucic:get_first(name, 'global'), 'global_server', ucic:get_first(name, uciType))
ucic:commit(name)
log('The current active server has been deleted and is being automatically replaced with the first node。')
luci.sys.call("/etc/init.d/" .. name .. " start > /dev/null 2>&1 &")
else
log('Maintain the current servers。')
luci.sys.call("/etc/init.d/" .. name .. " boot > /dev/null 2>&1 &")
end
else
log('There is no server node, stop the service')
luci.sys.call("/etc/init.d/" .. name .. " stop > /dev/null 2>&1 &")
end
end
log('New Nodes: ' .. add, 'Deleted Nodes: ' .. del)
log('Subscription updated successfully')
end
end
if subscribe_url and #subscribe_url > 0 then
xpcall(execute, function(e)
log(e)
log(debug.traceback())
log('An error occurred, restoring service')
local firstServer = ucic:get_first(name, uciType)
if firstServer then
luci.sys.call("/etc/init.d/" .. name .. " boot > /dev/null 2>&1 &")
log('Service Restart Successful')
else
luci.sys.call("/etc/init.d/" .. name .. " stop > /dev/null 2>&1 &")
log('Service Stop Successful')
end
end)
end
@@ -0,0 +1 @@