Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Server runs as a Windows service of its own, under a virtual account, logging to daily files

Status: Accepted (2026-10-01).

A Server meant to run unattended on Windows must be a Windows service: the Service Control Manager starts a service at boot, restarts it after a failure and stops it at shutdown. udpstp server wasn’t one. The stop-gaps all failed somewhere:

  • a Task Scheduler task’s account credentials expire after 42 days by default, it runs below normal priority and it has no standard output;
  • a third-party wrapper, WinSW or NSSM, is one more program in front of a network-facing Server to trust, ship and update, neither has had a stable release for years, and it can’t tell the Service Control Manager when the Server actually listens.

udpstp server --windows-service is the service, udpstp-server:

  • Native support, through windows-service (Mullvad’s crate), without its define_windows_service! macro. That macro, the documented way to write a service’s entry point, expands an unsafe block into the calling crate, where #![forbid(unsafe_code)] doesn’t see it (rustc doesn’t report the unsafe_code lint in another crate’s macro), so it would be udpstp’s own unsafe in all but name, which needs an ADR of its own (the specification). The entry point is written by hand instead, a safe extern "system" function that never reads the start parameters the Service Control Manager passes; the Server’s options come from the service’s command line. clippy.toml bans the macro, since the compiler won’t.
  • The same Server. The service runs the code a console runs, and reports its state: starting, running once it listens, stopping, and stopped with the exit status it would have had in a console as its service-specific exit code. It stops on the service’s stop and shutdown controls, through the same path as Ctrl+C, so its Tests get their STOP and the log its summaries (Stopping, reason ServiceStop or ServiceShutdown). It installs no console handlers: a service gets the console’s shutdown event too, before the service’s own shutdown control, and acting on it would log the wrong reason.
  • Its own virtual account, NT SERVICE\udpstp-server. Windows manages it, with no credential to expire. The Server needs no privilege, since Windows has no privileged ports, so the service keeps only SeChangeNotifyPrivilege, which every process keeps; a service otherwise holds SeImpersonatePrivilege, the usual way from a service account to SYSTEM. Its own SID lets the Key Table be readable by the Server and no one else, and lets a firewall rule be scoped to the service. LocalService would share its identity with other services, and LocalSystem is far more than the Server needs. The name is the systemd unit’s, and is costly to change: the account’s SID follows from it, and so do the ACLs, the firewall rule and administrators’ scripts.
  • The log goes to daily files (log_file), since a service has no standard output: a new file each day, the last 14 kept, through tracing-appender. Rotating one file by renaming it fails on Windows while a reader holds it open, and a size bound keeps only hours of history during a flood, which the log is kept for. The Windows Event Log was rejected: it needs an event source registered at install and a message resource, and its volume under a flood would crowd out other services’ events. With a log file set, the startup’s errors and warnings go into it too, as StartupError and StartupWarning, as well as to stderr: a service that fails to start would otherwise leave only an exit code. The service’s command line names the log file, so an error in the configuration file reaches it too.
  • Installed by a PowerShell script, contrib/windows/udpstp-server.ps1, not by a udpstp subcommand: the data folder’s ACL and the firewall rule need Win32 calls that are unsafe in Rust, or PowerShell anyway. It mirrors contrib/systemd/. Installation and reinstallation first disable startup, stop the service and clear recovery before changing data or configuration. A queued recovery restart cannot be canceled by changing its actions, but disabled startup prevents it. The settings-only preview validates configuration and extracts ports without opening keys or logs as the administrator. The real service trial starts on demand with no recovery and validates keys as the virtual account (ADR 0015). Only successful startup restores restart actions and automatic startup. Any later failure attempts disabled startup, cleared recovery and a verified stopped service, reports containment failure accurately when those operations fail, and leaves applied changes in place for remediation and rerun; it provides no rollback.

Considered options

  • A wrapper (WinSW, NSSM): no Rust code, its own log capture. Rejected for the reasons above.
  • The windows crate directly: the dispatcher with udpstp’s own unsafe, and an ADR for it, for nothing windows-service doesn’t give safely.
  • A size-rotated single file written by udpstp: a stable name to follow, but the rename fails while a reader holds the file without delete sharing, and 10 files of 10 MiB hold about 12 hours under a flood of limited log events.
  • A restricted SID type (a write-restricted token) as more hardening: possible later, after testing what the Server writes besides its log.

Consequences

  • windows-service and tracing-appender are direct dependencies (technology.md).
  • ADR 0013’s stdout and stderr rule gains the case of a log file: the log and the startup’s errors and warnings go there, and stderr still gets the latter. That stdout and stderr rule is stable from 0.1.0. --windows-service, log_file, the events StartupError and StartupWarning with the warnings’ names, and the stop reasons ServiceStop and ServiceShutdown are command-line, setting and log names, stable from 1.0 (ADR 0013, amended).
  • The firewall rule names the program and its UDP ports. An isolated service-scoped rule passed native Windows 11 IPv4/IPv6 traffic checks, and the Client from the same executable did not prompt. A console Server was denied incoming access without a prompt, and path-based dialog Block rules still overrode the scoped Allow rule (Windows firewall results). The installer retains the program/port rule; the scoped variant has been assessed on one Windows build, not adopted as a new default.
  • The end-to-end check, scripts/windows-service-check.ps1, installs a service and changes the machine, so it runs in Windows Sandbox (scripts/windows-service-sandbox.ps1) or in CI’s Windows job.