Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions docs/dev-time-tx-kill-switch.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Dev-time TX kill-switch

DAPPS is pre-1.0 software. Until it is judged stable enough for unsupervised use on the air, every running node polls a single URL controlled by the project author and stops transmitting if that URL says so. This page exists so you know exactly what that means before you put a node on the air.
DAPPS is pre-1.0 software. While it is, every running node checks a URL controlled by the project author every five minutes; the URL can ask nodes to pause transmissions. This page exists so you know what that means before you put a node on the air.

## What is it

Every DAPPS daemon, regardless of operator, polls
Every running DAPPS daemon polls

```
https://compute.oarc.uk/storage/public/folders/4803/dapps-devtime-killswitch.json
```

once a minute. The response is small JSON:
every five minutes. The response is small JSON:

```json
{
Expand All @@ -26,31 +26,31 @@ The dashboard shows a red banner across every page when the gate is closed, with

## Why it is here

DAPPS is alpha-quality software running on shared amateur radio bandwidth. A bug shipped in a release could, in principle, cause a fleet of nodes to flood the air. Most operators won't catch a regression in a release within minutes; they may not even be at the keyboard. The kill-switch lets the project author gag every running node within roughly one minute of detecting a problem, without coordinating with operators individually.
DAPPS is early-stage software running on shared amateur radio bandwidth. A bug in a release could, in principle, cause a fleet of nodes to transmit more than they should. An operator may not be at the keyboard to catch a regression, especially overnight or during a working day. The kill-switch lets the author signal every running node to pause within a few minutes of spotting a problem, without coordinating with each operator individually.

This is a software-development safety net, not a regulatory mechanism, not a moderation tool. It exists so the worst-case "I shipped a bug that hammers 144.950" stays bounded to a few minutes of harm before every node goes silent.
This is a software-development safety net, not a regulatory mechanism, not a moderation tool. The aim is to keep the worst case ("I shipped a bug that hammers 144.950") bounded.

## What you cannot do
## What's not configurable

- You cannot disable the polling.
- You cannot repoint it to a different URL.
- You cannot relax the cadence, the staleness window, or the fail-open behaviour.
- The polling cannot be disabled.
- The URL cannot be changed at runtime.
- The cadence, staleness window, and fail-open behaviour are fixed.

The values are constants in the source (`TxKillSwitchPoller.cs`); a fork can change them but the published binaries cannot be configured at runtime. This is deliberate. A configurable kill-switch defeats its purpose - the whole point is that the author can rely on every node polling the one URL.
The values are constants in the source (`TxKillSwitchPoller.cs`) and the published binaries do not expose them as settings. This is deliberate: a configurable kill-switch wouldn't reliably reach every node, which is the whole point.

If that posture is unacceptable to you, the answer is to not run pre-1.0 DAPPS, or to fork. Both are valid choices.
If you'd rather not run software with this in place, deferring until 1.0 is a reasonable call.

## What you can do

- See the current state in the dashboard banner and at `GET /TxControl/status`.
- Continue to use the operator master TX-stop button independently. It is a separate signal; closing the local toggle gags TX even when the remote signal is allowing, and reopening the local toggle does *not* override a remote block.
- Continue to use the operator master TX-stop button independently. It is a separate signal; closing the local toggle pauses TX even when the remote signal is allowing, and reopening the local toggle does *not* override a remote block.
- Monitor outbound HTTPS traffic to the kill-switch URL if you want to verify what's being polled. Nothing operator-identifying is sent: the request is a plain `GET` with no body and no auth.
- Read the `Services/TxKillSwitchPoller.cs` source. The whole mechanism is around two hundred lines.

## Failure modes

- **URL unreachable at startup**: the gate stays open. A new install with no internet does not silently refuse to TX.
- **URL unreachable after a successful poll**: the daemon keeps using the most recent successful state for ten minutes (the staleness window). After that it falls back to allow.
- **URL unreachable after a successful poll**: the daemon keeps using the most recent successful state for thirty minutes (the staleness window). After that it falls back to allow.
- **Malformed JSON**: same as unreachable - the failure is logged at debug level and the previous state is kept.

The staleness window is short enough that a genuinely stuck poller won't keep trusting hours-old state, and long enough to ride out the kind of network blip that's common on a domestic connection. Fail-open is the conservative posture for an amateur radio installation: an operator with a working RF stack and a flaky internet connection is not made worse off by losing transmissions on top.
Expand All @@ -63,9 +63,9 @@ If the project pivots and a configurable per-fleet kill-switch becomes useful (a

## What the network sees

A `GET` request to the URL above, once a minute, from every running DAPPS node. No body, no headers beyond a User-Agent generated by the .NET HTTP stack, no cookies, no auth. The response is cached only in process memory.
A `GET` request to the URL above, every five minutes, from every running DAPPS node. No body, no headers beyond a User-Agent generated by the .NET HTTP stack, no cookies, no auth. The response is cached only in process memory.

If you operate a node in an environment where polling that URL is itself a problem (an isolated network, a regulator concerned about outbound traffic), the answer is to not run pre-1.0 DAPPS in that environment. Fail-open will keep TX working when the URL is unreachable, but the request will still be made on the polling cadence.
If polling that URL is itself a problem in your environment (an isolated network, a regulatory concern about outbound traffic), waiting for 1.0 before deploying may be the right call. Fail-open keeps TX working when the URL is unreachable, but the request itself still happens on the polling cadence.

## Source

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The wire protocol is small and human-readable on the line: a peer connects, gets

## Before you put it on the air

DAPPS is pre-1.0. While it is, every running node polls a single URL controlled by the project author and stops transmitting if that URL says so. It's a development-phase safety net - not configurable, removed before 1.0. See [Dev-time TX kill-switch](dev-time-tx-kill-switch.md) for the full rationale and what it means in practice.
DAPPS is pre-1.0. While it is, every running node checks a URL controlled by the project author every five minutes; the URL can ask nodes to pause transmissions. It's a development-phase safety net - not configurable, removed before 1.0. See [Dev-time TX kill-switch](dev-time-tx-kill-switch.md) for the full rationale and what it means in practice.

## The journey

Expand Down
4 changes: 2 additions & 2 deletions src/dapps/dapps.core.tests/TxKillSwitchPollerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ public void HardcodedConstants_PinProductionValues()
// safety-critical defaults.
TxKillSwitchPoller.KillSwitchUrl.Should().Be(
"https://compute.oarc.uk/storage/public/folders/4803/dapps-devtime-killswitch.json");
TxKillSwitchPoller.PollSeconds.Should().Be(60);
TxKillSwitchPoller.StaleSeconds.Should().Be(600);
TxKillSwitchPoller.PollSeconds.Should().Be(300);
TxKillSwitchPoller.StaleSeconds.Should().Be(1800);
TxKillSwitchPoller.FailOpen.Should().BeTrue();
}

Expand Down
15 changes: 15 additions & 0 deletions src/dapps/dapps.core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@

builder.Services.AddHttpClient();

// Dedicated named client for the TX kill-switch poller. Pinned to
// a fresh SocketsHttpHandler so any future global handler tweak
// (e.g. someone adding a "trust all certs" callback to the default
// for testing and forgetting to remove it) cannot weaken the
// validation on the kill-switch fetch. SslOptions is left at
// default = system trust store + chain / hostname / expiry. The
// poller adds a runtime guard that the URL scheme is https://;
// together those two stop a downgrade or a bypassed validation
// from silently rendering the kill-switch ineffective.
builder.Services.AddHttpClient("tx-kill-switch")
.ConfigurePrimaryHttpMessageHandler(() => new System.Net.Http.SocketsHttpHandler
{
SslOptions = new System.Net.Security.SslClientAuthenticationOptions(),
});

// Plan A polish - single TimeProvider injected everywhere
// cadence-sensitive code reads time. Tests substitute
// FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) so
Expand Down
47 changes: 32 additions & 15 deletions src/dapps/dapps.core/Services/TxKillSwitchPoller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ namespace dapps.core.Services;
/// staleness window, and fail-open behaviour are constants. Operators
/// running pre-1.0 DAPPS cannot disable it, repoint it, or relax the
/// timings. The mechanism is a development-phase safety net so the
/// author can gag misbehaving experimental nodes if a release ships
/// with a bug that floods the air. It will be removed (or made
/// genuinely configurable per-fleet) before 1.0. See
/// author can ask experimental nodes to pause if a release ships with
/// a bug that transmits more than it should. It will be removed (or
/// made genuinely configurable per-fleet) before 1.0. See
/// <c>docs/dev-time-tx-kill-switch.md</c> for the operator-facing
/// rationale.</para>
///
Expand All @@ -36,9 +36,9 @@ namespace dapps.core.Services;
///
/// Fail behaviour: HTTP / parse failure keeps the last successful
/// state for <see cref="StaleSeconds"/>, then falls back to allow
/// (fail-open). A network outage doesn't gag a working amateur radio
/// installation - the kill-switch is for active intervention by the
/// project author, not graceful degradation.
/// (fail-open). A network outage doesn't silence a working amateur
/// radio installation - the kill-switch is for active intervention
/// by the project author, not graceful degradation.
///
/// Errors are swallowed by design: a poller crash mustn't take down
/// the daemon. Same posture as <see cref="UpdateChecker"/>.
Expand All @@ -58,19 +58,21 @@ public sealed class TxKillSwitchPoller(
"https://compute.oarc.uk/storage/public/folders/4803/dapps-devtime-killswitch.json";

/// <summary>Seconds between polls. Constant: cannot be tuned per
/// node. 60s gives near-real-time response without hammering the
/// publishing endpoint.</summary>
public const int PollSeconds = 60;
/// node. 300s (5 min) is responsive enough to pause a misbehaving
/// fleet within a single beacon cadence, slow enough that the
/// outbound traffic to the publishing endpoint stays trivial
/// even at hundreds of nodes.</summary>
public const int PollSeconds = 300;

/// <summary>Seconds without a successful fetch before the cached
/// value is considered stale. Constant: 600 (10 min) - long enough
/// to ride out transient network blips, short enough that a
/// genuinely-stuck poller stops trusting old state in a reasonable
/// time.</summary>
public const int StaleSeconds = 600;
/// value is considered stale. Constant: 1800 (30 min) - several
/// poll cycles of failure before declaring the cached value
/// untrustworthy, long enough to ride out a transient outage,
/// short enough that a stuck poller stops trusting hour-old state.</summary>
public const int StaleSeconds = 1800;

/// <summary>When stale or never-yet-fetched, allow TX. Constant:
/// the kill-switch is for active intervention, not for gagging
/// the kill-switch is for active intervention, not for silencing
/// nodes that lose internet.</summary>
public const bool FailOpen = true;

Expand Down Expand Up @@ -160,6 +162,21 @@ private async Task PollOnce(CancellationToken ct)
{
try
{
// Belt-and-braces: refuse to fetch over plaintext even if
// the constant ever drifts. Cert validation itself is
// done by the named HttpClient's primary handler (see DI
// registration in Program.cs), which uses a fresh
// SocketsHttpHandler with default SslOptions - the system
// trust store + chain/hostname/expiry checks. We never
// override RemoteCertificateValidationCallback anywhere
// in the process; this guard catches the URL-scheme
// angle that's not in the handler's job description.
if (!KillSwitchUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"TX kill-switch URL must be https:// - refusing to poll over plaintext");
}

var client = httpClientFactory.CreateClient("tx-kill-switch");
client.Timeout = TimeSpan.FromSeconds(10);

Expand Down
Loading