Files
2026-10-28 09:16:54 +01:00

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.