feat: add remote, with rss, cache, discord, and zulip
This commit is contained in:
parent
50ec63a474
commit
83baab68e0
32 changed files with 6615 additions and 40 deletions
107
pi/README.md
107
pi/README.md
|
|
@ -6,11 +6,63 @@ Runs on NixOS with Tailscale for networking and agenix for secrets.
|
|||
|
||||
## Setup
|
||||
|
||||
### 1. Hardware config
|
||||
### 1. Bootstrap
|
||||
|
||||
Replace `hardware-configuration.nix` with the output of `nixos-generate-config --show-hardware-config` on your Pi (or use an appropriate hardware module like `sd-card/sd-image-aarch64.nix`).
|
||||
Build the SD image, flash it, and boot the Pi:
|
||||
|
||||
### 2. SSH key
|
||||
```sh
|
||||
nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage
|
||||
dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress
|
||||
```
|
||||
|
||||
Insert the SD card into the Pi and power it on. It will connect to the Noisebridge WiFi network automatically.
|
||||
|
||||
### 2. Find the Pi
|
||||
|
||||
Once booted, find the Pi on the network:
|
||||
|
||||
```sh
|
||||
# Scan the local subnet
|
||||
nmap -sn 192.168.1.0/24
|
||||
|
||||
# Or check ARP table
|
||||
arp -a
|
||||
|
||||
# Or check your router's DHCP leases
|
||||
```
|
||||
|
||||
### 3. Get SSH host key
|
||||
|
||||
Grab the Pi's ed25519 host key and put it in `secrets/secrets.nix`:
|
||||
|
||||
```sh
|
||||
ssh-keyscan <pi-ip> | grep ed25519
|
||||
```
|
||||
|
||||
```nix
|
||||
# secrets/secrets.nix
|
||||
let
|
||||
pi = "ssh-ed25519 AAAA..."; # paste the key here
|
||||
in
|
||||
{
|
||||
"api-key.age".publicKeys = [ pi ];
|
||||
"inbound-api-key.age".publicKeys = [ pi ];
|
||||
"tailscale-auth-key.age".publicKeys = [ pi ];
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Secrets
|
||||
|
||||
Create the encrypted secret files:
|
||||
|
||||
```sh
|
||||
cd secrets
|
||||
agenix -e api-key.age # paste API key for the cache endpoint
|
||||
agenix -e inbound-api-key.age # paste API key that the cache uses to poll the Pi
|
||||
agenix -e tailscale-auth-key.age # paste Tailscale auth key
|
||||
```
|
||||
|
||||
### 5. Add SSH key
|
||||
|
||||
Add your SSH public key to `configuration.nix`:
|
||||
|
||||
|
|
@ -20,26 +72,9 @@ users.users.root.openssh.authorizedKeys.keys = [
|
|||
];
|
||||
```
|
||||
|
||||
### 3. Secrets
|
||||
|
||||
Get your Pi's SSH host public key and put it in `secrets/secrets.nix`:
|
||||
### 6. Deploy
|
||||
|
||||
```sh
|
||||
ssh-keyscan <pi-ip> | grep ed25519
|
||||
```
|
||||
|
||||
Then create the encrypted secret files:
|
||||
|
||||
```sh
|
||||
cd secrets
|
||||
agenix -e endpoint-url.age # paste webhook URL
|
||||
agenix -e tailscale-auth-key.age # paste Tailscale auth key
|
||||
```
|
||||
|
||||
### 4. Deploy
|
||||
|
||||
```sh
|
||||
nix build .#nixosConfigurations.pi.config.system.build.toplevel
|
||||
nixos-rebuild switch --flake .#pi --target-host root@noisebell
|
||||
```
|
||||
|
||||
|
|
@ -49,6 +84,9 @@ Options under `services.noisebell` in `flake.nix`:
|
|||
|
||||
| Option | Default | Description |
|
||||
|---|---|---|
|
||||
| `endpointUrl` | — | Webhook endpoint URL to POST state changes to |
|
||||
| `apiKeyFile` | — | Path to file containing outbound API key (agenix secret) |
|
||||
| `inboundApiKeyFile` | — | Path to file containing inbound API key for GET endpoint auth (agenix secret) |
|
||||
| `gpioPin` | 17 | GPIO pin to monitor |
|
||||
| `debounceSecs` | 5 | Debounce delay |
|
||||
| `port` | 8080 | HTTP status port |
|
||||
|
|
@ -58,6 +96,7 @@ Options under `services.noisebell` in `flake.nix`:
|
|||
| `bindAddress` | `0.0.0.0` | Address to bind the HTTP server to |
|
||||
| `activeLow` | `true` | Whether low GPIO level means open (depends on wiring) |
|
||||
| `restartDelaySecs` | 5 | Seconds before systemd restarts on failure |
|
||||
| `watchdogSecs` | 30 | Watchdog timeout — service is restarted if unresponsive |
|
||||
|
||||
## API
|
||||
|
||||
|
|
@ -67,4 +106,28 @@ Options under `services.noisebell` in `flake.nix`:
|
|||
{"status": "open", "timestamp": 1710000000}
|
||||
```
|
||||
|
||||
State changes (and initial state on startup) are POSTed to the configured endpoint in the same format.
|
||||
`GET /info` — system health and GPIO config:
|
||||
|
||||
```json
|
||||
{
|
||||
"uptime_secs": 3600,
|
||||
"started_at": 1710000000,
|
||||
"cpu_temp_celsius": 42.3,
|
||||
"memory_available_kb": 350000,
|
||||
"memory_total_kb": 512000,
|
||||
"disk_total_bytes": 16000000000,
|
||||
"disk_available_bytes": 12000000000,
|
||||
"load_average": [0.01, 0.05, 0.10],
|
||||
"nixos_version": "24.11.20240308.9dcb002",
|
||||
"commit": "c6e726c",
|
||||
"gpio": {
|
||||
"pin": 17,
|
||||
"active_low": true,
|
||||
"pull": "up",
|
||||
"open_level": "low",
|
||||
"current_raw_level": "low"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
State changes (and initial state on startup) are POSTed to the configured endpoint in the same format as `GET /`, with an `Authorization: Bearer <api-key>` header.
|
||||
|
|
|
|||
23
pi/bootstrap.nix
Normal file
23
pi/bootstrap.nix
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{ modulesPath, ... }:
|
||||
|
||||
{
|
||||
imports = [ "${modulesPath}/installer/sd-card/sd-image-aarch64.nix" ];
|
||||
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
|
||||
networking.hostName = "noisebell";
|
||||
|
||||
networking.wireless = {
|
||||
enable = true;
|
||||
networks = {
|
||||
"Noisebridge" = {
|
||||
psk = "noisebridge";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.openssh.enable = true;
|
||||
users.users.root.openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE40ISu3ydCqfdpb26JYD5cIN0Fu0id/FDS+xjB5zpqu"
|
||||
];
|
||||
}
|
||||
|
|
@ -6,13 +6,17 @@
|
|||
networking.hostName = "noisebell";
|
||||
|
||||
# Decrypted at runtime by agenix
|
||||
age.secrets.endpoint-url.file = ./secrets/endpoint-url.age;
|
||||
age.secrets.tailscale-auth-key.file = ./secrets/tailscale-auth-key.age;
|
||||
|
||||
age.secrets.api-key.file = ./secrets/api-key.age;
|
||||
age.secrets.inbound-api-key.file = ./secrets/inbound-api-key.age;
|
||||
|
||||
services.noisebell = {
|
||||
enable = true;
|
||||
port = 80;
|
||||
endpointUrlFile = config.age.secrets.endpoint-url.path;
|
||||
endpointUrl = "https://noisebell.extremist.software/webhook";
|
||||
apiKeyFile = config.age.secrets.api-key.path;
|
||||
inboundApiKeyFile = config.age.secrets.inbound-api-key.path;
|
||||
};
|
||||
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
|
|
|
|||
33
pi/flake.nix
33
pi/flake.nix
|
|
@ -38,9 +38,19 @@
|
|||
description = "HTTP port for the status endpoint.";
|
||||
};
|
||||
|
||||
endpointUrlFile = lib.mkOption {
|
||||
endpointUrl = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Webhook endpoint URL to POST state changes to.";
|
||||
};
|
||||
|
||||
apiKeyFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "Path to a file containing the endpoint URL (e.g. an agenix secret).";
|
||||
description = "Path to a file containing the outbound API key for the cache endpoint.";
|
||||
};
|
||||
|
||||
inboundApiKeyFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "Path to a file containing the inbound API key for authenticating GET requests.";
|
||||
};
|
||||
|
||||
retryAttempts = lib.mkOption {
|
||||
|
|
@ -78,6 +88,12 @@
|
|||
default = 5;
|
||||
description = "Seconds to wait before systemd restarts the service on failure.";
|
||||
};
|
||||
|
||||
watchdogSecs = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 30;
|
||||
description = "Watchdog timeout in seconds. The service is restarted if it fails to notify systemd within this interval.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
|
@ -109,17 +125,23 @@
|
|||
NOISEBELL_RETRY_ATTEMPTS = toString cfg.retryAttempts;
|
||||
NOISEBELL_RETRY_BASE_DELAY_SECS = toString cfg.retryBaseDelaySecs;
|
||||
NOISEBELL_HTTP_TIMEOUT_SECS = toString cfg.httpTimeoutSecs;
|
||||
NOISEBELL_ENDPOINT_URL = cfg.endpointUrl;
|
||||
NOISEBELL_BIND_ADDRESS = cfg.bindAddress;
|
||||
NOISEBELL_ACTIVE_LOW = if cfg.activeLow then "true" else "false";
|
||||
NOISEBELL_COMMIT = self.shortRev or "dirty";
|
||||
RUST_LOG = "info";
|
||||
};
|
||||
|
||||
script = ''
|
||||
export NOISEBELL_ENDPOINT_URL="$(cat ${cfg.endpointUrlFile})"
|
||||
export NOISEBELL_API_KEY="$(cat ${cfg.apiKeyFile})"
|
||||
export NOISEBELL_INBOUND_API_KEY="$(cat ${cfg.inboundApiKeyFile})"
|
||||
exec ${bin}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "notify";
|
||||
NotifyAccess = "all";
|
||||
WatchdogSec = cfg.watchdogSecs;
|
||||
Restart = "on-failure";
|
||||
RestartSec = cfg.restartDelaySecs;
|
||||
User = "noisebell";
|
||||
|
|
@ -153,5 +175,10 @@
|
|||
./hardware-configuration.nix
|
||||
];
|
||||
};
|
||||
|
||||
nixosConfigurations.bootstrap = nixpkgs.lib.nixosSystem {
|
||||
system = "aarch64-linux";
|
||||
modules = [ ./bootstrap.nix ];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ edition = "2021"
|
|||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
axum = "0.8"
|
||||
libc = "0.2"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
rppal = "0.22"
|
||||
sd-notify = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "signal"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "signal", "time"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ use std::sync::Arc;
|
|||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{extract::State, routing::get, Json, Router};
|
||||
use axum::extract::State;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::routing::get;
|
||||
use axum::{Json, Router};
|
||||
use rppal::gpio::{Gpio, Level, Trigger};
|
||||
use serde::Serialize;
|
||||
use tracing::{error, info, warn};
|
||||
|
|
@ -11,6 +14,19 @@ use tracing::{error, info, warn};
|
|||
struct AppState {
|
||||
is_open: AtomicBool,
|
||||
last_changed: AtomicU64,
|
||||
started_at: u64,
|
||||
gpio_pin: u8,
|
||||
active_low: bool,
|
||||
commit: String,
|
||||
inbound_api_key: String,
|
||||
}
|
||||
|
||||
fn validate_bearer(headers: &HeaderMap, expected: &str) -> bool {
|
||||
headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|v| v.strip_prefix("Bearer ").unwrap_or("") == expected)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -19,6 +35,30 @@ struct StatusResponse {
|
|||
timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GpioInfo {
|
||||
pin: u8,
|
||||
active_low: bool,
|
||||
pull: &'static str,
|
||||
open_level: &'static str,
|
||||
current_raw_level: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct InfoResponse {
|
||||
uptime_secs: u64,
|
||||
started_at: u64,
|
||||
cpu_temp_celsius: Option<f64>,
|
||||
memory_available_kb: Option<u64>,
|
||||
memory_total_kb: Option<u64>,
|
||||
disk_total_bytes: Option<u64>,
|
||||
disk_available_bytes: Option<u64>,
|
||||
load_average: Option<[f64; 3]>,
|
||||
nixos_version: Option<String>,
|
||||
commit: String,
|
||||
gpio: GpioInfo,
|
||||
}
|
||||
|
||||
fn unix_timestamp() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
|
@ -34,11 +74,102 @@ fn status_str(is_open: bool) -> &'static str {
|
|||
}
|
||||
}
|
||||
|
||||
async fn get_status(State(state): State<Arc<AppState>>) -> Json<StatusResponse> {
|
||||
Json(StatusResponse {
|
||||
fn read_cpu_temp() -> Option<f64> {
|
||||
std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<f64>().ok())
|
||||
.map(|m| m / 1000.0)
|
||||
}
|
||||
|
||||
fn read_meminfo_field(contents: &str, field: &str) -> Option<u64> {
|
||||
contents
|
||||
.lines()
|
||||
.find(|l| l.starts_with(field))
|
||||
.and_then(|l| l.split_whitespace().nth(1))
|
||||
.and_then(|v| v.parse().ok())
|
||||
}
|
||||
|
||||
fn read_disk_usage() -> Option<(u64, u64)> {
|
||||
let path = std::ffi::CString::new("/").ok()?;
|
||||
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
|
||||
let ret = unsafe { libc::statvfs(path.as_ptr(), &mut stat) };
|
||||
if ret != 0 {
|
||||
return None;
|
||||
}
|
||||
let block_size = stat.f_frsize as u64;
|
||||
Some((
|
||||
stat.f_blocks * block_size,
|
||||
stat.f_bavail * block_size,
|
||||
))
|
||||
}
|
||||
|
||||
fn read_load_average() -> Option<[f64; 3]> {
|
||||
let contents = std::fs::read_to_string("/proc/loadavg").ok()?;
|
||||
let mut parts = contents.split_whitespace();
|
||||
Some([
|
||||
parts.next()?.parse().ok()?,
|
||||
parts.next()?.parse().ok()?,
|
||||
parts.next()?.parse().ok()?,
|
||||
])
|
||||
}
|
||||
|
||||
fn read_nixos_version() -> Option<String> {
|
||||
std::fs::read_to_string("/run/current-system/nixos-version")
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
|
||||
async fn get_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<StatusResponse>, StatusCode> {
|
||||
if !validate_bearer(&headers, &state.inbound_api_key) {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
Ok(Json(StatusResponse {
|
||||
status: status_str(state.is_open.load(Ordering::Relaxed)),
|
||||
timestamp: state.last_changed.load(Ordering::Relaxed),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_info(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<InfoResponse>, StatusCode> {
|
||||
if !validate_bearer(&headers, &state.inbound_api_key) {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
let meminfo = std::fs::read_to_string("/proc/meminfo").ok();
|
||||
let disk = read_disk_usage();
|
||||
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||
let raw_level = match (state.active_low, is_open) {
|
||||
(true, true) | (false, false) => "low",
|
||||
_ => "high",
|
||||
};
|
||||
Ok(Json(InfoResponse {
|
||||
uptime_secs: unix_timestamp() - state.started_at,
|
||||
started_at: state.started_at,
|
||||
cpu_temp_celsius: read_cpu_temp(),
|
||||
memory_available_kb: meminfo
|
||||
.as_deref()
|
||||
.and_then(|m| read_meminfo_field(m, "MemAvailable:")),
|
||||
memory_total_kb: meminfo
|
||||
.as_deref()
|
||||
.and_then(|m| read_meminfo_field(m, "MemTotal:")),
|
||||
disk_total_bytes: disk.map(|(total, _)| total),
|
||||
disk_available_bytes: disk.map(|(_, avail)| avail),
|
||||
load_average: read_load_average(),
|
||||
nixos_version: read_nixos_version(),
|
||||
commit: state.commit.clone(),
|
||||
gpio: GpioInfo {
|
||||
pin: state.gpio_pin,
|
||||
active_low: state.active_low,
|
||||
pull: if state.active_low { "up" } else { "down" },
|
||||
open_level: if state.active_low { "low" } else { "high" },
|
||||
current_raw_level: raw_level,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -65,6 +196,9 @@ async fn main() -> Result<()> {
|
|||
let endpoint_url =
|
||||
std::env::var("NOISEBELL_ENDPOINT_URL").context("NOISEBELL_ENDPOINT_URL is required")?;
|
||||
|
||||
let api_key =
|
||||
std::env::var("NOISEBELL_API_KEY").context("NOISEBELL_API_KEY is required")?;
|
||||
|
||||
let retry_attempts: u32 = std::env::var("NOISEBELL_RETRY_ATTEMPTS")
|
||||
.unwrap_or_else(|_| "3".into())
|
||||
.parse()
|
||||
|
|
@ -80,13 +214,17 @@ async fn main() -> Result<()> {
|
|||
.parse()
|
||||
.context("NOISEBELL_HTTP_TIMEOUT_SECS must be a valid u64")?;
|
||||
|
||||
let bind_address = std::env::var("NOISEBELL_BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0".into());
|
||||
let bind_address =
|
||||
std::env::var("NOISEBELL_BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0".into());
|
||||
|
||||
let active_low: bool = std::env::var("NOISEBELL_ACTIVE_LOW")
|
||||
.unwrap_or_else(|_| "true".into())
|
||||
.parse()
|
||||
.context("NOISEBELL_ACTIVE_LOW must be true or false")?;
|
||||
|
||||
let inbound_api_key = std::env::var("NOISEBELL_INBOUND_API_KEY")
|
||||
.context("NOISEBELL_INBOUND_API_KEY is required")?;
|
||||
|
||||
info!(gpio_pin, debounce_secs, port, %endpoint_url, "starting noisebell");
|
||||
|
||||
let gpio = Gpio::new().context("failed to initialize GPIO")?;
|
||||
|
|
@ -101,9 +239,18 @@ async fn main() -> Result<()> {
|
|||
|
||||
let open_level = if active_low { Level::Low } else { Level::High };
|
||||
let initial_open = pin.read() == open_level;
|
||||
let now = unix_timestamp();
|
||||
let commit =
|
||||
std::env::var("NOISEBELL_COMMIT").unwrap_or_else(|_| "unknown".into());
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
is_open: AtomicBool::new(initial_open),
|
||||
last_changed: AtomicU64::new(unix_timestamp()),
|
||||
last_changed: AtomicU64::new(now),
|
||||
started_at: now,
|
||||
gpio_pin,
|
||||
active_low,
|
||||
commit,
|
||||
inbound_api_key,
|
||||
});
|
||||
|
||||
info!(initial_status = status_str(initial_open), "GPIO initialized");
|
||||
|
|
@ -111,7 +258,7 @@ async fn main() -> Result<()> {
|
|||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<(bool, u64)>();
|
||||
|
||||
// Sync initial state with the cache on startup
|
||||
let _ = tx.send((initial_open, unix_timestamp()));
|
||||
let _ = tx.send((initial_open, now));
|
||||
|
||||
let state_for_interrupt = state.clone();
|
||||
// pin must live for the entire program — rppal runs interrupts on a background
|
||||
|
|
@ -130,7 +277,9 @@ async fn main() -> Result<()> {
|
|||
let was_open = state_for_interrupt.is_open.swap(new_open, Ordering::Relaxed);
|
||||
if was_open != new_open {
|
||||
let timestamp = unix_timestamp();
|
||||
state_for_interrupt.last_changed.store(timestamp, Ordering::Relaxed);
|
||||
state_for_interrupt
|
||||
.last_changed
|
||||
.store(timestamp, Ordering::Relaxed);
|
||||
let _ = tx.send((new_open, timestamp));
|
||||
}
|
||||
},
|
||||
|
|
@ -150,7 +299,12 @@ async fn main() -> Result<()> {
|
|||
let payload = serde_json::json!({ "status": status, "timestamp": timestamp });
|
||||
|
||||
for attempt in 0..=retry_attempts {
|
||||
let result = client.post(&endpoint_url).json(&payload).send().await;
|
||||
let result = client
|
||||
.post(&endpoint_url)
|
||||
.bearer_auth(&api_key)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await;
|
||||
match result {
|
||||
Ok(resp) if resp.status().is_success() => break,
|
||||
_ => {
|
||||
|
|
@ -175,6 +329,7 @@ async fn main() -> Result<()> {
|
|||
|
||||
let app = Router::new()
|
||||
.route("/", get(get_status))
|
||||
.route("/info", get(get_info))
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind((&*bind_address, port))
|
||||
|
|
@ -183,9 +338,27 @@ async fn main() -> Result<()> {
|
|||
|
||||
info!(port, "listening");
|
||||
|
||||
let shutdown = tokio::signal::ctrl_c();
|
||||
// Start watchdog task if systemd watchdog is enabled
|
||||
if let Ok(usec_str) = std::env::var("WATCHDOG_USEC") {
|
||||
if let Ok(usec) = usec_str.parse::<u64>() {
|
||||
let period = Duration::from_micros(usec / 2);
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
sd_notify::notify(false, &[sd_notify::NotifyState::Watchdog]).ok();
|
||||
tokio::time::sleep(period).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sd_notify::notify(false, &[sd_notify::NotifyState::Ready]).ok();
|
||||
|
||||
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.context("failed to register SIGTERM handler")?;
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(async { shutdown.await.ok(); })
|
||||
.with_graceful_shutdown(async move {
|
||||
sigterm.recv().await;
|
||||
})
|
||||
.await
|
||||
.context("server error")?;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ let
|
|||
pi = "ssh-ed25519 AAAA..."; # Pi's SSH host public key
|
||||
in
|
||||
{
|
||||
"endpoint-url.age".publicKeys = [ pi ];
|
||||
"api-key.age".publicKeys = [ pi ];
|
||||
"inbound-api-key.age".publicKeys = [ pi ];
|
||||
"tailscale-auth-key.age".publicKeys = [ pi ];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue