mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 13:26:02 +00:00
v2.5.0: add shared 443 and per-user traffic
This commit is contained in:
@@ -86,6 +86,12 @@ create_backup() {
|
||||
if [ -f "$GOTELEGRAM_DIR/stats_history.csv" ]; then
|
||||
cp "$GOTELEGRAM_DIR/stats_history.csv" "$tmp_dir/stats_history.csv" 2>/dev/null
|
||||
fi
|
||||
if [ -f "$GOTELEGRAM_DIR/user_stats_history.csv" ]; then
|
||||
cp "$GOTELEGRAM_DIR/user_stats_history.csv" "$tmp_dir/user_stats_history.csv" 2>/dev/null
|
||||
fi
|
||||
if [ -f "$GOTELEGRAM_DIR/shared-443.json" ]; then
|
||||
cp "$GOTELEGRAM_DIR/shared-443.json" "$tmp_dir/shared-443.json" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Метаданные
|
||||
local ip mode engine lang port domain
|
||||
@@ -100,7 +106,7 @@ create_backup() {
|
||||
|
||||
cat > "$tmp_dir/metadata.json" << EOMETA
|
||||
{
|
||||
"backup_version": "1.4",
|
||||
"backup_version": "1.5",
|
||||
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
||||
"created_at": "$(date -Iseconds)",
|
||||
"hostname": "$(hostname)",
|
||||
@@ -310,6 +316,13 @@ restore_backup() {
|
||||
cp "$backup_dir/stats_history.csv" "$GOTELEGRAM_DIR/stats_history.csv" 2>/dev/null
|
||||
log_success "История статистики восстановлена"
|
||||
fi
|
||||
if [ -f "$backup_dir/user_stats_history.csv" ]; then
|
||||
cp "$backup_dir/user_stats_history.csv" "$GOTELEGRAM_DIR/user_stats_history.csv" 2>/dev/null
|
||||
log_success "История статистики пользователей восстановлена"
|
||||
fi
|
||||
if [ -f "$backup_dir/shared-443.json" ]; then
|
||||
cp "$backup_dir/shared-443.json" "$GOTELEGRAM_DIR/shared-443.json" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Восстанавливаем состояние бота
|
||||
if [ -d "$backup_dir/bot" ]; then
|
||||
|
||||
@@ -493,18 +493,25 @@ goTelegram Pro detected that 3x-ui/Xray already owns TCP/443. Two independent
|
||||
processes cannot bind the same IP:port at the same time. A safe shared setup
|
||||
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
|
||||
|
||||
- dispatcher: 0.0.0.0:443
|
||||
- dispatcher: 0.0.0.0:443 (nginx stream ssl_preread)
|
||||
- goTelegram Pro telemt: 127.0.0.1:7443
|
||||
- 3x-ui/Xray inbound: 127.0.0.1:9443
|
||||
- goTelegram Pro nginx mask site: 127.0.0.1:8443
|
||||
|
||||
The dispatcher must route Xray SNI domains to Xray and route the goTelegram Pro
|
||||
SNI domain to telemt. If Xray and goTelegram Pro use the same SNI domain, automatic
|
||||
sharing is not reliable: the first TLS ClientHello is intentionally identical.
|
||||
The dispatcher routes Xray SNI domains to Xray. Everything else goes to telemt;
|
||||
telemt then decides whether the session is MTProxy or regular HTTPS and forwards
|
||||
the website to nginx through dns_overrides.
|
||||
|
||||
goTelegram Pro intentionally does not rewrite the 3x-ui SQLite database or generated
|
||||
Xray config without explicit operator confirmation, because 3x-ui can overwrite
|
||||
manual JSON edits on the next panel change.
|
||||
goTelegram Pro can generate the dispatcher with:
|
||||
|
||||
source /opt/gotelegram/lib/shared443.sh
|
||||
shared443_enable <gotelegram-domain> <xray-sni-domain> 127.0.0.1:9443
|
||||
|
||||
Move the 3x-ui/Xray inbound from 0.0.0.0:443 to 127.0.0.1:9443 in the panel first,
|
||||
or nginx will not be able to own the public 443 socket. goTelegram Pro intentionally
|
||||
does not rewrite the 3x-ui SQLite database or generated Xray config without explicit
|
||||
operator confirmation, because 3x-ui can overwrite manual JSON edits on the next
|
||||
panel change.
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
248
lib/shared443.sh
Normal file
248
lib/shared443.sh
Normal 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
|
||||
82
lib/stats.sh
82
lib/stats.sh
@@ -12,9 +12,11 @@ NC='\033[0m' # No Color
|
||||
|
||||
STATS_DIR="/run/gotelegram"
|
||||
HISTORY_FILE="/opt/gotelegram/stats_history.csv"
|
||||
USER_HISTORY_FILE="/opt/gotelegram/user_stats_history.csv"
|
||||
SNAPSHOTS_DIR="$STATS_DIR/snapshots"
|
||||
CURRENT_SNAPSHOT="$STATS_DIR/stats_current.json"
|
||||
CONFIG_FILE="/opt/gotelegram/config.json"
|
||||
TELEMT_CONFIG_FILE="/etc/telemt/config.toml"
|
||||
|
||||
# Initialize stats infrastructure
|
||||
stats_init() {
|
||||
@@ -51,6 +53,9 @@ stats_init() {
|
||||
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||
echo "epoch,proxy_bytes,site_bytes" > "$HISTORY_FILE" 2>/dev/null
|
||||
fi
|
||||
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||
echo "epoch,user,total_octets,current_connections,active_unique_ips,recent_unique_ips" > "$USER_HISTORY_FILE" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Write initial snapshot
|
||||
stats_collect
|
||||
@@ -124,9 +129,63 @@ EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
stats_collect_users "$ts"
|
||||
|
||||
rm -f "$temp_file" 2>/dev/null
|
||||
}
|
||||
|
||||
# Print active telemt usernames from [access.users]. Usernames are restricted by
|
||||
# goTelegram to A-Z/a-z/0-9/_.- so they are safe in URLs and CSV fields.
|
||||
stats_active_users() {
|
||||
[[ -f "$TELEMT_CONFIG_FILE" ]] || return 0
|
||||
awk '
|
||||
/^\[access\.users\]$/ { in_users=1; next }
|
||||
in_users && /^\[/ { exit }
|
||||
in_users && /^[[:space:]]*#/ { next }
|
||||
in_users && /=/ {
|
||||
key=$1
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
||||
gsub(/^"|"$/, "", key)
|
||||
if (key ~ /^[A-Za-z0-9_.-]{1,48}$/) print key
|
||||
}
|
||||
' "$TELEMT_CONFIG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
stats_collect_users() {
|
||||
local ts="${1:-$(date +%s)}"
|
||||
local current_minute=$((ts - (ts % 60)))
|
||||
|
||||
mkdir -p "$(dirname "$USER_HISTORY_FILE")" 2>/dev/null
|
||||
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||
echo "epoch,user,total_octets,current_connections,active_unique_ips,recent_unique_ips" > "$USER_HISTORY_FILE" 2>/dev/null
|
||||
fi
|
||||
|
||||
command -v curl &>/dev/null || return 0
|
||||
command -v jq &>/dev/null || return 0
|
||||
|
||||
local user payload total conns active_ips recent_ips
|
||||
while IFS= read -r user; do
|
||||
[[ -n "$user" ]] || continue
|
||||
if awk -F, -v ts="$current_minute" -v user="$user" '$1 == ts && $2 == user { found=1 } END { exit found ? 0 : 1 }' "$USER_HISTORY_FILE" 2>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
|
||||
payload=$(curl -sS --max-time 2 "http://127.0.0.1:9091/v1/users/${user}" 2>/dev/null || true)
|
||||
[[ -n "$payload" ]] || continue
|
||||
total=$(echo "$payload" | jq -r '.data.total_octets // .total_octets // 0' 2>/dev/null)
|
||||
conns=$(echo "$payload" | jq -r '.data.current_connections // .current_connections // 0' 2>/dev/null)
|
||||
active_ips=$(echo "$payload" | jq -r '.data.active_unique_ips // .active_unique_ips // 0' 2>/dev/null)
|
||||
recent_ips=$(echo "$payload" | jq -r '.data.recent_unique_ips // .recent_unique_ips // 0' 2>/dev/null)
|
||||
[[ "$total" =~ ^[0-9]+$ ]] || total=0
|
||||
[[ "$conns" =~ ^[0-9]+$ ]] || conns=0
|
||||
[[ "$active_ips" =~ ^[0-9]+$ ]] || active_ips=0
|
||||
[[ "$recent_ips" =~ ^[0-9]+$ ]] || recent_ips=0
|
||||
echo "$current_minute,$user,$total,$conns,$active_ips,$recent_ips" >> "$USER_HISTORY_FILE" 2>/dev/null
|
||||
done < <(stats_active_users)
|
||||
|
||||
stats_cleanup_user_history
|
||||
}
|
||||
|
||||
# Read current snapshot as JSON
|
||||
stats_read_current() {
|
||||
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
|
||||
@@ -308,6 +367,23 @@ stats_cleanup_history() {
|
||||
mv "$temp_file" "$HISTORY_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
stats_cleanup_user_history() {
|
||||
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local now=$(date +%s)
|
||||
local ts_365d=$((now - 31536000))
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
{
|
||||
head -1 "$USER_HISTORY_FILE"
|
||||
awk -F, -v ts="$ts_365d" '$1 >= ts' "$USER_HISTORY_FILE" | tail -n +2
|
||||
} > "$temp_file" 2>/dev/null
|
||||
|
||||
mv "$temp_file" "$USER_HISTORY_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# Toggle stats collection on/off
|
||||
toggle_stats() {
|
||||
local current_state="false"
|
||||
@@ -432,13 +508,13 @@ remove_stats_collector() {
|
||||
|
||||
# Clean up directories and files
|
||||
rm -rf "$STATS_DIR" 2>/dev/null
|
||||
rm -f "$HISTORY_FILE" 2>/dev/null
|
||||
rm -f "$HISTORY_FILE" "$USER_HISTORY_FILE" 2>/dev/null
|
||||
|
||||
echo "Сервис статистики удалён" >&2
|
||||
}
|
||||
|
||||
# Export functions for external use
|
||||
export -f stats_init stats_collect stats_read_current stats_calculate_rates
|
||||
export -f stats_init stats_collect stats_collect_users stats_active_users stats_read_current stats_calculate_rates
|
||||
export -f show_traffic_stats format_bytes format_rate toggle_stats
|
||||
export -f stats_cleanup_history install_stats_collector remove_stats_collector
|
||||
export -f stats_cleanup_history stats_cleanup_user_history install_stats_collector remove_stats_collector
|
||||
export -f json_get
|
||||
|
||||
Reference in New Issue
Block a user