Add Trusted-LAN LocalSend receiver on NOMAD
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
All homelab services, their hosts, ports, and current status.
|
All homelab services, their hosts, ports, and current status.
|
||||||
|
|
||||||
*Last updated: 2026-05-21*
|
*Last updated: 2026-05-26*
|
||||||
|
|
||||||
## Shared Databases (PD)
|
## 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` |
|
| 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)
|
## Mapping / Geo (PD)
|
||||||
|
|
||||||
| Service | Port | Status |
|
| Service | Port | Status |
|
||||||
|
|||||||
@@ -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`
|
- 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
|
- 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
|
### MeshCore to MQTT relay
|
||||||
- Install date: 2026-05-18
|
- Install date: 2026-05-18
|
||||||
- Install root: `/opt/mctomqtt`
|
- Install root: `/opt/mctomqtt`
|
||||||
|
|||||||
9
localsend-nomad/.env.example
Normal file
9
localsend-nomad/.env.example
Normal 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
|
||||||
19
localsend-nomad/Dockerfile
Normal file
19
localsend-nomad/Dockerfile
Normal 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
71
localsend-nomad/README.md
Normal 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.
|
||||||
33
localsend-nomad/bin/prepare_nomad.sh
Normal file
33
localsend-nomad/bin/prepare_nomad.sh
Normal 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
|
||||||
35
localsend-nomad/docker-compose.yaml
Normal file
35
localsend-nomad/docker-compose.yaml
Normal 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}
|
||||||
92
localsend-nomad/patches/localsend-cli-doris.patch
Normal file
92
localsend-nomad/patches/localsend-cli-doris.patch
Normal 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 {
|
||||||
Reference in New Issue
Block a user