#!/bin/bash

export PATH="$PATH:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"

# 除了主脚本，其他文件都放这个目录，此值可被命令行参数覆盖
base_dir='/etc/ss-tproxy' # 同时也是工作目录
config_file='ss-tproxy.conf' # 可被命令行参数覆盖

# 已存在的函数和变量尽量别改名/删除，因为可能被 ss-tproxy.conf 引用(如钩子函数)
# 由于需要执行 iptables、策略路由、内核参数 等特权操作，脚本必须以 root 权限执行

# 有xtables锁时进行等待，而不是报错退出
iptables() {
    command iptables -w "$@"
}

# 有xtables锁时进行等待，而不是报错退出
ip6tables() {
    command ip6tables -w "$@"
}

font_bold() {
    printf "\e[1m$*\e[0m"
}

color_red() {
    printf "\e[35m$*\e[0m"
}

color_green() {
    printf "\e[32m$*\e[0m"
}

color_yellow() {
    printf "\e[31m$*\e[0m"
}

# 打印错误消息，并退出脚本
log_error() {
    echo "$(font_bold $(color_yellow '[ERROR]')) $*" 1>&2
    exit 1
}

is_func() {
    [ "$(type -t $1)" = 'function' ]
}

# 如果函数存在，则调用
call_func() {
    is_func "$1" && "$@"
}

# 使用数值0和1做布尔值最好，但由于历史原因，没法改了
is_true() {
    [ "$1" = 'true' ]
}

is_false() {
    ! is_true "$1"
}

is_uint() {
    [ "$1" ] && [ -z "${1//[0-9]/}" ]
}

# str substr
str_find() {
    [[ "$1" == *"$2"* ]]
}

file_is_exists() {
    [ -f "$1" ]
}

file_required() {
    file_is_exists "$1" || log_error "file not found: $1"
}

list_ext_ipv4() {
    grep '^-' "$1" | cut -c2-
}

list_ext_ipv6() {
    grep '^~' "$1" | cut -c2-
}

list_ext_domain() {
    grep '^@' "$1" | cut -c2-
}

command_path() {
    type -P "$1"
}

command_is_exists() {
    type -P "$1" &>/dev/null
}

command_required() {
    command_is_exists "$1" || log_error "command not found: $1"
}

group_is_exists() {
    if is_uint "$1"; then # gid
        grep -q ":$1:" /etc/group
    else # name
        grep -q "^$1:" /etc/group
    fi
}

create_group() {
    if is_uint "$1"; then # gid
        log_error "gid:'$1' not exists, please check proxy_procgroup/dns_procgroup"
    elif command_is_exists groupadd; then
        groupadd "$1" || log_error "failed to create group:'$1' via groupadd, exit-code: $?"
    elif command_is_exists addgroup; then
        addgroup "$1" || log_error "failed to create group:'$1' via addgroup, exit-code: $?"
    else
        log_error "group:'$1' not exists, and groupadd/addgroup are not found, please create it manually"
    fi
}

# group command
set_command_group() {
    command_required "$2"
    local group="$1" path="$(command_path "$2")"
    chgrp "$group" "$path" || log_error "chgrp failed: group=$group path=$path"
    chmod g+xs "$path"
}

# command
set_proxy_group() {
    set_command_group "$proxy_procgroup" "$1"
}

# command
set_dns_group() {
    set_command_group "$dns_procgroup" "$1"
}

process_is_running() {
    kill -0 "$1" &>/dev/null
}

# 优雅关闭进程
kill_by_pid() {
    [ $# -eq 0 ] && return

    # send SIGTERM
    for pid in "$@"; do
        process_is_running "$pid" && kill "$pid"
    done

    # wait up to 5s
    local running_pids
    for ((i = 0; i < 100; i++)); do
        running_pids=()
        for pid in "$@"; do
            process_is_running "$pid" && running_pids+=("$pid")
        done
        [ "${#running_pids[@]}" -eq 0 ] && return
        ((i < 5)) && sleep 0.01 || sleep 0.05
        ((i % 20 == 0 && i >= 30)) && echo "[notice] kill process: ${running_pids[@]}"
    done

    # force kill
    echo "[warning] force kill: ${running_pids[@]}"
    kill -9 "${running_pids[@]}"
}

# 优雅关闭进程
kill_by_name() {
    local pids=()
    for name in "$@"; do
        pids+=($(pidof "$name"))
    done
    kill_by_pid "${pids[@]}"
}

tcp_port_is_exists() {
    $netstat -lnpt | grep -q ":$1[[:blank:]]"
}

udp_port_is_exists() {
    $netstat -anpu | grep -q ":$1[[:blank:]]"
}

# 通过一些运行时特征(系统重启后不保留的状态)，判断是否处于start状态
ss_tproxy_is_started() {
    iptables  -t mangle -S SSTP_OUTPUT &>/dev/null ||
    iptables  -t nat    -S SSTP_OUTPUT &>/dev/null ||
    ip6tables -t mangle -S SSTP_OUTPUT &>/dev/null ||
    ip6tables -t nat    -S SSTP_OUTPUT &>/dev/null ||
    ip -4 rule 2>/dev/null | grep -q "lookup $ipts_rt_tab" ||
    ip -6 rule 2>/dev/null | grep -q "lookup $ipts_rt_tab" ||
    ip -4 route show table $ipts_rt_tab 2>/dev/null | grep -q '^' ||
    ip -6 route show table $ipts_rt_tab 2>/dev/null | grep -q '^'
}

is_ipv4_ipts() {
    [ "$1" = 'iptables' ]
}

is_ipv6_ipts() {
    [ "$1" = 'ip6tables' ]
}

# $ipts table chain
chain_is_exists() {
    local table="$2" chain="$3"
    $1 -t $table -S $chain &>/dev/null
}

# 先判断chain是否存在
# $ipts table chain
chain_is_empty() {
    local table="$2" chain="$3"
    [ $($1 -t $table -S $chain | wc -l) -le 1 ]
}

is_global_mode() {
    [ "$mode" = 'global' ]
}

is_gfwlist_mode() {
    [ "$mode" = 'gfwlist' ]
}

is_chnroute_mode() {
    [ "$mode" = 'chnroute' ]
}

# tcp也使用tproxy? (udp必须用tproxy)
is_tcp_tproxy() {
    is_true "$tproxy"
}

is_built_in_dns() {
    is_false "$dns_custom"
}

is_proxy_other() {
    is_false "$selfonly"
}

is_drop_quic() {
    case "$ipts_drop_quic" in
        tcponly)
            ! is_enabled_udp;;
        always)
            true;;
        *)
            false;;
    esac
}

