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.
|
||||
|
||||
*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 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
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