All notable changes to this project are documented in this file.
This is the English-language changelog. See CHANGELOG-RU.md for Russian and
CHANGELOG-ZH.md for Simplified Chinese.
- [P0] Hardened SSRF filtering and dial-time validation; tightened backup restore path/symlink checks.
- [P1] Hardened CSRF/session lifecycle behavior, including token renewal after logout/logout-all and tighter WS token handling.
- [P2] Expanded secret/settings safety checks and migration guardrails.
- [P3] Added listen fallback auditing and restart-path consistency hardening.
- [P0] Closed race-condition paths in tracker/session options/audit writer code.
- [P1] Stabilized realtime fallback behavior and frontend unit-test harness.
- [P2] Added reset hooks, tracker wait guards, and foreign-key migration checks.
- [P3] Unified restart scheduling and reduced global side effects with an initial DI slice.
- [P4] Moved the remaining service runtime globals behind a DI-compatible runtime while preserving zero-value service compatibility.
- [P5] Completed the logging backend cleanup without changing API endpoint behavior.
- [P0] Improved trusted-proxy handling and safer import error classification.
- [P1] Tightened realtime/session/CSRF flows and Telegram error taxonomy.
- [P2] Normalized batching and timeout behavior in heavy data paths.
- [P3] Added an initial slog adapter path for gradual migration from go-logging.
- [P4] Promoted
slogto the logger facade, leavingop/go-loggingisolated behind deprecated compatibility APIs. - [P5] Removed deprecated
logger.InitLogger/logger.GetLogger, moved facade output fully onto standardlog/slog, and kept panel/core log buffering. - [P5] Removed the legacy
github.com/op/go-loggingmodule fromgo.modandgo.sum. - [P4] Added a checked sing-box tracker revalidation policy for
github.com/sagernet/sing-box v1.13.11. - [P4] Added a checked SemVer release/version policy and prevented migration
code from downgrading future
settings.versionvalues.
- [P1] Fixed Vitest harness configuration in
frontend/vitest.config.ts. - [P1/P2] Aligned CSRF cache clearing, request dedupe boundaries, and realtime degraded-mode behavior.
- Baseline and phase reports:
plans/lint-baseline.txtplans/lint-baseline-normalized.txtplans/fix-validation.txt(P0)plans/p1-validation.txt(P1)plans/p2-validation.txt(P2)plans/p3-architecture-validation.txt(P3)plans/p4-architecture-debt-validation.txt(P4)plans/p5-logging-cleanup-validation.txt(P5)
- Each phase includes targeted checks and a final command pass set in its validation artifact.
- Prefix each completed item with a phase tag:
[P0],[P1],[P2],[P3],[P4],[P5]. - Add references in this format:
(ref: <commit|PR|chat-id>). - Use combined tags for cross-phase items, for example
[P1/P2]. - Keep deferred architecture items in a separate section and do not mix them into completed bullets.
- Treat P0->P5 as one release window; create a full SQLite backup before upgrade.
- Validate behavior changes around session/CSRF/realtime and listen fallback in staging before production rollout.
- Use the phase validation files above as upgrade verification evidence.
- External Go integrations that imported
logger.InitLoggerorlogger.GetLoggermust migrate tologger.Init(logger.Level*),logger.Slog(source), orslog.Default().
- Restore the pre-window SQLite snapshot and previous binary/image.
- If rollback crosses session/token behavior changes, invalidate active sessions after downgrade and rotate admin credentials.
- [P5] No P5 scope item is deferred. The legacy
op/go-loggingdependency and deprecated logger compatibility APIs have been removed.
- Use domain sections: Security, Reliability/Data integrity, API/Runtime, Frontend, Tests.
- Tag each bullet with phase marker(s) and append traceability refs.
- Add explicit
Upgrade notesandRollbackfor the full aggregated window.
- TUIC subscription/share links and Clash export now include
udp_relay_mode, preserving the configured value and defaulting generated links toquicwhen it is absent.
- Scheduled and manual encrypted SQLite database backup to Telegram. The backup
passphrase is configured only in the Telegram tab, and the feature is off by
default. New settings and defaults:
telegramBackupEnabled="false",telegramBackupPassphrase="",telegramBackupCron="",telegramBackupExcludeTables="stats,client_ips,audit_events,changes", andtelegramBackupMaxSizeMB="45". New manual trigger routes:POST /api/telegram/backup/runandPOST /apiv2/telegram/backup/run. - Restore now auto-detects uploaded
SUI-TGBKP\x00backup envelopes and shows a Backup passphrase field in Backup & Restore. Plaintext.dbuploads are still accepted without that field. - The existing Backup button can optionally download the same encrypted
envelope via checkbox "Encrypt with Telegram backup passphrase". The checkbox
is unchecked by default, plaintext download behavior is preserved, and the
existing
getdbendpoint uses the new non-breaking query parameterencryptTelegramBackup=true. - The main release binary now includes
s-ui decrypt-backupfor offline envelope decryption. No separate artifact is required. docs/scope-matrix.mdnow documents thetg_backup_runoperation.
- BREAKING: legacy
POST /api/telegram/backupandPOST /apiv2/telegram/backupnow delegate to the new Telegram backup service.backupKeyis removed from every response,telegramBackupEnabled=trueis required, and successful responses includetrigger="manual". There is no compatibility window. Strict migration step: after upgrading, enabletelegramBackupEnabledin the Telegram tab; otherwise the legacy call returns HTTP 503 witherrorClass=disabled. util/secretboxnow hasEncryptBytesandDecryptByteshelpers for byte-oriented secret handling.api/rateLimit.gohas a shared manual Telegram backup bucket for all four manual trigger routes: 3 attempts per 60 seconds withRetry-After.- New audit event types:
tg_backup_sent,tg_backup_failed,tg_backup_passphrase_changed,tg_backup_manual_encrypted, andtg_backup_restore_failed.
- Back up the SQLite database before upgrading. If using the system service,
stop
s-ui, copys-ui.dbplus any-wal/-shmsidecars, then start the service again. - Telegram database backup remains disabled until
telegramBackupEnabledis turned on in the Telegram tab and a Backup passphrase is configured. - Existing integrations that call the legacy Telegram backup endpoints must
handle the removed
backupKeyfield and the new HTTP 503disabledresponse until the setting is enabled.
- Restore the pre-upgrade SQLite backup and previous binary/image if rollback is required.
- Encrypted
.db.aesfiles remain decryptable with the passphrase that created them via any binary containings-ui decrypt-backup.
UNIQUE constraint failed: client_ips.client_name, client_ips.ipduring the 3x-ui pre-import auto-backup.client_ips.ipis a legacy column kept only for backfill since 1.5.x and is empty for new rows; the canonical unique key is(client_name, ip_hash). The model still carried an obsoletegorm:"index:idx_client_ips_client_ip,unique"on(client_name, ip), sodatabase/backup.gore-created the bad index in the temporary backup DB viaAutoMigrateand the chunked copy ofclient_ipsfailed as soon as one client owned more than one row with emptyip. After this hotfix the only unique index on the model is(client_name, ip_hash).
database/model/model.go— removed the legacyidx_client_ips_client_ip,uniquetag fromClientIP.ClientNameandClientIP.IP.cmd/migration/1_5.go— the1.5schema migration drops the legacyidx_client_ips_client_ipand creates a partial non-uniqueidx_client_ips_client_legacy_ip ON client_ips(client_name, ip) WHERE ip IS NOT NULL AND ip != ''for fast legacy lookups. The migration is fully idempotent (DROP INDEX IF EXISTS/CREATE INDEX IF NOT EXISTS), so installs already on1.5.2-betare-run it cleanly when the runner re-enters the1.5branch on the next start.database/db.go: ensureIndexes— drops the obsolete unique index at everyInitDB. This is a runtime safety net for installs that bypassMigrateDb(for example, restoring an older backup outside the panel) and ensures the temporary backup DB built byGetDb("")no longer carries the bad index either.
- No new columns, tables, settings, endpoints, scopes or environment variables. Combine with the previous hotfix's chunked-backup helpers.
- Regression coverage:
cmd/migration/migration_1_5_test.goproves the obsolete index is no longer created and accepts multiple empty-iprows for one client.database/db_test.go: TestInitDBDropsObsoleteClientIPUniqueIndexboots an old-shape DB with the legacy unique index already in place and verifiesInitDBremoves it.database/backup_test.go: TestGetDbHandlesHashedClientIPsWithEmptyLegacyIProunds-trips multipleip_hashrows with emptyipfor the same client throughGetDb("").
too many SQL variablesduring database backup and 3x-ui migration on installs with largestats,client_ips,audit_events,changesorclientstables. The backup routine indatabase/backup.gono longer emits a single multi-rowINSERT VALUES (...)that exceeded SQLite's compile-time variable limit (SQLITE_MAX_VARIABLE_NUMBER = 999inmattn/go-sqlite3). This unblocksWritePreImportBackupand the 3x-ui migration on production-sized databases (≈40k+ rows instats).- Stale
index.htmlafter upgrade no longer breaks the Clients tab./<base>/assets/*now returns a real 404 for missing files instead of falling through to the SPA fallback, so browsers stop receivingtext/htmlfor JS module requests (Failed to load module script/Failed to fetch dynamically imported module).index.htmlis served withCache-Control: no-cache, no-store, must-revalidate; hashed assets keeppublic, max-age=31536000, immutable. - The Vue Router now listens for
vite:preloadErrorand triggers one guardedwindow.location.reload()(asessionStorageflag prevents reload loops), so tabs left over from the previous build pick up the new bundle automatically. service/client.go(addbulk,editbulk,ResetClients,DepleteClients) anddatabase/importxui/history_routing.go(historical traffic import) now chunk their bulkSave/Createcalls through newdatabase/bulk.gohelpers (SafeSQLiteBatchSize,CreateInBatchesSafe,SaveInBatchesSafe). Reset/deplete jobs and historical-stats imports no longer fail on installs with thousands of clients.
- No schema migrations, new endpoints, scopes or environment variables.
- A regression test (
database/backup_test.go) now creates ≈43kstatsrows plus 5kclient_ipsand verifiesGetDb("")round-trips them.
- 3x-ui configuration import:
s-ui import-xuiCLI,POST /api/import-xuiHTTP endpoint, and a "Migrate from 3x-ui" section in the Backup & Restore modal. Import runs in one transaction with auto-backup, supportsmerge/replace/skipstrategies, and writesxui_importaudit events. - Full migration wizard at
/migrate-xui: per-object plan/apply withSource.Hashvalidation, WebSocketxui_import_progressevents, JSON preview, rollback to the auto-backup, and downloadable JSON/Markdown reports. Reports live inaudit_events.details. - Remote 3x-ui sources via
--remote ssh://...and--remote http://...(xuihttp), pluss-ui sync-xuifor scheduled incremental syncs. SSH uses host-key TOFU with axui_known_hoststable; HTTP supports the 3x-ui login flow. - Encrypted
xui_sync_profiles(AES-GCM with HKDF-SHA256 fromconfig.GetSecret(), override viaXUI_PROFILE_KEY_FILE),cmd/migration/1_7.goschema migration,xuiSyncJobcron job, and the/migrate-xui/scheduleUI for managing profiles. - Best-effort historical traffic import (
client_traffics/outbound_traffics→statsaggregates) and Xray routing rules import (geosite:*/geoip:*, block, direct) into sing-boxroute.rules/dns.servers. Balancers are reported as warnings. - New
xui_remotetoken scope required for all remote/sync endpoints; local/api/import-xui*endpoints stay underdatabase/admin.XUI_DISABLE_REMOTE=1disables remote sources and the cron mode.
test-db/holds local 3x-ui import fixtures with real production data and is no longer tracked in the repository (see.gitignore). Tests that need those fixtures are skipped automatically on CI; run them locally with the fixtures present intest-db/.
- Telegram notifications now use an async bounded queue with retry/backoff and audited overflow/failure events, so login and other handlers are not blocked by Telegram network failures.
- Telegram event payloads, audit details, change history payloads, and backup captions are redacted so bot tokens, proxy credentials, API tokens, and backup keys are not written to logs, audit, changes, or captions.
- Realtime WebSocket handshakes now enforce Origin allow-listing, per-IP handshake rate limits, one-time token replay rejection, ping/pong heartbeat, idle close, and session-rotation close-all semantics.
GET /api/security/auditnow has admin scope gating for API-token requests, endpoint rate limiting, cursor pagination, and validatedevent/severityfilters.POST /api/telegram/testis admin-scoped for API-token requests and writes an audit event containing only success/errorClass metadata.- Security headers middleware was added for the panel and subscription server, with no-store cache handling on subscription responses.
- Fresh-install admin passwords are no longer written to application logs; the
generated password is saved once to
<dataDir>/initial-admin.txtwith owner-only permissions, and startup only prints the file path. s-ui admin -showno longer prints the stored password hash; it shows the username and reset guidance instead.- The frontend clears cached CSRF tokens after logout, logout-all, and realtime session-rotation closes so the next mutating request fetches a new token.
install.shnow downloads the release*.sha256file and verifies the Linux tarball withsha256sum -cbefore extraction.- Added a pull-request CI workflow for Go vet/race tests and frontend lint/unit/build checks.
- Admin web sessions now use a SQLite-backed server-side store; the browser
cookie contains only a signed session ID and session data lives in the local
sessionstable.
- Client IP history is stored with salted hashes by default, raw display is disabled unless explicitly opted in, and retention is handled by cron GC.
- IP limiting still starts in monitor mode; enforce mode rejects only new over-limit connections and does not close active sessions.
- Subscription settings from the design are now persisted and used by link, JSON, and Clash subscription responses. Subscription paths are validated against reserved prefixes, headers are sanitized centrally, and the per-IP subscription rate limit is configurable.
POST /api/rotateSubSecretrotates per-client subscription secrets with an audit event. WhensubSecretRequired=true, legacy name URLs return 404.
- Telegram egress can use validated HTTP/HTTPS/SOCKS5 proxy settings stored as
secret-aware settings. Error classes are normalized to
unauthorized,chat_not_found,rate_limited,network, orunknown. - CPU hysteresis alerts, scheduled Telegram reports, and encrypted Telegram database backup export are implemented and remain opt-in.
- Observability history now uses bounded buckets (
2s,30s,1m,5m), sampled by cron, with validated metric/bucket/since API parameters. GET /api/logsaccepts boundedcount,level,source, and substringfilterparameters;GET /api/versionperforms a fail-soft 1h-cached GitHub release check.- Database import/export now enforces a 64 MiB cap, SQLite magic validation,
temporary staging, read-only
PRAGMA integrity_check, and audit events.
- Added the realtime frontend store with websocket reconnect/degraded states and polling fallback.
- Added secret-aware settings fields that show
••• stored •••and never submit the placeholder as a secret value. - Added IP history modal with raw-IP masking by default and confirmation before showing raw IPs to admins.
- Added Telegram settings and Audit views. The Audit view uses cursor
pagination and server-side
event/severityfilters.
- Docker builds now include a
CRONET_GO_VERSIONargument synchronized withrelease.ymland document the dated fallback to upstream's latest prebuiltlibcronetasset until commit-addressable assets are available. - The Docker image default
TZnow matches the panel defaultEurope/Moscow. - The manual release workflow now defaults to tag
v1.5.1-beta. - The container entrypoint no longer runs a duplicate automatic migration
before startup; use
SUI_MIGRATE_ONLY=1for a manual migration-only run. - The migration runner now performs the SQLite WAL checkpoint only after a
successful transaction commit, fixing
database table is lockedfailures seen during1.4.xto1.5.1-betaupgrades. - The admin frontend no longer depends on an inline base-path script, so the strict Content Security Policy is honored and custom web paths route API, CSRF and realtime fallback requests correctly.
- Added or extended regression coverage for secret settings migration, redaction, IP monitor cache/enforce behavior, audit filtering/rate limits, subscription header injection and 404 legacy URL behavior, realtime Origin, replay token and heartbeat behavior, migrations, and frontend websocket/IP helper behavior.
- Verification in this workspace:
go vet ./...,go test ./...,npm run test:unit,npm run build, andnpm run lintpass. Race tests require CGO and a C compiler; this Windows workspace currently lacksgcc.
- Back up the SQLite database before upgrading. If using the system service,
stop
s-ui, copys-ui.dbplus any-wal/-shmsidecars, then start the service again. - Legacy
/apiv2/*Tokenheader support remains temporary. Move clients toAuthorization: Bearer <token>before the Sunset date:Sat, 15 Aug 2026 00:00:00 GMT. - All new features remain off by default except realtime websocket support with frontend polling fallback and monitor-only IP tracking.
- Added an Admins panel action to invalidate all admin web sessions at once. The action rotates the session generation and clears the initiator's current cookie; API tokens are not revoked.
- Added an AES-GCM/HKDF secretbox helper for sensitive settings. New
secret-aware settings are encrypted with
SUI_SECRETBOX_KEYwhen set, or with the legacysettings.secretcompatibility key with a startup warning. - Secret-aware settings are masked from
api/settingsas<key>HasSecret; saving an empty value keeps the previously stored secret. - Added the
audit_eventstable, redaction helper, retention setting, and/api/security/auditendpoint. Login, logout, logout-all-admins, credential changes, and API token create/delete actions now write redacted audit events. - Added CSRF protection for browser
/api/*mutating requests.GET /api/csrfissues a session-bound token, frontend requests send it asX-CSRF-Token, and invalid or expired tokens return HTTP 403. Bearer-token/apiv2/*requests are not affected. - API tokens are now migrated from plaintext to salted SHA-256 hashes using
the per-install
installSalt; new tokens are shown only once, stored as hash/prefix metadata, and can be enabled or disabled from the Admins UI. /apiv2/*now acceptsAuthorization: Bearer <token>as the primary API token transport. The legacyTokenheader still works, emits audit events, and returnsDeprecationplusSunset: Sat, 15 Aug 2026 00:00:00 GMT.- Added per-client subscription secrets. New
/sub/<secret>,/sub/json/<secret>,/sub/clash/<secret>,/json/<secret>, and/clash/<secret>routes are supported; legacy/sub/<name>remains enabled untilsubSecretRequired=true. - Subscription endpoints now sanitize response headers, validate configured subscription paths, and apply a per-IP rate limit.
- Added grouped API route placeholders for the
1.5.0security, notification, observability, and bulk outbound-check work while preserving the existing one-level/api/<action>endpoints. - Added
GET /api/observability/history,GET /api/observability/core-history, andGET /api/version. - Added
POST /api/checkOutboundsfor bounded bulk outbound checks with concurrency8, per-outbound timeout5s, total timeout60s, and an HTTPS/public-IP target validator. - Added disabled-by-default Telegram notification service and
POST /api/telegram/test. Bot token and proxy-related settings are secret-aware; login, logout-all-admins, and core restart events notify only when Telegram is explicitly enabled. - Added authenticated realtime WebSocket foundation under
/api/realtime/ws-tokenand/api/realtime/wswith one-time tokens, bounded client queues, per-user/per-IP connection limits, and frontend polling fallback.logoutAllAdminscloses active realtime sockets with close code4401. - Added batched client IP monitoring with
client_ips, per-clientlimitIpandipLimitMode, last-online/IP-count metadata, Admins-audited clear action, and Clients UI controls.monitoris the default mode;enforcerejects only new over-limit connections and never closes active connections.
install.shand thes-uimanagement menu now also offer Chinese as option 3. 中文;SUI_LANG=zhis supported for non-interactive installs.
This release updates the embedded sing-box runtime from v1.13.4 to
v1.13.11 and keeps the panel, REST API, frontend forms, and database
schema unchanged.
- Updated
github.com/sagernet/sing-boxtov1.13.11. - Accepted the matching upstream dependency set, including
sing v0.8.9,sing-tun v0.8.9,sing-quic v0.6.1, and the April 2026cronet-gomodules required by NaiveProxy. - Pinned the Linux release workflow to the full
cronet-gocommite4926ba205fae5351e3d3eeafff7e7029654424aso release builds do not use a short commit prefix for the source checkout.
- No database migration is required; stored inbound/outbound/endpoint/service
JSON remains compatible with
sing-box v1.13.11. - No web UI fields were added because
sing-box 1.13.5through1.13.11only contain fixes and runtime updates, including the fake-ip DNS fix, NaiveProxy update, and process searcher regression fix. - Production upgrades should deploy the full release archive or rebuilt image
so the updated
libcronet.so/libcronet.dllstays in sync with the new binary.
go mod verifygo test ./...go test -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_purego,with_tailscale" ./...
This release rewrites large parts of the auth, transaction, and runtime
control flow, hardens the external-subscription fetcher against SSRF,
and renames the Go module to github.com/deposist/s-ui-x.
The full backend test suite (go test, go test -race,
go test -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_tailscale")
and the full frontend pipeline (npm ci, npm run build, npm run lint,
npm audit --audit-level=high) pass clean.
- Plaintext passwords replaced with bcrypt; existing accounts migrate transparently on first successful login.
- First-run admin password is randomly generated and printed once to the
application log (no more shipped
admin/admin). - Login rate limiter (5 failures / 15 minutes / 15 minutes block) with bounded memory.
- Bilingual (English/Russian)
install.shands-uimanagement menu; language pickable on first run, switchable from menu item 21. Language, persisted in/etc/s-ui/lang. Default language is English. - Default panel timezone changed from
Asia/ShanghaitoEurope/Moscow. - Default frontend locale changed from Simplified Chinese to English
(existing installations keep their saved
localStorage.locale). - External subscription URL fetcher rejects private/loopback/link-local targets and re-validates the resolved IP at dial time, blocking DNS-rebinding attacks.
- Configuration saves no longer leave the panel and sing-box out of sync on commit/start failures.
- Race-free core lifecycle, online-stats tracking, last-update bookkeeping, and v2 token store.
- Frontend code splitting re-enabled;
v-htmlremoved from the remaining surfaces;AbortControllerreplaces deprecatedaxios.CancelToken.
- Module path:
github.com/alireza0/s-ui→github.com/deposist/s-ui-x. Source consumers must update imports. Pre-built binaries are unaffected. - Default admin password: on a fresh database, a random 24-character
password is generated. Look for the line
created initial admin user. username=admin password=...in the application log on first start. Existing databases keep their configured admin user; nothing is reset. X-Forwarded-For: ignored unlessSUI_TRUSTED_PROXIESlists the immediate client. When set, the chain is walked right-to-left and the first non-trusted hop wins. Previously the leftmost (easily spoofed) value was returned.- Login lockout: 5 failed logins from the same client IP within 15 minutes block that IP for 15 minutes.
- Subscription fetcher TLS:
InsecureSkipVerifywas removed. Self-signed origins must now use a certificate trusted by the system store. - Subscription fetcher private targets: blocked by default. Set
SUI_ALLOW_PRIVATE_SUB_URLS=trueto opt back in (e.g. for127.0.0.1origins on the same host). - Sub fetcher size cap: responses larger than 4 MiB are rejected.
- Cookie store: cookies are now
HttpOnly,SameSite=Lax, andSecurewhen the request is HTTPS (directly or via a trusted proxy that sentX-Forwarded-Proto: https). - Frontend dedupe: only
GET/HEAD/OPTIONSrequests are deduped; concurrent mutating requests no longer cancel each other.
| Severity | Change |
|---|---|
| High | Replaced plaintext password storage with bcrypt hashes (util/common/password.go). Existing entries are detected via the bcrypt: prefix or the $2[aby]$ cost markers. |
| High | Lazy migration: a successful login with an unhashed password updates the DB record to a bcrypt hash. |
| High | Fixed admin/admin default removed; first-run admin password is randomly generated by common.Random(24) and logged once (database/db.go.initUser). |
| High | Login rate limiter introduced (api/rateLimit.go), with periodic state pruning and a hard cap of 4096 tracked keys to prevent unbounded memory growth. |
| High | Hardened session cookies with HttpOnly, SameSite=Lax, and HTTPS-aware Secure (api/session.go). |
| High | X-Forwarded-For is only consulted when SUI_TRUSTED_PROXIES is set; the parser now walks the chain right-to-left and returns the first non-trusted hop instead of the easily spoofed leftmost value (api/utils.go). |
| High | Replaced unsafe SQL string concatenation with parameterized queries in service/config.go.GetChanges and service/config.go.CheckChanges. |
| High | Static identifier allow-list inside the inbound user-fetch SQL builder (service/inbounds.go.fetchUsersByCondition) so future inbound types cannot become a SQL-injection vector. |
| High | Removed default TLS verification bypass for external subscription fetches (util/subToJson.go). |
| High | External subscription URL validation: HTTP/HTTPS only, blocks localhost/private/link-local/multicast/unspecified by default, opt-in via SUI_ALLOW_PRIVATE_SUB_URLS=true, response capped at 4 MiB. |
| High | DNS-rebinding-resistant dialer: a custom http.Transport.DialContext re-validates each resolved IP and dials the validated address directly, so an attacker DNS that swaps records between validation and dial cannot escape the filter. |
| Medium | Replaced error swallowing in WarpService.getWarpInfo/RegisterWarp/SetWarpLicense with explicit status-code and JSON-parse checks; replaced manual JSON formatting with encoding/json to avoid escaping bugs. |
| Medium | Domain validator middleware now compares case-insensitively and handles bare IPv6 hosts. |
- Backup export now includes the
servicesand APItokenstables (database/backup.go). - Backup import (UI: Backup → Restore) now also runs the schema migrations and the post-migration adapter (
database.AdaptToCurrentVersion) automatically. Old backups (S-UI 1.0/1.1/1.2/1.3 layouts, plaintext passwords, missingservices/tokenstables, missingversionrow) are upgraded to the current shape on the fly. If migration fails, the previous live database is restored and an error is returned to the panel — no half-migrated state on disk. - Schema migrations (
cmd/migration) now return errors instead of callinglog.Fatal, so a bad import no longer kills the panel process; the version pin is upserted instead of expecting an existing row. - The same migration + adaptation pipeline runs at panel start (
app.Init), so a fresh binary on top of an existing 1.x database upgrades automatically. - Added
database.AdaptToCurrentVersion, an idempotent post-migration step that:- rehashes any plaintext passwords with bcrypt (legacy backups before this fork shipped them in clear);
- re-applies the new
idx_stats_lookup/idx_changes_lookup/idx_clients_nameindexes; - bumps the
settings.versionrow to the build version so the migration runner short-circuits next time.
- Database path construction uses
filepath.Joininstead of string concatenation. - Database init creates
idx_stats_lookup,idx_changes_lookup, andidx_clients_nameindexes for the hottest queries (database/db.go.ensureIndexes). - SQLite connection pool tuned:
SetMaxOpenConns(8),SetMaxIdleConns(4),SetConnMaxLifetime(time.Hour), with_busy_timeout=10000and_journal_mode=WALalready in the DSN. AvoidsSQLITE_BUSYstorms during stats inserts. - Transaction commits in
service.config.Save,service.stats.SaveStats, andservice.client.DepleteClientsare checked; a failed commit is now reported up the call chain instead of being silently dropped. - Configuration saves only mutate sing-box runtime state after a successful DB commit. The previous behaviour could end with a runtime change applied but a rolled-back DB.
- User-driven core restarts (
RestartCore) bypass the cron cooldown so the API reflects the real start status. The cronCheckCoreJobcontinues to respect the cooldown. - Inbound restart and
GetSingboxInfoare now nil-safe against a concurrent core stop/start (previously could panic withnil pointer dereferenceoncorePtr.GetInstance().ConnTracker()). - Race-detector-clean synchronization around:
- API tokens (
api/apiV2Handler.go, now amap[string]TokenInMemorywith O(1) lookup). - Online stats (
service/stats.go.onlineResources) — readers receive a deep copy underRWMutex. - Core running state and instance pointer (
core/main.go.Core). - Last-update bookkeeping (
service/config.go.LastUpdate).
- API tokens (
- HTTP server now sets
ReadHeaderTimeout,ReadTimeout,WriteTimeout,IdleTimeout, andtls.Config.MinVersion = tls.VersionTLS12for both the panel and the subscription server.
- Fixed
npm ciby syncingpackage-lock.json. - Migrated ESLint to flat config (
frontend/eslint.config.mjs). - Lint script now reports without auto-fixing (
"lint": "eslint ."). npm audit --audit-level=highreports 0 vulnerabilities.- Axios setup moved onto the exported instance; deprecated
CancelTokenreplaced withAbortController. Dedupe limited to idempotent reads. - Removed unsafe
v-htmlfromLogs.vue,RuleImport.vue, the IP lists inMain.vue, and the gauge tile (components/tiles/Gauge.vue). - Fixed
enableTraffic=falsenot propagating to the store,loadClientscrashing on empty results, and the unused filtered status request list inMain.vue.reloadData. - Re-enabled Vite code splitting; bundle output uses
[hash].js/[hash].cssfilenames.
install.shand thes-uimanagement menu are now bilingual (English / Russian). On first run the user is asked to pick a language; the choice is stored in/etc/s-ui/langand reused on subsequent runs.SUI_LANG=en|ruoverrides interactively or in CI.- Added menu item 21. Language so the user can switch UI language without editing files.
- Default
timeLocationfor the panel changed fromAsia/ShanghaitoEurope/Moscow. - Default frontend locale (and Vuetify locale) changed from
zhHans(Simplified Chinese) toen. The user-selected locale saved inlocalStorageis still honoured, so existing browsers keep their language.
- Go module renamed to
github.com/deposist/s-ui-x; all internal imports updated. frontend/go.modkeeps root-levelgocommands away fromfrontend/node_modules.- README,
install.sh,s-ui.sh,docker-compose.ymlupdated to point athttps://github.com/deposist/s-ui-xandghcr.io/deposist/s-ui-x.
New regression tests:
util/common/password_test.go— hashing, plaintext detection, migration flag.util/subToJson_test.go— URL validation rejectsfile://,localhost, RFC1918, IPv6 loopback; opt-in restores private targets.util/subToJson_dial_test.go— dialer hook rejects loopback addresses post-validation; opt-in allows them.service/setting_test.go— default port omission forsubURI.database/backup_test.go— backup includesservicesandtokens.database/adapt_test.go— legacy plaintext password rehashing during import is correct, idempotent, and bumpssettings.version.api/rateLimit_test.go— block on max failures, reset clears state, concurrent access.api/utils_test.go— XFF parsing matrix (untrusted client, rightmost untrusted hop, all-trusted fallback, spoofed XFF from untrusted client).
| Command | Result |
|---|---|
go build ./... |
✅ |
go vet ./... |
✅ |
go test -count=1 ./... |
✅ |
go test -count=1 -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_tailscale" ./... |
✅ |
go test -race -count=1 ./... |
✅ (requires CGO and a C compiler, e.g. C:\msys64\ucrt64\bin\gcc.exe) |
npm ci |
✅ |
npm run build |
✅ |
npm run lint |
✅ |
npm audit --audit-level=high |
✅ (0 vulnerabilities) |
You can upgrade in place without losing data or reconfiguring the server.
The DB schema is migrated automatically on every panel start
(app.Init → cmd/migration → database.AdaptToCurrentVersion),
existing settings/inbounds/outbounds/clients/tokens stay intact, and
plaintext admin passwords migrate to bcrypt automatically on the next
login. Backups taken from older S-UI builds (1.0/1.1/1.2/1.3) can be
restored straight from the panel and will be brought up to the current
schema in the same flow.
- Make a backup, just in case:
- via panel: Backup → Backup, save the resulting
s-ui_*.db; - or copy the file:
cp /usr/local/s-ui/db/s-ui.db /root/s-ui.db.bak.
- via panel: Backup → Backup, save the resulting
- Stop the service:
systemctl stop s-ui. - Replace the binary or the docker image with the new build:
- manual: extract the new tarball into
/usr/local/s-ui/; - docker: bump the image tag to
ghcr.io/deposist/s-ui-xanddocker compose pull && docker compose up -d.
- manual: extract the new tarball into
- Start the service:
systemctl start s-ui. - Log in as usual. Your password is stored in plaintext today; the panel hashes it transparently on first successful login.
What you should review after the upgrade:
- If the panel sits behind a reverse proxy and you relied on
X-Forwarded-For(e.g. for IP audit logs), setSUI_TRUSTED_PROXIES=10.0.0.0/8,192.168.0.0/16,…to the CIDRs your proxy lives in. Without this variable, XFF is ignored and audit logs show the proxy IP instead of the real client. - If you fetch external subscriptions from a private endpoint
(
http://127.0.0.1:…/subetc.), setSUI_ALLOW_PRIVATE_SUB_URLS=true. - If you used the old install / update script (
deposist/s-ui), grab the new one once:wget -O /usr/bin/s-ui https://raw.githubusercontent.com/deposist/s-ui-x/main/s-ui.sh && chmod +x /usr/bin/s-ui.
If something goes wrong, restoring your backup is enough:
systemctl stop s-ui.cp /root/s-ui.db.bak /usr/local/s-ui/db/s-ui.db.- Either restore the previous binary or
docker composeto the previous image tag. systemctl start s-ui.
The bcrypt prefix in the users.password column is forward- and
backward-compatible with the old binary in the sense that the old binary
will simply not match a hashed password, in which case s-ui admin -reset
restores a known credential. So data is safe; only the admin password
might need a CLI reset on rollback.