feat: kit-busrouter v2.0 — complete Synology + GL fleet router platform
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>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
package="syno-balance"
|
||||
version="0.1-0001"
|
||||
description="Intelligent dual-WAN load balancing for Synology SRM routers. Rides on top of SmartWAN — never flushes conntrack, never fights for routing control. Uses the same 6-factor scoring engine as the GL-XE3000 kit-busrouter. Includes Eyeride signal monitoring, Starlink health, and fleet telemetry."
|
||||
maintainer="Keylink IT"
|
||||
arch="noarch"
|
||||
firmware="1.3.1-9346"
|
||||
checkport="no"
|
||||
startable="yes"
|
||||
displayname="KIT Bus Router Balancer"
|
||||
thirdparty="yes"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
# lib-decide.sh — PURE hysteresis + weight-formula decision helpers.
|
||||
# No side effects: args in, stdout/exit-code out. POSIX/ash-safe.
|
||||
|
||||
# Weight for WAN A given scoreA scoreB:
|
||||
# 50 + 2*(a-b), rounded to nearest 10, clamped to 20..80.
|
||||
# e.g. wan_weight 70 70 -> 50 ; wan_weight 80 60 -> 90 -> clamp 80.
|
||||
wan_weight() {
|
||||
awk -v a="$1" -v b="$2" 'BEGIN{
|
||||
w = 50 + (a-b)*2
|
||||
w = int((w+5)/10)*10 # round to nearest 10 (works for w>=-5; clamp handles low end)
|
||||
if (w < 20) w = 20
|
||||
if (w > 80) w = 80
|
||||
print w }'
|
||||
}
|
||||
|
||||
# Exit 0 (changed) if |target-current| >= 10, else exit 1 (within hysteresis band).
|
||||
weight_changed() {
|
||||
[ "$(( $2 > $1 ? $2 - $1 : $1 - $2 ))" -ge 10 ]
|
||||
}
|
||||
|
||||
# Exit 0 if bad_cycles >= threshold (time to switch SIM), else exit 1.
|
||||
sim_should_switch() {
|
||||
[ "$1" -ge "$2" ]
|
||||
}
|
||||
|
||||
# Exit 0 (gateway alive) if at least 2 of 3 probes pass (each arg: 1=pass 0=fail).
|
||||
gw_verdict() {
|
||||
[ "$(( $1 + $2 + $3 ))" -ge 2 ]
|
||||
}
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
# lib-score.sh — PURE per-metric + composite WAN scoring (ported from aiwanbal §2.3).
|
||||
# No side effects: args in, single integer 0..100 on stdout.
|
||||
# POSIX/ash-safe; integer/float math delegated to awk (available on OpenWrt).
|
||||
#
|
||||
# Scoring anchors (spec §2.3):
|
||||
# latency 5ms -> 100 , 200ms -> 0
|
||||
# download 25 -> 100 , 0.5 -> 0 (MB/s)
|
||||
# upload 12.5 -> 100 , 0.5 -> 0 (MB/s)
|
||||
# jitter 1ms -> 100 , 50ms -> 0
|
||||
# loss 0% -> 100 , 40% -> 0
|
||||
# signal -65 -> 100 , -95 -> 0 (dBm)
|
||||
|
||||
# clamp a numeric value to 0..100 and round half-up to an integer
|
||||
_clamp() {
|
||||
awk -v v="$1" 'BEGIN{ if(v<0)v=0; if(v>100)v=100; printf "%d", (v+0.5) }'
|
||||
}
|
||||
|
||||
# read one value from stdin and clamp/round it (pipe helper)
|
||||
_pipeclamp() { read v; _clamp "$v"; }
|
||||
|
||||
score_latency() { awk -v x="$1" 'BEGIN{ print 100*(200-x)/(200-5) }' | _pipeclamp; }
|
||||
score_download() { awk -v x="$1" 'BEGIN{ print 100*(x-0.5)/(25-0.5) }' | _pipeclamp; }
|
||||
score_upload() { awk -v x="$1" 'BEGIN{ print 100*(x-0.5)/(12.5-0.5) }' | _pipeclamp; }
|
||||
score_jitter() { awk -v x="$1" 'BEGIN{ print 100*(50-x)/(50-1) }' | _pipeclamp; }
|
||||
score_loss() { awk -v x="$1" 'BEGIN{ print 100*(40-x)/40 }' | _pipeclamp; }
|
||||
score_signal() { awk -v x="$1" 'BEGIN{ print 100*(x-(-95))/((-65)-(-95)) }' | _pipeclamp; }
|
||||
|
||||
# Weighted 6-factor composite score.
|
||||
# args: lat dl up jitter loss signal(optional)
|
||||
# weights: latency35 dl25 up10 jit10 loss10 sig10.
|
||||
# When signal is empty (no modem), its 10 points redistribute to latency(+5)
|
||||
# and download(+5) so the remaining weights still total 100.
|
||||
score_composite() {
|
||||
sl=$(score_latency "$1"); sd=$(score_download "$2"); su=$(score_upload "$3")
|
||||
sj=$(score_jitter "$4"); sp=$(score_loss "$5")
|
||||
if [ -n "$6" ]; then
|
||||
ss=$(score_signal "$6"); wsig=10; wlat=35; wdl=25
|
||||
else
|
||||
ss=0; wsig=0; wlat=40; wdl=30 # redistribute signal weight
|
||||
fi
|
||||
awk -v sl="$sl" -v sd="$sd" -v su="$su" -v sj="$sj" -v sp="$sp" -v ss="$ss" \
|
||||
-v wlat="$wlat" -v wdl="$wdl" -v wsig="$wsig" 'BEGIN{
|
||||
t=(sl*wlat + sd*wdl + su*10 + sj*10 + sp*10 + ss*wsig)/100
|
||||
printf "%d",(t+0.5) }'
|
||||
}
|
||||
|
||||
# Rolling average of N samples (integer, round half-up). Args: sample...
|
||||
rolling_avg() {
|
||||
awk 'BEGIN{ n=ARGC-1; s=0; for(i=1;i<ARGC;i++) s+=ARGV[i]; printf "%d",(s/n+0.5) }' "$@"
|
||||
}
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/bin/sh
|
||||
# smartwan-adapter.sh — Read SmartWAN state and write dw_weight_ratio.
|
||||
# NEVER calls loadbalance lb-enable. NEVER calls loadbalance at all.
|
||||
# Rides entirely on top of SmartWAN — does not own routing.
|
||||
#
|
||||
# Public API:
|
||||
# sw_read_state read SmartWAN mode, interfaces, current weights
|
||||
# sw_read_wan_state <iface> read per-WAN state: operstate, gateway, weight
|
||||
# sw_write_weights <w1> <w2> write dw_weight_ratio (50=balanced, 20/80=biased)
|
||||
# sw_signal_reload touch SmartWAN to re-read config (light touch, no conntrack flush)
|
||||
#
|
||||
# Config file: /usr/syno/etc/smartwan/smartwan.conf
|
||||
# Weight field: dw_weight_ratio = <0-100> (WAN1 share %, WAN2 = 100-WAN1)
|
||||
# - dw_weight_ratio appears twice in the conf (once for each ifname section)
|
||||
# - We update BOTH occurrences
|
||||
# - 50/50 = balanced, 70 = 70% WAN1 / 30% WAN2
|
||||
|
||||
SMARTWAN_CONF="/usr/syno/etc/smartwan/smartwan.conf"
|
||||
STATE_DIR="${STATE_DIR:-/tmp/syno-balance}"
|
||||
LOG_TAG="${LOG_TAG:-syno-balance}"
|
||||
|
||||
log() { logger -t "$LOG_TAG" -p local0.warn "$*"; }
|
||||
warn() { logger -t "$LOG_TAG" -p local0.warn "WARNING: $*"; }
|
||||
|
||||
# ── Read SmartWAN global state ──────────────────────────────────────
|
||||
sw_read_state() {
|
||||
[ -f "$SMARTWAN_CONF" ] || { warn "smartwan.conf not found"; return 1; }
|
||||
. "$SMARTWAN_CONF" 2>/dev/null
|
||||
|
||||
echo "MODE=${smartwan_mode:-unknown}"
|
||||
echo "WAN1_IFNAME=${smartwan_ifname_1:-wan}"
|
||||
echo "WAN2_IFNAME=${smartwan_ifname_2:-lan1}"
|
||||
echo "WAN1_WEIGHT=${dw_weight_ratio:-50}"
|
||||
}
|
||||
|
||||
# ── Read a single WAN's runtime state ───────────────────────────────
|
||||
# Returns: STATE=up|down GW=x.x.x.x WEIGHT=N
|
||||
sw_read_wan_state() {
|
||||
local iface="$1"
|
||||
[ -n "$iface" ] || return 1
|
||||
|
||||
# Physical link state
|
||||
local operstate="down"
|
||||
operstate=$(cat "/sys/class/net/${iface}/operstate" 2>/dev/null || echo "unknown")
|
||||
|
||||
# Gateway from routing table
|
||||
local gw=""
|
||||
gw=$(ip route show dev "$iface" 2>/dev/null | grep default | awk '{print $3}' | head -1)
|
||||
[ -z "$gw" ] && gw=$(ip route show dev "$iface" 2>/dev/null | awk '/via/ {print $3}' | head -1)
|
||||
|
||||
# Current weight from smartwan.conf
|
||||
local weight=0
|
||||
[ -f "$SMARTWAN_CONF" ] && . "$SMARTWAN_CONF" 2>/dev/null
|
||||
weight="${dw_weight_ratio:-50}"
|
||||
|
||||
echo "IFACE=${iface} STATE=${operstate} GW=${gw:-none} WEIGHT=${weight}"
|
||||
}
|
||||
|
||||
# ── Write dw_weight_ratio for both WANs ─────────────────────────────
|
||||
# $1: WAN1 weight (0-100, WAN2 = 100-WAN1)
|
||||
# Sets dw_weight_ratio in smartwan.conf for both ifname sections.
|
||||
# Does NOT call loadbalance. SmartWAN re-reads on its own cycle.
|
||||
sw_write_weights() {
|
||||
local w1="$1"
|
||||
case "$w1" in
|
||||
''|*[!0-9]*) warn "invalid weight: $w1"; return 1 ;;
|
||||
esac
|
||||
[ "$w1" -lt 0 ] && w1=0
|
||||
[ "$w1" -gt 100 ] && w1=100
|
||||
|
||||
local current=50
|
||||
[ -f "$SMARTWAN_CONF" ] && . "$SMARTWAN_CONF" 2>/dev/null
|
||||
current="${dw_weight_ratio:-50}"
|
||||
|
||||
# Only write if changed by >= 10 (hysteresis via lib-decide.sh)
|
||||
local diff=$(( w1 > current ? w1 - current : current - w1 ))
|
||||
if [ "$diff" -lt 10 ]; then
|
||||
return 0 # within hysteresis band — no-op
|
||||
fi
|
||||
|
||||
# Write to smartwan.conf using sed — safe, atomic-ish
|
||||
# SmartWAN has dw_weight_ratio on both ifname sections
|
||||
if [ -f "$SMARTWAN_CONF" ]; then
|
||||
sed -i "s/^dw_weight_ratio=.*/dw_weight_ratio=${w1}/" "$SMARTWAN_CONF" 2>/dev/null
|
||||
fi
|
||||
|
||||
log "smartwan: dw_weight_ratio ${current} -> ${w1} (WAN1=${w1}% WAN2=$((100-w1))%)"
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "$w1" > "$STATE_DIR/sw_weight_written"
|
||||
echo "$(date +%s)" > "$STATE_DIR/sw_weight_time"
|
||||
}
|
||||
|
||||
# ── Light touch — signal SmartWAN to re-read config ────────────────
|
||||
# Uses synoservice if available. NEVER calls loadbalance lb-enable.
|
||||
sw_signal_reload() {
|
||||
# SRM 1.3+ has synoservice to restart individual services
|
||||
if command -v synoservice >/dev/null 2>&1; then
|
||||
synoservice --restart smartwan 2>/dev/null && \
|
||||
{ log "smartwan: light-reload via synoservice"; return 0; }
|
||||
fi
|
||||
|
||||
# Fallback: just touch the config — SmartWAN polls it
|
||||
touch "$SMARTWAN_CONF" 2>/dev/null
|
||||
log "smartwan: config touched for polling re-read"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Check if SmartWAN is in load-balance mode ───────────────────────
|
||||
sw_is_lb_mode() {
|
||||
[ -f "$SMARTWAN_CONF" ] || return 1
|
||||
local mode=""
|
||||
mode=$(grep '^smartwan_mode=' "$SMARTWAN_CONF" 2>/dev/null | cut -d= -f2)
|
||||
case "$mode" in
|
||||
loadbalance|loadbalancing_failover) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
Executable
+226
@@ -0,0 +1,226 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,35 @@
|
||||
# syno-balance — Bus Router WAN Balancer Configuration
|
||||
# Rides on top of SmartWAN. NEVER calls loadbalance.
|
||||
|
||||
# ── WAN Interfaces ──────────────────────────────────────────────────
|
||||
WAN1_IFACE="eth0"
|
||||
WAN2_IFACE="eth2"
|
||||
|
||||
# ── Check Interval (seconds) ────────────────────────────────────────
|
||||
CHECK_INTERVAL=30
|
||||
|
||||
# ── Speed Test ─────────────────────────────────────────────────────
|
||||
SPEEDTEST_AUTO_ENABLED=0
|
||||
|
||||
# ── Eyeride (WAN1 cellular via Eyeride) ────────────────────────────
|
||||
EYERIDE_ENABLE=0
|
||||
EYERIDE_IP="192.168.10.1"
|
||||
EYERIDE_PORT="8080"
|
||||
EYERIDE_USER="root"
|
||||
EYERIDE_PASSWORD=""
|
||||
|
||||
# ── Starlink (WAN2) ─────────────────────────────────────────────────
|
||||
STARLINK_ENABLE=0
|
||||
STARLINK_IP="192.168.100.1"
|
||||
STARLINK_PORT="9200"
|
||||
|
||||
# ── Scoring ────────────────────────────────────────────────────────
|
||||
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 ──────────────────────────────────────────────────────
|
||||
TELEMETRY_HUB="http://167.172.237.162:8080/api/telemetry"
|
||||
TELEMETRY_BUFFER_DIR="/tmp/syno-balance/telemetry-buf"
|
||||
Reference in New Issue
Block a user