Files
xray-manager/xray-manager.sh
T
pyrschtjag 43d55eda8b feat: added proxy support
feat: minor updates and fixes
2026-07-14 01:51:05 +04:00

857 lines
30 KiB
Bash
Executable File

#!/bin/sh
. /lib/functions.sh
readonly GLOBAL_CTXKEY="__global__"
readonly LIST_TYPE_MAP="ip:ip:0 dom:domain:1 uip:url_ip:0 udom:url_domain:1"
readonly SRC_LIST_SUFFIXES="ip uip"
readonly ALL_LIST_SUFFIXES="ip dom uip udom"
readonly DEBUG=0
readonly DEBUG_LOG="/tmp/xray-manager.log"
_log() {
local message="$1"
local level="${2:-info}"
local to_console="${3:-1}"
[ "$level" = "debug" ] && [ "$DEBUG" -ne 1 ] && return
if [ "$DEBUG" = "1" ] && [ -n "$DEBUG_LOG" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $message" >> "$DEBUG_LOG"
fi
local syslog_level="$level"
[ "$level" = "warn" ] && syslog_level="warning"
[ "$level" != "debug" ] && logger -t xray-manager -p "daemon.$syslog_level" "$message"
if [ "$to_console" = "1" ]; then
printf "%s\n" "$message" >&2
fi
}
_get_md5() { echo "$1" | md5sum | cut -d' ' -f1; }
_file_md5() { [ -s "$1" ] && md5sum "$1" | cut -d' ' -f1; }
_is_url_type() {
case "$1" in
u*) return 0 ;;
*) return 1 ;;
esac
}
_sanitize_name() {
local __out_varname="$1" __source_value="$2"
__source_value=${__source_value//[^a-zA-Z0-9_-]/}
__source_value=${__source_value//-/_}
eval "$__out_varname=\"\$__source_value\""
}
_collect_list() {
local __out_varname="$1" __section="$2" __option="$3" __accum=""
__collect_list_cb() { __accum="${__accum}${__accum:+ }$1"; }
config_list_foreach "$__section" "$__option" __collect_list_cb
[ -z "$__accum" ] && config_get __accum "$__section" "$__option"
eval "$__out_varname=\"\$__accum\""
}
_curl_proxy_args() {
[ -n "$CURL_PROXY" ] || return
printf -- "--proxy %s " "$CURL_PROXY"
[ -n "$CURL_PROXY_USER" ] && printf -- "--proxy-user %s:%s " "$CURL_PROXY_USER" "$CURL_PROXY_PASS"
}
_fetch_url() {
local id="$1" url="$2"
local url_hash=$(echo "$url" | md5sum | cut -c1-8)
local cache_raw="$CACHE_DIR/${id}_$url_hash.raw"
local etag_file="$CACHE_DIR/${id}_$url_hash.etag"
local header_tmp="$CACHE_DIR/h_${id}_$url_hash"
local body_tmp="$CACHE_DIR/dl_${id}_${url_hash}.raw"
local etag=$(cat "$etag_file" 2>/dev/null)
curl -4 -sSfL --connect-timeout 15 --retry-connrefused --retry-delay 5 $(_curl_proxy_args) ${etag:+-H "If-None-Match: \"$etag\""} -D "$header_tmp" "$url" > "$body_tmp"
local curl_status=$?
if grep -qE '^HTTP/[0-9.]+ +304' "$header_tmp" 2>/dev/null; then
_log "$id: 304 not modified" "info"
rm -f "$body_tmp" "$header_tmp"
[ -f "$cache_raw" ] && { cat "$cache_raw"; return 0; }
return 1
elif [ "$curl_status" -eq 0 ] && [ -s "$body_tmp" ]; then
_log "$id: 200 ok" "info"
grep -i "^etag:" "$header_tmp" | awk -F': ' '{print $2}' | sed 's/W\///; s/["\r\n ]//g' > "$etag_file"
cp "$body_tmp" "$cache_raw"
cat "$body_tmp"
rm -f "$body_tmp" "$header_tmp"
return 0
else
_log "$id: download failed ($curl_status), using local cache" "warn"
rm -f "$body_tmp" "$header_tmp"
[ -f "$cache_raw" ] && { cat "$cache_raw"; return 0; }
return 1
fi
}
_cfg_get_default() {
local __out_varname="$1" __option="$2" __default="$3" __value
__value=$(uci -q get xray-manager."$MAIN_SECTION"."$__option")
[ -z "$__value" ] && __value="$__default"
eval "$__out_varname"="${__value:-$__default}"
}
_init_vars() {
_log "config: loading configuration" "debug"
config_load xray-manager
MAIN_SECTION="main"
_cfg_get_default TABLE "table_name"
_cfg_get_default TPROXY_PORT "tproxy_port"
_cfg_get_default TPROXY_MARK "tproxy_mark"
_cfg_get_default CLIENT_MARK "client_mark"
_cfg_get_default RT_TABLE "rt_table"
_cfg_get_default TIMEOUT "timeout"
_cfg_get_default EXCLUDE_MARK "exclude_mark" "0x1"
_cfg_get_default PRIO_EXC "prio_exc" "-200"
_cfg_get_default PRIO_PROXY "prio_proxy" "-150"
_cfg_get_default PRIO_OUTPUT "prio_output" "-100"
_cfg_get_default IPNET_DIR "ipnet_dir" '/tmp/.xray-manager/ipnet'
_cfg_get_default DNSMASQ_DIR "dnsmasq_dir" '/tmp/.xray-manager/dnsmasq.d'
_cfg_get_default CACHE_DIR "cache_dir" '/tmp/.xray-manager/cache'
_cfg_get_default ACTIVE_LIST "active_list" '/tmp/.xray-manager/active_files'
_cfg_get_default DOMSTAGE_DIR "domstage_dir" "$CACHE_DIR/domstage"
_cfg_get_default CTX_DIR "ctx_dir" "$CACHE_DIR/ctx"
_cfg_get_default CURL_PROXY "curl_proxy"
_cfg_get_default CURL_PROXY_USER "curl_proxy_user"
_cfg_get_default CURL_PROXY_PASS "curl_proxy_pass"
local required_vars="TABLE TPROXY_PORT TPROXY_MARK CLIENT_MARK RT_TABLE TIMEOUT"
for var in $required_vars; do
eval "val=\$$var"
if [ -z "$val" ]; then
_log "config: variable '$var' is missing" "crit"
return 1
fi
done
if [ "$((EXCLUDE_MARK))" -eq "$((TPROXY_MARK))" ] 2>/dev/null; then
_log "config: exclude_mark ($EXCLUDE_MARK) must differ from tproxy_mark ($TPROXY_MARK)" "crit"
return 1
fi
_log "config: loaded table=$TABLE, port=$TPROXY_PORT, mark=$TPROXY_MARK" "debug"
mkdir -p "$IPNET_DIR" "$DNSMASQ_DIR" "$CACHE_DIR" "$DOMSTAGE_DIR" "$CTX_DIR" "${ACTIVE_LIST%/*}"
if [ "$DEBUG" = "1" ] && [ -n "$DEBUG_LOG" ]; then
: > "$DEBUG_LOG" 2>/dev/null
fi
return 0
}
_get_src_names() {
local section="$1" name
config_get name "$section" name
if [ -n "$name" ]; then
_sanitize_name name "$name"
case " $existing_src_names " in
*" $name "*)
_log "config: duplicate xray-src name '$name' - source jump chains require unique names!" "crit"
has_errors=1
;;
esac
existing_src_names="${existing_src_names}${existing_src_names:+ }$name"
fi
}
_get_dst_names() {
local section="$1" name
config_get name "$section" name
if [ -n "$name" ]; then
_sanitize_name name "$name"
case " $existing_dst_names " in
*" $name "*)
_log "config: duplicate xray-dst name '$name' - names must be unique within xray-dst!" "crit"
has_errors=1
;;
esac
existing_dst_names="${existing_dst_names}${existing_dst_names:+ }$name"
fi
}
_get_out_names() {
local section="$1" name
config_get name "$section" name
if [ -n "$name" ]; then
_sanitize_name name "$name"
case " $existing_out_names " in
*" $name "*)
_log "config: duplicate xray-out name '$name' - names must be unique within xray-out!" "crit"
has_errors=1
;;
esac
existing_out_names="${existing_out_names}${existing_out_names:+ }$name"
fi
}
_validate_list_section() {
local section="$1" name type
config_get name "$section" name
config_get type "$section" TYPE
if [ -z "$name" ]; then
_log "config: section [$section] is missing 'name' option" "crit"
has_errors=1
return
fi
local config_option
for config_option in $(uci show xray-manager."$section" | cut -d'.' -f3 | cut -d'=' -f1 | sort -u); do
case "$config_option" in
.*) continue ;;
esac
if ! echo "$allowed_opts" | grep -qw "$config_option"; then
_log "config: section [$section] ($name) has unknown option: '$config_option'" "err"
has_errors=1
fi
done
case "$type" in
xray-dst|xray-src)
local parent_list
_collect_list parent_list "$section" parent
if [ -n "$parent_list" ]; then
local parent parent_clean
for parent in $parent_list; do
_sanitize_name parent_clean "$parent"
if ! echo "$existing_src_names" | grep -qw "$parent_clean"; then
_log "config: section [$section] ($name) contains parent '$parent' which does not exist as a valid xray-src!" "crit"
has_errors=1
fi
done
fi
;;
esac
}
_validate_config() {
_log "config: validating uci syntax and parents" "debug"
if [ -f "/etc/config/xray-manager" ]; then
sed -i 's/^ \+/ /g' /etc/config/xray-manager
fi
local dns_confdir
dns_confdir=$(uci -q get dhcp.@dnsmasq[0].confdir)
if [ -z "$dns_confdir" ] || ! echo "$dns_confdir" | grep -q "$DNSMASQ_DIR"; then
_log "config: $DNSMASQ_DIR is not set in dnsmasq" "warn"
fi
local allowed_opts="name exclude parent ip domain url_ip url_domain"
local has_errors=0
local existing_src_names=""
local existing_dst_names=""
local existing_out_names=""
config_foreach _get_src_names "xray-src"
config_foreach _get_dst_names "xray-dst"
config_foreach _get_out_names "xray-out"
config_foreach _validate_list_section "xray-src"
config_foreach _validate_list_section "xray-dst"
config_foreach _validate_list_section "xray-out"
if [ "$has_errors" = "1" ]; then
_log "config: validation failed! core configuration errors detected. aborting startup." "crit"
exit 1
fi
_log "config: validation complete" "info"
}
_nft_init() {
local mode="$1"
if ! nft list table ip "$TABLE" >/dev/null 2>&1; then
mode="full"
fi
if [ "$mode" = "full" ]; then
_log "$TABLE: full reset of nft table" "info"
nft delete table ip "$TABLE" 2>/dev/null
nft add table ip "$TABLE"
nft add chain ip "$TABLE" prerouting_exc { type filter hook prerouting priority $PRIO_EXC\; policy accept\; }
nft add chain ip "$TABLE" prerouting_proxy { type filter hook prerouting priority $PRIO_PROXY\; policy accept\; }
nft add chain ip "$TABLE" output { type route hook output priority $PRIO_OUTPUT\; policy accept\; }
nft add rule ip "$TABLE" prerouting_exc fib daddr type local accept
nft add rule ip "$TABLE" prerouting_proxy fib daddr type local accept
nft add rule ip "$TABLE" prerouting_proxy meta mark $EXCLUDE_MARK return
nft add rule ip "$TABLE" prerouting_proxy meta mark $TPROXY_MARK meta l4proto { tcp, udp } tproxy to 127.0.0.1:"$TPROXY_PORT" accept
rm -f "$CTX_DIR"/*.sig "$CTX_DIR/active.prev" 2>/dev/null
else
_log "$TABLE: keeping existing nft table" "debug"
fi
echo "$mode"
}
_nft_prepare_data() {
local id="$1" file="$2" type="$3"
local marker="$CACHE_DIR/${id}.nftok"
local file_hash=$(_file_md5 "$file")
local prev_hash=$(cat "$marker" 2>/dev/null)
local set_exists=1
nft list set ip "$TABLE" "$id" >/dev/null 2>&1 || set_exists=0
if [ "$set_exists" = "1" ] && [ -n "$file_hash" ] && [ "$file_hash" = "$prev_hash" ]; then
_log "$id: nft set unchanged, skipping flush" "debug"
return
fi
local set_params="type ipv4_addr; flags interval; auto-merge;"
case "$type" in
*dom) set_params="type ipv4_addr; flags interval,timeout; timeout $TIMEOUT; auto-merge;" ;;
esac
nft add set ip "$TABLE" "$id" { $set_params } 2>/dev/null
nft flush set ip "$TABLE" "$id"
case "$type" in
*ip*)
if [ -s "$file" ]; then
{
printf "add element ip %s %s { " "$TABLE" "$id"
awk '
/^[0-9]/ {
addr = ($1 ~ /\// ? $1 : $1"/32");
printf "%s%s", (count++ ? ", " : ""), addr
}' "$file"
printf " }\n"
} | nft -f -
fi
;;
esac
echo "$file_hash" > "$marker"
_log "$id: nft set (re)populated" "info"
}
_item_file_exists() {
local id="$1"
[ -f "$IPNET_DIR/$id.lst" ] || [ -f "$DOMSTAGE_DIR/$id.lst" ]
}
_existing_ids() {
local name="$1" direction="$2"; shift 2
local suffix id
for suffix in "$@"; do
id="${name}_${suffix}_${direction}"
_item_file_exists "$id" && echo "$id"
done
}
_process_item() {
local id="$1" value="$2" type="$3"
local fullpath hash_file changed=0
case "$type" in
*dom) fullpath="$DOMSTAGE_DIR/$id.lst" ;;
*) fullpath="$IPNET_DIR/$id.lst" ;;
esac
hash_file="$CACHE_DIR/$id.hash"
echo "$fullpath" >> "$ACTIVE_LIST"
echo "$hash_file" >> "$ACTIVE_LIST"
local new_hash=$(_get_md5 "$value")
local old_hash=$(cat "$hash_file" 2>/dev/null)
local status="stable"
if [ "$new_hash" != "$old_hash" ]; then
status="changed"
if [ "$SKIP_URL" = "1" ] && _is_url_type "$type"; then
NEED_UPDATE=1
fi
fi
_log "$id status: $status" "debug"
if [ "$SKIP_URL" = "1" ] && _is_url_type "$type"; then
if [ ! -f "$fullpath" ]; then
_log "$id: data file missing (reload mode), skipping" "warn"
return 1
fi
_log "$id: using local file (reload mode)" "debug"
_nft_prepare_data "$id" "$fullpath" "$type"
return 1
fi
if [ "$new_hash" != "$old_hash" ] || [ ! -f "$fullpath" ] || [ "$SKIP_URL" = "0" ]; then
case "$type" in
ip)
echo "$value" | tr ' ' '\n' | sed -e 's/\r//g' -e '/^#/d' -e '/^[[:space:]]*$/d' -e '/\//! s|$|/32|' | sort -u > "$fullpath.tmp"
;;
dom)
echo "$value" | tr ' ' '\n' | awk '/^[[:space:]]*#/ || /^[[:space:]]*$/ { next } { gsub(/\r/, ""); print tolower($1) }' | sort -u > "$fullpath.tmp"
;;
uip|udom)
_log "$id: fetching remote list" "info"
local tmp_all="$CACHE_DIR/dl_$id"
: > "$tmp_all"
local download_ok=0
local part_files="" pids="" n=0 part pid
for url in $value; do
n=$((n + 1))
part="$tmp_all.part${n}.$$"
part_files="${part_files}${part_files:+ }$part"
_log "$id: downloading $url" "debug"
_fetch_url "$id" "$url" > "$part" &
pids="${pids}${pids:+ }$!"
done
for pid in $pids; do
wait "$pid" && download_ok=1
done
cat $part_files > "$tmp_all" 2>/dev/null
rm -f $part_files
if [ "$download_ok" = "1" ] && [ -s "$tmp_all" ]; then
if [ "$type" = "uip" ]; then
sed -e 's/\r//g' -e '/^#/d' -e '/^[[:space:]]*$/d' "$tmp_all" | awk '{ print ($1 ~ /\// ? $1 : $1"/32") }' | sort -u > "$fullpath.tmp"
else
tr 'A-Z' 'a-z' < "$tmp_all" | grep -oE '([a-z0-9-]+\.)+[a-z]{2,}' | sort -u > "$fullpath.tmp"
fi
fi
;;
esac
if [ -s "$fullpath.tmp" ] && ! cmp -s "$fullpath.tmp" "$fullpath"; then
mv "$fullpath.tmp" "$fullpath"
changed=1
_log "$id: list updated" "info"
else
rm -f "$fullpath.tmp"
fi
echo "$new_hash" > "$hash_file"
fi
_nft_prepare_data "$id" "$fullpath" "$type"
if [ "$changed" = "1" ]; then
return 0
else
return 1
fi
}
_process_prepare() {
local section="$1" name="$2" direction="$3"
local map_entry list_type option_name affects_dns value
for map_entry in $LIST_TYPE_MAP; do
list_type="${map_entry%%:*}"
option_name="${map_entry#*:}"; option_name="${option_name%%:*}"
affects_dns="${map_entry##*:}"
_collect_list value "$section" "$option_name"
if [ -n "$value" ]; then
if _process_item "${name}_${list_type}_${direction}" "$value" "$list_type"; then
[ "$affects_dns" = "1" ] && DNS_CHANGES=1
fi
fi
done
}
_process_section() {
local section="$1" mode="$2" name direction type
config_get name "$section" name
_sanitize_name name "$name"
[ -z "$name" ] && return
config_get type "$section" TYPE
case "$type" in
xray-src) direction="src" ;;
xray-out) direction="out" ;;
*) direction="dst" ;;
esac
case "$mode" in
prepare)
_process_prepare "$section" "$name" "$direction"
;;
apply_out)
[ "$direction" = "out" ] || return 0
local exclude_flag; config_get exclude_flag "$section" exclude "0"
[ "$exclude_flag" = "1" ] && return 0
local suffix item
for suffix in $ALL_LIST_SUFFIXES; do
item="${name}_${suffix}_${direction}"
_item_file_exists "$item" || continue
_log "nft: output rule for $item" "debug"
nft add rule ip "$TABLE" output ip daddr @"$item" meta mark set "$TPROXY_MARK" accept
done
;;
apply_out_exc)
[ "$direction" = "out" ] || return 0
local exclude_flag; config_get exclude_flag "$section" exclude "0"
[ "$exclude_flag" = "1" ] || return 0
local suffix item
for suffix in $ALL_LIST_SUFFIXES; do
item="${name}_${suffix}_${direction}"
_item_file_exists "$item" || continue
_log "nft: output exclusion for $item" "debug"
nft add rule ip "$TABLE" output ip daddr @"$item" return
done
;;
*)
_log "config: unknown process mode: $mode" "err"
;;
esac
}
_in_scope() {
local context="$1" parent_list="$2"
if [ -z "$context" ]; then
[ -z "$parent_list" ]
return
fi
local parent parent_clean
for parent in $parent_list; do
_sanitize_name parent_clean "$parent"
[ "$parent_clean" = "$context" ] && return 0
done
return 1
}
_nft_scope_src() {
local section="$1" context="$2" want_exclude="$3"
local name exclude_flag parent_list
config_get name "$section" name
_sanitize_name name "$name"
[ -z "$name" ] && return
config_get exclude_flag "$section" exclude "0"
[ "$exclude_flag" != "1" ] && exclude_flag="0"
[ "$exclude_flag" = "$want_exclude" ] || return
_collect_list parent_list "$section" parent
_in_scope "$context" "$parent_list" || return
local id found=0
if [ "$exclude_flag" = "1" ]; then
for id in $(_existing_ids "$name" "src" $SRC_LIST_SUFFIXES); do
found=1
if [ -z "$context" ]; then
_log "nft: global source exclusion for $name ($id)" "debug"
nft add rule ip "$TABLE" prerouting_exc ip saddr @"$id" meta mark set $EXCLUDE_MARK accept
else
_log "nft: source exclusion for $name ($id) inside '$context'" "debug"
nft add rule ip "$TABLE" "$context" ip saddr @"$id" meta mark set $EXCLUDE_MARK return
fi
done
[ "$found" = "0" ] && _log "nft: excluded source '$name' has no ip/url_ip data, exclusion has no effect" "warn"
else
nft add chain ip "$TABLE" "$name" 2>/dev/null
local chain
[ -z "$context" ] && chain="prerouting_proxy" || chain="$context"
for id in $(_existing_ids "$name" "src" $SRC_LIST_SUFFIXES); do
found=1
_log "nft: dispatching $name ($id) into its own chain" "debug"
nft add rule ip "$TABLE" "$chain" ip saddr @"$id" jump "$name"
done
[ "$found" = "0" ] && _log "nft: source '$name' has no ip/url_ip data, its chain will never be entered" "warn"
fi
}
_nft_scope_dst() {
local section="$1" context="$2" want_exclude="$3"
local name exclude_flag parent_list
config_get name "$section" name
_sanitize_name name "$name"
[ -z "$name" ] && return
config_get exclude_flag "$section" exclude "0"
[ "$exclude_flag" != "1" ] && exclude_flag="0"
[ "$exclude_flag" = "$want_exclude" ] || return
_collect_list parent_list "$section" parent
_in_scope "$context" "$parent_list" || return
local id
for id in $(_existing_ids "$name" "dst" $ALL_LIST_SUFFIXES); do
if [ "$exclude_flag" = "1" ]; then
if [ -z "$context" ]; then
_log "nft: global destination exclusion for $name ($id)" "debug"
nft add rule ip "$TABLE" prerouting_exc ip daddr @"$id" meta mark set $EXCLUDE_MARK accept
else
_log "nft: destination exclusion for $name ($id) inside '$context'" "debug"
nft add rule ip "$TABLE" "$context" ip daddr @"$id" meta mark set $EXCLUDE_MARK return
fi
else
local chain
[ -z "$context" ] && chain="prerouting_proxy" || chain="$context"
_log "nft: proxy rule for $name ($id) inside '${context:-<global>}'" "debug"
nft add rule ip "$TABLE" "$chain" ip daddr @"$id" meta l4proto { tcp, udp } \
tproxy to 127.0.0.1:"$TPROXY_PORT" ct mark set "$CLIENT_MARK" \
meta mark set "$TPROXY_MARK" accept
fi
done
}
_ctx_sig_line_src() {
local section="$1" context="$2" name exclude_flag parent_list
config_get name "$section" name; _sanitize_name name "$name"; [ -z "$name" ] && return
config_get exclude_flag "$section" exclude "0"; [ "$exclude_flag" != "1" ] && exclude_flag="0"
_collect_list parent_list "$section" parent
_in_scope "$context" "$parent_list" || return
local suffix bits=""
for suffix in $SRC_LIST_SUFFIXES; do
_item_file_exists "${name}_${suffix}_src" && bits="${bits}1" || bits="${bits}0"
done
echo "src:$name:$exclude_flag:$bits"
}
_ctx_sig_line_dst() {
local section="$1" context="$2" name exclude_flag parent_list
config_get name "$section" name; _sanitize_name name "$name"; [ -z "$name" ] && return
config_get exclude_flag "$section" exclude "0"; [ "$exclude_flag" != "1" ] && exclude_flag="0"
_collect_list parent_list "$section" parent
_in_scope "$context" "$parent_list" || return
local suffix bits=""
for suffix in $ALL_LIST_SUFFIXES; do
_item_file_exists "${name}_${suffix}_dst" && bits="${bits}1" || bits="${bits}0"
done
echo "dst:$name:$exclude_flag:$bits"
}
_ctx_signature() {
local context="$1"
{ config_foreach _ctx_sig_line_src "xray-src" "$context"
config_foreach _ctx_sig_line_dst "xray-dst" "$context"; } | sort | md5sum | cut -d' ' -f1
}
_echo_child_src() {
local section="$1" context="$2" name exclude_flag parent_list
config_get name "$section" name; _sanitize_name name "$name"; [ -z "$name" ] && return
config_get exclude_flag "$section" exclude "0"; [ "$exclude_flag" = "1" ] && return
_collect_list parent_list "$section" parent
_in_scope "$context" "$parent_list" || return
echo "$name"
}
_child_srcs() { config_foreach _echo_child_src "xray-src" "$1"; }
_nft_build_scope() {
local context="$1"
local context_key="${context:-$GLOBAL_CTXKEY}"
case " $VISITED_CTX " in
*" $context_key "*)
_log "nft: cyclic parent reference at '$context_key', skipping to avoid infinite recursion" "crit"
return
;;
esac
VISITED_CTX="${VISITED_CTX}${VISITED_CTX:+ }$context_key"
echo "$context_key" >> "$CTX_ACTIVE_LIST"
local sigfile="$CTX_DIR/${context_key}.sig"
local newsig=$(_ctx_signature "$context")
local oldsig=$(cat "$sigfile" 2>/dev/null)
local chain_exists=1
[ -z "$context" ] || { nft list chain ip "$TABLE" "$context" >/dev/null 2>&1 || chain_exists=0; }
if [ "$FULL_REBUILD" = "1" ] || [ "$newsig" != "$oldsig" ] || [ "$chain_exists" = "0" ]; then
_log "nft: (re)building scope '${context:-<global>}' - structure changed" "info"
if [ -z "$context" ]; then
nft flush chain ip "$TABLE" prerouting_exc
nft flush chain ip "$TABLE" prerouting_proxy
nft add rule ip "$TABLE" prerouting_exc fib daddr type local accept
nft add rule ip "$TABLE" prerouting_proxy fib daddr type local accept
nft add rule ip "$TABLE" prerouting_proxy meta mark $EXCLUDE_MARK return
nft add rule ip "$TABLE" prerouting_proxy meta mark $TPROXY_MARK meta l4proto { tcp, udp } tproxy to 127.0.0.1:"$TPROXY_PORT" accept
else
nft add chain ip "$TABLE" "$context" 2>/dev/null
nft flush chain ip "$TABLE" "$context"
fi
config_foreach _nft_scope_src "xray-src" "$context" "1"
config_foreach _nft_scope_src "xray-src" "$context" "0"
config_foreach _nft_scope_dst "xray-dst" "$context" "1"
config_foreach _nft_scope_dst "xray-dst" "$context" "0"
echo "$newsig" > "$sigfile"
else
_log "nft: scope '${context:-<global>}' unchanged, skipping rebuild" "debug"
fi
local child
for child in $(_child_srcs "$context"); do
_nft_build_scope "$child"
done
}
_cleanup_orphan_ctx() {
local prev="$CTX_DIR/active.prev"
[ -f "$prev" ] || { cp "$CTX_ACTIVE_LIST" "$prev"; return; }
local old_context
while read -r old_context; do
[ -z "$old_context" ] && continue
if ! grep -Fxq "$old_context" "$CTX_ACTIVE_LIST"; then
[ "$old_context" = "$GLOBAL_CTXKEY" ] && continue
_log "nft: removing orphaned source chain '$old_context'" "info"
nft delete chain ip "$TABLE" "$old_context" 2>/dev/null
rm -f "$CTX_DIR/${old_context}.sig"
fi
done < "$prev"
cp "$CTX_ACTIVE_LIST" "$prev"
}
_merge_dnsmasq_sets() {
local out="$DNSMASQ_DIR/nftsets.lst" tmp="$DNSMASQ_DIR/.nftsets.lst.tmp"
: > "$tmp"
local file id
for file in "$DOMSTAGE_DIR"/*.lst; do
[ -e "$file" ] || continue
id="${file##*/}"; id="${id%.lst}"
awk -v id="$id" '{ print $0"\t"id }' "$file"
done | sort -t "$(printf '\t')" -k1,1 | awk -F'\t' -v t="$TABLE" '
function flush_domain() {
if (dom != "") printf "nftset=/%s/%s\n", dom, targets
}
{
if ($1 != dom) { flush_domain(); dom = $1; targets = "" }
targets = targets (targets != "" ? "," : "") "4#ip#" t "#" $2
}
END { flush_domain() }
' > "$tmp"
if ! cmp -s "$tmp" "$out" 2>/dev/null; then
mv "$tmp" "$out"
DNS_CHANGES=1
_log "dnsmasq: merged nftset directives changed ($out)" "info"
else
rm -f "$tmp"
_log "dnsmasq: merged nftset directives unchanged" "debug"
fi
}
_cleanup_orphan_files() {
local removed_ids_file="$1" file filename id
for file in "$IPNET_DIR"/* "$DOMSTAGE_DIR"/*; do
[ -e "$file" ] || continue
if ! grep -Fxq "$file" "$ACTIVE_LIST"; then
filename="${file##*/}"
id="${filename%.*}"
_log "orphan: removing $filename" "info"
case "$file" in "$DOMSTAGE_DIR"/*) DNS_CHANGES=1 ;; esac
echo "$id" >> "$removed_ids_file"
rm -f "$file" "$CACHE_DIR/${id}.nftok" "$CACHE_DIR/${id}.hash"
fi
done
}
_render_out_rules() {
local section="$1" want_file="$2" name direction type exclude_flag
config_get name "$section" name; _sanitize_name name "$name"; [ -z "$name" ] && return
config_get type "$section" TYPE; [ "$type" = "xray-out" ] || return
config_get exclude_flag "$section" exclude "0"
local suffix item
for suffix in $ALL_LIST_SUFFIXES; do
item="${name}_${suffix}_out"
_item_file_exists "$item" || continue
if [ "$exclude_flag" = "1" ]; then
echo "exc $item" >> "$want_file"
else
echo "acc $item" >> "$want_file"
fi
done
}
_run() {
_init_vars || return 1
_validate_config
DNS_CHANGES=0; NEED_UPDATE=0; VISITED_CTX=""
: > "$ACTIVE_LIST"
CTX_ACTIVE_LIST="$CACHE_DIR/ctx_active.$$"
: > "$CTX_ACTIVE_LIST"
local requested_mode="$1"
local actual_mode=$(_nft_init "$requested_mode")
[ "$actual_mode" = "full" ] && FULL_REBUILD=1 || FULL_REBUILD=0
local mark_hex=$(printf '0x%x' "$TPROXY_MARK")
ip route show table $RT_TABLE 2>/dev/null | grep -q "local default" || ip route add local default dev lo table $RT_TABLE
ip rule show | grep -q "fwmark $mark_hex lookup $RT_TABLE" || ip rule add fwmark "$TPROXY_MARK" table $RT_TABLE
config_foreach _process_section "xray-src" "prepare"
config_foreach _process_section "xray-dst" "prepare"
config_foreach _process_section "xray-out" "prepare"
local removed_ids_file="$CACHE_DIR/removed_ids.$$"
: > "$removed_ids_file"
_cleanup_orphan_files "$removed_ids_file"
_merge_dnsmasq_sets
_nft_build_scope ""
_cleanup_orphan_ctx
rm -f "$CTX_ACTIVE_LIST"
if [ -s "$removed_ids_file" ]; then
while read -r removed_id; do
[ -z "$removed_id" ] && continue
nft delete set ip "$TABLE" "$removed_id" 2>/dev/null
done < "$removed_ids_file"
fi
rm -f "$removed_ids_file"
if [ "$FULL_REBUILD" = "1" ]; then
config_foreach _process_section "xray-out" "apply_out_exc"
config_foreach _process_section "xray-out" "apply_out"
else
local want_file="$CACHE_DIR/out_rules.want" have_file="$CACHE_DIR/out_rules.have"
: > "$want_file"
config_foreach _render_out_rules "xray-out" "$want_file"
sort -o "$want_file" "$want_file"
if ! cmp -s "$want_file" "$have_file" 2>/dev/null; then
_log "nft: xray-out rules changed, rebuilding output chain" "info"
nft flush chain ip "$TABLE" output
config_foreach _process_section "xray-out" "apply_out_exc"
config_foreach _process_section "xray-out" "apply_out"
cp "$want_file" "$have_file"
else
_log "nft: xray-out rules unchanged" "debug"
fi
fi
[ "$DNS_CHANGES" = "1" ] && { _log "dnsmasq: restarting service" "info"; /etc/init.d/dnsmasq restart; }
if [ "$SKIP_URL" = "1" ] && [ "$NEED_UPDATE" = "1" ]; then
_log "main: outdated lists detected, run 'update' to refresh" "warn"
fi
_log "main: execution finished (mode=$actual_mode)" "info"
}
_stop() {
_log "main: stopping service" "info"
_init_vars
nft delete table ip "$TABLE" 2>/dev/null
ip rule del fwmark "$TPROXY_MARK" table $RT_TABLE 2>/dev/null
ip route del local default dev lo table $RT_TABLE 2>/dev/null
rm -f "$CTX_DIR"/*.sig "$CTX_DIR/active.prev" 2>/dev/null
/etc/init.d/dnsmasq restart
_log "main: service stopped" "info"
}
case "$1" in
start) export SKIP_URL=0; _run "full" ;;
stop) _stop ;;
restart) _stop; export SKIP_URL=0; _run "full" ;;
reload) export SKIP_URL=1; _run "incr" ;;
update) export SKIP_URL=0; _run "incr" ;;
*) echo "Usage: $0 {start|stop|restart|reload|update}"; exit 1 ;;
esac