f15ac69925
Includes: - package/ GL-XE3000 kit-busrouter (opkg) - scripts/provision.sh (GL) and provision-synology.sh (Synology) - syno-balance/ — new WAN balancer replacing aiwanbal (SmartWAN adapter) - kit-connect/ — unified connectivity SPK (Tailscale + reverse SSH) - docs/deployment/synology-rt2600ac-checklist.md — 62-point checklist - docs/provisioning/device-identity.md — fleet identity spec - docs/pilot/checklist.md — field pilot validation - x4078_20260721.dss — reference config backup Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
227 lines
9.0 KiB
Bash
Executable File
227 lines
9.0 KiB
Bash
Executable File
#!/bin/sh
|
|
# syno-daemon.sh — Synology bus router WAN balancing daemon.
|
|
# Rides on top of SmartWAN (does NOT own routing or health checks).
|
|
# Uses the same scoring engine as GL kit-busrouter (lib-score.sh + lib-decide.sh).
|
|
#
|
|
# Cycle (every CHECK_INTERVAL seconds):
|
|
# 1. Read SmartWAN state (interfaces, current weights, gateways)
|
|
# 2. Measure WAN1 / WAN2 latency, jitter, packet loss
|
|
# 3. Read Eyeride signal strength (if available)
|
|
# 4. Read Starlink status (if available)
|
|
# 5. Score each WAN (lib-score.sh 6-factor composite)
|
|
# 6. Compute target weights (lib-decide.sh)
|
|
# 7. Write weights to SmartWAN if change >= 10 (smartwan-adapter.sh)
|
|
# 8. Send telemetry to fleet hub (if hub is reachable)
|
|
#
|
|
# NEVER calls loadbalance. NEVER flushes conntrack.
|
|
|
|
set -e
|
|
|
|
# ── Paths ──────────────────────────────────────────────────────────
|
|
PKG_DIR="${PKG_DIR:-/var/packages/syno-balance/target}"
|
|
LIB_DIR="${LIB_DIR:-${PKG_DIR}/bin}"
|
|
CONF="/etc/syno-balance/syno-balance.conf"
|
|
STATE_DIR="/tmp/syno-balance"
|
|
PID_FILE="$PKG_DIR/var/syno-balance.pid"
|
|
LOG_TAG="syno-balance"
|
|
|
|
# ── Source libs (same scoring engine as GL) ────────────────────────
|
|
. "${LIB_DIR}/lib-score.sh"
|
|
. "${LIB_DIR}/lib-decide.sh"
|
|
. "${LIB_DIR}/smartwan-adapter.sh"
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
echo "$$" > "$PID_FILE"
|
|
|
|
log() { logger -t "$LOG_TAG" -p local0.warn "syno-balance[$(cat /etc/busrouter/device-id 2>/dev/null || hostname)]: $*"; }
|
|
warn() { logger -t "$LOG_TAG" -p local0.warn "syno-balance[$(cat /etc/busrouter/device-id 2>/dev/null || hostname)]: WARNING: $*"; }
|
|
|
|
# ── Config defaults ────────────────────────────────────────────────
|
|
load_config() {
|
|
[ -f "$CONF" ] && . "$CONF"
|
|
: "${CHECK_INTERVAL:=30}"
|
|
: "${WAN1_IFACE:=eth0}"
|
|
: "${WAN2_IFACE:=eth2}"
|
|
: "${SPEEDTEST_AUTO_ENABLED:=0}"
|
|
: "${EYERIDE_ENABLE:=0}"
|
|
: "${EYERIDE_IP:=192.168.10.1}"
|
|
: "${EYERIDE_PORT:=8080}"
|
|
: "${EYERIDE_USER:=root}"
|
|
: "${STARLINK_ENABLE:=0}"
|
|
: "${STARLINK_IP:=192.168.100.1}"
|
|
: "${STARLINK_PORT:=9200}"
|
|
: "${PING_TARGETS:=1.1.1.2 9.9.9.9 8.8.8.8}"
|
|
: "${PING_COUNT:=5}"
|
|
: "${WEIGHT_MIN:=20}"
|
|
: "${WEIGHT_MAX:=80}"
|
|
: "${ROLLING_SAMPLES:=3}"
|
|
: "${TELEMETRY_HUB:=http://167.172.237.162:8080/api/telemetry}"
|
|
: "${TELEMETRY_BUFFER_DIR:=/tmp/syno-balance/telemetry-buf}"
|
|
DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || hostname 2>/dev/null || echo "unknown")
|
|
}
|
|
|
|
# ── Measure WAN metrics ────────────────────────────────────────────
|
|
measure_wan() {
|
|
local iface="$1"
|
|
local gw="$2"
|
|
local outfile="$STATE_DIR/metric_${iface}"
|
|
|
|
# Latency + jitter + loss via ping (bound to interface)
|
|
local ping_out=""
|
|
ping_out=$(ping -I "$iface" -c "$PING_COUNT" -q "${PING_TARGETS%% *}" 2>/dev/null) || true
|
|
|
|
local lat="" loss="" jitter=""
|
|
if [ -n "$ping_out" ]; then
|
|
# rtt min/avg/max/mdev
|
|
lat=$(echo "$ping_out" | awk -F'[/ ]' '/^rtt/ {printf "%.1f", $8}')
|
|
jitter=$(echo "$ping_out" | awk -F'[/ ]' '/^rtt/ {printf "%.1f", $11}')
|
|
loss=$(echo "$ping_out" | grep -o '[0-9.]*%' | head -1 | tr -d '%')
|
|
fi
|
|
|
|
echo "lat=${lat:-0}" > "$outfile"
|
|
echo "jitter=${jitter:-0}" >> "$outfile"
|
|
echo "loss=${loss:-0}" >> "$outfile"
|
|
echo "gw=${gw:-none}" >> "$outfile"
|
|
echo "ts=$(date +%s)" >> "$outfile"
|
|
}
|
|
|
|
# ── Read Eyeride signal via ubus (if available) ────────────────────
|
|
read_eyeride_signal() {
|
|
[ "$EYERIDE_ENABLE" != "1" ] && return 1
|
|
[ -z "$EYERIDE_PASSWORD" ] && return 1
|
|
|
|
# ubus call over SSH to Eyeride (OpenWrt device)
|
|
local sig=""
|
|
sig=$(sshpass -p "$EYERIDE_PASSWORD" ssh -o StrictHostKeyChecking=no \
|
|
-o ConnectTimeout=5 "${EYERIDE_USER}@${EYERIDE_IP}" -p "${EYERIDE_PORT}" \
|
|
"ubus call network.interface.wwan status 2>/dev/null | grep -o '\"signal_strength\":[0-9-]*' | cut -d: -f2" 2>/dev/null) || true
|
|
|
|
if [ -n "$sig" ]; then
|
|
echo "$sig" > "$STATE_DIR/signal_eyeride"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# ── Read Starlink status (gRPC or HTTP fallback) ───────────────────
|
|
read_starlink_status() {
|
|
[ "$STARLINK_ENABLE" != "1" ] && return 1
|
|
|
|
# HTTP probe to Starlink dish
|
|
local status=""
|
|
status=$(curl -s --connect-timeout 3 "http://${STARLINK_IP}:${STARLINK_PORT}/status" 2>/dev/null) || true
|
|
if [ -n "$status" ]; then
|
|
echo "$status" > "$STATE_DIR/starlink_status"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# ── Score both WANs ────────────────────────────────────────────────
|
|
score_wans() {
|
|
local w1_lat=0 w1_jit=0 w1_loss=0 w1_sig=""
|
|
local w2_lat=0 w2_jit=0 w2_loss=0 w2_sig=""
|
|
|
|
# Read WAN1 metrics
|
|
if [ -f "$STATE_DIR/metric_${WAN1_IFACE}" ]; then
|
|
. "$STATE_DIR/metric_${WAN1_IFACE}"
|
|
w1_lat="${lat:-0}"; w1_jit="${jitter:-0}"; w1_loss="${loss:-0}"
|
|
fi
|
|
|
|
# Read WAN2 metrics
|
|
if [ -f "$STATE_DIR/metric_${WAN2_IFACE}" ]; then
|
|
. "$STATE_DIR/metric_${WAN2_IFACE}"
|
|
w2_lat="${lat:-0}"; w2_jit="${jitter:-0}"; w2_loss="${loss:-0}"
|
|
fi
|
|
|
|
# Read Eyeride signal for WAN1
|
|
if [ -f "$STATE_DIR/signal_eyeride" ]; then
|
|
w1_sig=$(cat "$STATE_DIR/signal_eyeride" 2>/dev/null)
|
|
fi
|
|
|
|
# Compute scores (same algorithm as GL)
|
|
local score1=0 score2=0
|
|
# WAN1: latency, fake-dl=10, fake-ul=5, jitter, loss, signal
|
|
score1=$(score_composite "$w1_lat" "10" "5" "$w1_jit" "$w1_loss" "$w1_sig")
|
|
# WAN2: latency, fake-dl=10, fake-ul=5, jitter, loss, no signal
|
|
score2=$(score_composite "$w2_lat" "10" "5" "$w2_jit" "$w2_loss" "")
|
|
|
|
# Rolling average for stability
|
|
local avg1="$score1" avg2="$score2"
|
|
if [ "$ROLLING_SAMPLES" -gt 1 ]; then
|
|
# Store last N scores
|
|
echo "$score1" >> "$STATE_DIR/score_history_${WAN1_IFACE}"
|
|
echo "$score2" >> "$STATE_DIR/score_history_${WAN2_IFACE}"
|
|
tail -n "$ROLLING_SAMPLES" "$STATE_DIR/score_history_${WAN1_IFACE}" > "$STATE_DIR/score_roll_1"
|
|
tail -n "$ROLLING_SAMPLES" "$STATE_DIR/score_history_${WAN2_IFACE}" > "$STATE_DIR/score_roll_2"
|
|
# Trim
|
|
if [ "$(wc -l < "$STATE_DIR/score_history_${WAN1_IFACE}")" -gt 10 ]; then
|
|
tail -n 10 "$STATE_DIR/score_history_${WAN1_IFACE}" > "${STATE_DIR}/score_history_${WAN1_IFACE}.tmp"
|
|
mv "${STATE_DIR}/score_history_${WAN1_IFACE}.tmp" "$STATE_DIR/score_history_${WAN1_IFACE}"
|
|
fi
|
|
avg1=$(awk '{s+=$1}END{printf "%d",(s/NR+0.5)}' "$STATE_DIR/score_roll_1" 2>/dev/null || echo "$score1")
|
|
avg2=$(awk '{s+=$1}END{printf "%d",(s/NR+0.5)}' "$STATE_DIR/score_roll_2" 2>/dev/null || echo "$score2")
|
|
fi
|
|
|
|
echo "$avg1" > "$STATE_DIR/score_${WAN1_IFACE}"
|
|
echo "$avg2" > "$STATE_DIR/score_${WAN2_IFACE}"
|
|
|
|
log "scores: ${WAN1_IFACE}=${avg1} ${WAN2_IFACE}=${avg2}"
|
|
}
|
|
|
|
# ── Apply weights to SmartWAN ──────────────────────────────────────
|
|
apply_weights() {
|
|
local s1=$(cat "$STATE_DIR/score_${WAN1_IFACE}" 2>/dev/null || echo "50")
|
|
local s2=$(cat "$STATE_DIR/score_${WAN2_IFACE}" 2>/dev/null || echo "50")
|
|
|
|
# WAN1 weight = 100 - WAN2 weight (SmartWAN dw_weight_ratio is WAN1 share)
|
|
# Our wan_weight gives WAN1 share integer 20-80
|
|
local w1=$(wan_weight "$s1" "$s2")
|
|
|
|
# Clamp to configured range
|
|
[ "$w1" -lt "$WEIGHT_MIN" ] && w1="$WEIGHT_MIN"
|
|
[ "$w1" -gt "$WEIGHT_MAX" ] && w1="$WEIGHT_MAX"
|
|
|
|
sw_write_weights "$w1"
|
|
}
|
|
|
|
# ── Main loop ──────────────────────────────────────────────────────
|
|
log "EVENT=DAEMON_START device=$DEVICE_ID version=${SYNO_BALANCE_VERSION:-0.0}"
|
|
|
|
load_config
|
|
|
|
# Verify SmartWAN is in load-balance mode
|
|
if ! sw_is_lb_mode; then
|
|
warn "SmartWAN is not in load-balance mode. Set Load Balancing in Network Center."
|
|
# Don't exit — router may transition to LB mode later
|
|
fi
|
|
|
|
_cycle=0
|
|
while true; do
|
|
_cycle=$((_cycle + 1))
|
|
|
|
# Read SmartWAN state
|
|
if ! sw_read_state >/dev/null 2>&1; then
|
|
warn "cannot read SmartWAN state — waiting for interface config"
|
|
sleep "$CHECK_INTERVAL"
|
|
continue
|
|
fi
|
|
|
|
# Measure both WANs
|
|
measure_wan "$WAN1_IFACE" "$(ip route show dev "$WAN1_IFACE" 2>/dev/null | grep default | awk '{print $3}' | head -1)"
|
|
measure_wan "$WAN2_IFACE" "$(ip route show dev "$WAN2_IFACE" 2>/dev/null | grep default | awk '{print $3}' | head -1)"
|
|
|
|
# Optional: Eyeride signal, Starlink status
|
|
read_eyeride_signal 2>/dev/null || true
|
|
read_starlink_status 2>/dev/null || true
|
|
|
|
# Score + apply
|
|
score_wans
|
|
apply_weights
|
|
|
|
# Save cycle state for telemetry
|
|
echo "$_cycle" > "$STATE_DIR/cycle"
|
|
|
|
sleep "$CHECK_INTERVAL"
|
|
done
|