Add Trusted-LAN LocalSend receiver on NOMAD
Some checks failed
secret-guardrails / artifact-secret-scan (push) Has been cancelled
secret-guardrails / gitleaks (push) Has been cancelled

This commit is contained in:
Fizzlepoof
2026-05-26 02:44:18 +00:00
parent 0d7e133283
commit 0c33bedbae
8 changed files with 275 additions and 1 deletions

View File

@@ -2,7 +2,7 @@
All homelab services, their hosts, ports, and current status.
*Last updated: 2026-05-21*
*Last updated: 2026-05-26*
## Shared Databases (PD)
@@ -92,6 +92,12 @@ OpenClaw is no longer treated as an active PD service in the operator inventory.
|---------|------|------|--------|
| MeshCore to MQTT relay | N.O.M.A.D. (10.5.30.7) | outbound MQTT only | ✅ Running as Docker container `mctomqtt` (`mctomqtt:latest`); config bind-mounted from `/etc/mctomqtt` |
## Local file intake
| Service | Host | Port | Status |
|---------|------|------|--------|
| LocalSend Trusted Inbox | N.O.M.A.D. (`10.5.1.16` on Trusted VLAN 51) | 53317/tcp+udp | ✅ Repo-tracked Docker stack at `/opt/localsend-nomad`; writes directly into `/home/fizzlepoof/private/inbox-secrets`; same-host checks from N.O.M.A.D. are unreliable because of macvlan isolation |
## Mapping / Geo (PD)
| Service | Port | Status |

View File

@@ -213,6 +213,15 @@ These are expected to be managed by Wings/Pelican rather than a local compose fi
- The live `.env` is also backed up into the encrypted PD secrets repo as `env/technitium-nomad.env`
- Same-host checks from NOMAD to `10.5.30.9` are unreliable because the resolver sits behind macvlan networking; verify from another LAN peer instead
### LocalSend Trusted inbox
- Live stack path: `/opt/localsend-nomad`
- Repo source path: `/home/fizzlepoof/repos/truenas-stacks/localsend-nomad`
- Trusted bind IP: `10.5.1.16`
- Role: headless LocalSend receiver for Trusted-LAN handoff into `/home/fizzlepoof/private/inbox-secrets`
- Runtime model: standalone Docker Compose stack on a Trusted-LAN macvlan (`enp5s0.51`, VLAN 51) while the host stays on Servers (`10.5.30.7`)
- LocalSend listener ports: `53317/tcp`, `53317/udp`
- Same-host checks from NOMAD to `10.5.1.16` are unreliable because the receiver sits behind macvlan networking; verify from another Trusted-LAN peer instead
### MeshCore to MQTT relay
- Install date: 2026-05-18
- Install root: `/opt/mctomqtt`

View File

@@ -0,0 +1,9 @@
TZ=America/Chicago
TRUSTED_PARENT_INTERFACE=enp5s0.51
TRUSTED_SUBNET=10.5.1.0/24
TRUSTED_GATEWAY=10.5.1.1
LOCALSEND_BIND_IP=10.5.1.16
LOCALSEND_DEVICE_NAME="Doris Trusted Inbox"
LOCALSEND_CLI_REF=54702513990eeb763e1615f274d5365442e3e393
APPDATA_LOCALSEND_ROOT=/opt/localsend-nomad/data
INBOX_DIR=/home/fizzlepoof/private/inbox-secrets

View File

@@ -0,0 +1,19 @@
FROM golang:1.25-alpine AS build
ARG LOCALSEND_CLI_REF=54702513990eeb763e1615f274d5365442e3e393
RUN apk add --no-cache git patch
WORKDIR /src
RUN git clone https://github.com/0w0mewo/localsend-cli.git . \
&& git checkout "$LOCALSEND_CLI_REF"
COPY patches/localsend-cli-doris.patch /tmp/localsend-cli-doris.patch
RUN git apply /tmp/localsend-cli-doris.patch \
&& CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/localsend .
FROM alpine:3.22
RUN apk add --no-cache ca-certificates tzdata \
&& adduser -D -u 1000 doris
WORKDIR /app
COPY --from=build /out/localsend /usr/local/bin/localsend
USER doris
ENTRYPOINT ["/usr/local/bin/localsend"]