is_enabled_ipv4() {
    is_true "$ipv4"
}

is_enabled_ipv6() {
    is_true "$ipv6"
}

is_enabled_udp() {
    is_false "$tcponly"
}

is_need_iproute() {
    is_tcp_tproxy || is_enabled_udp
}

# ip#port
get_ip_from_addr() {
    local addr="$1"
    echo "${addr%#*}"
}

# ip#port
get_port_from_addr() {
    local addr="$1"
    echo "${addr#*#}"
}

#===============================================================

# 加载上次start时的一些运行时状态，主要是pid
load_pidfile() {
    if ss_tproxy_is_started; then
        source .ss-tproxy.pid
    else
        # start状态下重启系统，pid文件会保留(未被清理)，这里删一下
        delete_pidfile
    fi
}

# start之后调用，保存start时的一些运行时状态
save_pidfile() {
    {
        if is_built_in_dns; then
            echo "sstp_pid_chinadns=$sstp_pid_chinadns"
        else
            call_func custom_dns_pid
        fi
        call_func extra_pid
    } >.ss-tproxy.pid
}

# stop时移除，或者重启系统后有残留，也移除
delete_pidfile() {
    rm -f .ss-tproxy.pid &>/dev/null
}

#===============================================================

load_config() {
    file_required $config_file

    source $config_file "${arg_list[@]}" ||
        log_error "$config_file load failed, exit-code: $?"

    # 覆盖同名配置，也可以用来定义新变量
    for override in "${override_list[@]}"; do
        eval "$override"
    done

    { ! is_enabled_ipv4 && ! is_enabled_ipv6; } &&
        log_error "both ipv4 and ipv6 are disabled, nothing to do"

    if is_global_mode; then
        # 白名单
        file_required ignlist.ext
    elif is_gfwlist_mode; then
        # 黑名单
        file_required gfwlist.txt
        file_required gfwlist.ext
    elif is_chnroute_mode; then
        # 白名单 + 黑名单(优先)
        file_required ignlist.ext
        file_required chnlist.txt
        file_required chnroute.txt
        file_required chnroute6.txt
        file_required gfwlist.txt
        file_required gfwlist.ext
    else
        log_error "invalid config value for mode: '$mode'"
    fi

    [ "$proxy_procgroup" -a "$proxy_procgroup" != 0 -a "$proxy_procgroup" != root ] ||
        log_error "invalid config value for proxy_procgroup: '$proxy_procgroup'"

    [ "$dns_procgroup" -a "$dns_procgroup" != 0 -a "$dns_procgroup" != root ] ||
        log_error "invalid config value for dns_procgroup: '$dns_procgroup'"

    [ "$proxy_procgroup" != "$dns_procgroup" ] ||
        log_error "proxy_procgroup and dns_procgroup must be different: '$proxy_procgroup'"

    group_is_exists "$proxy_procgroup" || create_group "$proxy_procgroup"
    group_is_exists "$dns_procgroup"   || create_group "$dns_procgroup"

    is_need_iproute && command_required 'ip'

    command_required 'ipset'

    is_enabled_ipv4 && command_required 'iptables'
    is_enabled_ipv6 && command_required 'ip6tables'

    if is_built_in_dns; then
        set_dns_group 'chinadns-ng'
    else
        call_func custom_dns_init
    fi

    case "$opts_ss_netstat" in
        auto)
            if command_is_exists 'ss'; then
                netstat='ss'
            elif command_is_exists 'netstat'; then
                netstat='netstat'
            else
                log_error "command not found: ss/netstat"
            fi
            ;;
        ss)
            command_required 'ss'
            netstat='ss'
            ;;
        netstat)
            command_required 'netstat'
            netstat='netstat'
            ;;
        *)
            log_error "invalid config value for opts_ss_netstat: '$opts_ss_netstat'"
            ;;
    esac
}

#===============================================================

# url filename
update_domain_list() {
    command_required 'curl'

    local url="$1" filename="$2"

    local data # 声明和赋值必须分开，不然错误码会丢失
    data="$(set -o pipefail; curl -4fsSkL "$url" | grep -v -e '^[[:space:]]*$' -e '^[[:space:]]*#')" ||
        log_error "download failed: $url (exit-code: $?)"

    if head -n1 <<<"$data" | grep -q /; then
        # server=/域名后缀/dns_ip
        echo "$data" | awk -F/ '{print $2}' | sort | uniq >$filename
    else
        # 纯域名后缀
        echo "$data" | sort | uniq >$filename
    fi
}

update_gfwlist() {
    update_domain_list "$url_gfwlist" gfwlist.txt
}

