Add optional Mieru (mita v3.28.0) install per server with mierus:// share links.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -277,6 +277,9 @@ GitHub Actions workflows in `.github/workflows/`:
|
||||
|
||||
## 📋 Fix / changelog (this fork)
|
||||
|
||||
### v2.6.4
|
||||
* **Mieru (mita v3.28.0)** — optional per-server install from [enfein/mieru](https://github.com/enfein/mieru): native Debian/RPM package, no Docker. Marketplace + server card, TCP port at install, user connections with `mierus://` share links. Pinned release **v3.28.0** (GitHub has no v2.8.0 tag).
|
||||
|
||||
### v2.6.3
|
||||
* **Client connection domain (AWG / WireGuard)** — set a DNS name per server (or per AWG/WG protocol) instead of IP in client configs (`Endpoint = domain:port`). Survives server IP changes — update the A-record only. UI on server list, server page, and when adding a server.
|
||||
* **Legacy `data.json` → PostgreSQL** — import normalizes old backups (UUIDs, duplicate usernames/tokens, string `server_info`); failed auto-import no longer bricks panel startup; JSON restore returns clear errors.
|
||||
|
||||
@@ -103,7 +103,7 @@ else:
|
||||
application_path = os.path.dirname(__file__)
|
||||
|
||||
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
||||
CURRENT_VERSION = "v2.6.3"
|
||||
CURRENT_VERSION = "v2.6.4"
|
||||
RELEASES_REPO_URL = repo_url()
|
||||
RELEASES_API_LATEST = api_latest_url()
|
||||
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
||||
@@ -1111,8 +1111,9 @@ async def wait_for_tunnel_url(provider: str, seconds: int = 20):
|
||||
return get_tunnel_status(provider)
|
||||
|
||||
|
||||
BASE_PROTOCOLS = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'dns', 'wireguard', 'socks5', 'adguard', 'nginx']
|
||||
BASE_PROTOCOLS = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'mieru', 'dns', 'wireguard', 'socks5', 'adguard', 'nginx']
|
||||
MULTI_INSTANCE_PROTOCOLS = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'socks5'}
|
||||
NON_DOCKER_PROTOCOLS = frozenset({'mieru'})
|
||||
|
||||
|
||||
def protocol_base(protocol: str) -> str:
|
||||
@@ -1154,6 +1155,7 @@ def protocol_display_name(protocol: str) -> str:
|
||||
'telemt': 'Telemt',
|
||||
'hysteria': 'Hysteria 2',
|
||||
'naiveproxy': 'NaiveProxy',
|
||||
'mieru': 'Mieru',
|
||||
'dns': 'AmneziaDNS',
|
||||
'wireguard': 'WireGuard',
|
||||
'socks5': 'SOCKS5',
|
||||
@@ -1175,6 +1177,7 @@ def protocol_container_name(protocol: str) -> Optional[str]:
|
||||
'telemt': 'telemt',
|
||||
'hysteria': 'amnezia-hysteria',
|
||||
'naiveproxy': 'amnezia-naiveproxy',
|
||||
'mieru': 'mita',
|
||||
'dns': 'amnezia-dns',
|
||||
'wireguard': 'amnezia-wireguard',
|
||||
'socks5': 'amnezia-socks5proxy',
|
||||
@@ -1220,6 +1223,9 @@ def get_protocol_manager(ssh, protocol: str):
|
||||
elif base == 'naiveproxy':
|
||||
from managers.naiveproxy_manager import NaiveProxyManager
|
||||
return NaiveProxyManager(ssh, protocol)
|
||||
elif base == 'mieru':
|
||||
from managers.mieru_manager import MieruManager
|
||||
return MieruManager(ssh, protocol)
|
||||
from managers.awg_manager import AWGManager
|
||||
return AWGManager(ssh)
|
||||
|
||||
@@ -1281,7 +1287,7 @@ def _manager_call(manager, method, protocol, *args, **kwargs):
|
||||
|
||||
|
||||
# Protocols that own VPN clients (can list/link connections)
|
||||
CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard', 'hysteria', 'naiveproxy', 'xui'}
|
||||
CLIENT_VPN_BASES = {'awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard', 'hysteria', 'naiveproxy', 'mieru', 'xui'}
|
||||
|
||||
|
||||
def generate_vpn_link(config_text):
|
||||
@@ -3466,6 +3472,12 @@ async def api_check_server(request: Request, server_id: int):
|
||||
merged[key] = db_proto[key]
|
||||
if db_proto.get('port') and not merged.get('port'):
|
||||
merged['port'] = db_proto['port']
|
||||
if protocol_base(proto) == 'mieru':
|
||||
for key in ('release',):
|
||||
if db_proto.get(key) not in (None, '') and not merged.get(key):
|
||||
merged[key] = db_proto[key]
|
||||
if db_proto.get('port') and not merged.get('port'):
|
||||
merged['port'] = db_proto['port']
|
||||
return merged
|
||||
|
||||
def should_preserve_saved_protocol(proto, result=None, err=None):
|
||||
@@ -3480,8 +3492,9 @@ async def api_check_server(request: Request, server_id: int):
|
||||
|
||||
def check_proto(proto):
|
||||
cname = protocol_container_name(proto)
|
||||
base = protocol_base(proto)
|
||||
# Fast path: skip deep manager probes when container is absent
|
||||
if cname and cname not in inventory:
|
||||
if cname and cname not in inventory and base not in NON_DOCKER_PROTOCOLS:
|
||||
return proto, merge_saved_protocol_status(proto, {
|
||||
'container_exists': False,
|
||||
'container_running': False,
|
||||
@@ -3574,7 +3587,9 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro
|
||||
|
||||
ssh = get_ssh(server)
|
||||
ssh.connect()
|
||||
docker_install_log = ensure_docker_installed(ssh)
|
||||
docker_install_log = ''
|
||||
if install_base not in NON_DOCKER_PROTOCOLS:
|
||||
docker_install_log = ensure_docker_installed(ssh)
|
||||
manager = get_protocol_manager(ssh, install_protocol)
|
||||
|
||||
# Pass parameters to installer
|
||||
@@ -3631,6 +3646,11 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro
|
||||
domain=req.naiveproxy_domain,
|
||||
email=req.naiveproxy_email,
|
||||
)
|
||||
elif install_base == 'mieru':
|
||||
result = manager.install_protocol(
|
||||
protocol_type=install_protocol,
|
||||
port=req.port,
|
||||
)
|
||||
else:
|
||||
result = manager.install_protocol(install_protocol, port=req.port)
|
||||
|
||||
@@ -3680,6 +3700,11 @@ async def api_install_protocol(request: Request, server_id: int, req: InstallPro
|
||||
proto_record['email'] = result.get('email')
|
||||
if result.get('port'):
|
||||
proto_record['port'] = str(result['port'])
|
||||
if install_base == 'mieru':
|
||||
if result.get('port'):
|
||||
proto_record['port'] = str(result['port'])
|
||||
if result.get('release'):
|
||||
proto_record['release'] = result.get('release')
|
||||
proto_record['base_protocol'] = install_base
|
||||
proto_record['instance'] = protocol_instance(install_protocol)
|
||||
proto_record['display_name'] = protocol_display_name(install_protocol)
|
||||
@@ -3867,6 +3892,7 @@ CONTAINER_NAMES = {
|
||||
'telemt': 'telemt',
|
||||
'hysteria': 'amnezia-hysteria',
|
||||
'naiveproxy': 'amnezia-naiveproxy',
|
||||
'mieru': 'mita',
|
||||
'dns': 'amnezia-dns',
|
||||
'wireguard': 'amnezia-wireguard',
|
||||
'socks5': 'amnezia-socks5proxy',
|
||||
@@ -4164,6 +4190,35 @@ async def api_container_toggle(request: Request, server_id: int, req: ProtocolRe
|
||||
server = data['servers'][server_id]
|
||||
ssh = get_ssh(server)
|
||||
ssh.connect()
|
||||
base = protocol_base(req.protocol)
|
||||
if base in NON_DOCKER_PROTOCOLS:
|
||||
manager = get_protocol_manager(ssh, req.protocol)
|
||||
is_running = manager.check_container_running(req.protocol)
|
||||
if is_running:
|
||||
manager.stop_service()
|
||||
action = 'stopped'
|
||||
else:
|
||||
manager.start_service()
|
||||
action = 'started'
|
||||
diag = manager.get_container_diagnostics(req.protocol)
|
||||
error = diag.get('error_summary') or ''
|
||||
recent_logs = diag.get('recent_logs') or ''
|
||||
if action == 'started' and not diag.get('running'):
|
||||
ssh.disconnect()
|
||||
return JSONResponse({
|
||||
'error': error or 'Mieru failed to start',
|
||||
'action': action,
|
||||
'container': container,
|
||||
'recent_logs': recent_logs,
|
||||
}, status_code=400)
|
||||
ssh.disconnect()
|
||||
return {
|
||||
'status': 'success',
|
||||
'action': action,
|
||||
'container': container,
|
||||
'error': error,
|
||||
'recent_logs': recent_logs,
|
||||
}
|
||||
# Check current state
|
||||
out, _, _ = ssh.run_sudo_command(
|
||||
f"docker inspect -f '{{{{.State.Running}}}}' {container} 2>/dev/null"
|
||||
@@ -4227,6 +4282,20 @@ async def api_container_logs(request: Request, server_id: int, req: ContainerLog
|
||||
container = protocol_container_name(req.protocol)
|
||||
ssh = get_ssh(server)
|
||||
ssh.connect()
|
||||
if protocol_base(req.protocol) in NON_DOCKER_PROTOCOLS:
|
||||
mgr = get_protocol_manager(ssh, req.protocol)
|
||||
logs = mgr.get_logs(req.protocol, tail=req.tail or 200)
|
||||
diag = mgr.get_container_diagnostics(req.protocol)
|
||||
ssh.disconnect()
|
||||
return {
|
||||
'status': 'success',
|
||||
'protocol': req.protocol,
|
||||
'container': container,
|
||||
'logs': logs,
|
||||
'running': bool(diag.get('running')),
|
||||
'error': diag.get('error_summary') or '',
|
||||
'container_status': diag.get('status') or '',
|
||||
}
|
||||
if protocol_base(req.protocol) == 'hysteria':
|
||||
from managers.hysteria_manager import HysteriaManager
|
||||
mgr = HysteriaManager(ssh, req.protocol)
|
||||
@@ -4292,6 +4361,17 @@ async def api_container_logs_stream(request: Request, server_id: int, protocol:
|
||||
ssh = get_ssh(server)
|
||||
ssh.connect()
|
||||
try:
|
||||
if protocol_base(protocol) in NON_DOCKER_PROTOCOLS:
|
||||
mgr = get_protocol_manager(ssh, protocol)
|
||||
logs = mgr.get_logs(protocol, tail=tail)
|
||||
diag = mgr.get_container_diagnostics(protocol)
|
||||
state = '|'.join([
|
||||
diag.get('status') or '',
|
||||
'true' if diag.get('running') else 'false',
|
||||
'',
|
||||
diag.get('error_summary') or '',
|
||||
])
|
||||
return logs, state
|
||||
out, err, _ = ssh.run_sudo_command(
|
||||
f"docker logs --tail {tail} --timestamps {shlex.quote(container)} 2>&1",
|
||||
timeout=25,
|
||||
|
||||
@@ -70,6 +70,9 @@ class BackupManager:
|
||||
remote_dir = inst_path('/opt/amnezia/naiveproxy')
|
||||
paths['host'] = [remote_dir]
|
||||
paths['container'] = ['/etc/caddy']
|
||||
elif base == 'mieru':
|
||||
remote_dir = inst_path('/opt/amnezia/mieru')
|
||||
paths['host'] = [remote_dir]
|
||||
elif base == 'dns':
|
||||
paths['host'] = ['/opt/amnezia/dns']
|
||||
paths['container'] = ['/opt/amnezia/dns']
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
"""
|
||||
Mieru protocol manager — native mita server package (systemd).
|
||||
|
||||
Installs pinned release from https://github.com/enfein/mieru
|
||||
Server binary: mita (systemd service), client: mieru.
|
||||
Share links: mierus://user:pass@host?port=...&protocol=TCP&profile=default
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import string
|
||||
from urllib.parse import quote
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MIERU_RELEASE = '3.28.0'
|
||||
GITHUB_RELEASE = f'https://github.com/enfein/mieru/releases/download/v{MIERU_RELEASE}'
|
||||
|
||||
|
||||
def _q(value):
|
||||
return shlex.quote(str(value))
|
||||
|
||||
|
||||
def _rand_token(length=16):
|
||||
alphabet = string.ascii_lowercase + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def _sanitize_username(name):
|
||||
base = re.sub(r'[^a-zA-Z0-9_-]', '', (name or 'user').strip())[:24] or 'user'
|
||||
return f'{base}_{_rand_token(4)}'
|
||||
|
||||
|
||||
class MieruManager:
|
||||
PROTOCOL = 'mieru'
|
||||
SERVICE_NAME = 'mita'
|
||||
BASE_DIR = '/opt/amnezia/mieru'
|
||||
DEFAULT_PORT = 2999
|
||||
|
||||
def __init__(self, ssh, protocol='mieru'):
|
||||
self.ssh = ssh
|
||||
self.protocol = protocol or self.PROTOCOL
|
||||
self.base_dir = self.BASE_DIR
|
||||
self.clients_path = f'{self.base_dir}/clients.json'
|
||||
self.meta_path = f'{self.base_dir}/metadata.json'
|
||||
self.config_path = f'{self.base_dir}/server_config.json'
|
||||
|
||||
# ===================== STATUS =====================
|
||||
|
||||
def check_docker_installed(self):
|
||||
return True
|
||||
|
||||
def _mita_installed(self):
|
||||
out, _, code = self.ssh.run_command('command -v mita 2>/dev/null')
|
||||
return code == 0 and bool(out.strip())
|
||||
|
||||
def check_protocol_installed(self, protocol_type=None):
|
||||
return self._mita_installed() and self._panel_installed()
|
||||
|
||||
def _panel_installed(self):
|
||||
out, _, code = self.ssh.run_sudo_command(f"test -f {_q(self.meta_path)} && echo yes")
|
||||
return code == 0 and 'yes' in out
|
||||
|
||||
def _proxy_running(self):
|
||||
out, _, code = self.ssh.run_sudo_command('mita status 2>/dev/null')
|
||||
if code != 0:
|
||||
return False
|
||||
return 'RUNNING' in (out or '').upper()
|
||||
|
||||
def check_container_running(self, protocol_type=None):
|
||||
return self._proxy_running()
|
||||
|
||||
def get_logs(self, protocol_type=None, tail=200):
|
||||
tail = max(20, min(int(tail or 200), 2000))
|
||||
out, err, code = self.ssh.run_sudo_command(
|
||||
f"journalctl -u {self.SERVICE_NAME} -n {tail} --no-pager 2>&1",
|
||||
timeout=30,
|
||||
)
|
||||
text = (out or err or '').strip()
|
||||
if not text:
|
||||
status, _, _ = self.ssh.run_sudo_command('mita status 2>&1')
|
||||
text = (status or '').strip()
|
||||
if not text:
|
||||
text = f'(no logs: exit {code})'
|
||||
return text
|
||||
|
||||
def get_container_diagnostics(self, protocol_type=None):
|
||||
running = self._proxy_running()
|
||||
out, _, _ = self.ssh.run_sudo_command(
|
||||
f"systemctl is-active {self.SERVICE_NAME} 2>/dev/null"
|
||||
)
|
||||
daemon_active = (out or '').strip() == 'active'
|
||||
diag = {
|
||||
'status': 'running' if running else ('idle' if daemon_active else 'stopped'),
|
||||
'running': running,
|
||||
'error_summary': '',
|
||||
'recent_logs': self.get_logs(protocol_type, tail=40),
|
||||
}
|
||||
if not self._mita_installed():
|
||||
diag['status'] = 'missing'
|
||||
diag['error_summary'] = 'mita package not installed'
|
||||
elif not running and daemon_active:
|
||||
status_out, _, _ = self.ssh.run_sudo_command('mita status 2>&1')
|
||||
if 'IDLE' in (status_out or '').upper():
|
||||
diag['error_summary'] = 'Proxy stopped (mita status IDLE)'
|
||||
elif status_out.strip():
|
||||
diag['error_summary'] = status_out.strip().splitlines()[-1][:160]
|
||||
elif not daemon_active:
|
||||
diag['error_summary'] = f'{self.SERVICE_NAME} systemd service is not active'
|
||||
return diag
|
||||
|
||||
def get_server_status(self, protocol_type=None):
|
||||
protocol_type = protocol_type or self.protocol
|
||||
exists = self.check_protocol_installed(protocol_type)
|
||||
running = self.check_container_running(protocol_type) if exists else False
|
||||
meta = self._read_metadata() if exists else {}
|
||||
clients = self._read_clients() if exists else []
|
||||
port = int(meta.get('port') or self.DEFAULT_PORT)
|
||||
return {
|
||||
'container_exists': exists,
|
||||
'container_running': running,
|
||||
'port': port,
|
||||
'release': meta.get('release') or MIERU_RELEASE,
|
||||
'clients_count': len(clients),
|
||||
'protocol': protocol_type,
|
||||
'base_protocol': self.PROTOCOL,
|
||||
'instance': 1,
|
||||
'container_name': self.SERVICE_NAME,
|
||||
}
|
||||
|
||||
# ===================== IO HELPERS =====================
|
||||
|
||||
def _read_file(self, path):
|
||||
out, _, code = self.ssh.run_sudo_command(f"cat {_q(path)} 2>/dev/null")
|
||||
return out if code == 0 else ''
|
||||
|
||||
def _write_file(self, path, content):
|
||||
import base64
|
||||
b64 = base64.b64encode((content or '').encode('utf-8')).decode('ascii')
|
||||
script = (
|
||||
f"mkdir -p $(dirname {_q(path)}) && "
|
||||
f"echo {_q(b64)} | base64 -d > {_q(path)}"
|
||||
)
|
||||
out, err, code = self.ssh.run_sudo_command(f"sh -c {_q(script)}", timeout=30)
|
||||
if code != 0:
|
||||
raise RuntimeError(f'Failed to write {path}: {err or out}')
|
||||
|
||||
def _read_metadata(self):
|
||||
raw = self._read_file(self.meta_path).strip()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _write_metadata(self, meta):
|
||||
self._write_file(self.meta_path, json.dumps(meta, indent=2))
|
||||
|
||||
def _read_clients(self):
|
||||
raw = self._read_file(self.clients_path).strip()
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
return data if isinstance(data, list) else []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _write_clients(self, clients):
|
||||
self._write_file(self.clients_path, json.dumps(clients, indent=2))
|
||||
|
||||
def _build_server_config(self, port, clients):
|
||||
users = []
|
||||
for c in clients:
|
||||
if c.get('enabled', True):
|
||||
users.append({
|
||||
'name': c.get('username') or c.get('name') or c.get('id'),
|
||||
'password': c.get('password') or '',
|
||||
})
|
||||
return {
|
||||
'portBindings': [{'port': int(port), 'protocol': 'TCP'}],
|
||||
'users': users,
|
||||
'loggingLevel': 'INFO',
|
||||
'mtu': 1400,
|
||||
}
|
||||
|
||||
def _apply_config(self, config, reload_only=False):
|
||||
self._write_file(self.config_path, json.dumps(config, indent=2))
|
||||
out, err, code = self.ssh.run_sudo_command(
|
||||
f"mita apply config {_q(self.config_path)} 2>&1",
|
||||
timeout=60,
|
||||
)
|
||||
if code != 0:
|
||||
raise RuntimeError((err or out or 'mita apply config failed').strip())
|
||||
if reload_only:
|
||||
self.ssh.run_sudo_command('mita reload 2>/dev/null || true', timeout=30)
|
||||
else:
|
||||
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
||||
out2, err2, code2 = self.ssh.run_sudo_command('mita start 2>&1', timeout=60)
|
||||
if code2 != 0:
|
||||
raise RuntimeError((err2 or out2 or 'mita start failed').strip())
|
||||
|
||||
def _sync_server(self, reload_only=True):
|
||||
meta = self._read_metadata()
|
||||
port = int(meta.get('port') or self.DEFAULT_PORT)
|
||||
clients = self._read_clients()
|
||||
config = self._build_server_config(port, clients)
|
||||
self._apply_config(config, reload_only=reload_only)
|
||||
|
||||
def _open_firewall_port(self, port):
|
||||
script = f"""
|
||||
PORT={int(port)}
|
||||
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qi active; then
|
||||
ufw allow "$PORT"/tcp || true
|
||||
fi
|
||||
if command -v firewall-cmd >/dev/null 2>&1; then
|
||||
firewall-cmd --permanent --add-port="$PORT"/tcp 2>/dev/null || true
|
||||
firewall-cmd --reload 2>/dev/null || true
|
||||
fi
|
||||
"""
|
||||
self.ssh.run_sudo_script(script, timeout=60)
|
||||
|
||||
def _package_url(self):
|
||||
arch_out, _, _ = self.ssh.run_command('uname -m')
|
||||
arch = (arch_out or '').strip().lower()
|
||||
_, _, deb_code = self.ssh.run_command('command -v dpkg 2>/dev/null')
|
||||
_, _, rpm_code = self.ssh.run_command('command -v rpm 2>/dev/null')
|
||||
use_deb = deb_code == 0 or rpm_code != 0
|
||||
if use_deb:
|
||||
if arch in ('aarch64', 'arm64'):
|
||||
return f'{GITHUB_RELEASE}/mita_{MIERU_RELEASE}_arm64.deb', 'deb'
|
||||
return f'{GITHUB_RELEASE}/mita_{MIERU_RELEASE}_amd64.deb', 'deb'
|
||||
if arch in ('aarch64', 'arm64'):
|
||||
return f'{GITHUB_RELEASE}/mita-{MIERU_RELEASE}-1.aarch64.rpm', 'rpm'
|
||||
return f'{GITHUB_RELEASE}/mita-{MIERU_RELEASE}-1.x86_64.rpm', 'rpm'
|
||||
|
||||
def _install_package(self, log):
|
||||
url, pkg_type = self._package_url()
|
||||
tmp = f'/tmp/mita_{MIERU_RELEASE}'
|
||||
if pkg_type == 'deb':
|
||||
tmp += '.deb'
|
||||
install_cmd = f"dpkg -i {_q(tmp)} || apt-get install -f -y"
|
||||
else:
|
||||
tmp += '.rpm'
|
||||
install_cmd = f"rpm -Uvh --force {_q(tmp)}"
|
||||
out, err, code = self.ssh.run_sudo_command(
|
||||
f"curl -fL {_q(url)} -o {_q(tmp)} 2>&1",
|
||||
timeout=300,
|
||||
)
|
||||
if code != 0:
|
||||
raise RuntimeError(f'Failed to download mita package: {err or out}')
|
||||
log.append(f'Downloaded mita v{MIERU_RELEASE}')
|
||||
out, err, code = self.ssh.run_sudo_command(install_cmd, timeout=180)
|
||||
if code != 0:
|
||||
raise RuntimeError(f'Failed to install mita package: {err or out}')
|
||||
log.append('Installed mita package')
|
||||
self.ssh.run_sudo_command(
|
||||
f"systemctl enable --now {self.SERVICE_NAME} 2>/dev/null || true",
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
def _build_share_uri(self, host, port, username, password, name=''):
|
||||
user = quote(username or '', safe='')
|
||||
pw = quote(password or '', safe='')
|
||||
params = f"port={int(port)}&protocol=TCP&profile=default"
|
||||
link = f"mierus://{user}:{pw}@{host}?{params}"
|
||||
if name:
|
||||
link += f"#{quote(name, safe='')}"
|
||||
return link
|
||||
|
||||
def _build_client_json(self, host, port, username, password):
|
||||
return json.dumps({
|
||||
'profiles': [{
|
||||
'profileName': 'default',
|
||||
'user': {'name': username, 'password': password},
|
||||
'servers': [{
|
||||
'ipAddress': host,
|
||||
'domainName': '',
|
||||
'portBindings': [{'port': int(port), 'protocol': 'TCP'}],
|
||||
}],
|
||||
'mtu': 1400,
|
||||
'multiplexing': {'level': 'MULTIPLEXING_LOW'},
|
||||
'handshakeMode': 'HANDSHAKE_STANDARD',
|
||||
}],
|
||||
'activeProfile': 'default',
|
||||
'rpcPort': 8964,
|
||||
'socks5Port': 1080,
|
||||
'loggingLevel': 'INFO',
|
||||
'socks5ListenLAN': False,
|
||||
}, indent=2)
|
||||
|
||||
# ===================== INSTALL / REMOVE =====================
|
||||
|
||||
def install_protocol(self, protocol_type=None, port=None):
|
||||
protocol_type = protocol_type or self.protocol
|
||||
port = int(port or self.DEFAULT_PORT)
|
||||
if port < 1025 or port > 65535:
|
||||
return {'status': 'error', 'message': 'Port must be between 1025 and 65535'}
|
||||
|
||||
log = []
|
||||
if not self._mita_installed():
|
||||
self._install_package(log)
|
||||
else:
|
||||
log.append(f'mita already installed, configuring panel (v{MIERU_RELEASE})')
|
||||
|
||||
self.ssh.run_sudo_command(f"mkdir -p {_q(self.base_dir)}")
|
||||
meta = {'port': port, 'release': MIERU_RELEASE}
|
||||
self._write_metadata(meta)
|
||||
self._write_clients([])
|
||||
log.append(f'Prepared {self.base_dir}')
|
||||
|
||||
try:
|
||||
config = self._build_server_config(port, [])
|
||||
self._apply_config(config, reload_only=False)
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e), 'log': log}
|
||||
|
||||
self._open_firewall_port(port)
|
||||
log.append(f'Started mita proxy on TCP {port}')
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': f'Mieru v{MIERU_RELEASE} installed',
|
||||
'log': log,
|
||||
'port': str(port),
|
||||
'release': MIERU_RELEASE,
|
||||
}
|
||||
|
||||
def remove_container(self, protocol_type=None):
|
||||
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
||||
self.ssh.run_sudo_command(f"rm -rf {_q(self.base_dir)}")
|
||||
return True
|
||||
|
||||
def start_service(self):
|
||||
out, err, code = self.ssh.run_sudo_command('mita start 2>&1', timeout=60)
|
||||
if code != 0:
|
||||
raise RuntimeError((err or out or 'mita start failed').strip())
|
||||
|
||||
def stop_service(self):
|
||||
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
||||
|
||||
def get_server_config(self, protocol_type=None):
|
||||
out, _, code = self.ssh.run_sudo_command('mita describe config 2>/dev/null')
|
||||
if code == 0 and (out or '').strip():
|
||||
return out
|
||||
return self._read_file(self.config_path)
|
||||
|
||||
def save_server_config(self, protocol_type=None, config_text=''):
|
||||
self._write_file(self.config_path, config_text or '')
|
||||
out, err, code = self.ssh.run_sudo_command(
|
||||
f"mita apply config {_q(self.config_path)} 2>&1",
|
||||
timeout=60,
|
||||
)
|
||||
if code != 0:
|
||||
raise RuntimeError((err or out or 'mita apply config failed').strip())
|
||||
self.ssh.run_sudo_command('mita stop 2>/dev/null || true', timeout=30)
|
||||
self.ssh.run_sudo_command('mita start 2>&1', timeout=60)
|
||||
return True
|
||||
|
||||
# ===================== CLIENTS =====================
|
||||
|
||||
def get_clients(self, protocol_type=None):
|
||||
clients = self._read_clients()
|
||||
result = []
|
||||
for c in clients:
|
||||
cname = c.get('name') or c.get('id')
|
||||
result.append({
|
||||
'clientId': c.get('id'),
|
||||
'client_id': c.get('id'),
|
||||
'id': c.get('id'),
|
||||
'name': cname,
|
||||
'email': cname,
|
||||
'enabled': c.get('enabled', True),
|
||||
'userData': {'clientName': cname, 'enabled': c.get('enabled', True)},
|
||||
})
|
||||
return result
|
||||
|
||||
def add_client(self, protocol_type, name, host, port=None):
|
||||
meta = self._read_metadata()
|
||||
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
||||
client_id = secrets.token_hex(8)
|
||||
username = _sanitize_username(name)
|
||||
password = _rand_token(20)
|
||||
clients = self._read_clients()
|
||||
clients.append({
|
||||
'id': client_id,
|
||||
'name': name or username,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'enabled': True,
|
||||
})
|
||||
self._write_clients(clients)
|
||||
self._sync_server(reload_only=True)
|
||||
config = self._build_share_uri(host, port, username, password, name or username)
|
||||
json_config = self._build_client_json(host, port, username, password)
|
||||
return {
|
||||
'clientId': client_id,
|
||||
'client_id': client_id,
|
||||
'id': client_id,
|
||||
'name': name or username,
|
||||
'config': config,
|
||||
'json_config': json_config,
|
||||
}
|
||||
|
||||
def get_client_config(self, protocol_type, client_id, host, port=None):
|
||||
meta = self._read_metadata()
|
||||
port = int(port or meta.get('port') or self.DEFAULT_PORT)
|
||||
clients = self._read_clients()
|
||||
client = next((c for c in clients if c.get('id') == client_id), None)
|
||||
if not client:
|
||||
return ''
|
||||
if not client.get('enabled', True):
|
||||
return ''
|
||||
username = client.get('username') or client.get('name') or client_id
|
||||
password = client.get('password') or ''
|
||||
name = client.get('name') or username
|
||||
return self._build_share_uri(host, port, username, password, name)
|
||||
|
||||
def remove_client(self, protocol_type, client_id):
|
||||
clients = [c for c in self._read_clients() if c.get('id') != client_id]
|
||||
self._write_clients(clients)
|
||||
self._sync_server(reload_only=True)
|
||||
return True
|
||||
|
||||
def toggle_client(self, protocol_type, client_id, enabled):
|
||||
clients = self._read_clients()
|
||||
for c in clients:
|
||||
if c.get('id') == client_id:
|
||||
c['enabled'] = bool(enabled)
|
||||
self._write_clients(clients)
|
||||
self._sync_server(reload_only=True)
|
||||
return True
|
||||
@@ -949,6 +949,11 @@ a:hover {
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.protocol-mieru .protocol-icon {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(168, 85, 247, 0.1));
|
||||
color: #818cf8;
|
||||
}
|
||||
|
||||
.protocol-dns .protocol-icon {
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(16, 185, 129, 0.1));
|
||||
color: var(--success);
|
||||
|
||||
+7
-3
@@ -24,7 +24,7 @@ _bot_task: Optional[asyncio.Task] = None
|
||||
_callback_refs = {}
|
||||
_pending_inputs = {}
|
||||
|
||||
CLIENT_PROTOCOLS = {"awg", "awg2", "awg_legacy", "xray", "telemt", "hysteria", "naiveproxy", "wireguard"}
|
||||
CLIENT_PROTOCOLS = {"awg", "awg2", "awg_legacy", "xray", "telemt", "hysteria", "naiveproxy", "mieru", "wireguard"}
|
||||
SERVICE_PROTOCOLS = {"dns", "adguard", "socks5", "nginx"}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ def _protocol_display_name(protocol: str) -> str:
|
||||
"telemt": "Telemt",
|
||||
"hysteria": "Hysteria 2",
|
||||
"naiveproxy": "NaiveProxy",
|
||||
"mieru": "Mieru",
|
||||
"dns": "AmneziaDNS",
|
||||
"wireguard": "WireGuard",
|
||||
"socks5": "SOCKS5",
|
||||
@@ -342,6 +343,7 @@ def _get_ssh_and_manager(server: dict, proto: str):
|
||||
from managers.telemt_manager import TelemtManager
|
||||
from managers.hysteria_manager import HysteriaManager
|
||||
from managers.naiveproxy_manager import NaiveProxyManager
|
||||
from managers.mieru_manager import MieruManager
|
||||
from managers.wireguard_manager import WireGuardManager
|
||||
from managers.dns_manager import DNSManager
|
||||
from managers.socks5_manager import Socks5Manager
|
||||
@@ -364,6 +366,8 @@ def _get_ssh_and_manager(server: dict, proto: str):
|
||||
manager = HysteriaManager(ssh, proto)
|
||||
elif base == "naiveproxy":
|
||||
manager = NaiveProxyManager(ssh, proto)
|
||||
elif base == "mieru":
|
||||
manager = MieruManager(ssh, proto)
|
||||
elif base == "wireguard":
|
||||
manager = WireGuardManager(ssh)
|
||||
elif base == "dns":
|
||||
@@ -569,7 +573,7 @@ async def _send_config_by_client(api: TelegramAPI, chat_id: int, server: dict, p
|
||||
server_name = server.get("name") or server.get("host", "Unknown")
|
||||
await api.send_message(chat_id, f"✅ <b>{_e(conn_name)}</b>\n🌐 Server: <b>{_e(server_name)}</b>\n🔌 Protocol: <b>{_e(proto.upper())}</b>")
|
||||
|
||||
is_link_proto = _proto_base(proto) in ("xray", "telemt", "hysteria", "naiveproxy")
|
||||
is_link_proto = _proto_base(proto) in ("xray", "telemt", "hysteria", "naiveproxy", "mieru")
|
||||
if is_link_proto:
|
||||
await api.send_message(chat_id, f"🔗 <b>Connection link</b> (tap to copy):\n<code>{_e(config)}</code>")
|
||||
else:
|
||||
@@ -926,7 +930,7 @@ async def _admin_create_client(api: TelegramAPI, chat_id: int, message_id: int,
|
||||
|
||||
async def _send_config_text(api: TelegramAPI, chat_id: int, server: dict, proto: str, conn_name: str, config: str, generate_vpn_link_fn: Callable):
|
||||
await api.send_message(chat_id, f"✅ <b>{_e(conn_name)}</b>\n🌐 Server: <b>{_e(server.get('name') or server.get('host'))}</b>\n🔌 Protocol: <b>{_e(proto.upper())}</b>")
|
||||
if _proto_base(proto) in ("xray", "telemt", "hysteria", "naiveproxy"):
|
||||
if _proto_base(proto) in ("xray", "telemt", "hysteria", "naiveproxy", "mieru"):
|
||||
await api.send_message(chat_id, f"🔗 <b>Connection link</b>:\n<code>{_e(config)}</code>")
|
||||
else:
|
||||
await api.send_message(chat_id, f"<b>📄 Configuration:</b>\n<pre>{_e(config)}</pre>")
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
if (p === 'xui') return '3x-ui VLESS';
|
||||
if (p === 'hysteria') return 'Hysteria 2';
|
||||
if (p === 'naiveproxy') return 'NaiveProxy';
|
||||
if (p === 'mieru') return 'Mieru';
|
||||
if (p === 'telemt') return 'Telemt';
|
||||
return (p || '').toUpperCase();
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
const xuiConfigured = {{ 'true' if xui_configured else 'false' }};
|
||||
const xuiDefaultInbound = {{ xui_default_inbound | int }};
|
||||
const xuiDefaultPanelId = {{ (xui_default_panel_id or '') | tojson }};
|
||||
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy'];
|
||||
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'mieru'];
|
||||
const PROTO_TITLES = {
|
||||
awg2: 'AmneziaWG 2.0',
|
||||
awg: 'AmneziaWG',
|
||||
@@ -206,6 +206,7 @@
|
||||
telemt: 'Telemt',
|
||||
hysteria: 'Hysteria 2',
|
||||
naiveproxy: 'NaiveProxy',
|
||||
mieru: 'Mieru',
|
||||
xui: '3x-ui VLESS',
|
||||
};
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
const vpnTab = document.querySelectorAll('.config-tab')[1];
|
||||
const vpnPanel = document.getElementById('panel-vpn');
|
||||
const base = String(proto || '').split('__')[0];
|
||||
if (proto === 'telemt' || base === 'hysteria' || base === 'naiveproxy') {
|
||||
if (proto === 'telemt' || base === 'hysteria' || base === 'naiveproxy' || base === 'mieru') {
|
||||
vpnTab.style.display = 'none';
|
||||
} else {
|
||||
vpnTab.style.display = '';
|
||||
|
||||
+71
-2
@@ -363,6 +363,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mieru Card -->
|
||||
<div class="card card-hover protocol-card protocol-mieru" id="proto-mieru">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||
<div class="protocol-icon">{{ icon('zap') }}</div>
|
||||
<div class="flex gap-sm" id="mieru-ctrl" style="display:none!important;"></div>
|
||||
</div>
|
||||
<div class="protocol-name">Mieru <span
|
||||
style="font-size:0.65rem; background:var(--accent, #6366f1); color:#fff; padding:2px 6px; border-radius:8px; vertical-align:middle;">v3.28.0</span></div>
|
||||
<div class="protocol-desc">
|
||||
{{ _('mieru_desc') }}
|
||||
</div>
|
||||
<div class="protocol-status" id="mieru-status">
|
||||
<span class="badge badge-warn"><span class="badge-dot"></span> {{ _('not_checked') }}</span>
|
||||
</div>
|
||||
<div id="mieru-info" class="hidden">
|
||||
<div class="protocol-info" id="mieru-info-grid"></div>
|
||||
</div>
|
||||
<div class="flex gap-sm" id="mieru-actions">
|
||||
<button class="btn btn-primary btn-sm" onclick="openInstallModal('mieru')" id="mieru-install-btn"
|
||||
style="flex:1">
|
||||
{{ _('install') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WireGuard Card -->
|
||||
<div class="card card-hover protocol-card protocol-wireguard" id="proto-wireguard">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:var(--space-sm);">
|
||||
@@ -777,6 +802,20 @@
|
||||
<div class="form-hint" style="margin-top: var(--space-sm); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); background: rgba(234,179,8,0.12); border: 1px solid rgba(234,179,8,0.35);">{{ _('naiveproxy_client_hint') }}</div>
|
||||
</div>
|
||||
|
||||
<div id="mieruOptions"
|
||||
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-hint" style="margin-bottom: var(--space-md); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); background: rgba(234,179,8,0.12); border: 1px solid rgba(234,179,8,0.35); color: var(--text);">
|
||||
{{ _('mieru_ports_warning') }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('port') }} (TCP) *</label>
|
||||
<input class="form-input" type="number" id="installMieruPort" value="2999" min="1025" max="65535">
|
||||
<div class="form-hint">{{ _('mieru_port_hint') }}</div>
|
||||
</div>
|
||||
<div class="form-hint">{{ _('mieru_install_hint') }}</div>
|
||||
<div class="form-hint" style="margin-top: var(--space-sm); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); background: rgba(234,179,8,0.12); border: 1px solid rgba(234,179,8,0.35);">{{ _('mieru_client_hint') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeModal('installModal')">{{ _('cancel') }}</button>
|
||||
<button class="btn btn-primary" onclick="installProtocol()" id="installBtn">
|
||||
@@ -1162,6 +1201,7 @@
|
||||
{ proto: 'telemt', category: 'protocols', icon: 'plane', title: 'Telemt (Telegram Proxy)', descKey: 'telemt_desc' },
|
||||
{ proto: 'hysteria', category: 'protocols', icon: 'refresh', title: 'Hysteria 2', descKey: 'hysteria_desc' },
|
||||
{ proto: 'naiveproxy', category: 'protocols', icon: 'link', title: 'NaiveProxy', descKey: 'naiveproxy_desc', badge: 'STABLE' },
|
||||
{ proto: 'mieru', category: 'protocols', icon: 'zap', title: 'Mieru', descKey: 'mieru_desc', badge: 'v3.28.0' },
|
||||
{ proto: 'wireguard', category: 'protocols', icon: 'lock', title: 'WireGuard', descKey: 'wireguard_desc' },
|
||||
{ proto: 'dns', category: 'services', icon: 'search', title: 'AmneziaDNS', descKey: 'dns_desc' },
|
||||
{ proto: 'adguard', category: 'services', icon: 'shield-check', title: 'AdGuard Home', descKey: 'adguard_desc' },
|
||||
@@ -1495,6 +1535,7 @@
|
||||
case 'telemt': title = 'Telemt'; break;
|
||||
case 'hysteria': title = 'Hysteria 2'; break;
|
||||
case 'naiveproxy': title = 'NaiveProxy'; break;
|
||||
case 'mieru': title = 'Mieru'; break;
|
||||
case 'wireguard': title = 'WireGuard'; break;
|
||||
case 'dns': title = 'AmneziaDNS'; break;
|
||||
case 'socks5': title = 'SOCKS5 Proxy'; break;
|
||||
@@ -1554,7 +1595,7 @@
|
||||
|
||||
for (const [proto, info] of orderedProtocolEntries(data.protocols)) {
|
||||
updateProtocolCard(proto, info);
|
||||
const isVPN = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'wireguard'].includes(protoBase(proto));
|
||||
const isVPN = ['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'mieru', 'wireguard'].includes(protoBase(proto));
|
||||
if (info.container_running && isVPN) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = proto;
|
||||
@@ -1782,7 +1823,7 @@
|
||||
if (isService) {
|
||||
grid = buildServiceInfoGrid(proto, info);
|
||||
} else if (info.port) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}/${(['xray', 'telemt', 'naiveproxy'].includes(protoBase(proto))) ? 'TCP' : 'UDP'}</span></div>`;
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('port')}</span><span class="protocol-info-value">${info.port}/${(['xray', 'telemt', 'naiveproxy', 'mieru'].includes(protoBase(proto))) ? 'TCP' : 'UDP'}</span></div>`;
|
||||
if (WG_AWG_BASES.has(protoBase(proto))) {
|
||||
const endpoint = (connectDomainEffective.effective_host && connectDomainEffective.effective_host !== SERVER_HOST)
|
||||
? connectDomainEffective.effective_host
|
||||
@@ -1795,6 +1836,9 @@
|
||||
if (protoBase(proto) === 'naiveproxy' && info.domain) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('naiveproxy_domain')}</span><span class="protocol-info-value">${info.domain}</span></div>`;
|
||||
}
|
||||
if (protoBase(proto) === 'mieru' && info.release) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('mieru_version')}</span><span class="protocol-info-value">v${info.release}</span></div>`;
|
||||
}
|
||||
}
|
||||
if (!isService && info.clients_count !== undefined) {
|
||||
grid += `<div class="protocol-info-item"><span class="protocol-info-label">${_('connections')}</span><span class="protocol-info-value">${info.clients_count}</span></div>`;
|
||||
@@ -1844,6 +1888,13 @@
|
||||
<button class="btn btn-danger btn-sm" onclick="uninstallProtocol('${proto}')">${_('uninstall')}</button>
|
||||
`;
|
||||
showConnectionsSection();
|
||||
} else if (protoBase(proto) === 'mieru') {
|
||||
actionsEl.innerHTML = `
|
||||
<button class="btn btn-secondary btn-sm" onclick="selectProtocolForConns('${proto}')" style="flex:1">${_('connections')}</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="showProtocolBackups('${proto}')">${_('backup')}</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="uninstallProtocol('${proto}')">${_('uninstall')}</button>
|
||||
`;
|
||||
showConnectionsSection();
|
||||
} else {
|
||||
actionsEl.innerHTML = `
|
||||
<button class="btn btn-secondary btn-sm" onclick="selectProtocolForConns('${proto}')" style="flex:1">${_('connections')}</button>
|
||||
@@ -2247,6 +2298,7 @@
|
||||
const nginxOpts = document.getElementById('nginxOptions');
|
||||
const hysteriaOpts = document.getElementById('hysteriaOptions');
|
||||
const naiveproxyOpts = document.getElementById('naiveproxyOptions');
|
||||
const mieruOpts = document.getElementById('mieruOptions');
|
||||
|
||||
telemtOpts.style.display = 'none';
|
||||
socks5Opts.style.display = 'none';
|
||||
@@ -2254,6 +2306,7 @@
|
||||
nginxOpts.style.display = 'none';
|
||||
hysteriaOpts.style.display = 'none';
|
||||
if (naiveproxyOpts) naiveproxyOpts.style.display = 'none';
|
||||
if (mieruOpts) mieruOpts.style.display = 'none';
|
||||
if (portGroup) portGroup.style.display = '';
|
||||
|
||||
if (base === 'dns') {
|
||||
@@ -2321,6 +2374,17 @@
|
||||
if (npDomain && !npDomain.value && SERVER_SSL_DOMAIN) npDomain.value = SERVER_SSL_DOMAIN;
|
||||
if (npEmail && !npEmail.value && SERVER_SSL_EMAIL) npEmail.value = SERVER_SSL_EMAIL;
|
||||
updateNaiveproxyDnsHint();
|
||||
} else if (base === 'mieru') {
|
||||
if (portGroup) portGroup.style.display = 'none';
|
||||
const suggested = '2999';
|
||||
portInput.value = suggested;
|
||||
portInput.disabled = false;
|
||||
if (mieruOpts) mieruOpts.style.display = 'block';
|
||||
const miPort = document.getElementById('installMieruPort');
|
||||
if (miPort) {
|
||||
miPort.value = suggested;
|
||||
miPort.oninput = () => { portInput.value = miPort.value; };
|
||||
}
|
||||
} else {
|
||||
portLabel.textContent = _('port') + ' (UDP)';
|
||||
portInput.value = currentInstallAnother ? nextSuggestedPort(currentInstallProto, 55424) : '55424';
|
||||
@@ -2398,6 +2462,11 @@
|
||||
params.port = '443';
|
||||
params.naiveproxy_domain = document.getElementById('installNaiveproxyDomain').value.trim();
|
||||
params.naiveproxy_email = document.getElementById('installNaiveproxyEmail').value.trim();
|
||||
} else if (protoBase(currentInstallProto) === 'mieru') {
|
||||
const miPortEl = document.getElementById('installMieruPort');
|
||||
if (miPortEl && miPortEl.value) {
|
||||
params.port = miPortEl.value;
|
||||
}
|
||||
}
|
||||
const result = await apiCall(`/api/servers/${SERVER_ID}/install`, 'POST', params);
|
||||
clearInterval(progressInterval);
|
||||
|
||||
@@ -1544,7 +1544,7 @@
|
||||
}
|
||||
|
||||
const guestServersData = {{ servers | default([]) | tojson }};
|
||||
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy'];
|
||||
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'mieru'];
|
||||
const PROTO_TITLES = {
|
||||
awg2: 'AmneziaWG 2.0',
|
||||
awg: 'AmneziaWG',
|
||||
@@ -1554,6 +1554,7 @@
|
||||
telemt: 'Telemt',
|
||||
hysteria: 'Hysteria 2',
|
||||
naiveproxy: 'NaiveProxy',
|
||||
mieru: 'Mieru',
|
||||
xui: '3x-ui VLESS',
|
||||
};
|
||||
function protoTitle(key) {
|
||||
|
||||
@@ -631,7 +631,7 @@
|
||||
const select = document.getElementById(selectId);
|
||||
const group = groupId ? document.getElementById(groupId) : null;
|
||||
select.innerHTML = '';
|
||||
const vpnOrder = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy'];
|
||||
const vpnOrder = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt', 'hysteria', 'naiveproxy', 'mieru'];
|
||||
const titles = {
|
||||
awg2: 'AmneziaWG 2.0',
|
||||
awg: 'AmneziaWG',
|
||||
@@ -641,6 +641,7 @@
|
||||
telemt: 'Telemt',
|
||||
hysteria: 'Hysteria 2',
|
||||
naiveproxy: 'NaiveProxy',
|
||||
mieru: 'Mieru',
|
||||
xui: '3x-ui VLESS',
|
||||
};
|
||||
const protoTitle = (key) => {
|
||||
|
||||
@@ -395,6 +395,12 @@
|
||||
"naiveproxy_dns_hint": "Create this DNS record before installation:",
|
||||
"naiveproxy_ports_warning": "Warning: free TCP ports 80 and 443 are required for stable operation (Let\u0027s Encrypt on 80, HTTPS proxy on 443).",
|
||||
"naiveproxy_client_hint": "Stable build. Do NOT use v2rayN. Confirmed working in Karing. Other clients are untested.",
|
||||
"mieru_desc": "Mieru (mita v3.28.0) — native TCP proxy from enfein/mieru. No Docker required. Client: mieru app or Clash.Meta/mihomo.",
|
||||
"mieru_version": "Version",
|
||||
"mieru_port_hint": "TCP port for mita (1025–65535). Open it in the server firewall.",
|
||||
"mieru_install_hint": "Installs the official mita package from GitHub release v3.28.0 (Debian/RPM). Requires Linux with systemd.",
|
||||
"mieru_ports_warning": "Warning: ensure the chosen TCP port is free and allowed through the firewall.",
|
||||
"mieru_client_hint": "Share links use mierus:// format. Import into the mieru client or Clash.Meta (type: mieru).",
|
||||
"server_ssl_domain": "SSL domain (default)",
|
||||
"server_ssl_email": "SSL email (default)",
|
||||
"server_ssl_hint": "Used when installing Hysteria / NGINX on this server (Let's Encrypt).",
|
||||
|
||||
@@ -395,6 +395,12 @@
|
||||
"naiveproxy_dns_hint": "Перед установкой создайте DNS-запись:",
|
||||
"naiveproxy_ports_warning": "Внимание: для стабильной работы нужны свободные TCP-порты 80 и 443 (Let\u0027s Encrypt на 80, HTTPS-прокси на 443).",
|
||||
"naiveproxy_client_hint": "Стабильная версия. НЕ используйте v2rayN. Точно работает в Karing. Остальные клиенты под вопросом.",
|
||||
"mieru_desc": "Mieru (mita v3.28.0) — нативный TCP-прокси из enfein/mieru. Docker не нужен. Клиент: приложение mieru или Clash.Meta/mihomo.",
|
||||
"mieru_version": "Версия",
|
||||
"mieru_port_hint": "TCP-порт для mita (1025–65535). Откройте его в фаерволе сервера.",
|
||||
"mieru_install_hint": "Устанавливает официальный пакет mita с GitHub релиза v3.28.0 (Debian/RPM). Нужен Linux со systemd.",
|
||||
"mieru_ports_warning": "Внимание: выбранный TCP-порт должен быть свободен и разрешён в фаерволе.",
|
||||
"mieru_client_hint": "Ссылки в формате mierus://. Импорт в клиент mieru или Clash.Meta (type: mieru).",
|
||||
"server_ssl_domain": "SSL-домен (по умолчанию)",
|
||||
"server_ssl_email": "SSL-email (по умолчанию)",
|
||||
"server_ssl_hint": "Подставляется при установке Hysteria / NGINX на этом сервере (Let's Encrypt).",
|
||||
|
||||
Reference in New Issue
Block a user