71
localsend-nomad/README.md Normal file
View File

@@ -0,0 +1,71 @@
# LocalSend trusted-lane receiver on N.O.M.A.D.
Repo-tracked Docker stack that runs a headless LocalSend receiver on N.O.M.A.D. with its own Trusted-LAN IP.
- Repo stack path: `/home/fizzlepoof/repos/truenas-stacks/localsend-nomad`
- Live stack path: `/opt/localsend-nomad`
- Trusted bind IP: `10.5.1.16`
- Trusted VLAN: `51`
- LocalSend ports: `53317/tcp`, `53317/udp`
- Inbox path on host: `/home/fizzlepoof/private/inbox-secrets`
- Advertised device name: `Doris Trusted Inbox`
## Why this exists
N.O.M.A.D. lives on the `Servers` VLAN (`10.5.30.7`), but some operator handoff flows need a LocalSend receiver directly reachable from the `Trusted` LAN (`10.5.1.0/24`). This stack gives LocalSend its own Trusted-LAN address without moving the host itself off the Servers network.
## Network model
- Docker uses a `macvlan` network with parent `enp5s0.51`.
- `enp5s0.51` is the Trusted VLAN subinterface on N.O.M.A.D.'s primary NIC.
- Docker will create the VLAN subinterface automatically when the macvlan network is created.
- Same-host testing from N.O.M.A.D. to `10.5.1.16` is unreliable because macvlan isolates the host from the container's address on the same parent interface.
- Verify reachability from another Trusted-LAN peer instead, such as PD on `10.5.1.6`.
## LocalSend build provenance
The image is built locally from upstream `0w0mewo/localsend-cli` at:
- commit: `54702513990eeb763e1615f274d5365442e3e393`
Then it applies `patches/localsend-cli-doris.patch`, which keeps the receiver compatible with the LocalSend endpoint behavior already working in the existing Doris receiver flow.
## Files
- `docker-compose.yaml` — live stack definition
- `Dockerfile` — reproducible CLI build from upstream + patch
- `.env.example` — deployment variables
- `bin/prepare_nomad.sh` — creates runtime dirs, validates config, optional `--up`
- `patches/localsend-cli-doris.patch` — repo-tracked patch applied during image build
## Deploy / refresh on NOMAD
```bash
cd /opt/localsend-nomad
cp .env.example .env
./bin/prepare_nomad.sh --up
```
## Verification
From N.O.M.A.D.:
```bash
docker compose --env-file .env ps
sudo docker logs --tail 50 localsend-trusted
```
From another Trusted-LAN host:
```bash
ping 10.5.1.16
nc -vz 10.5.1.16 53317
```
Then confirm `Doris Trusted Inbox` appears as a LocalSend target from a Trusted-LAN device.
## Notes
- The container writes directly into `/home/fizzlepoof/private/inbox-secrets` on the host so the existing autosort / intake flow can keep using the same inbox path.
- This stack does not publish host ports; the dedicated Trusted-LAN IP is the service endpoint.
- If the upstream CLI behavior changes later, rebuild from a reviewed newer commit and update the patch or drop it if upstream no longer needs the workaround.

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ENV_FILE="${ENV_FILE:-$STACK_DIR/.env}"
DO_UP="${1:-}"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Missing $ENV_FILE — copy .env.example to .env and fill in real values first." >&2
exit 1
fi
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
if [[ -z "${APPDATA_LOCALSEND_ROOT:-}" || -z "${INBOX_DIR:-}" ]]; then
echo "APPDATA_LOCALSEND_ROOT and INBOX_DIR must be set in .env" >&2
exit 1
fi
mkdir -p \
"$APPDATA_LOCALSEND_ROOT/state" \
"$INBOX_DIR"
docker compose --env-file "$ENV_FILE" -f "$STACK_DIR/docker-compose.yaml" config >/dev/null
if [[ "$DO_UP" == "--up" ]]; then
docker compose --env-file "$ENV_FILE" -f "$STACK_DIR/docker-compose.yaml" up -d --build
docker compose --env-file "$ENV_FILE" -f "$STACK_DIR/docker-compose.yaml" ps
fi