update_chnlist() {
    update_domain_list "$url_chnlist" chnlist.txt
}

update_chnroute() {
    command_required 'curl'

    local data # 声明和赋值必须分开，不然错误码会丢失
    data="$(set -o pipefail; curl -4fsSkL "$url_chnroute" | grep CN)" ||
        log_error "download failed: $url_chnroute (exit-code: $?)"

    awk -F'|' '/ipv4/ {printf("%s/%d\n", $4, 32-log($5)/log(2))}' <<<"$data" >chnroute.txt
    awk -F'|' '/ipv6/ {printf("%s/%d\n", $4, $5)}' <<<"$data" >chnroute6.txt
}

#===============================================================

# direct/remote "server1 server2 ..."
get_chinadns_upstream() {
    local opt use_tcp_dns

    if [ "$1" = direct ]; then
        opt='-c'
        use_tcp_dns=0
    else
        opt='-t'
        case "$dns_remote_tcp" in
            tcponly) ! is_enabled_udp && use_tcp_dns=1 || use_tcp_dns=0;;
            always) use_tcp_dns=1;;
            *) use_tcp_dns=0;;
        esac
    fi

    for upstream in $2; do
        if ((use_tcp_dns)) && ! str_find "$upstream" "://"; then
            upstream="tcp://$upstream"
        fi
        echo " $opt $upstream"
    done
}

start_chinadns() {
    local args=""

    if is_enabled_ipv6; then   
        args+=" -b ::"
    else
        args+=" -b 0.0.0.0"
    fi

    if [ "$chinadns_bind_port" ]; then
        args+=" -l $chinadns_bind_port"
    else
        args+=" -l $dns_mainport"
    fi

    is_enabled_ipv4 && args+="$(get_chinadns_upstream direct "$dns_direct")"
    is_enabled_ipv6 && args+="$(get_chinadns_upstream direct "$dns_direct6")"

    is_enabled_ipv4 && args+="$(get_chinadns_upstream remote "$dns_remote")"
    is_enabled_ipv6 && args+="$(get_chinadns_upstream remote "$dns_remote6")"

    args+=" --cache $chinadns_cache_size"
    args+=" --cache-stale $chinadns_cache_stale"
    args+=" --cache-refresh $chinadns_cache_refresh"
    args+=" --verdict-cache $chinadns_verdict_cache"

    [ "$chinadns_cache_db" ] && args+=" --cache-db $chinadns_cache_db"
    [ "$chinadns_verdict_db" ] && args+=" --verdict-cache-db $chinadns_verdict_db"

    for config in $chinadns_config_files; do
        args+=" -C $config"
    done

    [ "$chinadns_extra_options" ] && args+=" $chinadns_extra_options"

    is_true "$chinadns_verbose" && args+=' -v'

    if is_global_mode; then
        sstp_pid_chinadns=$(
            trap "" CHLD # 避免bash变为僵尸进程
            chinadns-ng \
            $args \
            -m <(list_ext_domain ignlist.ext) \
            -d gfw \
            -a sstp_white,sstp_white6 \
            </dev/null &>>$chinadns_logfile &
            echo $!
        )
    elif is_gfwlist_mode; then
        sstp_pid_chinadns=$(
            trap "" CHLD # 避免bash变为僵尸进程
            chinadns-ng \
            $args \
            -g gfwlist.txt,<(list_ext_domain gfwlist.ext) \
            -d chn \
            -A sstp_black,sstp_black6 \
            </dev/null &>>$chinadns_logfile &
            echo $!
        )
    else # chnroute
        sstp_pid_chinadns=$(
            trap "" CHLD # 避免bash变为僵尸进程
            chinadns-ng \
            $args \
            -m chnlist.txt,<(list_ext_domain ignlist.ext) \
            -g gfwlist.txt,<(list_ext_domain gfwlist.ext) \
            $(is_true "$chinadns_chnlist_first" && echo '-M') \
            -a sstp_white,sstp_white6 \
            -A sstp_black,sstp_black6 \
            -4 sstp_white -6 sstp_white6 \
            </dev/null &>>$chinadns_logfile &
            echo $!
        )
    fi
}

# dns: 删除dns-cache
# verdict: 删除verdict-cache
# 无参数: 删除dns-cache和verdict-cache
del_chinadns_cache() {
    if [ -z "$1" ] || [ "$1" = dns ]; then
        file_is_exists "$chinadns_cache_db" && rm "$chinadns_cache_db"
    fi
    if [ -z "$1" ] || [ "$1" = verdict ]; then
        file_is_exists "$chinadns_verdict_db" && rm "$chinadns_verdict_db"
    fi
}

#===============================================================

start_dnsserver() {
    if is_built_in_dns; then
        start_chinadns
    else
        call_func custom_dns_start
    fi
}

stop_dnsserver() {
    # 为防止修改配置导致进程残留，此处不做判断

    kill_by_pid $sstp_pid_chinadns

    call_func custom_dns_stop
}

restart_dnsserver() {
    ss_tproxy_is_started || return 1
    stop_dnsserver
    start_dnsserver
    save_pidfile
    return 0
}

flush_dnscache() {
    # ss_tproxy_is_started || return

    if is_built_in_dns; then
        if ss_tproxy_is_started; then
            kill_by_pid $sstp_pid_chinadns
            del_chinadns_cache "$1"
            start_chinadns
            save_pidfile
        else
            del_chinadns_cache "$1"
        fi
    else
        call_func custom_dns_flush "$1"
    fi
}

#===============================================================

