#!/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:-}'" "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() { _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