Safe Update
This commit is contained in:
98
README.md
98
README.md
@@ -12,6 +12,8 @@ DragonCoreSSH V40 é um painel/servidor em Go para SSH com HTTP Injection, paine
|
||||
- Integração com Xray-core/V2Ray
|
||||
- Configurador visual para VLESS e VMess
|
||||
- API pública `/check` para consultar usuário ou UUID
|
||||
- Aba de logs no painel para ver logs do sistema, DNSTT e Xray
|
||||
- Salvamento live das configurações principais, com checagem se o serviço realmente subiu
|
||||
- Serviço `systemd` para iniciar automaticamente com o sistema
|
||||
|
||||
### Protocolos suportados no configurador Xray/V2Ray
|
||||
@@ -104,6 +106,8 @@ Admin token
|
||||
```text
|
||||
80 SSH com HTTP Injection
|
||||
8080 SSH extra com HTTP Injection
|
||||
53/udp DNS público para DNSTT, redirecionado para 5300/udp
|
||||
5300/udp DNSTT interno
|
||||
9090 Painel web + API pública /check
|
||||
10086 Xray VLESS
|
||||
10087 Xray VMess
|
||||
@@ -115,11 +119,34 @@ Libere no firewall apenas as portas que você realmente usa. Exemplo com `ufw`:
|
||||
```bash
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 8080/tcp
|
||||
sudo ufw allow 53/udp
|
||||
sudo ufw allow 9090/tcp
|
||||
sudo ufw allow 10086/tcp
|
||||
sudo ufw allow 10087/tcp
|
||||
```
|
||||
|
||||
|
||||
### DNSTT na porta DNS 53
|
||||
|
||||
O instalador cria o serviço `sshpanel-dnstt-redirect.service`, que libera a porta 53 removendo o `systemd-resolved` quando ele existe, fixa `/etc/resolv.conf` com `1.1.1.1` e adiciona uma regra NAT para redirecionar DNS UDP público da porta `53` para o DNSTT em `5300`.
|
||||
|
||||
Comandos manuais equivalentes em sistemas com `iptables`:
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now systemd-resolved.service || true
|
||||
sudo rm -f /etc/resolv.conf
|
||||
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
|
||||
sudo iptables -t nat -C PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 5300 2>/dev/null \
|
||||
|| sudo iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
```
|
||||
|
||||
Verificar o redirect:
|
||||
|
||||
```bash
|
||||
systemctl status sshpanel-dnstt-redirect --no-pager -l
|
||||
sudo iptables -t nat -S PREROUTING | grep 5300
|
||||
```
|
||||
|
||||
### Comandos úteis
|
||||
|
||||
Ver status do serviço:
|
||||
@@ -146,6 +173,28 @@ Reiniciar serviço:
|
||||
systemctl restart sshpanel
|
||||
```
|
||||
|
||||
### Trocar senha perdida do admin
|
||||
|
||||
Se o dono perdeu a senha do painel, acesse o servidor como `root` e execute:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/sshpanel/change_admin_password.sh
|
||||
```
|
||||
|
||||
Também é possível passar a senha direto no comando:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/sshpanel/change_admin_password.sh admin 'NovaSenhaForteAqui'
|
||||
```
|
||||
|
||||
Ou gerar uma senha nova automaticamente:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/sshpanel/change_admin_password.sh --user admin --generate
|
||||
```
|
||||
|
||||
O script atualiza o usuário `admin` no PostgreSQL, ativa ele como `superadmin`, salva `ADMIN_PASSWORD` em `/opt/sshpanel/.env` e reinicia o serviço `sshpanel` para recarregar o cache interno de admins.
|
||||
|
||||
### Atualização
|
||||
|
||||
Entre na pasta do projeto atualizado e execute:
|
||||
@@ -250,6 +299,8 @@ DragonCoreSSH V40 is a Go-based SSH HTTP Injection server with a web panel, Post
|
||||
- Xray-core/V2Ray integration
|
||||
- Visual configurator for VLESS and VMess
|
||||
- Public `/check` API for checking username or UUID
|
||||
- Logs tab in the panel for system, DNSTT, and Xray logs
|
||||
- Live-save for main service settings, with checks that enabled services actually started
|
||||
- `systemd` service for automatic startup
|
||||
|
||||
### Supported protocols in the Xray/V2Ray configurator
|
||||
@@ -342,6 +393,8 @@ Admin token
|
||||
```text
|
||||
80 SSH with HTTP Injection
|
||||
8080 Extra SSH with HTTP Injection
|
||||
53/udp Public DNS for DNSTT, redirected to 5300/udp
|
||||
5300/udp Internal DNSTT listener
|
||||
9090 Web panel + public /check API
|
||||
10086 Xray VLESS
|
||||
10087 Xray VMess
|
||||
@@ -353,11 +406,34 @@ Open only the ports that you actually use. Example with `ufw`:
|
||||
```bash
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 8080/tcp
|
||||
sudo ufw allow 53/udp
|
||||
sudo ufw allow 9090/tcp
|
||||
sudo ufw allow 10086/tcp
|
||||
sudo ufw allow 10087/tcp
|
||||
```
|
||||
|
||||
|
||||
### DNSTT on DNS port 53
|
||||
|
||||
The installer creates `sshpanel-dnstt-redirect.service`. It frees port 53 by stopping `systemd-resolved` when present, writes `/etc/resolv.conf` with `1.1.1.1`, and adds a NAT rule that redirects public UDP DNS traffic from port `53` to DNSTT on `5300`.
|
||||
|
||||
Equivalent manual commands on systems with `iptables`:
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now systemd-resolved.service || true
|
||||
sudo rm -f /etc/resolv.conf
|
||||
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
|
||||
sudo iptables -t nat -C PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 5300 2>/dev/null \
|
||||
|| sudo iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
```
|
||||
|
||||
Check the redirect:
|
||||
|
||||
```bash
|
||||
systemctl status sshpanel-dnstt-redirect --no-pager -l
|
||||
sudo iptables -t nat -S PREROUTING | grep 5300
|
||||
```
|
||||
|
||||
### Useful commands
|
||||
|
||||
Check service status:
|
||||
@@ -384,6 +460,28 @@ Restart service:
|
||||
systemctl restart sshpanel
|
||||
```
|
||||
|
||||
### Reset lost admin password
|
||||
|
||||
If the owner loses the web panel password, access the server as `root` and run:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/sshpanel/change_admin_password.sh
|
||||
```
|
||||
|
||||
You can also pass the password directly:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/sshpanel/change_admin_password.sh admin 'NewStrongPasswordHere'
|
||||
```
|
||||
|
||||
Or generate a new password automatically:
|
||||
|
||||
```bash
|
||||
sudo bash /opt/sshpanel/change_admin_password.sh --user admin --generate
|
||||
```
|
||||
|
||||
The script updates the `admin` user in PostgreSQL, enables it as `superadmin`, saves `ADMIN_PASSWORD` in `/opt/sshpanel/.env`, and restarts `sshpanel` so the in-memory admin cache is reloaded.
|
||||
|
||||
### Update
|
||||
|
||||
Enter the updated source-code folder and run:
|
||||
|
||||
@@ -131,6 +131,7 @@ pre.log-box{background:rgba(15,23,42,.9);color:#9ca3af;border:1px solid rgba(55,
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="xray">Xray</button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="resellers">Resellers</button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="stats">Stats</button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="logs">Logs</button>
|
||||
<button class="tab-btn superadmin-only hidden" data-tab="server">Server Config</button>
|
||||
</nav>
|
||||
<div class="hright">
|
||||
@@ -533,6 +534,25 @@ pre.log-box{background:rgba(15,23,42,.9);color:#9ca3af;border:1px solid rgba(55,
|
||||
</div>
|
||||
</div><!-- /tab-stats -->
|
||||
|
||||
<!-- ═══════════ Logs Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-logs">
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="card-title">System Logs</div>
|
||||
<div class="form-actions" style="margin-top:0">
|
||||
<select id="logSource" class="btn-ghost" style="border-radius:999px;padding:4px 8px;background:rgba(15,23,42,.9);color:var(--text);border:1px solid rgba(55,65,81,.9);font-size:.7rem;">
|
||||
<option value="panel">Panel / system</option>
|
||||
<option value="dnstt">DNSTT</option>
|
||||
<option value="xray">Xray</option>
|
||||
</select>
|
||||
<button class="btn btn-sm" type="button" onclick="loadSystemLogs()">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="log-box" id="systemLogBox" style="max-height:430px;min-height:260px;">Select a log source and click Refresh.</pre>
|
||||
<div class="statusbar"><span id="systemLogStatus">Ready.</span></div>
|
||||
</div>
|
||||
</div><!-- /tab-logs -->
|
||||
|
||||
<!-- ═══════════ Server Config Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-server">
|
||||
|
||||
@@ -547,14 +567,10 @@ pre.log-box{background:rgba(15,23,42,.9);color:#9ca3af;border:1px solid rgba(55,
|
||||
<span class="chip green">live</span>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<div class="field" style="grid-column:1/-1">
|
||||
<label>Main Listen (SSH / HTTP)</label>
|
||||
<input type="text" id="cfgListen" placeholder="0.0.0.0:80"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Local SSH Listen</label>
|
||||
<input type="text" id="cfgLocalSSH" placeholder="127.0.0.1:2222 (blank = off)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" style="margin-top:6px">
|
||||
<label>Extra Listen Addresses <span class="hint">(one per line, e.g. 0.0.0.0:8080)</span></label>
|
||||
@@ -749,7 +765,7 @@ pre.log-box{background:rgba(15,23,42,.9);color:#9ca3af;border:1px solid rgba(55,
|
||||
<div class="form-actions" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border);">
|
||||
<button class="btn" onclick="saveServerConfig()">Save Config</button>
|
||||
<button class="btn btn-ghost" onclick="loadServerConfig()">Reload</button>
|
||||
<span id="srvCfgStatus" class="hint">All changes apply live. Only <strong>host_key_file</strong> requires a restart.</span>
|
||||
<span id="srvCfgStatus" class="hint">All service changes apply live.</span>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-server -->
|
||||
@@ -1535,6 +1551,29 @@ async function loadStats() {
|
||||
} catch (e) { if (e.message==="auth") doAuthError(); }
|
||||
}
|
||||
|
||||
// ─── Logs ─────────────────────────────────────────────────────────────────────
|
||||
document.querySelector("[data-tab='logs']")?.addEventListener("click", loadSystemLogs);
|
||||
document.getElementById("logSource")?.addEventListener("change", loadSystemLogs);
|
||||
|
||||
async function loadSystemLogs() {
|
||||
const box = document.getElementById("systemLogBox");
|
||||
const st = document.getElementById("systemLogStatus");
|
||||
const source = document.getElementById("logSource")?.value || "panel";
|
||||
st.textContent = "Loading…";
|
||||
try {
|
||||
const res = await api(`/api/system/logs?source=${encodeURIComponent(source)}&lines=500`);
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const data = await res.json();
|
||||
const lines = Array.isArray(data.lines) ? data.lines : [];
|
||||
box.textContent = lines.length ? lines.join("\n") : "No log lines yet.";
|
||||
box.scrollTop = box.scrollHeight;
|
||||
st.textContent = `${data.source || source} logs${data.path ? " · " + data.path : ""} · ${lines.length} lines · ` + new Date().toLocaleTimeString();
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else st.textContent = "Error: " + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Server Config ────────────────────────────────────────────────────────────
|
||||
document.querySelector("[data-tab='server']")?.addEventListener("click", loadServerConfig);
|
||||
|
||||
@@ -1559,7 +1598,6 @@ async function loadServerConfig() {
|
||||
|
||||
// Network
|
||||
document.getElementById("cfgListen").value = c.listen || "";
|
||||
document.getElementById("cfgLocalSSH").value = c.local_ssh_listen || "";
|
||||
document.getElementById("cfgExtraListen").value = (c.extra_listen || []).join("\n");
|
||||
|
||||
// SSH / general
|
||||
@@ -1620,7 +1658,6 @@ async function saveServerConfig() {
|
||||
const cfg = {
|
||||
listen: document.getElementById("cfgListen").value.trim(),
|
||||
extra_listen: extraLines,
|
||||
local_ssh_listen: document.getElementById("cfgLocalSSH").value.trim(),
|
||||
host_key_file: "/opt/sshpanel/ssh_host_rsa_key",
|
||||
admin_dir: "/opt/sshpanel/admin",
|
||||
default_limit_mbps_up: parseInt(document.getElementById("cfgLimitUp").value || "0", 10),
|
||||
@@ -1654,7 +1691,15 @@ async function saveServerConfig() {
|
||||
try {
|
||||
const res = await api("/api/server/config", { method: "POST", body: JSON.stringify(cfg) });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
st.textContent = "Saved. Banner applied live — other changes need a server restart.";
|
||||
const report = await res.json().catch(() => null);
|
||||
const warnings = report?.warnings || [];
|
||||
const bad = Object.entries(report?.services || {}).filter(([_, v]) => v?.enabled && !v?.running);
|
||||
if (warnings.length || bad.length) {
|
||||
const badText = bad.map(([name, v]) => `${name}: ${v.error || "not running"}`).join(" | ");
|
||||
st.textContent = "Saved live with warnings: " + [...warnings, badText].filter(Boolean).join(" | ");
|
||||
} else {
|
||||
st.textContent = "Saved and applied live.";
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message === "auth") doAuthError();
|
||||
else st.textContent = "Error: " + e.message;
|
||||
|
||||
8
auth.go
8
auth.go
@@ -536,10 +536,10 @@ func handleMe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if s.Role == RoleReseller {
|
||||
if u, ok := adminUsers.get(s.Username); ok {
|
||||
resp["max_users"] = u.MaxUsers
|
||||
resp["max_users"] = u.MaxUsers
|
||||
resp["used_users"] = countOwnedUsers(s.Username)
|
||||
resp["expires_at"] = u.ExpiresAt
|
||||
resp["is_active"] = u.IsActive
|
||||
resp["is_active"] = u.IsActive
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -633,8 +633,8 @@ func handleCreateReseller(store *Store) http.HandlerFunc {
|
||||
if p.Password != "" {
|
||||
u.PasswordHash = hashAdminPassword(p.Password)
|
||||
}
|
||||
u.MaxUsers = p.MaxUsers
|
||||
u.IsActive = p.IsActive
|
||||
u.MaxUsers = p.MaxUsers
|
||||
u.IsActive = p.IsActive
|
||||
u.ExpiresAt = nil
|
||||
if p.ExpiresAt != "" {
|
||||
t, err := time.Parse(time.RFC3339, p.ExpiresAt)
|
||||
|
||||
236
change_admin_password.sh
Normal file
236
change_admin_password.sh
Normal file
@@ -0,0 +1,236 @@
|
||||
#!/bin/bash
|
||||
# DragonCoreSSH V40 admin password recovery tool.
|
||||
# Usage:
|
||||
# sudo bash change_admin_password.sh
|
||||
# sudo bash change_admin_password.sh admin 'NewPasswordHere'
|
||||
# sudo bash change_admin_password.sh --user admin --generate
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
error() { echo -e "${RED}[x]${NC} $*"; exit 1; }
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/sshpanel}"
|
||||
SERVICE_NAME="${SERVICE_NAME:-sshpanel}"
|
||||
ENV_FILE="${ENV_FILE:-${INSTALL_DIR}/.env}"
|
||||
ADMIN_USER=""
|
||||
NEW_PASSWORD=""
|
||||
GENERATE_PASSWORD=false
|
||||
NO_RESTART=false
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
DragonCoreSSH V40 admin password recovery
|
||||
|
||||
Usage:
|
||||
sudo bash $0
|
||||
sudo bash $0 admin 'NewPasswordHere'
|
||||
sudo bash $0 --user admin --password 'NewPasswordHere'
|
||||
sudo bash $0 --user admin --generate
|
||||
|
||||
Options:
|
||||
-u, --user USERNAME Admin username to reset. Default: admin
|
||||
-p, --password PASSWORD New password. If omitted, you will be prompted.
|
||||
-g, --generate Generate a strong random password.
|
||||
--no-restart Do not restart the sshpanel service after changing DB.
|
||||
-h, --help Show this help.
|
||||
|
||||
Environment overrides:
|
||||
INSTALL_DIR=/opt/sshpanel
|
||||
ENV_FILE=/opt/sshpanel/.env
|
||||
SERVICE_NAME=sshpanel
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-u|--user)
|
||||
[[ $# -ge 2 ]] || error "Missing value for $1"
|
||||
ADMIN_USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-p|--password)
|
||||
[[ $# -ge 2 ]] || error "Missing value for $1"
|
||||
NEW_PASSWORD="$2"
|
||||
shift 2
|
||||
;;
|
||||
-g|--generate)
|
||||
GENERATE_PASSWORD=true
|
||||
shift
|
||||
;;
|
||||
--no-restart)
|
||||
NO_RESTART=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-* )
|
||||
error "Unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$ADMIN_USER" ]]; then
|
||||
ADMIN_USER="$1"
|
||||
elif [[ -z "$NEW_PASSWORD" ]]; then
|
||||
NEW_PASSWORD="$1"
|
||||
else
|
||||
error "Too many positional arguments. Use --help for usage."
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $EUID -ne 0 ]] && error "Run as root: sudo bash $0"
|
||||
[[ -f "$ENV_FILE" ]] || error "Environment file not found: $ENV_FILE"
|
||||
command -v psql >/dev/null 2>&1 || error "psql not found. Install PostgreSQL client first."
|
||||
|
||||
get_env_value() {
|
||||
local key="$1"
|
||||
awk -v key="$key" '
|
||||
$0 ~ "^" key "=" {
|
||||
sub("^[^=]*=", "")
|
||||
gsub(/^\"|\"$/, "")
|
||||
gsub(/^\047|\047$/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$ENV_FILE"
|
||||
}
|
||||
|
||||
update_env_password() {
|
||||
local new_password="$1"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
awk -v line="ADMIN_PASSWORD=${new_password}" '
|
||||
BEGIN { done = 0 }
|
||||
/^ADMIN_PASSWORD=/ { print line; done = 1; next }
|
||||
{ print }
|
||||
END { if (!done) print line }
|
||||
' "$ENV_FILE" > "$tmp"
|
||||
cat "$tmp" > "$ENV_FILE"
|
||||
rm -f "$tmp"
|
||||
chmod 600 "$ENV_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
generate_password() {
|
||||
local pw=""
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
pw="$(openssl rand -base64 24 | tr -d '\n' | tr -d '=/+' | head -c 24 || true)"
|
||||
fi
|
||||
if [[ ${#pw} -lt 20 ]]; then
|
||||
pw="$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 24 || true)"
|
||||
fi
|
||||
if [[ ${#pw} -lt 20 ]]; then
|
||||
pw="DragonCore$(date +%s%N)"
|
||||
fi
|
||||
printf '%s' "$pw"
|
||||
}
|
||||
|
||||
hash_password() {
|
||||
local pw="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
printf '%s' "$pw" | sha256sum | awk '{print $1}'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
printf '%s' "$pw" | shasum -a 256 | awk '{print $1}'
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
printf '%s' "$pw" | openssl dgst -sha256 -r | awk '{print $1}'
|
||||
else
|
||||
error "No SHA-256 tool found. Install coreutils or openssl."
|
||||
fi
|
||||
}
|
||||
|
||||
PG_DSN="$(get_env_value PG_DSN)"
|
||||
[[ -n "$PG_DSN" ]] || error "PG_DSN not found inside $ENV_FILE"
|
||||
|
||||
if [[ -z "$ADMIN_USER" ]]; then
|
||||
read -r -p "Admin username [admin]: " ADMIN_USER
|
||||
ADMIN_USER="${ADMIN_USER:-admin}"
|
||||
fi
|
||||
|
||||
[[ -n "$ADMIN_USER" ]] || error "Admin username cannot be empty."
|
||||
|
||||
if $GENERATE_PASSWORD; then
|
||||
NEW_PASSWORD="$(generate_password)"
|
||||
elif [[ -z "$NEW_PASSWORD" ]]; then
|
||||
read -r -s -p "New password: " PASS1
|
||||
echo
|
||||
read -r -s -p "Confirm password: " PASS2
|
||||
echo
|
||||
[[ "$PASS1" == "$PASS2" ]] || error "Passwords do not match."
|
||||
NEW_PASSWORD="$PASS1"
|
||||
fi
|
||||
|
||||
[[ -n "$NEW_PASSWORD" ]] || error "Password cannot be empty."
|
||||
if [[ ${#NEW_PASSWORD} -lt 8 ]]; then
|
||||
error "Password must have at least 8 characters."
|
||||
fi
|
||||
|
||||
PASSWORD_HASH="$(hash_password "$NEW_PASSWORD")"
|
||||
[[ ${#PASSWORD_HASH} -eq 64 ]] || error "Failed to generate valid SHA-256 password hash."
|
||||
|
||||
info "Updating admin user '${ADMIN_USER}' in PostgreSQL..."
|
||||
psql "$PG_DSN" -v ON_ERROR_STOP=1 \
|
||||
-v admin_user="$ADMIN_USER" \
|
||||
-v password_hash="$PASSWORD_HASH" <<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'reseller',
|
||||
max_users INT NOT NULL DEFAULT 30,
|
||||
expires_at TIMESTAMPTZ,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO admin_users (username, password_hash, role, max_users, expires_at, is_active)
|
||||
VALUES (:'admin_user', :'password_hash', 'superadmin', 0, NULL, TRUE)
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
password_hash = EXCLUDED.password_hash,
|
||||
role = 'superadmin',
|
||||
max_users = 0,
|
||||
expires_at = NULL,
|
||||
is_active = TRUE;
|
||||
SQL
|
||||
|
||||
if [[ "$ADMIN_USER" == "admin" ]]; then
|
||||
update_env_password "$NEW_PASSWORD"
|
||||
info "Updated ADMIN_PASSWORD inside $ENV_FILE"
|
||||
else
|
||||
warn "ADMIN_PASSWORD in $ENV_FILE was not changed because username is not 'admin'."
|
||||
fi
|
||||
|
||||
if ! $NO_RESTART; then
|
||||
info "Restarting ${SERVICE_NAME} so the in-memory admin cache reloads..."
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files "${SERVICE_NAME}.service" >/dev/null 2>&1; then
|
||||
systemctl restart "$SERVICE_NAME"
|
||||
sleep 1
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
info "${SERVICE_NAME} restarted successfully."
|
||||
else
|
||||
warn "${SERVICE_NAME} is not active after restart. Last logs:"
|
||||
journalctl -u "$SERVICE_NAME" -n 30 --no-pager 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
service "$SERVICE_NAME" restart || warn "Could not restart ${SERVICE_NAME}. Restart it manually."
|
||||
else
|
||||
warn "Could not restart ${SERVICE_NAME}. Restart it manually before logging in."
|
||||
fi
|
||||
else
|
||||
warn "Service restart skipped. Restart ${SERVICE_NAME} manually before logging in."
|
||||
fi
|
||||
|
||||
echo
|
||||
info "Admin password changed."
|
||||
echo " Username : ${ADMIN_USER}"
|
||||
echo " Password : ${NEW_PASSWORD}"
|
||||
echo
|
||||
warn "Save this password now. It is only shown here."
|
||||
146
config_safety.go
Normal file
146
config_safety.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMainListen = "0.0.0.0:80"
|
||||
defaultExtraListen = "0.0.0.0:8080"
|
||||
defaultDNSTTListen = "[::]:5300"
|
||||
defaultUDPGWListen = "0.0.0.0:7400"
|
||||
)
|
||||
|
||||
func normalizeRuntimePorts(cfg *Config) []string {
|
||||
var warnings []string
|
||||
warn := func(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
warnings = append(warnings, msg)
|
||||
log.Printf("config safety: %s", msg)
|
||||
}
|
||||
|
||||
cfg.Listen = strings.TrimSpace(cfg.Listen)
|
||||
if cfg.Listen == "" {
|
||||
cfg.Listen = defaultMainListen
|
||||
}
|
||||
if err := tcpAddrAvailableForPool(cfg.Listen, publicPool); err != nil {
|
||||
old := cfg.Listen
|
||||
cfg.Listen = defaultMainListen
|
||||
warn("main listener %s is unavailable (%v); using default %s", old, err, cfg.Listen)
|
||||
if err2 := tcpAddrAvailableForPool(cfg.Listen, publicPool); err2 != nil {
|
||||
warn("default main listener %s is also unavailable: %v", cfg.Listen, err2)
|
||||
}
|
||||
}
|
||||
|
||||
seen := map[string]bool{cfg.Listen: true}
|
||||
extra := make([]string, 0, len(cfg.ExtraListen))
|
||||
for _, addr := range cfg.ExtraListen {
|
||||
addr = strings.TrimSpace(addr)
|
||||
if addr == "" || seen[addr] {
|
||||
continue
|
||||
}
|
||||
if err := tcpAddrAvailableForPool(addr, publicPool); err != nil {
|
||||
warn("extra listener %s is unavailable (%v)", addr, err)
|
||||
fallback := defaultExtraListen
|
||||
if !seen[fallback] {
|
||||
if err2 := tcpAddrAvailableForPool(fallback, publicPool); err2 == nil {
|
||||
extra = append(extra, fallback)
|
||||
seen[fallback] = true
|
||||
warn("extra listener fell back to default %s", fallback)
|
||||
} else {
|
||||
warn("default extra listener %s is also unavailable: %v", fallback, err2)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
extra = append(extra, addr)
|
||||
seen[addr] = true
|
||||
}
|
||||
cfg.ExtraListen = extra
|
||||
|
||||
// DragonCore no longer uses an internal local SSH listener.
|
||||
cfg.LocalSSHListen = ""
|
||||
|
||||
if cfg.DNSTT != nil {
|
||||
cfg.DNSTT.UDPListen = strings.TrimSpace(cfg.DNSTT.UDPListen)
|
||||
if cfg.DNSTT.UDPListen == "" {
|
||||
cfg.DNSTT.UDPListen = defaultDNSTTListen
|
||||
}
|
||||
if err := udpAddrAvailableForDNSTT(cfg.DNSTT.UDPListen); err != nil {
|
||||
old := cfg.DNSTT.UDPListen
|
||||
cfg.DNSTT.UDPListen = defaultDNSTTListen
|
||||
warn("DNSTT UDP listener %s is unavailable (%v); using default %s", old, err, cfg.DNSTT.UDPListen)
|
||||
if err2 := udpAddrAvailableForDNSTT(cfg.DNSTT.UDPListen); err2 != nil {
|
||||
warn("default DNSTT UDP listener %s is also unavailable: %v", cfg.DNSTT.UDPListen, err2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.UDPGW != nil {
|
||||
cfg.UDPGW.Listen = strings.TrimSpace(cfg.UDPGW.Listen)
|
||||
if cfg.UDPGW.Listen == "" {
|
||||
cfg.UDPGW.Listen = defaultUDPGWListen
|
||||
}
|
||||
if err := tcpAddrAvailableForUDPGW(cfg.UDPGW.Listen); err != nil {
|
||||
old := cfg.UDPGW.Listen
|
||||
cfg.UDPGW.Listen = defaultUDPGWListen
|
||||
warn("UDPGW listener %s is unavailable (%v); using default %s", old, err, cfg.UDPGW.Listen)
|
||||
if err2 := tcpAddrAvailableForUDPGW(cfg.UDPGW.Listen); err2 != nil {
|
||||
warn("default UDPGW listener %s is also unavailable: %v", cfg.UDPGW.Listen, err2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
|
||||
func tcpAddrAvailableForPool(addr string, pool *listenerPool) error {
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
if pool != nil && pool.Has(addr) {
|
||||
return nil
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ln.Close()
|
||||
}
|
||||
|
||||
func tcpAddrAvailableForUDPGW(addr string) error {
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
globalCfgMu.RLock()
|
||||
current := globalCfg != nil && globalCfg.UDPGW != nil && globalCfg.UDPGW.Listen == addr && udpgwRunning()
|
||||
globalCfgMu.RUnlock()
|
||||
if current {
|
||||
return nil
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ln.Close()
|
||||
}
|
||||
|
||||
func udpAddrAvailableForDNSTT(addr string) error {
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
globalCfgMu.RLock()
|
||||
current := globalCfg != nil && globalCfg.DNSTT != nil && globalCfg.DNSTT.UDPListen == addr && dnsttRunning()
|
||||
globalCfgMu.RUnlock()
|
||||
if current {
|
||||
return nil
|
||||
}
|
||||
pc, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pc.Close()
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -332,55 +333,79 @@ func getDNSTTLogLines() []string {
|
||||
// the Noise private key from cfg.PrivKeyFile, parses cfg.Domain into a dns.Name,
|
||||
// and then launches runDNSTT in a goroutine. Any errors during start are
|
||||
// logged. The SSH server configuration is used when handling streams.
|
||||
func startDNSTT(cfg *DNSTTConfig, sshConf *ssh.ServerConfig) {
|
||||
func startDNSTT(cfg *DNSTTConfig, sshConf *ssh.ServerConfig) error {
|
||||
if cfg == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
startDNSTTCapReaper()
|
||||
dnsttSSHConfig = sshConf
|
||||
// Configure whether periodic DNSTT statistics should be emitted to stderr.
|
||||
// When DisableStatsLog is true, stats will be collected but log lines are suppressed.
|
||||
if cfg != nil {
|
||||
dnsttPrintStats = !cfg.DisableStatsLog
|
||||
// Initialise the log buffer once. Use a capacity of 100 lines (~few KB).
|
||||
if dnsttLogBuf == nil {
|
||||
dnsttLogBuf = newDNSTTLogBuffer(100)
|
||||
}
|
||||
// Configure the DNSTT logger output. If DisableConsoleLog is set,
|
||||
// write only to the buffer; otherwise tee to both the buffer and stderr.
|
||||
if cfg.DisableConsoleLog {
|
||||
dnsttLog.SetOutput(dnsttLogBuf)
|
||||
} else {
|
||||
dnsttLog.SetOutput(io.MultiWriter(dnsttLogBuf, os.Stderr))
|
||||
}
|
||||
dnsttPrintStats = !cfg.DisableStatsLog
|
||||
// Initialise the log buffer once. Use a capacity of 100 lines (~few KB).
|
||||
if dnsttLogBuf == nil {
|
||||
dnsttLogBuf = newDNSTTLogBuffer(100)
|
||||
}
|
||||
// Configure the DNSTT logger output. If DisableConsoleLog is set,
|
||||
// write only to the buffer; otherwise tee to both the buffer and stderr.
|
||||
if cfg.DisableConsoleLog {
|
||||
dnsttLog.SetOutput(dnsttLogBuf)
|
||||
} else {
|
||||
dnsttLog.SetOutput(io.MultiWriter(dnsttLogBuf, os.Stderr))
|
||||
}
|
||||
|
||||
// Read the private key from file.
|
||||
f, err := os.Open(cfg.PrivKeyFile)
|
||||
if err != nil {
|
||||
dnsttLog.Printf("cannot open privkey file %s: %v", cfg.PrivKeyFile, err)
|
||||
return
|
||||
msg := fmt.Errorf("cannot open privkey file %s: %w", cfg.PrivKeyFile, err)
|
||||
dnsttLog.Print(msg.Error())
|
||||
return msg
|
||||
}
|
||||
privkey, err := noise.ReadKey(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
dnsttLog.Printf("cannot read privkey from file: %v", err)
|
||||
return
|
||||
msg := fmt.Errorf("cannot read privkey from file: %w", err)
|
||||
dnsttLog.Print(msg.Error())
|
||||
return msg
|
||||
}
|
||||
// Parse the domain name. dns.ParseName accepts a domain with a trailing
|
||||
// dot or without. Any error here will abort the dnstt server.
|
||||
// Parse the domain name. dns.ParseName accepts a domain with a trailing
|
||||
// dot or without. Any error here will abort the dnstt server.
|
||||
domain, err := dns.ParseName(cfg.Domain)
|
||||
if err != nil {
|
||||
dnsttLog.Printf("invalid domain %q: %v", cfg.Domain, err)
|
||||
return
|
||||
msg := fmt.Errorf("invalid domain %q: %w", cfg.Domain, err)
|
||||
dnsttLog.Print(msg.Error())
|
||||
return msg
|
||||
}
|
||||
udpListen := cfg.UDPListen
|
||||
if udpListen == "" {
|
||||
udpListen = defaultDNSTTListen
|
||||
cfg.UDPListen = udpListen
|
||||
}
|
||||
|
||||
// Bind synchronously so the admin panel can immediately know whether DNSTT
|
||||
// really started or failed because of a bad address/locked port.
|
||||
dnsConn, err := net.ListenPacket("udp", udpListen)
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("dnstt: opening UDP listener on %s: %w", udpListen, err)
|
||||
dnsttLog.Print(msg.Error())
|
||||
return msg
|
||||
}
|
||||
|
||||
// Log initialisation parameters so DNSTT startup is visible even when
|
||||
// quiet logging is enabled. This helps with debugging.
|
||||
dnsttLog.Printf("starting: domain=%q udp_listen=%q privkey=%q", cfg.Domain, cfg.UDPListen, cfg.PrivKeyFile)
|
||||
// quiet logging is enabled. This helps with debugging.
|
||||
dnsttLog.Printf("starting: domain=%q udp_listen=%q privkey=%q", cfg.Domain, udpListen, cfg.PrivKeyFile)
|
||||
go func() {
|
||||
if err := runDNSTT(privkey, domain, cfg.UDPListen); err != nil {
|
||||
if err := runDNSTTOnConn(privkey, domain, udpListen, dnsConn); err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
dnsttLog.Printf("server exited with error: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func dnsttRunning() bool {
|
||||
dnsttConnMu.Lock()
|
||||
defer dnsttConnMu.Unlock()
|
||||
return dnsttConn != nil
|
||||
}
|
||||
|
||||
// handleDNSTTStream accepts a smux.Stream from a client and hands it off to
|
||||
@@ -991,6 +1016,10 @@ func runDNSTT(privkey []byte, domain dns.Name, udpListen string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnstt: opening UDP listener on %s: %v", udpListen, err)
|
||||
}
|
||||
return runDNSTTOnConn(privkey, domain, udpListen, dnsConn)
|
||||
}
|
||||
|
||||
func runDNSTTOnConn(privkey []byte, domain dns.Name, udpListen string, dnsConn net.PacketConn) error {
|
||||
if udp, ok := dnsConn.(*net.UDPConn); ok {
|
||||
_ = udp.SetReadBuffer(4 * 1024 * 1024)
|
||||
_ = udp.SetWriteBuffer(4 * 1024 * 1024)
|
||||
@@ -998,11 +1027,18 @@ func runDNSTT(privkey []byte, domain dns.Name, udpListen string) error {
|
||||
|
||||
// Register so stopDNSTT() can close this socket and unblock the read loop.
|
||||
dnsttConnMu.Lock()
|
||||
if dnsttConn != nil {
|
||||
if dnsttConn != nil && dnsttConn != dnsConn {
|
||||
_ = dnsttConn.Close()
|
||||
}
|
||||
dnsttConn = dnsConn
|
||||
dnsttConnMu.Unlock()
|
||||
defer func() {
|
||||
dnsttConnMu.Lock()
|
||||
if dnsttConn == dnsConn {
|
||||
dnsttConn = nil
|
||||
}
|
||||
dnsttConnMu.Unlock()
|
||||
}()
|
||||
// Log readiness of the UDP listener.
|
||||
dnsttLog.Printf("udp listener ready on %s", udpListen)
|
||||
// compute maximum encoded payload and resulting MTU
|
||||
|
||||
176
hotreload.go
176
hotreload.go
@@ -9,7 +9,9 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -86,6 +88,31 @@ func (p *listenerPool) Sync(addrs []string) []error {
|
||||
return errs
|
||||
}
|
||||
|
||||
func (p *listenerPool) Has(addr string) bool {
|
||||
if p == nil || addr == "" {
|
||||
return false
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
_, ok := p.entries[addr]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *listenerPool) HasAll(addrs []string) bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
if !p.Has(addr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------- Dynamic TLS listener pool ----------
|
||||
|
||||
type tlsListenerPool struct {
|
||||
@@ -142,11 +169,35 @@ func (p *tlsListenerPool) Sync(forwarders []TLSForwarderConfig) []error {
|
||||
return errs
|
||||
}
|
||||
|
||||
func (p *tlsListenerPool) Has(addr string) bool {
|
||||
if p == nil || addr == "" {
|
||||
return false
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
_, ok := p.entries[addr]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *tlsListenerPool) HasAll(forwarders []TLSForwarderConfig) bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
for _, f := range forwarders {
|
||||
if f.Listen == "" {
|
||||
continue
|
||||
}
|
||||
if !p.Has(f.Listen) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------- Global pool instances (initialised in main) ----------
|
||||
|
||||
var (
|
||||
publicPool *listenerPool // HTTP+SSH: listen + extra_listen
|
||||
localPool *listenerPool // raw SSH: local_ssh_listen
|
||||
tlsPool *tlsListenerPool // TLS forwarders
|
||||
)
|
||||
|
||||
@@ -196,8 +247,42 @@ func getAdminHandler() http.Handler {
|
||||
// applyFullConfigReload applies every field in newCfg to the running server
|
||||
// without a process restart. Port changes, DNSTT/UDPGW changes, Xray changes,
|
||||
// and bandwidth defaults all take effect immediately.
|
||||
// The only field that still requires a restart is host_key_file.
|
||||
func applyFullConfigReload(newCfg *Config) {
|
||||
// It returns a status report so the panel can show crashed or blocked services.
|
||||
type ServiceReloadStatus struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Running bool `json:"running"`
|
||||
Listen string `json:"listen,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigReloadReport struct {
|
||||
Applied bool `json:"applied"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Services map[string]ServiceReloadStatus `json:"services"`
|
||||
}
|
||||
|
||||
func newReloadReport() ConfigReloadReport {
|
||||
return ConfigReloadReport{Applied: true, Services: map[string]ServiceReloadStatus{}}
|
||||
}
|
||||
|
||||
func (r *ConfigReloadReport) warnf(format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
r.Warnings = append(r.Warnings, msg)
|
||||
log.Printf("config reload: %s", msg)
|
||||
}
|
||||
|
||||
func joinAddrs(addrs []string) string {
|
||||
clean := make([]string, 0, len(addrs))
|
||||
for _, a := range addrs {
|
||||
if a = strings.TrimSpace(a); a != "" {
|
||||
clean = append(clean, a)
|
||||
}
|
||||
}
|
||||
return strings.Join(clean, ", ")
|
||||
}
|
||||
|
||||
func applyFullConfigReload(newCfg *Config) ConfigReloadReport {
|
||||
report := newReloadReport()
|
||||
// Banner
|
||||
bt := newCfg.Banner
|
||||
if bt == "" && newCfg.BannerFile != "" {
|
||||
@@ -226,44 +311,97 @@ func applyFullConfigReload(newCfg *Config) {
|
||||
// Public SSH listeners (main listen + extra_listen)
|
||||
publicAddrs := append([]string{newCfg.Listen}, newCfg.ExtraListen...)
|
||||
for _, e := range publicPool.Sync(publicAddrs) {
|
||||
log.Printf("hotreload: %v", e)
|
||||
report.warnf("SSH listener error: %v", e)
|
||||
}
|
||||
report.Services["ssh"] = ServiceReloadStatus{
|
||||
Enabled: true,
|
||||
Running: publicPool.HasAll(publicAddrs),
|
||||
Listen: joinAddrs(publicAddrs),
|
||||
}
|
||||
if !report.Services["ssh"].Running {
|
||||
report.Services["ssh"] = ServiceReloadStatus{Enabled: true, Running: false, Listen: joinAddrs(publicAddrs), Error: "one or more SSH listeners could not be opened"}
|
||||
}
|
||||
|
||||
// Local raw SSH listener
|
||||
var localAddrs []string
|
||||
if newCfg.LocalSSHListen != "" {
|
||||
localAddrs = []string{newCfg.LocalSSHListen}
|
||||
}
|
||||
for _, e := range localPool.Sync(localAddrs) {
|
||||
log.Printf("hotreload: %v", e)
|
||||
}
|
||||
// Legacy local_ssh_listen is intentionally ignored. DragonCore handles DNSTT in-process.
|
||||
newCfg.LocalSSHListen = ""
|
||||
|
||||
// TLS forwarders
|
||||
for _, e := range tlsPool.Sync(newCfg.TLSForwarders) {
|
||||
log.Printf("hotreload: %v", e)
|
||||
report.warnf("TLS listener error: %v", e)
|
||||
}
|
||||
if len(newCfg.TLSForwarders) > 0 {
|
||||
report.Services["tls"] = ServiceReloadStatus{
|
||||
Enabled: true,
|
||||
Running: tlsPool.HasAll(newCfg.TLSForwarders),
|
||||
Listen: tlsForwarderList(newCfg.TLSForwarders),
|
||||
}
|
||||
if !report.Services["tls"].Running {
|
||||
report.Services["tls"] = ServiceReloadStatus{Enabled: true, Running: false, Listen: tlsForwarderList(newCfg.TLSForwarders), Error: "one or more TLS forwarders could not be opened"}
|
||||
}
|
||||
} else {
|
||||
report.Services["tls"] = ServiceReloadStatus{Enabled: false, Running: false}
|
||||
}
|
||||
|
||||
// DNSTT — stop current instance (no-op if not running) then start new one
|
||||
// DNSTT — stop current instance (no-op if not running) then start new one.
|
||||
stopDNSTT()
|
||||
startDNSTT(newCfg.DNSTT, getSSHConfig())
|
||||
if newCfg.DNSTT != nil {
|
||||
if err := startDNSTT(newCfg.DNSTT, getSSHConfig()); err != nil {
|
||||
report.warnf("DNSTT failed to start: %v", err)
|
||||
report.Services["dnstt"] = ServiceReloadStatus{Enabled: true, Running: false, Listen: newCfg.DNSTT.UDPListen, Error: err.Error()}
|
||||
} else {
|
||||
report.Services["dnstt"] = ServiceReloadStatus{Enabled: true, Running: true, Listen: newCfg.DNSTT.UDPListen}
|
||||
}
|
||||
} else {
|
||||
report.Services["dnstt"] = ServiceReloadStatus{Enabled: false, Running: false}
|
||||
}
|
||||
|
||||
// UDPGW — same pattern
|
||||
// UDPGW — same pattern.
|
||||
stopUDPGW()
|
||||
startUDPGW(newCfg.UDPGW)
|
||||
if newCfg.UDPGW != nil {
|
||||
if err := startUDPGW(newCfg.UDPGW); err != nil {
|
||||
report.warnf("UDPGW failed to start: %v", err)
|
||||
report.Services["udpgw"] = ServiceReloadStatus{Enabled: true, Running: false, Listen: newCfg.UDPGW.Listen, Error: err.Error()}
|
||||
} else {
|
||||
report.Services["udpgw"] = ServiceReloadStatus{Enabled: true, Running: udpgwRunning(), Listen: newCfg.UDPGW.Listen}
|
||||
}
|
||||
} else {
|
||||
report.Services["udpgw"] = ServiceReloadStatus{Enabled: false, Running: false}
|
||||
}
|
||||
|
||||
// Xray — update stored config then restart/stop as needed
|
||||
// Xray — update stored config then restart/stop as needed.
|
||||
if newCfg.Xray != nil {
|
||||
xrayMgr.mu.Lock()
|
||||
xrayMgr.cfg = newCfg.Xray
|
||||
xrayMgr.mu.Unlock()
|
||||
if newCfg.Xray.Enabled {
|
||||
_ = xrayMgr.Restart()
|
||||
if err := xrayMgr.Restart(); err != nil {
|
||||
report.warnf("Xray failed to restart: %v", err)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
st := xrayMgr.Status()
|
||||
report.Services["xray"] = ServiceReloadStatus{Enabled: true, Running: st.Running, Error: st.Error}
|
||||
if !st.Running && st.Error == "" {
|
||||
report.Services["xray"] = ServiceReloadStatus{Enabled: true, Running: false, Error: "xray exited immediately; check logs"}
|
||||
}
|
||||
} else {
|
||||
_ = xrayMgr.Stop()
|
||||
report.Services["xray"] = ServiceReloadStatus{Enabled: false, Running: false}
|
||||
}
|
||||
} else {
|
||||
_ = xrayMgr.Stop()
|
||||
report.Services["xray"] = ServiceReloadStatus{Enabled: false, Running: false}
|
||||
}
|
||||
|
||||
setGlobalCfg(newCfg)
|
||||
return report
|
||||
}
|
||||
|
||||
func tlsForwarderList(forwarders []TLSForwarderConfig) string {
|
||||
addrs := make([]string, 0, len(forwarders))
|
||||
for _, f := range forwarders {
|
||||
if strings.TrimSpace(f.Listen) != "" {
|
||||
addrs = append(addrs, strings.TrimSpace(f.Listen))
|
||||
}
|
||||
}
|
||||
return strings.Join(addrs, ", ")
|
||||
}
|
||||
|
||||
93
install.sh
93
install.sh
@@ -35,23 +35,23 @@ case "$OS_ID" in
|
||||
ubuntu|debian|linuxmint)
|
||||
PKG_UPDATE="apt-get update -qq"
|
||||
PKG_INSTALL="DEBIAN_FRONTEND=noninteractive apt-get install -y"
|
||||
PKG_DEPS="curl wget git build-essential postgresql postgresql-contrib ca-certificates unzip openssh-client openssl"
|
||||
PKG_DEPS="curl wget git build-essential postgresql postgresql-contrib ca-certificates unzip openssh-client openssl iptables nftables"
|
||||
;;
|
||||
centos|rhel|rocky|almalinux)
|
||||
PKG_UPDATE="yum makecache -q"
|
||||
PKG_INSTALL="yum install -y"
|
||||
PKG_DEPS="curl wget git gcc make postgresql-server postgresql-contrib ca-certificates unzip openssh-clients openssl"
|
||||
PKG_DEPS="curl wget git gcc make postgresql-server postgresql-contrib ca-certificates unzip openssh-clients openssl iptables nftables"
|
||||
;;
|
||||
fedora)
|
||||
PKG_UPDATE="dnf makecache -q"
|
||||
PKG_INSTALL="dnf install -y"
|
||||
PKG_DEPS="curl wget git gcc make postgresql-server postgresql-contrib ca-certificates unzip openssh-clients openssl"
|
||||
PKG_DEPS="curl wget git gcc make postgresql-server postgresql-contrib ca-certificates unzip openssh-clients openssl iptables nftables"
|
||||
;;
|
||||
*)
|
||||
warn "Unknown OS '$OS_ID' — attempting apt-get…"
|
||||
PKG_UPDATE="apt-get update -qq"
|
||||
PKG_INSTALL="DEBIAN_FRONTEND=noninteractive apt-get install -y"
|
||||
PKG_DEPS="curl wget git build-essential postgresql postgresql-contrib ca-certificates unzip openssh-client openssl"
|
||||
PKG_DEPS="curl wget git build-essential postgresql postgresql-contrib ca-certificates unzip openssh-client openssl iptables nftables"
|
||||
;;
|
||||
esac
|
||||
info " OS: $OS_ID"
|
||||
@@ -107,6 +107,11 @@ go build -ldflags="-s -w" -o "$INSTALL_DIR/sshpanel" .
|
||||
info " Binary: $INSTALL_DIR/sshpanel"
|
||||
cp -r "$SCRIPT_DIR/admin/"* "$INSTALL_DIR/admin/"
|
||||
info " Admin panel copied"
|
||||
if [[ -f "$SCRIPT_DIR/change_admin_password.sh" ]]; then
|
||||
cp "$SCRIPT_DIR/change_admin_password.sh" "$INSTALL_DIR/change_admin_password.sh"
|
||||
chmod 700 "$INSTALL_DIR/change_admin_password.sh"
|
||||
info " Admin password recovery script copied"
|
||||
fi
|
||||
|
||||
# ── 6. Xray binary ──────────────────────────────────────────────────────────
|
||||
info "[6/9] Downloading Xray-core…"
|
||||
@@ -230,7 +235,7 @@ GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};
|
||||
info " PostgreSQL database '${DB_NAME}' ready"
|
||||
|
||||
# ── 8. Config files ──────────────────────────────────────────────────────────
|
||||
info "[8/9] Generating config files…"
|
||||
info "[8/10] Generating config files…"
|
||||
|
||||
# Admin token
|
||||
ADMIN_TOKEN=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 48 || true)
|
||||
@@ -281,7 +286,6 @@ cat > "$INSTALL_DIR/config.json" <<EOF
|
||||
{
|
||||
"listen": "0.0.0.0:80",
|
||||
"extra_listen": ["0.0.0.0:8080"],
|
||||
"local_ssh_listen": "127.0.0.1:2222",
|
||||
"host_key_file": "${INSTALL_DIR}/ssh_host_rsa_key",
|
||||
"quiet": false,
|
||||
"admin_dir": "${INSTALL_DIR}/admin",
|
||||
@@ -333,18 +337,86 @@ EOF
|
||||
chmod 600 "$INSTALL_DIR/xray_config.json"
|
||||
info " VLESS UUID: ${UUID}"
|
||||
|
||||
# ── 9. Systemd service ───────────────────────────────────────────────────────
|
||||
info "[9/9] Creating systemd service '${SERVICE_NAME}'…"
|
||||
# ── 9. DNSTT DNS/53 redirect ─────────────────────────────────────────────────
|
||||
info "[9/10] Configuring DNSTT DNS redirect (UDP 53 -> 5300)…"
|
||||
cat > /usr/local/sbin/sshpanel-dnstt-redirect.sh <<'EOS'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
DNS_UPSTREAM="${DNS_UPSTREAM:-1.1.1.1}"
|
||||
DNSTT_PORT="${DNSTT_PORT:-5300}"
|
||||
|
||||
# Free port 53 on systemd-resolved based systems and keep outbound DNS working.
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now systemd-resolved.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -f /etc/resolv.conf
|
||||
printf 'nameserver %s\n' "$DNS_UPSTREAM" > /etc/resolv.conf
|
||||
|
||||
# Open DNS/UDP in common Linux firewalls when they are active.
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
ufw allow 53/udp >/dev/null 2>&1 || true
|
||||
fi
|
||||
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
||||
firewall-cmd --permanent --add-port=53/udp >/dev/null 2>&1 || true
|
||||
firewall-cmd --reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
add_iptables_rule() {
|
||||
local bin="$1" chain="$2"
|
||||
"$bin" -t nat -C "$chain" -p udp --dport 53 -j REDIRECT --to-ports "$DNSTT_PORT" 2>/dev/null \
|
||||
|| "$bin" -t nat -A "$chain" -p udp --dport 53 -j REDIRECT --to-ports "$DNSTT_PORT"
|
||||
}
|
||||
|
||||
if command -v iptables >/dev/null 2>&1; then
|
||||
add_iptables_rule iptables PREROUTING
|
||||
fi
|
||||
|
||||
if command -v ip6tables >/dev/null 2>&1; then
|
||||
add_iptables_rule ip6tables PREROUTING || true
|
||||
fi
|
||||
|
||||
# Fallback for minimal systems where only nft is present.
|
||||
if ! command -v iptables >/dev/null 2>&1 && command -v nft >/dev/null 2>&1; then
|
||||
nft add table inet sshpanel_nat 2>/dev/null || true
|
||||
nft 'add chain inet sshpanel_nat prerouting { type nat hook prerouting priority dstnat; policy accept; }' 2>/dev/null || true
|
||||
nft list chain inet sshpanel_nat prerouting 2>/dev/null | grep -q "udp dport 53 redirect to :$DNSTT_PORT" \
|
||||
|| nft add rule inet sshpanel_nat prerouting udp dport 53 redirect to :"$DNSTT_PORT"
|
||||
fi
|
||||
EOS
|
||||
chmod +x /usr/local/sbin/sshpanel-dnstt-redirect.sh
|
||||
|
||||
cat > /etc/systemd/system/sshpanel-dnstt-redirect.service <<'EOF'
|
||||
[Unit]
|
||||
Description=SSH Panel DNSTT DNS redirect (UDP 53 to 5300)
|
||||
After=network.target
|
||||
Before=sshpanel.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/sshpanel-dnstt-redirect.sh
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now sshpanel-dnstt-redirect.service || warn "DNSTT DNS redirect service failed; check: journalctl -u sshpanel-dnstt-redirect -e"
|
||||
info " DNSTT DNS redirect installed: UDP 53 -> 5300"
|
||||
|
||||
# ── 10. Systemd service ──────────────────────────────────────────────────────
|
||||
info "[10/10] Creating systemd service '${SERVICE_NAME}'…"
|
||||
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
|
||||
[Unit]
|
||||
Description=SSH Panel + Xray-core Server
|
||||
After=network.target postgresql.service
|
||||
Wants=postgresql.service
|
||||
After=network.target postgresql.service sshpanel-dnstt-redirect.service
|
||||
Wants=postgresql.service sshpanel-dnstt-redirect.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=${INSTALL_DIR}
|
||||
EnvironmentFile=${INSTALL_DIR}/.env
|
||||
Environment=PANEL_LOG_FILE=${INSTALL_DIR}/logs/panel.log
|
||||
ExecStart=${INSTALL_DIR}/sshpanel -config ${INSTALL_DIR}/config.json
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
@@ -371,6 +443,7 @@ echo -e " Server IP : ${YELLOW}${SERVER_IP}${NC}"
|
||||
echo -e " SSH ports : 80, 8080 (HTTP-injected SSH)"
|
||||
echo -e " VLESS port : 10086"
|
||||
echo -e " VLESS UUID : ${YELLOW}${UUID}${NC}"
|
||||
echo -e " DNSTT DNS : UDP 53 redirects to local UDP 5300"
|
||||
echo ""
|
||||
echo -e " Admin panel : ${YELLOW}http://${SERVER_IP}:9090${NC}"
|
||||
echo -e " Admin login : ${YELLOW}admin${NC}"
|
||||
|
||||
35
main.go
35
main.go
@@ -65,9 +65,8 @@ type Config struct {
|
||||
// "[::]:80", "[2001:db8::20]:8080". Empty slice means no
|
||||
// additional listeners.
|
||||
ExtraListen []string `json:"extra_listen"`
|
||||
// Optional: local-only raw SSH listener for other daemons (e.g. DNSTT upstream)
|
||||
// Set to "127.0.0.1:2222" or similar. Leave empty to disable.
|
||||
LocalSSHListen string `json:"local_ssh_listen"`
|
||||
// Legacy compatibility only. DragonCore no longer starts a local raw SSH listener.
|
||||
LocalSSHListen string `json:"local_ssh_listen,omitempty"`
|
||||
HostKeyFile string `json:"host_key_file"`
|
||||
Quiet bool `json:"quiet"`
|
||||
|
||||
@@ -1088,6 +1087,7 @@ func startAdminAPI(store *Store, addr string, adminDir string) {
|
||||
|
||||
// Superadmin-only: server stats + DNSTT
|
||||
mux.Handle("/api/stats", saSession(http.HandlerFunc(handleStats)))
|
||||
mux.Handle("/api/system/logs", saSession(http.HandlerFunc(handleSystemLogs)))
|
||||
mux.Handle("/api/dnstt", saSession(http.HandlerFunc(handleDnsttStats)))
|
||||
mux.Handle("/api/dnstt/logs", saSession(http.HandlerFunc(handleDnsttLogs)))
|
||||
|
||||
@@ -2497,31 +2497,32 @@ func main() {
|
||||
// Initialise default per-connection bandwidth limits.
|
||||
setDefaultLimits(cfg.DefaultLimitMbpsUp, cfg.DefaultLimitMbpsDown)
|
||||
|
||||
// Start the integrated DNSTT and UDPGW if configured.
|
||||
startDNSTT(cfg.DNSTT, sshConfig)
|
||||
startUDPGW(cfg.UDPGW)
|
||||
|
||||
// Initialise listener pools (used for initial startup and hot-reload alike).
|
||||
publicPool = newListenerPool(serveHTTP80)
|
||||
localPool = newListenerPool(serveRawSSH)
|
||||
tlsPool = newTLSListenerPool()
|
||||
|
||||
for _, msg := range normalizeRuntimePorts(cfg) {
|
||||
log.Printf("startup config fallback: %s", msg)
|
||||
}
|
||||
|
||||
// Start the integrated DNSTT and UDPGW if configured. Startup errors are logged
|
||||
// but do not crash the panel; the admin UI exposes the logs and service status.
|
||||
if err := startDNSTT(cfg.DNSTT, sshConfig); err != nil {
|
||||
log.Printf("dnstt auto-start failed: %v", err)
|
||||
}
|
||||
if err := startUDPGW(cfg.UDPGW); err != nil {
|
||||
log.Printf("udpgw auto-start failed: %v", err)
|
||||
}
|
||||
|
||||
// Start public SSH listeners (listen + extra_listen).
|
||||
publicAddrs := append([]string{cfg.Listen}, cfg.ExtraListen...)
|
||||
for _, e := range publicPool.Sync(publicAddrs) {
|
||||
log.Fatalf("failed to start listener: %v", e)
|
||||
}
|
||||
|
||||
// Start local raw SSH listener if configured.
|
||||
if cfg.LocalSSHListen != "" {
|
||||
for _, e := range localPool.Sync([]string{cfg.LocalSSHListen}) {
|
||||
log.Fatalf("failed to start local SSH listener: %v", e)
|
||||
}
|
||||
log.Printf("failed to start listener: %v", e)
|
||||
}
|
||||
|
||||
// Start TLS forwarder listeners if configured.
|
||||
for _, e := range tlsPool.Sync(cfg.TLSForwarders) {
|
||||
log.Fatalf("failed to start TLS listener: %v", e)
|
||||
log.Printf("failed to start TLS listener: %v", e)
|
||||
}
|
||||
|
||||
// Print user counts once at startup.
|
||||
|
||||
@@ -100,6 +100,8 @@ func serverConfigPost(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
globalCfgMu.RUnlock()
|
||||
|
||||
portWarnings := normalizeRuntimePorts(&newCfg)
|
||||
|
||||
out, err := json.MarshalIndent(newCfg, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, "marshal error", http.StatusInternalServerError)
|
||||
@@ -110,8 +112,13 @@ func serverConfigPost(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply all changes live — no restart needed.
|
||||
applyFullConfigReload(&newCfg)
|
||||
// Apply all changes live and return health checks to the panel.
|
||||
report := applyFullConfigReload(&newCfg)
|
||||
if len(portWarnings) > 0 {
|
||||
report.Warnings = append(portWarnings, report.Warnings...)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(report)
|
||||
}
|
||||
|
||||
116
system_logs_api.go
Normal file
116
system_logs_api.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const defaultPanelLogFile = "/opt/sshpanel/logs/panel.log"
|
||||
|
||||
type systemLogsResponse struct {
|
||||
Source string `json:"source"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Lines []string `json:"lines"`
|
||||
}
|
||||
|
||||
func handleSystemLogs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
limit := 300
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("lines")); raw != "" {
|
||||
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
if limit > 2000 {
|
||||
limit = 2000
|
||||
}
|
||||
|
||||
source := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("source")))
|
||||
if source == "" {
|
||||
source = "panel"
|
||||
}
|
||||
|
||||
resp := systemLogsResponse{Source: source, Lines: []string{}}
|
||||
switch source {
|
||||
case "dnstt":
|
||||
resp.Lines = limitLines(getDNSTTLogLines(), limit)
|
||||
case "xray":
|
||||
resp.Lines = limitLines(xrayLogBuf.snapshot(), limit)
|
||||
default:
|
||||
resp.Source = "panel"
|
||||
path := strings.TrimSpace(os.Getenv("PANEL_LOG_FILE"))
|
||||
if path == "" {
|
||||
path = defaultPanelLogFile
|
||||
}
|
||||
resp.Path = path
|
||||
lines, err := tailTextFile(path, limit)
|
||||
if err != nil {
|
||||
lines = []string{"unable to read " + path + ": " + err.Error()}
|
||||
}
|
||||
resp.Lines = lines
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func tailTextFile(path string, limit int) ([]string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 300
|
||||
}
|
||||
ring := make([]string, limit)
|
||||
count := 0
|
||||
scanner := bufio.NewScanner(f)
|
||||
buf := make([]byte, 0, 64*1024)
|
||||
scanner.Buffer(buf, 1024*1024)
|
||||
for scanner.Scan() {
|
||||
ring[count%limit] = scanner.Text()
|
||||
count++
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
outLen := count
|
||||
if outLen > limit {
|
||||
outLen = limit
|
||||
}
|
||||
out := make([]string, 0, outLen)
|
||||
start := 0
|
||||
if count > limit {
|
||||
start = count % limit
|
||||
}
|
||||
for i := 0; i < outLen; i++ {
|
||||
out = append(out, ring[(start+i)%limit])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func limitLines(lines []string, limit int) []string {
|
||||
if len(lines) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
if limit <= 0 || len(lines) <= limit {
|
||||
out := make([]string, len(lines))
|
||||
copy(out, lines)
|
||||
return out
|
||||
}
|
||||
out := make([]string, limit)
|
||||
copy(out, lines[len(lines)-limit:])
|
||||
return out
|
||||
}
|
||||
@@ -40,23 +40,30 @@ func stopUDPGW() {
|
||||
}
|
||||
}
|
||||
|
||||
func udpgwRunning() bool {
|
||||
udpgwMu.Lock()
|
||||
defer udpgwMu.Unlock()
|
||||
return udpgwLn != nil
|
||||
}
|
||||
|
||||
// startUDPGW starts the integrated UDP gateway if cfg is non‑nil and
|
||||
// cfg.Listen is non‑empty. It applies default values to any zero
|
||||
// configuration fields and converts duration strings to time.Duration.
|
||||
// The server runs in a goroutine; any fatal errors are logged and
|
||||
// prevent the gateway from starting, but do not terminate the main
|
||||
// process.
|
||||
func startUDPGW(cfg *UDPGWConfig) {
|
||||
func startUDPGW(cfg *UDPGWConfig) error {
|
||||
if cfg == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
// Default the listen address to the standalone default (0.0.0.0:7400) if
|
||||
// unspecified. This matches the behaviour of the original
|
||||
// badvpn-udpgw program, which listens on all interfaces by default.
|
||||
listenAddr := cfg.Listen
|
||||
if listenAddr == "" {
|
||||
listenAddr = "0.0.0.0:7400"
|
||||
listenAddr = defaultUDPGWListen
|
||||
}
|
||||
cfg.Listen = listenAddr
|
||||
// Apply defaults for numeric fields if zero.
|
||||
c := &internalUDPGWConfig{}
|
||||
c.listen = listenAddr
|
||||
@@ -135,7 +142,7 @@ func startUDPGW(cfg *UDPGWConfig) {
|
||||
ln, err := net.Listen("tcp", c.listen)
|
||||
if err != nil {
|
||||
log.Printf("udpgw: listen failed on %s: %v", c.listen, err)
|
||||
return
|
||||
return fmt.Errorf("udpgw: listen failed on %s: %w", c.listen, err)
|
||||
}
|
||||
|
||||
// Register as the active listener so stopUDPGW can close it.
|
||||
@@ -162,6 +169,7 @@ func startUDPGW(cfg *UDPGWConfig) {
|
||||
go handleUDPGWClient(conn, c)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// internalUDPGWConfig mirrors the exported UDPGWConfig but with
|
||||
|
||||
93
update.sh
93
update.sh
@@ -23,7 +23,7 @@ echo -e "${GREEN} SSH Panel · Updater ${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════${NC}\n"
|
||||
|
||||
# ── 1. Pre-flight checks ──────────────────────────────────────────────────────
|
||||
info "[1/5] Pre-flight checks…"
|
||||
info "[1/6] Pre-flight checks…"
|
||||
|
||||
[[ -d "$INSTALL_DIR" ]] || error "Install dir $INSTALL_DIR not found — run install.sh first."
|
||||
[[ -f "$INSTALL_DIR/.env" ]] || error "$INSTALL_DIR/.env not found — run install.sh first."
|
||||
@@ -34,7 +34,7 @@ info " Source dir : $SCRIPT_DIR"
|
||||
info " Go version : $GO_VERSION"
|
||||
|
||||
# ── 2. Go toolchain ───────────────────────────────────────────────────────────
|
||||
info "[2/5] Checking Go toolchain…"
|
||||
info "[2/6] Checking Go toolchain…"
|
||||
|
||||
NEED_GO=true
|
||||
if command -v go &>/dev/null; then
|
||||
@@ -67,7 +67,7 @@ export PATH=$PATH:/usr/local/go/bin
|
||||
go version
|
||||
|
||||
# ── 3. Build new binary ───────────────────────────────────────────────────────
|
||||
info "[3/5] Building new sshpanel binary…"
|
||||
info "[3/6] Building new sshpanel binary…"
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
export GOPATH=/tmp/gopath_sshpanel
|
||||
@@ -77,7 +77,7 @@ go build -ldflags="-s -w" -o /tmp/sshpanel_new .
|
||||
info " Build complete."
|
||||
|
||||
# ── 4. Apply update ───────────────────────────────────────────────────────────
|
||||
info "[4/5] Applying update…"
|
||||
info "[4/6] Applying update…"
|
||||
|
||||
# Stop the service
|
||||
if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
|
||||
@@ -103,6 +103,11 @@ info " Binary updated."
|
||||
mkdir -p "$INSTALL_DIR/admin"
|
||||
cp -r "$SCRIPT_DIR/admin/"* "$INSTALL_DIR/admin/"
|
||||
info " Admin panel updated."
|
||||
if [[ -f "$SCRIPT_DIR/change_admin_password.sh" ]]; then
|
||||
cp "$SCRIPT_DIR/change_admin_password.sh" "$INSTALL_DIR/change_admin_password.sh"
|
||||
chmod 700 "$INSTALL_DIR/change_admin_password.sh"
|
||||
info " Admin password recovery script updated."
|
||||
fi
|
||||
|
||||
# Ensure banner file exists (new in this version)
|
||||
if [[ ! -f "$INSTALL_DIR/banner.txt" ]]; then
|
||||
@@ -132,6 +137,18 @@ PYEOF
|
||||
info " Added banner_file to config.json"
|
||||
fi
|
||||
|
||||
# Remove legacy local_ssh_listen. DragonCore now handles DNSTT in-process.
|
||||
python3 - "$CFG" << 'PYEOF'
|
||||
import json, sys
|
||||
path = sys.argv[1]
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
changed = d.pop('local_ssh_listen', None) is not None
|
||||
if changed:
|
||||
with open(path, 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
PYEOF
|
||||
|
||||
# Fix routing: remove geoip:private rules that require geoip.dat from xray_config.json
|
||||
XCFG="$INSTALL_DIR/xray_config.json"
|
||||
if [[ -f "$XCFG" ]]; then
|
||||
@@ -158,8 +175,72 @@ PYEOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 5. Restart service ────────────────────────────────────────────────────────
|
||||
info "[5/5] Restarting service…"
|
||||
# ── 5. DNSTT DNS/53 redirect ─────────────────────────────────────────────────
|
||||
info "[5/6] Ensuring DNSTT DNS redirect (UDP 53 -> 5300)…"
|
||||
cat > /usr/local/sbin/sshpanel-dnstt-redirect.sh <<'EOS'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
DNS_UPSTREAM="${DNS_UPSTREAM:-1.1.1.1}"
|
||||
DNSTT_PORT="${DNSTT_PORT:-5300}"
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now systemd-resolved.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -f /etc/resolv.conf
|
||||
printf 'nameserver %s\n' "$DNS_UPSTREAM" > /etc/resolv.conf
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
ufw allow 53/udp >/dev/null 2>&1 || true
|
||||
fi
|
||||
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
||||
firewall-cmd --permanent --add-port=53/udp >/dev/null 2>&1 || true
|
||||
firewall-cmd --reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
add_iptables_rule() {
|
||||
local bin="$1" chain="$2"
|
||||
"$bin" -t nat -C "$chain" -p udp --dport 53 -j REDIRECT --to-ports "$DNSTT_PORT" 2>/dev/null \
|
||||
|| "$bin" -t nat -A "$chain" -p udp --dport 53 -j REDIRECT --to-ports "$DNSTT_PORT"
|
||||
}
|
||||
if command -v iptables >/dev/null 2>&1; then
|
||||
add_iptables_rule iptables PREROUTING
|
||||
fi
|
||||
if command -v ip6tables >/dev/null 2>&1; then
|
||||
add_iptables_rule ip6tables PREROUTING || true
|
||||
fi
|
||||
if ! command -v iptables >/dev/null 2>&1 && command -v nft >/dev/null 2>&1; then
|
||||
nft add table inet sshpanel_nat 2>/dev/null || true
|
||||
nft 'add chain inet sshpanel_nat prerouting { type nat hook prerouting priority dstnat; policy accept; }' 2>/dev/null || true
|
||||
nft list chain inet sshpanel_nat prerouting 2>/dev/null | grep -q "udp dport 53 redirect to :$DNSTT_PORT" \
|
||||
|| nft add rule inet sshpanel_nat prerouting udp dport 53 redirect to :"$DNSTT_PORT"
|
||||
fi
|
||||
EOS
|
||||
chmod +x /usr/local/sbin/sshpanel-dnstt-redirect.sh
|
||||
cat > /etc/systemd/system/sshpanel-dnstt-redirect.service <<'EOF'
|
||||
[Unit]
|
||||
Description=SSH Panel DNSTT DNS redirect (UDP 53 to 5300)
|
||||
After=network.target
|
||||
Before=sshpanel.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/sshpanel-dnstt-redirect.sh
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
mkdir -p /etc/systemd/system/sshpanel.service.d
|
||||
cat > /etc/systemd/system/sshpanel.service.d/override.conf <<EOF
|
||||
[Unit]
|
||||
Wants=sshpanel-dnstt-redirect.service
|
||||
After=sshpanel-dnstt-redirect.service
|
||||
|
||||
[Service]
|
||||
Environment=PANEL_LOG_FILE=${INSTALL_DIR}/logs/panel.log
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now sshpanel-dnstt-redirect.service || warn "DNSTT DNS redirect service failed; check: journalctl -u sshpanel-dnstt-redirect -e"
|
||||
|
||||
# ── 6. Restart service ────────────────────────────────────────────────────────
|
||||
info "[6/6] Restarting service…"
|
||||
|
||||
if $RESTART_NEEDED; then
|
||||
systemctl start "$SERVICE_NAME"
|
||||
|
||||
Reference in New Issue
Block a user