# 4/6 key=val
sysctl_all_iface() {
    for path in /proc/sys/net/ipv$1/conf/*; do
        sysctl -wq ${path#/proc/sys/}/$2
    done
}

set_kernel_param() {
    # 允许ip转发，充当网关
    is_enabled_ipv4 && sysctl -wq net.ipv4.ip_forward=1
    is_enabled_ipv6 && sysctl_all_iface 6 forwarding=1

    # 允许路由loopback地址，如果不开启，外部数据包将无法dnat至127.0.0.1
    sysctl_all_iface 4 route_localnet=1

    # 禁止发送icmp重定向包，防止内网机在ping的时候收到重定向消息("旁路由"拓扑)
    sysctl_all_iface 4 send_redirects=0
}

#===============================================================

# 失败也不退出，避免逻辑执行不完整
start_proxyproc() {
    eval "$proxy_startcmd"
}

stop_proxyproc() {
    eval "$proxy_stopcmd"
}

restart_proxyproc() {
    ss_tproxy_is_started || return 1
    stop_proxyproc
    start_proxyproc
    return 0
}

#===============================================================

# setname <ip_list
init_ipset() {
    ipset create $1 hash:net family $(str_find $1 6 && echo inet6 || echo inet)
    sed "s/^/add $1 /" | ipset -! restore
}

# [proto://][host@]ip[#port][path]
get_upstream_ip() {
    local upstream="$1"
    upstream="${upstream##*://}"
    upstream="${upstream##*@}"
    upstream="${upstream%%/*}"
    upstream="${upstream%%#*}"
    echo "$upstream"
}

# prefix "true/false/ip..." "upstreams..."
get_ext_ip() {
    case "$2" in
        true) for u in $3; do echo "$1$(get_upstream_ip "$u")"; done;;
        false) ;;
        *) for ip in $2; do echo "$1$ip"; done;;
    esac
}

get_ext_whiteip() {
    if is_built_in_dns; then
        get_ext_ip '-' "$dns_direct_white" "$dns_direct"
        get_ext_ip '~' "$dns_direct6_white" "$dns_direct6"
    else
        call_func custom_dns_whiteip
    fi
}

get_ext_blackip() {
    if is_built_in_dns; then
        get_ext_ip '-' "$dns_remote_black" "$dns_remote"
        get_ext_ip '~' "$dns_remote6_black" "$dns_remote6"
    else
        call_func custom_dns_blackip
    fi
}

# dns和iptables都需要ipset，为防止dns组件报错(A/AAAA)，v4和v6集合将始终创建
start_ipset() {
    if is_global_mode; then
        { list_ext_ipv4 ignlist.ext; get_ext_whiteip | list_ext_ipv4 -; } | init_ipset sstp_white
        { list_ext_ipv6 ignlist.ext; get_ext_whiteip | list_ext_ipv6 -; } | init_ipset sstp_white6
    elif is_gfwlist_mode; then
        { list_ext_ipv4 gfwlist.ext; get_ext_blackip | list_ext_ipv4 -; } | init_ipset sstp_black
        { list_ext_ipv6 gfwlist.ext; get_ext_blackip | list_ext_ipv6 -; } | init_ipset sstp_black6
    elif is_chnroute_mode; then
        { list_ext_ipv4 ignlist.ext; get_ext_whiteip | list_ext_ipv4 -; cat chnroute.txt;  } | init_ipset sstp_white
        { list_ext_ipv6 ignlist.ext; get_ext_whiteip | list_ext_ipv6 -; cat chnroute6.txt; } | init_ipset sstp_white6
        { list_ext_ipv4 gfwlist.ext; get_ext_blackip | list_ext_ipv4 -; } | init_ipset sstp_black
        { list_ext_ipv6 gfwlist.ext; get_ext_blackip | list_ext_ipv6 -; } | init_ipset sstp_black6
    fi
}

# stop就清空，如果只是换节点或重启代理，不要通过ss-tproxy进行，请直接操作代理进程
flush_ipset() {
    for setname in $(ipset -n list | grep '^sstp_'); do
        # https://github.com/zfl9/ss-tproxy/issues/234
        while ! ipset destroy $setname &>/dev/null; do
            sleep 0.02
        done
    done
}

#===============================================================

_start_iproute() {
    local family="$1"

    # 将数据包路由至本地
    # 内网过来的包: prerouting -> routing -> input(lo)
    # 本机发出的包: output -> routing -> postrouting(lo) -> prerouting(lo) -> routing -> input(lo)
    ip $family route add local default dev $ipts_if_lo table $ipts_rt_tab

    # https://github.com/zfl9/ss-tproxy/pull/186
    # https://man7.org/linux/man-pages/man8/ip-rule.8.html
    # https://man7.org/linux/man-pages/man7/rtnetlink.7.html
    # https://stackoverflow.com/questions/10259266/what-does-proto-kernel-means-in-unix-routing-table
    if ip rule help 2>&1 | grep -Fwq protocol; then
        ip $family rule add fwmark $ipts_rt_mark table $ipts_rt_tab protocol static
    else
        ip $family rule add fwmark $ipts_rt_mark table $ipts_rt_tab
    fi
}

start_iproute() {
    ! is_need_iproute && return

    is_enabled_ipv4 && _start_iproute -4
    is_enabled_ipv6 && _start_iproute -6
}

_flush_iproute() {
    while ip $1 rule del table $ipts_rt_tab &>/dev/null; do true; done
    ip $1 route flush table $ipts_rt_tab &>/dev/null
}

flush_iproute() {
    # 不做判断，防止相关配置改动导致残留
    _flush_iproute -4
    _flush_iproute -6
}

#===============================================================

start_iptables_pre() {
    $1 -t mangle -N SSTP_PREROUTING
    $1 -t mangle -N SSTP_OUTPUT

    $1 -t nat    -N SSTP_PREROUTING
    $1 -t nat    -N SSTP_OUTPUT
    $1 -t nat    -N SSTP_POSTROUTING
}

start_iptables_post() {
    $1 -t mangle -A PREROUTING  -j SSTP_PREROUTING
    $1 -t mangle -A OUTPUT      -j SSTP_OUTPUT

    $1 -t nat    -A PREROUTING  -j SSTP_PREROUTING
    $1 -t nat    -A OUTPUT      -j SSTP_OUTPUT
    $1 -t nat    -A POSTROUTING -j SSTP_POSTROUTING
}

#===============================================================

# local loopback_addr loopback_addrx white_setname black_setname
init_iptables_param() {
    if is_ipv4_ipts $1; then
        loopback_addr="127.0.0.1"
        loopback_addrx="127.0.0.1"
        white_setname="sstp_white"
        black_setname="sstp_black"
    else
        loopback_addr="::1"
        loopback_addrx="[::1]"
        white_setname="sstp_white6"
        black_setname="sstp_black6"
    fi
}

get_dst_port_match() {
    [ "$ipts_proxy_dst_port" ] &&
        echo "-m multiport --dports $ipts_proxy_dst_port"
}

#===============================================================

# $ipts tproxy/dnat
# tproxy: mangle表 (tcp,udp)
# dnat:   nat表    (tcp)
# 进入SSTP_RULE的必须是"新连接的首包"
create_sstp_rule() {
    local table action

    if [ "$2" = 'tproxy' ]; then
        table=mangle
        action="-j CONNMARK --set-mark $ipts_rt_mark" # 用来保存ipset判定结果(每个"连接"只判定一次)
    else
        table=nat
        action="-p tcp -j DNAT --to-destination $loopback_addrx:$proxy_tcpport" # nat的操作单位是"连接"
    fi

    $1 -t $table -N SSTP_RULE

    if is_global_mode; then
        $1 -t $table -A SSTP_RULE \
            -m set ! --match-set $white_setname dst \
            $action
    elif is_gfwlist_mode; then
        $1 -t $table -A SSTP_RULE \
            -m set --match-set $black_setname dst \
            $action
    elif is_chnroute_mode; then
        # 放行白名单ip (若该ip同时也位于黑名单，则不放行)
        $1 -t $table -A SSTP_RULE \
            -m set --match-set $white_setname dst \
            -m set ! --match-set $black_setname dst \
            -j RETURN
        $1 -t $table -A SSTP_RULE \
            $action
    fi
}

# mangle表 OUTPUT/PREROUTING -p udp --dport 443
drop_quic() {
    $1 -t mangle -N SSTP_QUIC

    if is_global_mode; then
        $1 -t mangle -A SSTP_QUIC \
            -m set ! --match-set $white_setname dst \
            -j DROP
    elif is_gfwlist_mode; then
        $1 -t mangle -A SSTP_QUIC \
            -m set --match-set $black_setname dst \
            -j DROP
    elif is_chnroute_mode; then
        # 放行白名单ip (若该ip同时也位于黑名单，则不放行)
        $1 -t mangle -A SSTP_QUIC \
            -m set --match-set $white_setname dst \
            -m set ! --match-set $black_setname dst \
            -j RETURN
        $1 -t mangle -A SSTP_QUIC \
            -j DROP
    fi

    $1 -t mangle -A SSTP_OUTPUT \
        -p udp \
        -m udp --dport 443 \
        -m conntrack --ctdir ORIGINAL \
        -m addrtype ! --dst-type LOCAL \
        -m owner ! --gid-owner $proxy_procgroup \
        -j SSTP_QUIC

    is_proxy_other &&
        $1 -t mangle -A SSTP_PREROUTING \
            -p udp \
            -m udp --dport 443 \
            -m conntrack --ctdir ORIGINAL \
            -m addrtype ! --dst-type LOCAL \
            -j SSTP_QUIC
}

#===============================================================

# mangle表 OUTPUT/PREROUTING
do_proxy_tproxy() {
    local tcp=$(is_tcp_tproxy && echo 1 || echo 0)
    local udp=$(is_enabled_udp && echo 1 || echo 0)

    create_sstp_rule $1 tproxy

    # 放行发往本机的流量
    $1 -t mangle -A SSTP_OUTPUT \
        -m addrtype --dst-type LOCAL \
        -j RETURN

    # 放行reply流量，只处理original方向
    $1 -t mangle -A SSTP_OUTPUT \
        -m conntrack --ctdir REPLY \
        -j RETURN

    # 放行本机代理进程传出的流量
    $1 -t mangle -A SSTP_OUTPUT \
        -m owner --gid-owner $proxy_procgroup \
        -j RETURN

    # 放行本机发出的dns请求，留给nat去重定向
    ((tcp)) &&
        $1 -t mangle -A SSTP_OUTPUT \
            -p tcp \
            -m tcp --dport 53 \
            -m owner ! --gid-owner $dns_procgroup \
            -j RETURN
    ((udp)) &&
        $1 -t mangle -A SSTP_OUTPUT \
            -p udp \
            -m udp --dport 53 \
            -m owner ! --gid-owner $dns_procgroup \
            -j RETURN

    # 本机传出流量 => SSTP_RULE
    ((tcp)) &&
        $1 -t mangle -A SSTP_OUTPUT \
            -p tcp \
            -m tcp --syn \
            $(get_dst_port_match) \
            -j SSTP_RULE
    ((udp)) &&
        $1 -t mangle -A SSTP_OUTPUT \
            -p udp \
            -m conntrack --ctstate NEW,RELATED \
            $(get_dst_port_match) \
            -j SSTP_RULE

    # 打mark是为了将包路由到本地，进入prerouting
    $1 -t mangle -A SSTP_OUTPUT \
        -m connmark --mark $ipts_rt_mark \
        -j MARK --set-mark $ipts_rt_mark

    ###################### prerouting ######################

    # 放行发往本机的流量
    $1 -t mangle -A SSTP_PREROUTING \
        -m addrtype --dst-type LOCAL \
        -j RETURN

    # 放行reply流量，只处理original方向
    $1 -t mangle -A SSTP_PREROUTING \
        -m conntrack --ctdir REPLY \
        -j RETURN

    # 内网传出流量 => SSTP_RULE
    if is_proxy_other; then
        ((tcp)) &&
            $1 -t mangle -A SSTP_PREROUTING \
                -p tcp \
                -m tcp --syn ! --dport 53 \
                -m addrtype ! --src-type LOCAL \
                $(get_dst_port_match) \
                -j SSTP_RULE
        ((udp)) &&
            $1 -t mangle -A SSTP_PREROUTING \
                -p udp \
                -m udp ! --dport 53 \
                -m conntrack --ctstate NEW,RELATED \
                -m addrtype ! --src-type LOCAL \
                $(get_dst_port_match) \
                -j SSTP_RULE
    fi

    # 本机发出的/内网过来的skb => 代理进程
    ((tcp)) &&
        $1 -t mangle -A SSTP_PREROUTING \
            -p tcp \
            -m connmark --mark $ipts_rt_mark \
            -j TPROXY --on-ip $loopback_addr --on-port $proxy_tcpport --tproxy-mark $ipts_rt_mark
    ((udp)) &&
        $1 -t mangle -A SSTP_PREROUTING \
            -p udp \
            -m connmark --mark $ipts_rt_mark \
            -j TPROXY --on-ip $loopback_addr --on-port $proxy_udpport --tproxy-mark $ipts_rt_mark
}

# nat表 OUTPUT/PREROUTING -p tcp
do_proxy_dnat() {
    create_sstp_rule $1 dnat

    # 本机传出流量 => SSTP_RULE
    $1 -t nat -A SSTP_OUTPUT \
        -p tcp \
        -m tcp --syn \
        -m addrtype ! --dst-type LOCAL \
        $(get_dst_port_match) \
        -m owner ! --gid-owner $proxy_procgroup \
        -j SSTP_RULE

    # 内网传出流量 => SSTP_RULE
    is_proxy_other &&
        $1 -t nat -A SSTP_PREROUTING \
            -p tcp \
            -m tcp --syn \
            -m addrtype ! --src-type LOCAL ! --dst-type LOCAL \
            $(get_dst_port_match) \
            -j SSTP_RULE
}

#===============================================================

# nat表 OUTPUT/PREROUTING -p tcp,udp
redir_dns_request() {
    # 本机发出的dns请求 (dnat)
    $1 -t nat -A SSTP_OUTPUT \
        -p tcp \
        -m tcp --dport 53 --syn \
        -m owner ! --gid-owner $proxy_procgroup \
        -m owner ! --gid-owner $dns_procgroup \
        -j REDIRECT --to-ports $dns_mainport
    $1 -t nat -A SSTP_OUTPUT \
        -p udp \
        -m udp --dport 53 \
        -m conntrack --ctstate NEW \
        -m owner ! --gid-owner $proxy_procgroup \
        -m owner ! --gid-owner $dns_procgroup \
        -j REDIRECT --to-ports $dns_mainport

    # 本机发出的dns请求 (snat)
    $1 -t nat -A SSTP_POSTROUTING \
        -d $loopback_addr \
        ! -s $loopback_addr \
        -j SNAT --to-source $loopback_addr

    # 其他主机发来的dns请求
    is_proxy_other &&
        $1 -t nat -A SSTP_PREROUTING \
            -p tcp \
            -m tcp --dport 53 --syn \
            -m addrtype ! --src-type LOCAL \
            -j REDIRECT --to-ports $dns_mainport
    is_proxy_other &&
        $1 -t nat -A SSTP_PREROUTING \
            -p udp \
            -m udp --dport 53 \
            -m conntrack --ctstate NEW \
            -m addrtype ! --src-type LOCAL \
            -j REDIRECT --to-ports $dns_mainport
}

#===============================================================

start_iptables_tproxy() {
    # 处理dns(tcp+udp) nat表
    redir_dns_request $1

    # 处理tcp/udp mangle表
    do_proxy_tproxy $1
}

start_iptables_redirect() {
    # 处理dns(tcp+udp) nat表
    redir_dns_request $1

    # 处理tcp nat表
    do_proxy_dnat $1

    # 处理udp mangle表
    if is_enabled_udp; then
        do_proxy_tproxy $1
    fi
}

_start_iptables() {
    start_iptables_pre $1

    local loopback_addr loopback_addrx white_setname black_setname
    init_iptables_param $1

    if is_drop_quic; then
        drop_quic $1
    fi

    if is_tcp_tproxy; then
        start_iptables_tproxy $1
    else
        start_iptables_redirect $1
    fi

    if is_proxy_other; then
        add_snat_rule $1
    fi

    start_iptables_post $1
}

start_iptables() {
    is_enabled_ipv4 && _start_iptables "iptables"
    is_enabled_ipv6 && _start_iptables "ip6tables"
}

#===============================================================

_delete_unused_chain() {
    local list=(
        mangle PREROUTING
        mangle OUTPUT
        nat PREROUTING
        nat OUTPUT
        nat POSTROUTING
    )
    for ((i = 0; i < ${#list[@]}; i += 2)); do
        local table="${list[i]}" chain="${list[i+1]}"
        if chain_is_empty $1 $table SSTP_$chain; then
            $1 -t $table -D $chain -j SSTP_$chain
            $1 -t $table -X SSTP_$chain
        fi
    done
}

delete_unused_chain() {
    is_enabled_ipv4 && _delete_unused_chain "iptables"
    is_enabled_ipv6 && _delete_unused_chain "ip6tables"
}

#===============================================================

_flush_iptables() {
    $1 -t mangle -D PREROUTING  -j SSTP_PREROUTING  &>/dev/null
    $1 -t mangle -D OUTPUT      -j SSTP_OUTPUT      &>/dev/null

    $1 -t nat    -D PREROUTING  -j SSTP_PREROUTING  &>/dev/null
    $1 -t nat    -D OUTPUT      -j SSTP_OUTPUT      &>/dev/null
    $1 -t nat    -D POSTROUTING -j SSTP_POSTROUTING &>/dev/null

    for table in mangle nat; do
        local chain_list=$($1 -t $table -S 2>/dev/null | grep '^-N SSTP_' | awk '{print $2}')
        for chain in $chain_list; do
            $1 -t $table -F $chain
        done
        for chain in $chain_list; do
            $1 -t $table -X $chain
        done
    done
}

flush_iptables() {
    # 不做判断，防止相关配置改动导致残留
    _flush_iptables "iptables"
    _flush_iptables "ip6tables"
}

#===============================================================

_show_iptables() {
    echo "$(color_green "==> $1-mangle <==")"
    $1 -t mangle -S
    echo
    echo "$(color_green "==> $1-nat <==")"
    $1 -t nat -S
}

show_iptables() {
    is_enabled_ipv4 && _show_iptables "iptables"
    { is_enabled_ipv4 && is_enabled_ipv6; } && echo
    is_enabled_ipv6 && _show_iptables "ip6tables"
}

#===============================================================

# stop后，dns进程未运行，为保证内网机器正常上网，需要重定向dns请求至直连dns
add_reddns_rule() {
    local direct_dns_ip direct_dns_ipx direct_dns_port

    if is_ipv4_ipts $1; then
        [ -z "$ipts_reddns_onstop" ] && return
        direct_dns_ip="$(get_ip_from_addr "$ipts_reddns_onstop")"
        direct_dns_ipx="$direct_dns_ip"
        direct_dns_port="$(get_port_from_addr "$ipts_reddns_onstop")"
    else
        [ -z "$ipts_reddns6_onstop" ] && return
        direct_dns_ip="$(get_ip_from_addr "$ipts_reddns6_onstop")"
        direct_dns_ipx="[$direct_dns_ip]"
        direct_dns_port="$(get_port_from_addr "$ipts_reddns6_onstop")"
    fi

    $1 -t nat -N SSTP_PREROUTING  &>/dev/null
    $1 -t nat -N SSTP_POSTROUTING &>/dev/null

    $1 -t nat -A SSTP_PREROUTING \
        -p tcp \
        -m tcp --dport 53 \
        -m addrtype ! --src-type LOCAL --dst-type LOCAL \
        -j DNAT --to-destination $direct_dns_ipx:$direct_dns_port
    $1 -t nat -A SSTP_PREROUTING \
        -p udp \
        -m udp --dport 53 \
        -m addrtype ! --src-type LOCAL --dst-type LOCAL \
        -j DNAT --to-destination $direct_dns_ipx:$direct_dns_port

    $1 -t nat -A SSTP_POSTROUTING \
        -d $direct_dns_ip \
        -p tcp \
        -m tcp --dport $direct_dns_port \
        -m addrtype ! --src-type LOCAL \
        -j MASQUERADE
    $1 -t nat -A SSTP_POSTROUTING \
        -d $direct_dns_ip \
        -p udp \
        -m udp --dport $direct_dns_port \
        -m addrtype ! --src-type LOCAL \
        -j MASQUERADE
}

# start和stop后都可能需要，取决于ss-tproxy.conf配置
add_snat_rule() {
    if is_ipv4_ipts $1; then
        is_false "$ipts_set_snat" && return
    else
        is_false "$ipts_set_snat6" && return
    fi

    $1 -t nat -N SSTP_POSTROUTING &>/dev/null

    # 排除dmz/端口映射(外网->内网)，只匹配"内网->外网"连接
    $1 -t nat -A SSTP_POSTROUTING \
        -m owner ! --socket-exists \
        -m conntrack --ctstate NEW,RELATED --ctdir ORIGINAL \
        -m conntrack ! --ctstate SNAT,DNAT \
        -j MASQUERADE
}

# $ipts table 预定义链
add_sstp_chain() {
    local table="$2" chain="$3"
    if chain_is_exists $1 $table SSTP_$chain; then
        $1 -t $table -A $chain -j SSTP_$chain
    fi
}

_add_stoprule() {
    add_reddns_rule $1
    add_snat_rule $1

    add_sstp_chain $1 nat PREROUTING
    add_sstp_chain $1 nat POSTROUTING
}

# 内网dns重定向、设置snat规则
add_stoprule() {
    ! is_proxy_other && return

    is_enabled_ipv4 && _add_stoprule "iptables"
    is_enabled_ipv6 && _add_stoprule "ip6tables"
}

flush_stoprule() {
    ss_tproxy_is_started && return

    flush_iptables
}

check_resolv_conf() {
    ss_tproxy_is_started || return

    local ipv4_dns="$(awk 'BEGIN {ORS=" "} $1 == "nameserver" && $2 ~ /\./ {print "`" $2 "`"}' /etc/resolv.conf)"
    local ipv6_dns="$(awk 'BEGIN {ORS=" "} $1 == "nameserver" && $2 ~ /:/ {print "`" $2 "`"}' /etc/resolv.conf)"

    if ! is_enabled_ipv4 && [ "$ipv4_dns" ]; then
        echo "$(color_red "[warning]") found ipv4-dns ${ipv4_dns}but ipv4='false'"
        echo "$(color_red "[warning]") please consider removing it from /etc/resolv.conf"
    elif ! is_enabled_ipv6 && [ "$ipv6_dns" ]; then
        echo "$(color_red "[warning]") found ipv6-dns ${ipv6_dns}but ipv6='false'"
        echo "$(color_red "[warning]") please consider removing it from /etc/resolv.conf"
    fi
}

#===============================================================

start() {
    ss_tproxy_is_started && { stop; status; echo; }

    # 清空并删除sstp_*规则，如stoprule
    flush_iptables

    call_func pre_start

    set_kernel_param
    start_ipset # 先创建ipset，因为dns进程需要
    start_proxyproc
    start_dnsserver
    start_iproute
    start_iptables

    call_func post_start

    # post_start后再保存pid等运行时状态到文件
    save_pidfile

    # post_start可能会添加规则到sstp_*，所以放到后面
    delete_unused_chain
}

stop() {
    call_func pre_stop

    # 与start顺序相反
    delete_pidfile
    flush_iptables
    flush_iproute
    stop_dnsserver
    stop_proxyproc
    flush_ipset

    # 内网dns重定向、设置snat规则
    add_stoprule

    call_func post_stop
}

# name func args...
_status() {
    local name="$1" func="$2"
    shift 2
    if $func "$@"; then
        echo -e "$name:\t$(color_green '[running]')"
    else
        echo -e "$name:\t$(color_red '[stopped]')"
    fi
}

status() {
    echo -e "mode:\t\t$(font_bold $mode)"

    _status "proxy/tcp" tcp_port_is_exists $proxy_tcpport
    is_enabled_udp && _status "proxy/udp" udp_port_is_exists $proxy_udpport

    if is_built_in_dns; then
        _status "chinadns" process_is_running $sstp_pid_chinadns
    else
        call_func custom_dns_status
    fi

    call_func extra_status

    check_resolv_conf
}

#===============================================================

version() {
    echo "ss-tproxy v4.8.3 (2024-07-24)"
}

help() {
    cat <<EOF
Usage: ss-tproxy <COMMAND> ... [-x] [-d basedir] [-c config] [name=value ...]
COMMAND := {
    start               start ss-tproxy
    stop                stop ss-tproxy
    restart             restart ss-tproxy
    restart-proxy       restart proxy program
    restart-dns         restart dns program
    status              status of ss-tproxy
    show-iptables       show iptables rules
    flush-stoprule      flush stop rules
    flush-dnscache      flush dns cache
    update-gfwlist      update gfwlist.txt
    update-chnlist      update chnlist.txt
    update-chnroute     update chnroute.txt
    set-proxy-group     set group for executable
    set-dns-group       set group for executable
    version             show version and exit
    help                show help and exit
}
Specify the -x option to debug script
Specify the -d option to use given base dir
Specify the -c option to use given ss-tproxy.conf
Specify the name=value to override ss-tproxy config
Issues or bug report: https://github.com/zfl9/ss-tproxy
See https://github.com/zfl9/ss-tproxy/wiki for more details
EOF
}

#===============================================================

main() {
    local argv=("$@")
    local arg_list=() #所有位置参数 会传给ss-tproxy.conf
    local override_list=() #覆盖ss-tproxy.conf中的同名配置

    for ((i = 0; i < ${#argv[@]}; ++i)); do
        local arg="${argv[i]}"
        case "$arg" in
            -x) set -x;;
            -d) base_dir="${argv[++i]}";;
            -c) config_file="${argv[++i]}";;
            -*) echo "$(color_yellow "Unknown option: $arg")"; help; return 1;;
            *=*) override_list+=("$arg");;
            *) arg_list+=("$arg");;
        esac
    done

    if [ "${#arg_list[@]}" -eq 0 ]; then
        help
        return 0
    fi

    [ "$base_dir" ] ||
        log_error "-d is specified, but no argument is given"

    [ "$config_file" ] ||
        log_error "-c is specified, but no argument is given"

    cd -- "$base_dir" ||
        log_error "base directory not exists: $base_dir"

    # 先加载ss-tproxy.conf
    load_config

    # 加载上次start时记录的状态，如dns进程pid
    load_pidfile

    case "${arg_list[0]}" in
        start)           start; status;;
        stop)            stop; status;;
        restart-p*)      restart_proxyproc && status;;
        restart-d*)      restart_dnsserver && status;;
        r*)              stop; status; echo; start; status;;
        stat*)           status;;
        show*)           show_iptables;;
        flush-stop*)     flush_stoprule;;
        flush-dns*)      flush_dnscache "${arg_list[1]}";;
        update-gfwlist)  update_gfwlist;;
        update-chnlist)  update_chnlist;;
        update-chnroute) update_chnroute;;
        set-proxy-group) set_proxy_group "${arg_list[1]}";;
        set-dns-group)   set_dns_group "${arg_list[1]}";;
        v*)              version;;
        h*)              help;;
        *)               echo "$(color_yellow "Unknown command: ${arg_list[0]}")"; help; return 1;;
    esac

    return 0
}

main "$@"
