Initial release v0.1.0
This commit is contained in:
commit
b997bd6078
62 changed files with 56224 additions and 0 deletions
269
refers/mh
Executable file
269
refers/mh
Executable file
|
|
@ -0,0 +1,269 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
||||
MH_ROOT="${MIHOMO_ROOT:-$SCRIPT_DIR}"
|
||||
MH_BIN="${MIHOMO_BIN:-${MH_ROOT}/mihomo}"
|
||||
MH_SYSTEM="${MH_ROOT}/system/config.yaml"
|
||||
MH_BYPASS="${MH_ROOT}/bypass.list"
|
||||
MH_PROFILES="${MH_ROOT}/profiles"
|
||||
MH_CURRENT="${MH_ROOT}/current.profile"
|
||||
MH_SETTINGS="${MH_ROOT}/settings.json"
|
||||
MH_RUNTIME="${MIHOMO_RUN_DIR:-${MH_ROOT}/.runtime}"
|
||||
MH_CONFIG="${MH_RUNTIME}/config.yaml"
|
||||
MH_RULES="${MH_RUNTIME}/bypass-rules.json"
|
||||
MH_PID_FILE="${MH_RUNTIME}/run.pid"
|
||||
MH_LOG="${MH_RUNTIME}/mihomo.log"
|
||||
MH_MIXED_PORT="${MIHOMO_PORT:-7890}"
|
||||
MH_SOCKS_PORT="${MIHOMO_SOCKS_PORT:-7891}"
|
||||
MH_API_PORT="${MIHOMO_API_PORT:-9090}"
|
||||
MH_API="http://127.0.0.1:${MH_API_PORT}"
|
||||
|
||||
die(){ printf 'mh: %s\n' "$*" >&2; exit 1; }
|
||||
info(){ printf 'mh: %s\n' "$*"; }
|
||||
warn(){ printf 'mh: warning: %s\n' "$*" >&2; }
|
||||
need(){ command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; }
|
||||
|
||||
init_layout(){
|
||||
[[ -x "$MH_BIN" ]] || die "mihomo binary not executable: $MH_BIN"
|
||||
[[ -f "$MH_SYSTEM" ]] || die "system config missing: $MH_SYSTEM"
|
||||
[[ -f "$MH_BYPASS" ]] || die "bypass list missing: $MH_BYPASS"
|
||||
need jq; need yq
|
||||
mkdir -p "$MH_PROFILES" "$MH_RUNTIME"
|
||||
[[ -f "$MH_SETTINGS" ]] || printf '%s\n' '{"mode":"rule","tun":false,"cliproxy":false,"sysproxy":false}' >"$MH_SETTINGS"
|
||||
}
|
||||
setting(){ jq -r --arg k "$1" '.[$k]' "$MH_SETTINGS"; }
|
||||
set_setting(){
|
||||
local k="$1" v="$2"
|
||||
if [[ "$v" == true || "$v" == false ]]; then jq --arg k "$k" --argjson v "$v" '.[$k]=$v' "$MH_SETTINGS" >"${MH_SETTINGS}.tmp"
|
||||
else jq --arg k "$k" --arg v "$v" '.[$k]=$v' "$MH_SETTINGS" >"${MH_SETTINGS}.tmp"; fi
|
||||
mv -f "${MH_SETTINGS}.tmp" "$MH_SETTINGS"
|
||||
}
|
||||
profile_dir(){ printf '%s/%s\n' "$MH_PROFILES" "$1"; }
|
||||
provider_file(){ printf '%s/provider.yaml\n' "$(profile_dir "$1")"; }
|
||||
metadata_file(){ printf '%s/source.json\n' "$(profile_dir "$1")"; }
|
||||
validate_name(){ [[ "$1" =~ ^[A-Za-z0-9._-]+$ ]] || die 'profile name contains unsupported characters'; }
|
||||
current_profile(){
|
||||
[[ -f "$MH_CURRENT" ]] || return 1
|
||||
local n="$(tr -d '[:space:]' <"$MH_CURRENT")"
|
||||
[[ -n "$n" && -f "$(provider_file "$n")" ]] || return 1
|
||||
echo "$n"
|
||||
}
|
||||
list_profiles(){ local f; for f in "$MH_PROFILES"/*/provider.yaml; do [[ -f "$f" ]] && basename "$(dirname "$f")"; done | sort; }
|
||||
validate_provider(){ yq -e '.proxies | type == "array" and length > 0' "$1" >/dev/null || die "provider must be Clash YAML with non-empty proxies: $1"; }
|
||||
normalize_provider(){ validate_provider "$1"; yq -y '{proxies:.proxies}' "$1" >"$2"; validate_provider "$2"; }
|
||||
fetch_provider(){
|
||||
local src="$1" dest="$2" raw
|
||||
raw="${dest}.raw"
|
||||
if [[ "$src" =~ ^https?:// ]]; then need curl; curl -fL --connect-timeout 15 --max-time 90 -A 'mihomo/1.19 mh-provider' "$src" -o "$raw"
|
||||
elif [[ -f "$src" ]]; then cp -f -- "$src" "$raw"
|
||||
else die "source is neither HTTP URL nor file: $src"; fi
|
||||
normalize_provider "$raw" "$dest"; rm -f "$raw"
|
||||
}
|
||||
add_cmd(){
|
||||
local name="${1:-}" src="${2:-}" dir tmp kind saved
|
||||
[[ -n "$name" && -n "$src" ]] || die 'usage: mh add <name> <url|file>'
|
||||
validate_name "$name"; dir="$(profile_dir "$name")"; [[ ! -e "$dir" ]] || die "profile exists: $name"
|
||||
mkdir -p "$dir"; tmp="${dir}/provider.yaml.tmp"
|
||||
if [[ "$src" =~ ^https?:// ]]; then kind=http; saved="$src"; else kind=file; saved="$(readlink -f "$src")"; fi
|
||||
if ! fetch_provider "$src" "$tmp"; then rm -f "$tmp" "${tmp}.raw"; rmdir "$dir" 2>/dev/null || true; return 1; fi
|
||||
mv -f "$tmp" "${dir}/provider.yaml"
|
||||
jq -n --arg name "$name" --arg type "$kind" --arg source "$saved" --arg added_at "$(date -Iseconds)" '{name:$name,type:$type,source:$source,added_at:$added_at}' >"${dir}/source.json"
|
||||
echo "$name" >"$MH_CURRENT"; info "added profile=${name} type=${kind}"; info "run: mh start ${name}"
|
||||
}
|
||||
|
||||
compile_bypass(){
|
||||
local line value prefix; : >"${MH_RULES}.lines"
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%%#*}"; line="$(sed -E 's/^[[:space:]]+|[[:space:]]+$//g' <<<"$line")"; [[ -n "$line" ]] || continue
|
||||
if [[ "$line" == */* ]]; then [[ "$line" == *:* ]] && echo "IP-CIDR6,${line},DIRECT,no-resolve" || echo "IP-CIDR,${line},DIRECT,no-resolve"
|
||||
elif [[ "$line" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "IP-CIDR,${line}/32,DIRECT,no-resolve"
|
||||
elif [[ "$line" =~ ^(10|127|172|192)(\..*)?\*$ ]]; then prefix="${BASH_REMATCH[1]}"; case "$prefix" in 10)value=10.0.0.0/8;;127)value=127.0.0.0/8;;172)value=172.16.0.0/12;;192)value=192.168.0.0/16;;esac; echo "IP-CIDR,${value},DIRECT,no-resolve"
|
||||
elif [[ "$line" == \** || "$line" == .* ]]; then value="${line#\*}"; value="${value#.}"; [[ -n "$value" ]] && echo "DOMAIN-SUFFIX,${value},DIRECT"
|
||||
else echo "DOMAIN,${line},DIRECT"; fi
|
||||
done <"$MH_BYPASS" | awk '!seen[$0]++' >"${MH_RULES}.lines"
|
||||
jq -R -s 'split("\n")|map(select(length>0))' "${MH_RULES}.lines" >"$MH_RULES"; rm -f "${MH_RULES}.lines"
|
||||
}
|
||||
|
||||
proxy_bypass_items(){
|
||||
local mode="$1" line value
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%%#*}"; line="$(sed -E 's/^[[:space:]]+|[[:space:]]+$//g' <<<"$line")"; [[ -n "$line" ]] || continue
|
||||
value="$line"
|
||||
if [[ "$mode" == no_proxy && "$value" == \*.* ]]; then value=".${value#*.}"
|
||||
elif [[ "$mode" == gsettings && "$value" == .* ]]; then value="*${value}"; fi
|
||||
printf '%s\n' "$value"
|
||||
done <"$MH_BYPASS" | awk '!seen[$0]++'
|
||||
}
|
||||
no_proxy_value(){ proxy_bypass_items no_proxy | paste -sd, -; }
|
||||
gsettings_bypass_value(){ proxy_bypass_items gsettings | jq -Rsc 'split("\n") | map(select(length > 0))'; }
|
||||
|
||||
build_config(){
|
||||
local profile="$1" provider sys rules mode tun
|
||||
provider="$(provider_file "$profile")"; [[ -f "$provider" ]] || die "profile not found: $profile"; validate_provider "$provider"
|
||||
compile_bypass; sys="$(yq -c . "$MH_SYSTEM")"; rules="$(<"$MH_RULES")"; mode="$(setting mode)"; tun="$(setting tun)"
|
||||
jq -n --argjson s "$sys" --argjson rules "$rules" --arg profile "$profile" --arg provider "$provider" --arg mode "$mode" --argjson tun "$tun" \
|
||||
--argjson mixed "$MH_MIXED_PORT" --argjson socks "$MH_SOCKS_PORT" --arg controller "127.0.0.1:${MH_API_PORT}" '
|
||||
$s
|
||||
| .["mixed-port"]=$mixed | .["socks-port"]=$socks | .["external-controller"]=$controller | .mode=$mode
|
||||
| .["proxy-providers"]={($profile):{type:"file",path:$provider,health_check:{enable:true,url:"https://www.gstatic.com/generate_204",interval:600}}}
|
||||
| .["proxy-groups"]=[{name:"Proxy",type:"select",use:[$profile]},{name:"Final",type:"select",proxies:["Proxy","DIRECT"]}]
|
||||
| .rules=($rules+["MATCH,Final"])
|
||||
| .tun={enable:$tun,stack:"system",device:"mihomo","auto-route":true,"auto-redir":true,"auto-detect-interface":true,"dns-hijack":["any:53"]}
|
||||
| if $tun then .dns={enable:true,listen:"127.0.0.1:1053","enhanced-mode":"redir-host",nameserver:["223.5.5.5","119.29.29.29"],fallback:["1.1.1.1","8.8.8.8"],"fallback-filter":{geoip:false}} else del(.dns) end
|
||||
' | yq -y '.' >"${MH_CONFIG}.tmp"
|
||||
mv -f "${MH_CONFIG}.tmp" "$MH_CONFIG"
|
||||
}
|
||||
|
||||
process_alive(){ local pid="$1" state _; [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null || return 1; read -r _ _ state _ <"/proc/${pid}/stat" 2>/dev/null || return 1; [[ "$state" != Z ]]; }
|
||||
is_running(){ [[ -f "$MH_PID_FILE" ]] && process_alive "$(<"$MH_PID_FILE")"; }
|
||||
port_in_use(){ ss -ltnH 2>/dev/null | awk -v s=":$1" 'substr($4,length($4)-length(s)+1)== s{f=1}END{exit !f}'; }
|
||||
api_curl(){ local secret="$(yq -r '.secret // ""' "$MH_CONFIG" 2>/dev/null || true)"; if [[ -n "$secret" ]]; then curl -fsS --noproxy '*' -H "Authorization: Bearer $secret" "$@"; else curl -fsS --noproxy '*' "$@"; fi; }
|
||||
wait_api(){ local i; for i in {1..50}; do api_curl "$MH_API/proxies" >/dev/null 2>&1 && return; sleep .1; done; return 1; }
|
||||
|
||||
stop_cmd(){
|
||||
if ! is_running; then rm -f "$MH_PID_FILE"; info 'not running'; return; fi
|
||||
local pid="$(<"$MH_PID_FILE")" expected actual comm cmdline i
|
||||
expected="$(readlink -f "$MH_BIN")"; actual="$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)"; comm="$(cat "/proc/$pid/comm" 2>/dev/null || true)"; cmdline="$(tr '\0' ' ' <"/proc/$pid/cmdline" 2>/dev/null || true)"
|
||||
if [[ "$actual" != "$expected" ]]; then [[ "$comm" == "$(basename "$MH_BIN")" && "$cmdline" == "$MH_BIN -d "* && "$cmdline" == *" -f $MH_CONFIG"* ]] || die "refusing to stop pid $pid: identity mismatch"; fi
|
||||
kill "$pid" 2>/dev/null || true; for i in {1..30}; do process_alive "$pid" || break; sleep .1; done; process_alive "$pid" && die "pid $pid did not stop"
|
||||
rm -f "$MH_PID_FILE"; info "stopped pid $pid"
|
||||
}
|
||||
|
||||
choose_profile(){
|
||||
local requested="${1:-}" current pick i; local -a profiles; mapfile -t profiles < <(list_profiles); ((${#profiles[@]})) || die 'no providers; use mh add'
|
||||
current="$(current_profile 2>/dev/null || true)"
|
||||
if [[ -n "$requested" ]]; then [[ -f "$(provider_file "$requested")" ]] || die "profile not found: $requested"; echo "$requested"; return; fi
|
||||
if [[ ! -t 0 ]]; then [[ -n "$current" ]] || die 'non-interactive start needs name'; echo "$current"; return; fi
|
||||
echo 'Available providers:' >&2; for i in "${!profiles[@]}"; do [[ "${profiles[$i]}" == "$current" ]] && printf ' %d) %s (current)\n' "$((i+1))" "${profiles[$i]}" >&2 || printf ' %d) %s\n' "$((i+1))" "${profiles[$i]}" >&2; done
|
||||
while true; do read -r -p "Choose provider (1-${#profiles[@]}, 0 cancel): " pick; [[ "$pick" =~ ^[0-9]+$ ]] || continue; ((pick== 0)) && return 1; ((pick>=1&&pick<=${#profiles[@]})) && { echo "${profiles[$((pick-1))]}"; return; }; done
|
||||
}
|
||||
start_cmd(){
|
||||
local profile pid i; profile="$(choose_profile "${1:-}")" || { info cancelled; return; }; build_config "$profile"
|
||||
"$MH_BIN" -t -d "$MH_ROOT" -f "$MH_CONFIG" >/dev/null; is_running && stop_cmd; need ss
|
||||
for i in "$MH_MIXED_PORT" "$MH_SOCKS_PORT" "$MH_API_PORT"; do port_in_use "$i" && die "port $i is in use"; done
|
||||
: >"$MH_LOG"; nohup "$MH_BIN" -d "$MH_ROOT" -f "$MH_CONFIG" >>"$MH_LOG" 2>&1 & pid=$!; echo "$pid" >"$MH_PID_FILE"; echo "$profile" >"$MH_CURRENT"
|
||||
for i in {1..50}; do ! process_alive "$pid" && { rm -f "$MH_PID_FILE"; tail -n 40 "$MH_LOG" >&2; die 'mihomo exited'; }; port_in_use "$MH_MIXED_PORT" && port_in_use "$MH_API_PORT" && { info "started profile=$profile pid=$pid mode=$(setting mode) tun=$(setting tun)"; wait_api || warn 'API not ready'; return; }; sleep .1; done
|
||||
die 'startup timeout'
|
||||
}
|
||||
restart_if_running(){ local p="$(current_profile 2>/dev/null || true)"; if is_running; then stop_cmd; start_cmd "$p"; fi; }
|
||||
check_cmd(){ local p="${1:-}"; [[ -n "$p" ]] || p="$(current_profile 2>/dev/null || true)"; [[ -n "$p" ]] || die 'no profile'; build_config "$p"; "$MH_BIN" -t -d "$MH_ROOT" -f "$MH_CONFIG"; info "configuration OK: profile=$p"; }
|
||||
require_api(){ is_running || die 'mihomo is not running'; api_curl "$MH_API/proxies" >/dev/null || die "API unavailable: $MH_API"; }
|
||||
urlencode(){ jq -nr --arg v "$1" '$v|@uri'; }
|
||||
api_delay(){ local n="$1" e u; e="$(urlencode "$n")"; u="$(urlencode 'https://www.gstatic.com/generate_204')"; api_curl "$MH_API/proxies/$e/delay?timeout=1800&url=$u" 2>/dev/null | jq -r 'if (.delay|type)=="number" then .delay else 999999 end' 2>/dev/null || echo 999999; }
|
||||
delay_text(){ local d="${1:-}"; [[ "$d" =~ ^[0-9]+$ && "$d" != 999999 ]] && printf '%sms\n' "$d" || printf 'timeout\n'; }
|
||||
main_group(){ jq -r 'if .proxies.Proxy.all then "Proxy" else [.proxies|to_entries[]|select(.value.all!=null)][0].key//empty end' <<<"$1"; }
|
||||
display_grid(){ local -n r=$1; local n=${#r[@]} w c=1 rows i j k; w="$(tput cols 2>/dev/null||echo 120)"; ((n>18&&w>=110))&&c=2; ((n>36&&w>=170))&&c=3; rows=$(((n+c-1)/c)); for((i=0;i<rows;i++));do for((j=0;j<c;j++));do k=$((i+j*rows));((k<n))||continue; if((j+1<c&&k+rows<n));then printf '%-56s' "${r[$k]}";else printf '%s' "${r[$k]}";fi;done;echo;done; }
|
||||
list_nodes_cmd(){
|
||||
local kw="${1:-}" json group now candidates tmp running=0 i d pick enc body; local -a nodes sorted=() delays=() view=()
|
||||
require_api; json="$(api_curl "$MH_API/proxies")"; group="$(main_group "$json")"; [[ -n "$group" ]]||die 'no selectable group'; now="$(jq -r --arg g "$group" '.proxies[$g].now//empty'<<<"$json")"
|
||||
candidates="$(jq -r --arg g "$group" --arg kw "$kw" 'def lc:ascii_downcase;. as $r|.proxies[$g].all[] as $n|($r.proxies[$n].type//"")as$t|select(["Direct","Reject","Selector","URLTest","Fallback","LoadBalance"]|index($t)== null)|select($kw==""or($n|contains($kw))or(($n|lc)|contains($kw|lc)))|$n'<<<"$json")"
|
||||
[[ -n "$candidates" ]]||die "no nodes matched: ${kw:-<all>}"; mapfile -t nodes<<<"$candidates"; tmp="$(mktemp -d "$MH_RUNTIME/delay.XXXXXX")"; info "testing ${#nodes[@]} nodes (1800ms, 8 jobs)..."
|
||||
for i in "${!nodes[@]}";do(api_delay "${nodes[$i]}">"$tmp/$i")& running=$((running+1));if((running>=8));then wait -n 2>/dev/null||wait;running=$((running-1));fi;done;wait;:>"$tmp/index"
|
||||
for i in "${!nodes[@]}";do d="$(<"$tmp/$i")";[[ "$d" =~ ^[0-9]+$ ]]||d=999999;printf '%s\t%s\n' "$d" "$i">>"$tmp/index";done
|
||||
while IFS=$'\t' read -r d i;do delays+=("$d");sorted+=("${nodes[$i]}");done< <(sort -n "$tmp/index");rm -rf -- "$tmp"
|
||||
printf 'Group: %s Current: %s' "$group" "$now";[[ -n "$kw" ]]&&printf ' Filter: %s' "$kw";echo
|
||||
for i in "${!sorted[@]}";do d="$(delay_text "${delays[$i]}")";[[ "${sorted[$i]}" == "$now" ]]&&view+=("[$((i+1))] * ${sorted[$i]} $d")||view+=("[$((i+1))] ${sorted[$i]} $d");done;display_grid view
|
||||
[[ -t 0 ]]||return;while true;do read -r -p "Choose node (1-${#sorted[@]}, 0 cancel): " pick;[[ "$pick" =~ ^[0-9]+$ ]]||continue;((pick== 0))&&return;((pick>=1&&pick<=${#sorted[@]}))&&break;done
|
||||
enc="$(urlencode "$group")";body="$(jq -nc --arg name "${sorted[$((pick-1))]}" '{name:$name}')";api_curl -X PUT -H 'Content-Type: application/json' --data "$body" "$MH_API/proxies/$enc">/dev/null;info "$group -> ${sorted[$((pick-1))]}"
|
||||
}
|
||||
|
||||
show_env(){ local no_proxy; no_proxy="$(no_proxy_value)"; cat <<EOF
|
||||
export http_proxy="http://127.0.0.1:${MH_MIXED_PORT}"
|
||||
export https_proxy="http://127.0.0.1:${MH_MIXED_PORT}"
|
||||
export all_proxy="socks5h://127.0.0.1:${MH_SOCKS_PORT}"
|
||||
export HTTP_PROXY="\$http_proxy"
|
||||
export HTTPS_PROXY="\$https_proxy"
|
||||
export ALL_PROXY="\$all_proxy"
|
||||
EOF
|
||||
printf 'export NO_PROXY=%q\n' "$no_proxy"
|
||||
printf '%s\n' 'export no_proxy="$NO_PROXY"'
|
||||
}
|
||||
show_noenv(){ echo 'unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY no_proxy'; }
|
||||
shell_init_cmd(){ local real="$(printf %q "$SCRIPT_PATH")";cat <<EOF
|
||||
mh() {
|
||||
MH_SHELL_HOOK=1 ${real} "\$@"
|
||||
local rc=\$?
|
||||
if ((rc== 0)) && [[ "\${1:-}" == set ]] && { [[ "\${2:-}" == cliproxy ]] || [[ "\${2:-}" == proxy ]]; }; then
|
||||
if [[ "\${3:-}" == on ]]; then eval "\$(${real} env)"; else eval "\$(${real} noenv)"; fi
|
||||
fi
|
||||
return \$rc
|
||||
}
|
||||
EOF
|
||||
[[ "$(setting cliproxy)" == true ]]&&show_env||show_noenv; }
|
||||
sysproxy_cmd(){ local a="$1" ignore_hosts;need gsettings;case "$a" in on)ignore_hosts="$(gsettings_bypass_value)";gsettings set org.gnome.system.proxy mode manual;for s in http https;do gsettings set org.gnome.system.proxy.$s host 127.0.0.1;gsettings set org.gnome.system.proxy.$s port "$MH_MIXED_PORT";done;gsettings set org.gnome.system.proxy.socks host 127.0.0.1;gsettings set org.gnome.system.proxy.socks port "$MH_SOCKS_PORT";gsettings set org.gnome.system.proxy ignore-hosts "$ignore_hosts";set_setting sysproxy true;info 'system proxy ON';;off)gsettings set org.gnome.system.proxy mode none;set_setting sysproxy false;info 'system proxy OFF';;status)echo "setting=$(setting sysproxy) mode=$(gsettings get org.gnome.system.proxy mode)";;*)die 'usage: mh set sysproxy on|off';;esac; }
|
||||
default_bypass(){ cat <<'EOF'
|
||||
# mh system bypass rules
|
||||
localhost
|
||||
*.local
|
||||
127.0.0.0/8
|
||||
10.0.0.0/8
|
||||
172.16.0.0/12
|
||||
192.168.0.0/16
|
||||
100.64.0.2
|
||||
100.64.0.0/10
|
||||
::1/128
|
||||
fc00::/7
|
||||
fe80::/10
|
||||
EOF
|
||||
}
|
||||
rules_cmd(){ case "${1:-show}" in show)echo "bypass.list: $MH_BYPASS";nl -ba "$MH_BYPASS";;update)compile_bypass;[[ "$(setting sysproxy)" == true ]]&&sysproxy_cmd on;info "compiled $(jq length "$MH_RULES") bypass rules";restart_if_running;;reset)default_bypass>"$MH_BYPASS.tmp";mv -f "$MH_BYPASS.tmp" "$MH_BYPASS";compile_bypass;[[ "$(setting sysproxy)" == true ]]&&sysproxy_cmd on;info 'bypass reset';restart_if_running;;*)die 'usage: mh set rules show|update|reset';;esac;}
|
||||
set_cmd(){ local t="${1:-}" a="${2:-}";[[ -n "$t" ]]|| { jq . "$MH_SETTINGS";sysproxy_cmd status 2>/dev/null||true;return;};case "$t" in cliproxy)[[ "$a" == on||"$a" == off ]]||die 'usage: mh set cliproxy on|off';set_setting cliproxy "$([[ "$a" == on ]]&&echo true||echo false)";info "CLI proxy $a";[[ -n "${MH_SHELL_HOOK:-}" ]]||warn 'run once: eval "$(mh shell-init)"';;sysproxy)sysproxy_cmd "$a";;proxy)[[ "$a" == on||"$a" == off ]]||die 'usage: mh set proxy on|off';set_setting cliproxy "$([[ "$a" == on ]]&&echo true||echo false)";sysproxy_cmd "$a";info "CLI + system proxy $a";;tun)[[ "$a" == on||"$a" == off ]]||die 'usage: mh set tun on|off';set_setting tun "$([[ "$a" == on ]]&&echo true||echo false)";[[ "$a" == on ]]&&! getcap "$MH_BIN" 2>/dev/null|grep -q cap_net_admin&&warn "TUN may need setcap on $MH_BIN";info "TUN $a";restart_if_running;;rules)rules_cmd "$a";;*)die 'usage: mh set cliproxy|sysproxy|proxy|tun|rules';;esac; }
|
||||
update_cmd(){ local p meta type src tmp running=false;p="$(current_profile 2>/dev/null||true)";[[ -n "$p" ]]||die 'no current profile';meta="$(metadata_file "$p")";type="$(jq -r .type "$meta")";src="$(jq -r .source "$meta")";[[ "$type" == http ]]||die 'current provider is local file';is_running&&running=true;tmp="$(provider_file "$p").tmp";fetch_provider "$src" "$tmp";mv -f "$tmp" "$(provider_file "$p")";jq --arg t "$(date -Iseconds)" '.updated_at=$t' "$meta">"$meta.tmp";mv -f "$meta.tmp" "$meta";info "updated HTTP provider: $p";[[ "$running" == true ]]&& { stop_cmd;start_cmd "$p";};}
|
||||
mode_cmd(){ local m="${1:-}";[[ "$m" == rule||"$m" == global||"$m" == direct ]]||die 'usage: mh mode rule|global|direct';set_setting mode "$m";info "mode -> $m";restart_if_running;}
|
||||
|
||||
status_cmd(){
|
||||
local p="$(current_profile 2>/dev/null||true)" now='' delay=''
|
||||
if is_running;then
|
||||
now="$(api_curl "$MH_API/proxies/Proxy" 2>/dev/null|jq -r '.now//empty'||true)"
|
||||
[[ -n "$now" ]]&&delay="$(delay_text "$(api_delay "$now")")"
|
||||
echo "mihomo: RUNNING pid=$(<"$MH_PID_FILE")"
|
||||
else
|
||||
echo 'mihomo: STOPPED'
|
||||
fi
|
||||
printf 'root: %s\nprofile: %s\nmode: %s\ntun: %s\ncliproxy: %s\nsysproxy: %s\nmixed: 127.0.0.1:%s\nsocks: 127.0.0.1:%s\napi: 127.0.0.1:%s\n' "$MH_ROOT" "${p:-<none>}" "$(setting mode)" "$(setting tun)" "$(setting cliproxy)" "$(setting sysproxy)" "$MH_MIXED_PORT" "$MH_SOCKS_PORT" "$MH_API_PORT"
|
||||
[[ -n "$now" ]]&&printf 'node: %s %s\n' "$now" "$delay"
|
||||
}
|
||||
|
||||
db_ports(){ echo '== proxy ports ==';ss -ltnp 2>/dev/null|awk -v a=":$MH_MIXED_PORT" -v b=":$MH_SOCKS_PORT" -v c=":$MH_API_PORT" 'NR== 1||index($4,a)||index($4,b)||index($4,c)';}
|
||||
db_process(){ local p="${1:-}";[[ -n "$p" ]]|| { [[ -f "$MH_PID_FILE" ]]&&p="$(<"$MH_PID_FILE")"||true;};[[ "$p" =~ ^[0-9]+$ ]]||die 'no managed PID';ps -p "$p" -o pid,ppid,user,state,etime,%cpu,%mem,cmd;}
|
||||
db_probe(){ local mode="$1" url="$2";if [[ "$mode" == direct ]];then curl -fsSIL --max-time 10 --noproxy '*' "$url" -o /dev/null;else curl -fsSIL --max-time 10 --proxy "http://127.0.0.1:$MH_MIXED_PORT" "$url" -o /dev/null;fi;}
|
||||
db_net(){ local mode="${1:-proxy}" u x y;local -a urls=('https://www.google.com' 'https://www.cloudflare.com' 'https://www.github.com' 'https://www.youtube.com' 'https://www.baidu.com' 'https://www.bing.com');[[ "$mode" == direct ]]||require_api;for u in "${urls[@]}";do if [[ "$mode" == compare ]];then db_probe direct "$u"&&x=OK||x=FAIL;db_probe proxy "$u"&&y=OK||y=FAIL;printf '%-32s DIRECT:%-4s PROXY:%-4s\n' "$u" "$x" "$y";else db_probe "$mode" "$u"&&x=OK||x=FAIL;printf '%-32s %s:%s\n' "$u" "${mode^^}" "$x";fi;done;}
|
||||
db_deps(){ local x p rc=0;for x in jq yq curl ss sed sort readlink;do p="$(command -v "$x" 2>/dev/null||true)";[[ -n "$p" ]]&&printf 'OK %-10s %s\n' "$x" "$p"|| { echo "MISSING $x";rc=1;};done;[[ -x "$MH_BIN" ]]&&echo "OK mihomo $MH_BIN"||rc=1;return "$rc";}
|
||||
db_interfaces(){ ip -brief link show 2>/dev/null||true;echo "TUN setting=$(setting tun)";}
|
||||
db_dns(){ echo '== generated DNS ==';yq .dns "$MH_CONFIG" 2>/dev/null||echo disabled;echo '== host resolver ==';resolvectl status 2>/dev/null|sed -n '1,140p'||cat /etc/resolv.conf;}
|
||||
db_routes(){ ip route show;echo;ip rule show;}
|
||||
db_api(){ require_api;api_curl "$MH_API/version"|jq .;api_curl "$MH_API/proxies"|jq -r '.proxies|to_entries[]|select(.value.all!=null)|"\(.key) -> \(.value.now//"-") (\(.value.all|length))"';}
|
||||
db_config(){ check_cmd; yq -c '{mode,ports:{mixed:."mixed-port",socks:."socks-port",api:."external-controller"},tun:.tun,providers:(."proxy-providers"|keys),groups:(."proxy-groups"|map(.name)),rule_count:(.rules|length)}' "$MH_CONFIG"|jq .;}
|
||||
db_log(){ local n="${1:-80}";[[ "$n" =~ ^[0-9]+$ ]]||die 'lines must be number';tail -n "$n" "$MH_LOG" 2>/dev/null||echo 'no log';}
|
||||
db_menu(){ local pick i;local -a c=(ports process net compare deps interfaces dns routes api config log);echo '== mh db ==';for i in "${!c[@]}";do printf ' %2d) %s\n' "$((i+1))" "${c[$i]}";done;[[ -t 0 ]]||return;read -r -p 'Choose: ' pick;[[ "$pick" =~ ^[0-9]+$ ]]&&((pick>=1&&pick<=${#c[@]}))&&db_cmd "${c[$((pick-1))]}";}
|
||||
db_cmd(){ local a="${1:-menu}";shift||true;case "$a" in menu)db_menu;;ports)db_ports;;process)db_process "${1:-}";;net)db_net "${1:-proxy}";;compare)db_net compare;;deps)db_deps;;interfaces)db_interfaces;;dns)db_dns;;routes)db_routes;;api)db_api;;config)db_config;;log)db_log "${1:-80}";;*)die 'usage: mh db [ports|process|net|compare|deps|interfaces|dns|routes|api|config|log]';;esac;}
|
||||
doctor_cmd(){ echo '== mh doctor ==';status_cmd;echo;db_deps;echo;db_ports;echo;db_config;echo;db_log 30;}
|
||||
help_cmd(){ local p="$(current_profile 2>/dev/null||true)" count="$(list_profiles|wc -l)";cat <<EOF
|
||||
mh - mihomo profile/provider CLI
|
||||
add <name> <url|file> Add Clash YAML provider
|
||||
start [name] Choose/start provider profile
|
||||
stop | end Stop managed mihomo
|
||||
update Update current HTTP provider
|
||||
-l, --list [keyword] Speed-test/sort/choose nodes
|
||||
mode rule|global|direct Set mode and restart
|
||||
set cliproxy on|off Terminal env (shell hook)
|
||||
set sysproxy on|off GNOME system proxy
|
||||
set proxy on|off Both terminal/system proxy
|
||||
set tun on|off TUN and restart
|
||||
set rules show|update|reset Manage root bypass.list
|
||||
status | check [name] Status/config check
|
||||
shell-init | env | noenv Shell integration
|
||||
db [action] Debug menu/actions
|
||||
log | doctor Logs/diagnostics
|
||||
root: $MH_ROOT
|
||||
profiles: $count current: ${p:-<none>}
|
||||
mode: $(setting mode) tun: $(setting tun) cliproxy: $(setting cliproxy) sysproxy: $(setting sysproxy)
|
||||
ports: mixed=$MH_MIXED_PORT socks=$MH_SOCKS_PORT api=$MH_API_PORT
|
||||
EOF
|
||||
}
|
||||
|
||||
init_layout
|
||||
case "${1:-status}" in add)shift;add_cmd "$@";;start)shift;start_cmd "${1:-}";;stop|end)stop_cmd;;update)update_cmd;;-l|--list|list)shift;list_nodes_cmd "${1:-}";;mode)shift;mode_cmd "${1:-}";;set)shift;set_cmd "$@";;status)status_cmd;;check)shift;check_cmd "${1:-}";;env)show_env;;noenv)show_noenv;;shell-init)shell_init_cmd;;db)shift;db_cmd "$@";;log)touch "$MH_LOG";tail -n 80 -f "$MH_LOG";;doctor)doctor_cmd;;-h|--help|help)help_cmd;;*)help_cmd>&2;exit 2;;esac
|
||||
684
refers/sb
Executable file
684
refers/sb
Executable file
|
|
@ -0,0 +1,684 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
||||
SB_ROOT="${SING_BOX_ROOT:-$SCRIPT_DIR}"
|
||||
SB_BIN="${SING_BOX_BIN:-${SB_ROOT}/sing-box/sing-box}"
|
||||
SB_SYSTEM="${SB_ROOT}/system/config.json"
|
||||
SB_BYPASS="${SB_ROOT}/bypass.list"
|
||||
SB_PROFILES="${SB_ROOT}/profiles"
|
||||
SB_CURRENT="${SB_ROOT}/current.profile"
|
||||
SB_SETTINGS="${SB_ROOT}/settings.json"
|
||||
SB_RUN_DIR="${SING_BOX_RUN_DIR:-${SB_ROOT}/.runtime}"
|
||||
SB_CONFIG="${SB_RUN_DIR}/config.json"
|
||||
SB_RULES="${SB_RUN_DIR}/bypass-rules.json"
|
||||
SB_PID_FILE="${SB_RUN_DIR}/run.pid"
|
||||
SB_LOG="${SB_RUN_DIR}/sing-box.log"
|
||||
SB_PORT="${SING_BOX_PORT:-7890}"
|
||||
SB_API_PORT="${SING_BOX_API_PORT:-9090}"
|
||||
SB_API="http://127.0.0.1:${SB_API_PORT}"
|
||||
|
||||
die() { printf 'sb: %s\n' "$*" >&2; exit 1; }
|
||||
info() { printf 'sb: %s\n' "$*"; }
|
||||
warn() { printf 'sb: warning: %s\n' "$*" >&2; }
|
||||
need() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; }
|
||||
|
||||
init_layout() {
|
||||
[[ -x "$SB_BIN" ]] || die "sing-box binary is not executable: $SB_BIN"
|
||||
[[ -f "$SB_SYSTEM" ]] || die "system config not found: $SB_SYSTEM"
|
||||
[[ -f "$SB_BYPASS" ]] || die "bypass list not found: $SB_BYPASS"
|
||||
need jq
|
||||
mkdir -p "$SB_PROFILES" "$SB_RUN_DIR"
|
||||
if [[ ! -f "$SB_SETTINGS" ]]; then
|
||||
printf '%s\n' '{"mode":"rule","tun":false,"cliproxy":false,"sysproxy":false}' >"$SB_SETTINGS"
|
||||
fi
|
||||
}
|
||||
|
||||
setting() { jq -r --arg key "$1" '.[$key]' "$SB_SETTINGS"; }
|
||||
|
||||
set_setting() {
|
||||
local key="$1" value="$2"
|
||||
if [[ "$value" == true || "$value" == false ]]; then
|
||||
jq --arg key "$key" --argjson value "$value" '.[$key] = $value' "$SB_SETTINGS" >"${SB_SETTINGS}.tmp"
|
||||
else
|
||||
jq --arg key "$key" --arg value "$value" '.[$key] = $value' "$SB_SETTINGS" >"${SB_SETTINGS}.tmp"
|
||||
fi
|
||||
mv -f "${SB_SETTINGS}.tmp" "$SB_SETTINGS"
|
||||
}
|
||||
|
||||
profile_dir() { printf '%s/%s\n' "$SB_PROFILES" "$1"; }
|
||||
provider_file() { printf '%s/provider.json\n' "$(profile_dir "$1")"; }
|
||||
metadata_file() { printf '%s/source.json\n' "$(profile_dir "$1")"; }
|
||||
validate_name() { [[ "$1" =~ ^[A-Za-z0-9._-]+$ ]] || die 'profile name contains unsupported characters'; }
|
||||
|
||||
current_profile() {
|
||||
[[ -f "$SB_CURRENT" ]] || return 1
|
||||
local name
|
||||
name="$(tr -d '[:space:]' <"$SB_CURRENT")"
|
||||
[[ -n "$name" && -f "$(provider_file "$name")" ]] || return 1
|
||||
printf '%s\n' "$name"
|
||||
}
|
||||
|
||||
list_profiles() {
|
||||
local file
|
||||
for file in "$SB_PROFILES"/*/provider.json; do
|
||||
[[ -f "$file" ]] && basename "$(dirname "$file")"
|
||||
done | sort
|
||||
}
|
||||
|
||||
validate_provider() {
|
||||
jq -e 'type == "object" and (.outbounds | type == "array") and (.outbounds | length > 0)' "$1" >/dev/null \
|
||||
|| die "provider must be sing-box JSON with non-empty .outbounds: $1"
|
||||
}
|
||||
|
||||
fetch_provider() {
|
||||
local src="$1" dest="$2"
|
||||
if [[ "$src" =~ ^https?:// ]]; then
|
||||
need curl
|
||||
curl -fL --connect-timeout 15 --max-time 90 -A 'sb/1.0 sing-box-provider' "$src" -o "$dest"
|
||||
elif [[ -f "$src" ]]; then
|
||||
cp -f -- "$src" "$dest"
|
||||
else
|
||||
die "source is neither an HTTP URL nor a file: $src"
|
||||
fi
|
||||
validate_provider "$dest"
|
||||
}
|
||||
|
||||
add_cmd() {
|
||||
local name="${1:-}" src="${2:-}" dir tmp kind saved_src
|
||||
[[ -n "$name" && -n "$src" ]] || die 'usage: sb add <name> <url|file>'
|
||||
validate_name "$name"
|
||||
dir="$(profile_dir "$name")"
|
||||
[[ ! -e "$dir" ]] || die "profile already exists: $name"
|
||||
mkdir -p "$dir"
|
||||
tmp="${dir}/provider.json.tmp"
|
||||
if [[ "$src" =~ ^https?:// ]]; then kind=http; saved_src="$src"; else kind=file; saved_src="$(readlink -f "$src")"; fi
|
||||
if ! fetch_provider "$src" "$tmp"; then rm -f "$tmp"; rmdir "$dir" 2>/dev/null || true; return 1; fi
|
||||
mv -f "$tmp" "${dir}/provider.json"
|
||||
jq -n --arg name "$name" --arg type "$kind" --arg source "$saved_src" --arg added_at "$(date -Iseconds)" \
|
||||
'{name:$name,type:$type,source:$source,added_at:$added_at}' >"${dir}/source.json"
|
||||
printf '%s\n' "$name" >"$SB_CURRENT"
|
||||
info "added profile=${name} type=${kind}"
|
||||
info "run: sb start ${name}"
|
||||
}
|
||||
|
||||
compile_bypass_rules() {
|
||||
local line value prefix
|
||||
: >"${SB_RULES}.ndjson"
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%%#*}"
|
||||
line="$(sed -E 's/^[[:space:]]+|[[:space:]]+$//g' <<<"$line")"
|
||||
[[ -n "$line" ]] || continue
|
||||
if [[ "$line" == */* ]]; then
|
||||
jq -nc --arg v "$line" '{ip_cidr:[$v],action:"route",outbound:"DIRECT"}' >>"${SB_RULES}.ndjson"
|
||||
elif [[ "$line" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
jq -nc --arg v "${line}/32" '{ip_cidr:[$v],action:"route",outbound:"DIRECT"}' >>"${SB_RULES}.ndjson"
|
||||
elif [[ "$line" =~ ^(10|127|172|192)(\..*)?\*$ ]]; then
|
||||
prefix="${BASH_REMATCH[1]}"
|
||||
case "$prefix" in 10) value='10.0.0.0/8';; 127) value='127.0.0.0/8';; 172) value='172.16.0.0/12';; 192) value='192.168.0.0/16';; esac
|
||||
jq -nc --arg v "$value" '{ip_cidr:[$v],action:"route",outbound:"DIRECT"}' >>"${SB_RULES}.ndjson"
|
||||
elif [[ "$line" == \** || "$line" == .* ]]; then
|
||||
value="${line#\*}"; value="${value#.}"
|
||||
[[ -n "$value" ]] && jq -nc --arg v "$value" '{domain_suffix:[$v],action:"route",outbound:"DIRECT"}' >>"${SB_RULES}.ndjson"
|
||||
else
|
||||
jq -nc --arg v "$line" '{domain:[$v],action:"route",outbound:"DIRECT"}' >>"${SB_RULES}.ndjson"
|
||||
fi
|
||||
done <"$SB_BYPASS"
|
||||
jq -s 'unique' "${SB_RULES}.ndjson" >"${SB_RULES}.tmp"
|
||||
mv -f "${SB_RULES}.tmp" "$SB_RULES"
|
||||
rm -f "${SB_RULES}.ndjson"
|
||||
}
|
||||
|
||||
proxy_bypass_items() {
|
||||
local mode="$1" line value
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%%#*}"
|
||||
line="$(sed -E 's/^[[:space:]]+|[[:space:]]+$//g' <<<"$line")"
|
||||
[[ -n "$line" ]] || continue
|
||||
value="$line"
|
||||
if [[ "$mode" == no_proxy && "$value" == \*.* ]]; then
|
||||
value=".${value#*.}"
|
||||
elif [[ "$mode" == gsettings && "$value" == .* ]]; then
|
||||
value="*${value}"
|
||||
fi
|
||||
printf '%s\n' "$value"
|
||||
done <"$SB_BYPASS" | awk '!seen[$0]++'
|
||||
}
|
||||
|
||||
no_proxy_value() { proxy_bypass_items no_proxy | paste -sd, -; }
|
||||
gsettings_bypass_value() { proxy_bypass_items gsettings | jq -Rsc 'split("\n") | map(select(length > 0))'; }
|
||||
|
||||
build_config() {
|
||||
local profile="$1" provider mode tun
|
||||
provider="$(provider_file "$profile")"
|
||||
[[ -f "$provider" ]] || die "profile not found: $profile"
|
||||
validate_provider "$provider"
|
||||
compile_bypass_rules
|
||||
mode="$(setting mode)"; tun="$(setting tun)"
|
||||
jq -n --slurpfile provider "$provider" --slurpfile system "$SB_SYSTEM" --slurpfile bypass "$SB_RULES" \
|
||||
--arg mode "$mode" --argjson tun "$tun" --argjson port "$SB_PORT" --arg controller "127.0.0.1:${SB_API_PORT}" '
|
||||
($provider[0]) as $p | ($system[0]) as $s | ($bypass[0]) as $b
|
||||
| ($p.outbounds // []) as $original
|
||||
| ($original | map(.tag // empty)) as $tags0
|
||||
| ($original + if ($tags0|index("DIRECT")) == null then [{type:"direct",tag:"DIRECT"}] else [] end) as $direct_added
|
||||
| ($direct_added | map(.tag // empty)) as $tags1
|
||||
| ($direct_added + if ($tags1|index("Proxy")) == null then
|
||||
[{type:"selector",tag:"Proxy",outbounds:[$direct_added[]|select(.type!="direct" and .type!="block" and .type!="dns" and .type!="selector")|.tag]}]
|
||||
else [] end) as $outbounds
|
||||
| ($outbounds | map(.tag // empty)) as $outbound_tags
|
||||
| ([{type:"mixed",tag:"mixed-in",listen:"127.0.0.1",listen_port:$port,set_system_proxy:false}]
|
||||
+ if $tun then [($p.inbounds // [] | map(select(.type=="tun")) | .[0]
|
||||
// {type:"tun",tag:"tun-in",address:["172.19.0.1/30","fdfe:dcba:9876::1/126"],auto_route:true,strict_route:true})] else [] end) as $inbounds
|
||||
| ([{action:"sniff"}] + if $tun then [{protocol:"dns",action:"hijack-dns"}] else [] end + $b) as $system_rules
|
||||
| (($p.route.rules // []) | map(select(.action!="sniff" and .action!="hijack-dns"))) as $provider_rules
|
||||
| (if $mode=="rule" then $system_rules+$provider_rules else $system_rules end) as $rules
|
||||
| (if $mode=="direct" then "DIRECT" elif $mode=="global" then "Proxy"
|
||||
elif (($p.route.final // "") as $f | ($outbound_tags|index($f)) != null) then $p.route.final else "Proxy" end) as $final
|
||||
| $p
|
||||
| .log=$s.log | .dns=$s.dns | .inbounds=$inbounds | .outbounds=$outbounds
|
||||
| .route=(($p.route // {}) * ($s.route // {}))
|
||||
| .route.auto_detect_interface=$tun | .route.rules=$rules | .route.final=$final
|
||||
| .experimental=(($p.experimental // {}) * ($s.experimental // {}))
|
||||
| .experimental.clash_api.external_controller=$controller
|
||||
' >"${SB_CONFIG}.tmp"
|
||||
mv -f "${SB_CONFIG}.tmp" "$SB_CONFIG"
|
||||
}
|
||||
|
||||
process_alive() {
|
||||
local pid="$1" state _
|
||||
[[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null || return 1
|
||||
read -r _ _ state _ <"/proc/${pid}/stat" 2>/dev/null || return 1
|
||||
[[ "$state" != Z ]]
|
||||
}
|
||||
|
||||
is_running() {
|
||||
[[ -f "$SB_PID_FILE" ]] || return 1
|
||||
process_alive "$(<"$SB_PID_FILE")"
|
||||
}
|
||||
|
||||
port_in_use() {
|
||||
ss -ltnH 2>/dev/null | awk -v suffix=":$1" 'substr($4,length($4)-length(suffix)+1)==suffix{f=1} END{exit !f}'
|
||||
}
|
||||
|
||||
api_curl() {
|
||||
local secret="$(jq -r '.experimental.clash_api.secret // empty' "$SB_CONFIG" 2>/dev/null || true)"
|
||||
if [[ -n "$secret" ]]; then curl -fsS --noproxy '*' -H "Authorization: Bearer ${secret}" "$@"; else curl -fsS --noproxy '*' "$@"; fi
|
||||
}
|
||||
|
||||
wait_api() { local i; for i in {1..50}; do api_curl "${SB_API}/proxies" >/dev/null 2>&1 && return 0; sleep 0.1; done; return 1; }
|
||||
|
||||
stop_cmd() {
|
||||
if ! is_running; then rm -f "$SB_PID_FILE"; info 'not running'; return 0; fi
|
||||
local pid="$(<"$SB_PID_FILE")" expected actual comm cmdline i
|
||||
expected="$(readlink -f "$SB_BIN")"
|
||||
actual="$(readlink -f "/proc/${pid}/exe" 2>/dev/null || true)"
|
||||
comm="$(cat "/proc/${pid}/comm" 2>/dev/null || true)"
|
||||
cmdline="$(tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true)"
|
||||
if [[ -z "$actual" ]] && ! process_alive "$pid"; then
|
||||
rm -f "$SB_PID_FILE"; info 'process already exited'; return 0
|
||||
fi
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
[[ "$comm" == "$(basename "$SB_BIN")" && "$cmdline" == "$SB_BIN run "* && "$cmdline" == *" -c $SB_CONFIG"* ]] \
|
||||
|| die "refusing to stop pid ${pid}: process identity does not match sb runtime"
|
||||
fi
|
||||
kill "$pid" 2>/dev/null || true
|
||||
for i in {1..30}; do process_alive "$pid" || break; sleep 0.1; done
|
||||
process_alive "$pid" && die "pid ${pid} did not stop"
|
||||
rm -f "$SB_PID_FILE"; info "stopped pid ${pid}"
|
||||
}
|
||||
|
||||
choose_profile() {
|
||||
local requested="${1:-}" current pick i
|
||||
local -a profiles
|
||||
mapfile -t profiles < <(list_profiles)
|
||||
(( ${#profiles[@]} )) || die 'no providers; use: sb add <name> <url|file>'
|
||||
current="$(current_profile 2>/dev/null || true)"
|
||||
if [[ -n "$requested" ]]; then [[ -f "$(provider_file "$requested")" ]] || die "profile not found: $requested"; echo "$requested"; return; fi
|
||||
if [[ ! -t 0 ]]; then [[ -n "$current" ]] || die 'non-interactive start requires profile name'; echo "$current"; return; fi
|
||||
echo 'Available providers:' >&2
|
||||
for i in "${!profiles[@]}"; do
|
||||
[[ "${profiles[$i]}" == "$current" ]] && printf ' %d) %s (current)\n' "$((i+1))" "${profiles[$i]}" >&2 || printf ' %d) %s\n' "$((i+1))" "${profiles[$i]}" >&2
|
||||
done
|
||||
while true; do
|
||||
read -r -p "Choose provider (1-${#profiles[@]}, 0 cancel): " pick
|
||||
[[ "$pick" =~ ^[0-9]+$ ]] || { echo 'Please enter a number.' >&2; continue; }
|
||||
(( pick==0 )) && return 1
|
||||
(( pick>=1 && pick<=${#profiles[@]} )) || { echo 'Out of range.' >&2; continue; }
|
||||
echo "${profiles[$((pick-1))]}"; return
|
||||
done
|
||||
}
|
||||
|
||||
start_cmd() {
|
||||
local profile pid i
|
||||
profile="$(choose_profile "${1:-}")" || { info 'cancelled'; return; }
|
||||
is_running && stop_cmd
|
||||
build_config "$profile"
|
||||
"$SB_BIN" check -D "$SB_RUN_DIR" -c "$SB_CONFIG"
|
||||
need ss
|
||||
port_in_use "$SB_PORT" && die "port ${SB_PORT} is in use (stop mihomo before sb)"
|
||||
port_in_use "$SB_API_PORT" && die "API port ${SB_API_PORT} is in use (stop mihomo before sb)"
|
||||
: >"$SB_LOG"
|
||||
nohup "$SB_BIN" run -D "$SB_RUN_DIR" -c "$SB_CONFIG" >>"$SB_LOG" 2>&1 & pid=$!
|
||||
echo "$pid" >"$SB_PID_FILE"; echo "$profile" >"$SB_CURRENT"
|
||||
for i in {1..40}; do
|
||||
if ! kill -0 "$pid" 2>/dev/null; then rm -f "$SB_PID_FILE"; tail -n 50 "$SB_LOG" >&2 || true; die 'sing-box exited during startup'; fi
|
||||
if port_in_use "$SB_PORT" && port_in_use "$SB_API_PORT"; then
|
||||
info "started profile=${profile} pid=${pid} mode=$(setting mode) tun=$(setting tun)"; wait_api || warn 'API not ready'; return
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
tail -n 50 "$SB_LOG" >&2 || true; die 'startup timed out'
|
||||
}
|
||||
|
||||
restart_if_running() {
|
||||
local profile="$(current_profile 2>/dev/null || true)"
|
||||
if is_running; then stop_cmd; start_cmd "$profile"; fi
|
||||
}
|
||||
|
||||
require_api() { is_running || die 'sing-box is not running'; api_curl "${SB_API}/proxies" >/dev/null || die "API unavailable: $SB_API"; }
|
||||
urlencode() { jq -nr --arg v "$1" '$v|@uri'; }
|
||||
|
||||
api_delay() {
|
||||
local node="$1" timeout="${2:-1800}" encoded url
|
||||
encoded="$(urlencode "$node")"; url="$(urlencode 'https://www.gstatic.com/generate_204')"
|
||||
api_curl "${SB_API}/proxies/${encoded}/delay?timeout=${timeout}&url=${url}" 2>/dev/null \
|
||||
| jq -r 'if (.delay|type)=="number" then .delay else 999999 end' 2>/dev/null || echo 999999
|
||||
}
|
||||
|
||||
delay_text() {
|
||||
local delay="${1:-}"
|
||||
[[ "$delay" =~ ^[0-9]+$ && "$delay" != 999999 ]] && printf '%sms\n' "$delay" || printf 'timeout\n'
|
||||
}
|
||||
|
||||
main_group() { jq -r 'if .proxies.Proxy.all then "Proxy" else [.proxies|to_entries[]|select(.value.all!=null)][0].key//empty end' <<<"$1"; }
|
||||
|
||||
display_node_grid() {
|
||||
local -n ref=$1
|
||||
local count=${#ref[@]} width cols=1 rows r c idx
|
||||
width="$(tput cols 2>/dev/null || echo 120)"
|
||||
(( count>18 && width>=110 )) && cols=2
|
||||
(( count>36 && width>=170 )) && cols=3
|
||||
rows=$(((count+cols-1)/cols))
|
||||
for ((r=0;r<rows;r++)); do
|
||||
for ((c=0;c<cols;c++)); do
|
||||
idx=$((r+c*rows)); ((idx<count)) || continue
|
||||
if ((c+1<cols && idx+rows<count)); then printf '%-56s' "${ref[$idx]}"; else printf '%s' "${ref[$idx]}"; fi
|
||||
done
|
||||
echo
|
||||
done
|
||||
}
|
||||
|
||||
list_nodes_cmd() {
|
||||
local keyword="${1:-}" json group now candidates tmp running=0 i d pick encoded body
|
||||
local -a nodes sorted_nodes=() sorted_delays=() display=()
|
||||
require_api; json="$(api_curl "${SB_API}/proxies")"; group="$(main_group "$json")"; [[ -n "$group" ]] || die 'no selectable group'
|
||||
now="$(jq -r --arg g "$group" '.proxies[$g].now//empty' <<<"$json")"
|
||||
candidates="$(jq -r --arg g "$group" --arg kw "$keyword" '
|
||||
def lc: ascii_downcase; . as $r | .proxies[$g].all[] as $n | ($r.proxies[$n].type//"") as $t
|
||||
| select(["Direct","Reject","Block","Selector","URLTest","Fallback","LoadBalance"]|index($t)==null)
|
||||
| select($kw=="" or ($n|contains($kw)) or (($n|lc)|contains($kw|lc))) | $n' <<<"$json")"
|
||||
[[ -n "$candidates" ]] || die "no nodes matched: ${keyword:-<all>}"; mapfile -t nodes <<<"$candidates"
|
||||
tmp="$(mktemp -d "${SB_RUN_DIR}/delay.XXXXXX")"; info "testing ${#nodes[@]} nodes (1800ms, 8 jobs)..."
|
||||
for i in "${!nodes[@]}"; do
|
||||
(api_delay "${nodes[$i]}" >"${tmp}/${i}") & running=$((running+1))
|
||||
if ((running>=8)); then wait -n 2>/dev/null || wait; running=$((running-1)); fi
|
||||
done
|
||||
wait; : >"${tmp}/index"
|
||||
for i in "${!nodes[@]}"; do d="$(<"${tmp}/${i}")"; [[ "$d" =~ ^[0-9]+$ ]] || d=999999; printf '%s\t%s\n' "$d" "$i" >>"${tmp}/index"; done
|
||||
while IFS=$'\t' read -r d i; do sorted_delays+=("$d"); sorted_nodes+=("${nodes[$i]}"); done < <(sort -n "${tmp}/index")
|
||||
rm -rf -- "$tmp"
|
||||
printf 'Group: %s Current: %s' "$group" "$now"; [[ -n "$keyword" ]] && printf ' Filter: %s' "$keyword"; echo
|
||||
for i in "${!sorted_nodes[@]}"; do
|
||||
d="$(delay_text "${sorted_delays[$i]}")"
|
||||
[[ "${sorted_nodes[$i]}" == "$now" ]] && display+=("[$((i+1))] * ${sorted_nodes[$i]} ${d}") || display+=("[$((i+1))] ${sorted_nodes[$i]} ${d}")
|
||||
done
|
||||
display_node_grid display
|
||||
[[ -t 0 ]] || return
|
||||
while true; do
|
||||
read -r -p "Choose node (1-${#sorted_nodes[@]}, 0 cancel): " pick
|
||||
[[ "$pick" =~ ^[0-9]+$ ]] || { echo 'Please enter a number.'; continue; }
|
||||
((pick==0)) && { info cancelled; return; }
|
||||
((pick>=1 && pick<=${#sorted_nodes[@]})) || { echo 'Out of range.'; continue; }; break
|
||||
done
|
||||
encoded="$(urlencode "$group")"; body="$(jq -nc --arg name "${sorted_nodes[$((pick-1))]}" '{name:$name}')"
|
||||
api_curl -X PUT -H 'Content-Type: application/json' --data "$body" "${SB_API}/proxies/${encoded}" >/dev/null
|
||||
info "${group} -> ${sorted_nodes[$((pick-1))]} (${sorted_delays[$((pick-1))]}ms)"
|
||||
}
|
||||
|
||||
show_env() {
|
||||
local no_proxy
|
||||
no_proxy="$(no_proxy_value)"
|
||||
cat <<EOF
|
||||
export http_proxy="http://127.0.0.1:${SB_PORT}"
|
||||
export https_proxy="http://127.0.0.1:${SB_PORT}"
|
||||
export all_proxy="socks5h://127.0.0.1:${SB_PORT}"
|
||||
export HTTP_PROXY="\$http_proxy"
|
||||
export HTTPS_PROXY="\$https_proxy"
|
||||
export ALL_PROXY="\$all_proxy"
|
||||
EOF
|
||||
printf 'export NO_PROXY=%q\n' "$no_proxy"
|
||||
printf '%s\n' 'export no_proxy="$NO_PROXY"'
|
||||
}
|
||||
show_noenv() { echo 'unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY no_proxy'; }
|
||||
|
||||
shell_init_cmd() {
|
||||
local real="$(printf '%q' "$SCRIPT_PATH")"
|
||||
cat <<EOF
|
||||
sb() {
|
||||
SB_SHELL_HOOK=1 ${real} "\$@"
|
||||
local rc=\$?
|
||||
if ((rc==0)) && [[ "\${1:-}" == set ]] && { [[ "\${2:-}" == cliproxy ]] || [[ "\${2:-}" == proxy ]]; }; then
|
||||
if [[ "\${3:-}" == on ]]; then eval "\$(${real} env)"; else eval "\$(${real} noenv)"; fi
|
||||
fi
|
||||
return \$rc
|
||||
}
|
||||
EOF
|
||||
[[ "$(setting cliproxy)" == true ]] && show_env || show_noenv
|
||||
}
|
||||
|
||||
sysproxy_cmd() {
|
||||
local action="$1" ignore_hosts; need gsettings
|
||||
case "$action" in
|
||||
on)
|
||||
ignore_hosts="$(gsettings_bypass_value)"
|
||||
gsettings set org.gnome.system.proxy mode manual
|
||||
gsettings set org.gnome.system.proxy.http host 127.0.0.1; gsettings set org.gnome.system.proxy.http port "$SB_PORT"
|
||||
gsettings set org.gnome.system.proxy.https host 127.0.0.1; gsettings set org.gnome.system.proxy.https port "$SB_PORT"
|
||||
gsettings set org.gnome.system.proxy.socks host 127.0.0.1; gsettings set org.gnome.system.proxy.socks port "$SB_PORT"
|
||||
gsettings set org.gnome.system.proxy ignore-hosts "$ignore_hosts"
|
||||
set_setting sysproxy true; info "system proxy ON -> 127.0.0.1:${SB_PORT}" ;;
|
||||
off) gsettings set org.gnome.system.proxy mode none; set_setting sysproxy false; info 'system proxy OFF' ;;
|
||||
status) printf 'sysproxy setting=%s gnome-mode=%s\n' "$(setting sysproxy)" "$(gsettings get org.gnome.system.proxy mode)" ;;
|
||||
*) die 'usage: sb set sysproxy on|off' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
default_bypass() {
|
||||
cat <<'EOF'
|
||||
# sb system bypass rules (one item per line)
|
||||
localhost
|
||||
*.local
|
||||
127.0.0.0/8
|
||||
10.0.0.0/8
|
||||
172.16.0.0/12
|
||||
192.168.0.0/16
|
||||
100.64.0.2
|
||||
100.64.0.0/10
|
||||
::1/128
|
||||
fc00::/7
|
||||
fe80::/10
|
||||
EOF
|
||||
}
|
||||
|
||||
rules_cmd() {
|
||||
case "${1:-show}" in
|
||||
show) echo "bypass.list: $SB_BYPASS"; nl -ba "$SB_BYPASS" ;;
|
||||
update) compile_bypass_rules; [[ "$(setting sysproxy)" == true ]] && sysproxy_cmd on; info "compiled $(jq length "$SB_RULES") bypass rules"; restart_if_running ;;
|
||||
reset) default_bypass >"${SB_BYPASS}.tmp"; mv -f "${SB_BYPASS}.tmp" "$SB_BYPASS"; compile_bypass_rules; [[ "$(setting sysproxy)" == true ]] && sysproxy_cmd on; info 'bypass.list reset'; restart_if_running ;;
|
||||
*) die 'usage: sb set rules show|update|reset' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
set_cmd() {
|
||||
local target="${1:-}" action="${2:-}"
|
||||
[[ -n "$target" ]] || { jq . "$SB_SETTINGS"; sysproxy_cmd status 2>/dev/null || true; return; }
|
||||
case "$target" in
|
||||
cliproxy)
|
||||
[[ "$action" == on || "$action" == off ]] || die 'usage: sb set cliproxy on|off'
|
||||
set_setting cliproxy "$([[ "$action" == on ]] && echo true || echo false)"; info "CLI proxy ${action}"
|
||||
[[ -n "${SB_SHELL_HOOK:-}" ]] || warn 'current shell needs one-time: eval "$(sb shell-init)"' ;;
|
||||
sysproxy) sysproxy_cmd "$action" ;;
|
||||
proxy)
|
||||
[[ "$action" == on || "$action" == off ]] || die 'usage: sb set proxy on|off'
|
||||
set_setting cliproxy "$([[ "$action" == on ]] && echo true || echo false)"; sysproxy_cmd "$action"; info "CLI + system proxy ${action}"
|
||||
[[ -n "${SB_SHELL_HOOK:-}" ]] || warn 'current shell needs one-time: eval "$(sb shell-init)"' ;;
|
||||
tun)
|
||||
[[ "$action" == on || "$action" == off ]] || die 'usage: sb set tun on|off'
|
||||
set_setting tun "$([[ "$action" == on ]] && echo true || echo false)"
|
||||
if [[ "$action" == on ]] && ! getcap "$SB_BIN" 2>/dev/null | grep -q cap_net_admin; then warn "TUN may need: sudo setcap cap_net_admin,cap_net_raw+ep '$SB_BIN'"; fi
|
||||
info "TUN ${action}"; restart_if_running ;;
|
||||
rules) rules_cmd "$action" ;;
|
||||
*) die 'usage: sb set cliproxy|sysproxy|proxy|tun|rules ...' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_cmd() {
|
||||
local profile meta source type dir tmp running=false
|
||||
profile="$(current_profile 2>/dev/null || true)"; [[ -n "$profile" ]] || die 'no current profile'
|
||||
meta="$(metadata_file "$profile")"; [[ -f "$meta" ]] || die "missing metadata: $meta"
|
||||
type="$(jq -r .type "$meta")"; source="$(jq -r .source "$meta")"; [[ "$type" == http ]] || die 'current provider is a local file, not HTTP'
|
||||
is_running && running=true; dir="$(profile_dir "$profile")"; tmp="${dir}/provider.json.tmp"
|
||||
fetch_provider "$source" "$tmp"; mv -f "$tmp" "${dir}/provider.json"
|
||||
jq --arg t "$(date -Iseconds)" '.updated_at=$t' "$meta" >"${meta}.tmp"; mv -f "${meta}.tmp" "$meta"
|
||||
info "updated HTTP provider: $profile"; [[ "$running" == true ]] && { stop_cmd; start_cmd "$profile"; }
|
||||
}
|
||||
|
||||
mode_cmd() {
|
||||
local mode="${1:-}"; [[ "$mode" == rule || "$mode" == global || "$mode" == direct ]] || die 'usage: sb mode rule|global|direct'
|
||||
set_setting mode "$mode"; info "mode -> $mode"; restart_if_running
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
local profile="$(current_profile 2>/dev/null || true)" now='' delay=''
|
||||
if is_running; then
|
||||
now="$(api_curl "${SB_API}/proxies/Proxy" 2>/dev/null | jq -r '.now//empty' || true)"
|
||||
[[ -n "$now" ]] && delay="$(delay_text "$(api_delay "$now")")"
|
||||
printf 'sing-box: RUNNING pid=%s\n' "$(<"$SB_PID_FILE")"
|
||||
else
|
||||
echo 'sing-box: STOPPED'
|
||||
fi
|
||||
printf 'root: %s\nprofile: %s\nmode: %s\ntun: %s\ncliproxy: %s\nsysproxy: %s\nmixed: 127.0.0.1:%s\napi: 127.0.0.1:%s\n' \
|
||||
"$SB_ROOT" "${profile:-<none>}" "$(setting mode)" "$(setting tun)" "$(setting cliproxy)" "$(setting sysproxy)" "$SB_PORT" "$SB_API_PORT"
|
||||
[[ -n "$now" ]] && printf 'node: %s %s\n' "$now" "$delay"
|
||||
}
|
||||
|
||||
check_cmd() {
|
||||
local profile="${1:-}"; [[ -n "$profile" ]] || profile="$(current_profile 2>/dev/null || true)"; [[ -n "$profile" ]] || die 'no profile to check'
|
||||
build_config "$profile"; "$SB_BIN" check -D "$SB_RUN_DIR" -c "$SB_CONFIG"; info "configuration OK: profile=$profile"
|
||||
}
|
||||
|
||||
db_ports() {
|
||||
echo "== listening proxy ports =="
|
||||
ss -ltnp 2>/dev/null | awk -v p1=":${SB_PORT}" -v p2=":${SB_API_PORT}" 'NR==1 || index($4,p1) || index($4,p2)'
|
||||
if ! port_in_use "$SB_PORT" && ! port_in_use "$SB_API_PORT"; then echo 'no sb ports listening'; fi
|
||||
}
|
||||
|
||||
db_process() {
|
||||
local pid="${1:-}"
|
||||
[[ -n "$pid" ]] || { [[ -f "$SB_PID_FILE" ]] && pid="$(<"$SB_PID_FILE")" || true; }
|
||||
[[ "$pid" =~ ^[0-9]+$ ]] || { echo 'no managed PID'; return 1; }
|
||||
ps -p "$pid" -o pid,ppid,user,state,etime,%cpu,%mem,cmd
|
||||
echo
|
||||
printf 'comm: '; cat "/proc/${pid}/comm" 2>/dev/null || echo '<unavailable>'
|
||||
printf 'cmdline: '; tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true; echo
|
||||
}
|
||||
|
||||
db_probe_one() {
|
||||
local mode="$1" url="$2"
|
||||
case "$mode" in
|
||||
direct) curl -fsSIL --max-time 10 --noproxy '*' "$url" -o /dev/null ;;
|
||||
proxy) curl -fsSIL --max-time 10 --proxy "http://127.0.0.1:${SB_PORT}" "$url" -o /dev/null ;;
|
||||
esac
|
||||
}
|
||||
|
||||
db_net() {
|
||||
local mode="${1:-proxy}" url direct_result proxy_result
|
||||
local -a urls=(
|
||||
'https://www.google.com'
|
||||
'https://www.cloudflare.com'
|
||||
'https://www.github.com'
|
||||
'https://www.youtube.com'
|
||||
'https://www.baidu.com'
|
||||
'https://www.bing.com'
|
||||
)
|
||||
case "$mode" in
|
||||
proxy)
|
||||
is_running || die 'sing-box is not running; proxy test unavailable'
|
||||
echo "== proxy network test (127.0.0.1:${SB_PORT}) =="
|
||||
for url in "${urls[@]}"; do db_probe_one proxy "$url" && proxy_result=OK || proxy_result=FAIL; printf '%-32s PROXY:%s\n' "$url" "$proxy_result"; done
|
||||
;;
|
||||
direct)
|
||||
echo '== direct network test =='
|
||||
for url in "${urls[@]}"; do db_probe_one direct "$url" && direct_result=OK || direct_result=FAIL; printf '%-32s DIRECT:%s\n' "$url" "$direct_result"; done
|
||||
;;
|
||||
compare)
|
||||
is_running || die 'sing-box is not running; comparison needs the mixed proxy'
|
||||
echo '== DIRECT vs PROXY =='
|
||||
for url in "${urls[@]}"; do
|
||||
db_probe_one direct "$url" && direct_result=OK || direct_result=FAIL
|
||||
db_probe_one proxy "$url" && proxy_result=OK || proxy_result=FAIL
|
||||
printf '%-32s DIRECT:%-4s PROXY:%-4s\n' "$url" "$direct_result" "$proxy_result"
|
||||
done
|
||||
;;
|
||||
*) die 'usage: sb db net [proxy|direct] | sb db compare' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
db_deps() {
|
||||
local item path rc=0
|
||||
local -a required=(jq curl ss sed sort readlink)
|
||||
local -a optional=(gsettings getcap ip resolvectl tput)
|
||||
echo '== required commands =='
|
||||
for item in "${required[@]}"; do path="$(command -v "$item" 2>/dev/null || true)"; [[ -n "$path" ]] && printf 'OK %-12s %s\n' "$item" "$path" || { printf 'MISSING %s\n' "$item"; rc=1; }; done
|
||||
echo '== optional commands =='
|
||||
for item in "${optional[@]}"; do path="$(command -v "$item" 2>/dev/null || true)"; [[ -n "$path" ]] && printf 'OK %-12s %s\n' "$item" "$path" || printf 'MISSING %s\n' "$item"; done
|
||||
[[ -x "$SB_BIN" ]] && echo "OK sing-box $SB_BIN" || { echo "MISSING sing-box $SB_BIN"; rc=1; }
|
||||
jq -e . "$SB_SYSTEM" "$SB_SETTINGS" >/dev/null && echo 'OK system/settings JSON' || rc=1
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
db_interfaces() {
|
||||
echo '== interfaces =='
|
||||
if command -v ip >/dev/null 2>&1; then ip -brief link show; else echo 'ip command not found'; fi
|
||||
echo
|
||||
echo '== TUN expectation =='
|
||||
printf 'setting: %s\n' "$(setting tun)"
|
||||
if command -v ip >/dev/null 2>&1; then ip -brief link show | awk '$1 ~ /^(tun|sing|utun)/ {print}' || true; fi
|
||||
}
|
||||
|
||||
db_dns() {
|
||||
echo '== sb DNS configuration =='
|
||||
jq .dns "$SB_SYSTEM"
|
||||
echo
|
||||
echo '== host resolver =='
|
||||
if command -v resolvectl >/dev/null 2>&1; then resolvectl status 2>/dev/null | sed -n '1,140p'; else cat /etc/resolv.conf; fi
|
||||
}
|
||||
|
||||
db_routes() {
|
||||
if ! command -v ip >/dev/null 2>&1; then echo 'ip command not found'; return 1; fi
|
||||
echo '== routes =='; ip route show
|
||||
echo; echo '== policy rules =='; ip rule show
|
||||
echo; echo '== IPv6 routes =='; ip -6 route show 2>/dev/null || true
|
||||
}
|
||||
|
||||
db_api() {
|
||||
local json
|
||||
require_api
|
||||
echo "== Clash API ${SB_API} =="
|
||||
api_curl "${SB_API}/version" | jq .
|
||||
json="$(api_curl "${SB_API}/proxies")"
|
||||
echo; echo '== selector groups =='
|
||||
jq -r '.proxies | to_entries[] | select(.value.all != null) | "\(.key) -> \(.value.now // "-") (\(.value.all|length) choices)"' <<<"$json"
|
||||
}
|
||||
|
||||
db_config() {
|
||||
local profile
|
||||
profile="$(current_profile 2>/dev/null || true)"
|
||||
[[ -n "$profile" ]] || die 'no current profile'
|
||||
check_cmd "$profile"
|
||||
jq '{log,inbounds,outbounds:(.outbounds|map({type,tag})),dns,route:{final:.route.final,rule_count:(.route.rules|length),rule_set_count:(.route.rule_set//[]|length)},experimental}' "$SB_CONFIG"
|
||||
}
|
||||
|
||||
db_log() {
|
||||
local lines="${1:-80}"
|
||||
[[ "$lines" =~ ^[0-9]+$ ]] || die 'usage: sb db log [lines]'
|
||||
[[ -f "$SB_LOG" ]] && tail -n "$lines" "$SB_LOG" || echo 'no log file'
|
||||
}
|
||||
|
||||
db_menu() {
|
||||
local pick i
|
||||
local -a commands=(ports process net compare deps interfaces dns routes api config log)
|
||||
local -a descriptions=(
|
||||
'proxy/API port listeners'
|
||||
'managed process details'
|
||||
'proxy-only network test'
|
||||
'direct versus proxy network test'
|
||||
'required and optional dependencies'
|
||||
'network interfaces and TUN expectation'
|
||||
'sing-box DNS and host resolver'
|
||||
'routes and policy routing'
|
||||
'Clash API and selector groups'
|
||||
'merged configuration summary and check'
|
||||
'recent sing-box log'
|
||||
)
|
||||
echo '== sb db =='
|
||||
for i in "${!commands[@]}"; do printf ' %2d) %-12s %s\n' "$((i+1))" "${commands[$i]}" "${descriptions[$i]}"; done
|
||||
[[ -t 0 ]] || return 0
|
||||
while true; do
|
||||
read -r -p "Choose debug action (1-${#commands[@]}, 0 cancel): " pick
|
||||
[[ "$pick" =~ ^[0-9]+$ ]] || { echo 'Please enter a number.'; continue; }
|
||||
(( pick==0 )) && { info cancelled; return 0; }
|
||||
(( pick>=1 && pick<=${#commands[@]} )) || { echo 'Out of range.'; continue; }
|
||||
db_cmd "${commands[$((pick-1))]}"; return
|
||||
done
|
||||
}
|
||||
|
||||
db_cmd() {
|
||||
local action="${1:-menu}"; shift || true
|
||||
case "$action" in
|
||||
menu) db_menu;; ports) db_ports;; process) db_process "${1:-}";;
|
||||
net) db_net "${1:-proxy}";; compare) db_net compare;; deps) db_deps;;
|
||||
interfaces|ifaces) db_interfaces;; dns) db_dns;; routes|route) db_routes;;
|
||||
api) db_api;; config) db_config;; log) db_log "${1:-80}";;
|
||||
help|-h|--help) db_menu;;
|
||||
*) die 'usage: sb db [ports|process|net|compare|deps|interfaces|dns|routes|api|config|log]' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
doctor_cmd() {
|
||||
echo '== sb doctor =='; status_cmd; echo; "$SB_BIN" version | sed -n '1,2p'; getcap "$SB_BIN" 2>/dev/null || true
|
||||
echo; echo '== profiles =='; list_profiles || true; echo; echo '== config =='; check_cmd
|
||||
echo; echo '== log =='; [[ -f "$SB_LOG" ]] && tail -n 30 "$SB_LOG" || echo 'no log'
|
||||
}
|
||||
|
||||
help_cmd() {
|
||||
local profile="$(current_profile 2>/dev/null || true)" count="$(list_profiles | wc -l)"
|
||||
cat <<EOF
|
||||
sb - sing-box profile/provider CLI
|
||||
|
||||
add <name> <url|file> Add a sing-box JSON provider
|
||||
start [name] Choose and start a provider profile
|
||||
stop | end Stop managed sing-box
|
||||
update Update current HTTP provider
|
||||
-l, --list [keyword] Speed-test, sort, show, choose nodes
|
||||
mode rule|global|direct Set route mode and restart
|
||||
set cliproxy on|off Terminal proxy state (shell hook)
|
||||
set sysproxy on|off GNOME system proxy
|
||||
set proxy on|off Both terminal and system proxy
|
||||
set tun on|off TUN and restart
|
||||
set rules show|update|reset Manage root bypass.list
|
||||
status | check [name] Status / merged config check
|
||||
shell-init Print Bash integration
|
||||
env | noenv Print exports / unsets
|
||||
db [action] Debug menu: ports/process/net/DNS/routes/API/config
|
||||
log | doctor Log / diagnostics
|
||||
-h, --help Help plus concise configuration
|
||||
|
||||
root: $SB_ROOT
|
||||
profiles: $count current: ${profile:-<none>}
|
||||
mode: $(setting mode) tun: $(setting tun) cliproxy: $(setting cliproxy) sysproxy: $(setting sysproxy)
|
||||
mixed: 127.0.0.1:$SB_PORT api: 127.0.0.1:$SB_API_PORT
|
||||
EOF
|
||||
}
|
||||
|
||||
init_layout
|
||||
case "${1:-status}" in
|
||||
add) shift; add_cmd "$@";; start) shift; start_cmd "${1:-}";; stop|end) stop_cmd;; update) update_cmd;;
|
||||
-l|--list|list) shift; list_nodes_cmd "${1:-}";; mode) shift; mode_cmd "${1:-}";; set) shift; set_cmd "$@";;
|
||||
status) status_cmd;; check) shift; check_cmd "${1:-}";; env) show_env;; noenv) show_noenv;; shell-init) shell_init_cmd;;
|
||||
db) shift; db_cmd "$@";;
|
||||
log) touch "$SB_LOG"; tail -n 80 -f "$SB_LOG";; doctor) doctor_cmd;; -h|--help|help) help_cmd;;
|
||||
*) help_cmd >&2; exit 2;;
|
||||
esac
|
||||
Loading…
Add table
Add a link
Reference in a new issue