View File

@@ -0,0 +1,35 @@
services:
localsend:
build:
context: .
args:
LOCALSEND_CLI_REF: ${LOCALSEND_CLI_REF}
image: localsend-nomad:local
container_name: localsend-trusted
hostname: localsend-trusted
restart: unless-stopped
environment:
TZ: ${TZ}
networks:
trusted_macvlan:
ipv4_address: ${LOCALSEND_BIND_IP}
working_dir: /app/state
volumes:
- ${APPDATA_LOCALSEND_ROOT}/state:/app/state
- ${INBOX_DIR}:/data/inbox
command:
- recv
- --devname
- ${LOCALSEND_DEVICE_NAME}
- --dir
- /data/inbox
networks:
trusted_macvlan:
driver: macvlan
driver_opts:
parent: ${TRUSTED_PARENT_INTERFACE}
ipam:
config:
- subnet: ${TRUSTED_SUBNET}
gateway: ${TRUSTED_GATEWAY}

View File

@@ -0,0 +1,92 @@
diff --git a/internal/localsend/localsend.go b/internal/localsend/localsend.go
index e30e6f5..c0c2b8e 100644
--- a/internal/localsend/localsend.go
+++ b/internal/localsend/localsend.go
@@ -2,6 +2,7 @@ package localsend
import (
"encoding/json"
+ "fmt"
"net"
"github.com/0w0mewo/localsend-cli/internal/localsend/constants"
@@ -13,41 +14,53 @@ import (
func GetDeviceInfo(ip string, https bool) (models.DeviceInfo, error) {
remoteAddr := net.JoinHostPort(ip, "53317")
- agent := fiber.AcquireAgent()
- defer fiber.ReleaseAgent(agent)
-
scheme := "http"
if https {
scheme = "https"
}
- req := agent.Request()
- req.URI().SetScheme(scheme)
- req.URI().SetHost(remoteAddr)
- req.URI().SetPath(constants.InfoPath)
- req.Header.SetMethod(fiber.MethodGet)
- err := agent.Parse()
- if err != nil {
- return models.DeviceInfo{}, err
- }
+ paths := []string{constants.InfoPath, "/api/localsend/v2/info", constants.InfoPathLegacy}
+ var lastErr error
- status, b, errs := agent.InsecureSkipVerify().Bytes()
- if len(errs) != 0 {
- return models.DeviceInfo{}, errs[0]
- }
- err = constants.ParseError(status)
- if err != nil {
- return models.DeviceInfo{}, err
- }
+ for _, path := range paths {
+ agent := fiber.AcquireAgent()
+ req := agent.Request()
+ req.URI().SetScheme(scheme)
+ req.URI().SetHost(remoteAddr)
+ req.URI().SetPath(path)
+ req.Header.SetMethod(fiber.MethodGet)
+ err := agent.Parse()
+ if err != nil {
+ fiber.ReleaseAgent(agent)
+ return models.DeviceInfo{}, err
+ }
- var res models.DeviceInfo
- err = json.Unmarshal(b, &res)
- if err != nil {
- return models.DeviceInfo{}, err
+ status, b, errs := agent.InsecureSkipVerify().Bytes()
+ fiber.ReleaseAgent(agent)
+ if len(errs) != 0 {
+ lastErr = errs[0]
+ continue
+ }
+ err = constants.ParseError(status)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+
+ var res models.DeviceInfo
+ err = json.Unmarshal(b, &res)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ res.IP = ip
+ return res, nil
}
- res.IP = ip
- return res, nil
+ if lastErr == nil {
+ lastErr = fmt.Errorf("device info not found on known endpoints")
+ }
+ return models.DeviceInfo{}, lastErr
}
func NewFileSender(useDownloadAPI ...bool) send.FileSender {