diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6112a4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# SSH keys +*_id_ed25519 +*_id_ed25519.pub +*.pem + +# Build artifacts +*.spk +build/ + +# IDE +.idea/ +.vscode/ diff --git a/hub/README.md b/hub/README.md new file mode 100644 index 0000000..841b1b7 --- /dev/null +++ b/hub/README.md @@ -0,0 +1,38 @@ +# Busfleet Hub + +Registration endpoint for kit-connect devices. Runs on the VPS at `162.243.83.36:8080`. + +## Files + +- **register.sh** — Called per-device. Assigns a tunnel port from pool 2230-2299, records it in `port-registry.json`, returns JSON. +- **server.py** — Minimal Python HTTP server. Routes `GET /api/register/` → `register.sh`. +- **port-registry.json** — Persistent device→port mapping. + +## Deploy + +```bash +scp hub/register.sh hub/server.py root@162.243.83.36:/opt/busfleet-hub/ +ssh root@162.243.83.36 systemctl restart busfleet-hub +``` + +## API + +``` +GET /api/register/ +``` + +Response (existing device): +```json +{"device_id": "x5925", "tunnel_port": 2230, "tailscale_auth_key": "tskey-auth-...", "status": "existing"} +``` + +Response (new device): +```json +{"device_id": "x9999", "tunnel_port": 2231, "tailscale_auth_key": "tskey-auth-...", "status": "new"} +``` + +## Port Pool + +2230–2299. Already assigned: +- x4078: 2226 (grandfathered) +- x5925: 2230 diff --git a/hub/port-registry.json b/hub/port-registry.json new file mode 100644 index 0000000..a531fb2 --- /dev/null +++ b/hub/port-registry.json @@ -0,0 +1,4 @@ +{ + "x4078": {"tunnel_port": 2226, "assigned": "2026-07-21"}, + "x5925": {"tunnel_port": 2230, "assigned": "2026-07-22"} +} diff --git a/hub/register.sh b/hub/register.sh new file mode 100644 index 0000000..a28ce0e --- /dev/null +++ b/hub/register.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# busfleet-hub register.sh — Device registration endpoint +# Called with DEVICE_ID as $1. Returns JSON on stdout. +# Port pool: 2230-2299 + +DEVICE_ID="${1:-unknown}" +REGISTRY="/opt/busfleet-hub/port-registry.json" +PORT_POOL_START="${PORT_POOL_START:-2230}" +PORT_POOL_END="${PORT_POOL_END:-2299}" +TAILSCALE_AUTH_KEY="${TAILSCALE_AUTH_KEY:-tskey-auth-kJz8wqNVo211CNTRL-GNL5EFjp5aWQcaWPSVn2aW9TNworKUNBV}" + +# Init registry if missing +if [ ! -f "$REGISTRY" ]; then + echo '{}' > "$REGISTRY" +fi + +# Check if device already registered +EXISTING_PORT=$(python3 -c " +import json +with open('$REGISTRY') as f: + reg = json.load(f) +print(reg.get('$DEVICE_ID', {}).get('tunnel_port', '')) +" 2>/dev/null) + +if [ -n "$EXISTING_PORT" ]; then + cat </dev/null) + +if [ -z "$ASSIGNED_PORT" ]; then + cat </dev/null + +cat < — assign tunnel port, return JSON + GET /health — health check +""" + +import json +import subprocess +import sys +from http.server import HTTPServer, BaseHTTPRequestHandler + +REGISTER_SH = "/opt/busfleet-hub/register.sh" + + +class HubHandler(BaseHTTPRequestHandler): + """Handle hub registration requests.""" + + def do_GET(self): + # Route: /api/register/ + if self.path.startswith("/api/register/"): + device_id = self.path.split("/api/register/", 1)[1].strip("/") + if not device_id: + self.send_json_error(400, "Missing device ID") + return + + try: + result = subprocess.run( + [REGISTER_SH, device_id], + capture_output=True, text=True, timeout=15, + ) + status = 200 if result.returncode == 0 else 500 + self.send_json(status, result.stdout.strip()) + except subprocess.TimeoutExpired: + self.send_json_error(504, "Registration timed out") + except Exception as exc: + self.send_json_error(500, str(exc)) + return + + # Health check + if self.path in ("/", "/health"): + self.send_json(200, '{"status":"ok","service":"busfleet-hub"}') + return + + self.send_json_error(404, "Not found") + + def send_json(self, status, body): + """Send a JSON response.""" + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(body.encode() if isinstance(body, str) else body) + + def send_json_error(self, status, message): + """Send a JSON error response.""" + self.send_json(status, json.dumps({"error": message})) + + def log_message(self, fmt, *args): + print(f"[hub] {args[0]}", file=sys.stderr) + + +def main(): + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080 + server = HTTPServer(("0.0.0.0", port), HubHandler) + print(f"Busfleet Hub listening on :{port}", flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nShutting down.", flush=True) + server.shutdown() + + +if __name__ == "__main__": + main() diff --git a/kit-connect/PACKAGE_ICON.PNG b/kit-connect/PACKAGE_ICON.PNG new file mode 100644 index 0000000..1b66a83 Binary files /dev/null and b/kit-connect/PACKAGE_ICON.PNG differ diff --git a/kit-connect/PACKAGE_ICON_256.PNG b/kit-connect/PACKAGE_ICON_256.PNG new file mode 100644 index 0000000..458053a Binary files /dev/null and b/kit-connect/PACKAGE_ICON_256.PNG differ diff --git a/kit-connect/build.sh b/kit-connect/build.sh new file mode 100755 index 0000000..e69ada7 --- /dev/null +++ b/kit-connect/build.sh @@ -0,0 +1,79 @@ +#!/bin/sh +# build.sh — Assemble kit-connect SPK for Synology SRM (ipq806x) +# Output: kit-connect-0.1-0001.spk +# +# SPK structure (outer tar, UNCOMPRESSED): +# INFO +# PACKAGE_ICON.PNG (64x64) +# PACKAGE_ICON_256.PNG (256x256) +# scripts/ +# postinst +# start-stop-status +# preinst +# preuninst +# postuninst +# conf/ +# privilege +# resource +# package.tgz (gzipped inner tar of bin/ + conf/) +# checksum (md5 of package.tgz) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +PKG_NAME="kit-connect" +VERSION="0.1-0001" +OUTPUT="${PKG_NAME}-${VERSION}.spk" +BUILD_DIR="/tmp/kit-connect-build-$$" + +echo "=== Building ${PKG_NAME}-${VERSION}.spk ===" + +# Clean and create build staging directory +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" + +# ── 1. Create inner package.tgz (gzipped tar of bin/ + conf/) ───────── +echo " → Creating package.tgz" +tar -czf "$BUILD_DIR/package.tgz" \ + --format=ustar \ + bin/ \ + conf/connect.conf + +# ── 2. Generate checksum ────────────────────────────────────────────── +echo " → Computing checksum" +md5sum "$BUILD_DIR/package.tgz" | awk '{print $1}' > "$BUILD_DIR/checksum" + +# ── 3. Assemble outer SPK (UNCOMPRESSED tar) ────────────────────────── +echo " → Assembling SPK" +tar -cf "${OUTPUT}" \ + --format=ustar \ + -C "$SCRIPT_DIR" \ + INFO \ + PACKAGE_ICON.PNG \ + PACKAGE_ICON_256.PNG \ + scripts/ \ + -C "$BUILD_DIR" \ + package.tgz \ + checksum + +# SPK conf/ goes in the outer tar too +tar -rf "${OUTPUT}" \ + --format=ustar \ + -C "$SCRIPT_DIR" \ + conf/privilege \ + conf/resource + +# ── 4. Verify ───────────────────────────────────────────────────────── +echo " → Verifying SPK contents" +tar -tf "${OUTPUT}" | sort + +SIZE=$(stat -f%z "${OUTPUT}" 2>/dev/null || stat -c%s "${OUTPUT}" 2>/dev/null) +echo "" +echo "=== Build complete: ${OUTPUT} ===" +echo " Size: ${SIZE} bytes" +echo " MD5: $(md5sum "${OUTPUT}" | awk '{print $1}')" + +# Cleanup +rm -rf "$BUILD_DIR" diff --git a/kit-connect/conf/privilege b/kit-connect/conf/privilege new file mode 100644 index 0000000..384af0e --- /dev/null +++ b/kit-connect/conf/privilege @@ -0,0 +1 @@ +{"defaults":{"run-as":"root"}} diff --git a/kit-connect/conf/resource b/kit-connect/conf/resource new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/kit-connect/conf/resource @@ -0,0 +1 @@ +{} diff --git a/kit-connect/scripts/postinst b/kit-connect/scripts/postinst new file mode 100755 index 0000000..b9ede72 --- /dev/null +++ b/kit-connect/scripts/postinst @@ -0,0 +1,43 @@ +#!/bin/sh +# kit-connect postinst — register with hub, set up config +PKG_DIR="/var/packages/kit-connect/target" +CONF="/etc/kit-connect/connect.conf" +LOG_TAG="kit-connect" + +log() { logger -t "$LOG_TAG" -p local0.warn "$*"; } + +mkdir -p /etc/kit-connect "$PKG_DIR/var" + +# If config doesn't exist, create from template +if [ ! -f "$CONF" ]; then + cp "$PKG_DIR/conf/connect.conf" "$CONF" +fi + +# Prompt for device ID if not set +DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || echo "") +if [ -z "$DEVICE_ID" ]; then + DEVICE_ID=$(hostname 2>/dev/null || echo "unknown") +fi + +# Write device ID into config +sed -i "s/^DEVICE_ID=.*/DEVICE_ID=$DEVICE_ID/" "$CONF" 2>/dev/null + +# Set hostname for Tailscale +sed -i "s/^TAILSCALE_HOSTNAME=.*/TAILSCALE_HOSTNAME=$DEVICE_ID/" "$CONF" 2>/dev/null + +# Install tailscale binaries if not present +if [ ! -f /usr/local/bin/tailscale ]; then + if [ -f "$PKG_DIR/bin/tailscale" ]; then + cp "$PKG_DIR/bin/tailscale" "$PKG_DIR/bin/tailscaled" /usr/local/bin/ 2>/dev/null + chmod +x /usr/local/bin/tailscale /usr/local/bin/tailscaled 2>/dev/null + fi +fi + +# Set key permissions +chmod 600 "$PKG_DIR/bin/connect_id_ed25519" 2>/dev/null + +# Try hub registration (non-fatal — daemon retries) +curl -s --connect-timeout 10 "http://162.243.83.36:8080/api/register/${DEVICE_ID}" 2>/dev/null || true + +log "postinst complete — device=${DEVICE_ID}" +exit 0 diff --git a/kit-connect/scripts/postuninst b/kit-connect/scripts/postuninst new file mode 100755 index 0000000..039e4d0 --- /dev/null +++ b/kit-connect/scripts/postuninst @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/kit-connect/scripts/preinst b/kit-connect/scripts/preinst new file mode 100755 index 0000000..039e4d0 --- /dev/null +++ b/kit-connect/scripts/preinst @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/kit-connect/scripts/preuninst b/kit-connect/scripts/preuninst new file mode 100755 index 0000000..039e4d0 --- /dev/null +++ b/kit-connect/scripts/preuninst @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/kit-connect/scripts/start-stop-status b/kit-connect/scripts/start-stop-status new file mode 100755 index 0000000..0ef13d8 --- /dev/null +++ b/kit-connect/scripts/start-stop-status @@ -0,0 +1,35 @@ +#!/bin/sh + +PKG="kit-connect" +PKG_DIR="/var/packages/${PKG}/target" +DAEMON="${PKG_DIR}/bin/connect-daemon.sh" +DAEMON_PID="${PKG_DIR}/var/daemon.pid" + +case "$1" in + start) + mkdir -p "${PKG_DIR}/var" + if [ -f "$DAEMON_PID" ] && kill -0 "$(cat "$DAEMON_PID")" 2>/dev/null; then + echo "Already running" + exit 0 + fi + setsid "$DAEMON" & + echo $! > "$DAEMON_PID" + echo "Started" + ;; + stop) + if [ -f "$DAEMON_PID" ]; then + kill "$(cat "$DAEMON_PID")" 2>/dev/null + sleep 1 + kill -9 "$(cat "$DAEMON_PID")" 2>/dev/null + rm -f "$DAEMON_PID" + fi + echo "Stopped" + ;; + status) + [ -f "$DAEMON_PID" ] && kill -0 "$(cat "$DAEMON_PID")" 2>/dev/null && exit 0 + exit 1 + ;; + *) + echo "Usage: $0 {start|stop|status}" + ;; +esac diff --git a/syno-balance/PACKAGE_ICON.PNG b/syno-balance/PACKAGE_ICON.PNG new file mode 100644 index 0000000..5ea7325 Binary files /dev/null and b/syno-balance/PACKAGE_ICON.PNG differ diff --git a/syno-balance/PACKAGE_ICON_256.PNG b/syno-balance/PACKAGE_ICON_256.PNG new file mode 100644 index 0000000..c61967e Binary files /dev/null and b/syno-balance/PACKAGE_ICON_256.PNG differ diff --git a/syno-balance/build.sh b/syno-balance/build.sh new file mode 100755 index 0000000..64208d5 --- /dev/null +++ b/syno-balance/build.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# build.sh — Assemble syno-balance SPK for Synology SRM (noarch) +# Output: syno-balance-0.1-0001.spk +# TEST BUILD — not for production. +# +# SPK structure (outer tar, UNCOMPRESSED): +# INFO +# PACKAGE_ICON.PNG (64x64) +# PACKAGE_ICON_256.PNG (256x256) +# scripts/ +# postinst +# start-stop-status +# preinst +# preuninst +# postuninst +# conf/ +# privilege +# resource +# package.tgz (gzipped inner tar of bin/ + etc/) +# checksum (md5 of package.tgz) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +PKG_NAME="syno-balance" +VERSION="0.1-0001" +OUTPUT="${PKG_NAME}-${VERSION}.spk" +BUILD_DIR="/tmp/syno-balance-build-$$" + +echo "=== Building ${PKG_NAME}-${VERSION}.spk (TEST BUILD) ===" + +# Clean and create build staging directory +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" + +# ── 1. Create inner package.tgz (gzipped tar of bin/ + etc/) ────────── +echo " → Creating package.tgz" +tar -czf "$BUILD_DIR/package.tgz" \ + --format=ustar \ + bin/ \ + etc/ + +# ── 2. Generate checksum ────────────────────────────────────────────── +echo " → Computing checksum" +md5sum "$BUILD_DIR/package.tgz" | awk '{print $1}' > "$BUILD_DIR/checksum" + +# ── 3. Assemble outer SPK (UNCOMPRESSED tar) ────────────────────────── +echo " → Assembling SPK" +tar -cf "${OUTPUT}" \ + --format=ustar \ + -C "$SCRIPT_DIR" \ + INFO \ + PACKAGE_ICON.PNG \ + PACKAGE_ICON_256.PNG \ + scripts/ \ + -C "$BUILD_DIR" \ + package.tgz \ + checksum + +# SPK conf/ goes in the outer tar too +tar -rf "${OUTPUT}" \ + --format=ustar \ + -C "$SCRIPT_DIR" \ + conf/privilege \ + conf/resource + +# ── 4. Verify ───────────────────────────────────────────────────────── +echo " → Verifying SPK contents" +tar -tf "${OUTPUT}" | sort + +SIZE=$(stat -f%z "${OUTPUT}" 2>/dev/null || stat -c%s "${OUTPUT}" 2>/dev/null) +echo "" +echo "=== Build complete: ${OUTPUT} ===" +echo " Size: ${SIZE} bytes" +echo " MD5: $(md5sum "${OUTPUT}" | awk '{print $1}')" + +# Cleanup +rm -rf "$BUILD_DIR" diff --git a/syno-balance/conf/privilege b/syno-balance/conf/privilege new file mode 100644 index 0000000..384af0e --- /dev/null +++ b/syno-balance/conf/privilege @@ -0,0 +1 @@ +{"defaults":{"run-as":"root"}} diff --git a/syno-balance/conf/resource b/syno-balance/conf/resource new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/syno-balance/conf/resource @@ -0,0 +1 @@ +{} diff --git a/syno-balance/scripts/postinst b/syno-balance/scripts/postinst new file mode 100755 index 0000000..6b019e3 --- /dev/null +++ b/syno-balance/scripts/postinst @@ -0,0 +1,18 @@ +#!/bin/sh +# syno-balance postinst — set up config, start daemon +PKG_DIR="/var/packages/syno-balance/target" +CONF="/etc/syno-balance/syno-balance.conf" +LOG_TAG="syno-balance" + +log() { logger -t "$LOG_TAG" -p local0.warn "$*"; } + +mkdir -p /etc/syno-balance "$PKG_DIR/var" + +# If config doesn't exist, create from template +if [ ! -f "$CONF" ]; then + cp "$PKG_DIR/etc/syno-balance.conf" "$CONF" + log "config created from template" +fi + +log "postinst complete" +exit 0 diff --git a/syno-balance/scripts/postuninst b/syno-balance/scripts/postuninst new file mode 100755 index 0000000..039e4d0 --- /dev/null +++ b/syno-balance/scripts/postuninst @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/syno-balance/scripts/preinst b/syno-balance/scripts/preinst new file mode 100755 index 0000000..039e4d0 --- /dev/null +++ b/syno-balance/scripts/preinst @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/syno-balance/scripts/preuninst b/syno-balance/scripts/preuninst new file mode 100755 index 0000000..039e4d0 --- /dev/null +++ b/syno-balance/scripts/preuninst @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/syno-balance/scripts/start-stop-status b/syno-balance/scripts/start-stop-status new file mode 100755 index 0000000..c685c67 --- /dev/null +++ b/syno-balance/scripts/start-stop-status @@ -0,0 +1,35 @@ +#!/bin/sh + +PKG="syno-balance" +PKG_DIR="/var/packages/${PKG}/target" +DAEMON="${PKG_DIR}/bin/syno-daemon.sh" +DAEMON_PID="${PKG_DIR}/var/syno-balance.pid" + +case "$1" in + start) + mkdir -p "${PKG_DIR}/var" + if [ -f "$DAEMON_PID" ] && kill -0 "$(cat "$DAEMON_PID")" 2>/dev/null; then + echo "Already running" + exit 0 + fi + setsid "$DAEMON" & + echo $! > "$DAEMON_PID" + echo "Started" + ;; + stop) + if [ -f "$DAEMON_PID" ]; then + kill "$(cat "$DAEMON_PID")" 2>/dev/null + sleep 1 + kill -9 "$(cat "$DAEMON_PID")" 2>/dev/null + rm -f "$DAEMON_PID" + fi + echo "Stopped" + ;; + status) + [ -f "$DAEMON_PID" ] && kill -0 "$(cat "$DAEMON_PID")" 2>/dev/null && exit 0 + exit 1 + ;; + *) + echo "Usage: $0 {start|stop|status}" + ;; +esac