probe: downloadable executables for machines without Docker
Each release attaches selfphoneng-probe-linux-x64 and -arm64 (Node's single executable applications: esbuild bundle + postject into the official Node binary, checked against nodejs.org's SHASUMS256.txt), the bundle as selfphoneng-probe.cjs (Node 22+) and SHA256SUMS. Built in binary.Dockerfile with BuildKit's local output, so Gitea's runner can build it too. deploy/probe/selfphoneng-probe.service runs it under systemd, sandboxed.
This commit is contained in:
1 parent
65a0438dde
commit
4143f6d064
9 files changed
+232
-5
No files matched your search
@@ -5,13 +5,14 @@
|
||||
# Gitea: <gitea host>/<owner>/selfphoneng-{api,asterisk,probe} (secrets REGISTRY_USER + REGISTRY_TOKEN:
|
||||
# a personal access token with write:package; Gitea's job token can't push packages)
|
||||
# Tags: the version (0.0.1), plus <major>.<minor> and latest; a pre-release gets the version and dev.
|
||||
# The release also gets the probe as downloads (selfphoneng-probe-linux-{x64,arm64}, .cjs, SHA256SUMS).
|
||||
name: release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write # attaching the probe downloads to the release
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
@@ -102,3 +103,28 @@ jobs:
|
||||
done
|
||||
done
|
||||
echo "pushed: $p-{api,asterisk,probe}:{${{ steps.v.outputs.tags }}}" | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}"
|
||||
- name: Probe downloads
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The probe without Docker (08 §2.6): a .cjs for Node 22+, and Linux executables with Node
|
||||
# inside. Built in BuildKit and handed back as files, so it works on Gitea's runner too.
|
||||
docker build -f apps/probe/binary.Dockerfile --output type=local,dest=probe-dist .
|
||||
id="${{ github.event.release.id }}"
|
||||
for f in probe-dist/*; do
|
||||
name=$(basename "$f")
|
||||
if [ "${{ steps.v.outputs.registry }}" = ghcr.io ]; then
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: Bearer $GH_TOKEN" \
|
||||
-H 'Content-Type: application/octet-stream' --data-binary "@$f" \
|
||||
"https://uploads.github.com/repos/$GITHUB_REPOSITORY/releases/$id/assets?name=$name"
|
||||
else
|
||||
# The REGISTRY_TOKEN personal access token needs write:repository for this.
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token ${REGISTRY_TOKEN:-$GH_TOKEN}" \
|
||||
-F "attachment=@$f" \
|
||||
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$id/assets?name=$name"
|
||||
fi
|
||||
echo "attached $name" | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}"
|
||||
done
|
||||
+3
-2
@@ -8,8 +8,9 @@ tag (08 §3.1): `selfphoneng-api` and `selfphoneng-asterisk`, on ghcr.io and on
|
||||
- **System → Diagnostics:** checks what usually keeps phones off the network (public IP, DNS,
|
||||
local networks, the phone network limit, SIP listening, free audio ports), with how to fix each.
|
||||
A **Quick** or **Complete network test** checks the ports from the internet through a network
|
||||
test probe (`apps/probe`, image `selfphoneng-probe`) you run outside your network; set its URL
|
||||
in Settings → Network.
|
||||
test probe you run outside your network; set its URL in Settings → Network. The probe comes as
|
||||
a Docker image (`selfphoneng-probe`), and, for a machine without Docker, as a single Linux
|
||||
executable (x64 and arm64) attached to each release, with a systemd unit.
|
||||
- The call audio port range is configurable: `RTP_PORT_START` and `RTP_PORT_END` (default
|
||||
10000–20000).
|
||||
- Updating (Docker only): a new version backs up before it changes the database, and stops
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Builds the probe's downloadable files (build-binary.mjs) and hands them back, no image:
|
||||
# docker build -f apps/probe/binary.Dockerfile --output type=local,dest=probe-dist .
|
||||
FROM node:24-trixie-slim AS build
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN corepack enable && pnpm install --frozen-lockfile --filter @selfphoneng/probe \
|
||||
&& node apps/probe/build-binary.mjs
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /src/apps/probe/dist /
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Builds the network test probe (08 §2.6) for machines without Docker:
|
||||
// dist/selfphoneng-probe.cjs one file; runs with Node 22 or newer
|
||||
// dist/selfphoneng-probe-linux-x64 standalone executables with Node inside (Node's
|
||||
// dist/selfphoneng-probe-linux-arm64 single executable applications), nothing to install
|
||||
// dist/SHA256SUMS
|
||||
// The executables carry this Node's version, downloaded from nodejs.org and checked against its
|
||||
// SHASUMS256.txt. Run on Linux (needs tar with xz): node apps/probe/build-binary.mjs
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { build } from 'esbuild';
|
||||
import postject from 'postject';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const dist = join(here, 'dist');
|
||||
const NAME = 'selfphoneng-probe';
|
||||
const version = process.version;
|
||||
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
|
||||
|
||||
rmSync(dist, { recursive: true, force: true });
|
||||
mkdirSync(dist);
|
||||
const script = join(dist, `${NAME}.cjs`);
|
||||
await build({
|
||||
entryPoints: [join(here, 'src/index.ts')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
target: 'node22',
|
||||
outfile: script,
|
||||
banner: { js: '#!/usr/bin/env node\n// SPDX-License-Identifier: AGPL-3.0-or-later' },
|
||||
});
|
||||
chmodSync(script, 0o755);
|
||||
|
||||
const work = mkdtempSync(join(tmpdir(), 'sp-probe-'));
|
||||
try {
|
||||
const blob = join(work, 'sea.blob');
|
||||
writeFileSync(
|
||||
join(work, 'sea.json'),
|
||||
JSON.stringify({
|
||||
main: script,
|
||||
output: blob,
|
||||
disableExperimentalSEAWarning: true,
|
||||
// Neither is portable between CPU architectures; the probe starts in milliseconds anyway.
|
||||
useSnapshot: false,
|
||||
useCodeCache: false,
|
||||
}),
|
||||
);
|
||||
execFileSync(process.execPath, ['--experimental-sea-config', join(work, 'sea.json')], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
const base = `https://nodejs.org/dist/${version}`;
|
||||
const sums = await (await fetch(`${base}/SHASUMS256.txt`)).text();
|
||||
for (const arch of ['x64', 'arm64']) {
|
||||
const file = `node-${version}-linux-${arch}.tar.xz`;
|
||||
const want = sums
|
||||
.split('\n')
|
||||
.find((l) => l.endsWith(` ${file}`))
|
||||
?.split(' ')[0];
|
||||
const tarball = Buffer.from(await (await fetch(`${base}/${file}`)).arrayBuffer());
|
||||
if (!want || sha256(tarball) !== want) throw new Error(`${file}: checksum doesn't match`);
|
||||
writeFileSync(join(work, file), tarball);
|
||||
const inner = `node-${version}-linux-${arch}/bin/node`;
|
||||
execFileSync('tar', ['-xJf', join(work, file), '-C', work, inner]);
|
||||
const out = join(dist, `${NAME}-linux-${arch}`);
|
||||
copyFileSync(join(work, inner), out);
|
||||
await postject.inject(out, 'NODE_SEA_BLOB', readFileSync(blob), {
|
||||
sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
|
||||
});
|
||||
chmodSync(out, 0o755);
|
||||
console.log(`${out} (Node ${version})`);
|
||||
}
|
||||
} finally {
|
||||
rmSync(work, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const files = [`${NAME}.cjs`, `${NAME}-linux-x64`, `${NAME}-linux-arm64`];
|
||||
writeFileSync(
|
||||
join(dist, 'SHA256SUMS'),
|
||||
files.map((f) => `${sha256(readFileSync(join(dist, f)))} ${f}\n`).join(''),
|
||||
);
|
||||
console.log(readFileSync(join(dist, 'SHA256SUMS'), 'utf8'));
|
||||
@@ -7,5 +7,9 @@
|
||||
"scripts": {
|
||||
"start": "node src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "0.28.2",
|
||||
"postject": "1.0.0-alpha.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# The network test probe (08 §2.6) as a service, without Docker. With the downloaded executable at
|
||||
# /usr/local/bin/selfphoneng-probe:
|
||||
# sudo cp selfphoneng-probe.service /etc/systemd/system/
|
||||
# sudo systemctl enable --now selfphoneng-probe
|
||||
# sudo ufw allow 8080/tcp
|
||||
[Unit]
|
||||
Description=SelfPhoneNG network test probe
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/selfphoneng-probe
|
||||
Environment=PORT=8080
|
||||
# Behind your own HTTPS reverse proxy that sets X-Forwarded-For: PORT to a local one, and
|
||||
#Environment=TRUST_PROXY=true
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
# It needs nothing but the network: no files, no privileges.
|
||||
DynamicUser=yes
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
LockPersonality=yes
|
||||
MemoryMax=200M
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -74,7 +74,31 @@ public address back in, or route it without going through the port forwards.
|
||||
- **Complete network test**: also SIP over TLS (if on), the browser phone's `/ws`, and a sample
|
||||
of the call audio ports (the server listens on them while the probe sends to them).
|
||||
|
||||
To run a probe, on any machine outside your network (a small VPS is plenty):
|
||||
To run a probe, on any machine outside your network (a small VPS is plenty). It needs about
|
||||
50 MB of memory and one open TCP port (8080).
|
||||
|
||||
**Without Docker:** each release has the probe as a single executable for Linux, with everything it
|
||||
needs inside (`selfphoneng-probe-linux-x64`, or `-arm64` for ARM machines), next to `SHA256SUMS`.
|
||||
With Node 22 or newer installed you can take the small `selfphoneng-probe.cjs` instead.
|
||||
|
||||
```sh
|
||||
v=v0.0.3 # the release
|
||||
curl -fLO https://git.dou.bet/spng/selfphoneng/releases/download/$v/selfphoneng-probe-linux-x64
|
||||
curl -fLO https://git.dou.bet/spng/selfphoneng/releases/download/$v/SHA256SUMS
|
||||
sha256sum --check --ignore-missing SHA256SUMS
|
||||
sudo install -m 755 selfphoneng-probe-linux-x64 /usr/local/bin/selfphoneng-probe
|
||||
curl -fLO https://git.dou.bet/spng/selfphoneng/raw/tag/$v/deploy/probe/selfphoneng-probe.service
|
||||
sudo cp selfphoneng-probe.service /etc/systemd/system/
|
||||
sudo systemctl enable --now selfphoneng-probe
|
||||
sudo ufw allow 8080/tcp
|
||||
```
|
||||
|
||||
(On GitHub, the downloads are at `https://github.com/iamdoubz/selfphoneng/releases/download/$v/…`.)
|
||||
To try it first without the service, run `./selfphoneng-probe-linux-x64` and open
|
||||
`http://<machine>:8080/healthz`. `PORT` changes the port; behind your own HTTPS reverse proxy
|
||||
that sets X-Forwarded-For, also set `TRUST_PROXY=true`.
|
||||
|
||||
**With Docker:**
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/probe/compose.yml up -d
|
||||
|
||||
@@ -107,6 +107,14 @@ how to fix it.
|
||||
`apps/probe` (no dependencies, its own image `selfphoneng-probe`, `deploy/probe/compose.yml`),
|
||||
run outside the network; `network.probeUrl` points at it. A server can't test its own ports:
|
||||
hairpin NAT is missing or skips the port forwards, and browsers refuse port 5060.
|
||||
- Without Docker (owner, 2026-10-21): each release also attaches `selfphoneng-probe-linux-x64`
|
||||
and `-arm64` (Node's single executable applications: esbuild bundles the probe into one CJS
|
||||
file, postject injects it into that Node's official binary, checked against nodejs.org's
|
||||
SHASUMS256.txt), the bundle itself (`selfphoneng-probe.cjs`, Node 22+) and `SHA256SUMS`.
|
||||
`apps/probe/build-binary.mjs`, run in `binary.Dockerfile` with BuildKit's local output (so it
|
||||
works on Gitea's runner, where the job's checkout can't be bind-mounted).
|
||||
`deploy/probe/selfphoneng-probe.service` runs it under systemd with `DynamicUser` and a
|
||||
read-only system. About 45 MB of memory.
|
||||
- `POST /v1/check {host, tests}`: the probe tests **only the address the request came from** (the
|
||||
last X-Forwarded-For entry with `TRUST_PROXY=true`), so it can't be aimed at anyone else; 20
|
||||
tests at most, 10 checks per address per 10 minutes, 4 s per test.
|
||||
|
||||
Generated
+23
-1
@@ -88,7 +88,14 @@ importers:
|
||||
specifier: ^0.31.11
|
||||
version: 0.31.11
|
||||
|
||||
apps/probe: {}
|
||||
apps/probe:
|
||||
devDependencies:
|
||||
esbuild:
|
||||
specifier: 0.28.2
|
||||
version: 0.28.2
|
||||
postject:
|
||||
specifier: 1.0.0-alpha.6
|
||||
version: 1.0.0-alpha.6
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
@@ -1521,6 +1528,10 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
commander@9.5.0:
|
||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||
engines: {node: ^12.20.0 || >=14}
|
||||
|
||||
content-disposition@3.0.0:
|
||||
resolution: {integrity: sha512-ZH/0Xs9rMIFWCOmGdmS9eHBTF62qqQYNz4nVjQhkdIO/a0fCP4UIM3mRz/wiqL0L14YgAz/1xio4OaSY4+ON/A==}
|
||||
engines: {node: '>=22'}
|
||||
@@ -2282,6 +2293,11 @@ packages:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postject@1.0.0-alpha.6:
|
||||
resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -3691,6 +3707,8 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
commander@9.5.0: {}
|
||||
|
||||
content-disposition@3.0.0: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
@@ -4370,6 +4388,10 @@ snapshots:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
postject@1.0.0-alpha.6:
|
||||
dependencies:
|
||||
commander: 9.5.0
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.9.10: {}
|
||||
|
||||
Reference in new issue
Block a user