8.1 KiB
Architecture — release 0.0.1
One process with explicit module boundaries
Axum serves the HTTP API and embedded interface assets. Tokio manages requests,
scheduling, and notification tasks. Local SQLite access uses rusqlite; a mutex
protects a single connection configured with foreign keys, WAL, and a busy timeout.
The SSH library is synchronous, so blocking sessions run through spawn_blocking.
The application does not launch Python, Node, ssh, scp, ping, or system cron
subprocesses.
Interface sources remain separated by responsibility. web/assets.list specifies
concatenation order. build.rs produces one app.js, one app.css, and the HTML in
Cargo's OUT_DIR; the compiler embeds them in the executable. Deployment does not
read web/ from disk or fetch UI assets from a CDN. A source change requires a new
build. The distribution contains source only, and start.sh builds before launching.
CSS and JavaScript URLs include content-derived identifiers so browsers fetch changed
assets after an update without requiring manual cache clearing.
The same process serves /docs and /openapi.json. The API documentation module
embeds checked-in files directly with include_str!, combines the local Swagger UI
and integration assets into one CSS and one JavaScript response, and supplies
content-derived cache identifiers. This path does not depend on generated
OUT_DIR/docs.* files. Documentation assets load only on the documentation page.
The specification covers 36 operations and uses the configured API prefix. Assets
and validators are not fetched from external services. See
API documentation integration.
Operations and data consistency
Requests validate an HS256 JWT, the current user's existence, and device/backup ownership. Password verification and hashing have a separate four-operation pool; saturation returns 429 without blocking the rest of the API. Login throttling also limits incorrect password attempts. Global settings and logs retain the source application's scope: authenticated operators.
Reading a complete request body has a 1 MiB limit and a 15-second deadline. An incomplete request cannot indefinitely prevent shutdown. This deadline does not limit the execution time of a backup operation.
A backup acquires a device lock and a slot in the operation pool. A tracked task owns
the lock, so a disconnected HTTP client cannot release it halfway through a transfer.
Data is written to a private temporary file under DATA_DIR. After the transfer,
the application validates size, synchronizes the file, calculates SHA-256, and
publishes it. The SQLite record is committed last; a database failure removes the
published file. Deletion stages the file under a temporary name and compensates if
the database update fails.
Each backup access checks DATA_DIR boundaries, names, and symlinks. Bulk ZIP creation
has an aggregate size limit and uses a temporary file; the response streams that
file instead of holding the complete archive in memory. The data directory must
remain private: protection against a local user concurrently changing files depends
on preventing shared write access to it.
Protocols and error handling
RouterOS operations use SSH, pinned host keys, password/private-key authentication,
/export, /system backup save, and SFTP with an SCP fallback. Device-side temporary
files are removed after transfer. Upload stops after writing the file; it never
issues a restore command. The default accept-new policy records the first observed
host key and requires the same key later. Explicit strict requires an already
trusted entry. Host identity keys and client authentication keys serve different
purposes. A timeout covers the entire session; a separate timer closes the socket
if the peer stalls an operation.
SwitchOS uses HTTP(S), Digest or Basic authentication, response validation, and a
size limit. Actual HTML is rejected as a backup regardless of its MIME type, while
otherwise nonempty firmware-specific .swb content is not rejected by MIME/signature
heuristics. A public HTTP 200 page does not prove backup authentication. When needed,
the adapter obtains a Digest challenge from /sys.b and signs the actual
/backup.swb request URI. Connection tests report web reachability separately from
backup availability. Bounded same-host redirects are followed; redirects to a
different host or an HTTPS-to-HTTP downgrade are stopped.
SwitchOS credentials have an explicit global or device-local source. Imported rows without a selection preserve their original field-by-field fallback. Selecting a source does not erase the other stored credentials. The global mode reads current settings on each connection. See protocol details and sources.
ICMP uses a native socket; missing socket permissions produce an explicit diagnostic error. Device connections are initiated only to hosts configured by operators. Known secrets are redacted from public responses and logs. Notification failures do not invalidate a successfully stored backup. Batch operations return a result for each device, including failures and skips.
Scheduling and shutdown
The scheduler reads settings once per second, evaluates five-field cron expressions in the configured timezone, and prevents overlapping instances of the same job. Numeric weekdays preserve APScheduler semantics: Monday is 0. Export and binary jobs respect device policy flags. After a restart, the next execution is calculated; the application does not replay a complete queue of jobs missed while it was off. A delayed tick tolerates a scheduling delay of up to five minutes.
SIGTERM and Ctrl+C stop accepting HTTP requests, prevent new pooled operations,
stop scheduling, and wait for operations already in progress. A WAL checkpoint runs
before exit. The process holds application.lock; the explicit importer uses the
same storage lock. Deployments should allow in-progress operations to finish.
Compose and systemd use a 300-second stop timeout by default; increase this if
operation timeouts or the number of devices require a longer shutdown window.
Fresh initialization and explicit import
src/schema.sql contains the complete current schema. If the database file does not
exist, startup creates a fresh database. If a database file exists, startup first
opens it read-only and validates that it already has the supported schema. An older
or unrelated schema is rejected with instructions to use a fresh installation and
explicit import. Startup does not run ALTER TABLE, create upgrade snapshots, add
migration journals, or move backup files.
The separate import-python --source DIR [--database PATH] [--backup-root DIR]
command imports into a new destination. The import-python.sh wrapper builds the
application and invokes that command. Import opens the source read-only, preserves
IDs, ownership, password hashes, settings, stored credentials, and operation logs,
and copies validated backup content to the destination. Existing destination
databases or files are not overwritten. See the full
Python import procedure.
DATA_DIR contains the database by default, backups, the JWT secret, trusted host
keys, and locks. A separately configured DATABASE_PATH must also be included in
operational backups. These files are deployment data, not release source. Persistent
changes should go through the application; a consistent copy of an active database
requires the SQLite Online Backup API or stopping the service first.
Binary files use backups/binary/<router_id>/, and text exports use
backups/exports/<router_id>/. Stable device IDs keep paths unchanged when devices
are renamed. Fresh backups and explicitly imported files use this layout; startup
does not convert an older layout automatically.
Cargo.lock, the pinned toolchain, the schema/import implementation, and documented
libssh2 backports are part of the release. The port matrix identifies
both source archives by SHA-256. Dependency checks must be repeated for future
releases; an audit result is evidence for a specific dependency set and date, not a
claim that every possible defect has been excluded.