From 36745349855d1858a4b10f5d1683f00dafedf1c1 Mon Sep 17 00:00:00 2001 From: mrvasil Date: Sat, 24 Oct 2026 00:20:12 +0300 Subject: [PATCH] Bind panel on all IPv4 interfaces and support TOML password --- .github/workflows/linux-release.yml | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 25 ++++++++++------ agent.md | 3 ++ deploy/QUICKSTART.md | 32 +++++++++++---------- deploy/run.sh | 16 ++++------- docs/api.md | 4 +++ docs/configuration.md | 6 ++-- mirror.example.toml | 5 ++-- scripts/verify-linux-package.py | 32 +++++++++++++++------ src/auth.rs | 44 +++++++++++++++++++++++------ src/config.rs | 24 ++++++++++++++-- task_plan.md | 4 +++ 14 files changed, 141 insertions(+), 60 deletions(-) diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index 5678092..6232662 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -53,4 +53,4 @@ jobs: GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ github.ref_name }} run: | - gh release create "$RELEASE_TAG" release/* --verify-tag --title "Mirror Tool $RELEASE_TAG — Linux x64" --notes 'Ready-to-run static Linux x64 package. Extract and run ./run.sh. First launch creates a private random panel password and an empty configuration. No Rust, Python, Node, glibc or external SQLite required. See included README.md. Flag substitution is disabled by default.' + gh release create "$RELEASE_TAG" release/* --verify-tag --title "Mirror Tool $RELEASE_TAG — Linux x64" --notes 'Ready-to-run static Linux x64 package. Extract and run ./run.sh. First launch creates mirror.toml with listen=0.0.0.0:6700 and password=mirror-tool-change-me. Change the password in the config before shared-network use. Existing configs are preserved. No Rust, Python, Node, glibc or external SQLite required. See included README.md. Flag substitution is disabled by default.' diff --git a/Cargo.lock b/Cargo.lock index 4b7004a..c72b2a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -921,7 +921,7 @@ dependencies = [ [[package]] name = "mirror-tool" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index a3d4234..f9b914f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mirror-tool" -version = "0.1.0" +version = "0.1.1" edition = "2024" rust-version = "1.85" description = "Async TCP traffic mirror and ForcAD-aware upstream router" diff --git a/README.md b/README.md index d9a4d20..b20ef59 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,19 @@ cd mirror-tool-linux-x64 ``` Статический musl-бинарник не требует установленной glibc, Rust, Python, Node, -Docker или отдельной SQLite. Скрипт создаст пустой `mirror.toml` и случайный пароль -в `data/panel-password` (0600); повторный запуск сохраняет настройки и пароль. -Панель — `http://127.0.0.1:6700`, пароль прочитайте в другом терминале командой -`cat data/panel-password`. Команды/сервисы добавляются через панель; подмена флагов -выключена. [Полная инструкция архива](deploy/QUICKSTART.md). +Docker или отдельной SQLite. Скрипт создаст `mirror.toml` (0600) с настройками: + +```toml +[web] +listen = "0.0.0.0:6700" +password = "mirror-tool-change-me" +state_file = "data/state.json" +``` + +Панель — `http://IP-СЕРВЕРА:6700`. Пароль меняется в TOML, затем процесс нужно +перезапустить. Перед использованием в общей сети замените стандартный пароль. +Повторный запуск сохраняет конфиг и состояние. Команды/сервисы добавляются через +панель; подмена флагов выключена. [Инструкция архива](deploy/QUICKSTART.md). GitHub Actions проверяет musl-тесты, статическую линковку и запуск архива с TCP, API и перезапуском; push тега `v*` публикует архив и SHA256SUMS в Releases. @@ -36,13 +44,12 @@ API и перезапуском; push тега `v*` публикует архи cargo build --release --locked cp mirror.example.toml mirror.toml # Отредактируйте команды, адреса и порты. Пример использует тестовую сеть RFC 5737. -read -s MIRROR_PASSWORD -export MIRROR_PASSWORD +# Укажите свой пароль в [web] password в mirror.toml. ./target/release/mirror-tool --config mirror.toml check ./target/release/mirror-tool --config mirror.toml ``` -Пароль должен содержать минимум 12 символов. Панель по умолчанию доступна на `http://127.0.0.1:6700`. Для удалённого доступа используйте VPN/SSH-туннель либо HTTPS reverse proxy; при HTTPS установите `secure_cookie = true`. TLS для управляющей панели внутри бинарника не реализован. Переменные окружения с паролем и конфигурацию игры не коммитьте. +Пароль должен содержать минимум 12 символов. Панель по умолчанию слушает `0.0.0.0:6700`, адрес в браузере — `http://IP-СЕРВЕРА:6700`. При HTTPS установите `secure_cookie = true`. TLS для управляющей панели внутри бинарника не реализован. Переменные окружения с паролем и конфигурацию игры не коммитьте. Панель сохраняет оформление старого `mirror_tool`: горизонтальные колонки сервисов, список команд, кружок primary, выбор реплики нажатием на строку, робот авто режима, открытие сервиса и переключатель запуска. При выборе нового primary прежний становится репликой. Кнопка «+» открывает прежнюю форму: название, порт и необязательный IPv4/IPv6. Предварительно создавать команду для произвольного IP не нужно. У custom-прокси остаются поле IP и удаление в самой колонке. @@ -115,7 +122,7 @@ Stop отключает приём новых соединений, сохран Запись состояния: временный файл на той же ФС → fsync → atomic rename; права `0600`. Сбой сохранения вызывает попытку отката runtime. Публикация topology сериализована с ручными и автоматическими изменениями. Runtime и ФС не образуют распределённую транзакцию: клиент, принятый в коротком окне до неудачного сохранения, может сохранить временно применённый маршрут. Необходимость отката выводится как ошибка. После допуска к транзакции она завершается независимо от HTTP-соединения: закрытие вкладки не оставляет сохранение/очистку наполовину. Если ответ потерян, обновите snapshot перед повтором — изменение могло сохраниться. -Старый `config.py` не выполняется и не импортируется. Перенесите `TEAMS` в `[[teams]]`, `TASKS` в `[[services]]`, ForcAD URL в `[scoreboard]`, `MAIN_TEAM_IP` в верхнеуровневый `main_team_ip`. `PASSWORD` заменён переменной окружения, `SECRET_KEY` не нужен: сервер хранит случайные сессии в памяти. Старые cookie после перезапуска недействительны. API описан в [docs/api.md](docs/api.md). +Старый `config.py` не выполняется и не импортируется. Перенесите `TEAMS` в `[[teams]]`, `TASKS` в `[[services]]`, ForcAD URL в `[scoreboard]`, `MAIN_TEAM_IP` в верхнеуровневый `main_team_ip`. `PASSWORD` переносится в `[web] password` (если поле отсутствует, используется `MIRROR_PASSWORD`), `SECRET_KEY` не нужен: сервер хранит случайные сессии в памяти. Старые cookie после перезапуска недействительны. API описан в [docs/api.md](docs/api.md). Для Linux есть [systemd unit](deploy/mirror-tool.service): создайте отдельного пользователя `mirror-tool`, разместите бинарник в `/usr/local/bin/`, конфиг и закрытый `mirror.env` в `/etc/mirror-tool/`; в TOML задайте `state_file = "/var/lib/mirror-tool/state.json"`. Unit даёт только CAP_NET_BIND_SERVICE для портов ниже 1024; при ненужности удалите capability. При увеличении drain timeout увеличьте `TimeoutStopSec`. Unit поставляется как шаблон, автоматически не устанавливается. diff --git a/agent.md b/agent.md index 38f7175..9205078 100644 --- a/agent.md +++ b/agent.md @@ -50,3 +50,6 @@ User explicitly requires optional substitution, default off. Implemented per-ser User wants a ready-to-run Linux x64 build on GitHub, exact distribution unknown. Package static musl binary with run.sh, empty first-run config and random persistent private password; do not package runtime data or enable substitution. Workflow verifies native Clippy/fmt and musl tests plus archive login/API/TCP/restart smoke, then tag releases publish privately. Main push artifacts do not replace releases. Actual CI/package verification pending; docs in deploy/QUICKSTART.md. Portable release completed: v0.1.0 at source f264f8a, private GitHub Release includes static musl Linux x64 tar.gz + SHA256SUMS. CI40011983417 succeeded: fmt/Clippy,50 musl tests, static linkage check, release build and packaged first-run/login/API/restart/256KiB TCP smoke. Downloaded release independently passed SHA256, full package smoke in Orb Ubuntu amd64 and launcher/HTTP/graceful-stop in isolated Alpine3.22 without glibc. Native49 tests/fmt/Clippy also passed. No runtime/compiler/database dependencies beyond normal shell utilities for run.sh. First-run config is empty; real targets still require operator setup. Live demo unchanged. See docs/linux-release-verification.md. + +## v0.1.1 startup correction — in progress +User explicitly requires panel bind0.0.0.0 and default password in TOML. Native default, example and first-run launcher now bind0.0.0.0:6700; launcher/example explicitly set web.password to a public placeholder. Explicit config password wins; absent field uses legacy environment, and run.sh preserves v0.1.0 password-file fallback. Existing configs are never overwritten. WebConfig password is excluded from serialization and redacted Debug. Native50 tests/fmt/Clippy passed; tag release/package verification pending. diff --git a/deploy/QUICKSTART.md b/deploy/QUICKSTART.md index 67543d3..e69b6b9 100644 --- a/deploy/QUICKSTART.md +++ b/deploy/QUICKSTART.md @@ -6,24 +6,26 @@ cd mirror-tool-linux-x64 ./run.sh ``` -Панель: http://127.0.0.1:6700. В другом терминале из этой папки: +Панель слушает все IPv4-интерфейсы: **http://IP-СЕРВЕРА:6700**. +Пароль по умолчанию: **mirror-tool-change-me**. Настройки — в `mirror.toml`: -```sh -cat data/panel-password +```toml +[web] +listen = "0.0.0.0:6700" +password = "mirror-tool-change-me" +state_file = "data/state.json" ``` -Пароль генерируется случайно при первом запуске и сохраняется с правами0600. -Можно передать собственный пароль через MIRROR_PASSWORD (не менее12 символов). -Пароль не выводится в логи. Запускать от root не требуется для портов выше1023. +Измените `password` и перезапустите процесс. Минимум12 символов; перед +использованием в общей сети замените стандартное значение. Созданный конфиг +имеет права0600. Пароль не выводится в логи/API. Если `password` не указан, +используется переменная MIRROR_PASSWORD; старый run.sh-пароль из +`data/panel-password` также подхватывается. Явный `web.password` имеет приоритет. +Запускать от root не требуется для портов выше1023. -Для удалённого сервера откройте туннель со своего компьютера: - -```sh -ssh -N -L 6700:127.0.0.1:6700 user@server -``` - -Затем откройте http://127.0.0.1:6700 на своём компьютере. Панель по умолчанию -доступна только локально; не публикуйте её HTTP-порт в игровой сети. +Обновление с v0.1.0 не перезаписывает ваш конфиг: для доступа по IP измените +существующее `[web] listen` на `"0.0.0.0:6700"` и задайте `password` в той же секции. +Для локального доступа можно явно оставить `"127.0.0.1:6700"`. Первый запуск создаёт пустой mirror.toml. Добавьте команды и прокси в панели; нажмите название сервиса для настройки слушателя и upstream-порта. Выберите @@ -46,7 +48,7 @@ mirror.example.toml содержит полный пример. Для ForcAD д Бинарник статический (musl), базовая архитектура x86-64, без требования AVX. Не нужны glibc, Rust, Python, Node, Docker или отдельная SQLite. Для run.sh нужны -обычные /bin/sh, uname, dirname, mkdir, od, tr, cat и /dev/urandom. Для распаковки +обычные /bin/sh, uname, dirname, mkdir и cat. Для распаковки нужен tar с gzip. Linux kernel3.2+ — базовый минимум target Rust; конкретная старая система не считается проверенной только по номеру ядра. Ограничения контейнера, seccomp, права, доступная память, файловые дескрипторы и сеть всё равно действуют. diff --git a/deploy/run.sh b/deploy/run.sh index 32a8450..6bd3360 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -13,22 +13,16 @@ if [ ! -e mirror.toml ]; then # Add teams/services in the panel or use mirror.example.toml as a reference. # Existing data/state.json takes precedence over teams/services in this file. [web] -listen = "127.0.0.1:6700" +listen = "0.0.0.0:6700" +password = "mirror-tool-change-me" state_file = "data/state.json" CONFIG ) fi -if [ -z "${MIRROR_PASSWORD:-}" ]; then - if [ ! -e data/panel-password ]; then - (set -C; od -An -N24 -tx1 /dev/urandom | tr -d ' \n' > data/panel-password) - fi +# Compatibility with v0.1.0: explicit web.password takes priority over this. +if [ -z "${MIRROR_PASSWORD:-}" ] && [ -f data/panel-password ]; then MIRROR_PASSWORD=$(cat data/panel-password) - if [ "${#MIRROR_PASSWORD}" -lt 12 ]; then - echo 'data/panel-password is invalid; restore it or supply MIRROR_PASSWORD.' >&2 - exit 1 - fi export MIRROR_PASSWORD - echo 'Panel password: read data/panel-password (kept across restarts).' fi -echo 'Config: mirror.toml. Default panel: http://127.0.0.1:6700 (see web.listen).' +echo 'Config/password: mirror.toml [web]. Default listener: 0.0.0.0:6700; open http://SERVER-IP:6700.' exec ./mirror-tool --config mirror.toml "$@" diff --git a/docs/api.md b/docs/api.md index b7120d7..8a3f111 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3,6 +3,10 @@ JSON API, UTF-8. Authenticated sessions use an HttpOnly SameSite=Strict cookie. Mutating authenticated requests require `X-CSRF-Token` from login or session response. Browser requests must be same-origin. No secrets returned in snapshots. Application-handled failures return `{ "error": "human readable explanation" }` with non-2xx status. Framework routing errors (for example 405) may have a plain-text body; clients must tolerate this. A snapshot is the source of truth; a 2xx response confirms completion. After admission under the serialized operation lock, topology mutations run independently of the HTTP connection through commit/rollback and cleanup. A lost response does not imply an uncommitted change: refresh the snapshot before retrying. Requests cancelled while waiting for admission make no change. Shutdown rejects new admissions and waits for admitted work within the documented process deadline. ## Routes +Panel credentials come from TOML `web.password` when present; otherwise the +configured `web.password_env` is used. They are never exposed in snapshots or +serialized WebConfig diagnostics. Changing the password requires process restart. + - `GET /` — embedded UI shell (login shown until session validated). - `GET /licenses.txt` — licenses and attribution for the bundled Bootstrap/Font Awesome assets. - `GET /healthz` — process readiness `{status:"ok"}` (not upstream checker health). diff --git a/docs/configuration.md b/docs/configuration.md index 327bb07..971fe98 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -57,8 +57,10 @@ state-файл в резервную копию и запустите проце сервиса относится к своей команде (`main_team_ip`), а не к выбранному upstream. Серый статус означает отсутствие свежих данных, а не успех checker. -Пароль обычного процесса задаётся в `MIRROR_PASSWORD` (минимум 12 символов), -не в TOML. Тестовый fixture для настоящей игры не используйте; запускайте бинарник +Пароль задаётся в `[web] password` в TOML (минимум12 символов). Стартовый +шаблон использует `mirror-tool-change-me`, измените его перед игрой. Если поле +отсутствует, используется `MIRROR_PASSWORD`. По умолчанию панель слушает +`0.0.0.0:6700`, в браузере открывайте `http://IP-СЕРВЕРА:6700`. Тестовый fixture для настоящей игры не используйте; запускайте бинарник напрямую с собственным конфигом и паролем. Реальная игра этой проверкой не затрагивалась. ## Повторяемое локальное демо diff --git a/mirror.example.toml b/mirror.example.toml index 0bd36e4..82db559 100644 --- a/mirror.example.toml +++ b/mirror.example.toml @@ -1,10 +1,11 @@ -# Copy to mirror.toml. Set MIRROR_PASSWORD in your environment (12+ characters). +# Copy to mirror.toml. Change web.password before using on a shared network. # Bind the panel to a VPN interface or put it behind HTTPS when used remotely. # Optional: own team's checker status in the top strip, as in the legacy UI. # main_team_ip = "192.0.2.10" [web] -listen = "127.0.0.1:6700" +listen = "0.0.0.0:6700" +password = "mirror-tool-change-me" password_env = "MIRROR_PASSWORD" state_file = "data/state.json" session_ttl_seconds = 43200 diff --git a/scripts/verify-linux-package.py b/scripts/verify-linux-package.py index cd03568..6b82539 100644 --- a/scripts/verify-linux-package.py +++ b/scripts/verify-linux-package.py @@ -29,13 +29,17 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary: env = os.environ.copy() env.pop("MIRROR_PASSWORD", None) subprocess.run([str(package / "run.sh"), "check"], cwd=root, env=env, check=True) - password_path = package / "data/panel-password" - password = password_path.read_text() - assert len(password) == 48 - assert stat.S_IMODE(password_path.stat().st_mode) == 0o600 config = package / "mirror.toml" + password = "mirror-tool-change-me" + assert f'password = "{password}"' in config.read_text() + assert 'listen = "0.0.0.0:6700"' in config.read_text() + assert stat.S_IMODE(config.stat().st_mode) == 0o600 + assert not (package / "data/panel-password").exists() + # Explicit config must win over a legacy password file during upgrades. + legacy_password = "legacy-synthetic-passphrase" + (package / "data/panel-password").write_text(legacy_password) web_port = free_port() - config.write_text(config.read_text().replace("127.0.0.1:6700", f"127.0.0.1:{web_port}")) + config.write_text(config.read_text().replace("0.0.0.0:6700", f"0.0.0.0:{web_port}")) original_config = config.read_bytes() base = f"http://127.0.0.1:{web_port}" upstream = socket.socket() @@ -55,7 +59,8 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary: thread = threading.Thread(target=echo, daemon=True) thread.start() proxy_port = free_port() - for restart in (False, True): + for phase in range(3): + restart = phase > 0 with (root / "server.log").open("wb") as log: process = subprocess.Popen([str(package / "run.sh")], cwd=root, env=env, stdout=log, stderr=log) try: @@ -71,6 +76,7 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary: time.sleep(0.1) else: raise RuntimeError("packaged server did not become ready") + assert any(line.split()[1] == f"00000000:{web_port:04X}" and line.split()[3] == "0A" for line in Path("/proc/net/tcp").read_text().splitlines()[1:]) def request(path, value=None, method=None, csrf=None): headers = {"Content-Type": "application/json"} @@ -99,9 +105,10 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary: assert not thread.is_alive() assert reply == received == payload snapshot = request("/api/snapshot") + assert password not in json.dumps(snapshot) assert snapshot["services"][0]["flag_protection"] is False assert snapshot["services"][0]["enabled"] is True - assert password_path.read_text() == password + assert (package / "data/panel-password").read_text() == legacy_password assert config.read_bytes() == original_config finally: process.terminate() @@ -112,5 +119,14 @@ with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary: process.wait() raise assert process.returncode == 0 + if not restart: + password = "updated-config-passphrase" + config.write_text(config.read_text().replace("mirror-tool-change-me", password)) + original_config = config.read_bytes() + elif phase == 1: + # Old v0.1.0 config without web.password still uses the saved password. + config.write_text(config.read_text().replace(f'password = "{password}"\n', "")) + password = legacy_password + original_config = config.read_bytes() upstream.close() -print("Package OK: first start, private password, empty topology, login, API persistence/restart, exact 256KiB TCP half-close, flag substitution off.") +print("Package OK: wildcard IPv4 listener, config password/change/restart, legacy-file precedence, empty topology, API persistence, exact 256KiB TCP half-close, flag substitution off.") diff --git a/src/auth.rs b/src/auth.rs index ff5370b..dd52b2d 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -46,16 +46,20 @@ fn random_token() -> String { } impl Auth { pub fn from_env(settings: &WebConfig) -> Result { - let password = std::env::var(&settings.password_env).map_err(|_| { - anyhow::anyhow!( - "set {} to a password with at least 12 characters", - settings.password_env - ) - })?; + let password = settings + .password + .clone() + .map(Ok) + .unwrap_or_else(|| std::env::var(&settings.password_env)) + .map_err(|_| { + anyhow::anyhow!( + "set web.password in TOML or {} to a password with at least 12 characters", + settings.password_env + ) + })?; ensure!( password.len() >= 12, - "{} must contain at least 12 characters", - settings.password_env + "panel password must contain at least 12 characters" ); Ok(Self::new(settings.clone(), &password)) } @@ -181,6 +185,30 @@ pub fn same_origin(headers: &HeaderMap) -> bool { mod tests { use super::*; #[test] + fn configured_password_authenticates_without_environment_and_is_redacted() { + let config: crate::config::Config = toml::from_str( + "[web]\npassword = 'configuration-test-passphrase'\npassword_env = 'MIRROR_UNUSED_TEST_ENV'", + ).unwrap(); + config.validate().unwrap(); + let a = Auth::from_env(&config.web).unwrap(); + let peer = "127.0.0.1".parse().unwrap(); + assert!(a.login("configuration-test-passphrase", peer).is_ok()); + assert!(matches!( + a.login("incorrect-password", peer), + Err(LoginError::Invalid) + )); + assert!(!format!("{:?}", config.web).contains("configuration-test-passphrase")); + assert!( + !serde_json::to_string(&config.web) + .unwrap() + .contains("configuration-test-passphrase") + ); + let mut invalid = config; + invalid.web.password = Some("short".into()); + assert!(invalid.validate().is_err()); + assert!(Auth::from_env(&invalid.web).is_err()); + } + #[test] fn session_csrf_and_revocation() { let a = Auth::new(WebConfig::default(), "test-passphrase"); let (cookie, info) = a diff --git a/src/config.rs b/src/config.rs index ca4afc7..0083f19 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,16 +19,31 @@ pub struct Config { pub services: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct WebConfig { pub listen: SocketAddr, + #[serde(skip_serializing)] + pub password: Option, pub password_env: String, pub state_file: PathBuf, pub session_ttl_seconds: u64, pub secure_cookie: bool, } +impl std::fmt::Debug for WebConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WebConfig") + .field("listen", &self.listen) + .field("password", &self.password.as_ref().map(|_| "[REDACTED]")) + .field("password_env", &self.password_env) + .field("state_file", &self.state_file) + .field("session_ttl_seconds", &self.session_ttl_seconds) + .field("secure_cookie", &self.secure_cookie) + .finish() + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct NetworkConfig { @@ -117,7 +132,8 @@ pub enum ReplicaPolicy { impl Default for WebConfig { fn default() -> Self { Self { - listen: "127.0.0.1:6700".parse().unwrap(), + listen: "0.0.0.0:6700".parse().unwrap(), + password: None, password_env: "MIRROR_PASSWORD".into(), state_file: "data/state.json".into(), session_ttl_seconds: 43200, @@ -192,6 +208,10 @@ impl Config { } pub fn validate(&self) -> Result<()> { + ensure!( + self.web.password.as_ref().is_none_or(|p| p.len() >= 12), + "web.password must contain at least 12 characters" + ); ensure!( !self.web.password_env.is_empty(), "password_env must name an environment variable" diff --git a/task_plan.md b/task_plan.md index 49f78f4..abd50e7 100644 --- a/task_plan.md +++ b/task_plan.md @@ -49,3 +49,7 @@ Flag implementation issues recorded: cargo-add combined feature syntax rejected - [complete] Add static x86_64-unknown-linux-musl packaging, first-run launcher and operator quickstart. - [complete] Add pinned GitHub Actions build/test/artifact and tag-triggered release workflow. - [complete] Validate actual archive on Linux, publish private GitHub release and verify downloaded asset. + +## v0.1.1 startup correction +- [complete] Wildcard IPv4 panel default and explicit TOML password, preserving existing configs/legacy auth. +- [in progress] Publish and validate static Linux release; package tests assert wildcard socket, config password/change/restart and legacy-file fallback.