60 Commits
Author SHA1 Message Date
風雪 5bb47282a0 Delete scripts/tests/test_ssh_menu.sh 2026-10-09 00:40:49 +08:00
風雪 c6cf289228 fix(scripts): make SSH menu config access indentation-agnostic (#101)
* fix(scripts): make SSH menu config access indentation-agnostic

Running the panel built from source exposed two defects that static review
could not see, because both depend on what the Go binary actually writes to
config.yaml.

The Go binary rewrites config.yaml through a YAML marshaller. `init` emits
two-space indentation, but any later rewrite — notably `reset-config`, the SSH
rescue path — emits four, and re-sorts the keys. Every shell helper matched on
a hardcoded two-space prefix, so the menu and the binary disagreed about the
file's shape.

1. Duplicate keys after an SSH rescue

After `reset-config`, `yaml_get '  port:'` matched nothing, so the menu showed
an empty current port. `yaml_set` then took its insert branch and appended a
two-space `port:` line alongside the marshaller's four-space one:

    server:
        mode: release
        port: 8080
        sub_port: 0
      port: 9090          <- inserted, duplicate

The menu then reported "Configured port: 9090" and restarted the service. The
failure mode matters more than the diff: SSH rescue is what an operator reaches
for when the panel is unreachable, which is exactly when the menu has to work.

2. `3m-ui config port <port>` left the panel unable to boot

That command hardcoded a two-space indent, producing mixed indentation inside
one block:

    server:
        mode: release
      port: 19090
        public_url: ""

The binary then refuses to start at all:

    failed to decode config YAML: yaml: line 12: did not find expected key

This command is documented in the README, so following the documentation
bricks the panel.

Both are fixed the same way: key lookup is now indentation-agnostic (matched
on $1 rather than a fixed prefix, which also removes any ambiguity between
`port:` and `sub_port:`), replacement preserves the indentation of the line it
rewrites, and insertion adopts the block's existing indent. Callers pass
"port:" instead of "  port:".

Also fixed: menu item 17 (Healthcheck) produced no output at all, because the
binary's healthcheck exits 0 silently on success. It now reports pass/fail
based on the exit status.

Verified against a panel compiled from source and running on loopback:
rescue -> change port -> restart yields a single `port:` key with consistent
indentation, config stays 0600, the panel serves HTTP 200, and the healthcheck
item reports OK. The regression script (scripts regression, 16 cases) covers
both defects by reproducing them against the old implementation first.

* fix(scripts): make SSH menu config access indentation-agnostic

Running the panel built from source exposed two defects that static review
could not see, because both depend on what the Go binary actually writes to
config.yaml.

The Go binary rewrites config.yaml through a YAML marshaller. `init` emits
two-space indentation, but any later rewrite — notably `reset-config`, the SSH
rescue path — emits four, and re-sorts the keys. Every shell helper matched on
a hardcoded two-space prefix, so the menu and the binary disagreed about the
file's shape.

1. Duplicate keys after an SSH rescue

After `reset-config`, `yaml_get '  port:'` matched nothing, so the menu showed
an empty current port. `yaml_set` then took its insert branch and appended a
two-space `port:` line alongside the marshaller's four-space one:

    server:
        mode: release
        port: 8080
        sub_port: 0
      port: 9090          <- inserted, duplicate

The menu then reported "Configured port: 9090" and restarted the service. The
failure mode matters more than the diff: SSH rescue is what an operator reaches
for when the panel is unreachable, which is exactly when the menu has to work.

2. `3m-ui config port <port>` left the panel unable to boot

That command hardcoded a two-space indent, producing mixed indentation inside
one block:

    server:
        mode: release
      port: 19090
        public_url: ""

The binary then refuses to start at all:

    failed to decode config YAML: yaml: line 12: did not find expected key

This command is documented in the README, so following the documentation
bricks the panel.

Both are fixed the same way: key lookup is now indentation-agnostic (matched
on $1 rather than a fixed prefix, which also removes any ambiguity between
`port:` and `sub_port:`), replacement preserves the indentation of the line it
rewrites, and insertion adopts the block's existing indent. Callers pass
"port:" instead of "  port:".

Also fixed: menu item 17 (Healthcheck) produced no output at all, because the
binary's healthcheck exits 0 silently on success. It now reports pass/fail
based on the exit status.

Verified against a panel compiled from source and running on loopback:
rescue -> change port -> restart yields a single `port:` key with consistent
indentation, config stays 0600, the panel serves HTTP 200, and the healthcheck
item reports OK. The regression script (scripts regression, 16 cases) covers
both defects by reproducing them against the old implementation first.

* test(scripts): add regression coverage for the SSH menu config helpers

The existing lifecycle tests exercise install.sh end to end but never touch
the menu's config read/write path, which is where both defects in the previous
commit lived. This adds 16 cases.

Each case runs the pre-fix implementation and the fixed one against the same
input and asserts both directions: the old one must reproduce the defect, the
new one must not exhibit it. A case whose "before" assertion passes is scored
FAIL — otherwise the suite could go green without ever having exercised the
bug.

Covers: block-scoped writes, 0600 preservation, `config port` with a missing
argument, the self-referencing entrypoint loop, insertion placement, missing
`server:` block, indentation-agnostic lookup, and indentation consistency.

The suite is self-contained: it needs no installed panel, writes only to a
temp dir, and the one case that touches /usr/local/bin/3m-ui skips itself if
that path already exists.
2026-10-09 00:39:10 +08:00
風雪 84eab7750c fix(scripts): scope SSH menu config writes to the server: block (#100)
* fix(scripts): scope SSH menu config writes to the server: block

The SSH management menu edited /etc/3m-ui/config.yaml with a global sed
substitution, which rewrote every identically-indented key in the file rather
than just the panel's. Changing the panel port therefore also rewrote unrelated
listener ports, and because the menu restarts the service right afterwards the
corruption went live immediately.

The same code path wrote the new file with `mv`, so a 0600 config holding the
JWT and credential-encryption keys came back as a umask-default 0644 file,
readable by every local user after any port or path change.

Both are fixed by scoping reads/writes to the `server:` block and by writing
back through the original inode (`cat > file`) instead of replacing it.

Three further defects found while auditing the menu:

- scripts/3m-ui.sh: `config port` read a bare "$2" under `set -u`, so running
  it without an argument aborted with "parameter not set" and the usage
  message below it was unreachable. Now "${2:-}".
- scripts/3m-ui.sh: the no-arg branch exec'd /usr/local/bin/3m-ui without
  checking whether it resolves back to this script, which spun in an endless
  exec loop on a broken entrypoint. Direct and indirect self-reference are now
  detected and reported with an actionable message.
- scripts/3m-ui: `enable_bbr` wrote into /etc/sysctl.d without creating it; on
  minimal/container images the redirect failed and `set -e` tore down the whole
  interactive menu. Now mkdir -p with a guarded failure.

Hardening and consistency:

- yaml_set reports an error instead of silently no-oping when neither the key
  nor a `server:` block exists; the menu no longer restarts the service for a
  change that never happened.
- yaml_get is scoped to the `server:` block so it matches yaml_set.
- temp files come from mktemp (0600) instead of a predictable .$$ name.
- open_firewall asks for confirmation before opening 22/80/443.
- status reports boot-time enablement on OpenRC, not only systemd.
- menu item 8 reports invalid input instead of returning silently.
- menu item 23 is relabelled: it updates all of 3m-ui, not only the script.
- the acme.sh helper is annotated as an example to review before running.

Verified locally: the listener port is left untouched when the panel port
changes, config mode stays 0600, `config port` with no argument prints usage,
the self-referencing entrypoint fails fast instead of looping, and both files
pass `sh -n`.

* fix(scripts): scope SSH menu config writes to the server: block

The SSH management menu edited /etc/3m-ui/config.yaml with a global sed
substitution, which rewrote every identically-indented key in the file rather
than just the panel's. Changing the panel port therefore also rewrote unrelated
listener ports, and because the menu restarts the service right afterwards the
corruption went live immediately.

The same code path wrote the new file with `mv`, so a 0600 config holding the
JWT and credential-encryption keys came back as a umask-default 0644 file,
readable by every local user after any port or path change.

Both are fixed by scoping reads/writes to the `server:` block and by writing
back through the original inode (`cat > file`) instead of replacing it.

Three further defects found while auditing the menu:

- scripts/3m-ui.sh: `config port` read a bare "$2" under `set -u`, so running
  it without an argument aborted with "parameter not set" and the usage
  message below it was unreachable. Now "${2:-}".
- scripts/3m-ui.sh: the no-arg branch exec'd /usr/local/bin/3m-ui without
  checking whether it resolves back to this script, which spun in an endless
  exec loop on a broken entrypoint. Direct and indirect self-reference are now
  detected and reported with an actionable message.
- scripts/3m-ui: `enable_bbr` wrote into /etc/sysctl.d without creating it; on
  minimal/container images the redirect failed and `set -e` tore down the whole
  interactive menu. Now mkdir -p with a guarded failure.

Hardening and consistency:

- yaml_set reports an error instead of silently no-oping when neither the key
  nor a `server:` block exists; the menu no longer restarts the service for a
  change that never happened.
- yaml_get is scoped to the `server:` block so it matches yaml_set.
- temp files come from mktemp (0600) instead of a predictable .$$ name.
- open_firewall asks for confirmation before opening 22/80/443.
- status reports boot-time enablement on OpenRC, not only systemd.
- menu item 8 reports invalid input instead of returning silently.
- menu item 23 is relabelled: it updates all of 3m-ui, not only the script.
- the acme.sh helper is annotated as an example to review before running.

Verified locally: the listener port is left untouched when the panel port
changes, config mode stays 0600, `config port` with no argument prints usage,
the self-referencing entrypoint fails fast instead of looping, and both files
pass `sh -n`.
2026-10-08 23:49:39 +08:00
kazeyukiro 141961be8d fix(menu): report invalid choice in BBR menu
与其它菜单保持一致:0/空输入直接返回,其它无效项给出提示,
而不是静默退出。
2026-10-05 14:13:44 +08:00
kazeyukiro 200427c11d fix(menu): return from SSL cert helper without prompting for domain
选 0 或无效项时 ssl_cert_menu 仍会继续索要域名,必须再回车一次
才能回到主菜单。改为先校验选项:0/空直接返回,无效项给出提示。

同时把 `[ -z "$dom" ] && return 0` 改成 if 形式,避免 set -e 下
AND 列表非零退出状态的边缘行为。
2026-10-05 14:07:20 +08:00
風雪and風雪 7129cc346d perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
perf(memory): keep install, update and runtime inside a small-RAM budget (#83)
* perf(memory): keep install, update and runtime inside a small-RAM budget
Small NAT/VPS targets have 128-512MB of RAM and no swap, so anything that
cannot be reclaimed is fatal. This tuning touches no swap and no mmap; it
works by not allocating in the first place.
Installer (covers install and update; update.sh only execs install.sh):
- Put the scratch directory under DATA_DIR instead of mktemp -d. /tmp is
tmpfs on many of these hosts, and without swap every byte there is pinned
in RAM: bundle, extracted release and a copy of the data directory were
held at once, exceeding total RAM of the machines being targeted.
- Hash assets on the way to disk instead of re-reading them. Download is a
single pass now; verification still completes before tar sees the bytes.
- Release the intermediate data.tar right after extraction rather than
holding the data directory twice for the rest of the run.
- Check scratch capacity before downloading, so we fail before the old
service is stopped rather than halfway through.
- Refresh Environment=GOMEMLIMIT/GOGC/THREE_M_UI_CORE_LOW_MEMORY and
MemoryMax on an existing unit. write_service() keeps an operator's unit,
which meant the values baked in at install time never tracked updates.
Panel:
- Bound the Go heap from the process itself via debug.SetMemoryLimit, so the
tuning reaches manual runs, containers and the installer's own init and
healthcheck instead of only a systemd unit. It acts in-process, so the
Mihomo core never inherits the panel's tighter GC target. An explicit
GOMEMLIMIT or GOGC always wins.
- Emit geodata-loader: memconservative for the core when low-memory mode is
on. The key is omitempty and pinned after user fragments merge, so nothing
changes for anyone who does not opt in, and no fragment can undo it.
No new calls to Mmap/Munmap/Madvise, no swap or mmap anywhere in the tree.
* fix(ci): keep the new scratch directory out of the installer's own paths
Two problems CI caught on the small-memory change, both from moving the
scratch directory from mktemp into DATA_DIR.
restore_files() rebuilds the data directory by deleting everything in it
except backups. The scratch directory now lives inside it, so a rollback
deleted the very directory it was reading its own rollback out of, and the
restore died looking for restore/ENABLED. Rollback therefore had to be
skipped, exempting the scratch directory from that purge; the cleanup trap
already removes it.
The test curl also modelled only one of curl's output modes. Writing to
stdout and answering -I are both normal curl behaviour, and both are now
covered, so the mock no longer pins the installer to a single invocation
shape. The distribution fake likewise emits its tampered asset on stdout
instead of through --output, keeping that test about checksum rejection
rather than about which flag was used.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-27 09:27:35 +08:00
Z User d50664377f feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
feat(mem): Go runtime memory auto-tuning (no swap, no mmap)
Pure Go runtime tuning via GOMEMLIMIT + GOGC, applied at every stage
of install AND update. No swap file creation. No mmap tricks.
4-tier auto-detection based on /proc/meminfo:
RAM ≤ 128MB:  GOMEMLIMIT=48MiB  GOGC=20  MemoryMax=72M
RAM ≤ 256MB:  GOMEMLIMIT=96MiB  GOGC=50  MemoryMax=144M
RAM ≤ 512MB:  GOMEMLIMIT=192MiB GOGC=75  MemoryMax=288M
RAM > 512MB:  off               GOGC=100 MemoryMax=infinity
Applied to:
1. Early: export GOMEMLIMIT/GOGC before install_deps so download +
extract + init all inherit the limits.
2. init step: explicitly passed GOMEMLIMIT/GOGC to the Go binary.
3. healthcheck: explicitly passed to the Go binary.
4. systemd unit: Environment=GOMEMLIMIT=... + Environment=GOGC=...
+ MemoryMax=... (hard OOM cap, kills panel not SSH).
5. OpenRC unit (Alpine): export GOMEMLIMIT/GOGC (OpenRC doesn't
support MemoryMax, but GOMEMLIMIT handles it at Go runtime level).
Override: THREE_M_UI_GOMEMLIMIT=80MiB THREE_M_UI_GOGC=75
Why this works without swap:
- GOMEMLIMIT tells Go GC 'you have X MiB of heap, GC aggressively
to stay under it'. Go runtime respects this natively (Go 1.19+).
- GOGC=20 means GC triggers at 1.2x live heap (vs default 2x),
keeping RSS tight at the cost of ~3-5% more CPU.
- MemoryMax is the systemd safety net: if Go somehow leaks past
GOMEMLIMIT×1.5, systemd kills the panel (which Restart=always
brings back), not SSH.
- No swap needed: the goal is to keep RSS within physical RAM,
not to extend virtual memory.
2026-09-26 07:28:17 +00:00
kazeyukiro 0918929195 fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
fix(update): run self-update outside 3m-ui.service cgroup
Panel web update and any updater still inside the service were killed by
systemctl stop before start. Schedule via systemd-run (fallback setsid)
and re-escape in install.sh when cgroup is 3m-ui.service.
2026-09-25 15:46:19 +00:00
Z User bffb21e415 feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
2026-09-25 06:26:55 +00:00
Z User 00249cd168 feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
feat(scripts): wire reset-config into 3m-ui CLI wrapper + install hint
The new '3m-ui reset-config' Go subcommand (commit 8639b01) lets SSH
operators undo panel misconfigurations (wrong port / 127.0.0.1 bind /
broken SSL). This change exposes it through the systemd management
wrapper and documents it in the install banner.
scripts/3m-ui.sh:
- Add 'reset_config' function: stops the panel service (required by
the Go binary since it writes config.yaml + DB rows the running
panel might cache), invokes 3m-ui-bin reset-config <flags>, then
restarts the panel.
- --help / -h short-circuits the stop/restart cycle (read-only).
- Add 'reset-config' to the main case dispatch.
- Document the new subcommand + flags in usage() with examples.
scripts/install.sh:
- Append a line to the post-install banner pointing operators at
'3m-ui reset-config --panel --yes' for SSH rescue scenarios.
USAGE
sudo 3m-ui reset-config                       # interactive y/N, panel scope
sudo 3m-ui reset-config --panel --yes        # scripted rescue (common case)
sudo 3m-ui reset-config --all --yes          # full reset
sudo 3m-ui reset-config --help               # full help (no service touched)
Smoke-tested with a fake binary: dispatch + flag passthrough + env vars
all correct; --help does not stop the service.
2026-09-25 06:22:39 +00:00
kazeyukiro 6a18d01855 fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
fix(ci): harden release smoke SS path for slow arm64 runners
Wait for Shadowsocks TCP bind after async ApplyConfig; lengthen proxy checks.
2026-09-16 12:28:00 +08:00
風雪 7f664a5d1a fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
fix(ci): wait for async certstore after listener create in smoke-runtime
Listener TLS PEMs are written during debounced GenerateFinalConfig/ApplyConfig.
Smoke was asserting listener-certs immediately after POST /listeners and failed on amd64/arm64 pre builds.
2026-09-15 11:29:07 +08:00
風雪 68aafdea86 feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
2026-09-14 23:01:35 +08:00
風雪 d5629d0247 security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
security: remove unverified script downloads
2026-09-14 12:31:46 +08:00
風雪 0084c4f053 fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
fix(install): always refresh all lifecycle scripts on update
- Force-install 3m-ui-bin, install/update/uninstall.sh, 3m-ui.sh, 3m-ui
- Re-install /usr/local/bin/3m-ui from the same release (no stale entry)
- Log each installed path; verify executables after write
- chmod scripts in prepare_release; document in update.sh
2026-09-14 11:53:36 +08:00
風雪 508b3e30c0 fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
fix(scripts/3m-ui): harden yaml edit, DATA_DIR, logs follow, update_self prompt
- Expand $BASE in update_self confirm
- mkdir DATA_DIR before run_app
- Flexible yaml_get/yaml_set indentation; fail if no server:
- logs -f tolerates Ctrl+C under set -e
- Clearer enable/disable errors
2026-09-14 11:33:08 +08:00
風雪 aa08f34be4 feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
2026-09-14 11:27:17 +08:00
風雪 bcdec99d82 fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
fix(scripts/3m-ui): menu set -e survival, service_is_active, safer config edits
- Menu actions no longer abort the whole console on failure (set -e)
- Use local status/start/stop/logs/version instead of 3m-ui.sh detour
- service_is_active for post-restart checks; help without root
- Safer port/public_url YAML edits (missing field, BusyBox insert, sed escape)
2026-09-14 00:34:16 +08:00
Z User 72a81853b7 fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
fix: version menu shows release files; reclaim soft-deleted listener names
- 3m-ui version: fall back to VERSION/CHANNEL/MIHOMO when --version is empty
- ensureUniqueName: hard-delete soft-deleted rows still holding UNIQUE(name)
- Frontend suggests next 节点N after already-exists conflicts
2026-09-11 16:39:59 +00:00
Z User d404de8cb9 fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
fix(mihomo): start core synchronously on first apply; poll in smoke
ApplyConfigDeferredRestart returned before Start() when core was down, so
smoke (and UI) saw running=false right after creating the first listener.
- First start: still synchronous (fail the API if core cannot start)
- Already running: keep async restart for responsive create/update
- smoke-runtime: brief poll for mihomo/status.running
2026-09-11 16:25:34 +00:00
Z User 09fd9d0069 docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
docs: 18 language READMEs; main README language hub; 3m-ui menu CN/EN only
- Add README.<lang>.md for en, zh-CN, zh-TW, ja, ko, es, fr, de, ru, pt-BR, vi, id, th, tr, ar, hi, pl, uk
- Main README: language switcher + docs/site links; English body moved to README.en.md
- Management menu bilingual Chinese|English only (no extra locales in shell)
2026-09-09 15:14:16 +00:00
freephilx 2e6256fb6a feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
feat(distribution): add complete installation and recovery workflows
Ship stable-only Linux bundles and multiarch Docker images with a pinned Mihomo core. Share secure first-run initialization across native and container installs.
Validate downloads before downtime, snapshot persistent state before updates, and restore the previous installation after a failed upgrade. Add backup, restore, healthcheck, and randomized administrator reset commands.
Add release verification, lifecycle regression tests, and installation documentation.
2026-09-08 22:47:35 +08:00
Z User db08c0745c fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
fix(scripts): quiet Mihomo download; try asset name variants; stable tag for mihomo
The curl 404 during 3m-ui update was optional Mihomo core refresh
(wrong/pre tag or missing -compatible asset). Panel update already succeeded.
2026-09-07 16:33:48 +00:00
Z User b7f0389353 ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
ci(test): single rolling Pre-release tag 'pre' (overwrite, no duplicates)
2026-09-06 15:57:03 +00:00
Z User 04865db275 feat(test): PWA manifest; update.sh HTTP health probe before success 2026-09-06 15:23:08 +00:00
Z User db96350bd8 fix(scripts): clean version argument case pattern 2026-09-06 15:06:00 +00:00
Z User 8086e729c0 fix(scripts): dedupe version arg pattern after test-* support 2026-09-06 15:05:51 +00:00
Z User f4bb43ecd3 ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
ci(test): publish Pre-releases; install/update prefer newest prerelease
2026-09-06 15:05:44 +00:00
Z User 70e9b0c423 fix(cli): support 3m-ui update|install|uninstall; embed logo in README 2026-09-05 16:40:54 +00:00
Z User 1f484b8c06 feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
feat(p1-p3): safe config publish, batch user ops, cluster push, update harden, settings icons
- Config: generate no longer applies; Apply accepts editor YAML; Rollback UI
- Users batch: extend-days and add-traffic (GiB)
- Cluster: POST push-node with dry-run then create disabled on remote
- Install: NONINTERACTIVE result file; update: longer health poll, strip more seccomp
- Settings sidebar icons softer/outlined set
2026-09-05 15:22:27 +00:00
Z User 6102d9cf55 fix(systemd): drop SystemCallFilter causing SIGSYS (status=31) on pure-Go binary 2026-09-04 15:13:53 +00:00
Z User c6e33de929 fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
fix(update,tls): wait for service health; hydrate certs on boot without re-save
- update.sh: poll up to 15s before rollback (fixes false failure after binary update)
- On startup, restore listener PEMs from /var/lib/3m-ui/listener-certs and
fall back to names in existing mihomo config.yaml into DB before generate
2026-09-04 15:02:31 +00:00
Z User b30de84bec fix: import Alert in Settings; keep 3m-ui menu after child scripts; idempotent BBR 2026-09-04 14:56:16 +00:00
Z User 286873b128 feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
feat(ops+sub): Phase A/B install menu, admin reset CLI, UA/sub UX
- Keep initial admin password as admin (no random password)
- Install summary prints local/public panel URLs
- Management menu: show URL, reset-admin to admin, BBR, geo update, public_url
- Binary: reset-admin command
- Expand subscription User-Agent auto-detect; frontend hints + user status filters
2026-09-04 14:53:52 +00:00
Z User d6e64ab313 chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
chore(release): set version to v1.0.0
Align Makefile, embedded default, frontend package, and script help
text with the first stable release tag.
2026-08-31 06:21:47 +00:00
Z User 7addcdc5c7 fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
fix(non-listener): enforcement retry, DB restore warning, security headers, config placeholder, sensitive key gaps, dead UI options, bot API server, update.sh dedup
Non-listener comprehensive audit found 4 Medium + 4 Low issues.
All fixed. VLESS+REALITY and listener schema untouched.
== Medium ==
M-NEW-C1: Enforcement doesn't retry failed config regen
traffic/enforcement.go CheckAndEnforce updated lastBlocked BEFORE
calling RegenerateConfig. If regen failed, lastBlocked was already
set, so next tick saw no change and never retried — over-quota
users stayed connected. Fix: update lastBlocked only after
successful regen; preserve old state on failure for retry.
M-NEW-C2: DB restore orphans running process's SQLite handle
system/api.go RestoreDatabase renames the DB file, but the running
panel keeps writing to the old (unlinked) inode. Added prominent
[WARNING] log + restart_required:true + *** RESTART REQUIRED ***
message in response. In-place GORM reconnect not feasible; restart
is the correct approach.
M-NEW-G1: Backend doesn't send CSP/security HTTP headers
CSP was only in index.html meta tag. Added SecurityHeaders()
middleware setting X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin,
X-XSS-Protection: 1; mode=block on all responses. CSP NOT set
on API responses (meta CSP handles the SPA shell).
M-NEW-A1: Default config.yaml has placeholder secrets
backend/config/config.yaml contained DefaultJWTSecret and
DefaultCredentialKey (rejected by Validate → startup failure for
developers). Replaced with CHANGE_ME placeholders + comments
explaining use install.sh or openssl rand -hex 32.
== Low ==
L-NEW-B1: isSensitiveSettingKey heuristic gaps
panel_settings.go blocklist missing 'key' and 'auth' substrings.
Added (api deliberately excluded — too broad).
L-NEW-D1: enabled_events UI has dead options
Settings.tsx Select included node_down/node_up/block/unblock which
have no backend effect. Removed. Kept only implemented events:
login/cpu/memory/crash/expiry/traffic.
L-NEW-D3: Bot loop hardcodes api.telegram.org
bot.go getUpdates/deleteWebhook hardcoded the official Telegram API
URL. Now takes apiBase parameter; loop() passes tgClient.apiBase()
so custom api_server setting is respected for long-polling.
L-NEW-H1: update.sh duplicate CONFIG_DIR
Line 11-12 had CONFIG_DIR defined twice. Removed duplicate.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : 16 packages all PASS
- npm run build : PASS
- gofmt : clean
- sh -n update.sh : OK
2026-08-30 11:13:08 +00:00
Z User dbec0b3099 fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
fix(scripts): install/update/uninstall port handling
Three script-level port UX issues found and fixed:
== install.sh: re-install with PANEL_PORT now updates existing config ==
write_config previously returned early when config.yaml already existed,
ignoring PANEL_PORT env var. Re-running 'PANEL_PORT=9000 bash install.sh'
on an existing install silently kept the old port.
Fix: when config.yaml exists and PANEL_PORT/THREE_M_UI_PORT is explicitly
set, sed-update the port/listen/public_url in the existing file. First-
time install (no config.yaml) still writes full config with random keys.
== update.sh: show panel port after successful update ==
Update completed with no indication of which port the panel is on.
Users had to check config.yaml manually.
Fix: after '[5/5] Update completed', read server.port from config.yaml
and display:
Panel: http://SERVER_IP:<port>/
(to change port: 3m-ui config port <new-port>)
Also added CONFIG_DIR constant (was missing).
== uninstall.sh: show previous panel port before removing config ==
Uninstall removed config without telling the user what port was in use.
Fix: before rm -rf $CONFIG_DIR, read and display:
Previous panel port: <port>
2026-08-30 09:47:16 +00:00
Z User 2fd6992574 feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
feat(scripts): add config port command to 3m-ui management scripts
Users changing the panel port from the web UI had to SSH in and run
'systemctl restart 3m-ui' manually — easy to forget and not discoverable.
Now both the 3m-ui entry script and 3m-ui.sh manager support:
3m-ui config show              — display config.yaml
3m-ui config port <1-65535>    — change port + auto-restart
The interactive menu (running '3m-ui' with no args) also gains:
9. 修改面板端口  (interactive port change with current/new display)
a. 卸载 3m-ui    (moved from slot 9 to 'a')
Flow:
1. Reads current port from config.yaml
2. Validates new port (1-65535, numeric)
3. sed replaces '  port: N' line in config.yaml
4. Restarts the service
5. Waits 1s, checks if active
6. Prints new access URL or failure message
This closes the UX gap: web UI saves port → user runs '3m-ui config
port 9000' → script restarts and confirms.
2026-08-30 09:41:36 +00:00
風雪 7bc01e0298 fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
fix(installers): Mihomo checksum via GitHub API digest, best-effort by default
The previous verify_mihomo_sha256 tried to download a SHA256SUMS file
from MetaCubeX/mihomo releases. Mihomo does NOT publish such a file
(verified across v1.17.0 — v1.19.30, all 404), so every install that
reached the Mihomo step failed with:
'Error: Mihomo checksum verification failed.
Set THREE_M_UI_INSECURE=1 to bypass (NOT recommended).'
Two-part fix:
1. Use GitHub's Release API per-asset 'digest' field instead of a
SHA256SUMS file. GitHub computes sha256 server-side when an asset
is uploaded, so the digest is authoritative even though Mihomo
itself publishes no checksums. Added github_asset_digest() helper
that queries /repos/{repo}/releases/tags/{tag} and parses the
matching asset's digest field (no jq dependency, pure awk).
2. Make Mihomo verification best-effort by default. Anonymous GitHub
API is rate-limited to 60 requests/hour/IP, so hard-failing would
break installs for anyone sharing a NAT IP or after a few retries.
The fail-closed guarantee already covers our own 3m-ui binary
(SHA256SUMS + cosign). Mihomo integrity is additionally protected
by HTTPS transport + the ELF smoke-test.
Users who want strict Mihomo verification can set
THREE_M_UI_VERIFY_MIHOMO=1 to make verification failures abort
(requires GitHub API to be reachable).
Threat model unchanged: our binary is fully verified end-to-end;
Mihomo is an independent project we can only best-effort verify.
The HTTPS download to GitHub + ELF check catches the realistic
threats (network MITM, truncated download, wrong file).
2026-08-29 11:00:34 +00:00
風雪 8d730a2d08 fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
fix(installers): cosign verify accepts tag-ref or main-branch identity
The back-filled v1.0.0 was signed via workflow_dispatch (ref=main), so
its OIDC certificate identity is
.../release.yml@refs/heads/main
rather than the tag-ref identity
.../release.yml@refs/tags/v1.0.0
that tag-pushed releases produce.
Make verify_release_cosign try both identities so verification works
for both normal tag-triggered releases and workflow_dispatch back-fills.
If either matches, verification passes.
2026-08-29 10:01:58 +00:00
風雪 4f038eba42 feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
feat(release): cosign keyless signing of SHA256SUMS
Closes M-10 (supply-chain authenticity). Replaces the TODO comment with
a working cosign keyless OIDC signing flow.
Workflow (release.yml):
- Add id-token: write permission (required for GitHub OIDC token).
- Add sigstore/cosign-installer@v3 step.
- After SHA256SUMS is generated, cosign sign-blob --yes produces
SHA256SUMS.pem (certificate) and SHA256SUMS.sig (signature).
These are uploaded alongside the existing release assets.
- No long-lived secret is stored on GitHub; the signing identity is
the ephemeral GitHub Actions OIDC token bound to this workflow + tag.
- Release notes body updated with the cosign verify-blob command so
users know how to verify authenticity end-to-end.
Installers (install.sh / update.sh):
- Add verify_release_cosign() function. OPT-IN via
THREE_M_UI_VERIFY_COSIGN=1 (off by default to keep the curl|bash UX
simple — SHA256SUMS integrity is already fail-closed).
- When enabled AND cosign is installed, downloads SHA256SUMS.pem +
SHA256SUMS.sig and runs cosign verify-blob against the identity
'https://github.com/<repo>/.github/workflows/release.yml@refs/tags/<tag>'.
- If cosign is missing or the release predates signing, prints a
warning and continues (does not hard-fail).
- If verification runs and FAILS, aborts (fail-closed, bypassable via
THREE_M_UI_INSECURE=1).
Threat model:
- Before: SHA256SUMS provided integrity but not authenticity — an
attacker who could modify the release assets could also modify
SHA256SUMS, defeating the checksum.
- After: cosign signature ties SHA256SUMS to the GitHub Actions OIDC
identity, so a modified sums file fails signature verification even
if the attacker controls the release CDN.
Verification command (for end users):
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity 'https://github.com/kazeyukiro/3m-ui/.github/workflows/release.yml@refs/tags/<tag>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
2026-08-29 09:58:00 +00:00
風雪 39608cd0fb fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
fix(infra): fail-closed checksums, sandbox hardening, non-root Docker
Critical (supply chain):
- C-3: Mihomo binary download had no checksum. Add verify_mihomo_sha256()
downloading MetaCubeX SHA256SUMS and verifying the .gz BEFORE
decompression. Mismatch is fatal.
- C-4: verify_release_sha256 soft-failed (return 0) when SHA256SUMS
was missing or asset unlisted. Now fail-closed: err exit unless the
operator sets THREE_M_UI_INSECURE=1 (prints a loud WARNING). sha256
tooling is now a hard dependency.
- C-5: Downloaded binaries were executed as root (--version / -v) for
smoke-testing. Remove every invocation; rely on checksum + 'file'
ELF check instead.
- H-1: Drop raw.githubusercontent.com fallback for helper scripts.
Missing release asset now hard-fails (supply-chain posture).
Sandbox:
- H-2: systemd unit gains UMask=0077, ProtectSystem=strict,
ProtectHome, PrivateTmp, PrivateDevices, ProtectKernelTunables/
Modules/Logs, ProtectControlGroups, ReadWritePaths,
RestrictAddressFamilies, RestrictNamespaces, RestrictRealtime,
RestrictSUIDSGID, LockPersonality, SystemCallArchitectures=native,
SystemCallFilter=@system-service ~@privileged @resources.
CapabilityBoundingSet now includes CAP_NET_ADMIN for Mihomo TUN.
- C-4(Docker): Dockerfile creates non-root user 3m-ui (UID 10001),
pre-creates & chowns volumes, sets USER 10001:10001.
- M-3: docker-compose ports 8080:8080 -> 127.0.0.1:8080:8080
(loopback-only by default; operator uses reverse proxy).
- M-9: docker-compose gains security notes (reverse-proxy guidance,
admin/admin + forced first-login change, THREE_M_UI_ADMIN_PASSWORD
example, UID 10001 volume ownership).
- M-10: release.yml gains a TODO comment for cosign keyless signing.
- LOW: AGENTS.md Go 1.24+ -> Go 1.25+; install.sh final message now
'Initial administrator: admin / admin. You MUST change the password
on first login.'
2026-08-29 09:39:25 +00:00
風雪 1d1a69bf74 feat(nat): custom panel port/listen/public_url via env, API, settings UI, install.sh 2026-08-29 12:33:30 +08:00
風雪 bb875405b3 fix(scripts): update.sh match SHA256SUMS with or without ./ prefix 2026-08-28 23:46:47 +08:00
風雪 5cdbedaabf fix: restore install.sh + SHA256SUMS basename matching 2026-08-28 00:09:09 +08:00
風雪 52a66c88cc fix: SHA256SUMS basename match; attach scripts to release assets 2026-08-28 00:04:57 +08:00
風雪 1a79102cac revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
2026-08-27 23:54:52 +08:00
風雪 29ccad2597 revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
revert(scripts): restore pre-s-ui menu/banner versions
2026-08-27 23:54:48 +08:00
風雪 6a859d2e60 feat(install): s-ui style finish banner with panel URLs 2026-08-27 23:18:22 +08:00
風雪 392649c005 feat(scripts): s-ui style installer UX — colors, menu, panel URL, one-liner 2026-08-27 23:15:09 +08:00