v2.5.0: add shared 443 and per-user traffic

This commit is contained in:
Виталий Литвинов
2026-04-25 14:07:47 +03:00
parent c1b5ffc5a7
commit 63b564f70f
12 changed files with 990 additions and 34 deletions

248
lib/shared443.sh Normal file
View File

@@ -0,0 +1,248 @@
#!/bin/bash
# goTelegram Pro v2.5.0 — shared TCP/443 dispatcher helpers
SHARED443_CONFIG="${SHARED443_CONFIG:-/opt/gotelegram/shared-443.json}"
SHARED443_STREAM_CONF="${SHARED443_STREAM_CONF:-/etc/nginx/stream-conf.d/gotelegram-shared443.conf}"
SHARED443_TELEMT_PORT="${SHARED443_TELEMT_PORT:-7443}"
SHARED443_PUBLIC_PORT="${SHARED443_PUBLIC_PORT:-443}"
SHARED443_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
type log_error >/dev/null 2>&1 || source "$SHARED443_LIB_DIR/common.sh"
type install_nginx >/dev/null 2>&1 || source "$SHARED443_LIB_DIR/website.sh"
shared443_detect_nginx_stream() {
nginx -V 2>&1 | grep -Eq -- '--with-stream|ngx_stream_module|ngx_stream_ssl_preread_module'
}
shared443_install_stream_module() {
if shared443_detect_nginx_stream; then
return 0
fi
case "$(get_pkg_manager 2>/dev/null || echo unknown)" in
apt)
apt_update >/dev/null 2>&1 || true
apt_install libnginx-mod-stream || return 1
;;
dnf|yum)
install_pkg nginx-mod-stream || true
;;
esac
shared443_detect_nginx_stream
}
shared443_ensure_nginx_include() {
mkdir -p /etc/nginx/stream-conf.d
if nginx -T 2>/dev/null | grep -q '/etc/nginx/stream-conf.d/\*.conf'; then
return 0
fi
if grep -Eq '^[[:space:]]*stream[[:space:]]*\{' /etc/nginx/nginx.conf 2>/dev/null; then
log_warning "В nginx уже есть stream-блок, но нет include /etc/nginx/stream-conf.d/*.conf"
log_dim "Добавьте include вручную или перенесите $SHARED443_STREAM_CONF в существующий stream-блок."
return 1
fi
cp /etc/nginx/nginx.conf "/etc/nginx/nginx.conf.gotelegram.$(date +%Y%m%d_%H%M%S).bak" 2>/dev/null || true
cat >> /etc/nginx/nginx.conf <<'EOF'
# goTelegram Pro shared TCP/443 routes
stream {
include /etc/nginx/stream-conf.d/*.conf;
}
EOF
}
shared443_rewrite_telemt_bind() {
local listen_port="${1:-$SHARED443_TELEMT_PORT}"
local public_port="${2:-$SHARED443_PUBLIC_PORT}"
local listen_addr="${3:-127.0.0.1}"
command -v python3 >/dev/null 2>&1 || {
log_error "python3 нужен для безопасного изменения $TELEMT_CONFIG"
return 1
}
python3 - "$TELEMT_CONFIG" "$listen_port" "$public_port" "$listen_addr" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
listen_port = sys.argv[2]
public_port = sys.argv[3]
listen_addr = sys.argv[4]
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() if path.exists() else []
out = []
section = ""
server_seen = False
server_port_seen = False
server_addr_seen = False
links_seen = False
public_seen = False
def flush_section(next_line=None):
global section, server_port_seen, server_addr_seen, public_seen
if section == "server":
if not server_port_seen:
out.append(f"port = {listen_port}")
if not server_addr_seen:
out.append(f'listen_addr_ipv4 = "{listen_addr}"')
if section == "general.links" and not public_seen:
out.append(f"public_port = {public_port}")
if next_line is not None:
out.append(next_line)
for raw in lines:
stripped = raw.strip()
if stripped.startswith("[") and stripped.endswith("]"):
flush_section(raw)
section = stripped.strip("[]")
if section == "server":
server_seen = True
server_port_seen = False
server_addr_seen = False
elif section == "general.links":
links_seen = True
public_seen = False
continue
if section == "server" and stripped.startswith("port") and "=" in stripped:
out.append(f"port = {listen_port}")
server_port_seen = True
continue
if section == "server" and stripped.startswith("listen_addr_ipv4") and "=" in stripped:
out.append(f'listen_addr_ipv4 = "{listen_addr}"')
server_addr_seen = True
continue
if section == "general.links" and stripped.startswith("public_port") and "=" in stripped:
out.append(f"public_port = {public_port}")
public_seen = True
continue
out.append(raw)
flush_section()
if not links_seen:
if out and out[-1].strip():
out.append("")
out.extend(["[general.links]", f"public_port = {public_port}"])
if not server_seen:
if out and out[-1].strip():
out.append("")
out.extend(["[server]", f"port = {listen_port}", f'listen_addr_ipv4 = "{listen_addr}"'])
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
tmp.chmod(0o600)
tmp.replace(path)
PY
}
shared443_write_stream_config() {
local domain="$1"
local xray_domain="${2:-}"
local xray_target="${3:-}"
local telemt_target="${4:-127.0.0.1:${SHARED443_TELEMT_PORT}}"
mkdir -p "$(dirname "$SHARED443_STREAM_CONF")"
{
echo "# goTelegram Pro shared TCP/443 dispatcher"
echo "# Browser/Telegram for goTelegram domain goes to telemt; telemt masks the site to nginx."
echo "map \$ssl_preread_server_name \$gotelegram_shared443_backend {"
echo " hostnames;"
if [[ -n "$xray_domain" && -n "$xray_target" ]]; then
echo " ${xray_domain} ${xray_target};"
fi
echo " default ${telemt_target};"
echo "}"
echo ""
echo "server {"
echo " listen 0.0.0.0:${SHARED443_PUBLIC_PORT};"
echo " proxy_pass \$gotelegram_shared443_backend;"
echo " ssl_preread on;"
echo " proxy_connect_timeout 5s;"
echo " proxy_timeout 10m;"
echo "}"
} > "$SHARED443_STREAM_CONF"
mkdir -p "$(dirname "$SHARED443_CONFIG")"
if command -v jq >/dev/null 2>&1; then
jq -n \
--arg domain "$domain" \
--arg telemt "$telemt_target" \
--arg xdomain "$xray_domain" \
--arg xtarget "$xray_target" \
--arg updated "$(date -Iseconds)" \
--argjson public_port "$SHARED443_PUBLIC_PORT" \
'{
enabled: true,
dispatcher: "nginx-stream",
public_port: $public_port,
domain: $domain,
telemt_target: $telemt,
site_target: "127.0.0.1:8443",
xray_routes: (if ($xdomain != "" and $xtarget != "") then [{public: ($xdomain + ":443"), target: $xtarget}] else [] end),
updated_at: $updated
}' > "$SHARED443_CONFIG"
else
cat > "$SHARED443_CONFIG" <<EOF
{"enabled":true,"dispatcher":"nginx-stream","public_port":${SHARED443_PUBLIC_PORT},"domain":"${domain}","telemt_target":"${telemt_target}","site_target":"127.0.0.1:8443","xray_routes":[],"updated_at":"$(date -Iseconds)"}
EOF
fi
chmod 600 "$SHARED443_CONFIG" 2>/dev/null || true
}
shared443_enable() {
local domain="$1"
local xray_domain="${2:-}"
local xray_target="${3:-}"
local telemt_target="127.0.0.1:${SHARED443_TELEMT_PORT}"
[[ -n "$domain" ]] || domain="$(config_get domain 2>/dev/null || echo "")"
[[ -n "$domain" ]] || {
log_error "Не указан домен goTelegram Pro для shared-443"
return 1
}
install_nginx || return 1
shared443_install_stream_module || {
log_error "nginx stream/ssl_preread недоступен"
return 1
}
shared443_ensure_nginx_include || return 1
shared443_rewrite_telemt_bind "$SHARED443_TELEMT_PORT" "$SHARED443_PUBLIC_PORT" "127.0.0.1" || return 1
systemctl restart "$TELEMT_SERVICE" 2>/dev/null || true
shared443_write_stream_config "$domain" "$xray_domain" "$xray_target" "$telemt_target"
if nginx -t 2>/dev/null; then
systemctl restart nginx
log_success "shared-443 включён: 0.0.0.0:${SHARED443_PUBLIC_PORT} -> nginx stream -> telemt ${telemt_target}"
if [[ -n "$xray_domain" && -n "$xray_target" ]]; then
log_success "Xray route: ${xray_domain}:443 -> ${xray_target}"
fi
else
log_error "nginx -t не прошёл после настройки shared-443"
nginx -t
return 1
fi
}
shared443_detect_direct_conflict() {
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiv '(nginx|telemt)' || true
}
shared443_status() {
echo "shared-443 config: $SHARED443_CONFIG"
[ -f "$SHARED443_CONFIG" ] && cat "$SHARED443_CONFIG" || echo "not enabled"
local conflict
conflict="$(shared443_detect_direct_conflict)"
if [[ -n "$conflict" ]]; then
echo ""
echo "direct 443 listeners that need migration behind dispatcher:"
echo "$conflict"
fi
}
export -f shared443_detect_nginx_stream shared443_install_stream_module shared443_ensure_nginx_include
export -f shared443_rewrite_telemt_bind shared443_write_stream_config shared443_enable
export -f shared443_detect_direct_conflict shared443_status