mirror of
https://github.com/mrvasil/ctf-ad-mirror-tool-rust.git
synced 2026-10-24 22:49:25 +00:00
Package static Linux x64 releases with first-run launcher
This commit is contained in:
parent
8dcd1feaf1
commit
f264f8a673
56
.github/workflows/linux-release.yml
vendored
Normal file
56
.github/workflows/linux-release.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
name: Linux x64
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
- name: Install pinned Rust and musl build tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools
|
||||
rustup toolchain install 1.93.1 --profile minimal --component rustfmt --component clippy --target x86_64-unknown-linux-musl
|
||||
rustup override set 1.93.1
|
||||
- name: Verify
|
||||
run: |
|
||||
cargo fmt --check
|
||||
cargo clippy --all-targets --locked -- -D warnings
|
||||
cargo test --all-targets --locked --target x86_64-unknown-linux-musl
|
||||
node --check web/app.js
|
||||
sh -n deploy/run.sh scripts/package-linux.sh
|
||||
- name: Build static release and verify package
|
||||
run: |
|
||||
cargo build --release --locked --target x86_64-unknown-linux-musl
|
||||
sh scripts/package-linux.sh
|
||||
python3 scripts/verify-linux-package.py dist/release/mirror-tool-linux-x64.tar.gz
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: mirror-tool-linux-x64
|
||||
path: dist/release/*
|
||||
if-no-files-found: error
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: mirror-tool-linux-x64
|
||||
path: release
|
||||
- name: Publish verified archive
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
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.'
|
||||
24
README.md
24
README.md
@ -6,6 +6,30 @@
|
||||
|
||||
## Запуск
|
||||
|
||||
Для Linux x64 скачайте **mirror-tool-linux-x64.tar.gz** и **SHA256SUMS** из
|
||||
[Releases](https://github.com/mrvasil/ctf-ad-mirror-tool-rust/releases/latest)
|
||||
(нужен доступ к приватному репозиторию):
|
||||
|
||||
```sh
|
||||
sha256sum -c SHA256SUMS
|
||||
tar -xzf mirror-tool-linux-x64.tar.gz
|
||||
cd mirror-tool-linux-x64
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Статический 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).
|
||||
|
||||
GitHub Actions проверяет musl-тесты, статическую линковку и запуск архива с TCP,
|
||||
API и перезапуском; push тега `v*` публикует архив и SHA256SUMS в Releases.
|
||||
Обычный push в main сохраняет сборку как Actions artifact, не меняя релиз.
|
||||
|
||||
Для сборки из исходников:
|
||||
|
||||
Нужен актуальный stable Rust (проверенные версии записаны в `docs/verification.md`).
|
||||
|
||||
```sh
|
||||
|
||||
3
agent.md
3
agent.md
@ -45,3 +45,6 @@ User authorized design explanation followed by implementation. Explained random
|
||||
|
||||
## Flag protection completed — 2026-10-23
|
||||
User explicitly requires optional substitution, default off. Implemented per-service `flag_protection=false`, backwards-compatible omitted config/state fields, settings checkbox and indicator without redesign. Mode is captured for each TCP session; disabling affects new sessions. Random same-format tokens use an XChaCha20-Poly1305 encrypted SQLite vault, durable before forwarding, bounded worker queue, TTL/capacity and fail-closed errors. See docs/flag-protection.md and docs/flag-protection-verification.md. Fresh49 native/50 Orb tests, native fmt/Clippy, JS syntax and both release builds passed. Browser toggled on/off and persisted both; TCP tests verify byte integrity/half-close/replica equality and session mode. Live demo16700 upgraded with ignored persistent synthetic vault, original topology unchanged, protection off. No real hosts or flags used.
|
||||
|
||||
## Portable Linux release in progress
|
||||
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.
|
||||
|
||||
60
deploy/QUICKSTART.md
Normal file
60
deploy/QUICKSTART.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Mirror Tool — Linux x64
|
||||
|
||||
```sh
|
||||
tar -xzf mirror-tool-linux-x64.tar.gz
|
||||
cd mirror-tool-linux-x64
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Панель: http://127.0.0.1:6700. В другом терминале из этой папки:
|
||||
|
||||
```sh
|
||||
cat data/panel-password
|
||||
```
|
||||
|
||||
Пароль генерируется случайно при первом запуске и сохраняется с правами0600.
|
||||
Можно передать собственный пароль через MIRROR_PASSWORD (не менее12 символов).
|
||||
Пароль не выводится в логи. Запускать от root не требуется для портов выше1023.
|
||||
|
||||
Для удалённого сервера откройте туннель со своего компьютера:
|
||||
|
||||
```sh
|
||||
ssh -N -L 6700:127.0.0.1:6700 user@server
|
||||
```
|
||||
|
||||
Затем откройте http://127.0.0.1:6700 на своём компьютере. Панель по умолчанию
|
||||
доступна только локально; не публикуйте её HTTP-порт в игровой сети.
|
||||
|
||||
Первый запуск создаёт пустой mirror.toml. Добавьте команды и прокси в панели;
|
||||
нажмите название сервиса для настройки слушателя и upstream-порта. Выберите
|
||||
primary, реплики и включите сервис. Порты должны быть свободны. Подмена флагов
|
||||
выключена; системные маршруты/firewall не изменяются. Игровые адреса не угадываются.
|
||||
|
||||
mirror.example.toml содержит полный пример. Для ForcAD добавьте [scoreboard]
|
||||
в mirror.toml и перезапустите процесс. Подробности в docs/configuration.md.
|
||||
`./run.sh check` проверяет конфиг, но не сетевую доступность или ключ кеша.
|
||||
|
||||
Остановка: Ctrl+C или SIGTERM. Для постоянной работы используйте tmux либо
|
||||
приложенный шаблон systemd (требует установки бинарника, пользователя и конфигов,
|
||||
как описано в исходном README проекта). С закрытием SSH foreground-процесс может
|
||||
завершиться. run.sh сам автозапуск и системную службу не устанавливает.
|
||||
|
||||
Сохраняйте mirror.toml и каталог data при обновлении. Архив их не содержит:
|
||||
распаковка обновления не заменяет рабочие настройки/пароль/состояние. Остановите
|
||||
процесс перед обновлением. Настройки команд/сервисов из data/state.json имеют
|
||||
приоритет над TOML. Резервную копию делайте при остановленном процессе.
|
||||
|
||||
Бинарник статический (musl), базовая архитектура x86-64, без требования AVX.
|
||||
Не нужны glibc, Rust, Python, Node, Docker или отдельная SQLite. Для run.sh нужны
|
||||
обычные /bin/sh, uname, dirname, mkdir, od, tr, cat и /dev/urandom. Для распаковки
|
||||
нужен tar с gzip. Linux kernel3.2+ — базовый минимум target Rust; конкретная старая
|
||||
система не считается проверенной только по номеру ядра. Ограничения контейнера,
|
||||
seccomp, права, доступная память, файловые дескрипторы и сеть всё равно действуют.
|
||||
|
||||
Проверка архива: `sha256sum -c SHA256SUMS` до распаковки. BUILD-COMMIT указывает
|
||||
исходный коммит. Репозиторий и Releases приватные: скачивание требует доступа
|
||||
к GitHub. С GitHub CLI: `gh release download --repo mrvasil/ctf-ad-mirror-tool-rust
|
||||
--pattern 'mirror-tool-linux-x64.tar.gz' --pattern SHA256SUMS` (одной строкой).
|
||||
|
||||
Перед игрой проверьте checker и переключение на своих сервисах: зеркалирование
|
||||
TCP-запросов не является репликацией БД. Подробнее: docs/flag-protection.md.
|
||||
34
deploy/run.sh
Normal file
34
deploy/run.sh
Normal file
@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# Portable Linux launcher. Runtime data is never included in release archives.
|
||||
set -eu
|
||||
case "$(uname -s):$(uname -m)" in
|
||||
Linux:x86_64|Linux:amd64) ;;
|
||||
*) echo 'This package requires Linux x86-64.' >&2; exit 1 ;;
|
||||
esac
|
||||
cd -- "$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
|
||||
umask 077
|
||||
mkdir -p data
|
||||
if [ ! -e mirror.toml ]; then
|
||||
(set -C; cat > mirror.toml <<'CONFIG'
|
||||
# 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"
|
||||
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
|
||||
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).'
|
||||
exec ./mirror-tool --config mirror.toml "$@"
|
||||
25
scripts/package-linux.sh
Normal file
25
scripts/package-linux.sh
Normal file
@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# Run from the repository root after building the musl target.
|
||||
set -eu
|
||||
binary=${1:-target/x86_64-unknown-linux-musl/release/mirror-tool}
|
||||
out=${2:-dist/release}
|
||||
readelf -h "$binary" | grep -q 'Advanced Micro Devices X86-64'
|
||||
if readelf -l "$binary" | grep -q INTERP || readelf -d "$binary" | grep -q NEEDED; then
|
||||
echo 'Refusing to package a dynamically linked binary.' >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$out"
|
||||
stage=$(mktemp -d)
|
||||
trap 'rm -rf "$stage"' EXIT HUP INT TERM
|
||||
package="$stage/mirror-tool-linux-x64"
|
||||
mkdir -p "$package/docs"
|
||||
cp "$binary" "$package/mirror-tool"
|
||||
cp deploy/run.sh "$package/run.sh"
|
||||
chmod 755 "$package/mirror-tool" "$package/run.sh"
|
||||
cp mirror.example.toml "$package/"
|
||||
cp deploy/QUICKSTART.md "$package/README.md"
|
||||
cp docs/configuration.md docs/flag-protection.md "$package/docs/"
|
||||
cp deploy/mirror-tool.service "$package/"
|
||||
git rev-parse HEAD > "$package/BUILD-COMMIT"
|
||||
tar -czf "$out/mirror-tool-linux-x64.tar.gz" -C "$stage" mirror-tool-linux-x64
|
||||
(cd "$out" && sha256sum mirror-tool-linux-x64.tar.gz > SHA256SUMS)
|
||||
116
scripts/verify-linux-package.py
Normal file
116
scripts/verify-linux-package.py
Normal file
@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test the shipped archive in isolation; never print generated credentials."""
|
||||
import http.cookiejar
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
def free_port():
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
return sock.getsockname()[1]
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="mirror package ") as temporary:
|
||||
root = Path(temporary)
|
||||
with tarfile.open(sys.argv[1]) as archive:
|
||||
archive.extractall(root, filter="data")
|
||||
package = root / "mirror-tool-linux-x64"
|
||||
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"
|
||||
web_port = free_port()
|
||||
config.write_text(config.read_text().replace("127.0.0.1:6700", f"127.0.0.1:{web_port}"))
|
||||
original_config = config.read_bytes()
|
||||
base = f"http://127.0.0.1:{web_port}"
|
||||
upstream = socket.socket()
|
||||
upstream.bind(("127.0.0.1", 0))
|
||||
upstream.listen()
|
||||
upstream.settimeout(15)
|
||||
received = bytearray()
|
||||
|
||||
def echo():
|
||||
connection, _ = upstream.accept()
|
||||
with connection:
|
||||
connection.settimeout(10)
|
||||
while data := connection.recv(65536):
|
||||
received.extend(data)
|
||||
connection.sendall(data)
|
||||
|
||||
thread = threading.Thread(target=echo, daemon=True)
|
||||
thread.start()
|
||||
proxy_port = free_port()
|
||||
for restart in (False, True):
|
||||
with (root / "server.log").open("wb") as log:
|
||||
process = subprocess.Popen([str(package / "run.sh")], cwd=root, env=env, stdout=log, stderr=log)
|
||||
try:
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
||||
for _ in range(100):
|
||||
if process.poll() is not None:
|
||||
raise RuntimeError("packaged process exited during startup")
|
||||
try:
|
||||
with opener.open(base + "/", timeout=1) as response:
|
||||
assert b"html" in response.read().lower()
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise RuntimeError("packaged server did not become ready")
|
||||
|
||||
def request(path, value=None, method=None, csrf=None):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if csrf:
|
||||
headers["x-csrf-token"] = csrf
|
||||
req = urllib.request.Request(base + path, data=None if value is None else json.dumps(value).encode(), headers=headers, method=method)
|
||||
with opener.open(req, timeout=3) as response:
|
||||
return json.load(response)
|
||||
|
||||
csrf = request("/api/login", {"password": password})["csrf"]
|
||||
if not restart:
|
||||
snapshot = request("/api/snapshot")
|
||||
assert snapshot["teams"] == [] and snapshot["services"] == []
|
||||
assert snapshot["flag_protection"] is None
|
||||
request("/api/teams/local", {"id": "local", "name": "Local", "address": "127.0.0.1"}, "PUT", csrf)
|
||||
service = {"id": "echo", "name": "Echo", "listen": f"127.0.0.1:{proxy_port}", "upstream_port": upstream.getsockname()[1], "primary": "local", "replicas": [], "enabled": True, "auto_switch": False}
|
||||
request("/api/services/echo", service, "PUT", csrf)
|
||||
payload = bytes(range(256)) * 1024
|
||||
with socket.create_connection(("127.0.0.1", proxy_port), timeout=5) as connection:
|
||||
connection.sendall(payload)
|
||||
connection.shutdown(socket.SHUT_WR)
|
||||
reply = bytearray()
|
||||
while part := connection.recv(65536):
|
||||
reply.extend(part)
|
||||
thread.join(5)
|
||||
assert not thread.is_alive()
|
||||
assert reply == received == payload
|
||||
snapshot = request("/api/snapshot")
|
||||
assert snapshot["services"][0]["flag_protection"] is False
|
||||
assert snapshot["services"][0]["enabled"] is True
|
||||
assert password_path.read_text() == password
|
||||
assert config.read_bytes() == original_config
|
||||
finally:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=20)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
raise
|
||||
assert process.returncode == 0
|
||||
upstream.close()
|
||||
print("Package OK: first start, private password, empty topology, login, API persistence/restart, exact 256KiB TCP half-close, flag substitution off.")
|
||||
@ -44,3 +44,8 @@ One native executable with an async TCP engine and embedded web UI. No Python/ru
|
||||
Design limits: stateful tokenization, not stateless same-length AEAD; XChaCha20-Poly1305 protects mappings on disk. Raw visible fixed-shape ASCII only; no TLS/compression/framing adapter. Incomplete possible flags may hold a protocol tail; bounded timeout closes rather than leaking. All clients get reverse translation, so not an authorization boundary. Disabled by default; no real game hosts/flags used.
|
||||
|
||||
Flag implementation issues recorded: cargo-add combined feature syntax rejected and corrected with separate calls; first module write preceded async mkdir and was retried after explicit directory creation; first test compile warned about unused Arc import, removed before Clippy gate. No failing runtime regressions so far. Final durability review upgraded SQLite synchronous from FULL to EXTRA; final native/Orb gates rerun on that source.
|
||||
|
||||
## Portable Linux release — 2026-10-23
|
||||
- [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.
|
||||
- [in progress] Validate actual archive on Linux, publish private GitHub release and verify downloaded asset.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user