Make Hysteria UDP port selectable when 443 is busy.
Show an inline port field on the stopped card, a dedicated install port, and clearer bind-error hints so admins can switch to 8998/8443 without guessing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2708,6 +2708,13 @@ async def api_check_server(request: Request, server_id: int):
|
|||||||
for key in ('domain', 'email', 'site_url'):
|
for key in ('domain', 'email', 'site_url'):
|
||||||
if db_proto.get(key) not in (None, ''):
|
if db_proto.get(key) not in (None, ''):
|
||||||
merged[key] = db_proto[key]
|
merged[key] = db_proto[key]
|
||||||
|
if protocol_base(proto) == 'hysteria':
|
||||||
|
for key in ('domain', 'email'):
|
||||||
|
if db_proto.get(key) not in (None, '') and not merged.get(key):
|
||||||
|
merged[key] = db_proto[key]
|
||||||
|
# Keep a useful error even when merging preserved records
|
||||||
|
if db_proto.get('port') and not merged.get('port'):
|
||||||
|
merged['port'] = db_proto['port']
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
def should_preserve_saved_protocol(proto, result=None, err=None):
|
def should_preserve_saved_protocol(proto, result=None, err=None):
|
||||||
|
|||||||
@@ -156,33 +156,38 @@ class HysteriaManager:
|
|||||||
elif diag['error']:
|
elif diag['error']:
|
||||||
diag['error_summary'] = diag['error']
|
diag['error_summary'] = diag['error']
|
||||||
elif diag['exit_code'] not in (None, 0):
|
elif diag['exit_code'] not in (None, 0):
|
||||||
# Pull last meaningful log line
|
# Prefer bind/port conflicts over generic FATAL lines
|
||||||
|
bind_line = ''
|
||||||
last = ''
|
last = ''
|
||||||
for line in reversed((diag['recent_logs'] or '').splitlines()):
|
for raw in reversed((diag['recent_logs'] or '').splitlines()):
|
||||||
line = self._strip_ansi(line).strip()
|
line = self._strip_ansi(raw).strip()
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
# Prefer the FATAL / bind error line
|
# strip docker --timestamps prefix
|
||||||
if 'address already in use' in line.lower() or 'FATAL' in line or 'failed to load' in line.lower():
|
m_ts = re.match(r'^\d{4}-\d{2}-\d{2}T\S+\s+(.*)$', line)
|
||||||
|
if m_ts:
|
||||||
|
line = m_ts.group(1).strip()
|
||||||
|
low = line.lower()
|
||||||
|
if 'address already in use' in low and not bind_line:
|
||||||
|
bind_line = line
|
||||||
|
if ('FATAL' in line or 'failed to load' in low) and not last:
|
||||||
last = line
|
last = line
|
||||||
break
|
|
||||||
if not last:
|
if not last:
|
||||||
last = line
|
last = line
|
||||||
if last:
|
pick = bind_line or last
|
||||||
# strip docker timestamp prefix if present
|
if pick and 'address already in use' in pick.lower():
|
||||||
if ' ' in last and (last[0].isdigit() or last[0] == '2'):
|
m = re.search(r'listen udp :(\d+)', pick, re.I) or re.search(r':(\d+):\s*bind', pick)
|
||||||
last = last.split(' ', 1)[-1]
|
p = m.group(1) if m else '?'
|
||||||
last = self._strip_ansi(last)
|
if p == '?':
|
||||||
if 'address already in use' in last.lower():
|
m2 = re.search(r'udp\s*:(\d+)', pick, re.I)
|
||||||
m = re.search(r'listen udp :(\d+)', last, re.I) or re.search(r':(\d+):\s*bind', last)
|
p = m2.group(1) if m2 else '?'
|
||||||
p = m.group(1) if m else '?'
|
diag['error_summary'] = (
|
||||||
diag['error_summary'] = (
|
f'UDP port {p} already in use — change port below (try 8998 or 8443)'
|
||||||
f'UDP port {p} already in use — open Settings and choose another port'
|
)
|
||||||
)
|
diag['port_busy'] = True
|
||||||
else:
|
elif pick:
|
||||||
# keep message short for badge
|
short = re.sub(r'\s+', ' ', pick)[:160]
|
||||||
short = re.sub(r'\s+', ' ', last)[:160]
|
diag['error_summary'] = f"exit {diag['exit_code']}: {short}"
|
||||||
diag['error_summary'] = f"exit {diag['exit_code']}: {short}"
|
|
||||||
else:
|
else:
|
||||||
diag['error_summary'] = f"exited with code {diag['exit_code']}"
|
diag['error_summary'] = f"exited with code {diag['exit_code']}"
|
||||||
elif diag['status'] and diag['status'] != 'running':
|
elif diag['status'] and diag['status'] != 'running':
|
||||||
@@ -244,6 +249,7 @@ class HysteriaManager:
|
|||||||
'exit_code': diag.get('exit_code'),
|
'exit_code': diag.get('exit_code'),
|
||||||
'container_status': diag.get('status') or '',
|
'container_status': diag.get('status') or '',
|
||||||
'recent_logs': diag.get('recent_logs') or '',
|
'recent_logs': diag.get('recent_logs') or '',
|
||||||
|
'port_busy': bool(diag.get('port_busy')),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ===================== HELPERS =====================
|
# ===================== HELPERS =====================
|
||||||
@@ -460,8 +466,10 @@ iptables -C INPUT -p udp --dport {port} -j ACCEPT 2>/dev/null || iptables -I INP
|
|||||||
|
|
||||||
def get_settings(self):
|
def get_settings(self):
|
||||||
meta = self._ensure_secrets(self._read_metadata())
|
meta = self._ensure_secrets(self._read_metadata())
|
||||||
|
port = int(meta.get('port') or self.DEFAULT_PORT)
|
||||||
return {
|
return {
|
||||||
'port': int(meta.get('port') or self.DEFAULT_PORT),
|
'port': port,
|
||||||
|
'suggested_port': 8998 if port in (443, 80) else port,
|
||||||
'domain': meta.get('domain') or '',
|
'domain': meta.get('domain') or '',
|
||||||
'email': meta.get('email') or '',
|
'email': meta.get('email') or '',
|
||||||
'protocol': self.protocol,
|
'protocol': self.protocol,
|
||||||
|
|||||||
+83
-17
@@ -574,7 +574,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="installForm">
|
<div id="installForm">
|
||||||
<div class="form-group">
|
<div class="form-group" id="installPortGroup">
|
||||||
<label class="form-label" id="installPortLabel">{{ _('port') }}</label>
|
<label class="form-label" id="installPortLabel">{{ _('port') }}</label>
|
||||||
<input class="form-input" type="number" id="installPort" value="55424" min="1" max="65535">
|
<input class="form-input" type="number" id="installPort" value="55424" min="1" max="65535">
|
||||||
<div class="form-hint" id="installPortHint">{{ _('port_default_hint') }}</div>
|
<div class="form-hint" id="installPortHint">{{ _('port_default_hint') }}</div>
|
||||||
@@ -681,6 +681,11 @@
|
|||||||
|
|
||||||
<div id="hysteriaOptions"
|
<div id="hysteriaOptions"
|
||||||
style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
|
style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ _('port') }} (UDP) *</label>
|
||||||
|
<input class="form-input" type="number" id="installHysteriaPort" value="8998" min="1" max="65535">
|
||||||
|
<div class="form-hint">{{ _('hysteria_port_hint') }}</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">{{ _('hysteria_domain') }}</label>
|
<label class="form-label">{{ _('hysteria_domain') }}</label>
|
||||||
<input class="form-input" type="text" id="installHysteriaDomain" placeholder="vpn.example.com" oninput="updateHysteriaDnsHint()">
|
<input class="form-input" type="text" id="installHysteriaDomain" placeholder="vpn.example.com" oninput="updateHysteriaDnsHint()">
|
||||||
@@ -1632,15 +1637,16 @@
|
|||||||
}
|
}
|
||||||
} else if (info.container_exists) {
|
} else if (info.container_exists) {
|
||||||
installedProtocols[proto] = true;
|
installedProtocols[proto] = true;
|
||||||
const errText = (info.error || '').trim();
|
const errText = String(info.error || '').trim();
|
||||||
|
const portBusy = !!info.port_busy || /address already in use|UDP port \d+ already in use|already in use/i.test(errText);
|
||||||
statusEl.innerHTML = errText
|
statusEl.innerHTML = errText
|
||||||
? `<span class="badge badge-danger"><span class="badge-dot"></span> ${_('stop')}: ${escapeHtml(errText.slice(0, 80))}</span>`
|
? `<span class="badge badge-danger" style="max-width:100%; white-space:normal; text-align:left; line-height:1.35;"><span class="badge-dot"></span> ${_('stop')}: ${escapeHtml(errText.slice(0, 120))}</span>`
|
||||||
: `<span class="badge badge-danger"><span class="badge-dot"></span> ${_('stop')}</span>`;
|
: `<span class="badge badge-danger"><span class="badge-dot"></span> ${_('stop')}</span>`;
|
||||||
if (infoEl && infoGrid) {
|
if (infoEl && infoGrid) {
|
||||||
infoEl.classList.remove('hidden');
|
infoEl.classList.remove('hidden');
|
||||||
let grid = '';
|
let grid = '';
|
||||||
if (info.port) {
|
if (info.port) {
|
||||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}</span></div>`;
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}/UDP</span></div>`;
|
||||||
}
|
}
|
||||||
if (info.container_status) {
|
if (info.container_status) {
|
||||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('container_status')}</span><span class="protocol-info-value">${escapeHtml(String(info.container_status))}</span></div>`;
|
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('container_status')}</span><span class="protocol-info-value">${escapeHtml(String(info.container_status))}</span></div>`;
|
||||||
@@ -1651,6 +1657,26 @@
|
|||||||
if (errText) {
|
if (errText) {
|
||||||
grid += `<div class="protocol-info-item" style="grid-column:1/-1;"><span class="protocol-info-label">${_('error')}</span><span class="protocol-info-value" style="color:var(--danger); word-break:break-word;">${escapeHtml(errText)}</span></div>`;
|
grid += `<div class="protocol-info-item" style="grid-column:1/-1;"><span class="protocol-info-label">${_('error')}</span><span class="protocol-info-value" style="color:var(--danger); word-break:break-word;">${escapeHtml(errText)}</span></div>`;
|
||||||
}
|
}
|
||||||
|
// Inline port picker — always visible when Hysteria is stopped so admin can fix bind conflicts
|
||||||
|
if (protoBase(proto) === 'hysteria') {
|
||||||
|
const curPort = parseInt(info.port, 10) || 443;
|
||||||
|
const suggest = (curPort === 443 || portBusy) ? 8998 : curPort;
|
||||||
|
grid += `
|
||||||
|
<div class="protocol-info-item" style="grid-column:1/-1; display:block; padding-top:8px;">
|
||||||
|
<span class="protocol-info-label" style="display:block; margin-bottom:6px;">${_('hysteria_change_port')} (UDP)</span>
|
||||||
|
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
||||||
|
<input class="form-input" type="number" id="hysteriaInlinePort-${protoDomKey(proto)}"
|
||||||
|
value="${suggest}" min="1" max="65535" style="width:120px; flex:0 0 auto;">
|
||||||
|
<button class="btn btn-primary btn-sm" type="button"
|
||||||
|
onclick="applyHysteriaPort('${proto}', document.getElementById('hysteriaInlinePort-${protoDomKey(proto)}').value)">
|
||||||
|
${_('save')} & ${_('start_btn')}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" type="button" onclick="openHysteriaSettings('${proto}')">${_('hysteria_settings_title')}</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-hint" style="margin-top:6px;">${_('hysteria_port_change_hint')}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
infoGrid.innerHTML = grid;
|
infoGrid.innerHTML = grid;
|
||||||
}
|
}
|
||||||
if (isService) {
|
if (isService) {
|
||||||
@@ -1965,6 +1991,7 @@
|
|||||||
const portInput = document.getElementById('installPort');
|
const portInput = document.getElementById('installPort');
|
||||||
const portLabel = document.getElementById('installPortLabel');
|
const portLabel = document.getElementById('installPortLabel');
|
||||||
const portHint = document.getElementById('installPortHint');
|
const portHint = document.getElementById('installPortHint');
|
||||||
|
const portGroup = document.getElementById('installPortGroup');
|
||||||
const telemtOpts = document.getElementById('telemtOptions');
|
const telemtOpts = document.getElementById('telemtOptions');
|
||||||
const socks5Opts = document.getElementById('socks5Options');
|
const socks5Opts = document.getElementById('socks5Options');
|
||||||
const adguardOpts = document.getElementById('adguardOptions');
|
const adguardOpts = document.getElementById('adguardOptions');
|
||||||
@@ -1976,6 +2003,7 @@
|
|||||||
adguardOpts.style.display = 'none';
|
adguardOpts.style.display = 'none';
|
||||||
nginxOpts.style.display = 'none';
|
nginxOpts.style.display = 'none';
|
||||||
hysteriaOpts.style.display = 'none';
|
hysteriaOpts.style.display = 'none';
|
||||||
|
if (portGroup) portGroup.style.display = '';
|
||||||
|
|
||||||
if (base === 'dns') {
|
if (base === 'dns') {
|
||||||
portLabel.textContent = _('port') + ' (Internal)';
|
portLabel.textContent = _('port') + ' (Internal)';
|
||||||
@@ -2013,11 +2041,17 @@
|
|||||||
nginxOpts.style.display = 'block';
|
nginxOpts.style.display = 'block';
|
||||||
updateNginxDnsHint();
|
updateNginxDnsHint();
|
||||||
} else if (base === 'hysteria') {
|
} else if (base === 'hysteria') {
|
||||||
portLabel.textContent = _('port') + ' (UDP)';
|
// Port is chosen inside hysteriaOptions (dedicated UDP field)
|
||||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 8998) : '8998';
|
if (portGroup) portGroup.style.display = 'none';
|
||||||
|
const suggested = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 8998) : '8998';
|
||||||
|
portInput.value = suggested;
|
||||||
portInput.disabled = false;
|
portInput.disabled = false;
|
||||||
portHint.textContent = currentInstallAnother ? _('port_next_instance_hint') : _('hysteria_port_hint');
|
|
||||||
hysteriaOpts.style.display = 'block';
|
hysteriaOpts.style.display = 'block';
|
||||||
|
const hyPort = document.getElementById('installHysteriaPort');
|
||||||
|
if (hyPort) {
|
||||||
|
hyPort.value = suggested;
|
||||||
|
hyPort.oninput = () => { portInput.value = hyPort.value; };
|
||||||
|
}
|
||||||
updateHysteriaDnsHint();
|
updateHysteriaDnsHint();
|
||||||
} else {
|
} else {
|
||||||
portLabel.textContent = _('port') + ' (UDP)';
|
portLabel.textContent = _('port') + ' (UDP)';
|
||||||
@@ -2086,6 +2120,10 @@
|
|||||||
params.nginx_domain = document.getElementById('installNginxDomain').value.trim();
|
params.nginx_domain = document.getElementById('installNginxDomain').value.trim();
|
||||||
params.nginx_email = document.getElementById('installNginxEmail').value.trim();
|
params.nginx_email = document.getElementById('installNginxEmail').value.trim();
|
||||||
} else if (protoBase(currentInstallProto) === 'hysteria') {
|
} else if (protoBase(currentInstallProto) === 'hysteria') {
|
||||||
|
const hyPortEl = document.getElementById('installHysteriaPort');
|
||||||
|
if (hyPortEl && hyPortEl.value) {
|
||||||
|
params.port = hyPortEl.value;
|
||||||
|
}
|
||||||
params.hysteria_domain = document.getElementById('installHysteriaDomain').value.trim();
|
params.hysteria_domain = document.getElementById('installHysteriaDomain').value.trim();
|
||||||
params.hysteria_email = document.getElementById('installHysteriaEmail').value.trim();
|
params.hysteria_email = document.getElementById('installHysteriaEmail').value.trim();
|
||||||
}
|
}
|
||||||
@@ -2262,22 +2300,50 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== Hysteria Settings (port) ==========
|
// ========== Hysteria Settings (port) ==========
|
||||||
async function openHysteriaSettings(proto = 'hysteria') {
|
async function applyHysteriaPort(proto, portValue) {
|
||||||
|
const port = parseInt(portValue, 10);
|
||||||
|
if (!port || port < 1 || port > 65535) {
|
||||||
|
showToast(_('error') + ': invalid port', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (port === 80) {
|
||||||
|
showToast(_('error') + ': port 80 reserved', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings?protocol=${encodeURIComponent(proto)}`);
|
showToast(_('saving') || 'Saving...', 'info');
|
||||||
document.getElementById('hysteriaSetProto').value = proto;
|
const res = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings`, 'POST', {
|
||||||
document.getElementById('hysteriaSettingsTitle').textContent =
|
protocol: proto || 'hysteria', port,
|
||||||
`${_('hysteria_settings_title')} — ${getProtoTitle(proto)}`;
|
});
|
||||||
document.getElementById('hysteriaSetDomain').value = data.domain || '';
|
showToast(res.message || _('hysteria_settings_saved'), 'success');
|
||||||
// Prefer live/status port, then settings, then 8998
|
setTimeout(() => checkServer(), 800);
|
||||||
const livePort = (currentProtocolStatus[proto] || {}).port;
|
|
||||||
document.getElementById('hysteriaSetPort').value = livePort || data.port || 8998;
|
|
||||||
openModal('hysteriaSettingsModal');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(_('error') + ': ' + err.message, 'error');
|
showToast(_('error') + ': ' + err.message, 'error');
|
||||||
|
// Still open settings so admin can try another port
|
||||||
|
openHysteriaSettings(proto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openHysteriaSettings(proto = 'hysteria') {
|
||||||
|
const live = currentProtocolStatus[proto] || {};
|
||||||
|
const livePort = live.port || 443;
|
||||||
|
const suggest = (parseInt(livePort, 10) === 443) ? 8998 : (livePort || 8998);
|
||||||
|
document.getElementById('hysteriaSetProto').value = proto;
|
||||||
|
document.getElementById('hysteriaSettingsTitle').textContent =
|
||||||
|
`${_('hysteria_settings_title')} — ${getProtoTitle(proto)}`;
|
||||||
|
document.getElementById('hysteriaSetDomain').value = live.domain || '';
|
||||||
|
document.getElementById('hysteriaSetPort').value = suggest;
|
||||||
|
openModal('hysteriaSettingsModal');
|
||||||
|
// Enrich from server when possible (don't block UI if SSH is slow/fails)
|
||||||
|
try {
|
||||||
|
const data = await apiCall(`/api/servers/${SERVER_ID}/hysteria/settings?protocol=${encodeURIComponent(proto)}`);
|
||||||
|
if (data.domain) document.getElementById('hysteriaSetDomain').value = data.domain;
|
||||||
|
const p = parseInt(data.port, 10);
|
||||||
|
if (p && p !== 443) document.getElementById('hysteriaSetPort').value = p;
|
||||||
|
else if (p === 443) document.getElementById('hysteriaSetPort').value = 8998;
|
||||||
|
} catch (_) { /* modal already open with local values */ }
|
||||||
|
}
|
||||||
|
|
||||||
async function saveHysteriaSettings() {
|
async function saveHysteriaSettings() {
|
||||||
const btn = document.getElementById('hysteriaSaveBtn');
|
const btn = document.getElementById('hysteriaSaveBtn');
|
||||||
const text = document.getElementById('hysteriaSaveBtnText');
|
const text = document.getElementById('hysteriaSaveBtnText');
|
||||||
|
|||||||
Reference in New Issue
Block a user