Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
fix(backup): close critical backup/restore bugs
Audit found 5 HIGH-severity backup bugs. Fixed:
B1 (HIGH — silent data loss): RestoreDatabase handler returned 200 OK
but the panel's GORM pool kept writing to the old (unlinked) SQLite
inode. Every write between restore and a manual 'systemctl restart'
was silently lost. Fix: handler now os.Exit(0)s 500ms after responding
so systemd's Restart=always reboots the panel with the new DB. The
frontend polls /api/v1/health and auto-reloads the page when the
panel is back (no more dismissable 'remember to restart' modal).
B4 (HIGH): RestoreDatabase didn't delete stale -journal / -wal / -shm
siblings. SQLite could roll back the just-restored DB to a stale
transaction state on next open. Fix: remove all three suffixes
(ignore not-exist errors) after the rename.
B5 (HIGH): No SQLite magic-byte validation — any file (txt, corrupt
db, etc.) was accepted as a 'database' and silently overwrote the
live DB, then the panel boot-looped on the bad file. Fix: validate
the 16-byte 'SQLite format 3\0' header before rename; return 400
on mismatch.
B8 (HIGH — data loss): cleanupLocalBackups used OR semantics for
keep+age. With keep=3 + older_than_days=7 and all 3 backups older
than 7 days, ALL 3 were deleted — operator lost every snapshot.
Fix: change to AND semantics. The newest 'keep' entries ALWAYS
survive even if they're older than the age threshold. Age only
applies to entries beyond the keep window.
C2 (HIGH): Telegram sendBackup had no size guard. DB > 50 MB fails
the Bot API sendDocument limit with HTTP 413, and the scheduled
backup silently stopped working forever (error only in journalctl).
Fix: check buf.Len() > 50 MB before sending; if exceeded, send a
text notice to all chats instead ('DB too large to attach: N MB;
download via the web UI').
Also fixed:
- B6/B7: mihomo config restore was best-effort (errors silently
discarded). Now returns proper errors + a MihomoSkipped flag when
the zip contained mihomo-config.yaml but the panel has no
mihomo.config path configured.
- B13: RestoreDatabase MkdirAll mode 0o750 → 0o700 (matches
database.go, DB contains password hashes + secrets).
- B10: frontend restore shows 'Restarting panel…' modal with
auto-reload indicator; surfaces mihomo_config_skipped message.
Tests: 8 new unit tests in backup_test.go covering:
- RestoreDatabase rejects non-SQLite / empty uploads
- RestoreDatabase accepts raw SQLite + removes -wal/-shm/-journal
- RestoreDatabase restores mihomo config from zip
- RestoreDatabase reports MihomoSkipped when path empty
- cleanupLocalBackups keep+age AND semantics (newest N always safe)
- cleanupLocalBackups keep-only / age-only paths
Full suite: 23/23 backend packages PASS, frontend build + lint clean.
Not addressed in this commit (follow-ups):
- B2/C4: WriteZip still reads live DB without VACUUM INTO / Online
Backup API (inconsistent snapshot risk). Needs sqlite driver work.
- B3/C1: backup zip is unencrypted and contains bot_token + 2FA
secrets. Needs passphrase encryption or sanitized dump.
- A1: shell snapshot has no sha256 checksum verification on restore.
- B11: backup-meta.txt missing panel/schema version.
3m-ui
Languages / 语言: README · English · 简体中文 · 繁體中文 · 日本語 · 한국어 · Español · Français · Deutsch · Русский · Português (Brasil) · Tiếng Việt · Bahasa Indonesia · ไทย · Türkçe · العربية · हिन्दी · Polski · Українська
Mihomo server web management panel
Stable first: install and update use formal releases by default. Pre-releases require an explicit choice. Full bundles and Docker images ship a pinned Mihomo core — see installation docs.
Lightweight and self-hosted. Manage Mihomo listeners, users, subscriptions, and runtime status on Linux.
Documentation: https://3m-ui.top/docs/ · Website
Features
| Area | Capabilities |
|---|---|
| Nodes | Protocol-registry listeners (VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC, …); REALITY; TLS self-signed; batch certificate; traffic multiplier; transport validation |
| Users | Quick create (auto username/password/UUID, optional bind-all nodes), bind nodes, traffic/expiry, per-node usage (billed = raw × node multiplier), IP/sub-pull limits, start-on-first-use, cycle renew/reset, groups/tags, external sub merge, batch ops, subscription tokens |
| Subscriptions | UA routing for Clash/Mihomo YAML, v2ray Base64 (?target=v2ray always Base64), sing-box JSON; optional ?target=; HTML info page; TUIC/HY2 share TLS params |
| Config | Generate → validate → apply; rollback previous config.yaml on failure |
| Telegram | Alerts and bot commands (token + chat IDs) |
| Cluster | Register remote panels, health checks, node mirror sync, merged subscriptions; push local nodes by name (created disabled on remote) |
| Ops | Core lifecycle, Geo, panel SSL/ACME, backup, Cloudflare WARP one-click register (WireGuard/MASQUE YAML) |
Install
curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/scripts/install.sh | sudo sh
After install, update with:
sudo 3m-ui update
# sudo 3m-ui update v1.0.2
Default panel: http://SERVER_IP:8080/ — user admin, one-time random password printed at install (change on first login).
Install with an AI assistant prompt
Copy the prompt into ChatGPT / Claude / Cursor (and state your OS, e.g. Ubuntu 22.04, and whether you have root SSH). The assistant should only use the official installer — not rewrite URLs or invent third-party commands. The full prompt and usage notes live in the AI install prompt doc.
Skip manual copy-paste — load the plain-text prompt straight to your clipboard with curl:
curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/docs/ai-install-prompt.en.txt | pbcopy # macOS
curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/docs/ai-install-prompt.en.txt | xclip -sel clip # Linux X11
curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/docs/ai-install-prompt.en.txt | wl-copy # Linux Wayland
The prompt only ever runs the official one-liner below; all downloads verify SHA256SUMS:
curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/scripts/install.sh | sudo sh
For the optional pre-release / test channel, see installation docs.
Build from source
cd frontend && npm install && npm run build
cp -r dist ../backend/cmd/server/web/dist
cd ../backend && CGO_ENABLED=0 go build -tags sqlite_modernc -trimpath -ldflags='-s -w' -o ../3m-ui ./cmd/server
Static Linux binaries for multiple architectures are on Releases.
Acknowledgements
- Mihomo — core engine and listener model
- clashmeta-inbound — listener examples
- 3x-ui / s-ui — panel UX inspiration
- Gin, GORM, React, Ant Design
- Lucide — icons (
lucide-react) - AIsouler/MyClash — Mihomo routing config / lite rule ideas
- echs-top/proxy — Mihomo rules and proxy-group schemes
- YiXuanZX/rules — Rule sets and mihomo routing templates
- Zustand, golang-jwt/jwt
Extra docs (limits / certs / subscriptions)
-
Go, Node.js, and the open-source community
Contributors: Contributors
Star History
License
Third-party notices: THIRD-PARTY-NOTICES.md.
