From 3f383af8d4b29f597aa520eca6a58c90fdd3e45a Mon Sep 17 00:00:00 2001 From: svefnz Date: Wed, 8 Jul 2026 01:53:59 +0800 Subject: [PATCH] feat: automate HAProxy L4 SNI routing for Caddy and VLESS-REALITY co-existence on port 443 --- src/core.sh | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/src/core.sh b/src/core.sh index 8e5fa9a..b15c243 100644 --- a/src/core.sh +++ b/src/core.sh @@ -171,6 +171,116 @@ update_caddyfile_fallback() { fi } +install_haproxy() { + [[ $(type -P haproxy) ]] && return + msg "正在安装 HAProxy 用于四层端口分流..." + if [[ $(type -P apt-get) ]]; then + apt-get update && apt-get install -y haproxy + elif [[ $(type -P dnf) ]]; then + dnf install -y haproxy + elif [[ $(type -P yum) ]]; then + yum install -y haproxy + else + err "未知的软件包管理器,请手动安装 haproxy!" + fi +} + +restore_caddy_default() { + [[ ! -f $is_caddyfile ]] && return + if [[ -f ${is_caddyfile}.bak ]]; then + mv -f ${is_caddyfile}.bak $is_caddyfile + else + sed -i 's/servers :4443/servers :443/g' $is_caddyfile + sed -i 's/:4443[[:space:]]*{/:443 {/g' $is_caddyfile + sed -i 's/https_port 4443/https_port 443/g' $is_caddyfile + fi + systemctl restart caddy >/dev/null 2>&1 +} + +update_haproxy_config() { + [[ ! -d $is_conf_dir ]] && return + + local haproxy_conf="/etc/haproxy/haproxy.cfg" + [[ ! -d /etc/haproxy ]] && mkdir -p /etc/haproxy + + [[ -f $haproxy_conf ]] && cp -f $haproxy_conf ${haproxy_conf}.bak + + cat >$haproxy_conf <<'EOF' +global + log /dev/log local0 + log /dev/log local1 notice + chroot /var/lib/haproxy + user haproxy + group haproxy + daemon + +defaults + log global + mode tcp + timeout connect 5s + timeout client 50s + timeout server 50s + +frontend https-in + bind *:443 + mode tcp + tcp-request inspect-delay 5s + tcp-request content accept if { req_ssl_hello_type 1 } +EOF + + local acl_rules="" + local use_backend_rules="" + local backends="" + local has_reality=0 + + for config in $is_conf_dir/*.json; do + [[ ! -f $config ]] && continue + local json_clean=$(cat "$config" | sed s#//.*##) + + local is_real=$(jq -r '.inbounds[0].tls.reality.enabled // empty' <<<"$json_clean") + [[ $is_real != "true" ]] && continue + + local local_port=$(jq -r '.inbounds[0].listen_port // empty' <<<"$json_clean") + local sni=$(jq -r '.inbounds[0].tls.server_name // empty' <<<"$json_clean") + + [[ ! $local_port || ! $sni ]] && continue + [[ $local_port == "443" ]] && continue + + has_reality=1 + local backend_name="singbox_backend_${local_port}" + + acl_rules="${acl_rules}\n acl is_reality_${local_port} req.ssl_sni -i ${sni}" + use_backend_rules="${use_backend_rules}\n use_backend ${backend_name} if is_reality_${local_port}" + + backends="${backends}\n\nbackend ${backend_name}\n mode tcp\n server srv1 127.0.0.1:${local_port}" + done + + if [[ $has_reality -eq 1 ]]; then + echo -e "$acl_rules" >>$haproxy_conf + echo -e "$use_backend_rules" >>$haproxy_conf + echo -e " default_backend caddy_backend" >>$haproxy_conf + + cat >>$haproxy_conf <<'EOF' + +backend caddy_backend + mode tcp + server caddy 127.0.0.1:4443 +EOF + echo -e "$backends" >>$haproxy_conf + + systemctl daemon-reload + systemctl enable haproxy >/dev/null 2>&1 + systemctl restart haproxy >/dev/null 2>&1 + else + if systemctl is-active --quiet haproxy; then + systemctl stop haproxy >/dev/null 2>&1 + systemctl disable haproxy >/dev/null 2>&1 + fi + restore_caddy_default + fi +} + + get_pbk() { is_tmp_pbk=($($is_core_bin generate reality-keypair | sed 's/.*://')) is_public_key=${is_tmp_pbk[1]} @@ -369,7 +479,12 @@ create() { # get json [[ $is_change || ! $json_str ]] && get protocol $2 [[ $net == "reality" ]] && is_add_public_key=",outbounds:[{type:\"direct\"},{tag:\"public_key_$is_public_key\",type:\"direct\"}]" - is_new_json=$(jq "{inbounds:[{tag:\"$is_config_name\",type:\"$is_protocol\",$is_listen,listen_port:$port,$json_str}]$is_add_public_key}" <<<{}) + local write_port=$port + if [[ $is_reality_fallback ]]; then + get_port + write_port=$tmp_port + fi + is_new_json=$(jq "{inbounds:[{tag:\"$is_config_name\",type:\"$is_protocol\",$is_listen,listen_port:$write_port,$json_str}]$is_add_public_key}" <<<{}) [[ $is_test_json ]] && return # tmp test # only show json, dont save to file. [[ $is_gen ]] && { @@ -394,6 +509,8 @@ create() { msg "正在重启 Caddy 以释放 443 端口..." manage restart caddy &>/dev/null sleep 1 + install_haproxy + update_haproxy_config fi # caddy auto tls [[ $is_caddy && $host && ! $is_no_auto_tls ]] && { @@ -701,6 +818,7 @@ del() { pause fi rm -rf $is_conf_dir/"$is_config_file" + update_haproxy_config [[ ! $is_new_json ]] && manage restart & [[ ! $is_no_del_msg ]] && _green "\n已删除: $is_config_file\n" @@ -1174,11 +1292,19 @@ get() { [[ ${!v} == 'null' ]] && unset $v done + # Override port if it's mapped behind HAProxy + local file_port=$(echo "$is_config_file" | grep -oE '[0-9]+\.json' | grep -oE '[0-9]+') + if [[ $file_port && $port && $port != $file_port ]]; then + port=$file_port + fi + if [[ $is_private_key ]]; then is_reality=1 net_type+=reality is_public_key=${is_public_key/public_key_/} - [[ $is_reality_fallback_port == '4443' ]] && is_reality_fallback=1 + if [[ $is_reality_fallback_port == '4443' || $port == '443' ]]; then + is_reality_fallback=1 + fi fi is_socks_user=$username is_socks_pass=$password