Files
darroyo 8e08c73f60 feat: complete SyncServer implementation
Full-stack Go monolith with embedded React frontend for orchestrating
rsync-over-SSH file synchronization with Wake-on-LAN support.

Features:
- JWT auth (HS256) with bcrypt password hashing
- CRUD for machines (with WoL config) and sync_pairs
- Ed25519 SSH key generation and known_hosts management
- WoL magic packet sender + TCP-connect waiter with backoff
- Sync engine: rsync subprocess, per-pair job queue, progress parsing
- Homebrew cron parser for scheduled syncs
- SSE stream for live job status (queued/waking_up/running/success/failed)
- React+TS+Vite+Tailwind SPA embedded via embed.FS
- Debian packaging with systemd unit, postinst/prerm/postrm

Tech stack:
- Go 1.22+ (CGO_ENABLED=0, pure SQLite via modernc.org/sqlite)
- chi router for HTTP API
- TypeScript + React 18 + Tailwind CSS frontend
- Cross-compiled to Linux amd64 for Proxmox LXC deployment

Tests: wol (MAC parsing, magic packet), syncengine/queue, scheduler/cron
2026-07-07 15:03:22 -04:00

300 lines
9.2 KiB
Markdown

# SyncServer
Self-hosted file sync orchestrator with Wake-on-LAN and web UI. Orchestrates `rsync` over SSH between multiple machines from a single central server.
## Features
- **Web UI**: React-based interface to manage machines, sync pairs, and job history
- **Wake-on-LAN**: Power on remote machines before syncing (UDP magic packet)
- **Rsync over SSH**: Efficient file synchronization with progress tracking
- **Scheduled syncs**: Cron-style scheduler for automated backups
- **Live progress**: Server-Sent Events (SSE) for real-time job status
- **Single binary**: No dynamic dependencies, pure Go with embedded SQLite
## Architecture
```
┌─────────────────────────────────────┐
│ SyncServer (Go binary) │
│ ┌─────────────────────────────────┐ │
│ │ Web UI (React, embedded) │ │
│ │ REST API (chi router) │ │
│ │ Auth (JWT + bcrypt) │ │
│ │ Scheduler (cron) │ │
│ │ Sync Engine (rsync runner) │ │
│ │ WoL (UDP magic packet) │ │
│ │ SQLite (embedded) │ │
│ └─────────────────────────────────┘ │
└──────────────┬────────────────────────┘
│ SSH + UDP
┌─────────┴──────────┐
▼ ▼
Machine A Machine B
(rsync+sshd) (rsync+sshd)
```
## Network Requirements for Wake-on-LAN
**Critical**: The magic packet (WoL) only works when the SyncServer is on the **same L2 network** (same broadcast domain) as the target machine.
### Proxmox LXC Setup
- Use **bridge networking** (`vmbr0`) — NOT NAT
- The LXC must share the same VLAN/LAN as the machines it needs to wake
- No special LXC privileges required (works with unprivileged containers)
- `SO_BROADCAST` works in unprivileged LXC containers
If your LXC is on a different subnet, use the `broadcast_addr` field on the machine to specify the broadcast address of the target machine's subnet (e.g., `192.168.1.255`).
### BIOS/UEFI Requirements on Target Machines
- Enable **Wake-on-LAN** in BIOS/UEFI
- Set to "Power On by PCIe" or "Wake by PCI-E"
- Some motherboards require both PCIe and LAN WoL to be enabled
## Installation
### Quick Start (Development)
```bash
git clone https://github.com/syncserver/syncserver
cd syncserver
# Run with default settings (data in ./data)
SYNCSERVER_ADMIN_USER=admin SYNCSERVER_ADMIN_PASSWORD=secret \
./syncserver --addr :8080
# Open http://your-server:8080
```
### Production Install (.deb)
```bash
# Transfer to your Proxmox LXC (Debian/Ubuntu)
scp dist/syncserver_*.deb root@your-lxc:/tmp/
# On the LXC:
dpkg -i /tmp/syncserver_*.deb
# postinst will:
# - Create syncserver user
# - Create /var/lib/syncserver/{data,ssh,logs}
# - Install config at /etc/syncserver/config.yaml
# - Enable and start the systemd service
# To configure the admin user on first run:
# Set environment variables BEFORE first start (in /etc/syncserver/config.yaml or systemd unit):
```
### Configuration
Edit `/etc/syncserver/config.yaml`:
```yaml
data_dir: /var/lib/syncserver
addr: :8080
auth:
jwt_secret: "your-secret-here" # Generate with: openssl rand -hex 32
jwt_expiry_hours: 24
scheduler:
timezone: UTC
```
Or use environment variables (override config file):
```bash
SYNCSERVER_ADMIN_USER=admin SYNCSERVER_ADMIN_PASSWORD=secret \
SYNCSERVER_JWT_SECRET=your-secret \
SYNCSERVER_DATA_DIR=/var/lib/syncserver \
/usr/bin/syncserver --config /etc/syncserver/config.yaml
```
## Setup: SSH Keys
### Step 1: Get the Server's Public Key
In the UI, go to **Settings** to copy the server's SSH public key. It looks like:
```
ssh-ed25519 AAAA... syncserver
```
### Step 2: Add the Key to Remote Machines
On each remote machine, add the public key to `~/.ssh/authorized_keys`:
```bash
# As root on the remote machine:
echo "ssh-ed25519 AAAA... syncserver" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
```
Or use `ssh-copy-id`:
```bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@remote-machine
# Then replace the key in the remote's authorized_keys with the syncserver key
```
### Step 3: Register Machines in the UI
Go to **Machines****Add Machine**:
- Name: e.g., `backup-nas`
- Host: IP or hostname (e.g., `192.168.1.100`)
- SSH Port: `22`
- SSH User: `root` (or your preferred user)
- SSH Key: leave empty to use the server's default key
For **Wake-on-LAN**:
- Enable "Wake-on-LAN"
- Enter the MAC address (e.g., `AA:BB:CC:DD:EE:FF`)
- Optionally set a broadcast address (leave empty for `255.255.255.255`)
- Configure timeout and check interval
## Creating Sync Pairs
Go to **Sync Pairs****Add Sync Pair**:
| Field | Description |
|-------|-------------|
| Name | Descriptive name, e.g., `backup-home` |
| Source | Machine + path (or "Local server" for the syncserver itself) |
| Destination | Machine + path |
| Direction | `push` (source→dest), `pull` (dest→source), `mirror` (bidirectional with delete) |
| Rsync Flags | Default: `-aP` (archive, progress). Add `--delete` for mirror mode. |
| Exclude Patterns | One pattern per line, e.g., `*.tmp`, `node_modules/` |
## Scheduling
Add a schedule to any sync pair:
| Cron Expr | Description |
|-----------|-------------|
| `0 2 * * *` | Daily at 2 AM |
| `0 */6 * * *` | Every 6 hours |
| `0 9-17 * * 1-5` | Business hours, weekdays |
| `*/15 * * * *` | Every 15 minutes |
All schedules run in **UTC** by default. Set `scheduler.timezone` in config to change.
## Job States
| State | Description |
|-------|-------------|
| `queued` | Waiting for a worker |
| `waking_up` | Sending WoL magic packet and waiting for machine |
| `running` | rsync in progress |
| `success` | Completed successfully |
| `failed` | Error during sync |
| `cancelled` | User cancelled the job |
## Logs
Job logs are stored at `/var/lib/syncserver/logs/<job_id>.log`.
Application logs go to systemd journal:
```bash
journalctl -u syncserver -f
```
## Upgrade
```bash
# On the LXC:
dpkg -i /tmp/syncserver-new-version.deb
# The service will automatically restart
```
Config at `/etc/syncserver/config.yaml` is preserved (conffile).
## Build from Source
```bash
# Requires: Go 1.22+, Node 18+, npm
./scripts/build.sh
# Output: packaging/debian/usr/bin/syncserver
./scripts/package-deb.sh
# Output: dist/syncserver_VERSION_amd64.deb
```
For ARM64:
```bash
GOARCH=arm64 ./scripts/build.sh
ARCH=arm64 ./scripts/package-deb.sh
```
## Data Storage
```
/var/lib/syncserver/
├── data/
│ └── app.db # SQLite database (machines, sync_pairs, jobs, etc.)
├── ssh/
│ ├── id_ed25519 # Server's private SSH key
│ ├── id_ed25519.pub # Server's public SSH key
│ └── known_hosts # Host keys of registered machines
└── logs/
└── <job_id>.log # Per-job rsync output logs
```
**Recommended**: Bind mount `/var/lib/syncserver` to persistent storage outside the LXC root filesystem. This way you can recreate the LXC without losing data.
In Proxmox LXC config (`/etc/pve/lxc/<id>.conf`):
```
mp0: /mnt/data/syncserver,mp=/var/lib/syncserver,backup=0
```
## Troubleshooting
### WoL doesn't work
1. Verify the LXC is on the same L2 network as the target
2. Check `ip link` in the LXC shows the bridge interface
3. Try: `tcpdump -i <iface> udp port 9` in the LXC while triggering WoL
4. Verify WoL is enabled in the target machine's BIOS
5. Try sending WoL from another machine on the same subnet to isolate the issue
### SSH connection fails
1. Verify the public key is in `~/.ssh/authorized_keys` on the target
2. Check `sshd` is running on the target machine
3. Try manually: `ssh -i /var/lib/syncserver/ssh/id_ed25519 user@host`
4. Check the target's `sshd_config`: `PubkeyAuthentication yes`, `AuthorizedKeysFile .ssh/authorized_keys`
### rsync fails with "connection unexpectedly closed"
- Usually means SSH key auth failed
- Check the job log for the exact SSH error
- Try running the rsync command manually from the server with `-v` flag
### Machine shows "unknown" status
- Status is updated when a job runs or when you test SSH
- It does not auto-poll; this is by design to avoid network load
## API Reference
All API endpoints require authentication (JWT cookie).
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/auth/login` | Login with username/password |
| POST | `/api/auth/logout` | Clear session |
| GET | `/api/auth/me` | Current user info |
| GET/POST | `/api/machines` | List/create machines |
| GET/PUT/DELETE | `/api/machines/{id}` | Get/update/delete machine |
| GET/POST | `/api/sync-pairs` | List/create sync pairs |
| GET/PUT/DELETE | `/api/sync-pairs/{id}` | Get/update/delete sync pair |
| POST | `/api/sync-pairs/{id}/run` | Trigger manual sync |
| GET | `/api/jobs` | List jobs |
| POST | `/api/jobs/{id}/cancel` | Cancel a running job |
| GET | `/api/jobs/{id}/log` | Stream job log |
| GET | `/api/jobs/stream` | SSE stream of all job events |
| GET | `/api/settings/pubkey` | Get server's SSH public key |
## License
MIT