From 64fe40fcd994a2bd62d61d1afe189ec424423a83 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 13 Jul 2026 21:27:12 +0400 Subject: [PATCH] feat v0.1.26: refactor script --- xray-manager.sh | 933 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 650 insertions(+), 283 deletions(-) diff --git a/xray-manager.sh b/xray-manager.sh index 06ea5e3..a1befc8 100755 --- a/xray-manager.sh +++ b/xray-manager.sh @@ -2,64 +2,123 @@ . /lib/functions.sh -readonly LNET_RESERVED="127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10 224.0.0.0/4 255.255.255.255/32 0.0.0.0/8" -readonly LNET_NAME="system_lnet" -readonly TEST_DOMAIN="github.com" -readonly PING_ADDR=1.1.1.1 -readonly RT_TABLE=110 +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 msg="$1" + 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') $msg" >> "$DEBUG_LOG" + echo "$(date '+%Y-%m-%d %H:%M:%S') $message" >> "$DEBUG_LOG" fi - local sys_level="info" - case "$level" in - info) sys_level="info" ;; - crit) sys_level="crit" ;; - err) sys_level="err" ;; - warn) sys_level="warning" ;; - debug) sys_level="debug" ;; - esac + local syslog_level="$level" + [ "$level" = "warn" ] && syslog_level="warning" - [ "$level" != "debug" ] && logger -t xray-manager -p "daemon.$sys_level" "$msg" + [ "$level" != "debug" ] && logger -t xray-manager -p "daemon.$syslog_level" "$message" if [ "$to_console" = "1" ]; then - printf "%s\n" "$msg" + printf "%s\n" "$message" 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\"" +} + +_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 -sSfL --connect-timeout 15 --retry 3 --retry-connrefused --retry-delay 5 ${etag:+-H "If-None-Match: \"$etag\""} -D "$header_tmp" "$url" > "$body_tmp" + local curl_status=$? + + if grep -q "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 - local s="main" - TABLE=$(uci -q get xray-manager.$s.table_name) - TPROXY_PORT=$(uci -q get xray-manager.$s.tproxy_port) - TPROXY_MARK=$(uci -q get xray-manager.$s.tproxy_mark) - CLIENT_MARK=$(uci -q get xray-manager.$s.client_mark) - TIMEOUT=$(uci -q get xray-manager.$s.timeout) - IPNET_DIR=$(uci -q get xray-manager.$s.ipnet_dir) - DNSMASQ_DIR=$(uci -q get xray-manager.$s.dnsmasq_dir) - CACHE_DIR=$(uci -q get xray-manager.$s.cache_dir) - ACTIVE_LIST=$(uci -q get xray-manager.$s.active_list) - - _get_proxy_list() { PROXY_SERVERS="${PROXY_SERVERS}${PROXY_SERVERS:+ }$1"; } - PROXY_SERVERS="" - config_list_foreach "$s" proxy_ip _get_proxy_list + MAIN_SECTION="main" - local required_vars="TABLE TPROXY_PORT TPROXY_MARK CLIENT_MARK TIMEOUT IPNET_DIR DNSMASQ_DIR CACHE_DIR ACTIVE_LIST" + _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" + + local required_vars="TABLE TPROXY_PORT TPROXY_MARK CLIENT_MARK RT_TABLE TIMEOUT" for var in $required_vars; do - eval val=\$$var + eval "val=\$$var" if [ -z "$val" ]; then _log "config: variable '$var' is missing" "crit" return 1 @@ -67,7 +126,7 @@ _init_vars() { done _log "config: loaded table=$TABLE, port=$TPROXY_PORT, mark=$TPROXY_MARK" "debug" - mkdir -p "$IPNET_DIR" "$DNSMASQ_DIR" "$CACHE_DIR" "${ACTIVE_LIST%/*}" + 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 @@ -76,11 +135,66 @@ _init_vars() { 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 +} + +_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" "debug" - + _log "config: validating uci syntax and parents" "debug" + if [ -f "/etc/config/xray-manager" ]; then - sed -i 's/^ \+/ /g' /etc/config/xray-manager + sed -i 's/^ \+/ /g' /etc/config/xray-manager fi local dns_confdir @@ -89,343 +203,596 @@ _validate_config() { _log "config: $DNSMASQ_DIR is not set in dnsmasq" "warn" fi - local allowed_opts="name direction exclude parent ip domain url_ip url_domain" + local allowed_opts="name exclude parent ip domain url_ip url_domain" + local has_errors=0 + local existing_src_names="" + config_foreach _get_src_names "xray-src" - validate_list_section() { - local s="$1" name direction - config_get name "$s" name - config_get direction "$s" direction "dst" + config_foreach _validate_list_section "xray-src" + config_foreach _validate_list_section "xray-dst" + config_foreach _validate_list_section "xray-out" - if [ -z "$name" ]; then - _log "config: section $s is missing name" "crit" - exit 1 - fi + if [ "$has_errors" = "1" ]; then + _log "config: validation failed! core configuration errors detected. aborting startup." "crit" + exit 1 + fi - local current_opt - for current_opt in $(uci show xray-manager."$s" | cut -d'.' -f3 | cut -d'=' -f1 | sort -u); do - case "$current_opt" in - .*) continue ;; - esac - - if ! echo "$allowed_opts" | grep -qw "$current_opt"; then - _log "$name: unknown option: '$current_opt'" "err" - exit 1 - fi - done - - case "$direction" in - src|dst|out|force_src) ;; - *) _log "$name: invalid direction: $direction" "err"; exit 1 ;; - esac - } - config_foreach validate_list_section "xray-list" _log "config: validation complete" "info" } _nft_init() { - _log "$TABLE: initializing nft table" "info" - nft add table ip "$TABLE" 2>/dev/null - - _log "$TABLE: setting up base chains" "debug" - nft add chain ip "$TABLE" prerouting { type filter hook prerouting priority mangle \; policy accept \; } 2>/dev/null - nft flush chain ip "$TABLE" prerouting - nft add rule ip "$TABLE" prerouting fib daddr type local accept - - nft add chain ip "$TABLE" output { type route hook output priority -150 \; policy accept \; } 2>/dev/null - nft flush chain ip "$TABLE" output - nft add rule ip "$TABLE" output fib daddr type local accept + local mode="$1" - local mark_hex=$(printf '0x%x' "$TPROXY_MARK") - _log "route: configuring table $RT_TABLE, mark $mark_hex" "debug" - 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 + 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" changed="$4" dir="$5" - local sname="s_$id" + 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;" - - [[ "$type" == *dom ]] && set_params="type ipv4_addr; flags interval,timeout; timeout $TIMEOUT; auto-merge;" - - _log "$sname: creating nft set" "debug" - nft add set ip "$TABLE" "$sname" { $set_params } 2>/dev/null + case "$type" in + *dom) set_params="type ipv4_addr; flags interval,timeout; timeout $TIMEOUT; auto-merge;" ;; + esac - if [ -s "$file" ] && [[ "$type" == *"ip"* ]]; then - if [ "$changed" = "1" ] || [ "$FULL_LOAD" = "1" ]; then - _log "$sname: flushing and loading elements" "debug" - nft flush set ip "$TABLE" "$sname" - { - printf "add element ip %s %s { " "$TABLE" "$sname" - awk '/^[[:space:]]*#/ || /^[[:space:]]*$/ { next } { addr = ($1 ~ /\// ? $1 : $1"/32"); printf "%s%s", (count++ ? ", " : ""), addr }' "$file" - printf " }\n" - } | nft -f - - fi - fi + nft add set ip "$TABLE" "$id" { $set_params } 2>/dev/null + nft flush set ip "$TABLE" "$id" - if [ "$dir" = "force_src" ]; then - local cn="f_$id" - _log "$cn: creating force-chain" "debug" - nft add chain ip "$TABLE" "$cn" 2>/dev/null - nft flush chain ip "$TABLE" "$cn" - nft add rule ip "$TABLE" "$cn" meta l4proto "{ tcp, udp }" tproxy to 127.0.0.1:"$TPROXY_PORT" ct mark set "$CLIENT_MARK" meta mark set "$TPROXY_MARK" accept 2>/dev/null - fi -} - -_nft_apply_rule() { - local sname="$1" dir="$2" exc="$3" parent="$4" mode="$5" - local nft_sname="s_$sname" - local sel="ip daddr" - [ "$dir" = "src" ] && sel="ip saddr" - - case "$mode" in - jumps) - [ "$dir" = "force_src" ] && { - _log "$nft_sname: applying jump to f_$sname" "debug" - nft add rule ip "$TABLE" prerouting ip saddr "@$nft_sname" jump "f_${sname}" 2>/dev/null - } - ;; - exclude) - [ "$exc" != "1" ] && [ "$sname" != "$LNET_NAME" ] && return - - local target_info="" - [ -n "$parent" ] && target_info=" target=$parent" - _log "$nft_sname: applying exclude rule$target_info" "debug" - - if [ -n "$parent" ]; then - nft insert rule ip "$TABLE" "$parent" ip daddr "@$nft_sname" accept 2>/dev/null - else - [ "$dir" != "out" ] && nft add rule ip "$TABLE" prerouting "$sel" "@$nft_sname" accept 2>/dev/null - [ "$dir" = "out" ] && nft add rule ip "$TABLE" output "$sel" "@$nft_sname" accept 2>/dev/null - fi - ;; - main_rules) - [ "$exc" = "1" ] || [ "$dir" = "force_src" ] && return - - _log "$nft_sname: applying tproxy rules" "debug" - - if [ "$mode_chain" = "prerouting" ] && [ "$dir" != "out" ]; then - if [ "$dir" = "src" ]; then - nft add rule ip "$TABLE" prerouting ip saddr "@$nft_sname" ct mark set "$CLIENT_MARK" 2>/dev/null - else - nft add rule ip "$TABLE" prerouting ct mark "$CLIENT_MARK" ip daddr "@$nft_sname" meta l4proto "{ tcp, udp }" tproxy to 127.0.0.1:"$TPROXY_PORT" meta mark set "$TPROXY_MARK" accept 2>/dev/null - fi - elif [ "$mode_chain" = "output" ] && [ "$dir" = "out" ]; then - nft add rule ip "$TABLE" output ip daddr "@$nft_sname" meta l4proto "{ tcp, udp }" counter ct mark set "$CLIENT_MARK" meta mark set "$TPROXY_MARK" accept 2>/dev/null + 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" val="$2" type="$3" dir="$4" exc="$5" parent="$6" - local fullpath hfile changed=0 - - [[ "$type" == *dom ]] && fullpath="$DNSMASQ_DIR/$id.lst" || fullpath="$IPNET_DIR/$id.lst" - hfile="$CACHE_DIR/$id.hash" - + 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 "$hfile" >> "$ACTIVE_LIST" - - local new_hash=$(_get_md5 "$val") - local old_hash=$(cat "$hfile" 2>/dev/null) + 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" - [ "$SKIP_URL" = "1" ] && [[ "$type" == u* ]] && NEED_UPDATE=1 + if [ "$SKIP_URL" = "1" ] && _is_url_type "$type"; then + NEED_UPDATE=1 + fi fi - + _log "$id status: $status" "debug" - if [ "$SKIP_URL" = "1" ] && [[ "$type" == u* ]]; then - _log "$id: skipping download (reload mode)" "debug" - [ ! -f "$fullpath" ] && { _log "$id: data file missing" "warn"; NEED_UPDATE=1; } - [ -f "$fullpath" ] && _nft_prepare_data "$id" "$fullpath" "$type" 0 "$dir" - return + 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 "$val" | tr ' ' '\n' | sed -e 's/\r//g' -e '/^#/d' -e '/^[[:space:]]*$/d' -e '/\//! s|$|/32|' | sort -u > "$fullpath.tmp" + echo "$value" | tr ' ' '\n' | sed -e 's/\r//g' -e '/^#/d' -e '/^[[:space:]]*$/d' -e '/\//! s|$|/32|' | sort -u > "$fullpath.tmp" ;; dom) - echo "$val" | tr ' ' '\n' | awk -v t="$TABLE" -v i="s_$id" '/^[[:space:]]*#/ || /^[[:space:]]*$/ { next } { gsub(/\r/, ""); printf "nftset=/%s/4#ip#%s#%s\n", tolower($1), t, i }' | sort -u > "$fullpath.tmp" + 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 dl_ok=0 + local download_ok=0 - for url in $val; do - local uhash=$(echo "$url" | md5sum | cut -c1-8) + for url in $value; do _log "$id: downloading $url" "debug" - local cache_raw="$CACHE_DIR/${id}_$uhash.raw" - local hfile_tmp="$CACHE_DIR/h_$id" - local etag_f="$CACHE_DIR/${id}_$uhash.etag" - local etag=$(cat "$etag_f" 2>/dev/null) - - curl -sSfL --connect-timeout 15 ${etag:+-H "If-None-Match: \"$etag\""} -D "$hfile_tmp" "$url" > "$tmp_all.raw" - local res=$? - - if grep -q "304" "$hfile_tmp" 2>/dev/null; then - _log "$id: 304 not modified" "info" - [ -f "$cache_raw" ] && cat "$cache_raw" >> "$tmp_all" && dl_ok=1 - elif [ $res -eq 0 ] && [ -s "$tmp_all.raw" ]; then - _log "$id: 200 ok" "info" - grep -i "^etag:" "$hfile_tmp" | awk -F': ' '{print $2}' | sed 's/W\///; s/["\r\n ]//g' > "$etag_f" - cat "$tmp_all.raw" > "$cache_raw" - cat "$tmp_all.raw" >> "$tmp_all" - dl_ok=1 - else - _log "$id: download failed, using local cache" "warn" - [ -f "$cache_raw" ] && cat "$cache_raw" >> "$tmp_all" && dl_ok=1 + if _fetch_url "$id" "$url" >> "$tmp_all"; then + download_ok=1 fi done - if [ "$dl_ok" = "1" ]; then + 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 | awk -v t="$TABLE" -v i="s_$id" '{ printf "nftset=/%s/4#ip#%s#%s\n", $1, t, i }' > "$fullpath.tmp" + tr 'A-Z' 'a-z' < "$tmp_all" | grep -oE '([a-z0-9-]+\.)+[a-z]{2,}' | sort -u > "$fullpath.tmp" fi - fi + fi ;; esac if [ -s "$fullpath.tmp" ] && ! cmp -s "$fullpath.tmp" "$fullpath"; then mv "$fullpath.tmp" "$fullpath" changed=1 - [[ "$type" == *dom ]] && DNS_CHANGES=1 _log "$id: list updated" "info" - else + else rm -f "$fullpath.tmp" fi - echo "$new_hash" > "$hfile" + echo "$new_hash" > "$hash_file" fi - _nft_prepare_data "$id" "$fullpath" "$type" "$changed" "$dir" + + _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 s="$1" mode="$2" name dir exc parent - config_get name "$s" name - config_get dir "$s" direction "dst" - config_get exc "$s" exclude "0" - config_get parent "$s" parent - + local section="$1" mode="$2" name direction type + + config_get name "$section" name + _sanitize_name name "$name" [ -z "$name" ] && return - name=${name//[^a-zA-Z0-9_]/} - [ "$exc" = "1" ] && [ -n "$parent" ] && parent="f_${parent}_ip" + config_get type "$section" TYPE + case "$type" in + xray-src) direction="src" ;; + xray-out) direction="out" ;; + *) direction="dst" ;; + esac - if [ -z "$mode" ]; then - _log "$name: configuring section" "debug" - _get_v() { V="${V}${V:+ }$1"; } - V=""; config_list_foreach "$s" ip _get_v; [ -n "$V" ] && _process_item "${name}_ip" "$V" "ip" "$dir" "$exc" "$parent" - V=""; config_list_foreach "$s" domain _get_v; [ -n "$V" ] && _process_item "${name}_dom" "$V" "dom" "$dir" "$exc" "$parent" - V=""; config_list_foreach "$s" url_ip _get_v; [ -n "$V" ] && _process_item "${name}_uip" "$V" "uip" "$dir" "$exc" "$parent" - V=""; config_list_foreach "$s" url_domain _get_v; [ -n "$V" ] && _process_item "${name}_udom" "$V" "udom" "$dir" "$exc" "$parent" - else - [ -f "$IPNET_DIR/${name}_ip.lst" ] && _nft_apply_rule "${name}_ip" "$dir" "$exc" "$parent" "$mode" - [ -f "$DNSMASQ_DIR/${name}_dom.lst" ] && _nft_apply_rule "${name}_dom" "$dir" "$exc" "$parent" "$mode" - [ -f "$IPNET_DIR/${name}_uip.lst" ] && _nft_apply_rule "${name}_uip" "$dir" "$exc" "$parent" "$mode" - [ -f "$DNSMASQ_DIR/${name}_udom.lst" ] && _nft_apply_rule "${name}_udom" "$dir" "$exc" "$parent" "$mode" + 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 +} - if [ "$mode" = "exclude" ] && [ "$dir" = "force_src" ]; then - _log "$name: force-src: injecting lnet exclusion" "debug" - _nft_apply_rule "$LNET_NAME" "dst" "1" "f_${name}_ip" "exclude" - fi +_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:-}'" "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:-}' - 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:-}' 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() { - _log "main: starting execution ($1)" "info" _init_vars || return 1 _validate_config - DNS_CHANGES=0; NEED_UPDATE=0; : > "$ACTIVE_LIST" - - [ "$FULL_LOAD" = "1" ] && _wait_for_net - _nft_init + DNS_CHANGES=0; NEED_UPDATE=0; VISITED_CTX="" + : > "$ACTIVE_LIST" + CTX_ACTIVE_LIST="$CACHE_DIR/ctx_active.$$" + : > "$CTX_ACTIVE_LIST" - _log "main: processing data sections" "debug" - config_foreach _process_section "xray-list" - _process_item "$LNET_NAME" "$LNET_RESERVED" "ip" "dst" "1" + local requested_mode="$1" + local actual_mode=$(_nft_init "$requested_mode") + [ "$actual_mode" = "full" ] && FULL_REBUILD=1 || FULL_REBUILD=0 - _log "main: applying nft rules" "info" - config_foreach _process_section "xray-list" "jumps" - _nft_apply_rule "$LNET_NAME" "dst" "1" "" "exclude" - config_foreach _process_section "xray-list" "exclude" - - _log "main: applying proxy bypass" "debug" - if [ -n "$PROXY_SERVERS" ]; then - _process_item "proxy_servers" "$PROXY_SERVERS" "ip" "out" "0" - nft add rule ip "$TABLE" output ip daddr "@s_proxy_servers" accept 2>/dev/null + 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 - - _log "main: applying tproxy rules" "debug" - mode_chain="prerouting"; config_foreach _process_section "xray-list" "main_rules" - mode_chain="output"; config_foreach _process_section "xray-list" "main_rules" + rm -f "$removed_ids_file" - _log "main: cleaning up orphans" "info" - if [ -n "$IPNET_DIR" ] && [ -n "$DNSMASQ_DIR" ]; then - for f in "$IPNET_DIR"/* "$DNSMASQ_DIR"/*; do - [ -e "$f" ] || continue - if ! grep -Fxq "$f" "$ACTIVE_LIST"; then - local fname="${f##*/}" - _log "orphan: removing $fname" "info" - case "$f" in "$DNSMASQ_DIR"/*) DNS_CHANGES=1 ;; esac - nft delete set ip "$TABLE" "s_${fname%.*}" 2>/dev/null - rm -f "$f" - fi - done + 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; } - [ "$SKIP_URL" = "1" ] && [ "$NEED_UPDATE" = "1" ] && { - _log "main: outdated lists detected" "warn" - _log "main: run 'update' to refresh data" "warn" - } - - _log "main: execution finished" "info" -} -_wait_for_net() { - _log "network: checking connectivity" "info" - local t=1 - while [ "$t" -le 20 ]; do - if ping -q -c 1 -W 2 "$PING_ADDR" >/dev/null 2>&1; then - if nslookup "$TEST_DOMAIN" >/dev/null 2>&1; then - _log "network: access confirmed" "info" - return 0 - else - _log "network: dns resolution failed" "warn" - fi - else - _log "network: unreachable" "warn" - fi - sleep 2; t=$((t + 2)) - done - _log "network: timeout: switching to offline mode" "warn" - export SKIP_URL=1 + 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 + _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 FULL_LOAD=1; _run "start" ;; + start) export SKIP_URL=0; _run "full" ;; stop) _stop ;; - restart) _stop; export SKIP_URL=0 FULL_LOAD=1; _run "restart" ;; - reload) export SKIP_URL=1 FULL_LOAD=0; _run "reload" ;; - update) export SKIP_URL=0 FULL_LOAD=0; _run "update" ;; + 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 +esac \ No newline at end of file