diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index fdd8e1e5d..4059c9abc 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/frontend/src/lib/xray/inbound-link.ts @@ -684,13 +684,11 @@ export interface GenMtprotoLinkInput { inbound: Inbound; address: string; port?: number; - remark?: string; } // Builds a Telegram proxy deep link for an mtproto inbound: -// tg://proxy?server=&port=&secret=. export function genMtprotoLink(input: GenMtprotoLinkInput): string { - const { inbound, address, port = inbound.port, remark = '' } = input; + const { inbound, address, port = inbound.port } = input; if (inbound.protocol !== 'mtproto') return ''; const secret = inbound.settings.secret ?? ''; if (secret.length === 0) return ''; @@ -698,7 +696,6 @@ export function genMtprotoLink(input: GenMtprotoLinkInput): string { url.searchParams.set('server', address); url.searchParams.set('port', String(port)); url.searchParams.set('secret', secret); - url.hash = encodeURIComponent(remark); return url.toString(); } @@ -890,7 +887,7 @@ export function genLink(input: GenLinkInput): string { externalProxy, }); case 'mtproto': - return genMtprotoLink({ inbound, address, port, remark }); + return genMtprotoLink({ inbound, address, port }); default: return ''; } diff --git a/frontend/src/pages/index/LogModal.tsx b/frontend/src/pages/index/LogModal.tsx index 5a278ecae..28b4ee4d1 100644 --- a/frontend/src/pages/index/LogModal.tsx +++ b/frontend/src/pages/index/LogModal.tsx @@ -5,6 +5,7 @@ import { DownloadOutlined, SyncOutlined } from '@ant-design/icons'; import { HttpUtil, FileManager, PromiseUtil } from '@/utils'; import { useMediaQuery } from '@/hooks/useMediaQuery'; +import { parseLogLine } from './logParse'; import './LogModal.css'; interface LogModalProps { @@ -12,50 +13,6 @@ interface LogModalProps { onClose: () => void; } -interface ParsedLog { - date: string; - time: string; - stamp: string; - levelText: string; - levelClass: string; - service: string; - body: string; -} - -const LEVELS = ['DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR']; -const LEVEL_CLASSES = ['level-debug', 'level-info', 'level-notice', 'level-warning', 'level-error']; - -function parseLogLine(line: string): ParsedLog { - const [head, ...rest] = (line || '').split(' - '); - const message = rest.join(' - '); - const parts = head.split(' '); - - let date = ''; - let time = ''; - let levelText: string; - if (parts.length >= 3) { - [date, time, levelText] = parts; - } else { - levelText = head; - } - - const li = LEVELS.indexOf(levelText); - const levelClass = li >= 0 ? LEVEL_CLASSES[li] : 'level-unknown'; - - let service = ''; - let body = message || ''; - if (body.startsWith('XRAY:')) { - service = 'XRAY:'; - body = body.slice('XRAY:'.length).trimStart(); - } else if (body) { - service = 'X-UI:'; - } - - const stamp = [date, time].filter(Boolean).join(' '); - - return { date, time, stamp, levelText, levelClass, service, body }; -} - export default function LogModal({ open, onClose }: LogModalProps) { const { t } = useTranslation(); const { isMobile } = useMediaQuery(); diff --git a/frontend/src/pages/index/logParse.ts b/frontend/src/pages/index/logParse.ts new file mode 100644 index 000000000..82f595489 --- /dev/null +++ b/frontend/src/pages/index/logParse.ts @@ -0,0 +1,113 @@ +// Parser for the panel log viewer. Logs reach the UI in two shapes: +// +// - App log (SysLog off): the in-memory buffer, formatted as +// "2006/01/02 15:04:05 LEVEL - message" +// - SysLog (journalctl -o short): every entry is prefixed with +// "Mon DD HH:MM:SS host ident[pid]: " before the real message, and the +// message itself is one of several shapes depending on which subsystem +// emitted it: +// "INFO - mtproto: ..." go-logging (x-ui + xray) +// "2026/06/08 19:22:22 http: ..." Go std log (net/http, runtime) +// "[Mon Jun 8 23:56:52 UTC 2026] ERROR ..." telego bot +// "Stopping x-ui.service - ..." systemd +// +// parseLogLine normalises all of these into a stamp + level + service + body so +// the viewer renders a readable line instead of a bare timestamp. + +export interface ParsedLog { + date: string; + time: string; + stamp: string; + levelText: string; + levelClass: string; + service: string; + body: string; +} + +export const LEVELS = ['DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR']; +export const LEVEL_CLASSES = [ + 'level-debug', + 'level-info', + 'level-notice', + 'level-warning', + 'level-error', +]; + +// "Mon DD HH:MM:SS host ident[pid]: " — captures the journal date, +// time, and the message that follows the syslog identifier. +const SYSLOG_PREFIX = /^([A-Za-z]{3}\s+\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+\S+\s+\S+?:\s+(.*)$/; +// Redundant Go std-log date prefix ("2006/01/02 15:04:05 ") to strip — the +// journal already carries the timestamp. +const GO_LOG_DATE = /^\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/; +// telego's own line prefix: "[Mon Jan _2 15:04:05 MST 2006] LEVEL rest". +const TELEGO = /^\[[^\]]+\]\s+([A-Z]+)\s+(.*)$/; + +// splitLevelDash pulls a leading "LEVEL - " off a message, returning the level +// and the remainder. Returns null when the message does not start with a level. +function splitLevelDash(message: string): { level: string; rest: string } | null { + const dash = message.indexOf(' - '); + if (dash < 0) return null; + const level = message.slice(0, dash).trim(); + if (LEVELS.indexOf(level) < 0) return null; + return { level, rest: message.slice(dash + 3) }; +} + +export function parseLogLine(line: string): ParsedLog { + const raw = (line || '').trim(); + + let date = ''; + let time = ''; + let levelText = ''; + let body: string; + + const sys = raw.match(SYSLOG_PREFIX); + if (sys) { + date = sys[1]; + time = sys[2]; + let message = sys[3]; + + const ld = splitLevelDash(message); + if (ld) { + // go-logging: "LEVEL - message" + levelText = ld.level; + body = ld.rest; + } else { + // Strip the redundant Go std-log date, then try to lift a level out of a + // telego "[timestamp] LEVEL ..." line; otherwise keep the message as-is. + message = message.replace(GO_LOG_DATE, ''); + const tg = message.match(TELEGO); + if (tg && LEVELS.indexOf(tg[1]) >= 0) { + levelText = tg[1]; + body = tg[2]; + } else { + body = message; + } + } + } else { + // App-log format: "2006/01/02 15:04:05 LEVEL - body" + const [head, ...rest] = raw.split(' - '); + const message = rest.join(' - '); + const parts = head.split(' '); + if (parts.length >= 3) { + [date, time, levelText] = parts; + } else { + levelText = head; + } + body = message || ''; + } + + const li = LEVELS.indexOf(levelText); + const levelClass = li >= 0 ? LEVEL_CLASSES[li] : 'level-unknown'; + + let service = ''; + if (body.startsWith('XRAY:')) { + service = 'XRAY:'; + body = body.slice('XRAY:'.length).trimStart(); + } else if (body) { + service = 'X-UI:'; + } + + const stamp = [date, time].filter(Boolean).join(' '); + + return { date, time, stamp, levelText, levelClass, service, body }; +} diff --git a/frontend/src/test/log-parse.test.ts b/frontend/src/test/log-parse.test.ts new file mode 100644 index 000000000..75fb27e97 --- /dev/null +++ b/frontend/src/test/log-parse.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; + +import { parseLogLine } from '@/pages/index/logParse'; + +// Fixtures are real lines captured from `journalctl -u x-ui` on a production +// host (the SysLog view) plus the in-memory app-log format. Each journald entry +// carries a "Mon DD HH:MM:SS host ident[pid]: " prefix that the viewer used to +// mistake for the level, leaving only a bare timestamp on screen. +describe('parseLogLine — SysLog (journalctl) formats', () => { + it('x-ui go-logging line: keeps level, strips prefix, tags X-UI', () => { + const r = parseLogLine( + 'Jun 08 23:57:28 ubuntu-4gb-fsn1-1 /usr/local/x-ui/x-ui[72297]: INFO - mtproto: started mtg for inbound 3 on 0.0.0.0:8443', + ); + expect(r.stamp).toBe('Jun 08 23:57:28'); + expect(r.levelText).toBe('INFO'); + expect(r.service).toBe('X-UI:'); + expect(r.body).toBe('mtproto: started mtg for inbound 3 on 0.0.0.0:8443'); + }); + + it('xray go-logging line: lifts the XRAY service tag', () => { + const r = parseLogLine( + 'Jun 08 23:56:52 ubuntu-4gb-fsn1-1 /usr/local/x-ui/x-ui[72297]: WARNING - XRAY: core: Xray 26.6.1 started', + ); + expect(r.stamp).toBe('Jun 08 23:56:52'); + expect(r.levelText).toBe('WARNING'); + expect(r.service).toBe('XRAY:'); + expect(r.body).toBe('core: Xray 26.6.1 started'); + }); + + it('Go std-log line: strips the redundant embedded date, keeps the message', () => { + const r = parseLogLine( + 'Jun 08 19:22:22 ubuntu-4gb-fsn1-1 x-ui[1439]: 2026/06/08 19:22:22 http: TLS handshake error from 18.97.5.1:36022: EOF', + ); + expect(r.stamp).toBe('Jun 08 19:22:22'); + expect(r.levelText).toBe(''); + expect(r.body).toBe('http: TLS handshake error from 18.97.5.1:36022: EOF'); + }); + + it('telego bracketed line: lifts the ERROR level out of "[ts] ERROR ..."', () => { + const r = parseLogLine( + 'Jun 09 00:14:52 ubuntu-4gb-fsn1-1 x-ui[72297]: [Tue Jun 9 00:14:52 UTC 2026] ERROR Retrying getting updates in 8s...', + ); + expect(r.stamp).toBe('Jun 09 00:14:52'); + expect(r.levelText).toBe('ERROR'); + expect(r.body).toBe('Retrying getting updates in 8s...'); + }); + + it('systemd line: shows the body rather than a bare timestamp', () => { + const r = parseLogLine( + 'Jun 08 23:56:47 ubuntu-4gb-fsn1-1 systemd[1]: Stopping x-ui.service - x-ui Service...', + ); + expect(r.stamp).toBe('Jun 08 23:56:47'); + expect(r.body).toBe('Stopping x-ui.service - x-ui Service...'); + }); + + it('never collapses a journald entry to just its timestamp', () => { + const r = parseLogLine( + 'Jun 09 00:15:00 ubuntu-4gb-fsn1-1 x-ui[72297]: [Tue Jun 9 00:15:00 UTC 2026] ERROR Getting updates: telego: getUpdates: api: 409 "Conflict"', + ); + expect(r.body.length).toBeGreaterThan(0); + expect(r.body).toContain('Conflict'); + }); +}); + +describe('parseLogLine — app-log format (SysLog off)', () => { + it('parses "YYYY/MM/DD HH:MM:SS LEVEL - body"', () => { + const r = parseLogLine('2026/06/09 00:35:09 INFO - mtproto: started mtg for inbound 3 on 0.0.0.0:8443'); + expect(r.date).toBe('2026/06/09'); + expect(r.time).toBe('00:35:09'); + expect(r.levelText).toBe('INFO'); + expect(r.service).toBe('X-UI:'); + expect(r.body).toBe('mtproto: started mtg for inbound 3 on 0.0.0.0:8443'); + }); + + it('handles an empty line without throwing', () => { + const r = parseLogLine(''); + expect(r.stamp).toBe(''); + expect(r.body).toBe(''); + }); +}); diff --git a/install.sh b/install.sh index 70bac7cf2..24d90c9a8 100644 --- a/install.sh +++ b/install.sh @@ -1177,6 +1177,11 @@ install_x-ui() { else systemctl stop x-ui fi + # Kill any leftover mtg (MTProto) sidecars. x-ui runs them outside its own + # lifecycle, so on Linux a stale one can survive the stop and keep holding + # an inbound port with an outdated secret, silently breaking new clients. + # The freshly installed panel respawns a clean mtg per inbound on start. + pkill -f 'mtg-linux-[^ ]* run ' > /dev/null 2>&1 || true rm ${xui_folder}/ -rf fi diff --git a/mtproto/manager.go b/mtproto/manager.go index c5acfbb23..054251a79 100644 --- a/mtproto/manager.go +++ b/mtproto/manager.go @@ -58,6 +58,9 @@ type managed struct { type Manager struct { mu sync.Mutex procs map[int]*managed + // swept records that the one-time startup cleanup of orphaned mtg + // processes (survivors of a previous x-ui run) has already run. + swept bool } var ( @@ -107,9 +110,24 @@ func InstanceFromInbound(ib *model.Inbound) (Instance, bool) { func (m *Manager) Ensure(inst Instance) error { m.mu.Lock() defer m.mu.Unlock() + m.sweepOrphansLocked() return m.ensureLocked(inst) } +// sweepOrphansLocked kills mtg processes left running by a previous x-ui run, +// exactly once per process lifetime and before any of our own mtg are started. +// Because x-ui owns every mtg process, anything alive at this point is an orphan +// that would otherwise keep holding an inbound port with a stale secret. +func (m *Manager) sweepOrphansLocked() { + if m.swept { + return + } + m.swept = true + if n := killStrayMtgProcesses(GetBinaryPath()); n > 0 { + logger.Warningf("mtproto: terminated %d orphaned mtg process(es) from a previous run", n) + } +} + func (m *Manager) ensureLocked(inst Instance) error { fp := inst.fingerprint() if cur, ok := m.procs[inst.Id]; ok { @@ -128,7 +146,7 @@ func (m *Manager) ensureLocked(inst Instance) error { if err := writeConfig(cfgPath, inst.Secret, inst.bindTo(), metricsPort); err != nil { return err } - proc := newProcess(cfgPath) + proc := newProcess(cfgPath, fmt.Sprintf("inbound %d", inst.Id)) if err := proc.Start(); err != nil { return err } @@ -138,7 +156,7 @@ func (m *Manager) ensureLocked(inst Instance) error { fingerprint: fp, metricsPort: metricsPort, } - logger.Info("mtproto: started mtg for inbound", inst.Id, "on", inst.bindTo()) + logger.Infof("mtproto: started mtg for inbound %d on %s", inst.Id, inst.bindTo()) return nil } @@ -150,7 +168,7 @@ func (m *Manager) Remove(id int) { cur.proc.Stop() delete(m.procs, id) _ = os.Remove(configPathForID(id)) - logger.Info("mtproto: stopped mtg for inbound", id) + logger.Infof("mtproto: stopped mtg for inbound %d", id) } } @@ -160,6 +178,7 @@ func (m *Manager) Remove(id int) { func (m *Manager) Reconcile(desired []Instance) { m.mu.Lock() defer m.mu.Unlock() + m.sweepOrphansLocked() want := make(map[int]struct{}, len(desired)) for _, inst := range desired { want[inst.Id] = struct{}{} @@ -173,7 +192,7 @@ func (m *Manager) Reconcile(desired []Instance) { } for _, inst := range desired { if err := m.ensureLocked(inst); err != nil { - logger.Warning("mtproto: reconcile failed for inbound", inst.Id, ":", err) + logger.Warningf("mtproto: reconcile failed for inbound %d: %v", inst.Id, err) } } } diff --git a/mtproto/orphans_linux.go b/mtproto/orphans_linux.go new file mode 100644 index 000000000..518a6a60e --- /dev/null +++ b/mtproto/orphans_linux.go @@ -0,0 +1,79 @@ +//go:build linux + +package mtproto + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +// killStrayMtgProcesses terminates orphaned mtg sidecars left over from a +// previous x-ui run and returns how many were killed. +// +// x-ui starts one mtg process per mtproto inbound outside its own lifecycle, and +// on Linux a child is not guaranteed to die with the panel (there is no +// kill-on-exit, unlike the Windows job object). A survivor keeps holding the +// inbound port with a now-stale secret, so new clients are silently +// domain-fronted to the FakeTLS domain instead of proxied to Telegram. x-ui is +// the sole owner of mtg, so any process matching our binary name at startup is +// an orphan and is safe to kill before we start our own. +// +// binaryPath is the configured mtg path (e.g. "bin/mtg-linux-amd64"); matching +// is done on the executable's base name so it is independent of the bin folder +// and still works after an update has deleted the binary (the running process's +// /proc//exe then reads as " (deleted)", so argv[0] is used too). +func killStrayMtgProcesses(binaryPath string) int { + base := filepath.Base(binaryPath) + if base == "" || base == "." || base == string(filepath.Separator) { + return 0 + } + self := os.Getpid() + entries, err := os.ReadDir("/proc") + if err != nil { + return 0 + } + killed := 0 + for _, e := range entries { + pid, err := strconv.Atoi(e.Name()) + if err != nil || pid == self { + continue + } + if procExeBase(pid) != base && cmdlineArgv0Base(pid) != base { + continue + } + if err := syscall.Kill(pid, syscall.SIGKILL); err == nil { + killed++ + } + } + return killed +} + +// procExeBase returns the base name of /proc//exe, or "" if unreadable. +func procExeBase(pid int) string { + exe, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid)) + if err != nil { + return "" + } + return filepath.Base(exe) +} + +// cmdlineArgv0Base returns the base name of argv[0] from /proc//cmdline, +// the reliable fallback when the binary has been replaced or exe is unreadable. +func cmdlineArgv0Base(pid int) string { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + if err != nil || len(data) == 0 { + return "" + } + argv0 := data + if i := strings.IndexByte(string(data), 0); i >= 0 { + argv0 = data[:i] + } + if len(argv0) == 0 { + return "" + } + return filepath.Base(string(argv0)) +} diff --git a/mtproto/orphans_other.go b/mtproto/orphans_other.go new file mode 100644 index 000000000..b1c2c2e6c --- /dev/null +++ b/mtproto/orphans_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package mtproto + +// killStrayMtgProcesses is a no-op off Linux. On Windows the kill-on-exit job +// object already terminates mtg together with the panel (see +// attachChildLifetime), so orphans do not arise there; other platforms are not +// a supported deployment target for the mtg sidecar. +func killStrayMtgProcesses(_ string) int { return 0 } diff --git a/mtproto/process.go b/mtproto/process.go index 759d4da02..8b260874c 100644 --- a/mtproto/process.go +++ b/mtproto/process.go @@ -49,22 +49,55 @@ var ( forceStopTimeout = 2 * time.Second ) -type lastLineWriter struct { +// procLogWriter consumes the mtg child process's stdout/stderr. It splits the +// stream into lines, forwards each one to the x-ui log — so mtg's own messages, +// including why it cannot reach Telegram, become visible in the panel log viewer +// and journald — and remembers the most recent line for GetResult. +type procLogWriter struct { mu sync.Mutex + label string + buf string lastLine string } -func (w *lastLineWriter) Write(p []byte) (int, error) { - line := strings.TrimSpace(string(p)) - if line != "" { - w.mu.Lock() - w.lastLine = line - w.mu.Unlock() +func (w *procLogWriter) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + w.buf += string(p) + for { + i := strings.IndexByte(w.buf, '\n') + if i < 0 { + break + } + line := w.buf[:i] + w.buf = w.buf[i+1:] + w.emitLocked(line) } return len(p), nil } -func (w *lastLineWriter) LastLine() string { +// Flush emits any buffered partial line; called once the process exits so a +// final un-terminated error line is not lost. +func (w *procLogWriter) Flush() { + w.mu.Lock() + defer w.mu.Unlock() + if w.buf != "" { + line := w.buf + w.buf = "" + w.emitLocked(line) + } +} + +func (w *procLogWriter) emitLocked(line string) { + trimmed := strings.TrimSpace(strings.TrimRight(line, "\r")) + if trimmed == "" { + return + } + w.lastLine = trimmed + logger.Infof("mtproto: mtg %s | %s", w.label, trimmed) +} + +func (w *procLogWriter) LastLine() string { w.mu.Lock() defer w.mu.Unlock() return w.lastLine @@ -75,15 +108,15 @@ type Process struct { cmd *exec.Cmd done chan struct{} configPath string - logWriter *lastLineWriter + logWriter *procLogWriter exitErr error intentionalStop atomic.Bool } -func newProcess(configPath string) *Process { +func newProcess(configPath, label string) *Process { return &Process{ configPath: configPath, - logWriter: &lastLineWriter{}, + logWriter: &procLogWriter{label: label}, } } @@ -141,6 +174,7 @@ func (p *Process) Start() error { func (p *Process) wait(cmd *exec.Cmd) { defer close(p.done) err := cmd.Wait() + p.logWriter.Flush() if err == nil || p.intentionalStop.Load() { return } @@ -150,7 +184,7 @@ func (p *Process) wait(cmd *exec.Cmd) { return } } - logger.Error("mtproto: mtg process exited:", err) + logger.Errorf("mtproto: mtg process exited: %v", err) p.exitErr = err } diff --git a/mtproto/process_windows.go b/mtproto/process_windows.go index 3e255db0a..f19320f80 100644 --- a/mtproto/process_windows.go +++ b/mtproto/process_windows.go @@ -51,16 +51,16 @@ func attachChildLifetime(cmd *exec.Cmd) { } job, err := ensureKillOnExitJob() if err != nil { - logger.Warning("mtproto: kill-on-exit job unavailable:", err) + logger.Warningf("mtproto: kill-on-exit job unavailable: %v", err) return } h, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(cmd.Process.Pid)) if err != nil { - logger.Warning("mtproto: OpenProcess for job attach failed:", err) + logger.Warningf("mtproto: OpenProcess for job attach failed: %v", err) return } defer windows.CloseHandle(h) if err := windows.AssignProcessToJobObject(job, h); err != nil { - logger.Warning("mtproto: AssignProcessToJobObject failed:", err) + logger.Warningf("mtproto: AssignProcessToJobObject failed: %v", err) } } diff --git a/sub/subService.go b/sub/subService.go index 2d0d649b8..65397c9e1 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -381,8 +381,7 @@ func (s *SubService) GetLink(inbound *model.Inbound, email string) string { } // genMtprotoLink builds a Telegram proxy deep link for an mtproto inbound: -// tg://proxy?server=&port=&secret=. -func (s *SubService) genMtprotoLink(inbound *model.Inbound, email string) string { +func (s *SubService) genMtprotoLink(inbound *model.Inbound, _ string) string { if inbound.Protocol != model.MTProto { return "" } @@ -403,7 +402,7 @@ func (s *SubService) genMtprotoLink(inbound *model.Inbound, email string) string "port": fmt.Sprintf("%d", inbound.Port), "secret": secret, } - return buildLinkWithParams("tg://proxy", params, s.genRemark(inbound, email, "")) + return buildLinkWithParams("tg://proxy", params, "") } // Protocol link generators are intentionally ordered as: diff --git a/update.sh b/update.sh index 9473998f2..36b501c1f 100755 --- a/update.sh +++ b/update.sh @@ -890,6 +890,11 @@ update_x-ui() { _fail "ERROR: x-ui systemd unit not installed." fi fi + # Kill any leftover mtg (MTProto) sidecars. x-ui runs them outside its own + # lifecycle, so on Linux a stale one can survive the stop and keep holding + # an inbound port with an outdated secret, silently breaking new clients. + # The new panel respawns a clean mtg per inbound on next start. + pkill -f 'mtg-linux-[^ ]* run ' > /dev/null 2>&1 || true echo -e "${green}Removing old x-ui version...${plain}" rm ${xui_folder} -f > /dev/null 2>&1 rm ${xui_folder}/x-ui.service -f > /dev/null 2>&1 diff --git a/x-ui.sh b/x-ui.sh index eb70dbc99..dd9b69b88 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -864,6 +864,7 @@ show_status() { ;; esac show_xray_status + show_mtproto_status } show_enable_status() { @@ -897,6 +898,34 @@ show_xray_status() { fi } +# show_mtproto_status reports each mtproto inbound's mtg sidecar (one process per +# inbound, run outside xray). Silent when no mtproto inbound is configured. +show_mtproto_status() { + local cfg_dir="${xui_folder}/bin/mtproto" + local cfgs=() + if [[ -d "${cfg_dir}" ]]; then + for f in "${cfg_dir}"/mtg-*.toml; do + [[ -e "$f" ]] && cfgs+=("$f") + done + fi + [[ ${#cfgs[@]} -eq 0 ]] && return + + local running + running=$(ps -ef | grep "mtg-linux" | grep -v "grep" | grep -oE 'mtg-[0-9]+\.toml') + for f in "${cfgs[@]}"; do + local name id bind + name=$(basename "$f") + id=$(echo "${name}" | sed -E 's/mtg-([0-9]+)\.toml/\1/') + bind=$(grep -E '^[[:space:]]*bind-to' "$f" | head -1 | cut -d'"' -f2) + if echo "${running}" | grep -qx "${name}"; then + echo -e "mtproto inbound ${id} (${bind}): ${green}Running${plain}" + else + echo -e "mtproto inbound ${id} (${bind}): ${red}Not Running${plain}" + fi + done + echo -e " ${yellow}mtg logs:${plain} journalctl -u x-ui --no-pager -n 200 | grep -i mtproto" +} + firewall_menu() { echo -e "${green}\t1.${plain} ${green}Install${plain} Firewall" echo -e "${green}\t2.${plain} Port List [numbered]"