6 - Shell - partie 3
set_stp_ip.sh
set_stp_ip.sh
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# set_stp_ip.sh - donne a CETTE VM son adresse SS7 sur le reseau en pont
#
# Une seule ISO, deux roles. Le script demande d'abord LEQUEL, puis les
# adresses - parce que ce ne sont pas les memes des deux cotes :
#
# inter-STP (hub) son IP de pont, + l'IP de chaque osmo-operator-N
# osmo-operator-N son IP de pont, + l'IP de l'inter-STP
#
# osmo-operator-1 192.168.1.11 ─┐
# osmo-operator-2 192.168.1.12 ─┼─ M3UA/SCTP 2908 ─► inter-STP 192.168.1.10
# osmo-operator-3 192.168.1.13 ─┘ PC 0.0.0
#
# POURQUOI LE PONT ET PAS L'HOTE-SEUL
# Les configs de l'image visent 192.168.56.1 (host-only VirtualBox) ou
# 172.20.0.10 (reseau docker). Ni l'un ni l'autre n'existe quand les VM sont en
# "acces par pont" : elles sont sur le LAN, en 192.168.1.X. Un ASP qui vise
# une adresse absente du segment ne dit rien d'autre que "connection
# refused" cote noeud - le hub, lui, ne voit jamais personne arriver.
#
# CE QUE LE SCRIPT ECRIT
# hub /etc/osmocom/osmo-stp-interop.cfg local-ip = son IP de pont
# /etc/osmo-wan.conf la table des operateurs
# /etc/osmo-role OSMO_ROLE=interstp
# operateur /etc/osmocom/osmo-stp.cfg local-ip = la sienne
# remote-ip = celle du hub
# /etc/osmocom/osmo-{msc,bsc}.cfg point codes du plan WAN
# /etc/osmo-wan.conf, /etc/osmo-role
#
# Idempotent : relancer avec d'autres adresses les remplace, il n'y a pas
# d'etat a nettoyer entre deux passages.
#
# Usage :
# sudo ./set_stp_ip.sh menu : role, puis adresses
# sudo ./set_stp_ip.sh --inter --ip 192.168.1.10 --ips "192.168.1.11 192.168.1.12"
# sudo ./set_stp_ip.sh --operator --node 1 --ip 192.168.1.11 --hub-ip 192.168.1.10
# sudo ./set_stp_ip.sh --show etat actuel, sans rien modifier
#
# --inter | --operator role (sinon : demande)
# --ip ADRESSE IP de pont de CETTE machine
# --ips "a b c" hub : IP des operateurs, dans l'ordre
# --hub-ip ADRESSE operateur : IP de l'inter-STP
# --node N operateur : son numero de noeud (1-9)
# --op K COMBIEN d'operateurs (PLMN) ce noeud porte (defaut 1)
# --op-index K LEQUEL de ces operateurs est celui de ce noeud, dans
# le point code 1.<noeud><op>.<role> (1 a 9, pas plus
# que --op ; defaut 1)
# --iface NOM interface ou poser l'adresse (defaut : detectee)
# --port N port M3UA (defaut 2908)
# --no-ip ne touche pas a la configuration reseau
# --defaut la configuration du banc, sans une question : role
# deduit, IP de pont deduite, hub 192.168.1.49, noeud
# lu dans /etc/osmo-role ou la table WAN.
# --restart relance la pile apres reecriture
# --dry-run montre ce qui serait ecrit
# ══════════════════════════════════════════════════════════════════════════════
set -uo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF_DIR="${CONF_DIR:-/etc/osmocom}"
ROLE_FILE="${ROLE_FILE:-/etc/osmo-role}"
INTEROP_CFG="${CONF_DIR}/osmo-stp-interop.cfg"
# wan_detect_local_ip, _wan_ask, la table WAN et sa validation vivent ici. Les
# redefinir localement, c'est se garantir qu'un jour ce script et les lanceurs
# ne designeront plus la meme carte ni la meme table.
# shellcheck source=network/wan-nodes.sh
. "$HERE/network/wan-nodes.sh"
ROLE=""
SELF_IP=""
HUB_IP=""
IPS=""
NODE=""
OPS=1
# --op COMPTE les operateurs : il dimensionne le pare-feu, les blocs "as" du hub
# et la table WAN. Il ne dit pas LEQUEL est celui de ce noeud - et c'est pourtant
# lui qui partait en --op de set-node-id.sh, ou l'option est un INDICE. Avec
# --op 2 sur le noeud 3, l'ecran annoncait STP 1.31.2 pendant que les fichiers
# recevaient 1.32.2 et le routing context 3250 : l'ASP se presentait au hub sous
# une identite que sa table ne connait pas.
OP_INDEX=1
IFACE=""
PORT=2908
SET_IP=1
RESTART=0
# ── La configuration du banc, sans une question ─────────────────────────────
# --defaut repond a tout ce que ce script demande d'habitude, a partir de ce
# que la machine sait deja d'elle-meme :
# role interstp si son adresse EST celle du hub, operateur sinon ;
# son IP celle du pont, deduite ;
# le hub 192.168.1.49 ;
# le noeud /etc/osmo-role, puis la table WAN, puis 1.
# Chaque valeur reste surchargeable : --defaut --node 3 gagne sur la deduction.
USE_DEFAULTS=0
DEFAULT_HUB_IP="192.168.1.49"
DRY=0
SHOW=0
while [ $# -gt 0 ]; do
case "$1" in
--inter|--interstp|--hub) ROLE="interstp" ;;
--operator|--op-node) ROLE="operator" ;;
--role) ROLE="${2:-}"; shift ;;
--role=*) ROLE="${1#*=}" ;;
--ip) SELF_IP="${2:-}"; shift ;;
--ip=*) SELF_IP="${1#*=}" ;;
--hub-ip) HUB_IP="${2:-}"; shift ;;
--hub-ip=*) HUB_IP="${1#*=}" ;;
--ips) IPS="${2:-}"; shift ;;
--ips=*) IPS="${1#*=}" ;;
--node) NODE="${2:-}"; shift ;;
--node=*) NODE="${1#*=}" ;;
--op) OPS="${2:-1}"; shift ;;
--op=*) OPS="${1#*=}" ;;
--ops) OPS="${2:-1}"; shift ;;
--ops=*) OPS="${1#*=}" ;;
--op-index) OP_INDEX="${2:-1}"; shift ;;
--op-index=*) OP_INDEX="${1#*=}" ;;
--iface) IFACE="${2:-}"; shift ;;
--iface=*) IFACE="${1#*=}" ;;
--port) PORT="${2:-2908}"; shift ;;
--port=*) PORT="${1#*=}" ;;
--conf-dir) CONF_DIR="${2:-}"; INTEROP_CFG="${CONF_DIR}/osmo-stp-interop.cfg"; shift ;;
--conf-dir=*) CONF_DIR="${1#*=}"; INTEROP_CFG="${CONF_DIR}/osmo-stp-interop.cfg" ;;
--defaut|--default) USE_DEFAULTS=1 ;;
--no-ip) SET_IP=0 ;;
--restart) RESTART=1 ;;
--dry-run) DRY=1 ;;
--show) SHOW=1 ;;
-h|--help) sed -n '2,53p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo -e "${RED}option inconnue : $1${NC}" >&2; exit 2 ;;
esac
shift
done
banner() {
echo -e "${CYAN}${BOLD}"
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ set_stp_ip.sh - adressage SS7 sur le reseau en pont ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
is_ipv4() {
local ip="$1" o
[[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
for o in ${ip//./ }; do [ "$o" -le 255 ] || return 1; done
return 0
}
need_ipv4() { # $1=valeur $2=ce que c'est
is_ipv4 "$1" || { echo -e "${RED}$2 : '$1' n'est pas une IPv4${NC}" >&2; exit 2; }
}
# ── L'adresse de pont de cette VM ────────────────────────────────────────────
# wan_detect_local_ip privilegie le host-only VirtualBox (192.168.56-63), qui
# est le bon choix pour le banc mais pas ici : en acces par pont, la VM est sur
# le LAN de l'hote. On cherche donc d'abord une adresse de LAN privee qui ne
# soit ni le NAT VirtualBox (10.0.2.x), ni un alias du plan docker (172.20.x),
# ni le host-only - et on ne retombe sur wan_detect_local_ip qu'a defaut.
bridge_ip() {
local addrs a
addrs="$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)"
for a in $addrs; do
case "$a" in
192.168.5[6-9].*|192.168.6[0-3].*) ;; # host-only : pas le pont
192.168.*) printf '%s' "$a"; return 0 ;;
esac
done
for a in $addrs; do
case "$a" in
10.0.2.*|172.20.*|127.*|169.254.*) ;;
10.*|172.1[6-9].*|172.2[0-9].*|172.3[01].*) printf '%s' "$a"; return 0 ;;
esac
done
wan_detect_local_ip 2>/dev/null || return 1
}
# L'interface qui porte deja ce /24 : c'est celle du segment vise. A defaut, la
# premiere interface globale - poser l'adresse sur la mauvaise carte donne un
# ASP qui part par une route qui ne mene pas au pair.
iface_for() { # $1=ip
local ip="$1" f
f="$(ip -4 -o addr show scope global 2>/dev/null \
| awk -v pfx="${ip%.*}." '$4 ~ "^"pfx {print $2; exit}')"
[ -z "$f" ] && f="$(ip -4 -o addr show scope global 2>/dev/null | awk 'NR==1{print $2}')"
printf '%s' "$f"
}
ensure_ip() { # $1=ip - la pose si elle manque
local ip="$1" dev
[ "$SET_IP" = "1" ] || { echo -e " ${YELLOW}-${NC} --no-ip : reseau inchange"; return 0; }
if ip -4 addr show 2>/dev/null | grep -q "inet ${ip}/"; then
echo -e " ${GREEN}✓${NC} ${CYAN}${ip}${NC} deja presente sur cette machine"
return 0
fi
dev="${IFACE:-$(iface_for "$ip")}"
if [ "$DRY" = "1" ]; then
echo -e " [dry-run] ip addr add ${ip}/24 dev ${dev:-?}"; return 0
fi
if [ -n "$dev" ] && ip addr add "${ip}/24" dev "$dev" 2>/dev/null; then
echo -e " ${GREEN}✓${NC} ${CYAN}${ip}/24${NC} posee sur ${dev}"
else
echo -e " ${YELLOW}⚠${NC} impossible de poser ${ip} (interface ${dev:-?})"
echo -e " Tant qu'elle manque, aucun ASP ne peut s'attacher."
fi
}
write_role() { # $1=role $2=hub-ip $3=node (vide pour le hub)
if [ "$DRY" = "1" ]; then
echo -e " [dry-run] ${ROLE_FILE} : OSMO_ROLE=$1 OSMO_HUB_IP=$2 ${3:+OSMO_WAN_NODE=$3}"
return 0
fi
{
printf '# %s - genere par set_stp_ip.sh\n' "$ROLE_FILE"
printf 'OSMO_ROLE=%s\n' "$1"
[ -n "${3:-}" ] && printf 'OSMO_WAN_NODE=%s\n' "$3"
printf 'OSMO_HUB_IP=%s\n' "$2"
} > "$ROLE_FILE"
echo -e " ${GREEN}✓${NC} ${CYAN}${ROLE_FILE}${NC}"
}
role_of_file() { awk -F= '/^OSMO_ROLE=/{gsub(/[ \r\t]/,"",$2); print $2}' "$ROLE_FILE" 2>/dev/null | tail -1; }
hub_of_file() { awk -F= '/^OSMO_HUB_IP=/{gsub(/[ \r\t]/,"",$2); print $2}' "$ROLE_FILE" 2>/dev/null | tail -1; }
node_of_file() { awk -F= '/^OSMO_WAN_NODE=/{gsub(/[ \r\t]/,"",$2); print $2}' "$ROLE_FILE" 2>/dev/null | tail -1; }
# ══════════════════════════════════════════════════════════════════════════════
# --show : ce que cette machine croit etre, aujourd'hui
# ══════════════════════════════════════════════════════════════════════════════
if [ "$SHOW" = "1" ]; then
banner
r="$(role_of_file)"; h="$(hub_of_file)"; n="$(node_of_file)"
echo -e " ${BOLD}Role${NC} : ${CYAN}${r:-inconnu}${NC}${n:+ noeud ${CYAN}${n}${NC}}"
echo -e " ${BOLD}IP de pont${NC}: ${CYAN}$(bridge_ip || echo '-')${NC}"
echo -e " ${BOLD}Inter-STP${NC} : ${CYAN}${h:--}${NC}"
echo ""
if [ -r "$INTEROP_CFG" ]; then
echo -e " ${BOLD}$(basename "$INTEROP_CFG")${NC}"
awk '/^ *listen m3ua/{f=1} f&&/^ *(local-ip|listen)/{print " "$0} f&&/^!/{f=0}' "$INTEROP_CFG"
fi
for f in "$CONF_DIR/osmo-stp.cfg" "$CONF_DIR/osmo-msc.cfg" "$CONF_DIR/osmo-bsc.cfg"; do
[ -r "$f" ] || continue
pc="$(awk '/^cs7 instance/{c=1} c && /^ *point-code /{print $2; exit}' "$f")"
lo="$(awk '/asp asp-to-inter/{f=1} f&&/local-ip/{print $2; exit}' "$f")"
re="$(awk '/asp asp-to-inter/{f=1} f&&/remote-ip/{print $2; exit}' "$f")"
printf ' %-16s PC %-10s local-ip %-16s remote-ip %s\n' \
"$(basename "$f")" "${pc:--}" "${lo:--}" "${re:--}"
done
echo ""
[ -r "$WAN_CONF_FILE" ] && { echo -e " ${BOLD}${WAN_CONF_FILE}${NC}"; sed 's/^/ /' "$WAN_CONF_FILE"; }
exit 0
fi
if [ "$USE_DEFAULTS" = "1" ]; then
[ -n "$HUB_IP" ] || HUB_IP="$DEFAULT_HUB_IP"
[ -n "$SELF_IP" ] || SELF_IP="$(bridge_ip || true)"
if [ -z "$ROLE" ]; then
if [ "$SELF_IP" = "$HUB_IP" ]; then ROLE="interstp"; else ROLE="operator"; fi
fi
if [ -z "$NODE" ] && [ "$ROLE" = "operator" ]; then
NODE="$(node_of_file)"
if [ -z "$NODE" ] && [ -r "$WAN_CONF_FILE" ]; then
NODE="$(awk -F\" '/WAN_NODES=/{print $2}' "$WAN_CONF_FILE" 2>/dev/null \
| tr ' ' '\n' | awk -F: -v ip="$SELF_IP" '$2==ip{print $1; exit}')"
fi
[ -n "$NODE" ] || NODE=1
fi
if [ -z "$IPS" ] && [ "$ROLE" = "interstp" ] && [ -r "$WAN_CONF_FILE" ]; then
IPS="$(awk -F\" '/WAN_NODES=/{print $2}' "$WAN_CONF_FILE" 2>/dev/null \
| tr ' ' '\n' | awk -F: '{print $2}' | tr '\n' ' ')"
fi
fi
banner
[ "$DRY" = "1" ] || [ "$(id -u)" -eq 0 ] \
|| { echo -e "${RED}Root requis (ecriture dans ${CONF_DIR} et sur l'interface).${NC}" >&2; exit 1; }
# ══════════════════════════════════════════════════════════════════════════════
# 1. QUI EST CETTE MACHINE ?
# ══════════════════════════════════════════════════════════════════════════════
# Le role decide de tout le reste : le hub ecoute et connait ses operateurs, un
# operateur s'attache et ne connait que le hub. Se tromper ici produit une
# machine qui ecoute sans que personne l'appelle, ou qui appelle une adresse
# qui n'ecoute pas.
if [ -z "$ROLE" ]; then
def_role="$(role_of_file)"; [ -n "$def_role" ] || def_role="operator"
if [ -t 0 ]; then
echo -e "${BOLD}Role de cette machine${NC}"
echo -e " ${CYAN}1${NC}) inter-STP - le hub SS7, PC 0.0.0, celui que tous appellent"
echo -e " ${CYAN}2${NC}) operateur - osmo-operator-N, qui s'attache au hub"
echo ""
def_n=2; [ "$def_role" = "interstp" ] && def_n=1
ans="$(_wan_ask "Role" "1 = inter-STP, 2 = operateur :" "$def_n")" || exit 1
case "$ans" in
1|inter|interstp|hub) ROLE="interstp" ;;
2|op|operator|operateur|operateur) ROLE="operator" ;;
*) echo -e "${RED}reponse invalide : '$ans'${NC}" >&2; exit 2 ;;
esac
else
ROLE="$def_role"
echo -e " ${YELLOW}Pas de terminal - role repris de ${ROLE_FILE} : ${ROLE}${NC}"
fi
fi
case "$ROLE" in
interstp|inter|hub) ROLE="interstp" ;;
operator|op) ROLE="operator" ;;
*) echo -e "${RED}--role : interstp ou operator${NC}" >&2; exit 2 ;;
esac
[[ "$OPS" =~ ^[1-9]$ ]] || { echo -e "${RED}--op : un chiffre de 1 a 9${NC}" >&2; exit 2; }
[[ "$OP_INDEX" =~ ^[1-9]$ ]] || { echo -e "${RED}--op-index : un chiffre de 1 a 9${NC}" >&2; exit 2; }
# Un indice au-dela du compteur designe un operateur que ce noeud ne porte pas :
# le point code partirait dans une plage dont personne n'a la route.
[ "$OP_INDEX" -le "$OPS" ] \
|| { echo -e "${RED}--op-index ${OP_INDEX} : ce noeud n'annonce que ${OPS} operateur(s) (--op)${NC}" >&2; exit 2; }
DETECTED="$(bridge_ip || true)"
echo -e "${BOLD}Role :${NC} ${CYAN}${ROLE}${NC}${DETECTED:+ IP de pont detectee : ${CYAN}${DETECTED}${NC}}"
echo ""
# ══════════════════════════════════════════════════════════════════════════════
# 2A. INTER-STP - son IP de pont, et celles des osmo-operator-N
# ══════════════════════════════════════════════════════════════════════════════
if [ "$ROLE" = "interstp" ]; then
if [ -z "$SELF_IP" ]; then
def="${DETECTED:-192.168.1.10}"
SELF_IP="$(_wan_ask "Inter-STP" "IP de pont de CE hub :" "$def")" || exit 1
fi
need_ipv4 "$SELF_IP" "IP du hub"
# Les IP des operateurs. Le hub n'en a pas besoin pour ROUTER - il ecoute et
# les ASP viennent a lui, c'est le point code qui decide du reste. Il en a
# besoin pour n'ouvrir son pare-feu qu'a eux, pour ecrire la table que
# liront les noeuds, et pour que --status puisse dire QUI manque : sans la
# liste, un noeud absent est indiscernable d'un noeud qui n'existe pas.
WAN_IP=(); WAN_IND=(); WAN_NODE_LIST=(); i=0
if [ -n "$IPS" ]; then
for ip in ${IPS//,/ }; do
i=$((i+1)); need_ipv4 "$ip" "IP de l'operateur $i"
WAN_IP[$i]="$ip"; WAN_IND[$i]="$(wan_default_ind "$i")"; WAN_NODE_LIST+=("$i")
done
else
old_n=0
[ -r "$WAN_CONF_FILE" ] && { wan_nodes_load "$WAN_CONF_FILE" >/dev/null 2>&1 && old_n="$WAN_NODE_COUNT"; }
declare -A old_ip=()
for k in ${WAN_NODE_LIST[@]+"${WAN_NODE_LIST[@]}"}; do old_ip[$k]="${WAN_IP[$k]}"; done
WAN_IP=(); WAN_IND=(); WAN_NODE_LIST=()
n="$(_wan_ask "Inter-STP" "Nombre d'osmo-operator (1-9) :" "$([ "$old_n" -ge 1 ] && echo "$old_n" || echo 3)")" || exit 1
[[ "$n" =~ ^[1-9]$ ]] || { echo -e "${RED}nombre invalide : $n${NC}" >&2; exit 2; }
for i in $(seq 1 "$n"); do
# Defaut aligne sur le plan du banc : osmo-operator-N a .1N sur le
# meme /24 que le hub.
def="${old_ip[$i]:-${SELF_IP%.*}.$((10 + i))}"
ip="$(_wan_ask "osmo-operator-$i" "IP de pont de osmo-operator-$i :" "$def")" || exit 1
need_ipv4 "$ip" "IP de osmo-operator-$i"
WAN_IP[$i]="$ip"; WAN_IND[$i]="$(wan_default_ind "$i")"; WAN_NODE_LIST+=("$i")
done
fi
NODES="${#WAN_NODE_LIST[@]}"
[ "$NODES" -ge 1 ] || { echo -e "${RED}aucun operateur saisi${NC}" >&2; exit 2; }
# Le hub ne porte aucun operateur : il n'est pas un noeud de la table. On note
# 0 - que wan_nodes_validate accepte - plutot qu'un numero qui le ferait
# ressembler a un noeud et lui volerait un point code.
WAN_NODE_ID=0; WAN_NODE_COUNT="$NODES"; WAN_OPS="$OPS"
wan_nodes_validate || exit 2
echo ""
echo -e " Hub ${CYAN}${SELF_IP}:${PORT}${NC} PC ${CYAN}0.0.0${NC}"
for i in "${WAN_NODE_LIST[@]}"; do
printf ' osmo-operator-%s %-15s STP %s MSC %s BSC %s\n' \
"$i" "${WAN_IP[$i]}" "1.${i}1.2" "1.${i}1.1" "1.${i}1.3"
done
echo ""
ensure_ip "$SELF_IP"
# ── La config du hub ────────────────────────────────────────────────────
# local-ip 0.0.0.0 marche, mais annonce dans l'INIT SCTP toutes les adresses
# de la machine - NAT 10.0.2.15 et alias 172.20.x compris. Les noeuds tentent
# alors des chemins qui, vus d'eux, ne menent nulle part. Une adresse unique
# et juste vaut mieux qu'un multi-homing dont trois branches sur quatre sont
# mortes.
if [ "$DRY" = "1" ]; then
echo -e " [dry-run] ${INTEROP_CFG} : listen m3ua ${PORT} / local-ip ${SELF_IP}"
else
mkdir -p "$CONF_DIR"
# Regeneree a chaque passage, comme le fait start-interstp.sh : ce
# fichier est entierement derive de (NODES, OPS): le garder tel quel
# apres un changement de nombre d'operateurs laisserait des blocs "as"
# pour des noeuds disparus et aucun pour le nouveau - un ASP qui arrive
# sans "as" correspondant est refuse, sans que rien ne le dise.
bash "$HERE/helpers/create_interop.sh" --wan "$NODES" "$OPS" "$INTEROP_CFG" || exit 1
tmp="$(mktemp)"
awk -v hub="$SELF_IP" -v port="$PORT" '
/^ *listen m3ua/ { sub(/listen m3ua .*/, "listen m3ua " port); in_l=1; print; next }
in_l && /^ *local-ip / { sub(/local-ip .*/, "local-ip " hub); print; next }
in_l && /^(!|[a-z])/ { in_l=0 }
{ print }
' "$INTEROP_CFG" > "$tmp"
# cat plutot que mv : sur l'ISO comme dans un conteneur ces fichiers
# peuvent etre des points de montage, et rename() y echoue.
cat "$tmp" > "$INTEROP_CFG"; rm -f "$tmp"
echo -e " ${GREEN}✓${NC} $(basename "$INTEROP_CFG") - ecoute ${CYAN}${SELF_IP}:${PORT}${NC}"
fi
if [ "$DRY" = "1" ]; then
echo -e " [dry-run] ${WAN_CONF_FILE} : $(wan_nodes_spec)"
else
WAN_AUTO=0 wan_nodes_save "$WAN_CONF_FILE" \
&& echo -e " ${GREEN}✓${NC} table ecrite dans ${CYAN}${WAN_CONF_FILE}${NC}"
for i in "${WAN_NODE_LIST[@]}"; do
bash "$HERE/network/firewall-wan.sh" "${WAN_IP[$i]}" "$OPS" >/dev/null 2>&1 || true
done
echo -e " ${GREEN}✓${NC} pare-feu ouvert pour ${NODES} operateur(s)"
fi
write_role interstp "$SELF_IP" ""
echo ""
echo -e " ${BOLD}Cote operateurs :${NC} sur chaque VM osmo-operator-N, lancer"
echo -e " ${CYAN}sudo ./set_stp_ip.sh --operator --node N --ip <son IP> --hub-ip ${SELF_IP}${NC}"
if [ "$RESTART" = "1" ] && [ "$DRY" != "1" ]; then
echo ""
if [ -x "$HERE/start-interstp.sh" ]; then
"$HERE/start-interstp.sh" --ip "$SELF_IP" --nodes "$NODES" --ops "$OPS" --port "$PORT" --no-ip
else
systemctl restart osmo-interstp 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} osmo-interstp relance" \
|| echo -e " ${YELLOW}⚠${NC} relancez le hub a la main : ${CYAN}./start-interstp.sh${NC}"
fi
else
echo -e " ${YELLOW}Le hub deja lance garde l'ancienne adresse :${NC} ${CYAN}./start-interstp.sh${NC}"
fi
exit 0
fi
# ══════════════════════════════════════════════════════════════════════════════
# 2B. OSMO-OPERATOR-N - son IP de pont, et celle de l'inter-STP
# ══════════════════════════════════════════════════════════════════════════════
if [ -z "$NODE" ]; then
def="$(node_of_file)"; [ -n "$def" ] || def=1
NODE="$(_wan_ask "osmo-operator" "Numero de CE noeud operateur (1-9) :" "$def")" || exit 1
fi
[[ "$NODE" =~ ^[1-9]$ ]] || { echo -e "${RED}--node : un chiffre de 1 a 9${NC}" >&2; exit 2; }
if [ -z "$SELF_IP" ]; then
def="${DETECTED:-192.168.1.$((10 + NODE))}"
SELF_IP="$(_wan_ask "osmo-operator-$NODE" "IP de pont de CE noeud :" "$def")" || exit 1
fi
need_ipv4 "$SELF_IP" "IP de ce noeud"
if [ -z "$HUB_IP" ]; then
def="$(hub_of_file)"
# Un hub en 172.20.x (plan docker) ou 192.168.56.x (host-only) herite de
# l'image n'existe pas sur le pont : le proposer comme defaut ferait
# reconduire l'adresse meme qu'on est venu corriger.
case "$def" in ""|172.20.*|192.168.5[6-9].*|192.168.6[0-3].*) def="${SELF_IP%.*}.10" ;; esac
HUB_IP="$(_wan_ask "Inter-STP" "IP de pont de l'inter-STP :" "$def")" || exit 1
fi
need_ipv4 "$HUB_IP" "IP de l'inter-STP"
if [ "$SELF_IP" = "$HUB_IP" ]; then
echo -e "${RED}Ce noeud et l'inter-STP portent la meme adresse (${SELF_IP}).${NC}" >&2
echo -e "${RED}L'ASP s'attacherait a lui-meme - corrigez l'une des deux.${NC}" >&2
exit 2
fi
if [ "${SELF_IP%.*}" != "${HUB_IP%.*}" ]; then
echo -e " ${YELLOW}⚠${NC} ${SELF_IP} et ${HUB_IP} ne sont pas sur le meme /24 -"
echo -e " il faudra une route entre les deux pour que l'ASP monte."
fi
echo ""
echo -e " ${BOLD}osmo-operator-${NODE}${NC} ${CYAN}${SELF_IP}${NC} ──M3UA──► inter-STP ${CYAN}${HUB_IP}:${PORT}${NC}"
# Le <op> du plan est l'indice, pas le compteur. Ces trois point codes portaient
# un 1 en dur : ils annoncaient 1.<noeud>1.x quoi qu'on demande, pendant que
# set-node-id.sh en ecrivait un autre dans les fichiers.
echo -e " STP ${CYAN}1.${NODE}${OP_INDEX}.2${NC} MSC ${CYAN}1.${NODE}${OP_INDEX}.1${NC} BSC ${CYAN}1.${NODE}${OP_INDEX}.3${NC} (plan WAN 1.<noeud><op>.<role>)"
echo ""
ensure_ip "$SELF_IP"
# ── Point codes et adresses de l'ASP ────────────────────────────────────────
# set-node-id.sh est le seul endroit qui sait tenir d'accord les TROIS fichiers
# (osmo-stp, osmo-msc, osmo-bsc) : son PC, son routing-key, et le PC du pair que
# chacun appelle. En rater un donne un coeur qui demarre et des appels qui
# n'aboutissent pas - d'ou la delegation plutot qu'un second sed ici.
# Son --op est un INDICE d'operateur, pas un nombre : lui passer le compteur
# revenait a lui demander le point code du DERNIER operateur du noeud.
SNI_ARGS=(--node "$NODE" --op "$OP_INDEX" --native
--local-ip "$SELF_IP" --hub-ip "$HUB_IP" --conf-dir "$CONF_DIR")
[ "$DRY" = "1" ] && SNI_ARGS+=(--dry-run)
bash "$HERE/network/set-node-id.sh" "${SNI_ARGS[@]}" || exit 1
# ── La table WAN ────────────────────────────────────────────────────────────
# On repart de celle qui existe pour ne pas perdre les autres noeuds : ce script
# ne connait que CE noeud et le hub, il n'a rien a dire sur les voisins.
WAN_IP=(); WAN_IND=(); WAN_NODE_LIST=()
[ -r "$WAN_CONF_FILE" ] && wan_nodes_load "$WAN_CONF_FILE" >/dev/null 2>&1 || true
if [ -z "${WAN_IP[$NODE]:-}" ]; then
WAN_NODE_LIST+=("$NODE")
WAN_IND[$NODE]="$(wan_default_ind "$NODE")"
fi
WAN_IP[$NODE]="$SELF_IP"
[ -n "${WAN_IND[$NODE]:-}" ] || WAN_IND[$NODE]="$(wan_default_ind "$NODE")"
WAN_NODE_ID="$NODE"; WAN_NODE_COUNT="${#WAN_NODE_LIST[@]}"; WAN_OPS="$OPS"
if [ "$DRY" = "1" ]; then
echo -e " [dry-run] ${WAN_CONF_FILE} : $(wan_nodes_spec) (ce noeud : ${NODE})"
else
if wan_nodes_validate; then
wan_nodes_save "$WAN_CONF_FILE" \
&& echo -e " ${GREEN}✓${NC} ${CYAN}${WAN_CONF_FILE}${NC} - ce noeud : ${NODE}"
else
echo -e " ${YELLOW}⚠${NC} table WAN incoherente, non reecrite - ${CYAN}${WAN_CONF_FILE}${NC} inchange"
fi
bash "$HERE/network/firewall-wan.sh" "$HUB_IP" "$OPS" >/dev/null 2>&1 || true
fi
write_role operator "$HUB_IP" "$NODE"
echo ""
if [ "$RESTART" = "1" ] && [ "$DRY" != "1" ]; then
if [ -x "$HERE/start-direct.sh" ]; then
echo -e " Relance de la pile..."
"$HERE/start-direct.sh" --node "$NODE" || true
else
echo -e " ${YELLOW}⚠${NC} start-direct.sh absent - relancez la pile a la main."
fi
else
echo -e " ${YELLOW}Les demons deja lances gardent l'ancienne adresse :${NC} relancez la pile"
echo -e " ${CYAN}sudo ./start-direct.sh --node ${NODE}${NC}"
fi
echo -e " ${BOLD}Verifier l'attachement, cote hub :${NC} ${CYAN}./start-interstp.sh --status${NC}"start-direct.sh
start-direct.sh
#!/bin/bash
# =============================================================================
# start-direct.sh - Prepare l'environnement Calypso puis lance run.sh
# =============================================================================
#
# Ce script ne demarre aucun demon.
#
# Il :
# 1. charge l'environnement ;
# 2. detecte le materiel / les binaires ;
# 3. choisit le mode (profil) ;
# 4. genere les fichiers mobile_*.cfg ;
# 5. exporte les variables necessaires ;
# 6. execute run.sh.
#
# Toute la logique GSM vit ensuite dans run.sh (et run_modules/).
#
# CHAINE DE CONFIGURATION - NE PAS LA CASSER
# VAR=x ./start-direct.sh ← la ligne de commande gagne toujours
# -> environment/load.env
# -> paths / modes / domaines
# puis exec run.sh avec le profil choisi.
# -----------------------------------------------------------------------------
set -uo pipefail
# ── [2026-08-14] GARDE "on doit etre dans Docker" RETIREE ───────────────────
# Elle exigeait /.dockerenv ET /etc/docker-entrypoint-cmd, et sortait en exit 1
# quand l'un manquait. Sur l'ISO il n'y a pas de Docker : les deux fichiers sont
# absents, la garde bloquait donc le seul lanceur utilisable la-bas. Retiree sur
# demande explicite - c'est le cas d'usage ISO qui prime.
#
# CE QU'ON PERD, et c'etait sa raison d'etre (constat du 12/08) : lance par
# erreur sur un HOTE qui a Docker, ce script repart sur un environnement a
# moitie construit - /opt/GSM/qosmo-grgsm n'y existe pas, mais /tmp/osmo-nitb/logs,
# lui, se cree tout seul - et meurt sur un "run.sh introuvable" qui ne designe
# pas la vraie cause. Si ce symptome reapparait sur une machine avec Docker,
# c'est ca : la-bas le lanceur est ./start.sh (ou ./start-nitb.sh), pas celui-ci.
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$HERE"
# --- options ------------------------------------------------------------------
DRY=0 VERBOSE=0 ACTION=start PROFILE="${CALYPSO_PROFILE:-faketrx-qemu}" FORCE=0
# WAN : jamais par defaut. --wan (ou WAN_AUTO=1 dans /etc/osmo-wan.conf, ce que
# pose une ISO construite avec --wan) le monte avant de passer la main a run.sh.
WAN_MESH=0
# --virtualbox : monte le segment et les VM avant le WAN. Refuse si l'on tourne
# DANS une VM - c'est l'hote qui pilote VirtualBox (le script le verifie).
VBOX_INTERCO=0
VBOX_NODES=""
VBOX_HOST_NODE=1
# --node : le numero de CE noeud, choisi au lancement et pas a la construction.
# C'est ce qui permet une ISO unique pour les neuf noeuds.
NODE_ID=""
NODE_OP=1
HUB_IP=""
# Les gabarits ne sont PAS regeneres par defaut : run.sh le ferait au demarrage
# et effacerait l'identite SS7 posee juste avant. --regen retablit l'ancien
# comportement pour qui veut repartir des gabarits.
REGEN_GABARITS=0
# --menu : demande les choix au lieu de les deviner. Voir le bloc « Menu » plus bas.
MENU_MODE=0
usage() {
cat <<'USAGE'
Usage : ./start-direct.sh [options] [mode]
Modes (profils) :
faketrx-qemu coeur + BTS#0 QEMU + BTS#1 faketrx (defaut)
faketrx coeur + fake_trx + trxcon + mobile
qemu pipeline Calypso QEMU seul
noproc coeur seul (no-process)
core alias noproc
hybrid alias faketrx-qemu
Options :
--list affiche le plan (delegue a run.sh --list)
--dry-run deroule sans effet de bord
--profile <nom> force le profil
--stop arrete la pile (delegue a run.sh --stop)
--status interroge l'etat (delegue a run.sh --status)
--force relance meme les modules deja demarres
--verbose montre la sortie des modules
--check-paths verifie les dependances declarees
--wan monte le WAN a N noeuds (1 a 9) AVANT run.sh et demande :
nombre de noeuds, IP de chaque noeud, indicatif de
chaque noeud, numero du noeud construit par ce lancement
--wan-nodes "1:IP:IND ..." meme chose sans question (scriptable)
--wan-id N numero du noeud local (sinon deduit des IP locales)
--wan-conf FICHIER table a lire/ecrire (defaut /etc/osmo-wan.conf)
--node N numero de CE noeud, 1 a 9. DEDUIT AUTOMATIQUEMENT s'il
--menu demande les choix au lieu de les deviner : profil,
noeud, operateur, inter-STP, WAN, regeneration des
configs. Les valeurs proposees sont
celles que le lanceur aurait prises tout seul, donc
valider sans rien changer equivaut a ne pas passer
l'option. Sans terminal, l'option est ignoree.
--regen regenere les configs depuis les gabarits (par defaut on
CONSERVE celles deja en place : run.sh les ecraserait)
est omis : environnement, puis /etc/osmo-role, puis la
table WAN comparee aux adresses locales. Cette option
ne sert qu'a forcer. Reecrit son identite SS7
(point codes 1.<noeud><op>.<role>, routing contexts) et
pointe son ASP sur l'inter-STP. Une seule ISO suffit
alors pour les neuf noeuds - le numero se choisit ici.
--op N operateur porte par ce noeud (defaut 1)
--hub-ip ADRESSE inter-STP a joindre (defaut : selon docker ou VM)
--air-mesh[=PORT] maillage de BURSTS entre noeuds : le milieu radio
devient commun et un mobile peut entendre - et choisir -
la BTS d'un autre noeud. Les pairs viennent de la table
WAN, la portee de data/air-mesh.txt. Declare deux
mobiles de plus pour s'y brancher (voir pont/airmesh.py).
--virtualbox[=N] WAN entre CETTE machine et N-1 VM VirtualBox (implique
--wan). A lancer depuis l'hote, pas depuis une VM.
--vbox-node N numero de noeud porte par cette machine (defaut 1)
--mobile client de couche 2 = mobile (defaut) -- pile L2/L3
complete derriere le firmware QEMU
--ccch_scan client de couche 2 = ccch_scan -- ecoute le CCCH
--bcch_scan client de couche 2 = bcch_scan -- releve les BCCH
--cell_log client de couche 2 = cell_log -- journal de cellules
Ces quatre-la sont EXCLUSIFS et ECRASENT le mobile de
QEMU : ils prennent sa socket L1CTL et sa VTY.
-h, --help cette aide
Sans --wan : aucun WAN. Avec --wan le profil reste faketrx-qemu (hybride,
1 faketrx + 1 QEMU Calypso), qui est deja le defaut de ce script.
Composer <indicatif><numero> joint ce numero sur le noeud correspondant.
Toute variable CALYPSO_* passee en prefixe est transmise a run.sh / QEMU :
CALYPSO_LANG=en ./start-direct.sh CALYPSO_NO_ATTACH=1 ./start-direct.sh
USAGE
}
while [ $# -gt 0 ]; do
case "$1" in
--list) ACTION=list ;;
--dry-run) DRY=1 ;;
--profile) if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
printf '%s\n\n' "--profile attend un nom de profil" >&2
usage >&2; exit 2
fi
PROFILE="$2"; shift ;;
--stop) ACTION=stop ;;
--status) ACTION=status ;;
--force) FORCE=1 ;;
--verbose) VERBOSE=1 ;;
--check-paths) ACTION=checkpaths ;;
--wan) WAN_MESH=1 ;;
--wan=*) WAN_MESH=1; WAN_NODES="${1#*=}" ;;
--wan-nodes) WAN_MESH=1; WAN_NODES="${2:-}"; shift ;;
--wan-nodes=*) WAN_MESH=1; WAN_NODES="${1#*=}" ;;
--wan-id) WAN_MESH=1; WAN_NODE_ID="${2:-}"; shift ;;
--wan-id=*) WAN_MESH=1; WAN_NODE_ID="${1#*=}" ;;
--wan-conf) WAN_CONF_FILE="${2:-}"; shift ;;
--wan-conf=*) WAN_CONF_FILE="${1#*=}" ;;
--virtualbox) WAN_MESH=1; VBOX_INTERCO=1 ;;
--virtualbox=*) WAN_MESH=1; VBOX_INTERCO=1; VBOX_NODES="${1#*=}" ;;
--vbox-node) VBOX_HOST_NODE="${2:-1}"; shift ;;
--vbox-node=*) VBOX_HOST_NODE="${1#*=}" ;;
--node) NODE_ID="${2:-}"; shift ;;
--node=*) NODE_ID="${1#*=}" ;;
--air-mesh) AIR_MESH=1 ;;
--air-mesh=*) AIR_MESH=1; AIRMESH_PORT="${1#*=}" ;;
--regen) REGEN_GABARITS=1 ;;
--menu) MENU_MODE=1 ;;
--op) NODE_OP="${2:-1}"; shift ;;
--op=*) NODE_OP="${1#*=}" ;;
--hub-ip) HUB_IP="${2:-}"; shift ;;
--hub-ip=*) HUB_IP="${1#*=}" ;;
--mobile|--ccch_scan|--bcch_scan|--cell_log)
# run_modules/70-l2.sh gere deja les quatre clients via
# CALYPSO_L2_CLIENT ; il manquait de quoi les choisir.
# Un seul a la fois : ils se disputeraient la socket
# L1CTL (/tmp/osmocom_l2) et la VTY 4247, et le second
# lance resterait muet pour toujours.
_l2c="${1#--}"
if [ -n "${L2_CLIENT_CHOISI:-}" ] && \
[ "$L2_CLIENT_CHOISI" != "$_l2c" ]; then
printf '%s\n\n' \
"--$_l2c et --$L2_CLIENT_CHOISI sont exclusifs : un seul client de couche 2" >&2
usage >&2; exit 2
fi
L2_CLIENT_CHOISI="$_l2c" ;;
-h|--help) usage; exit 0 ;;
faketrx-qemu|faketrx|qemu|noproc|core|hybrid)
PROFILE="$1"
[ "$PROFILE" = "hybrid" ] && PROFILE=faketrx-qemu
[ "$PROFILE" = "core" ] && PROFILE=noproc
;;
*) printf 'option inconnue : %s\n\n' "$1" >&2; usage >&2; exit 2 ;;
esac
shift
done
# --- ou tourne-t-on ? ---------------------------------------------------------
# La question n'est pas cosmetique : le plan d'adressage EN DEPEND.
# docker → l'inter-STP est le conteneur osmo-inter-stp (172.20.0.10), le
# noeud a deja son IP quand les demons demarrent.
# VM/ISO → l'inter-STP est une machine a part (192.168.1.49 sur le banc,
# ou une host-only 192.168.56.x selon le montage), et l'adresse
# du noeud vient du DHCP : elle peut manquer au lancement.
# Prendre l'un pour l'autre donne un ASP qui ne s'attache jamais, avec pour
# seule trace un "connection refused" vers une adresse inexistante ici.
#
# /.dockerenv seul ne suffit pas - il est vrai dans n'importe quel conteneur.
# On exige aussi /etc/docker-entrypoint-cmd, que scripts/entrypoint.sh depose :
# c'est ce couple qui identifie un conteneur DE CE DEPOT.
detect_runtime_env() {
if [ -f /.dockerenv ] && [ -f /etc/docker-entrypoint-cmd ]; then
echo docker; return
fi
if [ -f /.dockerenv ] || grep -qa 'docker\|containerd' /proc/1/cgroup 2>/dev/null; then
echo docker; return
fi
# systemd-detect-virt SORT EN CODE 1 quand il ne trouve rien. Un
# "$(cmd || echo none)" produit alors DEUX lignes - la sortie "none" du
# programme, plus celle du repli - et aucun motif ne correspond : une
# machine physique se retrouvait classee "vm". On ignore le code de
# sortie et on ne garde que la premiere ligne.
local v
v="$(systemd-detect-virt 2>/dev/null | head -1)" || true
v="${v:-none}"
case "$v" in
oracle|kvm|qemu|vmware|microsoft|xen|bochs|parallels) echo vm ;;
none|"") echo bare ;;
*) echo vm ;;
esac
}
RUNTIME_ENV="${RUNTIME_ENV:-$(detect_runtime_env)}"
# --- quel noeud sommes-nous ? --------------------------------------------------
# Le numero de noeud decide des point codes, du routing context et de l'indicatif.
# Le taper a chaque lancement est une occasion de se tromper - et une erreur ici
# ne se voit pas : la pile demarre, elle presente simplement au hub l'adresse SS7
# d'un autre noeud. On le deduit donc, et on DIT d'ou vient la reponse.
#
# Ordre, du plus explicite au plus deduit. Le premier qui repond gagne :
# 1. --node passe en ligne de commande
# 2. l'environnement - c'est ainsi que start.sh le transmet au conteneur
# 3. /etc/osmo-role, fige dans l'image par build-iso.sh --node N
# 4. la table WAN : l'adresse de ce noeud y figure, et nous l'avons
# 5. rien - on ne touche alors PAS a l'identite SS7, plutot que d'en inventer une
# Renvoie "numero|origine". Pas deux variables : la fonction est appelee dans
# une substitution de commande, donc dans un SOUS-SHELL - toute variable qu'elle
# poserait y resterait, et l'appelant n'afficherait "?" qu'apres coup.
detect_node_id() {
local n
n="${WAN_NODE_ID:-${OSMO_WAN_NODE:-}}"
if [[ "$n" =~ ^[1-9]$ ]]; then echo "${n}|environnement"; return 0; fi
if [ -r /etc/osmo-role ]; then
n="$(awk -F= '/^OSMO_WAN_NODE=/{print $2}' /etc/osmo-role 2>/dev/null | tr -d ' \r')"
if [[ "$n" =~ ^[1-9]$ ]]; then echo "${n}|/etc/osmo-role"; return 0; fi
fi
# La table WAN et nos propres adresses : c'est la source la plus fiable en VM,
# ou une seule ISO sert les neuf noeuds et ou seule l'IP les distingue.
local conf="${WAN_CONF_FILE:-/etc/osmo-wan.conf}"
if [ -r "$conf" ] && [ -r "$HERE/network/wan-nodes.sh" ]; then
n="$(
# Sous-shell : wan-nodes.sh pose des variables et des tableaux qu'on
# ne veut pas voir deborder dans le lanceur.
set +u
. "$HERE/network/wan-nodes.sh" 2>/dev/null || exit 0
WAN_NODE_ID=0
wan_nodes_load "$conf" 2>/dev/null || exit 0
wan_nodes_detect_self 2>/dev/null || exit 0
printf '%s' "$WAN_NODE_ID"
)"
if [[ "$n" =~ ^[1-9]$ ]]; then echo "${n}|table WAN (IP locale)"; return 0; fi
fi
return 1
}
NODE_ID_SRC=""
if [ -n "$NODE_ID" ]; then
NODE_ID_SRC="option --node"
else
_detected="$(detect_node_id || true)"
NODE_ID="${_detected%%|*}"
[ -n "$NODE_ID" ] && NODE_ID_SRC="${_detected#*|}"
unset _detected
fi
# L'adresse du hub arrive par le MEME chemin que le noeud : start.sh la pose
# dans l'environnement du conteneur (OSMO_HUB_IP, et INTER_STP_IP pour les
# scripts plus anciens). Sans ce repli, HUB_IP restait vide alors que la
# reponse etait deja la : un conteneur lance par docker exec -d n'a pas de
# terminal, la question du hub est donc sautee et l'adresse restait nulle
# jusqu'aux configs. Le repli est pose ici, apres la boucle d'options, pour
# que --hub-ip garde le dernier mot.
[ -n "$HUB_IP" ] || HUB_IP="${OSMO_HUB_IP:-${INTER_STP_IP:-}}"
# ── DERNIER REPLI : L ADRESSE QUE L ASP COMPOSE DEJA ─────────────────────────
# [2026-08-31] Sans HUB_IP, network/set-node-id.sh retombe sur le hub WAN par
# defaut, 192.168.1.49, calcule la source qui y route (192.168.1.31 ici) et
# l ecrit en local-ip de l ASP. Or sur un banc a une machine le hub vit dans un
# conteneur et l ASP compose 127.0.0.1:2908 - le port SCTP que le hub publie
# sur la boucle. La source calculee pour 192.168.1.49 ne mene donc pas au hub,
# et l ASP reste ASP_DOWN. Vu le 31/08 :
# asp-to-inter as-inter ASP_DOWN client
# Local 172.20.0.11:2910 Remote 127.0.0.1:2908
# (172.20.0.11 etant, lui, la valeur du gabarit jamais corrigee - l adresse du
# PREMIER CONTENEUR, qui n existe meme pas sur l hote.)
# Rien de tout cela ne se signale : le journal ne dit qu un timeout.
#
# La config SAIT ou est le hub - c est son remote-ip. On le lit plutot que de
# laisser deviner : la source et la destination viennent alors du meme
# endroit, et `ip route get` rend la bonne patte locale.
# --hub-ip et OSMO_HUB_IP gardent la priorite, ce repli ne sert qu au silence.
if [ -z "$HUB_IP" ] && [ -r "${OSMOCOM_CFG:-/etc/osmocom}/osmo-stp.cfg" ]; then
HUB_IP="$(awk '/^[[:space:]]*asp[[:space:]]+asp-to-inter[[:space:]]/{a=1; next}
a && /^[[:space:]]*remote-ip[[:space:]]/{print $2; exit}' \
"${OSMOCOM_CFG:-/etc/osmocom}/osmo-stp.cfg" 2>/dev/null)"
[ -n "$HUB_IP" ] && echo " hub deduit du remote-ip de l ASP : $HUB_IP" >&2
fi
# Pour les scripts qui ne connaissent que deux mondes : docker, ou tout le reste.
case "$RUNTIME_ENV" in docker) NODE_MODE=docker ;; *) NODE_MODE=native ;; esac
# --- affichage (identique a run.sh) -------------------------------------------
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
TTY=1
C_OK=$'\033[32m'; C_KO=$'\033[31m'; C_SK=$'\033[33m'
C_DIM=$'\033[2m'; C_CYAN=$'\033[36m'; C_BOLD=$'\033[1m'; C_Z=$'\033[0m'
else
TTY=0
C_OK=""; C_KO=""; C_SK=""; C_DIM=""; C_CYAN=""; C_BOLD=""; C_Z=""
fi
say_begin() {
if [ $TTY -eq 1 ]; then
printf '[ %s.. %s] %s' "$C_DIM" "$C_Z" "$1"
else
printf '[ .. ] %s\n' "$1"
fi
}
say_end() { # $1=tag $2=couleur $3=libelle $4=detail
if [ $TTY -eq 1 ]; then printf '\r\033[K'; fi
printf '[%s%s%s] %s' "$2" "$1" "$C_Z" "$3"
[ -n "${4:-}" ] && printf ' %s(%s)%s' "$C_DIM" "$4" "$C_Z"
printf '\n'
}
banner() {
echo -e "${C_CYAN}"
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Calypso GSM - bootstrap → run.sh ║"
echo "╚══════════════════════════════════════════════════════╝"
echo -e "${C_Z}"
}
# --- 1. configuration ---------------------------------------------------------
# [2026-08-03] globals.conf - les reglages reseau (MCC/MNC/ARFCN/KI/IMSI/A5...).
# Genere par ./generate_configs.sh cote hote ; ici on se contente de le lire.
# L'idiome ":=" qu'il utilise laisse gagner toute variable deja posee, donc
# ARFCN=520 ./start-direct.sh
# surcharge sans toucher au fichier.
[ -r "$HERE/globals.conf" ] && { set -a; . "$HERE/globals.conf"; set +a; }
say_begin "Chargement de l'environnement"
if [ -f "$HERE/environment/load.env" ]; then
set -a; . "$HERE/environment/load.env"; set +a
say_end " OK " "$C_OK" "Chargement de l'environnement" "environment/load.env"
elif [ -f "$HERE/env/load.env" ]; then
set -a; . "$HERE/env/load.env"; set +a
say_end " OK " "$C_OK" "Chargement de l'environnement" "env/load.env"
else
# Fallback minimal (chemins typiques du depot)
: "${GSM_ROOT:=/opt/GSM}"
: "${NITB_TREE:=$HERE}"
if [ -x "$HERE/../qosmo-grgsm/run.sh" ]; then
: "${OQC_ROOT:=$(cd "$HERE/../qosmo-grgsm" && pwd)}"
else
: "${OQC_ROOT:=$GSM_ROOT/qosmo-grgsm}"
fi
say_end " OK " "$C_OK" "Chargement de l'environnement" "fallback minimal"
fi
: "${RUN_DIR:=/run/osmo-direct}"
# Repli si load.env est absent. Les journaux restent sous RUN_DIR (tmpfs) :
# le defilement tmux en depend - cf. environment/paths.env.
: "${LOG_DIR:=$RUN_DIR/logs}"
# A5/1 par defaut : c'est le chiffrement que la maquette valide de bout en bout
# (Calypso ↔ BTS), et ce que start.sh impose deja dans son hand-off vers ce
# script. Un defaut a "a5 0" faisait diverger le lancement direct du lancement
# par start.sh - meme stack, deux chiffrements, selon la porte d'entree.
#
# ATTENTION : globals.conf est lu AVANT et fait autorite sur cette variable (son
# en-tete le dit). Ce ":=" n'est donc qu'un repli quand globals.conf est absent
# - l'ISO, un conteneur nu. La valeur qui s'applique en pratique vient de la-bas,
# et c'est pourquoi elle y a ete changee aussi.
: "${ENCRYPTION:=a5 1}"
# Le pont TRX par defaut : un transceiver unique (pont/pont.py) qui se presente
# a osmo-bts-trx en TRX-UDP, decode le DL vers GSMTAP 4730/4731 et encode l'UL.
# CALYPSO_BRIDGE n'est pas declare dans globals.conf (les CALYPSO_* "passent au
# travers, intact" d'apres son en-tete) : l'environnement gagne donc vraiment.
# CALYPSO_BRIDGE=none ./start-direct.sh -> pas de pont (QEMU + BTS seuls)
#
# [2026-09-02] LE DSP EST ENTERRE SUR CE FORK. La chaine I/Q (osmo-trx-ipc,
# calypso-ipc-device, si_bridge, demod-bridge), le mode CALYPSO_BRIDGE=ipc,
# l'option --dsp (trace asm, mailbox) et les drapeaux CALYPSO_SHUNT_* /
# CALYPSO_CANNED / CALYPSO_SKIP_* / CALYPSO_PIPELINE ont ete retires d'ici :
# le modele QEMU de qosmo-grgsm ne lit plus qu'UNE variable d'environnement,
# CALYPSO_SIM_CFG (voir plus bas), et son lanceur run.sh aucune de celles-la.
# Le travail DSP continue dans le fork qosmo-dsp, pas ici.
: "${CALYPSO_BRIDGE:=pont}"
: "${MS_COUNT:=2}"
: "${HOST_IP:=127.0.0.1}"
mkdir -p "$RUN_DIR" "$LOG_DIR" /root/.osmocom/bb 2>/dev/null || true
# --- 2. detection des chemins / binaires --------------------------------------
say_begin "Resolution de run.sh"
# [2026-08-08] Le chemin etait cable en dur ICI, ce qui rendait MORTE toute la
# resolution de OQC_ROOT faite plus haut (trois candidats testes pour rien) et
# contredisait le contrat annonce en tete de fichier ("la ligne de commande
# gagne toujours") : RUN_SH etait la seule variable non surchargeable.
# Ordre : RUN_SH explicite > OQC_ROOT resolu > chemin historique.
: "${RUN_SH:=${OQC_ROOT:+$OQC_ROOT/run.sh}}"
: "${RUN_SH:=/opt/GSM/qosmo-grgsm/run.sh}"
if [ ! -x "$RUN_SH" ]; then
say_end "FAIL" "$C_KO" "Resolution de run.sh" "$RUN_SH introuvable ou non executable"
printf ' %s→ Verifiez que /opt/GSM/qosmo-grgsm/run.sh existe et est executable%s\n' "$C_DIM" "$C_Z"
exit 1
fi
say_end " OK " "$C_OK" "Resolution de run.sh" "$RUN_SH"
RUN_ROOT="$(dirname "$RUN_SH")"
# ══ --menu : on DEMANDE, au lieu de deviner ════════════════════════════════
# Le lanceur resout tout seul le profil, le noeud, l'inter-STP, le WAN. C'est ce
# qu'il faut pour une ISO qui demarre sans personne devant. Mais devant un banc,
# la meme resolution silencieuse oblige a relire le script pour savoir ce qui a
# ete choisi - et a relancer avec les bonnes options quand ce n'etait pas ca.
#
# Ce menu ne CHANGE aucune regle : il PRE-REMPLIT chaque question avec la valeur
# que le lanceur aurait prise seul, et n'ecrit que ce que l'operateur modifie.
# Valider tout sans rien toucher donne donc exactement le lancement par defaut.
#
# Il s'efface de lui-meme quand personne ne peut repondre (pas de terminal,
# --dry-run, NO_MENU=1 que start.sh pose devant un conteneur) : une question
# sans lecteur, c'est un lancement qui se fige sans le dire.
menu_interactif() {
[ "${MENU_MODE:-0}" -eq 1 ] || return 0
[ "${NO_MENU:-0}" = "1" ] && return 0
[ "$DRY" -eq 1 ] && return 0
[ -t 0 ] || { printf " %smenu%s ignore : pas de terminal\n" "${C_DIM:-}" "${C_Z:-}"; return 0; }
local has_wt=0
command -v whiptail >/dev/null 2>&1 && has_wt=1
# Une question, une valeur par defaut, une reponse. Sans whiptail on
# retombe sur read : une ISO minimale n'a pas toujours newt.
_ask() { # $1 = titre $2 = question $3 = defaut
local rep
if [ "$has_wt" -eq 1 ]; then
rep="$(whiptail --title "$1" --inputbox "$2" 11 70 "$3" 3>&1 1>&2 2>&3)" || rep="$3"
else
printf " %s [%s] : " "$2" "$3" >&2
read -r rep </dev/tty || rep=""
fi
printf "%s" "${rep:-$3}"
}
_yesno() { # $1 = titre $2 = question $3 = defaut (0=oui 1=non)
if [ "$has_wt" -eq 1 ]; then
if [ "$3" -eq 0 ]; then whiptail --title "$1" --yesno "$2" 10 70; else
whiptail --title "$1" --defaultno --yesno "$2" 10 70; fi
return $?
fi
local d="o" rep
[ "$3" -eq 0 ] || d="n"
printf " %s [%s] : " "$2" "$d" >&2
read -r rep </dev/tty || rep=""
case "${rep:-$d}" in [oOyY]*) return 0 ;; *) return 1 ;; esac
}
printf " %smenu%s --menu : les defauts proposes sont ceux du lancement automatique\n" \
"${C_DIM:-}" "${C_Z:-}"
PROFILE="$(_ask "Profil" \
"Profil de pile :\n faketrx-qemu coeur + BTS QEMU + BTS faketrx (defaut)\n faketrx qemu noproc" \
"$PROFILE")"
# Le noeud est deja resolu plus haut (--node, environnement, /etc/osmo-role,
# table WAN) : on propose CE resultat, pas un 1 arbitraire.
NODE_ID="$(_ask "Noeud SS7" \
"Numero de ce noeud (1-9). Il fixe le point code 1.<noeud><op>.<role>,\nle routing context et les MSISDN <noeud>00<op><rang>." \
"${NODE_ID:-1}")"
NODE_OP="$(_ask "Operateur" "Operateur porte par ce noeud (1-9) :" "${NODE_OP:-1}")"
HUB_IP="$(_ask "Inter-STP" \
"Adresse du hub SS7 (vide = ne pas toucher au remote-ip en place) :" \
"${HUB_IP:-$(awk -F= '/^OSMO_HUB_IP=/{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' "${ROLE_FILE:-/etc/osmo-role}" 2>/dev/null)}")"
_yesno "WAN" "Monter le maillage WAN entre noeuds ?" "$([ "${WAN_MESH:-0}" -eq 1 ] && echo 0 || echo 1)" \
&& WAN_MESH=1 || WAN_MESH=0
_yesno "Configurations" \
"Regenerer /etc/osmocom et /etc/asterisk depuis les gabarits ?\n\nLa pile sera ARRETEE, les fichiers sauvegardes, puis reecrits\navec l'identite du noeud ci-dessus." \
"$([ "${REGEN_GABARITS:-0}" -eq 1 ] && echo 0 || echo 1)" \
&& REGEN_GABARITS=1 || REGEN_GABARITS=0
printf " %smenu%s profil=%s noeud=%s op=%s hub=%s wan=%s regen=%s\n" \
"${C_DIM:-}" "${C_Z:-}" "$PROFILE" "${NODE_ID:-?}" "$NODE_OP" \
"${HUB_IP:-inchange}" "$WAN_MESH" "$REGEN_GABARITS"
}
menu_interactif
# Profil → mapping vers les profils attendus par run.sh
case "$PROFILE" in
faketrx-qemu|hybrid) CALYPSO_PROFILE=hybrid; MODE=faketrx-qemu ;;
faketrx) CALYPSO_PROFILE=faketrx; MODE=faketrx ;;
qemu) CALYPSO_PROFILE=calypso; MODE=qemu ;;
noproc|core) CALYPSO_PROFILE=core; MODE=noproc; RUN_NO_PROCESS=1 ;;
*) CALYPSO_PROFILE=hybrid; MODE=faketrx-qemu; PROFILE=faketrx-qemu ;;
esac
export CALYPSO_PROFILE MODE
export RUN_NO_PROCESS="${RUN_NO_PROCESS:-0}"
export ENCRYPTION MS_COUNT HOST_IP
# Client de couche 2 : l'option de ligne de commande ECRASE tout, et elle est
# EXPORTEE -- une valeur posee sans export ne traverse pas jusqu'a run.sh,
# defaut deja paye sur ENCRYPTION (cf. generate_configs.sh).
if [ -n "${L2_CLIENT_CHOISI:-}" ]; then
CALYPSO_L2_CLIENT="$L2_CLIENT_CHOISI"
fi
export CALYPSO_L2_CLIENT="${CALYPSO_L2_CLIENT:-mobile}"
export LOG_DIR RUN_DIR
# --- 3. validation des chemins critiques --------------------------------------
say_begin "Validation des chemins"
path_ok=1
_check() {
local name="$1" val="${!1:-}"
if [ -z "$val" ]; then
printf '\n %s%s non defini%s\n' "$C_DIM" "$name" "$C_Z"
path_ok=0
elif [ -e "$val" ]; then
:
else
printf '\n %s%s introuvable : %s%s\n' "$C_DIM" "$name" "$val" "$C_Z"
path_ok=0
fi
}
# Variables optionnelles selon le profil ; on ne bloque que si presentes et cassees
# (DSP_PROM0 retire : ROM plus generee depuis le merge `sans-dsp` de
# qosmo-grgsm — cf. run.sh --check-paths et run_modules/00-prereqs.sh.)
for v in QEMU_BIN FIRMWARE_ELF OSMOCON; do
# `[ -n x ] && _check || true` avalait le verdict : _check pouvait poser
# path_ok=0 sans que la boucle ne le laisse remonter. Forme explicite.
if [ -n "${!v:-}" ]; then _check "$v"; fi
done
if [ $path_ok -eq 1 ]; then
say_end " OK " "$C_OK" "Validation des chemins"
else
say_end "WARN" "$C_SK" "Validation des chemins" "certains chemins manquent (run.sh verifiera)"
fi
# --- 4. generation des configs mobile -----------------------------------------
# MS#1 (QEMU / mobile principal) et MS#2 (faketrx side-car) pour le profil hybrid.
BB_DIR="/root/.osmocom/bb"
mkdir -p "$BB_DIR"
# Sources possibles pour le template mobile
MS_TEMPLATE=""
for t in \
"${OQC_ROOT:-}/cfgs/mobile_group1.cfg" \
"$RUN_ROOT/cfgs/mobile_group1.cfg" \
"$HERE/configs/mobile.cfg.template" \
"$HERE/configs/mobile.cfg" \
"$BB_DIR/mobile_group1.cfg" \
"$BB_DIR/mobile.cfg"
do
if [ -f "$t" ]; then MS_TEMPLATE="$t"; break; fi
done
generate_mobile_cfg() {
local dest="$1" vty_port="$2" l2sock="$3" sapsock="$4" arfcn="$5" imsi="$6" ki="$7"
# [2026-08-08] Peripheriques ALSA PAR MOBILE. Le template porte
# gsm_out/gsm_in pour tout le monde ; laisser ca remettrait les deux
# mobiles sur le meme sink et la meme source, c'est-a-dire la boucle
# audio qu'on vient d'ouvrir (cf /etc/asound.conf).
local aout="${8:-gsm_out}" ain="${9:-gsm_in}"
# [2026-08-08] GARDE-FOU : la liste de templates contient $BB_DIR/mobile.cfg,
# qui est aussi une DESTINATION. Si c'est lui qui est retenu, `sed "$tpl" >
# "$dest"` avec tpl == dest fait tronquer le fichier par le shell AVANT que
# sed ne le lise : on produit un mobile.cfg VIDE et le mobile ne demarre plus.
# On bascule alors sur le template de secours plutot que de se detruire.
local tpl="$MS_TEMPLATE"
if [ -n "$tpl" ] && [ "$tpl" -ef "$dest" ] 2>/dev/null; then
printf '\n %sMS_TEMPLATE == destination (%s) : template de secours%s\n' \
"$C_DIM" "$dest" "$C_Z"
tpl=""
fi
if [ -n "$tpl" ]; then
sed \
-e "s|bind 127.0.0.1 424[0-9]|bind 127.0.0.1 ${vty_port}|" \
-e "s|layer2-socket /tmp/osmocom_l2[_0-9]*|layer2-socket ${l2sock}|" \
-e "s|sap-socket /tmp/osmocom_sap[_0-9]*|sap-socket ${sapsock}|" \
-e "s|stick [0-9]*|stick ${arfcn}|" \
-e "s|^\([[:space:]]*\)imsi .*|\1imsi ${imsi}|" \
-e "s|^\([[:space:]]*\)imei .*|\1imei $(rand_imei) 0|" \
-e "s|^\([[:space:]]*\)ki .*|\1ki comp128 ${ki}|" \
-e "s|alsa-output-dev .*|alsa-output-dev ${aout}|" \
-e "s|alsa-input-dev .*|alsa-input-dev ${ain}|" \
"$tpl" > "$dest"
else
# Template minimal de secours
cat > "$dest" <<EOF
!
! mobile.cfg genere par start-direct.sh (secours)
!
log stderr
logging color 1
logging print category 1
logging timestamp 0
!
line vty
no login
bind 127.0.0.1 ${vty_port}
!
ms 1
layer2-socket ${l2sock}
sap-socket ${sapsock}
sim reader
imsi ${imsi}
ki comp128 ${ki}
network-selection-mode auto
stick ${arfcn}
!
EOF
fi
}
# ⚠️ [2026-08-08, resolu le 26/08] LE RUN N'UTILISE PAS mobile.cfg POUR MS#1.
# Le mobile Calypso tourne avec
# mobile -c /root/.osmocom/bb/mobile_group1.cfg
# alors qu'on ne generait ici que mobile.cfg. mobile_group1.cfg, lui, est COPIE
# tel quel par qosmo-grgsm (run_modules/20-mobile-cfg.sh, depuis $QEMU_TREE/cfgs) :
# un fichier statique, jamais derive de l'operateur. Il porte donc l'identite de
# l'operateur 1 - IMSI 001010001000001, ARFCN 514 - quel que soit l'operateur.
#
# Constate sur le banc, operateur 2 : le HLR attend 002010002000001, la BTS
# emet sur 516, et le mobile se presentait avec 001010001000001 sur 514.
# Mobile Subscriber of MS '1':
# IMSI: 001010001000001
# Status: U2_NOT_UPDATED IMSI detached LAI: invalid
# Registered PLMN: MCC-MNC 001-01
# Tout le reste etait juste - clefs, MSISDN, point codes, ARFCN des BTS - et
# l'abonne restait detache, parce que la seule carte SIM qui compte n'etait
# generee nulle part.
#
# On ecrit donc MS#1 dans LES DEUX fichiers. mobile.cfg reste la reference
# (d'autres modules la lisent), mobile_group1.cfg est celui que le lanceur
# ouvre. Verifier lequel tourne, avant de croire un reglage pose ici :
# pgrep -a mobile
# ── IMSI et Ki des MS : le plan du HLR, pas celui de l'operateur 1 ──────────
# Ces valeurs etaient ecrites EN DUR sur le PLMN de l'operateur 1 (001-01) et
# sur son numero d'abonne. Sur l'operateur 2, dont le reseau diffuse un autre
# MCC, le mobile presentait donc l'IMSI d'un abonne d'un autre pays : ni le MSC
# ni le HLR n'ont de fiche a ce numero, le Location Updating reste sans reponse,
# et l'on cherche la panne dans la BTS alors que la pile entiere est debout.
# Le plan est celui de start.sh (section "HLR subscribers") :
# IMSI = MCC MNC <operateur sur 4 chiffres> <index du MS sur 6 chiffres>
# Ki = 00112233445566778899aabbccdd <index du MS> <operateur>
# MCC et MNC sont relus dans la configuration REELLEMENT deployee - c'est elle
# que la BTS diffuse - et non redevines a partir d'une convention.
MS_OP_ID="${OPERATOR_ID:-1}"
MS_MCC="$(awk '$1=="network" && $2=="country" && $3=="code" {print $4; exit}' /etc/osmocom/osmo-bsc.cfg 2>/dev/null || true)"
MS_MNC="$(awk '$1=="mobile" && $2=="network" && $3=="code" {print $4; exit}' /etc/osmocom/osmo-bsc.cfg 2>/dev/null || true)"
# ── LES REPLIS SUIVENT LE PLAN, ILS NE L INVENTENT PAS ──────────────────────
# [2026-08-31] Ces deux lignes se declenchent quand osmo-bsc.cfg n est pas
# encore lisible (configs pas regenerees a cet instant), et elles etaient
# fausses toutes les deux :
#
# MS_MNC="01" en dur : l operateur 2 heritait du MNC de l operateur 1, et
# ms_imsi() produisait 001|01|0002|000001 - MNC d un operateur, identifiant
# d un autre. Ces abonnes-la arrivaient dans le HLR SANS MSISDN ni IMEI
# ("none" / "-------------"), a cote des bons : le VLR ne pouvait les
# rattacher a personne. Rien ne signalait l incoherence.
#
# MS_MCC=<op sur 3 chiffres> : aurait donne MCC 002 pour l operateur 2,
# alors que TOUT le banc est en 001 - c est le PLMN d essai, commun a tous
# les operateurs, et c est le MNC qui les distingue.
#
# Le plan reel, verifie dans /var/tmp/osmo-annuaire.csv :
# op 1 -> 001|01 op 2 -> 001|02 op 3 -> 001|03
[ -n "$MS_MCC" ] || MS_MCC="001"
[ -n "$MS_MNC" ] || MS_MNC="$(printf '%02d' "$MS_OP_ID")"
ms_imsi() { printf '%s%s%04d%06d' "$MS_MCC" "$MS_MNC" "$MS_OP_ID" "$1"; }
# ── LE QUATRIEME PROVISIONNEUR ──────────────────────────────────────────────
# run.sh embarque son propre module d'abonnes (run_modules/21-abonnes-hlr.sh,
# dans qosmo-grgsm). Il calcule son IMSI comme MCC+MNC+operateur+rang - la meme
# formule que nous - mais lit MCC et MNC dans /etc/osmocom/osmo-msc.cfg via un
# chemin qui n'aboutit pas toujours ; il retombe alors sur ses defauts, 001 et
# 01. Sur l'operateur 2 il fabriquait donc 001010002000001 : un abonne que
# personne ne presente, avec en prime un MSISDN de l'ancien plan (20001), qui
# RETIENT ce numero et empeche le bon abonne de le recuperer.
#
# Il honore MCC et MNC depuis l'environnement. En les posant, son IMSI devient
# exactement celui que start.sh a deja provisionne - et son propre controle
# d'etat (mod_abonnes_hlr_status) le trouve present, donc il ne fait rien.
# On neutralise ainsi un doublon par son idempotence, sans toucher a qosmo-grgsm.
export MCC="${MCC:-$MS_MCC}"
export MNC="${MNC:-$MS_MNC}"
# ── Le plan radio : on le LIT, on ne le recalcule pas ───────────────────────
# generate_configs.sh l'ecrit dans /etc/osmocom/radio-plan.env en meme temps
# que les configurations. Le relire ici garantit que le mobile s'accorde sur
# ce que la BTS diffuse VRAIMENT - et non sur une formule recopiee qui finira
# par diverger, comme l'ont fait l'unit-id, l'ARFCN et l'IMSI avant elle.
# Les replis restent, pour un banc monte sans ce fichier.
if [ -r /etc/osmocom/radio-plan.env ]; then
# shellcheck disable=SC1091
. /etc/osmocom/radio-plan.env
printf ' %splan radio%s lu dans /etc/osmocom/radio-plan.env\n' "${C_DIM:-}" "${C_Z:-}"
fi
# ── Les deux canaux radio, et a qui chaque MS s'accroche ────────────────────
# MS#1 et MS#2 avaient leur ARFCN ecrit EN DUR, 514 et 516 : les valeurs de
# l'operateur 1, et de lui seul. Sur l'operateur 2 la BTS principale passe a
# 516 et le side-car y restait aussi : deux cellules sur la meme frequence,
# leurs bursts melanges dans un milieu ou fake_trx apparie par frequence.
# Le mobile envoyait ses RACH, aucune Immediate Assignment ne revenait, et
# MS#1 restait accroche a 514 ou plus rien n'emettait.
#
# MS#1 suit la BTS principale, dont l'ARFCN est lu dans la configuration
# REELLEMENT deployee - c'est le BSC qui en decide et qui l'impose a la BTS
# par OML, donc c'est la qu'est la verite.
MS_ARFCN1="${PLAN_ARFCN:-}"
[ -n "$MS_ARFCN1" ] || MS_ARFCN1="$(awk '/^ bts 0$/{b=1} b && /^ *arfcn /{print $2; exit}' /etc/osmocom/osmo-bsc.cfg 2>/dev/null || true)"
[ -n "$MS_ARFCN1" ] || MS_ARFCN1=$(( 512 + MS_OP_ID * 2 ))
# MS#2 suit le side-car. Celui-la, c'est NOUS qui le decidons : le module
# 13-sidecar-cfg.sh de qosmo-grgsm lit SC_ARFCN et SC_UNIT_ID dans
# l'environnement (run_modules/_lib/radio.sh : « : "${SC_ARFCN:=516}" »).
# Les poser ici evite de modifier qosmo-grgsm, et garde une seule source par
# valeur - les memes formules que generate_configs.sh.
export SC_ARFCN="${SC_ARFCN:-${PLAN_ARFCN_BTS1:-$(( 612 + MS_OP_ID * 2 ))}}"
export SC_UNIT_ID="${SC_UNIT_ID:-${PLAN_UNIT_ID_BTS1:-$(( 6000 + MS_OP_ID * 10 + 2 ))}}"
MS_ARFCN2="$SC_ARFCN"
ms_ki() { printf '00 11 22 33 44 55 66 77 88 99 aa bb cc dd %02x %02x' "$1" "$MS_OP_ID"; }
# IMEI tire au hasard, cle de Luhn calculee - un par MS, jamais deux identiques
# dans une meme trace.
rand_imei() {
local body d i sum=0 dbl parity
body="35892500$(printf '%06d' "$(( (RANDOM << 15 | RANDOM) % 1000000 ))")"
for (( i = ${#body} - 1; i >= 0; i-- )); do
d=${body:i:1}
parity=$(( (${#body} - 1 - i) % 2 ))
if [ "$parity" -eq 0 ]; then
dbl=$(( d * 2 )); [ "$dbl" -gt 9 ] && dbl=$(( dbl - 9 ))
sum=$(( sum + dbl ))
else
sum=$(( sum + d ))
fi
done
printf '%s%d' "$body" "$(( (10 - sum % 10) % 10 ))"
}
printf ' %sPLMN MS%s MCC %s MNC %s operateur %s -> IMSI %s / %s\n' \
"${C_DIM:-}" "${C_Z:-}" "$MS_MCC" "$MS_MNC" "$MS_OP_ID" "$(ms_imsi 1)" "$(ms_imsi 2)"
printf ' %sradio%s MS#1 sur ARFCN %s (BTS principale) MS#2 sur ARFCN %s (side-car, unit-id %s)\n' \
"${C_DIM:-}" "${C_Z:-}" "$MS_ARFCN1" "$MS_ARFCN2" "$SC_UNIT_ID"
say_begin "Generation mobile MS#1"
MS1_CFG="${MOBILE_CFG_MS1_PATH:-$BB_DIR/mobile.cfg}"
generate_mobile_cfg "$MS1_CFG" \
4247 \
/tmp/osmocom_l2 \
/tmp/osmocom_sap_1 \
"$MS_ARFCN1" \
"$(ms_imsi 1)" \
"$(ms_ki 1)"
# La copie que le lanceur ouvre reellement. On la REGENERE plutot que de la
# copier : generate_mobile_cfg fixe aussi le port VTY, les sockets et l'ARFCN,
# et un simple cp propagerait le fichier statique de qosmo-grgsm.
# ── ET ON EMPECHE QU'IL SOIT ECRASE ─────────────────────────────────────
# Ecrire ce fichier ne suffit pas : run.sh le REECRIT juste apres. Son module
# 20-mobile-cfg.sh copie MOBILE_CFG_SRC par-dessus, et ce chemin vaut par
# defaut $QEMU_TREE/cfgs/mobile_group1.cfg - le fichier statique de qosmo-grgsm,
# fige sur l'operateur 1. Mesure sur le banc :
# mobile.cfg 18:22:22 imsi=002010002000001 arfcn=516
# mobile_group1.cfg 18:22:27 imsi=001010001000001 arfcn=514
# Cinq secondes d'ecart, et c'est le second que le mobile ouvre.
# On designe donc NOTRE fichier comme source : le module recopie alors le bon
# contenu, et son controle d'idempotence (cmp -s src dest) passe puisque les
# deux sont identiques. Aucune modification de qosmo-grgsm.
export MOBILE_CFG_SRC="$MS1_CFG"
MS1_GROUP_CFG="$BB_DIR/mobile_group1.cfg"
if [ "$MS1_GROUP_CFG" != "$MS1_CFG" ]; then
generate_mobile_cfg "$MS1_GROUP_CFG" \
4247 \
/tmp/osmocom_l2 \
/tmp/osmocom_sap_1 \
"$MS_ARFCN1" \
"$(ms_imsi 1)" \
"$(ms_ki 1)"
printf ' %sMS#1%s aussi ecrit dans %s (le fichier que run.sh ouvre)\n' \
"${C_DIM:-}" "${C_Z:-}" "$MS1_GROUP_CFG"
fi
say_end " OK " "$C_OK" "Generation mobile MS#1" "$MS1_CFG"
say_begin "Generation mobile MS#2 (faketrx)"
MS2_CFG="$BB_DIR/mobile_faketrx_bts1.cfg"
generate_mobile_cfg "$MS2_CFG" \
4248 \
/tmp/ms2_l2 \
/tmp/ms2_sap \
"$MS_ARFCN2" \
"$(ms_imsi 2)" \
"$(ms_ki 2)" \
gsm_out gsm_in
say_end " OK " "$C_OK" "Generation mobile MS#2 (faketrx)" "$MS2_CFG"
# ── LA SIM EMULEE LIT LE MEME FICHIER QUE LE MOBILE ─────────────────────────
# CALYPSO_SIM_CFG est la SEULE variable d'environnement que lit le modele QEMU
# (hw/arm/calypso/calypso_sim.c) : il y prend la section « test-sim » - imsi et
# ki comp128 - au format de mobile.cfg, donc exactement le fichier MS#1 genere
# ci-dessus. Sans elle, la SIM emulee garde son IMSI par defaut
# (001010000000001), inconnu du HLR provisionne avec le plan. Elle n'etait
# exportee nulle part.
# (MOBILE_CFG_MS1/MS2 et CALYPSO_MS2_CFG, exportes ici jusqu'au 2026-09-02,
# n'avaient aucun lecteur dans qosmo-grgsm ; le side-car MS#2 lit
# SC_MOBILE_CFG / le fichier lui-meme.)
export CALYPSO_SIM_CFG="$MS1_CFG"
# --- Mode PONT TRX (CALYPSO_BRIDGE=pont) : le pont maison est le transceiver --
# Le pont (pont/pont.py) se presente comme transceiver TRX-UDP a osmo-bts-trx
# (5700/5701/5702), decode les bursts DL en L2 -> GSMTAP 4730/4731 vers le
# modele QEMU, et encode l'UL depuis les sidebands /dev/shm/calypso_* -> TRXD
# -> BTS. Il a BESOIN d'osmo-bts-trx en face : c'est qosmo-grgsm/60-bts.sh qui
# le lance, et la barriere plus bas attend son retour avant de lancer le pont.
if [ "${CALYPSO_BRIDGE:-}" = pont ]; then
export CALYPSO_BRIDGE
# ── LE PONT VIENT DU DEPOT, POINT ──────────────────────────────────────
# Le defaut etait /opt/GSM/pont/pont.py, un chemin HORS DEPOT alimente par un
# `COPY pont/pont.py` de Dockerfile.run et fige par un `ENV PONT_PY=`. Rien
# ne le gardait en phase avec pont/pont.py, et rien ne signalait sa derive.
#
# CE QUE CA A COUTE, le 2026-08-27 : la copie executee etait restee a une
# version anterieure de 198 lignes, SANS AUCUN plan SDCCH/8 (ni PLAN_SD8, ni
# _dcch_plan, ni sacch_dl102). Elle raisonnait sur le SDCCH/4 combine du TS0
# pendant que le mobile parlait sur SDCCH/8 SS0 du TS1. Pendant deux heures,
# sans un seul message d erreur : 79 % de CRC fail, montant emis dans la
# fenetre du /4 (donc hors de celle que la BTS ecoute, avec hors_fenetre=0
# puisque coherent avec sa propre table), SABM sans reponse, T200 x6, LU en
# echec, SMS rejete, appel impossible. Restaurer la copie du depot a suffi a
# retablir le LU complet, chiffrement A5/1 compris, des le premier essai.
#
# La copie libre et son COPY sont supprimes ; PONT_PY reste disponible pour
# qui veut essayer une variante, mais c est alors un choix explicite.
#
# [2026-08-30] NITB_ROOT RETIRE DE CE CHEMIN. Le defaut passait par lui, et
# NITB_ROOT n appartient pas a ce depot : qosmo-grgsm/environnement/paths.env
# le pose a "$GSM_ROOT/osmo_egprs" - l ANCIEN nom de cet arbre. Le pont etait
# donc cherche dans /opt/GSM/osmo_egprs/pont/pont.py, d ou
# "pont introuvable (/opt/GSM/osmo_egprs/pont/pont.py)" sur un banc ou le
# fichier est pourtant la, dans le depot. Pire quand ce vieux dossier existe
# encore : on y trouve une COPIE, et on retombe mot pour mot dans la panne
# decrite ci-dessus - un pont hors depot qui derive sans que rien ne le dise.
# $HERE est le repertoire de CE script : le pont voyage avec lui, toujours.
_PONT="${PONT_PY:-$HERE/pont/pont.py}"
# ── LE PONT DOIT SAVOIR SUR QUELLE CELLULE IL TRAVAILLE ─────────────────
# pont.py fait le codage/decodage de canal entre le Calypso de QEMU et la
# BTS. Il lisait PONT_ARFCN et PONT_BSIC dans son environnement, avec pour
# defauts 514 et 7 - les valeurs de l'operateur 1. Personne ne les posait.
#
# Le BSIC n'est pas decoratif : sa partie basse (BCC) EST la sequence
# d'apprentissage des canaux dedies. Avec un BSIC faux, la correlation
# echoue et les blocs ne se decodent pas. Constate sur l'operateur 2, dont
# la cellule est en ARFCN 516 / BSIC 14 :
# dl_bursts=20660 dl_dec=1180 crc_fail=3984
# TS0/22 : 0/295 <- la voie ou arrive l'Immediate Assignment
# ul_sent=23 rach=23 <- le RACH partait bien
# Le mobile emettait son RACH, le reseau repondait, et la reponse n'etait
# jamais rendue au Calypso : "radio resource layer state: connection
# pending", indefiniment. L'operateur 1 s'en sortait parce que ses valeurs
# sont justement celles des defauts.
#
# On les prend dans le plan radio publie par generate_configs.sh, avec les
# memes replis que les mobiles.
export PONT_ARFCN="${PONT_ARFCN:-${PLAN_ARFCN:-$MS_ARFCN1}}"
export PONT_BSIC="${PONT_BSIC:-${PLAN_BSIC:-$(( MS_OP_ID * 7 % 64 ))}}"
printf ' %spont TRX%s : cellule ARFCN %s BSIC %s\n' \
"${C_DIM:-}" "${C_Z:-}" "$PONT_ARFCN" "$PONT_BSIC"
# [2026-08-16] N'ARMER LE LANCEUR QUE SUR UN VRAI DEMARRAGE.
# Ce bloc s'execute AVANT le `case "$ACTION"` plus bas. Sur `--stop` (comme
# sur --list/--status/--check-paths) on armait donc quand meme le lanceur
# differe, que `run.sh --stop` tuait aussitot - d'ou le "line 450: Killed"
# crache par bash en plein arret propre. Un arret qui programme un demarrage
# n'a aucun sens, et le message faisait croire a une panne.
# Le pont DEJA VIVANT, lui, est bien arrete : le teardown le connait
# desormais (`f:pont/pont.py` dans _td_patterns) et mod_teardown_stop
# l'applique aussi.
if [ "$ACTION" != start ]; then
printf ' %spont TRX%s : non arme (action=%s, pas un demarrage)\n' \
"${C_DIM:-}" "${C_Z:-}" "$ACTION"
elif [ -r "$_PONT" ]; then
# SINGLETON : tuer un pont precedent, sinon il tient 5700-5702 et le
# teardown de run.sh echoue ("restes du run precedent : port:5700...").
pkill -f "$_PONT" 2>/dev/null || true
# [2026-08-16] KILLALL DEMANDE PAR L'OPERATEUR, et assume.
# Le motif nominatif ci-dessus rate tout pont lance sous un autre chemin
# (PONT_PY surcharge, une sauvegarde .bak), et c'est ce qui faisait
# echouer le teardown sur "port:5700 port:5701 port:5702".
# ⚠️ CE QUE CA EMPORTE, en plus du pont : fake_trx.py (le transceiver de
# MS#2), sms-interop-relay.py (routage SMS inter-operateurs, HORS pile
# run.sh donc non relance automatiquement), gsm_sniff.py et les scripts
# des panes tmux. C'est acceptable ICI parce qu'on est sur le chemin d'un
# DEMARRAGE (ACTION=start) : le teardown de run.sh va de toute facon tout
# arreter deux secondes plus tard. Ne PAS reprendre ce geste dans un
# module de run.sh, ou il tuerait le filtre d'horodatage du run en cours.
# CALYPSO_NO_KILLALL=1 pour s'en passer.
if [ "${CALYPSO_NO_KILLALL:-0}" != "1" ]; then
printf ' %spont TRX%s : killall -9 python3 (demande ; emporte aussi fake_trx / sms-interop-relay)\n' "${C_DIM:-}" "${C_Z:-}"
killall -9 python3 2>/dev/null || true
fi
sleep 1
printf ' %spont TRX%s : transceiver TRX-UDP d osmo-bts-trx (pont/pont.py)\n' "${C_DIM:-}" "${C_Z:-}"
# LANCEMENT DIFFERE : run.sh fait d'abord son teardown (qui verifie que
# 5700-5702 sont LIBRES). On ne binde donc qu'apres, sinon on se
# bloque nous-memes. 25 s = teardown + demarrage des modules.
# LOG HORS LOG_DIR : run.sh archive/efface LOG_DIR pendant son teardown ;
# un pont.log ouvert avant se retrouve sur un inode SUPPRIME (sortie
# invisible, "(deleted)" dans /proc/<pid>/fd/1). /dev/shm survit.
# setsid : le pont doit survivre a la fin du shell de start-direct.
# [2026-08-16] LE `sleep 25` ETAIT UNE COURSE, PAS UNE BARRIERE.
# 25 s etait un pari sur la duree du teardown de run.sh. Des que
# celui-ci archive de gros journaux (mesure : 62 Mo -> ~40 s), le pont
# se rebindait sur 5700-5702 EN PLEIN teardown, qui echouait alors sur
# "restes du run precedent : port:5700 port:5701 port:5702" et
# abandonnait toute la sequence. Le symptome se deplacait avec la
# taille des journaux, ce qui le faisait passer pour intermittent.
# On attend maintenant un EVENEMENT et non une duree : osmo-bts-trx,
# qui est l'interlocuteur TRX-UDP du pont et ne demarre qu'APRES le
# teardown. Sa presence prouve donc que les ports sont a nous.
# [2026-08-27] LA BARRIERE ATTEND D'ABORD QU'IL DISPARAISSE.
# Elle n'attendait que sa PRESENCE, en se fiant a « osmo-bts-trx ne
# demarre qu'APRES le teardown ». Vrai seulement si l'on a fait --stop
# avant. Sans --stop, l'osmo-bts-trx du run PRECEDENT est encore la :
# pgrep reussit immediatement (_n=0), le pont binde 5700-5702 en plein
# teardown, run.sh le tue derriere (f:pont/pont.py est dans
# _td_patterns) et rien ne le relance -- « le pont n'a pas de bursts ».
# On attend donc les DEUX fronts : la disparition (le teardown a bien
# eu lieu), puis l'apparition (la pile est remontee). Avec --stop le
# premier front est deja franchi et rien ne change.
( _n=0
while [ "$_n" -lt 180 ] && pgrep -x osmo-bts-trx >/dev/null 2>&1; do
_n=$((_n + 1)); sleep 1
done
_dispar=$_n
while [ "$_n" -lt 180 ] && ! pgrep -x osmo-bts-trx >/dev/null 2>&1; do
_n=$((_n + 1)); sleep 1
done
if [ "$_n" -ge 180 ]; then
echo "[pont] osmo-bts-trx jamais (re)vu apres 180 s - demarrage quand meme"
else
echo "[pont] osmo-bts-trx parti a ${_dispar}s puis revu a ${_n}s - la pile est debout, on binde"
fi
sleep 1
pkill -f "$_PONT" 2>/dev/null; sleep 1
exec setsid python3 -u "$_PONT" ) >/dev/shm/pont.log 2>&1 &
printf ' %spont: journal%s /dev/shm/pont.log (hors LOG_DIR, efface par le teardown)\n' "${C_DIM:-}" "${C_Z:-}"
else
printf ' pont introuvable (%s)\n' "$_PONT"
fi
fi
# --- MAILLAGE DE BURSTS (--air-mesh) : le milieu radio devient commun ---------
# Jusqu'ici deux noeuds se parlaient tout en haut de la pile - un trunk SIP,
# un relais SMS, du M3UA vers un hub - et chacun gardait sa radio hermetique :
# le mobile de l'operateur 1 ne POUVAIT PAS entendre la BTS de l'operateur 2.
# Ici c'est le burst qui traverse : pont/airmesh.py prend ce qui passe sur le
# fake_trx local et l'echange avec les pairs. Voir l'en-tete d'airmesh.py pour
# le detail - notamment pourquoi il faut DEUX emplacements de transceiver, et
# comment on les obtient sans modifier une ligne de qosmo-grgsm.
if [ "${AIR_MESH:-0}" = "1" ]; then
_AIRMESH="${AIRMESH_PY:-${HERE}/pont/airmesh.py}"
[ -f "$_AIRMESH" ] || _AIRMESH=/opt/GSM/osmo-operator/pont/airmesh.py
if [ ! -f "$_AIRMESH" ]; then
printf ' %sair-mesh%s : airmesh.py introuvable - maillage de bursts non lance\n' "${C_KO:-}" "${C_Z:-}"
else
# DEUX MOBILES DE PLUS QUE LE BANC N'EN A. fake_trx cree un transceiver
# par mobile declare ; airmesh prend les deux derniers et les accorde
# autrement (l'un comme un mobile, l'autre comme une BTS). Sans ce +2,
# il n'y a aucun emplacement libre et airmesh se tait sans rien dire.
_NMS_REEL="${N_MS:-2}"
N_MS=$(( _NMS_REEL + 2 )); export N_MS
_BBP="${BB_BASE_PORT:-6700}"; _BBS="${BB_PORT_STEP:-3}"
_TRX_MS=$(( _BBP + (_NMS_REEL + 1 - 1) * _BBS )) # emplacement N+1
_TRX_BTS=$(( _BBP + (_NMS_REEL + 2 - 1) * _BBS )) # emplacement N+2
# Les pairs viennent de la table WAN : tout noeud qui n'est pas nous.
_PAIRS=""
for _tok in ${WAN_NODES:-}; do
_pid="${_tok%%:*}"; _rest="${_tok#*:}"; _pip="${_rest%%:*}"
[ -n "$_pid" ] && [ -n "$_pip" ] || continue
[ "$_pid" = "${NODE_ID:-1}" ] && continue
_PAIRS="$_PAIRS --pair ${_pid}:${_pip}:${AIRMESH_PORT:-4739}"
done
printf ' %sair-mesh%s noeud %s arfcn %s emplacements TRXD %s et %s pairs:%s\n' \
"${C_DIM:-}" "${C_Z:-}" "${NODE_ID:-1}" "${ARFCN:-514}" \
"$_TRX_MS" "$_TRX_BTS" "${_PAIRS:- aucun}"
# On attend que fake_trx ait BINDE l'emplacement, pas une duree : il ne
# demarre qu'apres le teardown de run.sh, dont la duree depend de la
# taille des journaux archives. Une temporisation ferait de ce lancement
# une course - c'est la lecon du pont, juste au-dessus.
( _n=0
while [ "$_n" -lt 180 ] && ! ss -uln 2>/dev/null | grep -q ":$(( _TRX_MS + 1 ))\b"; do
_n=$((_n + 1)); sleep 1
done
if [ "$_n" -ge 180 ]; then
echo "[airmesh] emplacement $(( _TRX_MS + 1 )) jamais ouvert par fake_trx apres 180 s"
echo "[airmesh] verifiez que le banc tourne bien avec N_MS=${N_MS} (deux de plus que ${_NMS_REEL})"
exit 1
fi
echo "[airmesh] fake_trx a ouvert nos emplacements apres ${_n} s"
sleep 1
# shellcheck disable=SC2086
exec setsid python3 -u "$_AIRMESH" \
--node "${NODE_ID:-1}" --arfcn "${ARFCN:-514}" \
--port "${AIRMESH_PORT:-4739}" \
--trx-ms "$_TRX_MS" --trx-bts "$_TRX_BTS" \
--topologie "${HERE}/data/air-mesh.txt" \
$_PAIRS ) >/dev/shm/airmesh.log 2>&1 &
printf ' %sair-mesh: journal%s /dev/shm/airmesh.log\n' "${C_DIM:-}" "${C_Z:-}"
fi
fi
# --- 6. resume ----------------------------------------------------------------
banner
printf ' %sprofil%s %s (%s)\n' "$C_DIM" "$C_Z" "$CALYPSO_PROFILE" "$MODE"
printf ' %senviron.%s %s\n' "$C_DIM" "$C_Z" \
"$(case "$RUNTIME_ENV" in docker) echo "conteneur docker" ;; vm) echo "machine virtuelle" ;; *) echo "machine physique" ;; esac)"
if [ -n "$NODE_ID" ]; then
printf ' %snoeud%s %s %s(%s)%s\n' "$C_DIM" "$C_Z" "$NODE_ID" "$C_DIM" "${NODE_ID_SRC:-?}" "$C_Z"
else
printf ' %snoeud%s %s\n' "$C_DIM" "$C_Z" "aucun - identite SS7 inchangee"
fi
printf ' %srun.sh%s %s\n' "$C_DIM" "$C_Z" "$RUN_SH"
printf ' %sMS#1%s %s IMSI %s ARFCN %s VTY 4247\n' "$C_DIM" "$C_Z" "$MS1_CFG" "$(ms_imsi 1)" "$MS_ARFCN1"
printf ' %sMS#2%s %s IMSI %s ARFCN %s VTY 4248\n' "$C_DIM" "$C_Z" "$MS2_CFG" "$(ms_imsi 2)" "$MS_ARFCN2"
printf ' %sjournaux%s %s\n' "$C_DIM" "$C_Z" "$LOG_DIR"
printf ' %schiffrement%s %s\n' "$C_DIM" "$C_Z" "$ENCRYPTION"
printf '\n'
# --- 7. actions deleguees a run.sh --------------------------------------------
RUN_ARGS=()
[ "$DRY" -eq 1 ] && RUN_ARGS+=(--dry-run)
[ "$FORCE" -eq 1 ] && RUN_ARGS+=(--force)
[ "$VERBOSE" -eq 1 ] && RUN_ARGS+=(--verbose)
RUN_ARGS+=(--profile "$CALYPSO_PROFILE")
# [2026-08-08] --restart etait ajoute DIRECTEMENT sur la ligne exec, hors de
# RUN_ARGS. Consequence : `--dry-run` affichait une commande SANS --restart puis
# le vrai lancement en ajoutait un. Une simulation qui ne decrit pas ce qui va
# se passer est pire qu'absente. Il entre donc dans RUN_ARGS, ou il est visible.
# CALYPSO_NO_RESTART=1 pour demarrer sans reinitialiser (l'ancien comportement
# n'etait pas atteignable du tout).
[ "${CALYPSO_NO_RESTART:-0}" = "1" ] || RUN_ARGS+=(--restart)
# ── Les sessions tmux d'un run precedent ────────────────────────────────────
# Le teardown de run.sh arrete les processus mais laisse la session tmux
# debout. Au demarrage suivant, son module de panes RECREE ses fenetres dans la
# session survivante : il y a alors DEUX fenetres nommees « all ».
#
# Et deux fenetres de meme nom ne sont pas seulement inesthetiques : tmux ne
# sait plus resoudre la cible. Verifie sur le banc, tmux 3.2a :
# $ tmux split-window -t calypso:all
# can't find window: all
# Les trois splits echouent, le module les compte comme "pane refuse par tmux
# (place insuffisante)" - un diagnostic FAUX qui envoie chercher un probleme de
# taille de terminal - et la barriere conclut « la fenetre "all" n'a pas garde
# ses panes ». La cause etait deux fenetres plus haut.
#
# On efface donc les sessions du run precedent avec lui. Trois conventions
# cohabitent selon le lanceur, on les couvre toutes ; aucune n'a besoin
# d'exister.
purge_sessions_tmux() {
local n=0 s
for s in calypso gapk osmo; do
if tmux has-session -t "$s" 2>/dev/null; then
tmux kill-session -t "$s" 2>/dev/null && n=$((n + 1))
fi
done
if tmux -S /tmp/osmocom_tmux has-session -t osmocom 2>/dev/null; then
tmux -S /tmp/osmocom_tmux kill-session -t osmocom 2>/dev/null && n=$((n + 1))
fi
[ "$n" -gt 0 ] && printf ' %ssessions tmux%s %s session(s) du run precedent effacee(s)\n' \
"${C_DIM:-}" "${C_Z:-}" "$n"
return 0
}
case "$ACTION" in
list)
exec env CALYPSO_PROFILE="$CALYPSO_PROFILE" bash "$RUN_SH" --list "${RUN_ARGS[@]}"
;;
stop)
say_begin "Arret de la pile via run.sh"
bash "$RUN_SH" --stop --profile "$CALYPSO_PROFILE"
say_end " OK " "$C_OK" "Arret de la pile via run.sh"
purge_sessions_tmux
# Filet identique a celui de run.sh, pose ici aussi : on peut arreter par
# ce script sans passer par l'autre, et un fake_trx.py orphelin suffit a
# faire echouer le run suivant sur « port UDP 5720 deja pris ».
# run.sh le fait deja de son cote ; le repeter ne coute rien et couvre le
# cas ou RUN_SH est introuvable ou sort en erreur.
# PORTEE : tue TOUS les python3 de la machine. CALYPSO_STOP_KILL_PYTHON=0
# le desactive.
if [ "${CALYPSO_STOP_KILL_PYTHON:-1}" != 0 ]; then
if killall -9 python3 2>/dev/null; then
say_end " OK " "$C_OK" "python3 restants termines (killall)"
fi
fi
exit 0
;;
status)
exec env CALYPSO_PROFILE="$CALYPSO_PROFILE" bash "$RUN_SH" --status --profile "$CALYPSO_PROFILE"
;;
checkpaths)
exec env CALYPSO_PROFILE="$CALYPSO_PROFILE" bash "$RUN_SH" --check-paths
;;
esac
# --- 7bis-menu. Ce qui manque, demande UNE fois, avec le cache en defaut ------
# Les trois valeurs qui decident de l'identite SS7 - numero de noeud, operateur,
# adresse du hub - se deduisent la plupart du temps : environnement transmis par
# start.sh, /etc/osmo-role fige dans l'image, table WAN. Quand la deduction ne
# donne rien, la pile demarrait quand meme et gardait les point codes du
# gabarit : 1.1.2 partout, ASP vers 127.0.0.1, interco morte sans un mot.
#
# On demande donc - mais seulement ce qui manque, et en proposant ce que le
# cache sait deja. Sans terminal, rien n'est demande : les defauts s'appliquent,
# une ISO qui demarre seule ne peut pas rester bloquee sur une question.
ask_node_identity() {
# NO_MENU=1 est ce que start.sh pose devant le lanceur quand les choix ont
# deja ete faits sur l'hote. Aujourd'hui la garde tty juste en dessous
# suffit (start.sh lance par docker exec -d, sans terminal) : ce test ne
# repare donc aucune panne observable, il aligne l'identite SS7 sur la
# meme convention que les menus de pile de start.sh, pour le jour ou le
# lanceur automatique aura un terminal.
[ "${NO_MENU:-0}" = "1" ] && return 0
[ -t 0 ] || return 0
[ "$DRY" -eq 1 ] && return 0
local cached_node cached_src cached_hub
IFS='|' read -r cached_node cached_src <<< "$(detect_node_id)"
# L'ENVIRONNEMENT NE SE DEMANDE PAS. C'est ainsi que start.sh transmet le
# noeud a un conteneur : la reponse est deja connue, et la question n'a pas
# de sens - pire, dans un conteneur ou personne ne lit le terminal, elle
# repart vide. L'identite n'etait alors PAS appliquee, la pile regenerait
# ses configs par defaut (point-code 1.1.2, ASP vers le hub docker, en
# shutdown) et ecrasait celle que start.sh venait d'ecrire. Le fichier
# portait les bonnes valeurs juste avant, les mauvaises juste apres.
# Le rattrapage qui se trouvait ici ne servait plus a rien : il exigeait un
# NODE_ID vide, alors que la resolution du noeud, bien plus haut, l'a deja
# rempli depuis cette meme source. NODE_ID et HUB_IP sont donc repris de
# l'environnement la-bas, une fois, et pas ici.
cached_hub="$(awk -F= '/^OSMO_HUB_IP=/{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' /etc/osmo-role 2>/dev/null)"
[ -n "$cached_hub" ] || cached_hub="192.168.1.49"
if command -v whiptail >/dev/null 2>&1; then
[ -n "$NODE_ID" ] || NODE_ID="$(whiptail --title "Identite du noeud" \
--inputbox "Numero de ce noeud (1-9)${cached_src:+ [$cached_src]}\nVide = ne pas toucher a l'identite SS7." \
11 66 "${cached_node:-}" 3>&1 1>&2 2>&3)" || NODE_ID=""
if [ -n "$NODE_ID" ] && [ -z "$HUB_IP" ]; then
HUB_IP="$(whiptail --title "Inter-STP" \
--inputbox "Adresse du hub SS7 (PC 0.0.0)\nVide = laisser le remote-ip deja present." \
11 66 "$cached_hub" 3>&1 1>&2 2>&3)" || HUB_IP=""
fi
else
if [ -z "$NODE_ID" ]; then
printf ' Numero de ce noeud (1-9)%s [%s] : ' \
"${cached_src:+ - $cached_src}" "${cached_node:-aucun}"
read -r NODE_ID || NODE_ID=""
[ -n "$NODE_ID" ] || NODE_ID="${cached_node:-}"
fi
if [ -n "$NODE_ID" ] && [ -z "$HUB_IP" ]; then
printf ' Adresse de l inter-STP [%s] : ' "$cached_hub"
read -r HUB_IP || HUB_IP=""
[ -n "$HUB_IP" ] || HUB_IP="$cached_hub"
fi
fi
}
ask_node_identity
# ══ La regeneration des configurations, POUR DE VRAI ═══════════════════════
# CE QUE --regen FAISAIT JUSQU'ICI : RIEN.
# Il levait OSMO_NO_REGEN et comptait sur run_modules/08-gabarits.sh pour
# rejouer apply_config_templates au demarrage. Ce module N'EXISTE PAS - il n'a
# jamais existe dans run_modules/ - et run.sh ne genere aucune configuration.
# Le message « regeneration demandee » s'affichait, la pile s'arretait, et pas
# un octet de /etc/osmocom ne changeait. Le seul effet reel de l'option etait
# d'arreter le banc.
#
# CE QU'IL FAIT MAINTENANT : la meme chose que start.sh cote docker, et que
# build-iso.sh a la construction - c'est-a-dire la sequence complete :
#
# 1. apply_config_templates substitue configs/*.cfg et *.conf dans un
# repertoire NEUF (jamais en place : une
# substitution a moitie faite sur /etc/osmocom
# laisse un banc qui ne demarre plus) ;
# 2. apply_native_post_patches la recette NATIVE de l'ISO - sms-routing par
# MS reel, SGSN/MSC sur l'adresse locale du
# coeur, run.sh degraisse ;
# 3. install_configs_native pose le resultat dans /etc/osmocom et
# /etc/asterisk.
#
# EN SOUS-SHELL. lib/gabarits.sh charge globals.conf et pose une trentaine de
# variables - MCC, ARFCN, HOST_IP, N_OPERATORS, KI... - qui portent ici un tout
# autre sens (HOST_IP est l'adresse du banc pour le lanceur, l'adresse interne
# du coeur pour les gabarits). Les laisser fuir, c'est changer le plan radio
# d'un run parce qu'on a demande une regeneration.
#
# $1 numero de noeud $2 numero d'operateur $3 inter-STP (peut etre vide)
# ── LE POINT CODE AFFICHE SUIT LE PLAN REELLEMENT APPLIQUE ──────────────────
# [2026-08-31] Ces bandeaux calculaient toujours 1.<noeud><op>.x, la formule du
# plan WAN, alors que set-node-id.sh recoit --local hors WAN et ecrit
# 1.<op>.x. On lisait donc, dans la meme sortie :
# [ OK ] Identite SS7 (noeud 1 · PC 1.13.x · mode docker)
# SS7 point-code 1.3.2
# deux valeurs contradictoires pour la meme chose - la premiere fausse. Sur un
# banc ou le desaccord de plan a justement coute une demi-journee, un bandeau
# qui annonce le mauvais plan est exactement ce qu il ne faut pas.
_pc_affiche() { # $1 = noeud, $2 = operateur
if [ "${WAN_MESH:-0}" != "1" ] && [ -z "${WAN_NODES:-}" ]; then
printf '1.%s.x' "$2" # plan local, celui qu on applique
else
printf '1.%s%s.x' "$1" "$2" # plan WAN, entre machines
fi
}
regen_configs_natif() {
local node="$1" op="$2" hub="$3"
local tmp rc=0
# ── Le plan NATIF, celui de build-iso.sh, et non celui de docker ────────
# 127.0.0.2 (__CONTAINER_IP__) porte le coeur : GGSN, SGSN, UPF, la nsvc du
# BSC. Une adresse de boucle locale existe TOUJOURS et est prete avant tout
# service ; une adresse de carte peut manquer au moment ou osmo-ggsn se lie,
# et le bind echoue sans rapport visible avec le reseau.
# 127.0.0.1 (__GATEWAY_IP__) recoit le gsmtap, la ou tshark capte deja.
local core_ip="${OSMO_NATIVE_CORE_IP:-127.0.0.2}"
local gw_ip="${OSMO_NATIVE_GW_IP:-127.0.0.1}"
# ── L'identite SUIT LE NOEUD ────────────────────────────────────────────
# 1.<noeud><op>.<role>, le plan de network/set-node-id.sh et de build-iso.sh
# --node. Sans le noeud dans le point code, trois machines attachees au meme
# hub y presentent trois fois 1.11.2 : un point code est une ADRESSE, et
# deux equipements qui partagent la leur donnent du routage faux, silencieux.
# Le routing context suit la meme regle : c'est lui que le hub lit pour
# distinguer les AS, et la formule locale (op*100+50) rend la meme valeur
# sur tous les noeuds.
local mid="${node}${op}"
local pc_msc="1.${mid}.1" pc_stp="1.${mid}.2" pc_bsc="1.${mid}.3"
local shut="shutdown"
[ -n "$hub" ] && shut="no shutdown"
# ── Le PLMN : celui de la machine, pas celui de la table ────────────────
# Relu dans osmo-msc.cfg installe. Reprendre les defauts de globals.conf
# renommerait le reseau a chaque regeneration - OsmoDirect redevenait
# OsmoQEMU - et un mobile deja provisionne ne reconnait plus sa cellule.
local cfgdir="${OSMOCOM_CFG:-/etc/osmocom}"
local mcc mnc opname
mcc="$(awk '/^ *network country code /{print $4; exit}' "$cfgdir/osmo-msc.cfg" 2>/dev/null)"
mnc="$(awk '/^ *mobile network code /{print $4; exit}' "$cfgdir/osmo-msc.cfg" 2>/dev/null)"
opname="$(awk '/^ *short name /{print $3; exit}' "$cfgdir/osmo-msc.cfg" 2>/dev/null)"
[ -n "$mcc" ] || mcc="${MCC:-001}"
[ -n "$mnc" ] || mnc="${MNC:-01}"
[ -n "$opname" ] || opname="${OP_NAME:-OsmoGSM}"
# Le nombre de MS vient de coeur.env, ecrit par build-iso.sh : c'est lui qui
# dit combien d'abonnes le HLR provisionne, donc combien de routes SMS il
# faut. Au-dela, le relais rend "No route for destination".
local n_ms="${N_MS:-}"
if [ -z "$n_ms" ] && [ -r "$cfgdir/coeur.env" ]; then
n_ms="$(awk -F'[:=}]' '/N_MS:=/{print $3; exit}' "$cfgdir/coeur.env" 2>/dev/null)"
fi
[ -n "$n_ms" ] || n_ms=2
printf ' %splan natif%s coeur %s gsmtap %s PC %s rctx %s PLMN %s-%s (%s) %s MS\n' \
"${C_DIM:-}" "${C_Z:-}" "$core_ip" "$gw_ip" "$pc_stp" \
"$(( node * 1000 + op * 100 + 50 ))" "$mcc" "$mnc" "$opname" "$n_ms"
tmp="$(mktemp -d)" || return 1
# ── La sauvegarde, AVANT d'ecrire ───────────────────────────────────────
# Une regeneration ecrase des fichiers que quelqu'un a pu retoucher a la
# main sur le banc. Qu'elle soit demandee ne veut pas dire qu'on accepte de
# perdre l'etat d'avant sans trace.
local sav="${LOG_DIR:-/tmp}/osmocom-avant-regen-$(date +%Y%m%d-%H%M%S).tar.gz"
tar czf "$sav" -C / etc/osmocom etc/asterisk 2>/dev/null && \
printf ' %ssauvegarde%s %s\n' "${C_DIM:-}" "${C_Z:-}" "$sav"
(
cd "$HERE" || exit 1
# AVANT de sourcer. lib/gabarits.sh fige ENCRYPTION a "a5 0" et HOST_IP
# a 127.0.0.1 par ":=" des qu'ils sont vides, et gc_load tient ensuite
# ces valeurs pour un choix de l'appelant : le banc repartait en clair
# apres un --regen, sans un mot.
: "${ENCRYPTION:=a5 1}"
HOST_IP="$core_ip"
# Le routing context et l'adresse source de l'ASP : les deux surcharges
# que build-iso.sh pose pour un noeud de WAN. 0.0.0.0 parce que
# l'adresse du noeud vient du DHCP et n'est pas forcement montee quand
# osmo-stp demarre - se lier a une adresse absente echoue au lancement.
export RCTX_INTER_OVERRIDE=$(( node * 1000 + op * 100 + 50 ))
export INTER_LOCAL_IP_OVERRIDE="0.0.0.0"
# _apply_node_ss7_addressing (appele en fin de apply_config_templates)
# ne rejoue set-node-id.sh que s'il sait QUI il sert, et il ne le lit
# que la.
export OSMO_ROLE=operator OSMO_WAN_NODE="$node"
[ -n "$hub" ] && export OSMO_HUB_IP="$hub"
unset OSMO_NO_REGEN
# shellcheck source=lib/gabarits.sh
. "$HERE/lib/gabarits.sh" || exit 1
apply_config_templates "$tmp" \
"$core_ip" "$gw_ip" \
"$op" "$pc_msc" "$pc_stp" "$pc_bsc" \
"$mcc" "$mnc" "$opname" \
"${hub:-127.0.0.1}" "$shut" "1" || exit 1
# Le noeud et la table WAN : c'est de la table que sms-routing.conf
# tire l'adresse de chaque noeud, et du noeud qu'il tire les MSISDN
# (<noeud>00<operateur><rang>).
apply_native_post_patches "$tmp" "$op" "$n_ms" "$core_ip" \
"$node" "${WAN_CONF_FILE:-/etc/osmo-wan.conf}" || exit 1
# ── bb/ RETIRE, et ce n'est pas un oubli ───────────────────────────
# install_configs_native poserait $tmp/bb/mobile.cfg par-dessus
# /root/.osmocom/bb/mobile.cfg. Or ce fichier a deja ete ECRIT plus haut
# par generate_mobile_cfg, qui seul fixe le port VTY, les sockets L1CTL
# et l'ARFCN de CHAQUE mobile. Le gabarit, lui, est fige sur l'operateur
# 1 : le remettre ici rendait les deux mobiles au meme ARFCN et a la
# meme VTY, et le second restait muet pour toujours.
rm -rf "$tmp/bb"
install_configs_native "$tmp" "" || exit 1
) || rc=1
rm -rf "$tmp"
return "$rc"
}
# ── Les gabarits : conserves, sauf --regen ──────────────────────────────────
# Par defaut on ne touche a rien : dans un conteneur, ce que start.sh vient
# d'ecrire - point codes du noeud, inter-STP, ASP actif - ne doit pas etre
# remplace par les valeurs des gabarits, sans quoi l'identite SS7 est juste
# puis fausse 80 secondes plus tard, sans que rien ne l'annonce.
if [ "${REGEN_GABARITS:-0}" -eq 1 ]; then
unset OSMO_NO_REGEN
printf ' %sgabarits%s regeneration demandee (--regen)\n' "${C_DIM:-}" "${C_Z:-}"
# ── REGEN : ON ARRETE D ABORD ───────────────────────────────────────────
# Regenerer /etc/osmocom sous une pile VIVANTE ne sert a rien et trompe :
# les demons ont deja lu leur configuration, ils gardent l'ancienne en
# memoire, et les fichiers neufs ne prennent effet qu'au demarrage suivant.
# On lisait donc une conf juste sur le disque pendant que la pile tournait
# sur la precedente -- le genre d'ecart qui se paie dix minutes plus tard.
# Un --regen arrete donc la pile, puis regenere : l'etat est sans ambiguite.
if [ -x "$RUN_SH" ] || [ -r "$RUN_SH" ]; then
say_begin "Arret de la pile avant regeneration"
bash "$RUN_SH" --stop --profile "$CALYPSO_PROFILE" >/dev/null 2>&1 || true
say_end " OK " "$C_OK" "Arret de la pile avant regeneration"
declare -F purge_sessions_tmux >/dev/null && purge_sessions_tmux
# Meme filet qu'au --stop : un python3 orphelin (pont, fake_trx, trxcon)
# garde son port et fait echouer le demarrage suivant sur « port UDP
# 5720 deja pris ». PORTEE : tue TOUS les python3 de la machine.
if [ "${CALYPSO_STOP_KILL_PYTHON:-1}" != 0 ]; then
killall python3 2>/dev/null && \
say_end " OK " "$C_OK" "python3 restants termines (killall)"
fi
fi
# ── LE NOEUD, PAR DEFAUT 1 ──────────────────────────────────────────────
# Une regeneration produit une identite COMPLETE, jamais un plan « sans
# noeud ». Sans numero, apply_config_templates rendait le plan du gabarit -
# point-code 1.1.2 pour toute machine - et deux noeuds du meme banc se
# retrouvaient a la meme adresse SS7. On numerote donc toujours : le noeud
# resolu plus haut (--node, l'environnement, /etc/osmo-role, la table WAN),
# et 1 quand il n'y en a aucun. Les configurations s'incrementent alors
# avec lui : PC 1.<noeud><op>.<role>, routing context <noeud>*1000+..., et
# le segment prive 192.168.<noeud+1>.x.
REGEN_NODE="${NODE_ID:-1}"
if ! [[ "$REGEN_NODE" =~ ^[1-9]$ ]]; then
say_end " KO " "$C_KO" "--regen" "numero de noeud invalide : $REGEN_NODE (1 a 9)"
exit 2
fi
REGEN_OP="${NODE_OP:-1}"
if ! [[ "$REGEN_OP" =~ ^[1-9]$ ]]; then
say_end " KO " "$C_KO" "--regen" "numero d'operateur invalide : $REGEN_OP (1 a 9)"
exit 2
fi
# cached_hub est LOCAL a ask_node_identity : le lire ici rendait le vide et,
# sans inter-STP, l'ASP repartait en shutdown apres chaque regeneration.
REGEN_HUB="${HUB_IP:-${OSMO_HUB_IP:-}}"
[ -n "$REGEN_HUB" ] || REGEN_HUB="$(awk -F= '/^OSMO_HUB_IP=/{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' \
"${ROLE_FILE:-/etc/osmo-role}" 2>/dev/null)"
say_begin "Regeneration des configurations (noeud $REGEN_NODE, operateur $REGEN_OP)"
if [ "$DRY" -eq 1 ]; then
say_end " -- " "$C_DIM" "Regeneration des configurations" "dry-run"
elif regen_configs_natif "$REGEN_NODE" "$REGEN_OP" "$REGEN_HUB"; then
say_end " OK " "$C_OK" "Regeneration des configurations" \
"/etc/osmocom + /etc/asterisk · PC $(_pc_affiche "$REGEN_NODE" "$REGEN_OP")"
else
say_end " KO " "$C_KO" "Regeneration des configurations" \
"echec - les fichiers d'avant sont dans ${LOG_DIR:-/tmp}"
exit 1
fi
# ── LE HLR SUIT LA NUMEROTATION ─────────────────────────────────────────
# Une regeneration change les MSISDN (<noeud>00<operateur><rang>) : ceux du
# HLR datent du plan precedent. L'IMSI, lui, ne bouge pas - il ne porte pas
# le noeud, precisement pour que le Ki et l'authentification restent
# valables. L'etape d'abonnes ne peut donc pas se contenter de constater
# que l'IMSI existe : elle relit desormais AUSSI le MSISDN
# (run_modules/21-abonnes-hlr.sh, mod_abonnes_hlr_status) et rejoue
# "subscriber ... update msisdn" quand il a change.
#
# On la force en plus ici : la pile vient d'etre arretee, le HLR repartira
# avec run.sh, et l'etape doit s'executer meme si un cache d'etat la croit
# deja faite. Sans ca, l'abonne s'attachait (IMSI connu) mais restait
# injoignable, sans qu'une ligne ne dise que son numero avait change.
FORCE=1
printf ' %sHLR%s abonnes a reapprovisionner : MSISDN %s..%s\n' \
"${C_DIM:-}" "${C_Z:-}" \
"$(( REGEN_NODE * 100000 + REGEN_OP * 100 + 1 ))" \
"$(( REGEN_NODE * 100000 + REGEN_OP * 100 + ${N_MS:-2} ))"
# Le noeud retenu vaut pour la suite du lancement : c'est celui qui vient
# d'etre ecrit dans les fichiers. Le laisser vide ici, c'est laisser la
# section « Identite SS7 » plus bas repartir sur une autre resolution et
# reecrire par-dessus une identite differente de celle qu'on vient de poser.
NODE_ID="$REGEN_NODE"
NODE_OP="$REGEN_OP"
[ -n "$REGEN_HUB" ] && HUB_IP="${HUB_IP:-$REGEN_HUB}"
export OSMO_ROLE=operator
export OSMO_WAN_NODE="$NODE_ID"
[ -n "$HUB_IP" ] && export OSMO_HUB_IP="$HUB_IP"
elif [ "${RUNTIME_ENV:-native}" = "docker" ]; then
# DANS UN CONTENEUR seulement. Les configs y sont deja ecrites par start.sh,
# avec l'identite du noeud : les regenerer les remplace par les valeurs des
# gabarits, et l'interco meurt.
export OSMO_NO_REGEN=1
else
# SUR UNE VM sans --regen : on CONSERVE ce qui est en place. C'est l'ISO qui
# a genere ces fichiers a la construction, avec la meme recette que
# regen_configs_natif ; les refaire a chaque demarrage ne changerait rien et
# ecraserait toute retouche faite sur le banc. --regen est la pour ca, et il
# le dit.
if [ -n "$NODE_ID" ]; then
export OSMO_ROLE=operator
export OSMO_WAN_NODE="$NODE_ID"
[ -n "$HUB_IP" ] && export OSMO_HUB_IP="$HUB_IP"
fi
fi
# --- 7ter. Identite de noeud (--node) -------------------------------------------
# AVANT le WAN et avant run.sh : les point codes sont lus par osmo-stp, osmo-msc
# et osmo-bsc a leur demarrage. Les changer apres ne servirait a rien.
#
# UNE SEULE SOURCE DE VERITE. start.sh calcule deja l'identite d'un conteneur et
# la lui passe par l'environnement ; la recalculer ici en donnerait deux, et
# c'est la mauvaise qui gagnait. On reprend donc la sienne quand elle existe.
if [ -z "$NODE_ID" ] && [ -n "${OSMO_WAN_NODE:-${WAN_NODE_ID:-}}" ]; then
NODE_ID="${OSMO_WAN_NODE:-$WAN_NODE_ID}"
[ -n "$HUB_IP" ] || HUB_IP="${OSMO_HUB_IP:-}"
fi
# Sans identite alors que le WAN est demande, on ne part PAS en silence : la
# pile demarrerait avec les point codes du gabarit (1.1.2 pour tout le monde),
# deux noeuds porteraient la meme adresse SS7, et aucun ASP ne s'attacherait -
# sans qu'une seule ligne ne le signale.
if [ -z "$NODE_ID" ] && [ "${WAN_MESH:-0}" -eq 1 ]; then
say_end " KO " "$C_KO" "Identite SS7" "WAN demande mais aucun noeud : --node N, ou WAN_NODE_ID"
exit 1
fi
if [ -n "$NODE_ID" ]; then
if ! [[ "$NODE_ID" =~ ^[1-9]$ ]]; then
say_end " KO " "$C_KO" "--node" "un chiffre de 1 a 9"; exit 2
fi
SETID="$HERE/network/set-node-id.sh"
if [ ! -r "$SETID" ]; then
say_end " KO " "$C_KO" "--node" "network/set-node-id.sh absent"; exit 1
fi
say_begin "Identite SS7 du noeud $NODE_ID"
setid_args=(--node "$NODE_ID" --op "$NODE_OP" --mode "$NODE_MODE")
# ── PLAN LOCAL HORS WAN ─────────────────────────────────────────────────
# [2026-08-31] --local n etait JAMAIS passe. set-node-id.sh prenait donc
# toujours le plan WAN, MID=<noeud><op> :
# noeud 1 / op 2 -> 1.12.2 noeud 1 / op 3 -> 1.13.2
# alors que le hub et la topologie du banc attendent le plan LOCAL,
# MID=<op> :
# op 2 -> 1.2.2 op 3 -> 1.3.2
# Les conteneurs s enregistraient donc en 1.12.2 / 1.13.2 (visible cote hub
# sous as-rkm-1250 / as-rkm-1350, ACTIFS), pendant que les AS statiques
# as-op2 / as-op3 attendaient en 1.2.2 / 1.3.2 et restaient AS_DOWN.
# Consequence : tout le monde attache, personne routable - les routes vers
# 1.2.2 et 1.3.2 sortaient PROHIB chez le natif. Rien ne signalait le
# desaccord de plan : les deux cotes avaient raison, chacun dans le sien.
#
# Le plan WAN n a de sens qu ENTRE MACHINES. Sans WAN, une seule machine
# porte tout : c est le plan local qui vaut, et c est deja celui du natif.
if [ "${WAN_MESH:-0}" != "1" ] && [ -z "${WAN_NODES:-}" ]; then
setid_args+=(--local)
fi
[ -n "$HUB_IP" ] && setid_args+=(--hub-ip "$HUB_IP")
if [ "$DRY" -eq 1 ]; then
say_end " -- " "$C_DIM" "Identite SS7 du noeud $NODE_ID" "dry-run"
bash "$SETID" "${setid_args[@]}" --dry-run 2>&1 | sed 's/^/ /'
elif bash "$SETID" "${setid_args[@]}" </dev/null > "${LOG_DIR:-/tmp}/set-node-id.log" 2>&1; then
say_end " OK " "$C_OK" "Identite SS7" "noeud $NODE_ID · PC $(_pc_affiche "$NODE_ID" "$NODE_OP") · mode $NODE_MODE"
# Ce qui a REELLEMENT ete ecrit, relu dans le fichier. Jusqu'ici il
# fallait deduire l'identite d'un routing-key pour savoir si elle avait
# pris - et une regeneration ulterieure pouvait l'effacer sans bruit.
_pc_now="$(awk "/^cs7 instance/{c=1} c && /^ *point-code /{print \$2; exit}" \
"${OSMOCOM_CFG:-/etc/osmocom}/osmo-stp.cfg" 2>/dev/null)"
_hub_now="$(awk "/asp asp-to-inter/{f=1} f&&/remote-ip/{print \$2; exit}" \
"${OSMOCOM_CFG:-/etc/osmocom}/osmo-stp.cfg" 2>/dev/null)"
printf " %sSS7%s point-code %s inter-STP %s\n" \
"${C_DIM:-}" "${C_Z:-}" "${_pc_now:-?}" "${_hub_now:-?}"
else
say_end " KO " "$C_KO" "Identite SS7" "voir ${LOG_DIR:-/tmp}/set-node-id.log"
exit 1
fi
# Le numero de noeud vaut aussi pour la voix et les SMS : c'est le meme noeud.
WAN_NODE_ID="$NODE_ID"
fi
# --- 7bis. WAN a N noeuds ------------------------------------------------------
# ICI et pas apres : ce processus va devenir run.sh (exec), il n'y a pas d'apres.
# Asterisk n'est pas encore lance - setup-wan-mesh.sh le voit, ecrit la conf et
# ne redemarre rien ; run.sh demarrera Asterisk avec le WAN deja en place.
#
# Sur vis-a-vis des gabarits : en natif rien ne reinstalle /etc/asterisk au
# demarrage (install_configs_native n'a aucun appelant, run_modules/08-gabarits.sh
# n'existe pas), donc le bloc WAN ecrit ici survit a run.sh. Si un module de
# gabarits revient un jour, il faudra rejouer le WAN APRES lui.
if [ -r "${WAN_CONF_FILE:-/etc/osmo-wan.conf}" ] && [ "$WAN_MESH" -eq 0 ]; then
# Table figee dans l'image (ISO construite avec --wan) : WAN_AUTO=1 dit
# "ce systeme EST un noeud", on n'oblige pas a retaper l'option.
if grep -q '^WAN_AUTO=1' "${WAN_CONF_FILE:-/etc/osmo-wan.conf}" 2>/dev/null; then
WAN_MESH=1
printf ' %sWAN%s table figee dans %s (WAN_AUTO=1)\n' \
"$C_DIM" "$C_Z" "${WAN_CONF_FILE:-/etc/osmo-wan.conf}"
fi
fi
if [ "$WAN_MESH" -eq 1 ] && [ "$ACTION" = "start" ]; then
WAN_LIB="$HERE/network/wan-nodes.sh"
WAN_MESH_SH="$HERE/network/setup-wan-mesh.sh"
if [ ! -r "$WAN_LIB" ] || [ ! -r "$WAN_MESH_SH" ]; then
say_end " KO " "$C_KO" "WAN" "network/wan-nodes.sh ou setup-wan-mesh.sh absent"
exit 1
fi
# shellcheck source=network/wan-nodes.sh
. "$WAN_LIB"
# --virtualbox : segment host-only + VM + table, avant tout le reste.
if [ "$VBOX_INTERCO" -eq 1 ] && [ -z "${WAN_NODES:-}" ]; then
VB="$HERE/network/setup-vbox-interco.sh"
[ -r "$VB" ] || { say_end " KO " "$C_KO" "WAN" "network/setup-vbox-interco.sh absent"; exit 1; }
vb_args=(--host-node "$VBOX_HOST_NODE" --conf "${WAN_CONF_FILE:-/etc/osmo-wan.conf}")
[ -n "$VBOX_NODES" ] && vb_args+=(--nodes "$VBOX_NODES")
bash "$VB" "${vb_args[@]}" || exit 1
wan_nodes_load "${WAN_CONF_FILE:-/etc/osmo-wan.conf}" || exit 1
WAN_NODES="$(wan_nodes_spec)"
WAN_NODE_ID="$VBOX_HOST_NODE"
fi
if [ -n "${WAN_NODES:-}" ]; then
wan_nodes_parse "$WAN_NODES" || exit 1
# --node DIT DEJA quel noeud est cette machine : c'est la meme notion
# que --wan-id, vue depuis le SS7. S'en servir evite d'echouer sur une
# detection par IP quand la table embarquee est plus ancienne que la
# machine - le cas d'une ISO gravee avant le dernier plan d'adressage.
if [ "${WAN_NODE_ID:-0}" = "0" ] && [ -n "$NODE_ID" ]; then
WAN_NODE_ID="$NODE_ID"
printf ' %sWAN%s noeud %s (repris de --node)\n' "$C_DIM" "$C_Z" "$WAN_NODE_ID"
fi
[ "${WAN_NODE_ID:-0}" != "0" ] || wan_nodes_detect_self || {
printf ' %sWAN%s : aucune IP locale dans la table - passez --wan-id N\n' "$C_KO" "$C_Z"
printf ' %s (ou --node N : le numero de noeud vaut pour les deux)%s\n' "$C_DIM" "$C_Z"
exit 1; }
else
wan_nodes_load "${WAN_CONF_FILE:-/etc/osmo-wan.conf}" 2>/dev/null || true
# Une ISO diffusee sur N machines porte la MEME table : chaque machine
# se reconnait a son IP plutot que d'exiger une image par noeud.
wan_nodes_detect_self 2>/dev/null || true
if [ "${WAN_NODE_COUNT:-0}" -lt 1 ] || [ "${WAN_NODE_ID:-0}" = "0" ]; then
wan_nodes_prompt || exit 1
fi
fi
wan_nodes_validate || exit 1
printf '\n'
wan_nodes_summary
printf '\n'
if [ "$DRY" -eq 1 ]; then
printf ' [dry-run] %s --native --id %s\n' "$WAN_MESH_SH" "$WAN_NODE_ID"
else
say_begin "WAN - application de la table"
# --native / --docker : meme distinction que pour les point codes.
if WAN_AUTO=1 bash "$WAN_MESH_SH" "--$NODE_MODE" --id "$WAN_NODE_ID" \
--nodes "$(wan_nodes_spec)" --ops 1 \
--config "${WAN_CONF_FILE:-/etc/osmo-wan.conf}" > "${LOG_DIR:-/tmp}/wan-mesh.log" 2>&1; then
say_end " OK " "$C_OK" "WAN - noeud $WAN_NODE_ID, indicatif $(wan_local_ind)"
else
say_end " KO " "$C_KO" "WAN" "voir ${LOG_DIR:-/tmp}/wan-mesh.log"
fi
fi
# --- 7quater. Adressage SS7 du noeud (set_stp_ip.sh) ----------------------
# setup-wan-mesh.sh monte la VOIX et les SMS entre noeuds ; il ne touche pas
# au SS7. Sans ce qui suit, --wan donnait un WAN ou les appels passaient mais
# ou l'ASP inter-STP visait encore 127.0.0.1, en shutdown : l'interco SS7 ne
# montait jamais, et rien dans la sortie ne disait pourquoi.
#
# set_stp_ip.sh est le seul a ecrire d'un coup les trois configs, la table
# WAN et /etc/osmo-role. On lui passe --no-ip : l'adresse locale est deja
# posee (DHCP ou bloc WAN ci-dessus), et --restart serait absurde ici
# puisque run.sh va demarrer la pile juste apres, avec les fichiers corriges.
STP_IP_SH="$HERE/set_stp_ip.sh"
if [ -r "$STP_IP_SH" ] && [ "${WAN_NODE_ID:-0}" != "0" ]; then
self_ip="${WAN_IP[$WAN_NODE_ID]:-}"
hub_ip="$HUB_IP"
[ -n "$hub_ip" ] || hub_ip="$(awk -F= '/^OSMO_HUB_IP=/{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' /etc/osmo-role 2>/dev/null)"
stp_args=(--operator --node "$WAN_NODE_ID" --no-ip --conf-dir "${OSMOCOM_CFG:-/etc/osmocom}")
[ -n "$self_ip" ] && stp_args+=(--ip "$self_ip")
[ -n "$hub_ip" ] && stp_args+=(--hub-ip "$hub_ip")
if [ "$DRY" -eq 1 ]; then
printf ' [dry-run] %s %s\n' "$STP_IP_SH" "${stp_args[*]}"
else
say_begin "Adressage SS7 - inter-STP ${hub_ip:-?}"
# </dev/null : appel automatique, jamais interactif (set_stp_ip.sh
# demande l'adresse du hub quand elle manque - ici, personne ne
# repondrait et le lancement resterait fige).
if bash "$STP_IP_SH" "${stp_args[@]}" </dev/null > "${LOG_DIR:-/tmp}/set-stp-ip.log" 2>&1; then
say_end " OK " "$C_OK" "Adressage SS7" "noeud $WAN_NODE_ID -> inter-STP ${hub_ip:-?}"
else
# Non fatal : la pile locale tourne tres bien sans interco. On le
# DIT, plutot que de laisser croire a un WAN complet.
say_end " ~~ " "$C_SK" "Adressage SS7" "echec - voir ${LOG_DIR:-/tmp}/set-stp-ip.log"
fi
fi
fi
fi
# --- 7quinquies. Le wrapper tcpdump ------------------------------------------
# La capture GSMTAP est lancee par run_modules/75-gsmtap.sh, dans qosmo-grgsm : un
# autre depot, qu'on ne modifie pas. Elle ecrit un pcap NON BORNE, et la racine
# du live etant un tmpfs, une capture un peu chargee finit par remplir la RAM.
#
# Le wrapper y substitue un anneau. Il est aussi pose par build-iso.sh, mais une
# ISO deja gravee ne le connait pas, et un live sans persistance le perd a chaque
# reboot : le reposer ICI, a chaque demarrage de la pile, est ce qui le rend
# effectif sans reconstruire ni toucher a qosmo-grgsm.
#
# DEUX PIEGES, DEUX PARADES - c'est tout l'objet de ce script :
# -Z root : avec -C/-W, tcpdump cree les membres suivants de l'anneau APRES
# avoir lache ses privileges. Sans lui : "Permission denied", et
# AUCUNE capture - alors que sans -C il ouvrait le fichier avant.
# le lien : avec -C/-W, tcpdump numerote (capture.pcap0, .pcap1...) ; le nom
# exact demande n'existe jamais, et la barriere du module - un
# "test -s <nom exact>" - conclut a un echec sur une capture qui
# tourne. On maintient <nom exact> -> membre courant.
#
# OSMO_NO_PCAP_RING=1 saute l'installation.
install_pcap_wrapper() {
local dst=/usr/local/bin/tcpdump tmp
[ "${OSMO_NO_PCAP_RING:-0}" = "1" ] && return 0
[ "$(id -u)" -eq 0 ] || return 0
command -v tcpdump >/dev/null 2>&1 || return 0
tmp="$(mktemp)" || return 0
cat > "$tmp" <<'PCAPWRAP'
#!/bin/sh
# tcpdump - wrapper osmo-operator : impose un anneau aux captures sur fichier.
# Pose par start-direct.sh et par build-iso.sh. Voir leurs commentaires.
# Reglable : OSMO_PCAP_RING_MB (32), OSMO_PCAP_RING_FILES (5).
REAL=/usr/bin/tcpdump
[ -x "$REAL" ] || REAL=/usr/sbin/tcpdump
has_w=0; has_ring=0; has_z=0; wfile=""; next_is_w=0
for a in "$@"; do
if [ "$next_is_w" = 1 ]; then wfile="$a"; next_is_w=0; continue; fi
case "$a" in
-w) has_w=1; next_is_w=1 ;;
-w?*) has_w=1; wfile="${a#-w}" ;;
-C|-C*|-W|-W*) has_ring=1 ;;
-Z|-Z*) has_z=1 ;;
esac
done
# Rien a borner, ou l'appelant a deja choisi son anneau : on s'efface.
if [ "$has_w" != 1 ] || [ "$has_ring" = 1 ] || [ -z "$wfile" ]; then
exec "$REAL" "$@"
fi
# Le veilleur du lien. Lance AVANT l'exec : apres, ce processus EST tcpdump.
# $$ survit a l'exec, donc il suit exactement sa vie et s'arrete avec lui.
( ppid=$$
n=0
while [ "$n" -lt 300 ]; do
[ -e "${wfile}0" ] && break
kill -0 "$ppid" 2>/dev/null || exit 0
sleep 0.2; n=$((n + 1))
done
while kill -0 "$ppid" 2>/dev/null; do
newest=$(ls -t "${wfile}"[0-9]* 2>/dev/null | head -1)
if [ -n "$newest" ] && [ "$(readlink "$wfile" 2>/dev/null)" != "$newest" ]; then
ln -sfn "$newest" "$wfile"
fi
sleep 2
done ) >/dev/null 2>&1 &
if [ "$has_z" = 1 ]; then
exec "$REAL" -C "${OSMO_PCAP_RING_MB:-32}" -W "${OSMO_PCAP_RING_FILES:-5}" "$@"
fi
exec "$REAL" -Z root -C "${OSMO_PCAP_RING_MB:-32}" -W "${OSMO_PCAP_RING_FILES:-5}" "$@"
PCAPWRAP
# Le wrapper s'appelle "tcpdump" et vit dans /usr/local/bin, qui precede
# /usr/bin : il ne doit donc JAMAIS s'installer par-dessus le vrai binaire,
# sous peine de recursion infinie. On verifie ou pointe le vrai.
case "$(command -v tcpdump)" in
"$dst") ;; # deja le wrapper, on remplace
/usr/bin/tcpdump|/usr/sbin/tcpdump|/bin/tcpdump) ;;
*) rm -f "$tmp"; return 0 ;; # emplacement inattendu : on s'abstient
esac
if cmp -s "$tmp" "$dst" 2>/dev/null; then
rm -f "$tmp" # deja a jour, rien a dire
else
install -m 0755 "$tmp" "$dst" 2>/dev/null \
&& printf ' %spcap%s anneau de capture arme (%s)\n' \
"$C_DIM" "$C_Z" "$dst"
rm -f "$tmp"
fi
}
[ "$DRY" -eq 1 ] || install_pcap_wrapper
# --- 7ter. ARRET SYSTEMATIQUE AVANT DEMARRAGE ---------------------------------
# Tout demarrage arrete d'abord la pile. Ce n'est pas de la prudence : sans cela
# le lanceur differe du pont part sur un etat ambigu.
#
# CE QUE CA CORRIGE. La barriere du pont attend osmo-bts-trx pour savoir que les
# ports 5700-5702 sont a lui. Si un run precedent tourne encore, son osmo-bts-trx
# est deja la : la barriere est franchie a l'instant meme, le pont binde EN PLEIN
# teardown de run.sh, qui le tue derriere (f:pont/pont.py est dans _td_patterns)
# -- et rien ne le relance. Symptome : « le pont n'a pas de bursts », sans un
# message pour le dire. La barriere attend desormais les deux fronts (disparition
# puis apparition), mais partir d'une pile arretee rend la question sans objet.
#
# Et cela libere les ports : un fake_trx ou un trxcon orphelin suffit a faire
# echouer le demarrage suivant sur « port UDP 5720 deja pris » -- constate le
# 27/08. Le killall couvre les processus Python que le registre ne connait pas.
#
# CALYPSO_NO_AUTOSTOP=1 desactive ce comportement.
if [ "$DRY" -eq 0 ] && [ "${CALYPSO_NO_AUTOSTOP:-0}" != 1 ]; then
say_begin "Arret de la pile avant demarrage"
bash "$RUN_SH" --stop --profile "$CALYPSO_PROFILE" >/dev/null 2>&1 || true
declare -F purge_sessions_tmux >/dev/null && purge_sessions_tmux
if [ "${CALYPSO_STOP_KILL_PYTHON:-1}" != 0 ]; then
killall python3 2>/dev/null || true
fi
say_end " OK " "$C_OK" "Arret de la pile avant demarrage"
fi
# --- 8. lancement : exec run.sh -----------------------------------------------
# ── LES ADRESSES QUE LES CONFIGS RECLAMENT DOIVENT EXISTER ──────────────────
# Un demon qui se lie a une adresse absente ne demarre pas, et il le dit dans
# SON journal, pas dans le notre :
# osmo-ggsn : « gtp bind-ip 192.168.2.10 » -> adresse introuvable localement
# osmo-sgsn : « listen 192.168.2.10 23000 » -> idem
# Ces adresses viennent du generateur de gabarits (op_private_ip), pas du
# reseau : rien ne les pose. Elles etaient figees dans 20-dhcp.network cote ISO,
# donc absentes de toute machine montee autrement - et la panne se lisait comme
# un defaut de coeur paquet.
#
# osmo-ip-plan.sh les EXTRAIT des configs plutot que de les recalculer (le plan
# peut venir d'un autre generateur que le notre), choisit la carte qui fournit
# reellement Internet, et retombe sur la boucle locale quand aucune ne mene
# nulle part. Il ne fait rien dans un conteneur : la, c'est docker qui adresse.
if [ "$ACTION" = "start" ] && [ "$DRY" -ne 1 ]; then
IPPLAN="$HERE/network/osmo-ip-plan.sh"
if [ -r "$IPPLAN" ]; then
say_begin "Adresses privees du noeud"
_ipout="$(OSMOCOM_CFG="${OSMOCOM_CFG:-/etc/osmocom}" \
OSMO_WAN_NODE="${NODE_ID:-${OSMO_WAN_NODE:-}}" \
bash "$IPPLAN" --apply 2>&1)" || true
say_end " OK " "$C_OK" "Adresses privees du noeud" \
"$(printf '%s' "$_ipout" | tail -1 | sed 's/^osmo-ip-plan : //')"
fi
fi
say_begin "Transmission a run.sh"
if [ $DRY -eq 1 ]; then
say_end " -- " "$C_DIM" "Transmission a run.sh" "dry-run"
printf ' commande : CALYPSO_PROFILE=%s bash %s %s\n' \
"$CALYPSO_PROFILE" "$RUN_SH" "${RUN_ARGS[*]}"
exit 0
fi
say_end " OK " "$C_OK" "Transmission a run.sh" "profil=$CALYPSO_PROFILE"
# Hand-off total : ce processus devient run.sh
exec bash "$RUN_SH" "${RUN_ARGS[@]}"start-interstp.sh
start-interstp.sh
#!/bin/bash
# =============================================================================
# start-interstp.sh - lance le NOEUD INTER-STP : le hub SS7 commun au WAN
# =============================================================================
#
# Dans un lab a une seule machine, l'inter-STP est un conteneur parmi d'autres,
# a 172.20.0.10, et il ne dessert que les operateurs de CETTE machine. Des qu'on
# repartit les operateurs sur plusieurs noeuds, il devient ce qu'il est vraiment :
# un equipement a part, avec sa propre adresse, auquel tous les noeuds
# s'attachent. C'est lui - et lui seul - qui fait traverser le SS7 au WAN.
#
# noeud 1 (1.11.2) ─┐
# noeud 2 (1.21.2) ─┼─ M3UA/SCTP 2908 ─► inter-STP 192.168.1.49 PC 0.0.0
# noeud 3 (1.31.2) ─┘
#
# LE PLAN DE POINT CODES, ET POURQUOI IL CHANGE
# Sur une machine isolee, chaque operateur N porte 1.N.2 - et cette formule se
# recalcule a l'identique partout. Trois noeuds attaches au meme hub y
# presenteraient donc trois fois le meme point code. Un point code est une
# ADRESSE : deux equipements qui partagent la leur, ce n'est pas un conflit de
# nom, c'est du routage faux. Le plan WAN encode donc le noeud dedans :
#
# PC = 1.<noeud><op>.<role> role 1=MSC 2=STP 3=BSC
# RCTX = noeud*1000 + op*100 + 50
#
# Usage :
# sudo ./start-interstp.sh [--nodes 3] [--ops 1] [--ip 192.168.1.49]
# sudo ./start-interstp.sh --status | --stop
#
# --nodes N nombre de noeuds desservis (1-9, defaut : lu dans /etc/osmo-wan.conf)
# --ops K operateurs par noeud (1-9, defaut 1)
# --ip ADRESSE adresse du hub ; posee sur l'interface si elle manque
# --iface NOM interface ou poser l'adresse (defaut : detectee)
# --port N port M3UA (defaut 2908)
# --menu pose les questions meme si une table existe deja
# --defaut applique la table du banc, en ignorant celle deja posee
# --ips "a b c" les IP des noeuds operateurs, dans l'ordre (sans question)
# --no-ip ne touche pas a la configuration reseau
# --foreground reste au premier plan (journal a l'ecran)
# =============================================================================
set -uo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF_DIR="${CONF_DIR:-/etc/osmocom}"
CFG="${CONF_DIR}/osmo-stp-interop.cfg"
LOG="${LOG:-/var/log/osmocom/osmo-stp-interop.log}"
PIDFILE="/run/osmo-interstp.pid"
NODES=""
OPS=1
MENU=0
USE_DEFAULTS=0
IPS=""
# Le hub du banc, en acces par pont. L'ancien defaut (192.168.56.1, host-only
# VirtualBox) n'existe sur aucun segment des que les VM sont pontees : le hub
# s'y liait sans que personne ne puisse l'atteindre.
HUB_IP="192.168.1.49"
# ── La table du banc, defaut de tous les prompts ────────────────────────────
# Les conteneurs d'abord, dans l'ordre de leur nom, puis les machines distantes.
# C'est le meme ordre que start.sh et build-iso.sh : une table lue ici doit
# ressembler a celle qu'ils ecrivent, sinon les point codes d'un cote ne
# correspondent plus aux AS declares de l'autre.
# noeud 1 172.20.0.11 osmo-operator-1 (conteneur)
# noeud 2 172.20.0.12 osmo-operator-2 (conteneur)
# noeud 3 192.168.1.2 la VM
WAN_NODES_DEFAULT="1:172.20.0.11:11 2:172.20.0.12:22 3:192.168.1.2:33"
# L'adresse par defaut du noeud i, lue dans cette table.
_default_node_ip() {
printf '%s' "$WAN_NODES_DEFAULT" | tr ' ' '\n' \
| awk -F: -v n="$1" '$1==n{print $2; exit}'
}
IFACE=""
PORT=2908
SET_IP=1
FOREGROUND=0
ACTION=start
while [ $# -gt 0 ]; do
case "$1" in
--nodes) NODES="${2:-}"; shift ;;
--nodes=*) NODES="${1#*=}" ;;
--ops) OPS="${2:-1}"; shift ;;
--ops=*) OPS="${1#*=}" ;;
--ip) HUB_IP="${2:-}"; shift ;;
--ip=*) HUB_IP="${1#*=}" ;;
--iface) IFACE="${2:-}"; shift ;;
--iface=*) IFACE="${1#*=}" ;;
--port) PORT="${2:-2908}"; shift ;;
--port=*) PORT="${1#*=}" ;;
--menu) MENU=1 ;;
--defaut|--default) USE_DEFAULTS=1 ;;
--ips) IPS="${2:-}"; shift ;;
--ips=*) IPS="${1#*=}" ;;
--no-ip) SET_IP=0 ;;
--foreground) FOREGROUND=1 ;;
--status) ACTION=status ;;
--stop) ACTION=stop ;;
-h|--help) sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo -e "${RED}option inconnue : $1${NC}" >&2; exit 2 ;;
esac
shift
done
banner() {
echo -e "${CYAN}${BOLD}"
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ Inter-STP - hub SS7 du WAN osmo-operator · PC 0.0.0 ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
stp_pid() {
if [ -r "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
cat "$PIDFILE"; return 0
fi
pgrep -f 'osmo-stp .*osmo-stp-interop\.cfg' | head -1
}
# ── status ───────────────────────────────────────────────────────────────────
if [ "$ACTION" = "status" ]; then
banner
pid="$(stp_pid)"
if [ -z "$pid" ]; then
echo -e " ${RED}✗${NC} inter-STP arrete"; exit 1
fi
echo -e " ${GREEN}✓${NC} inter-STP actif (pid ${pid})"
echo -e " Ecoute : $(ss -lna 2>/dev/null | grep -E "[:.]${PORT}\b" | head -3 | tr -s ' ' | cut -d' ' -f1,5 | tr '\n' ' ')"
echo ""
# Le VTY est la seule source de verite sur QUI est attache : un ASP peut
# avoir ouvert sa SCTP sans jamais passer ASP-ACTIVE, et rien d'autre ne le
# montre. Le port ouvert, lui, ne prouve que l'ecoute.
if command -v nc >/dev/null 2>&1; then
echo -e " ${BOLD}ASP attaches :${NC}"
printf 'enable\nshow cs7 instance 0 asp\nshow cs7 instance 0 as all\nexit\n' \
| nc -q2 127.0.0.1 4239 2>/dev/null | sed 's/^/ /' | grep -vE '^\s*$' | head -40 \
|| echo -e " ${YELLOW}VTY 4239 muet${NC}"
else
echo -e " ${YELLOW}nc absent : ${CYAN}telnet 127.0.0.1 4239${NC} puis "show cs7 instance 0 asp""
fi
exit 0
fi
# ── stop ─────────────────────────────────────────────────────────────────────
if [ "$ACTION" = "stop" ]; then
pid="$(stp_pid)"
if [ -n "$pid" ]; then
kill "$pid" 2>/dev/null; sleep 1
kill -9 "$pid" 2>/dev/null || true
rm -f "$PIDFILE"
echo -e "${GREEN}Inter-STP arrete.${NC}"
else
echo -e "${YELLOW}Inter-STP deja arrete.${NC}"
fi
exit 0
fi
banner
[ "$(id -u)" -eq 0 ] || { echo -e "${RED}Root requis.${NC}" >&2; exit 1; }
command -v osmo-stp >/dev/null 2>&1 || {
echo -e "${RED}osmo-stp introuvable.${NC} Ce noeud doit porter la pile Osmocom." >&2; exit 1; }
# ══════════════════════════════════════════════════════════════════════════════
# Qui sont les noeuds ? - le menu
# ══════════════════════════════════════════════════════════════════════════════
# Le hub n'a pas besoin des IP pour ROUTER : il ecoute, les ASP viennent a lui
# (accept-asp-connections dynamic-permitted) et c'est le point code qui decide
# du reste. Il en a besoin pour trois choses concretes :
# • ouvrir son pare-feu a ces adresses et a elles seules ;
# • ecrire /etc/osmo-wan.conf, la table que liront les noeuds et le diagnostic ;
# • dire, dans --status, QUI devrait etre attache - sans cette liste, un noeud
# absent est indiscernable d'un noeud qui n'existe pas.
# shellcheck source=network/wan-nodes.sh
. "$HERE/network/wan-nodes.sh"
ask_ips() {
local n i ip def
echo -e "${BOLD}Configuration du hub SS7${NC}"
echo -e " Saisissez les adresses des noeuds operateurs, dans l'ordre."
echo ""
n=$(_wan_ask "Inter-STP" "Nombre de noeuds operateurs (1-9) :" "${NODES:-3}") || exit 1
[[ "$n" =~ ^[1-9]$ ]] || { echo -e "${RED}Nombre invalide : $n${NC}" >&2; exit 2; }
WAN_IP=(); WAN_IND=(); WAN_NODE_LIST=()
for i in $(seq 1 "$n"); do
# Defaut : la table du banc. Le repli ".1N sur le segment du hub" ne
# vaut plus rien depuis que les deux premiers noeuds sont des
# conteneurs, sur le backbone docker et non sur le LAN.
def="$(_default_node_ip "$i")"
[ -n "$def" ] || def="${HUB_IP%.*}.$((10 + i))"
ip=$(_wan_ask "Noeud $i/$n" "IP du noeud operateur $i :" "$def") || exit 1
[[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|| { echo -e "${RED}'$ip' n'est pas une IPv4${NC}" >&2; exit 2; }
WAN_IP[$i]="$ip"; WAN_IND[$i]="$(wan_default_ind "$i")"; WAN_NODE_LIST+=("$i")
done
WAN_NODE_COUNT="$n"; NODES="$n"
# Le hub ne porte aucun operateur : il n'est pas un noeud de la table. On note
# 0, que wan_nodes_validate accepte, plutot qu'un numero qui le ferait
# ressembler a un noeud et lui volerait un point code.
WAN_NODE_ID=0
WAN_OPS="$OPS"
}
# --defaut : la table du banc, quoi qu'il y ait sur la machine.
# Sans cette option, une table deja posee dans /etc/osmo-wan.conf est rechargee
# telle quelle - c'est le bon comportement au quotidien, mais c'est aussi ce qui
# fait qu'un hub garde indefiniment des adresses perimees apres un changement de
# plan : le nouveau defaut du script n'est jamais consulte.
if [ "$USE_DEFAULTS" = "1" ] && [ -z "$IPS" ]; then
IPS="$(printf '%s' "$WAN_NODES_DEFAULT" | tr ' ' '\n' | awk -F: '{print $2}' | tr '\n' ' ')"
[ -n "$NODES" ] || NODES="$(printf '%s' "$WAN_NODES_DEFAULT" | wc -w)"
echo -e " ${CYAN}--defaut : table du banc${NC} ${WAN_NODES_DEFAULT}"
fi
if [ -n "$IPS" ]; then
# Forme scriptable : --ips "10.0.0.11 10.0.0.12 10.0.0.13"
WAN_IP=(); WAN_IND=(); WAN_NODE_LIST=(); i=0
for ip in $IPS; do
i=$((i+1))
WAN_IP[$i]="$ip"; WAN_IND[$i]="$(wan_default_ind "$i")"; WAN_NODE_LIST+=("$i")
done
WAN_NODE_COUNT="$i"; NODES="$i"; WAN_NODE_ID=0; WAN_OPS="$OPS"
elif [ "$MENU" = "1" ] || { [ -z "$NODES" ] && [ ! -r "$WAN_CONF_FILE" ]; }; then
if [ -t 0 ]; then
ask_ips
else
echo -e "${YELLOW}Pas de terminal et pas de table : on part sur 3 noeuds sans adresses.${NC}"
NODES=3
fi
elif [ -z "$NODES" ] && [ -r "$WAN_CONF_FILE" ]; then
wan_nodes_load "$WAN_CONF_FILE" 2>/dev/null || true
NODES="${WAN_NODE_COUNT:-3}"
OPS="${WAN_OPS:-$OPS}"
echo -e " Table lue dans ${CYAN}${WAN_CONF_FILE}${NC} : ${NODES} noeud(s)"
echo -e " ${CYAN}--menu${NC} pour la ressaisir."
fi
[[ "$NODES" =~ ^[1-9]$ ]] || { echo -e "${RED}--nodes : 1 a 9${NC}" >&2; exit 2; }
[[ "$OPS" =~ ^[1-9]$ ]] || { echo -e "${RED}--ops : 1 a 9${NC}" >&2; exit 2; }
# Table connue : on la persiste et on ouvre le pare-feu pour ces adresses.
if [ "${#WAN_NODE_LIST[@]}" -gt 0 ] && [ -n "${WAN_IP[1]:-}" ]; then
echo ""
wan_nodes_summary 2>/dev/null || true
WAN_AUTO=0 wan_nodes_save "$WAN_CONF_FILE" \
&& echo -e " ${GREEN}✓${NC} table ecrite dans ${CYAN}${WAN_CONF_FILE}${NC}"
if [ -x "$HERE/network/firewall-wan.sh" ] || [ -r "$HERE/network/firewall-wan.sh" ]; then
for i in "${WAN_NODE_LIST[@]}"; do
bash "$HERE/network/firewall-wan.sh" "${WAN_IP[$i]}" "$OPS" >/dev/null 2>&1 || true
done
echo -e " ${GREEN}✓${NC} pare-feu ouvert pour ${#WAN_NODE_LIST[@]} noeud(s)"
fi
fi
# ── L'adresse du hub ─────────────────────────────────────────────────────────
# Elle est ecrite en dur dans la config de CHAQUE noeud : si elle manque au
# demarrage, tous les ASP echouent a s'attacher et le seul symptome est un
# "connection refused" cote noeuds, jamais cote hub.
if [ "$SET_IP" = "1" ]; then
if ip -4 addr show 2>/dev/null | grep -q "inet ${HUB_IP}/"; then
echo -e " ${GREEN}✓${NC} ${CYAN}${HUB_IP}${NC} deja presente sur cette machine"
else
if [ -z "$IFACE" ]; then
# L'interface qui porte deja le meme /24 : c'est le segment du WAN.
IFACE="$(ip -4 -o addr show scope global 2>/dev/null \
| awk -v pfx="${HUB_IP%.*}." '$4 ~ "^"pfx {print $2; exit}')"
[ -z "$IFACE" ] && IFACE="$(ip -4 -o addr show scope global 2>/dev/null | awk 'NR==1{print $2}')"
fi
if [ -n "$IFACE" ] && ip addr add "${HUB_IP}/24" dev "$IFACE" 2>/dev/null; then
echo -e " ${GREEN}✓${NC} ${CYAN}${HUB_IP}/24${NC} posee sur ${IFACE}"
else
echo -e " ${YELLOW}⚠${NC} impossible de poser ${HUB_IP} (interface ${IFACE:-?})"
echo -e " Les noeuds ne pourront pas s'attacher tant qu'elle manque."
fi
fi
fi
# ── Config du hub ────────────────────────────────────────────────────────────
mkdir -p "$CONF_DIR" "$(dirname "$LOG")"
if [ ! -x "$HERE/helpers/create_interop.sh" ] && [ ! -r "$HERE/helpers/create_interop.sh" ]; then
echo -e "${RED}helpers/create_interop.sh introuvable${NC}" >&2; exit 1
fi
# --listen-ip : le hub se lie a SON adresse, pas a 0.0.0.0. Sans elle, SCTP
# annonce toutes les adresses de la machine dans son INIT et les noeuds tentent
# des chemins morts - l'ASP monte puis retombe, en boucle.
bash "$HERE/helpers/create_interop.sh" --listen-ip "$HUB_IP" \
--wan "$NODES" "$OPS" "$CFG" || exit 1
[ "$PORT" = "2908" ] || sed -i "s/^ listen m3ua 2908/ listen m3ua ${PORT}/" "$CFG"
echo ""
# ── Lancement ────────────────────────────────────────────────────────────────
if pid="$(stp_pid)" && [ -n "$pid" ]; then
echo -e " ${YELLOW}Un inter-STP tourne deja (pid ${pid}) - arret puis relance.${NC}"
kill "$pid" 2>/dev/null; sleep 1; kill -9 "$pid" 2>/dev/null || true
fi
echo -e " ${BOLD}Noeuds desservis :${NC} ${NODES} × ${OPS} operateur(s)"
for n in $(seq 1 "$NODES"); do
for o in $(seq 1 "$OPS"); do
printf ' noeud %s op %s → STP %s MSC %s BSC %s (rctx %s)\n' \
"$n" "$o" "1.${n}${o}.2" "1.${n}${o}.1" "1.${n}${o}.3" "$(( n*1000 + o*100 + 50 ))"
done
done
echo ""
if [ "$FOREGROUND" = "1" ]; then
echo -e " ${GREEN}osmo-stp au premier plan - Ctrl-C pour arreter${NC}"
exec osmo-stp -c "$CFG"
fi
osmo-stp -c "$CFG" >> "$LOG" 2>&1 &
echo $! > "$PIDFILE"
sleep 2
if kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo -e " ${GREEN}✓ inter-STP lance${NC} - M3UA ${CYAN}${HUB_IP}:${PORT}${NC}, VTY 4239"
echo -e " Journal : ${CYAN}${LOG}${NC}"
echo ""
echo -e " ${BOLD}Cote noeuds :${NC} leur osmo-stp doit viser ${CYAN}${HUB_IP}${NC}"
echo -e " ISO construite avec ${CYAN}--role operator --node N${NC} : deja fait."
echo -e " ${BOLD}Verifier qui est attache :${NC} ${CYAN}./start-interstp.sh --status${NC}"
else
echo -e " ${RED}✗ osmo-stp n'a pas demarre${NC} - ${CYAN}tail -30 ${LOG}${NC}"
tail -15 "$LOG" 2>/dev/null | sed 's/^/ /'
exit 1
fistart-multi.sh
start-multi.sh
#!/bin/bash
# =============================================================================
# start-multi.sh - LE BANC MULTI-OPERATEUR : natif + docker, relies en SS7
#
# op 1 NATIF deja en place (start-direct.sh) PC 1.1.2
# op 2 DOCKER conteneur PC 1.2.2 172.20.0.12
# op 3 DOCKER conteneur PC 1.3.2 172.20.0.13
# hub DOCKER inter-STP PC 0.0.0 172.20.0.10
# M3UA/SCTP 2908
#
# CE SCRIPT NE REIMPLEMENTE RIEN. start.sh sait deja lancer des operateurs en
# conteneur, poser le hub (--hub-ip) et DECLARER un operateur sans lui lancer de
# conteneur (--operator IP:PREFIXE) - ce dernier point est exactement le cas du
# natif, qui tourne deja et qu on veut seulement raccorder. Refaire ce travail
# ici donnerait une seconde formule a maintenir, et le depot montre deja ou cela
# mene : start.sh et lib/gabarits.sh portent les MEMES fonctions en double.
# start-multi.sh se contente donc de trois choses : verifier, traduire la
# topologie en options, et deleguer.
#
# LA TOPOLOGIE VIENT DE addition.sh, pas d ici : /etc/osmocom/osmo-multi.conf.
# Un seul endroit la calcule.
#
# Usage :
# sudo ./start-multi.sh monte le banc multi-operateur
# sudo ./start-multi.sh --status etat des conteneurs et du hub
# sudo ./start-multi.sh --stop arrete les conteneurs (le natif reste)
# sudo ./start-multi.sh --dry-run affiche la commande sans la lancer
# =============================================================================
set -uo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
MULTI_CONF="${MULTI_CONF:-/etc/osmocom/osmo-multi.conf}"
ACTION="start"
for a in "$@"; do
case "$a" in
--status) ACTION="status" ;;
--stop) ACTION="stop" ;;
--dry-run) ACTION="dry" ;;
-h|--help) sed -n '2,29p' "$0"; exit 0 ;;
esac
done
# ── LANCE PAR UNE ICONE : TERMINAL ET PRIVILEGES ────────────────────────────
# [2026-08-31] Sans ce preambule, l antenne bleue du bureau ne faisait RIEN de
# visible. Le .desktop est en Terminal=false ; le script exigeait root et
# ecrivait son refus sur stdout, c est-a-dire nulle part. Un double-clic
# lancait donc bien le script, qui sortait aussitot sur "Root requis" sans une
# fenetre ni un message : l icone paraissait morte, alors qu elle marchait.
# launch.sh avait deja ce traitement ; celui-ci ne l avait pas.
#
# On teste la presence d un TTY, pas $DISPLAY : c est la difference reelle
# entre "lance a la main" et "lance par une icone".
# ⚠️ SEULEMENT pour le LANCEMENT. --status, --dry-run et --help sont des
# lectures : les rejouer dans un terminal graphique ouvrirait une fenetre a
# chaque appel depuis un script ou un pipe - et leur sortie n irait pas a
# l appelant, qui n obtiendrait RIEN. Constate en les testant d ici.
_multi_readonly=0
case " $* " in
*" --status "*|*" --dry-run "*|*" -h "*|*" --help "*) _multi_readonly=1 ;;
esac
# [2026-08-31] stdin ET stdout : `[ ! -t 0 ]` seul prenait un
# `./start-multi.sh < fichier` lance depuis un vrai terminal pour un
# double-clic, et ouvrait une fenetre dont la sortie n allait pas a l appelant.
_multi_besoin=0
if [ "$_multi_readonly" = "0" ]; then
{ [ ! -t 0 ] || [ ! -t 1 ]; } && _multi_besoin=1
fi
if [ "$_multi_besoin" = "1" ] && [ "${OSMO_MULTI_TERM:-0}" != "1" ]; then
export OSMO_MULTI_TERM=1
for _t in gnome-terminal xfce4-terminal konsole xterm; do
command -v "$_t" >/dev/null 2>&1 || continue
case "$_t" in
gnome-terminal) exec "$_t" --title="osmo-operator multi-operateur" -- "$0" "$@" ;;
*) exec "$_t" -T "osmo-operator multi-operateur" -e "$0" "$@" ;;
esac
done
fi
# Le drapeau ne sert qu a ne pas boucler sur la relance. On le RETIRE de
# l environnement : exporte, il descendait dans start.sh, dans le raccourci du
# natif et jusqu a launch.sh, ou il n a aucun sens. La valeur reste ici, dans
# une variable de shell : c est elle qui decide de retenir la fenetre a la fin.
_multi_own_window=0
[ "${OSMO_MULTI_TERM:-0}" = "1" ] && _multi_own_window=1
unset OSMO_MULTI_TERM
# Meme raison pour root : une lecture n a pas a demander de mot de passe.
if [ "$_multi_readonly" = "0" ]; then
if [ "$(id -u)" -ne 0 ]; then
if command -v pkexec >/dev/null 2>&1 && [ -n "${DISPLAY:-}" ]; then
exec pkexec env DISPLAY="${DISPLAY:-}" XAUTHORITY="${XAUTHORITY:-}" \
OSMO_MULTI_TERM="$_multi_own_window" "$0" "$@"
fi
command -v sudo >/dev/null 2>&1 && exec sudo -E "$0" "$@"
fi
fi
echo -e "${CYAN}${BOLD}"
echo "╔══════════════════════════════════════════════════════╗"
echo "║ osmo-operator - banc MULTI-OPERATEUR (SS7) ║"
echo "╚══════════════════════════════════════════════════════╝"
echo -e "${NC}"
# ── PREFLIGHT ───────────────────────────────────────────────────────────────
# Docker N EST PAS dans l ISO (voir addition.sh) : son absence est le cas
# NORMAL au premier lancement, pas une panne. On le dit comme tel, et on
# renvoie vers le supplement plutot que de laisser docker repondre
# "command not found" - message qui n indique nulle part quoi faire.
manque() {
echo -e " ${RED}✗ $1${NC}"
echo -e " ${CYAN}→ sudo ${DIR}/addition.sh${NC} (ou l icone ${BOLD}Supplements${NC} du bureau)"
exit 1
}
command -v docker >/dev/null 2>&1 || manque "docker n est pas installe"
docker info >/dev/null 2>&1 || manque "le demon docker ne repond pas"
if [ ! -f "$MULTI_CONF" ]; then
manque "topologie absente : $MULTI_CONF"
fi
# shellcheck disable=SC1090
. "$MULTI_CONF"
: "${MULTI_HUB_IP:=172.20.0.10}"
: "${MULTI_M3UA_PORT:=2908}"
: "${MULTI_OPS:=}"
: "${MULTI_IMAGE:=osmocom-run}"
# ── LIRE LE NATIF PLUTOT QUE LE SUPPOSER ────────────────────────────────────
# [2026-08-31] La topologie ecrite par addition.sh dit ce qu on ATTEND du
# natif ; ces deux fonctions disent ce qu il EST. Les confondre a coute cher :
# on affichait "op natif actif" sur la seule presence d un processus, sans voir
# qu il portait le point code d un conteneur.
#
# Le point code depend du plan choisi, et les deux existent :
# plan LOCAL (set-node-id.sh --local) : 1.<op>.<role> - une machine
# plan WAN (defaut) : 1.<noeud><op>.<role> - plusieurs
# Sur un banc a une machine c est le plan local qui vaut, mais rien n empeche
# la config d avoir ete posee autrement : on LIT, on ne recalcule pas.
natif_pc() {
awk '/^[[:space:]]*point-code[[:space:]]/{print $2; exit}' \
/etc/osmocom/osmo-stp.cfg 2>/dev/null
}
# L adresse par laquelle le natif sort vers le hub : le local-ip de son ASP
# asp-to-inter. C est elle qui doit exister sur l hote - un local-ip inbindable
# est un ASP qui ne monte jamais, sans message clair (cf. le 172.20.0.11 des
# gabarits, corrige dans generate_configs.sh).
natif_asp_ip() {
local ip
ip="$(awk '/^[[:space:]]*asp[[:space:]]+asp-to-inter[[:space:]]/{a=1; next}
a && /^[[:space:]]*local-ip[[:space:]]/{print $2; exit}' \
/etc/osmocom/osmo-stp.cfg 2>/dev/null)"
[ -n "$ip" ] || return 0
# [2026-08-31] ON VERIFIE QUE L ADRESSE EXISTE ICI.
# Cette colonne affichait bêtement ce que disait la config, et la config
# portait 172.20.0.11 - l adresse du PREMIER CONTENEUR, remise par les
# gabarits a chaque demarrage du natif. On lisait donc "op 1 native
# 172.20.0.11", ce qui est impossible : le natif tourne sur l hote.
# Une adresse absente de la machine est un ASP qui ne montera jamais ; on le
# DIT dans la colonne plutot que de laisser croire a une topologie saine.
if ip -4 -o addr show 2>/dev/null | awk '{print $4}' | cut -d/ -f1 \
| grep -qx "$ip"; then
printf '%s\n' "$ip"
else
printf '%s(absente!)\n' "$ip"
fi
}
# ── UN PROCESSUS DE L HOTE, PAS CELUI D UN CONTENEUR ────────────────────────
# [2026-08-31] `pgrep -x osmo-bsc` et `pgrep -f 'osmo-bsc|asterisk'` repondaient
# OUI des qu un CONTENEUR tournait : l hote voit TOUS les processus, y compris
# les leurs (mesure : osmo-bsc pid 140689 et asterisk pid 140878, enfants du
# systemd de osmo-operator-2, listes par le `ps` de l hote). Le natif etait donc
# confondu avec n importe quelle pile du banc. Deux consequences, vues toutes
# les deux :
# - etat() affichait « op 1 native actif » sans rien de natif en marche ;
# - lancer_natif_si_absent() sortait aussitot et NE LANCAIT JAMAIS le natif
# des qu un conteneur tournait deja - precisement le cas d un start-multi
# relance, celui ou on en a le plus besoin.
# Le discriminant est le NAMESPACE PID : un conteneur a le sien, l hote garde
# celui de son init. Plus sur que le cgroup, qui depend de --cgroupns.
#
# `pgrep -x` ET NON `pgrep -f` : on compare le NOM du processus, pas sa ligne de
# commande. `-f 'osmo-bsc|asterisk'` repondait OUI sur n importe quelle ligne
# CITANT ces mots - le `tail -F /var/log/osmocom/osmo-bsc.log` que le natif
# ouvre lui-meme, un editeur, ou le shell appelant. Un nom exact ne peut pas
# mentir de cette facon, et rend inutile l exclusion de notre propre PID.
_ns_hote="$(readlink /proc/1/ns/pid 2>/dev/null)"
_est_natif() { # $@ = noms EXACTS ; vrai si l un tourne hors conteneur
local n p
for n in "$@"; do
for p in $(pgrep -x "$n" 2>/dev/null); do
[ "$(readlink "/proc/$p/ns/pid" 2>/dev/null)" = "$_ns_hote" ] && return 0
done
done
return 1
}
# ── ETAT ────────────────────────────────────────────────────────────────────
etat() {
local spec idx mode ip pc _r
echo -e " ${BOLD}Topologie${NC} ($MULTI_CONF)"
for spec in $MULTI_OPS; do
IFS=: read -r idx mode ip pc _rctx <<< "$spec"
if [ "$mode" = "native" ]; then ip="$(natif_asp_ip)"; fi
printf ' op %-2s %-7s %-13s PC %-8s ' "$idx" "$mode" "${ip:-(hote)}" "$pc"
if [ "$mode" = "native" ]; then
if _est_natif osmo-bsc asterisk \
|| systemctl is-active --quiet asterisk 2>/dev/null; then
_r="$(natif_pc)"
if [ -n "$_r" ] && [ "$_r" != "$pc" ]; then
echo -e "${GREEN}actif${NC} ${YELLOW}(PC reel ${_r})${NC}"
else
echo -e "${GREEN}actif${NC}"
fi
else
echo -e "${YELLOW}arrete${NC}"
fi
else
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "osmo-operator-${idx}"; then
echo -e "${GREEN}actif${NC}"
else
echo -e "${YELLOW}arrete${NC}"
fi
fi
done
printf ' hub %-7s %-13s PC %-8s ' "docker" "$MULTI_HUB_IP" "${MULTI_HUB_PC:-0.0.0}"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "${MULTI_HUB_NAME:-osmo-inter-stp}"; then
echo -e "${GREEN}actif${NC}"
else
echo -e "${YELLOW}arrete${NC}"
fi
}
case "$ACTION" in
status) etat; exit 0 ;;
stop)
for spec in $MULTI_OPS; do
IFS=: read -r idx mode _ <<< "$spec"
[ "$mode" = "docker" ] || continue
docker rm -f "osmo-operator-${idx}" >/dev/null 2>&1 \
&& echo -e " ${GREEN}✓${NC} osmo-operator-${idx} arrete"
done
docker rm -f "${MULTI_HUB_NAME:-osmo-inter-stp}" >/dev/null 2>&1 \
&& echo -e " ${GREEN}✓${NC} hub arrete"
echo -e " ${CYAN}i${NC} l operateur NATIF n est pas touche - ${BOLD}sudo ${DIR}/start-direct.sh stop${NC} pour lui."
exit 0 ;;
esac
# ── L IMAGE ─────────────────────────────────────────────────────────────────
# NOM DE L IMAGE : osmocom-run, PAS "osmo-operator".
# build.sh produit osmocom-nitb (Dockerfile, ~11 Go) puis Dockerfile.run en
# derive osmocom-run, et c est CELLE-LA que start.sh lance - verifie sur le
# banc : `docker inspect osmo-operator-1 --format {{.Config.Image}}` rend
# osmocom-run. Chercher "osmo-operator" (le nom du DEPOT, pas de l image)
# rendait la sonde toujours negative : "image absente" en permanence, et une
# recompilation Osmocom de 40 minutes relancee pour rien a chaque passage.
# [2026-09-02] SI SEULE LA BASE EST LA, ON DERIVE ICI. addition.sh peut
# desormais TELECHARGER osmocom-nitb (docker pull bastienbaranoff/norf_gsm)
# au lieu de la compiler ; osmocom-run en decoule par Dockerfile.run en
# quelques secondes. Refuser de demarrer pour ca et renvoyer vers addition.sh
# - qui aurait repondu "image deja presente" - etait une boucle sans issue.
if ! docker image inspect "$MULTI_IMAGE" >/dev/null 2>&1; then
if docker image inspect osmocom-nitb >/dev/null 2>&1; then
echo -e " ${CYAN}→${NC} image '$MULTI_IMAGE' absente, base osmocom-nitb presente : derivation (Dockerfile.run)"
( cd "$DIR" && docker build --build-arg QEMU_CACHE_BUST=$(date +%s) \
-f Dockerfile.run -t "$MULTI_IMAGE" . ) \
|| manque "derivation de l image '$MULTI_IMAGE' echouee"
else
manque "image '$MULTI_IMAGE' absente"
fi
fi
# ── LE RACCORD DU NATIF AU HUB ──────────────────────────────────────────────
# [2026-08-31] RESOLU, dans start.sh, au lancement du hub.
# /etc/osmocom/osmo-stp.cfg (natif) declare
# asp asp-to-inter 2908 2910 m3ua
# remote-ip 127.0.0.1
# et rien n ecoutait sur 127.0.0.1:2908 : le hub vit dans le conteneur et ne
# publiait aucun port. Le hub publie desormais 2908 en SCTP sur la boucle
# locale (docker run -p 127.0.0.1:2908:2908/sctp), ce qui rend le remote-ip du
# natif exact sans toucher a une seule config regeneree.
# Verifie sur le banc : docker port -> "2908/sctp -> 127.0.0.1:2908",
# ss -an -> "sctp LISTEN 127.0.0.1:2908", docker-proxy -proto sctp actif.
#
# L autre voie - repointer l ASP natif sur 172.20.0.10 / local-ip 172.20.0.1 -
# a ete ecartee : ces deux adresses n existent que tant que le bridge docker
# est monte, et disparaissent avec les conteneurs.
#
# Le second defaut du meme bloc, lui, etait dans la config : local-ip valait
# 172.20.0.11, l adresse du PREMIER CONTENEUR, inbindable sur l hote. Corrige
# dans generate_configs.sh (apply_native_post_patches), qui ne couvrait pas
# osmo-stp.cfg.
# ── COMBIEN DE CONTENEURS ───────────────────────────────────────────────────
# Ceux marques "docker" dans la table, pas un compte fige : la topologie est
# la seule source.
N_DOCKER=0
for spec in $MULTI_OPS; do
IFS=: read -r idx mode _ <<< "$spec"
[ "$mode" = "docker" ] && N_DOCKER=$((N_DOCKER + 1))
done
[ "$N_DOCKER" -ge 1 ] || { echo -e " ${RED}✗ aucun operateur docker dans la topologie${NC}"; exit 1; }
# OPERATOR_COUNT_HINT : start.sh l utilise pour NE PAS reposer la question du
# nombre de conteneurs (voir start_bridge_mode). Sans elle, le script s arrete
# sur une boite de dialogue - fatal pour un lancement par icone.
# [2026-08-31] PAS DE --wan ICI, ET C EST LE POINT IMPORTANT.
# La premiere version passait --wan --wan-id --hub-ip --operator. Resultat
# mesure : le check de fin cherchait le hub en 192.168.1.49 et l operateur
# portait le point code 1.23.2, alors que la topologie dit 172.20.0.10 et
# 1.2.2. --wan est la machinerie MULTI-MACHINE : il lit/ecrit
# /etc/osmo-wan.conf (qui n existe meme pas ici), renumerote les point codes en
# 1.<noeud><op>.<role> et attend un hub a une adresse routable entre machines.
# Sur UNE machine, c est l inverse qu on veut - et c est deja le defaut :
# checks/wan_ss7_check.sh le dit noir sur blanc, "sans WAN l ASP de chaque
# operateur pointe sur 172.20.0.10", le hub local, avec un catch-all
# "route 0.0.0 0.0.0 → as-inter". Il n y a donc rien a demander a start.sh
# au-dela du nombre de conteneurs.
#
# OPERATOR_COUNT_HINT : start.sh l utilise pour NE PAS reposer la question du
# nombre de conteneurs (voir start_bridge_mode). Sans elle, le script s arrete
# sur une boite de dialogue - fatal pour un lancement par icone.
# OSMO_QUICK=1 : reutiliser le cache docker. Sans elle start.sh partait en
# "Build : normal (--no-cache)", soit une recompilation Osmocom complete de ~40
# minutes A CHAQUE lancement du multi-operateur - alors que l image est deja la
# (addition.sh s en charge, et le prealable plus haut le verifie).
# OSMO_NONINTERACTIVE=1 : les boites whiptail de start.sh rendent leur valeur
# par defaut au lieu d attendre. Sans elle, le lancement s arretait sur
# « Nombre d'operateurs (1-36) » - un dialogue que personne ne voit quand on a
# clique sur une icone, et le banc paraissait mort.
# --operators : le drapeau EXPLICITE de start.sh (l.2552), et non la seule
# variable d environnement. start.sh remettait OPERATOR_COUNT_HINT a vide des
# sa ligne 70 - corrige depuis, mais l argument reste la voie sure : il est
# analyse apres, il prime, et il documente l intention dans la ligne de
# commande qu on affiche.
# HANDOFF_MODE=faketrx-qemu : la radio MIXTE, un faketrx + un QEMU Calypso par
# operateur - le meme montage que le natif. start.sh ne la choisissait seul
# qu en mode WAN ; impose ici, elle vaut aussi pour ce banc a une machine.
# OSMO_SKIP_CHECKS=1 : start.sh joue desormais ss7_check et operator_summary en
# fin de lancement. Ici on enchaine verifier(), qui rejoue ss7_check avec en
# plus la lecture de la topologie - inutile de le passer deux fois de suite.
# OP_ID_BASE=2 : les conteneurs prennent les rangs 2 et 3, le 1 reste au natif.
# OSMO_NO_ATTACH=1 : ne pas finir bloque dans le tmux d un conteneur - le script
# doit rendre la main pour enchainer verifier().
CMD=(env "OSMO_QUICK=1" "OSMO_NONINTERACTIVE=1" "HANDOFF_MODE=faketrx-qemu" "OSMO_SKIP_CHECKS=1"
"OP_ID_BASE=2" "OSMO_NO_ATTACH=1"
"$DIR/start.sh" virtual --operators "$N_DOCKER")
echo -e " ${BOLD}Topologie${NC} : ${N_DOCKER} conteneur(s) + 1 natif + hub ${MULTI_HUB_IP}"
echo -e " ${CYAN}→${NC} ${CMD[*]}"
echo
if [ "$ACTION" = "dry" ]; then
echo -e " ${YELLOW}--dry-run : rien n a ete lance.${NC}"
exit 0
fi
[ "$(id -u)" -eq 0 ] || { echo -e "${RED}Root requis : sudo $0${NC}"; exit 1; }
# ── SE PLACER DANS LE DEPOT AVANT D APPELER start.sh ────────────────────────
# [2026-08-31] start.sh appelle ses aides en chemin RELATIF - ligne 2595 :
# ./helpers/prepare_host.sh
# Lance depuis ailleurs (une icone du bureau demarre dans $HOME), il sortait
# donc sur
# start.sh: line 2595: ./helpers/prepare_host.sh: No such file or directory
# et abandonnait AVANT de creer le moindre conteneur. Le bilan affichait
# ensuite "conteneur absent" trois fois, ce qui faisait chercher un probleme
# de docker alors que le script n avait jamais atteint le docker run.
cd "$DIR" || { echo -e "${RED}Impossible d entrer dans $DIR${NC}"; exit 1; }
# ── ALIGNER L IDENTITE SS7 DU NATIF, AVANT DE LANCER ────────────────────────
# start.sh numerote SES conteneurs 1..N sans decalage possible : avec 2
# conteneurs il produit les point codes 1.1.2 et 1.2.2. Un natif laisse a son
# defaut porte 1.1.2 lui aussi - deux equipements a la meme adresse SS7, ce que
# ce depot decrit comme "pas un conflit de nom, du routage faux".
#
# On repointe donc le natif sur le rang que la topologie lui donne, AVANT de
# lancer les conteneurs. Deux drapeaux comptent :
# --local plan d une seule machine, MID=<op> -> 1.<op>.<role>. SANS lui,
# set-node-id.sh prend le plan WAN (MID=<noeud><op>) et rend
# 1.13.2 au lieu de 1.3.2 - verifie en dry-run.
# --hub-ip sinon l ASP est repointe sur 192.168.1.49, le hub WAN par defaut,
# qui n existe pas ici : c est de la que venait le "Aucune
# association SCTP vers 192.168.1.49:2908" des premiers essais.
# ── L IDENTITE DU NATIF NE TIENT PAS QU AU POINT CODE ───────────────────────
# [2026-08-31] set-node-id.sh repointe les point codes, et rien d autre. Or
# start-direct.sh derive TOUT le reste de son operateur depuis OPERATOR_ID
# (/etc/osmocom/coeur.env) :
# MS_OP_ID = OPERATOR_ID
# ms_imsi() = MCC MNC <op sur 4> <ms sur 6> -> l IMSI
# MSISDN = 100<op><ms>
# MS_ARFCN1 = 512 + MS_OP_ID * 2 -> la FREQUENCE
# Un natif realigne en operateur 3 mais laisse a OPERATOR_ID=1 gardait donc les
# abonnes de l operateur 1 - MSISDN 100101, IMSI 001010001000001 - exactement
# ceux que start.sh donne a son conteneur osmo-operator-1. Deux abonnes avec le
# meme MSISDN dans deux HLR, et deux cellules sur le MEME ARFCN : rien ne
# proteste au demarrage, et c est a l appel que ca part de travers.
_aligner_coeur_env() {
local idx="$1" f="/etc/osmocom/coeur.env"
[ -f "$f" ] || return 0
if grep -qE '^[[:space:]]*:[[:space:]]*"\$\{OPERATOR_ID:=' "$f"; then
sed -i -E "s|^([[:space:]]*:[[:space:]]*\"\\\$\\{OPERATOR_ID:=)[0-9]+(\\}\")|\\1${idx}\\2|" "$f"
grep -qE "OPERATOR_ID:=${idx}\\}" "$f" \
|| echo -e " ${GREEN}✓${NC} coeur.env : OPERATOR_ID=${idx} (IMSI, MSISDN et ARFCN du natif suivent)"
else
echo ": \"\${OPERATOR_ID:=${idx}}\"" >> "$f"
echo -e " ${GREEN}✓${NC} coeur.env : OPERATOR_ID=${idx} ajoute"
fi
}
aligner_natif() {
local idx pc spec reel
for spec in $MULTI_OPS; do
IFS=: read -r idx mode _ip pc _rctx <<< "$spec"
[ "$mode" = "native" ] || continue
# [2026-08-31] coeur.env est aligne D ABORD, et INCONDITIONNELLEMENT.
# Il etait traite dans la branche "point code different" plus bas : une
# fois les point codes deja bons, aligner_natif sortait par le
# raccourci "deja en PC 1.3.2" et OPERATOR_ID restait a 1 pour
# toujours. Le natif gardait alors les IMSI/MSISDN de l operateur 1 -
# 001010001000001, 100101 - exactement ceux du conteneur op1, et
# l ARFCN avec. Les deux reglages sont INDEPENDANTS : point codes
# (set-node-id.sh) et plan d abonnes (OPERATOR_ID) ; les lier faisait
# que le second ne se rattrapait jamais.
_aligner_coeur_env "$idx"
reel="$(natif_pc)"
[ -n "$reel" ] || return 0
[ "$reel" = "$pc" ] && { echo -e " ${GREEN}✓${NC} natif deja en PC ${pc} (op ${idx})"; return 0; }
echo -e " ${CYAN}→${NC} natif en PC ${reel}, attendu ${pc} : realignement sur l operateur ${idx}"
if [ -x "$DIR/network/set-node-id.sh" ] || [ -r "$DIR/network/set-node-id.sh" ]; then
if bash "$DIR/network/set-node-id.sh" --node "${MULTI_NODE:-1}" --op "$idx" \
--local --hub-ip 127.0.0.1 >/dev/null 2>&1; then
echo -e " ${GREEN}✓${NC} natif realigne : PC $(natif_pc)"
echo -e " ${YELLOW}!${NC} les demons natifs gardent l ancienne identite en memoire :"
echo -e " ${CYAN}sudo ${DIR}/start-direct.sh${NC} pour qu ils la relisent."
else
echo -e " ${RED}✗${NC} realignement du natif echoue - voir network/set-node-id.sh"
fi
fi
return 0
done
}
aligner_natif
# ── TOUT ARRETER AVANT DE RELANCER ──────────────────────────────────────────
# [2026-08-31] Un clic = un banc NEUF. Sans ca, un lancement par-dessus un banc
# deja debout ne relance presque rien : les demons Osmocom ne relisent PAS leur
# configuration a chaud. On l a paye plusieurs fois - un osmo-stp realigne sur
# le bon point code gardait l ancien en memoire, et `kill -HUP` n y change rien
# (verifie : config a jour sur le disque, ASP toujours ASP_DOWN sur l ancienne
# adresse). Le seul geste qui compte est l arret complet.
#
# On arrete le NATIF (il delegue a run.sh --stop, qui demonte proprement radio
# et coeur). Rien ici ne doit faire echouer le lancement : ce qui est deja
# arrete l est tres bien, d ou les || true.
tout_arreter() {
echo -e " ${CYAN}→${NC} arret du banc en place (un clic = un banc neuf)"
if [ -x "$DIR/start-direct.sh" ]; then
timeout 120 "$DIR/start-direct.sh" --stop >/dev/null 2>&1 || true
echo -e " ${GREEN}✓${NC} pile native arretee"
fi
# Les conteneurs ne sont PAS touches ici : start.sh fait deja
# `docker rm -f $(docker ps -aq --filter name=osmo-)` en tete de course.
# Le refaire serait redondant, et surtout destructeur depuis launch.sh -
# un clic sur le telephone rouge, qui ne monte que le natif, balayerait
# les conteneurs multi-operateur d a cote.
# Les ponts audio survivent aux conteneurs (setsid) : sans ca on empile un
# relais de plus a chaque relance, et le son se dedouble.
pkill -f 'paplay --server=tcp:' 2>/dev/null || true
}
tout_arreter
# ── SI LE NATIF EST A TERRE, ON LANCE SON RACCOURCI ─────────────────────────
# Le natif n est PAS lance par ce script : il a le sien (le telephone rouge du
# bureau -> launch.sh -> start-direct.sh). Mais monter les conteneurs sans lui,
# c est monter un banc a trois operateurs dont le premier manque : le hub
# n aura qu un ASP sur les trois attendus, et le bilan de fin sortira en
# DEGRADE pour une raison qui n a rien a voir avec ce qu on vient de lancer.
#
# On declenche donc son raccourci, et AVANT les conteneurs : l ordre compte,
# start-direct.sh a besoin de poser ses configs et son coeur pendant que le
# plan d adressage est encore libre.
#
# Pourquoi le .desktop et pas launch.sh en direct : c est le meme chemin que le
# double-clic, donc le meme terminal, la meme elevation pkexec et le meme
# comportement. Passer a cote reviendrait a tester une autre facon de demarrer
# que celle des utilisateurs.
NATIF_DESKTOP="${NATIF_DESKTOP:-/root/Desktop/osmo-launch.desktop}"
lancer_natif_si_absent() {
_est_natif osmo-bsc asterisk && return 0
echo -e " ${YELLOW}!${NC} operateur natif a l arret - lancement de son raccourci"
local d="$NATIF_DESKTOP"
# Repli sur les autres emplacements : Bureau (locale fr) puis le menu.
for _c in "$d" "${d%/Desktop/*}/Bureau/${d##*/}" "/usr/share/applications/${d##*/}"; do
[ -f "$_c" ] && { d="$_c"; break; }
done
if [ ! -f "$d" ]; then
echo -e " ${RED}✗${NC} raccourci introuvable ($NATIF_DESKTOP)"
echo -e " ${CYAN}sudo ${DIR}/start-direct.sh${NC}"
return 1
fi
# gio launch est LA facon d executer un .desktop : il lit Exec, applique
# Terminal= et passe par le bus de session. gtk-launch en repli (il veut un
# ID d application, pas un chemin - d ou le basename sans .desktop).
# En dernier recours on extrait Exec a la main : mieux vaut demarrer le banc
# que d echouer sur l outillage du bureau.
# Le natif nous sert de PILE, pas de poste de travail : wireshark, linphone
# et firefox sont deja ouverts par ailleurs (ou le seront par nos soins pour
# les dashboards). Sans ce drapeau, launch.sh les rouvrait une seconde fois.
export OSMO_LAUNCH_APPS=0
# (a) LE NATIF PREND SA PROPRE FENETRE. Sans ce marqueur, launch.sh voyait
# un tty - LE NOTRE - le prenait pour le sien, et start-direct.sh le tenait
# jusqu au bout : notre sortie ecrasee, et la main rendue seulement a la fin
# du natif. Voir launch.sh, garde « 1. Un terminal ».
export OSMO_TERM_TAKEN=1
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=${XDG_RUNTIME_DIR}/bus}"
if command -v gio >/dev/null 2>&1 && gio launch "$d" >/dev/null 2>&1; then
echo -e " ${GREEN}✓${NC} raccourci lance : ${CYAN}${d}${NC}"
elif command -v gtk-launch >/dev/null 2>&1 \
&& gtk-launch "$(basename "$d" .desktop)" >/dev/null 2>&1; then
echo -e " ${GREEN}✓${NC} raccourci lance (gtk-launch) : ${CYAN}${d}${NC}"
else
local _exec
_exec="$(sed -n 's/^Exec=//p' "$d" | head -1 | sed 's/ *%[fFuUdDnNickvm]//g')"
[ -n "$_exec" ] || { echo -e " ${RED}✗${NC} pas de ligne Exec dans $d"; return 1; }
echo -e " ${YELLOW}○${NC} gio/gtk-launch indisponibles - execution directe : ${CYAN}${_exec}${NC}"
setsid bash -c "$_exec" </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
fi
# On attend qu il soit reellement debout : enchainer les conteneurs pendant
# que le natif se monte, c est se disputer les memes ressources.
echo -ne " ${CYAN}→${NC} attente du natif"
local i
for i in $(seq 1 120); do
_est_natif osmo-bsc asterisk && { echo -e " ${GREEN}✓${NC}"; return 0; }
sleep 1; echo -n "."
done
echo -e " ${YELLOW}toujours absent apres 120 s - on continue quand meme${NC}"
return 0
}
lancer_natif_si_absent
"${CMD[@]}"
rc=$?
# ── LE CHECK DE FIN ─────────────────────────────────────────────────────────
# start.sh peut rendre 0 alors que le SS7 n est pas monte : son code de retour
# dit que les conteneurs ont DEMARRE, pas que les ASP se sont associes. C est
# la distinction que le depot documente ailleurs sous le nom de sonde
# mensongere - un superviseur vivant au-dessus d un pont qui n a jamais
# transporte un echantillon. On verifie donc l etat REEL, apres coup.
verifier() {
local ko=0 spec idx mode nom _ip pc _rctx _pc_reel
echo
echo -e " ${BOLD}── Verification ──${NC}"
# 1. Les quatre elements sont-ils la ?
for spec in $MULTI_OPS; do
IFS=: read -r idx mode _ip pc _rctx <<< "$spec"
if [ "$mode" = "docker" ]; then
nom="osmo-operator-${idx}"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$nom"; then
echo -e " ${GREEN}✓${NC} conteneur ${nom}"
else
echo -e " ${RED}✗${NC} conteneur ${nom} absent"; ko=1
fi
else
if ! _est_natif osmo-bsc asterisk; then
echo -e " ${RED}✗${NC} operateur natif arrete - ${CYAN}sudo ${DIR}/start-direct.sh --op ${idx}${NC}"; ko=1
else
# ── LE POINT CODE DU NATIF DOIT CORRESPONDRE A SON RANG ──
# Un natif lance sans --op tourne en operateur 1, donc en
# PC 1.1.2 - le meme que le PREMIER CONTENEUR, que start.sh
# numerote toujours a partir de 1. La pile demarre des deux
# cotes, tout parait vert, et c est le ROUTAGE SS7 qui est faux :
# deux equipements a la meme adresse. On le verifie donc au lieu
# de se contenter de "le processus tourne".
_pc_reel="$(natif_pc)"
if [ -z "$_pc_reel" ]; then
echo -e " ${YELLOW}?${NC} operateur natif actif, point code illisible (/etc/osmocom/osmo-stp.cfg)"
elif [ "$_pc_reel" = "$pc" ]; then
echo -e " ${GREEN}✓${NC} operateur natif (op ${idx}, PC ${_pc_reel})"
else
echo -e " ${RED}✗${NC} operateur natif en PC ${_pc_reel}, attendu ${pc}"
echo -e " il tourne en operateur $(echo "$_pc_reel" | cut -d. -f2), rang deja pris par un CONTENEUR"
echo -e " ${CYAN}sudo ${DIR}/start-direct.sh --op ${idx}${NC} (puis relancer ce script)"
ko=1
fi
fi
fi
done
nom="${MULTI_HUB_NAME:-osmo-inter-stp}"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$nom"; then
echo -e " ${GREEN}✓${NC} inter-STP ${nom}"
else
echo -e " ${RED}✗${NC} inter-STP ${nom} absent"; ko=1
fi
# 2. Le point d entree M3UA du natif. On interroge `ss`, PAS /dev/tcp :
# M3UA roule sur SCTP, et une sonde TCP y est muette - la version
# precedente de ce check l avouait ("non concluant"), ce qui en faisait
# une ligne inutile. `ss -an` voit les sockets SCTP nommement.
# awk par CHAMPS et pas un regex : `ss -an` aligne ses colonnes avec un
# nombre d espaces VARIABLE selon la longueur des adresses, et un
# "^sctp[[:space:]]+LISTEN.*" s y casse sans prevenir - teste, il ne
# trouvait pas une ligne pourtant bien presente. $1/$2/$5 sont stables.
if ss -an 2>/dev/null | awk -v p=":${MULTI_M3UA_PORT}" \
'$1=="sctp" && $2=="LISTEN" && index($5,p)==length($5)-length(p)+1 {f=1} END{exit !f}'; then
echo -e " ${GREEN}✓${NC} M3UA en SCTP sur 127.0.0.1:${MULTI_M3UA_PORT} (l operateur natif peut s attacher)"
else
echo -e " ${RED}✗${NC} rien n ecoute en SCTP sur le port ${MULTI_M3UA_PORT}"
echo -e " l ASP du natif (osmo-stp.cfg, remote-ip 127.0.0.1) ne trouvera personne"
ko=1
fi
# 3. Le verificateur SS7 du depot, qui sait lire les ASP et les routes.
if [ -x "$DIR/checks/ss7_check.sh" ]; then
echo -e " ${CYAN}→${NC} checks/ss7_check.sh"
"$DIR/checks/ss7_check.sh" || ko=1
else
echo -e " ${YELLOW}○${NC} checks/ss7_check.sh absent - verification SS7 sautee"
fi
echo
if [ "$ko" = "0" ]; then
echo -e " ${GREEN}${BOLD}Banc multi-operateur OPERATIONNEL.${NC}"
else
echo -e " ${RED}${BOLD}Le banc n est PAS complet${NC} - voir les lignes ✗ ci-dessus."
echo -e " ${CYAN}→${NC} etat detaille : ${BOLD}sudo $0 --status${NC}"
fi
return "$ko"
}
# ── LES DASHBOARDS DES CONTENEURS, EN ONGLETS SUPPLEMENTAIRES ───────────────
# launch.sh a deja ouvert Firefox sur le dashboard du NATIF et le tutoriel, et
# lance Linphone et Wireshark avec lui : on ne redouble rien de tout ca. Il
# manque seulement les consoles des conteneurs, une par operateur docker.
#
# Les adresses viennent de la topologie, pas d une liste en dur : ajouter un
# operateur a osmo-multi.conf suffit a lui ouvrir son onglet.
#
# On attend que chaque port reponde avant d ouvrir : un onglet lance trop tot
# affiche une page d erreur, et il faut alors recharger a la main - le
# dashboard d un conteneur met plusieurs secondes a monter apres le demarrage.
ouvrir_dashboards() {
command -v firefox >/dev/null 2>&1 || return 0
local spec idx mode ip urls="" i
for spec in $MULTI_OPS; do
IFS=: read -r idx mode ip _pc _rctx <<< "$spec"
[ "$mode" = "docker" ] && [ -n "$ip" ] || continue
for i in $(seq 1 60); do
(exec 3<>"/dev/tcp/${ip}/8080") 2>/dev/null && { exec 3>&- 2>/dev/null; break; }
sleep 1
done
if (exec 3<>"/dev/tcp/${ip}/8080") 2>/dev/null; then
exec 3>&- 2>/dev/null
urls="$urls http://${ip}:8080"
else
echo -e " ${YELLOW}○${NC} dashboard op ${idx} (${ip}:8080) ne repond pas - onglet non ouvert"
fi
done
[ -n "$urls" ] || return 0
# Sous le compte de la session : Firefox y tourne deja, il ajoutera les URL
# comme ONGLETS a la fenetre existante plutot que d en ouvrir une seconde.
local u uid home
u="$(natif_session_user)"; uid="$(id -u "$u" 2>/dev/null || echo 0)"
home="$(getent passwd "$u" 2>/dev/null | cut -d: -f6)"
# shellcheck disable=SC2086
setsid sudo -u "$u" \
env DISPLAY="${DISPLAY:-:0}" \
XAUTHORITY="${XAUTHORITY:-${home:-/root}/.Xauthority}" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${uid}/bus" \
firefox $urls </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
echo -e " ${GREEN}✓${NC} onglets dashboards :${CYAN}${urls}${NC}"
}
# Le compte de la session graphique, meme critere qu ailleurs : la presence de
# son socket pulse, et non son nom (l ISO ouvre la session sous root).
natif_session_user() {
local u uid sock
for u in "${HOST_PULSE_USER:-}" "${SUDO_USER:-}" "$(logname 2>/dev/null || true)"; do
[ -n "$u" ] || continue
uid="$(id -u "$u" 2>/dev/null)" || continue
[ -S "/run/user/${uid}/pulse/native" ] && { echo "$u"; return 0; }
done
for sock in /run/user/*/pulse/native; do
[ -S "$sock" ] || continue
uid="${sock#/run/user/}"; uid="${uid%%/*}"
u="$(getent passwd "$uid" 2>/dev/null | cut -d: -f1)"
[ -n "$u" ] && { echo "$u"; return 0; }
done
echo "${SUDO_USER:-$(id -un)}"
}
# EN TACHE DE FOND, et c est essentiel : cette fonction attend jusqu a 60 s par
# conteneur que le port 8080 reponde. En avant-plan, ca fige le script jusqu a
# deux minutes AVANT les verifications - et le silence ressemble a un blocage.
# Pire quand le dashboard ne monte pas du tout : on attendait le maximum pour
# n ouvrir aucun onglet. Les onglets s ouvriront quand ils pourront ; les tests,
# eux, partent tout de suite.
ouvrir_dashboards &
disown 2>/dev/null || true
echo
etat
verifier || rc=1
# Ouverte par nos soins : on la retient, sinon le bilan disparait avec elle.
if [ "$_multi_own_window" = "1" ]; then
echo
echo "Fenetre maintenue ouverte - Entree pour fermer."
read -r _ || true
fi
exit "$rc"start-nitb.sh
start-nitb.sh
#!/bin/bash
# start-nitb.sh - meme lanceur que start.sh, mais sur l'image osmocom-nitb.
#
# POURQUOI UN WRAPPER ET PAS UNE COPIE. start.sh fait 1 300 lignes (reseaux,
# generation de configs, SS7 inter-operateurs, ports, audio). Le dupliquer
# creerait une 2e copie qui divergerait - le depot en a deja paye le prix
# ailleurs (4 copies de si_bridge.py, 3 de gapk-start.sh, toutes divergentes).
# Ici il n'y a qu'UNE variable qui change : l'image.
#
# CE QUI A ETE VERIFIE AVANT D'ECRIRE CE SCRIPT (12/08/2026) :
# - les deux images n'ont PAS le meme entrypoint :
# osmocom-run -> /scripts/entrypoint.sh
# osmocom-nitb -> /etc/osmocom/entrypoint.sh
# - start.sh monte le dossier de config genere SUR /etc/osmocom, donc il
# MASQUE l'entrypoint de l'image nitb. Ce n'est pas un probleme : le
# dossier genere contient bien un entrypoint.sh (copie de scripts/
# entrypoint.sh, le meme fichier que /scripts/entrypoint.sh dans l'image
# run - 1988 o, identique). C'est la condition qui rend ce wrapper viable :
# SI scripts/entrypoint.sh disparaissait du dossier genere, l'image nitb
# demarrerait sans entrypoint alors que l'image run continuerait de
# marcher. Le controle ci-dessous le verifie a chaque lancement.
#
# USAGE : identique a start.sh, tous les arguments sont transmis tels quels.
# ./start-nitb.sh [args de start.sh...]
#
# REVENIR A L'IMAGE RUN : lancer ./start.sh directement.
set -eu
HERE="$(cd "$(dirname "$0")" && pwd)"
IMAGE_NITB="${IMAGE_NITB:-osmocom-nitb}"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
# ── 1. L'image doit EXISTER : on ne la construit pas ici ──────────────────────
# start.sh a un check_image() qui, si l'image nommee par IMAGE_RUN est absente,
# lance build_run_image() -> `docker build -f Dockerfile.run -t "$IMAGE_RUN" .`.
# Avec IMAGE_RUN=osmocom-nitb, cela ECRASERAIT osmocom-nitb par une image
# construite depuis Dockerfile.run. On barre la route en verifiant d'abord.
if ! docker image inspect "$IMAGE_NITB" >/dev/null 2>&1; then
echo -e "${RED}Image '${IMAGE_NITB}' introuvable.${NC}"
echo -e "Ce script ne la construit pas volontairement : le build integre de"
echo -e "start.sh utilise ${YELLOW}Dockerfile.run${NC} et ecraserait l'image nitb."
echo -e "Construire d'abord, depuis ${CYAN}${HERE}${NC} :"
echo -e " ${CYAN}docker build -f Dockerfile -t ${IMAGE_NITB} .${NC}"
exit 1
fi
# ── 2. L'entrypoint doit survivre au montage de /etc/osmocom ──────────────────
# On teste la PRESENCE, pas le bit d'execution : dans le depot le fichier est en
# 644, et c'est normal. Le bit +x est pose EN AVAL, aux deux endroits qui en ont
# besoin - generate_configs.sh l.204-205 (`cp scripts/*` puis `chmod +x` dans le
# dossier genere, celui qui est monte sur /etc/osmocom) et Dockerfile.run l.75
# (`chmod +x /scripts/*.sh` dans l'image). Tester `-x` ici refusait donc de
# demarrer sur un depot parfaitement sain.
if [ ! -s "${HERE}/scripts/entrypoint.sh" ]; then
echo -e "${RED}scripts/entrypoint.sh absent ou vide.${NC}"
echo -e "L'image ${IMAGE_NITB} a son entrypoint dans /etc/osmocom, que start.sh"
echo -e "recouvre par le dossier de config genere. Sans ce fichier, le"
echo -e "conteneur demarrerait sans entrypoint - alors que l'image"
echo -e "osmocom-run, elle, continuerait de marcher (son entrypoint est dans"
echo -e "/scripts). Panne asymetrique et illisible : on refuse avant."
exit 1
fi
echo -e "${GREEN}Lanceur nitb${NC} : image = ${CYAN}${IMAGE_NITB}${NC} (au lieu d'osmocom-run)"
echo -e " entrypoint attendu dans le conteneur : /etc/osmocom/entrypoint.sh (monte)"
# ── 3. Delegation ─────────────────────────────────────────────────────────────
# start.sh lit IMAGE_RUN avec l'idiome `:=` : la valeur posee ici gagne.
export IMAGE_RUN="$IMAGE_NITB"
cd "$HERE"
exec ./start.sh "$@"start.sh
start.sh
#!/bin/bash
# start.sh - Lance la stack Osmocom GSM multi-operateurs
#
# Modes : net-host (1 operateur, SDR physique) | bridge (N operateurs SS7 inter-op)
set -eu
# ── Ce script est le lanceur de l'HOTE. Refus net s'il tourne DANS le conteneur ──
# [2026-08-12] start.sh pilote docker (build, reseaux, `docker run`, volumes) ;
# start-direct.sh, lui, prepare l'environnement Calypso et lance run.sh A
# L'INTERIEUR. Les confondre ne donne pas une erreur lisible : `docker` n'existe
# pas dans l'image, donc on part sur une cascade de "command not found" apres
# avoir deja cree des fichiers et touche des configs. Mieux vaut s'arreter avant
# d'avoir rien fait, et nommer le script a utiliser.
#
# Discriminant verifie dans LES DEUX SENS le 12/08 : `/.dockerenv` et
# `/etc/docker-entrypoint-cmd` (pose par scripts/entrypoint.sh) sont presents
# dans le conteneur et absents sur l'hote. On exige les DEUX : `/.dockerenv`
# seul serait vrai dans n'importe quel conteneur, y compris un ou ce script
# aurait legitimement sa place.
if [ -f /.dockerenv ] && [ -f /etc/docker-entrypoint-cmd ]; then
printf '\033[1;31mVous etes dans le docker ! Utilisez start-direct.sh\033[0m\n' >&2
printf '\n \033[0;36m/opt/GSM/osmo-operator/start-direct.sh\033[0m (ou ./start-direct.sh depuis le depot)\n' >&2
printf '\n start.sh est le lanceur de l'"'"'HOTE : il construit l'"'"'image, cree les\n' >&2
printf ' reseaux et fait le "docker run". Rien de tout ca n'"'"'a de sens ici.\n' >&2
exit 1
fi
DEBUG=
if [[ -n "$DEBUG" ]]; then
set -x
PS4='[DEBUG] + ${BASH_SOURCE}:${LINENO}: '
echo "=== MODE DEBUG ACTIVE ==="
fi
IMAGE_BASE="osmocom-nitb"
# [2026-08-12] Surchargeable depuis l'environnement - c'est ce que fait
# start-nitb.sh, qui relance exactement ce script avec IMAGE_RUN=osmocom-nitb.
# Idiome `:=` du projet : une valeur posee explicitement en amont gagne, le
# defaut ne s'applique qu'a vide. NE PAS remettre en dur : start-nitb.sh
# deviendrait silencieusement un alias de start.sh, ce qui est indiscernable
# d'un lanceur qui marche.
IMAGE_RUN="${IMAGE_RUN:-osmocom-run}"
INTER_NET="gsm-inter"
INTER_NET_SUBNET="172.20.0.0/24"
INTER_NET_GATEWAY="172.20.0.1"
# Quel noeud du WAN heberge le hub SS7. UN SEUL doit le faire : deux hubs, c'est
# deux reseaux SS7 separes qui s'ignorent, et rien ne le signale.
WAN_HUB_NODE="${WAN_HUB_NODE:-1}"
# --hub-ip : l'inter-STP designe par son ADRESSE, pas par un numero de noeud.
# --hub-node oblige a le faire figurer dans la table WAN ; sur un banc ou le hub
# est une machine a part - une VM dediee, sans operateur - cela forcait a
# inventer un noeud pour lui, qui apparaissait ensuite dans les indicatifs et le
# routage voix/SMS comme s'il portait des abonnes. Avec --hub-ip la table ne
# contient que de vrais noeuds.
WAN_HUB_IP="${WAN_HUB_IP:-}"
# --node-per-op : chaque conteneur est un NOEUD, pas un operateur de la machine.
# A n'utiliser que quand les conteneurs ont une adresse joignable de l'exterieur
# (backbone route vers le LAN) : sinon les autres noeuds ne peuvent pas les
# appeler, et un noeud injoignable vaut moins qu'un operateur bien range.
WAN_NODE_PER_OP="${WAN_NODE_PER_OP:-0}"
# --operator IP:PREFIXE : declarer un operateur du WAN, ou qu'il soit.
# La table WAN se saisissait jusqu'ici par --wan-nodes "id:ip:ind", une forme
# exacte mais qui suppose qu'on connaisse deja les numeros de noeud. Declarer
# "une machine a cette adresse, joignable par ce prefixe" est plus proche de ce
# qu'on a sous les yeux - une VM, un conteneur - et les numeros suivent l'ordre
# de declaration.
OPERATOR_DECLS=()
# [2026-08-31] On HERITE de l environnement au lieu de l ecraser.
# Cette ligne valait OPERATOR_COUNT_HINT="" sec : un appelant qui posait
# la variable (env OPERATOR_COUNT_HINT=2 ./start.sh ...) la voyait effacee
# ici, bien avant sa lecture l.1421 - et start.sh reposait la question
# « Nombre d'operateurs (1-36) » qu on croyait justement avoir evitee.
# Silencieux de bout en bout : la variable etait bien passee, bien nommee,
# et simplement remise a zero en chemin.
# --operators N reste la voie explicite (l.2552) et prime, puisqu il est
# analyse plus tard.
OPERATOR_COUNT_HINT="${OPERATOR_COUNT_HINT:-}"
# --host : l'adresse de CETTE machine, telle que les VM doivent la joindre.
# Elle est deduite de l'interface par defaut ; --host la fixe quand la deduction
# se trompe - plusieurs cartes, un wifi et un ethernet actifs en meme temps, ou
# une adresse qui vient de changer. Elle sert de recours, pas de reglage
# quotidien : c'est elle que les VM inscrivent dans leur route vers le backbone.
HOST_LAN_IP=""
IMAGE_STP="${IMAGE_STP:-osmocom-stp}"
INTER_STP_CONTAINER="osmo-inter-stp"
INTER_STP_IP="172.20.0.10"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; NC='\033[0m'; BOLD='\033[1m'
# ── SMS Routing ────────────────────────────────────────────────────────────────
SMS_ROUTING_SCRIPT="$(dirname "$0")/scripts/sms-routing-setup.sh"
if [ -f "$SMS_ROUTING_SCRIPT" ]; then
source "$SMS_ROUTING_SCRIPT"
fi
SMS_ROUTING_DIR=""
# ══════════════════════════════════════════════════════════════════════════════
# WAN Interop
# ══════════════════════════════════════════════════════════════════════════════
# Deux WAN coexistent ici, et ils ne font PAS la meme chose :
#
# WAN_ENABLED (legacy, --pas d'option, question whiptail) : DEUX serveurs,
# prefixe unique 66, network/setup-wan-interop.sh. Conserve tel quel.
#
# WAN_MESH (option --wan) : N noeuds (1 a 9), UN INDICATIF PAR NOEUD,
# network/setup-wan-mesh.sh. C'est le seul qui tienne au-dela de deux
# serveurs - le legacy reecrit son bloc de conf a chaque appel, donc
# l'enchainer sur trois pairs ne laisse que le dernier.
#
# Les deux sont EXCLUSIFS et aucun n'est actif par defaut.
WAN_MESH=0
# --virtualbox : les pairs du WAN sont des VM VirtualBox sur CETTE machine, et
# cette machine est elle-meme un noeud. Voir network/setup-vbox-interco.sh.
VBOX_INTERCO=0
VBOX_NODES=""
VBOX_HOST_NODE=1
BUILD_STP=0
# shellcheck source=network/wan-nodes.sh
. "$(dirname "$0")/network/wan-nodes.sh"
WAN_ENABLED="false"
WAN_LOCAL_IP=""
WAN_REMOTE_IP=""
WAN_N_REMOTE=""
WAN_PREFIX="66"
WAN_SIP_BASE=5080
WAN_RTP_BASE=20000
WAN_RTP_PER_OP=500
PHY_MODE="faketrx" # faketrx | virtphy
# Choix passes au start-direct.sh DANS le conteneur via le handoff (NO_MENU=1),
# pour eviter les menus whiptail laggy en docker exec -ti. Fixes sur l'HOTE.
# Retenu AVANT le defaut : "l'appelant a-t-il impose le mode ?". C'est ce qui
# permet a --wan de basculer en hybride sans ecraser un HANDOFF_MODE=... explicite.
HANDOFF_MODE_FROM_ENV="${HANDOFF_MODE:+1}"
HANDOFF_MODE="${HANDOFF_MODE:-qemu}" # qemu | faketrx-qemu (combine)
HANDOFF_QEMU_CHOICE="${HANDOFF_QEMU_CHOICE:-full-grgsm}"
# ── Helpers ────────────────────────────────────────────────────────────────────
# ── Les deux plans d'adressage d'un operateur ───────────────────────────────
# backbone 172.20.0.<10+op> le segment que TOUS partagent avec l'inter-STP
# (.10). C'est par la que passe le SS7, et c'est ce
# reseau que network/setup-docker-lan-route.sh rend
# joignable depuis le LAN des VM.
# prive 192.168.<op+1>.x un segment par operateur : son BTS, son PCU, son
# GGSN. Rien n'y entre de l'exterieur.
#
# POURQUOI <op+1> ET PAS <op>
# Le LAN du banc est 192.168.1.0/24 - celui des VM et du hub SS7 en .49. Un
# operateur 1 en 192.168.1.0/24 entrerait en collision avec lui : deux routes
# pour le meme reseau sur l'hote, et un conteneur qui ne peut plus joindre le
# hub. Le decalage d'un rang laisse le .1 au LAN et commence les operateurs a
# 192.168.2.0/24.
# ── LE RANG DU PREMIER CONTENEUR ────────────────────────────────────────────
# [2026-08-31] start.sh numerotait SES operateurs 1..N, en dur. C est juste
# quand la machine ne porte que des conteneurs ; ca ne l est plus des qu un
# operateur NATIF tourne a cote, car il occupe deja un rang - et deux
# equipements au meme point code, ce n est pas un conflit de nom, c est du
# routage faux (checks/wan_ss7_check.sh).
# OP_ID_BASE=2 fait donc commencer les conteneurs a l operateur 2, laissant le 1
# au natif. TOUT en decoule sans autre reglage, puisque tout derive de l index :
# nom du conteneur (osmo-operator-N), backbone (172.20.0.<10+N>), point code
# (1.N.2), RCTX, segment prive (192.168.<N+1>.0/24) et ports publies.
OP_ID_BASE="${OP_ID_BASE:-1}"
op_backbone_ip() { echo "172.20.0.$((10 + $1))"; }
# ── L'INDEX DU SEGMENT PRIVE : LE NOEUD OU L'OPERATEUR, SELON L'HOTE ────────
# 192.168.<index+1>.x, et tout le desaccord tenait a « index ».
#
# DOCKER un hote porte N conteneurs operateurs, chacun dans son netns. Ce
# qui les distingue est le NUMERO D'OPERATEUR ; le noeud, lui, est
# commun a tous les conteneurs de la machine. index = operateur.
# VM/NATIF la machine EST le noeud et ne porte qu'un operateur. Ce qui la
# distingue de ses voisines est le NUMERO DE NOEUD. index = noeud.
#
# Sans cette distinction, qosmo-grgsm/run_modules/08-gabarits.sh appelait
# op_private_ip($OPERATOR_ID) et ecrivait 192.168.2.10 dans osmo-ggsn.cfg sur
# TOUTES les VM - operateur 1 partout - pendant que le plan de l'ISO reservait
# 192.168.<noeud+1>.x. Sur le noeud 1 les deux coincidaient et rien ne se
# voyait ; sur le noeud 2, le GGSN se liait a l'adresse du noeud 1.
# La bascule est ici, une fois, et les deux jumelles (start.sh, lib/gabarits.sh)
# en heritent.
_osmo_priv_index() {
local op="${1:-1}"
# [2026-08-31] L ORCHESTRATEUR N EST PAS DANS LE CONTENEUR QU IL CREE.
# La detection ci-dessous demande "suis-je DANS un conteneur ?" et c est la
# bonne question pour du code qui tourne a l interieur. Mais start.sh, lui,
# provisionne les conteneurs DEPUIS L HOTE : il prenait donc la branche
# VM/natif et rendait l index du NOEUD - 1 - pour tous les operateurs.
# Les deux reseaux prives se retrouvaient sur 192.168.2.0/24, docker
# refusait le second pour chevauchement, et la boucle s arretait net :
# ── Operateur 2 : ... Prive : 192.168.2.10
# ✗ Echec creation reseau gsm-net-op2 (192.168.2.0/24)
# osmo-operator-2 n etait jamais cree, sans qu aucun message ne parle de
# collision d adresses.
# OSMO_PRIV_BY_OP=1 dit "je provisionne par operateur" ; start_bridge_mode
# la pose. C est la seule chose que la detection ne pouvait pas deviner.
[ "${OSMO_PRIV_BY_OP:-0}" = "1" ] && { printf '%s' "$op"; return; }
# Meme detection que start-direct.sh : le couple /.dockerenv +
# /etc/docker-entrypoint-cmd identifie un conteneur DE CE DEPOT ; le cgroup
# sert de repli pour un conteneur quelconque.
if [ -f /.dockerenv ] || grep -qa 'docker\|containerd' /proc/1/cgroup 2>/dev/null; then
printf '%s' "$op"; return
fi
local n="${OSMO_WAN_NODE:-${WAN_NODE_ID:-}}"
[ -n "$n" ] || n="$(awk -F= '/^OSMO_WAN_NODE=/{gsub(/[ \r\t]/,"",$2);v=$2} END{print v}' \
"${ROLE_FILE:-/etc/osmo-role}" 2>/dev/null)"
[ -n "$n" ] || n="$(sed -n 's/^PLAN_NODE=//p' "${OSMOCOM_CFG:-/etc/osmocom}/radio-plan.env" 2>/dev/null | tail -1)"
case "$n" in [1-9]) ;; *) n=1 ;; esac
printf '%s' "$n"
}
op_private_ip() { echo "192.168.$(($(_osmo_priv_index "$1") + 1)).10"; }
op_private_gw() { echo "192.168.$(($(_osmo_priv_index "$1") + 1)).1"; }
op_private_net() { echo "192.168.$(($(_osmo_priv_index "$1") + 1)).0/24"; }
op_container() { echo "osmo-operator-$1"; }
op_rctx_msc() { echo $(( $1 * 100 + 10 )); }
op_rctx_stp() { echo $(( $1 * 100 + 20 )); }
op_rctx_bsc() { echo $(( $1 * 100 + 30 )); }
op_rctx_inter() { echo $(( $1 * 100 + 50 )); }
# ── Le PLMN : le MCC est le PAYS, donc le NOEUD ; le MNC, l'operateur ────────
# Un MCC fige a 001 pour tout le monde ne laissait que le MNC pour distinguer
# les reseaux, et toute valeur ecrite en dur ailleurs - les IMSI des MS
# l'etaient - retombait sur un PLMN plausible mais etranger : le mobile de
# l'operateur 2 presentait 001-01, son reseau diffusait 001-02, et le Location
# Updating restait sans reponse, pile entiere debout.
# Le noeud, c'est ce que --node passe a start-direct.sh. Avec --node-per-op
# chaque conteneur est son propre noeud, donc son propre pays, et le MNC
# retombe a 01 ; sans lui, les conteneurs sont N operateurs d'un meme pays.
op_plmn_node() { if [ "${WAN_NODE_PER_OP:-0}" = "1" ]; then echo $(( ${WAN_NODE_ID:-1} + $1 - 1 )); else echo "${WAN_NODE_ID:-1}"; fi; }
op_plmn_index() { if [ "${WAN_NODE_PER_OP:-0}" = "1" ]; then echo 1; else echo "$1"; fi; }
# printf '%03d' sur une valeur vide ou nulle rend "000", et osmo-msc REFUSE
# "network country code 000" : il abandonne la lecture a cette ligne et systemd
# le relance trois fois par seconde jusqu'a y renoncer. Le lanceur, lui,
# affichait une croix puis "Core Osmocom pret". Trois chiffres, une heure de
# recherche dans le SS7. Un numero de noeud a zero est un bug, pas un MCC : on
# le borne et on le dit, une fois.
_plmn_borne() { # $1 = valeur, $2 = plancher, $3 = quoi
case "$1" in
''|*[!0-9]*) echo " ATTENTION : $3 illisible ('$1') - ramene a $2" >&2; echo "$2" ;;
*) if [ "$1" -lt "$2" ]; then echo " ATTENTION : $3 vaut $1 - ramene a $2" >&2; echo "$2"; else echo "$1"; fi ;;
esac
}
op_mcc() { printf '%03d' "$(_plmn_borne "$(op_plmn_node "$1")" 1 "numero de noeud (MCC)")"; }
op_mnc() { printf '%02d' "$(_plmn_borne "$(op_plmn_index "$1")" 1 "rang d'operateur (MNC)")"; }
# Noms sans espace : la commande VTY "short name" d'osmo-msc prend un mot, et
# un nom coupe en deux y passe pour un argument surnumeraire.
# ── Les jeux de donnees du banc : data/*.txt ─────────────────────────────────
# Noms d'operateur, pays, etat civil et adresses ne sont pas ecrits dans ce
# script : ils vivent dans data/, un par ligne, editables sans toucher au code.
# On les MELANGE au chargement - c'est la le tirage au hasard - puis on les
# fige pour toute la duree du lancement : un operateur qui changerait de nom ou
# de pays entre deux lignes du journal ne serait plus le meme reseau.
OSMO_DATA_DIR="${OSMO_DATA_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/data}"
# $1 = nom du tableau a remplir, $2 = fichier, le reste = valeurs de secours.
# Le depot peut etre incomplet (une ISO n'embarque pas toujours data/) : sans
# repli, un banc entier s'arreterait sur un fichier d'agrement absent.
_load_pool() {
local -n __out=$1
local __file=$2; shift 2
__out=()
if [ -r "$__file" ]; then
if command -v shuf >/dev/null 2>&1; then
mapfile -t __out < <(grep -vE '^[[:space:]]*(#|$)' "$__file" | shuf)
else
mapfile -t __out < <(grep -vE '^[[:space:]]*(#|$)' "$__file")
fi
fi
[ "${#__out[@]}" -gt 0 ] || __out=("$@")
}
declare -a POOL_OP POOL_PAYS POOL_NOMS POOL_ADR
_load_pool POOL_OP "$OSMO_DATA_DIR/op.txt" OsmoOP1 OsmoOP2 OsmoOP3
_load_pool POOL_PAYS "$OSMO_DATA_DIR/pays.txt" Syldavie Bordurie Poldevie
_load_pool POOL_NOMS "$OSMO_DATA_DIR/noms.txt" "Jean Dupont" "Marie Martin"
_load_pool POOL_ADR "$OSMO_DATA_DIR/adresses.txt" "rue des Ondes Courtes, Fontaine-sur-Onde"
op_default_name() { echo "${POOL_OP[$(( ($1 - 1) % ${#POOL_OP[@]} ))]}"; }
op_country() { local n; n=$(op_plmn_node "$1"); echo "${POOL_PAYS[$(( (n - 1) % ${#POOL_PAYS[@]} ))]}"; }
# WAN helpers
wan_sip_port() { echo $(( WAN_SIP_BASE + ($1 - 1) * 2 )); }
wan_rtp_start() { echo $(( WAN_RTP_BASE + ($1 - 1) * WAN_RTP_PER_OP )); }
wan_rtp_end() { echo $(( WAN_RTP_BASE + $1 * WAN_RTP_PER_OP - 1 )); }
wan_sms_port() { echo $(( 7890 + $1 - 1 )); }
# Un WAN est actif - legacy OU mesh. Sert partout ou la question est "faut-il
# ouvrir les ports WAN", independamment du mecanisme choisi.
wan_active() { [ "$WAN_ENABLED" = "true" ] || [ "${WAN_MESH:-0}" = "1" ]; }
# Linphone helpers - port SIP/RTP expose sur le host
# ── PLUS AUCUN DECALAGE DE PORTS ────────────────────────────────────────────
# [2026-08-31] Il y avait ici une detection : "5060 est-il pris ? alors decale
# les publications de +100". Elle ne servait qu a une chose - eviter que le
# 5060 publie d un conteneur n entre en conflit avec l Asterisk du natif.
#
# On ne publie PLUS aucun port SIP/RTP pour les conteneurs : ils sont sur un
# bridge routable et se joignent en direct, a leur propre adresse, sur le port
# nominal (verifie : 172.20.0.12:5060 et 172.20.0.13:5060 repondent 401 depuis
# l hote, sans la moindre publication). Le conflit ayant disparu avec sa cause,
# le decalage n a plus d objet - et il tirait avec lui une sonde instantanee,
# donc une course : lancee juste apres l arret du banc, elle voyait 5060 libre,
# ne decalait rien, et le conteneur mourait quand le natif remontait.
#
# Les fonctions ci-dessous gardent leur forme (generate_configs.sh s en sert
# encore pour ecrire les configs) mais rendent desormais les valeurs NOMINALES.
# OSMO_PORT_OFFSET=N reste disponible pour qui voudrait retablir un decalage.
PORT_OFFSET="${OSMO_PORT_OFFSET:-0}"
# Le RTP suit la meme regle : plage nominale, 200 ports par operateur.
linphone_sip_port() { echo $(( 5060 + ${PORT_OFFSET:-0} + ($1 - 1) )); }
linphone_rtp_start() { echo $(( 30000 + ${PORT_OFFSET:-0} * 10 + ($1 - 1) * 200 )); }
linphone_rtp_end() { echo $(( 30000 + ${PORT_OFFSET:-0} * 10 + $1 * 200 - 1 )); }
# Global - detecte avant la boucle operateurs
HOST_IP="127.0.0.1"
ALSA_OUTPUT="${ALSA_OUTPUT:-default}"
ALSA_INPUT="${ALSA_INPUT:-default}"
HOST_AUDIO_RELAY=""
WSLG_TCP_PORT="${WSLG_TCP_PORT:-4713}"
banner() {
echo -e "${CYAN}"
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Osmocom GSM/EGPRS Virtual Network ║"
echo "║ Multi-Operator SS7 + osmo-gapk Audio ║"
echo "╚══════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
# ── Whiptail helpers ────────────────────────────────────────────────────────
WT_BACKTITLE="Osmocom GSM/EGPRS Virtual Network"
WT_WIDTH=72
check_whiptail() {
if ! command -v whiptail >/dev/null 2>&1; then
echo -e "${RED}whiptail introuvable.${NC} Installez-le :" >&2
echo -e " ${CYAN}apt-get install -y whiptail${NC} (Debian/Ubuntu)" >&2
echo -e " ${CYAN}dnf install -y newt${NC} (Fedora/RHEL)" >&2
exit 1
fi
}
# ── MODE NON INTERACTIF : LE QUESTIONNAIRE REPOND TOUT SEUL ─────────────────
# [2026-08-31] Chaque wt_* ouvre une boite whiptail et ATTEND. C est parfait au
# terminal, et fatal partout ailleurs : lance par une icone du bureau, par
# start-multi.sh ou par un script, start.sh s arretait sur « Nombre
# d'operateurs (1-36) » - un dialogue que personne ne voit et que personne ne
# valide. Le lancement paraissait bloque ou mort, alors qu il attendait une
# reponse.
#
# On repond donc AUTOMATIQUEMENT, avec la valeur que la boite proposait deja :
# wt_input -> son defaut ($3), celui qui est pre-rempli dans le champ ;
# wt_menu -> la PREMIERE entree, celle sur laquelle le curseur se pose ;
# wt_yesno -> non, parce que les boites sont en --defaultno ;
# wt_msg -> ecrit sur stderr au lieu d attendre un <Ok>.
# Aucune de ces reponses n est inventee : c est exactement ce qu on obtient en
# appuyant sur Entree.
#
# DEUX DECLENCHEURS. OSMO_NONINTERACTIVE=1 le force (start-multi.sh le pose).
# Et l absence de TTY sur l entree standard le declenche seule : c est la
# signature d un appel par icone, par cron ou par pipe - la ou aucun dialogue
# ne peut aboutir. Au terminal, rien ne change : les questions sont posees.
_wt_auto() { [ "${OSMO_NONINTERACTIVE:-0}" = "1" ] || [ ! -t 0 ]; }
wt_input() {
if _wt_auto; then
echo "[auto] $1 : ${3}" >&2
printf '%s' "$3"; return 0
fi
whiptail --backtitle "$WT_BACKTITLE" --title "$1" \
--inputbox "$2" 10 "$WT_WIDTH" "$3" 3>&1 1>&2 2>&3
}
wt_yesno() {
if _wt_auto; then echo "[auto] $1 : non" >&2; return 1; fi
whiptail --backtitle "$WT_BACKTITLE" --title "$1" \
--defaultno --yesno "$2" 11 "$WT_WIDTH" 3>&1 1>&2 2>&3
}
wt_menu() {
local title="$1" prompt="$2"; shift 2
local nitems=$(( $# / 2 ))
if _wt_auto; then
echo "[auto] $title : ${1}" >&2
printf '%s' "$1"; return 0
fi
whiptail --backtitle "$WT_BACKTITLE" --title "$title" \
--menu "$prompt" 20 "$WT_WIDTH" "$nitems" "$@" 3>&1 1>&2 2>&3
}
wt_msg() {
if _wt_auto; then echo "$1" >&2; return 0; fi
whiptail --backtitle "$WT_BACKTITLE" --msgbox "$1" 10 "$WT_WIDTH"
}
# ── Loopback audio cote session utilisateur ───────────────────────────────
# ── Session audio de l'hote : QUI, et OU ────────────────────────────────────
# [2026-08-12] Ces deux fonctions codaient "nirvana" en dur et cherchaient
# /home/<user>/osmo-operator/loopback.sh - un chemin QUI N'EXISTE PAS (le script
# est network/loopback.sh). Resultat : enable_user_loopback echouait sur toute
# machine sans utilisateur nirvana, et disable_user_loopback (appele a l'arret)
# etait deja du code mort ICI. On resout les deux a l'execution.
#
# Le critere du bon utilisateur n'est pas son nom mais la PRESENCE de son socket
# pulse : c'est la seule chose qui distingue une session graphique vivante d'un
# compte qui existe. Meme logique que session_user() de wslg-audio-bridge.sh,
# etendue au balayage de /run/user/*.
session_pulse_user() {
local u uid sock
for u in "${HOST_PULSE_USER:-}" "${SUDO_USER:-}" "$(logname 2>/dev/null || true)" "${USER:-}"; do
[ -n "$u" ] || continue
uid="$(id -u "$u" 2>/dev/null)" || continue
[ -S "/run/user/${uid}/pulse/native" ] && { echo "$u"; return 0; }
done
# Dernier recours : proprietaire du premier socket pulse trouve. Couvre le
# `sudo -i` (SUDO_USER perdu) et les sessions sans logname (cron, service).
for sock in /run/user/*/pulse/native; do
[ -S "$sock" ] || continue
uid="${sock#/run/user/}"; uid="${uid%%/*}"
u="$(getent passwd "$uid" 2>/dev/null | cut -d: -f1)"
[ -n "$u" ] && { echo "$u"; return 0; }
done
return 1
}
# Le script de loopback vit dans le depot, pas dans un $HOME.
find_loopback_script() {
local d c
d="$(cd "$(dirname "$0")" && pwd)"
for c in "$d/network/loopback.sh" "$d/loopback.sh"; do
[ -x "$c" ] && { echo "$c"; return 0; }
done
return 1
}
enable_user_loopback() {
local target_user target_uid target_runtime loopback_script
target_user="$(session_pulse_user)" || {
echo -e " ${YELLOW}[audio] aucune session PulseAudio utilisateur - loopback ignore${NC}"
return 0
}
loopback_script="$(find_loopback_script)" || {
echo -e " ${YELLOW}[audio] network/loopback.sh introuvable - loopback ignore${NC}"
return 0
}
target_uid="$(id -u "$target_user")"
target_runtime="/run/user/${target_uid}"
echo -e "${GREEN}=== [audio] Loopback user session ===${NC}"
echo -e " user : ${CYAN}${target_user}${NC}"
echo -e " runtime : ${CYAN}${target_runtime}${NC}"
echo -e " script : ${CYAN}${loopback_script}${NC}"
sudo -u "$target_user" \
XDG_RUNTIME_DIR="$target_runtime" \
PULSE_SERVER="unix:${target_runtime}/pulse/native" \
"$loopback_script" enable >/dev/null 2>&1 || {
echo -e " ${RED}[FAIL]${NC} enable loopback"
return 1
}
echo -e "[ OK ]${NC} loopback active"
}
disable_user_loopback() {
local target_user target_uid target_runtime loopback_script
target_user="$(session_pulse_user)" || return 0
loopback_script="$(find_loopback_script)" || return 0
target_uid="$(id -u "$target_user")"
target_runtime="/run/user/${target_uid}"
sudo -u "$target_user" \
XDG_RUNTIME_DIR="$target_runtime" \
PULSE_SERVER="unix:${target_runtime}/pulse/native" \
"$loopback_script" disable || true
}
build_alsa_args() {
local alsa_args=""
local src_asound="$(dirname "$0")/configs/asound.conf"
local host_asound="/tmp/osmocom-alsa/asound.conf"
mkdir -p /tmp/osmocom-alsa
# Docker cree un REPERTOIRE quand la source d'un bind -v n'existe pas :
# si un run precedent a laisse ce repertoire, le mount sur /etc/asound.conf
# (un fichier) echoue avec "not a directory". On le nettoie avant la copie.
if [ -d "$host_asound" ]; then
rm -rf "$host_asound"
fi
if [ -d /dev/snd ]; then
alsa_args="${alsa_args} --device /dev/snd"
if getent group audio >/dev/null 2>&1; then
alsa_args="${alsa_args} --group-add $(getent group audio | cut -d: -f3)"
fi
fi
local has_pulse="true"
# ── LE SOCKET PULSE DOIT ETRE MONTE, PAS SEULEMENT NOMME ────────────────
# [2026-08-31] Cette ligne posait PULSE_SERVER=unix:/run/pulse/native sans
# que RIEN ne monte ce socket dans le conteneur. Le chemin existe sur
# l hote, pas chez l invite : le plugin ALSA-pulse ouvrait donc dans le
# vide, et gapk mourait a l initialisation -
# pulse.c:242 (pulse_connect) PulseAudio: Unable to connect: Connection refused
# pq_alsa.c:168 Couldn't init ALSA device 'gsm_out': Connection refused
# gapk_io.c:468 Failed to initialize GAPK I/O
# soit un appel parfaitement etabli et TOTALEMENT muet, sans une ligne qui
# parle d audio dans les logs du coeur. Verifie le 31/08 dans
# osmo-operator-1 : `docker inspect` ne montrait aucun montage pulse, et
# `PULSE_SERVER=unix:/run/pulse/native pactl info` echouait dans le
# conteneur la ou tcp:172.20.0.1:4713 repondait.
#
# On monte donc le socket quand il existe - c est le chemin le plus court
# et le moins bavard - et on retombe sur le relais TCP sinon. Le socket est
# en srwxrwxrwx et le module unix est en auth-anonymous=1 : l utilisateur
# osmocom du conteneur peut s y connecter.
local _pa_sock=""
for _s in /var/run/pulse/native /run/pulse/native "/run/user/$(id -u)/pulse/native"; do
[ -S "$_s" ] && { _pa_sock="$_s"; break; }
done
if [ -n "$_pa_sock" ]; then
alsa_args="${alsa_args} -v ${_pa_sock}:/run/pulse/native"
alsa_args="${alsa_args} -e PULSE_SERVER=unix:/run/pulse/native"
elif [ -n "${HOST_AUDIO_RELAY:-}" ]; then
alsa_args="${alsa_args} -e PULSE_SERVER=${HOST_AUDIO_RELAY}"
else
# Ni socket ni relais : on le DIT. Sans ce message, le silence de
# l appel restait la seule trace du probleme.
alsa_args="${alsa_args} -e PULSE_SERVER=tcp:${INTER_NET_GATEWAY:-172.20.0.1}:${WSLG_TCP_PORT:-4713}"
echo -e " ${YELLOW}Audio : aucun socket pulse trouve et relais non arme - repli TCP a l aveugle${NC}" >&2
fi
local asound_ok="false"
if [ -f "$src_asound" ]; then
if cp -f "$src_asound" "$host_asound" 2>/dev/null && [ -f "$host_asound" ]; then
asound_ok="true"
else
echo -e " ${YELLOW}Audio : copie de asound.conf vers ${host_asound} impossible${NC}" >&2
fi
fi
if [ "$asound_ok" = "true" ]; then
alsa_args="${alsa_args} -v ${host_asound}:/etc/asound.conf:rw"
alsa_args="${alsa_args} -e ALSA_OUTPUT=gsm_out -e ALSA_INPUT=gsm_in"
alsa_args="${alsa_args} -e ALSA_CARD=gsm_out -e GAPK_ALSA_DEV=gsm_out"
if [ "$has_pulse" = "true" ]; then
echo -e " ${GREEN}Audio : PulseAudio + asound.conf${NC}" >&2
else
echo -e " ${YELLOW}Audio : asound.conf sans PulseAudio${NC}" >&2
fi
else
alsa_args="${alsa_args} -e ALSA_OUTPUT=default -e ALSA_INPUT=default"
alsa_args="${alsa_args} -e ALSA_CARD=default -e GAPK_ALSA_DEV=default"
echo -e " ${YELLOW}Audio : configs/asound.conf absent, fallback default${NC}" >&2
fi
echo "$alsa_args"
}
# ── Audio hote : expose le pulse de l'hote en TCP ──────────────────────────
ensure_host_audio_relay() {
[ -n "${HOST_AUDIO_RELAY_DONE:-}" ] && return 0
HOST_AUDIO_RELAY_DONE=1
local bridge="$(dirname "$0")/scripts/wslg-audio-bridge.sh"
[ -x "$bridge" ] || { echo -e " ${YELLOW}[host-audio] $bridge introuvable${NC}" >&2; return 0; }
if "$bridge" host-relay; then
# [2026-08-31] ON VERIFIE QUE LE PORT REPOND, on ne se fie plus au seul
# code de retour. `host-relay` rend 0 des que le chargement du module a
# ete DEMANDE - pas quand pulseaudio accepte des connexions. On
# annoncait donc "relai pret" pendant que gapk, dans le conteneur,
# recevait :
# lib pulse.c:242:(pulse_connect) PulseAudio: Unable to connect:
# Connection refused
# pq_alsa.c:168 Couldn't init ALSA device 'gsm_out': Connection refused
# gapk_io.c:468 Failed to initialize GAPK I/O
# -> appel etabli et TOTALEMENT muet, avec un demarrage qui affichait
# "relai pret" en vert. Constate le 31/08, cinq minutes apres un boot :
# la course se gagne ou se perd selon ce que la machine fait par
# ailleurs, ce qui en fait un "ca marche une fois sur deux".
# gapk n init son I/O QU UNE FOIS : rater cette fenetre, c est le
# silence pour tout l appel.
local _p=0 _i
for _i in $(seq 1 30); do
if (exec 3<>"/dev/tcp/${INTER_NET_GATEWAY}/${WSLG_TCP_PORT:-4713}") 2>/dev/null; then
exec 3>&- 2>/dev/null; _p=1; break
fi
sleep 1
done
if [ "$_p" = "1" ]; then
HOST_AUDIO_RELAY="tcp:${INTER_NET_GATEWAY}:${WSLG_TCP_PORT:-4713}"
echo -e " ${GREEN}[host-audio] relai pret → ${HOST_AUDIO_RELAY}${NC}"
else
HOST_AUDIO_RELAY=""
echo -e " ${RED}[host-audio] relai charge mais ${INTER_NET_GATEWAY}:${WSLG_TCP_PORT:-4713} NE REPOND PAS apres 30 s${NC}" >&2
echo -e " ${YELLOW} → l audio du conteneur sera muet. Verifier :${NC}" >&2
echo -e " ${YELLOW} pactl list modules | grep -A2 native-protocol-tcp${NC}" >&2
fi
else
HOST_AUDIO_RELAY=""
echo -e " ${YELLOW}[host-audio] relai non ouvert - fallback carte locale dans le conteneur${NC}"
fi
# [2026-08-12] Sens MONTANT (micro). Le relai ci-dessus ne couvre que la
# descente (gsm_audio.monitor → HP de l'hote). Le micro, lui, vient du
# navigateur, qui capture la source PAR DEFAUT de l'hote - laquelle peut
# tres bien etre une entree fantome qui ne sort que du zero (vecu ici :
# defaut non mute a 93 % et pourtant silence total, cf. host_mic()).
# On choisit donc la source a l'oreille, pas au nom. Non fatal.
"$bridge" host-mic || true
}
# ══════════════════════════════════════════════════════════════════════════════
# Build
# ══════════════════════════════════════════════════════════════════════════════
# [2026-08-31] QUICK PAR DEFAUT, et sans poser la question.
# Le defaut etait 0 - "normal (--no-cache)" - et, faute de choix explicite,
# start.sh ouvrait une boite whiptail (l.2578, choose_build_mode) pour la
# demander. Deux consequences :
# - la recompilation Osmocom complete, ~40 minutes, etait ce qu on obtenait
# en repondant trop vite, alors que l image ne change quasiment jamais ;
# - la QUESTION elle-meme rendait le script inutilisable depuis une icone ou
# un appel scripte : il s arretait sur un dialogue que personne ne voit.
# Le cache docker est la bonne reponse dans la vie de tous les jours ; on ne
# reconstruit a neuf que quand on le DEMANDE.
#
# Comment forcer une reconstruction complete, les deux marchent :
# ./start.sh normal ... (argument positionnel)
# OSMO_QUICK=0 ./start.sh ... (variable d environnement)
QUICK="${OSMO_QUICK:-1}"
# EXPLICIT a 1 meme sans demande : c est ce qui supprime le dialogue. Le passer
# a 0 ici ferait revenir la question a chaque lancement non interactif.
QUICK_EXPLICIT=1
build_run_image() {
if [ "${QUICK:-0}" = "1" ]; then
echo -e "${GREEN}Build de l'image run...${NC} ${YELLOW}(quick - cache docker reutilise)${NC}"
docker build --build-arg QEMU_CACHE_BUST=$(date +%s) -f Dockerfile.run -t "$IMAGE_RUN" .
else
echo -e "${GREEN}Build de l'image run...${NC} ${CYAN}(normal - --no-cache)${NC}"
docker build --no-cache --build-arg QEMU_CACHE_BUST=$(date +%s) -f Dockerfile.run -t "$IMAGE_RUN" .
fi
echo -e "${GREEN}Image '$IMAGE_RUN' prete.${NC}"
}
check_image() {
if ! docker image inspect "$IMAGE_RUN" &>/dev/null; then
echo -e "${RED}Image '$IMAGE_RUN' introuvable - build en cours...${NC}"
build_run_image
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# Generation dynamique - pjsip interop trunks
# ══════════════════════════════════════════════════════════════════════════════
generate_pjsip_interop_trunks() {
local op_id=$1
local n_operators=$2
# ── LES OPERATEURS SOUS LA BASE SONT LES NATIFS ────────────────────────
# [2026-08-31] La boucle partait DE la base. Avec OP_ID_BASE=2 - les
# conteneurs prenant les rangs 2..N, le 1 restant au natif sur l hote -
# op2 ne fabriquait qu un trunk vers op3 : RIEN vers le natif. Un appel
# 100201 -> 100101 tombait dans le fourre-tout _X. de [interop_out] et
# sortait en Congestion, sans qu une ligne dise que la route n existe pas.
# Meme trou que sms-routing.conf, sur le chemin VOIX.
for remote_op in $(seq 1 $(( ${OP_ID_BASE:-1} - 1 ))) \
$(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
[ "$remote_op" -eq "$op_id" ] && continue
local remote_ip
# Un operateur sous la base est NATIF : il tourne sur l hote, pas dans
# un conteneur. Vu d un conteneur, l hote est la passerelle du bridge
# docker - la meme adresse que le pont audio et que sms-routing.conf.
# NB : match=172.20.0.1 n avale pas les REGISTER Linphone, qui arrivent
# par la meme passerelle : pjsip.conf pose endpoint_identifier_order=
# username,ip,anonymous - le username passe avant l IP.
if [ "$remote_op" -lt "${OP_ID_BASE:-1}" ]; then
remote_ip="${INTER_NET_GATEWAY:-172.20.0.1}"
else
remote_ip=$(op_backbone_ip "$remote_op")
fi
cat <<EOF
[interop-identify-op${remote_op}]
type=identify
endpoint=interop_trunk_op${remote_op}
match=${remote_ip}
[interop_trunk_op${remote_op}]
type=endpoint
transport=transport-udp
context=interop_in
disallow=all
allow=gsm
allow=ulaw
aors=interop_trunk_op${remote_op}
direct_media=no
rtp_symmetric=yes
force_rport=yes
media_encryption=no
[interop_trunk_op${remote_op}]
type=aor
contact=sip:${remote_ip}:5060
qualify_frequency=15
qualify_timeout=5.0
EOF
done
}
generate_extensions_interop_out() {
local op_id=$1
local n_operators=$2
# Le prefixe porte le NOEUD : MSISDN = <noeud>00<operateur><rang>. Fige a
# "600", le motif n'accrochait plus rien des que le numero commencait par le
# numero de noeud, et les appels inter-operateurs sortaient en Congestion
# sans qu'une ligne ne designe le motif.
local _pfx; _pfx="$(osmo_msisdn_pfx "$(osmo_node_id)")"
cat <<'EOF'
[interop_out]
EOF
# Un seul motif : les MSISDN font six chiffres, <noeud>00<operateur><rang>.
# deux motifs _<op>XXXX / _<op>XXXXX visaient l'ancien plan a cinq chiffres,
# ou le premier chiffre du numero ETAIT l'operateur - plus rien ne matchait.
# ── LES OPERATEURS SOUS LA BASE SONT LES NATIFS ────────────────────────
# [2026-08-31] La boucle partait DE la base. Avec OP_ID_BASE=2 - les
# conteneurs prenant les rangs 2..N, le 1 restant au natif sur l hote -
# op2 ne fabriquait qu un trunk vers op3 : RIEN vers le natif. Un appel
# 100201 -> 100101 tombait dans le fourre-tout _X. de [interop_out] et
# sortait en Congestion, sans qu une ligne dise que la route n existe pas.
# Meme trou que sms-routing.conf, sur le chemin VOIX.
for remote_op in $(seq 1 $(( ${OP_ID_BASE:-1} - 1 ))) \
$(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
[ "$remote_op" -eq "$op_id" ] && continue
cat <<EOF
exten => _${_pfx}${remote_op}XX,1,NoOp(=== INTEROP OUT Op${remote_op}: \${EXTEN} ===)
same => n,Dial(PJSIP/\${EXTEN}@interop_trunk_op${remote_op},,rT)
same => n,Congestion()
same => n,Hangup()
EOF
done
# ── LES SOFTPHONES AUSSI, ET SANS PREFIXE ───────────────────────────────
# [2026-08-31] Depuis [internal], un Linphone ne pouvait joindre un autre
# operateur qu'en composant 9 + numero (_9. du gabarit - dont l'exemple
# « 920001 pour joindre 20001 sur Op2 » date d'ailleurs du plan a CINQ
# chiffres, abandonne). Compose tel quel, 100201 tombait sur le fourre-tout
# _X. de [internal] et partait au MSC LOCAL, qui le rendait en
# rx MNCC_SETUP_REQ for unknown subscriber number '100201'
# avant de le ressortir par MNCC vers Asterisk. Le detour aboutissait, mais
# il consommait une transaction MNCC pour rien et faisait dependre un appel
# SIP->SIP du bon vouloir du MSC.
# On declare donc la route explicitement. _<pfx><op>XX est PLUS SPECIFIQUE
# que _X. : Asterisk la choisit d'office, sans qu'il faille toucher au
# fourre-tout du gabarit. Gosub(sub-record) comme pour _9., sinon ces
# appels-la seraient les seuls a ne pas etre enregistres.
printf '[internal]\n\n'
for remote_op in $(seq 1 $(( ${OP_ID_BASE:-1} - 1 ))) \
$(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
[ "$remote_op" -eq "$op_id" ] && continue
cat <<EOF
exten => _${_pfx}${remote_op}XX,1,NoOp(=== SIP -> INTER-OP Op${remote_op}: \${EXTEN} - CallerID: \${CALLERID(all)} ===)
same => n,Gosub(sub-record,s,1(\${EXTEN}))
same => n,Goto(interop_out,\${EXTEN},1)
EOF
done
# PAS de fourre-tout _X. ici : le gabarit configs/extensions.conf en declare
# deja un dans ce meme contexte. Deux extensions identiques dans un contexte
# font refuser la seconde, avec six avertissements a chaque chargement :
# add_priority: Unable to register extension '_X.' priority 1
# in 'interop_out', already in use
# Rien ne cassait - le fourre-tout du gabarit restait en place - mais ces
# lignes noyaient celles qui comptent.
}
# ══════════════════════════════════════════════════════════════════════════════
# SMS routing fallback
# ══════════════════════════════════════════════════════════════════════════════
_generate_sms_routing_conf_fallback() {
# 'local i' n'est PAS cosmetique. Cette fonction est appelee, via
# apply_config_templates, DEPUIS la boucle "for i in ..." qui demarre les
# operateurs. Sans local, sa propre boucle ecrase la variable de l'appelant
# et la laisse a n_operators : tous les conteneurs suivants calculaient
# alors leurs ports avec le MEME indice, d'ou
# Bind for 0.0.0.0:5082 failed: port is already allocated
# au deuxieme conteneur - et, avant l'erreur, deux operateurs annonces sur
# les memes SIP/RTP/SMS.
local i
local op_id=$1 n_operators=$2
printf '# sms-routing.conf - Fallback\n\n[local]\noperator_id = %s\nsc_address = 1999001%s444\n\n[operators]\n' "$op_id" "$op_id"
# ── LES OPERATEURS SOUS LA BASE SONT LES NATIFS ─────────────────────────
# [2026-08-31] start-multi.sh pose OP_ID_BASE=2 : les conteneurs prennent
# les rangs 2..N et l'operateur 1 reste NATIF, sur l'hote. Comme la boucle
# part de OP_ID_BASE, il n'apparaissait ni dans [operators] ni dans
# [routes] - releve dans osmo-operator-2 : « 2 = ... / 3 = ... » et rien
# pour le 1. Un SMS vers 100101 sortait donc en « No route for
# destination », et le natif etait injoignable depuis tout le banc.
# Il existe pourtant : il n'est simplement pas dans un conteneur. On le
# pointe sur la passerelle du bridge docker, qui EST l'hote vu d'un
# conteneur - la meme adresse que le pont audio (172.20.0.1).
local _natif_gw="${INTER_NET_GATEWAY:-172.20.0.1}"
for i in $(seq 1 $(( ${OP_ID_BASE:-1} - 1 ))); do printf '%s = %s\n' "$i" "$_natif_gw"; done
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
printf '%s = %s\n' "$i" "$(op_backbone_ip "$i")"
done
printf '\n[routes]\n'
for i in $(seq 1 $(( ${OP_ID_BASE:-1} - 1 ))); do
for ms in 1 2; do printf '%s = %s\n' "$(osmo_msisdn "$(osmo_node_id)" "$i" "$ms")" "$i"; done
done
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
for ms in 1 2; do printf '%s = %s\n' "$(osmo_msisdn "$(osmo_node_id)" "$i" "$ms")" "$i"; done # MSISDN exacts <noeud>00<op><ms> - PAS de concatenation
done
printf '\n[relay]\nport = 7890\nconnect_timeout = 10\nretry_count = 3\nretry_delay = 5\n'
}
# ══════════════════════════════════════════════════════════════════════════════
# ─────────────────────────────────────────────────────────────────────────────
# Configuration du reseau - deleguee a ./generate_configs.sh
#
# [2026-08-03] Ce fichier portait ici une copie de 132 lignes de
# apply_config_templates(), jumelle de celle de lib/gabarits.sh (70 lignes).
# Deux implementations de la meme chose, qui divergeaient en silence : celle-ci
# copiait tout scripts/, l'autre une liste blanche perimee. Elles vivent
# desormais dans generate_configs.sh, une seule fois.
#
# generate_configs.sh cree globals.conf a la racine s'il n'existe pas - et ne
# l'ecrase JAMAIS ensuite. Tes reglages survivent donc a tous les runs. Pour
# repartir des valeurs d'usine : ./generate_configs.sh --force
# ─────────────────────────────────────────────────────────────────────────────
# start.sh n'a pas de variable de repertoire (il travaille en chemins relatifs
# depuis la racine du depot) : on resout ici, sans rien supposer.
#
# BASH_SOURCE ne suffit pas : build-iso.sh extrait ce fichier en bibliotheque
# ($WORK/start.lib.sh) puis la source, et BASH_SOURCE designe alors la COPIE,
# dans un repertoire de travail ou generate_configs.sh n'existe pas.
# OSMO_REPO_DIR laisse l'appelant nommer la vraie racine du depot.
_GC_SH="${OSMO_REPO_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/generate_configs.sh"
"$_GC_SH" >/dev/null || true
. "$_GC_SH"
# ── Mise a jour forcee des trois arbres, AVANT run.sh ─────────────────────────
# [2026-08-12] Les trois depots sont clones et `git pull`-es DANS LE Dockerfile,
# donc a l'etat du BUILD. Entre deux builds d'image (11,5 Go, plusieurs dizaines
# de minutes) un `git push` n'atteignait jamais un conteneur neuf : on lancait un
# run sur du code perime sans le voir. On les rafraichit donc a chaque demarrage,
# avant que run.sh ne lise quoi que ce soit.
#
# ORDRE : cette fonction DOIT etre appelee apres `docker run` et AVANT le
# `docker exec ... run.sh`. Apres, run.sh aurait deja lu l'ancien code.
#
# `--ff-only`, PAS de `reset --hard` : le depot qosmo-grgsm du conteneur recoit des
# commits faits a la main pendant une session (c'est le mode de travail : on
# corrige, on compile, on commite dans le conteneur). Un reset dur les
# effacerait sans le dire. En avance-rapide seule, une divergence s'ARRETE et se
# VOIT au lieu d'etre ecrasee en silence.
#
# qosmo-grgsm est RECOMPILE apres le pull : mettre la source a jour sans relier le
# binaire ne change rien a ce qui tourne - et c'est indiscernable d'un pull qui
# aurait echoue. Le juge d'un binaire a jour reste `stat -L /proc/<pid>/exe`.
#
# Non bloquant (reseau absent = on continue), mais BRUYANT : un pull rate doit
# se lire dans la sortie du lanceur, pas se deviner.
force_update_trees() {
local c=$1
echo -e " ${GREEN}[*] Mise a jour forcee des depots (avant run.sh)...${NC}"
for repo in /opt/GSM/qosmo-grgsm /opt/GSM/osmo-operator /opt/GSM/osmo-egprs-web; do
if ! docker exec "$c" test -d "$repo/.git" 2>/dev/null; then
echo -e " ${YELLOW}$repo : pas un depot git - ignore${NC}"
continue
fi
local before after
before=$(docker exec "$c" git -C "$repo" rev-parse --short HEAD 2>/dev/null || echo '?')
if docker exec "$c" git -C "$repo" pull --ff-only 2>&1 | tail -2 | sed 's/^/ /'; then
after=$(docker exec "$c" git -C "$repo" rev-parse --short HEAD 2>/dev/null || echo '?')
if [ "$before" = "$after" ]; then
echo -e " ${CYAN}$repo${NC} : deja a jour ($after)"
else
echo -e " ${GREEN}$repo${NC} : $before -> $after"
# ── LE SERVEUR WEB TOURNE DEJA, ET NODE A DEJA LU SON FICHIER ──
# [2026-08-31] Exactement le piege du ninja ci-dessous, sur un
# autre maillon : le pull change le FICHIER, pas le PROCESSUS.
# osmo-egprs-web.service est une unite systemd, donc lancee au
# BOOT du conteneur - avant cet appel - et node ne lit server.js
# qu'une fois. Mesure du 31/08 dans osmo-operator-2 :
# node demarre 18:58:18 server.js reecrit 18:58:21
# Le disque portait le correctif Kc (grep kcPollVty : 2), le
# process tournait encore sur l'ancien code, et le dashboard
# affichait « Kc absent » - panne indiscernable d'un pull rate.
# On ne relance QUE si le pull a reellement bouge.
if [ "$repo" = /opt/GSM/osmo-egprs-web ]; then
if docker exec "$c" systemctl restart osmo-egprs-web 2>/dev/null; then
echo -e " ${GREEN}osmo-egprs-web.service relance${NC} (le dashboard charge le nouveau code)"
else
echo -e " ${YELLOW}osmo-egprs-web.service : relance KO - le dashboard garde l'ancien code${NC}"
fi
fi
fi
else
echo -e " ${RED}$repo : pull KO (divergence ou reseau) - le run part sur $before${NC}"
fi
done
# Recompilation de QEMU : sans elle le pull ci-dessus ne change RIEN au
# binaire qui tourne. ninja ne reconstruit que ce qui a bouge.
echo -e " ${GREEN}[*] Recompilation QEMU (ninja)...${NC}"
if docker exec "$c" bash -c 'cd /opt/GSM/qosmo-grgsm/build && ninja' >/dev/null 2>&1; then
echo -e " ${GREEN}qemu-system-arm relie${NC} ($(docker exec "$c" stat -L -c %y /opt/GSM/qosmo-grgsm/build/qemu-system-arm 2>/dev/null | cut -c1-19))"
else
echo -e " ${RED}ninja KO - le run utilisera le binaire de l'image${NC}"
docker exec "$c" bash -c 'cd /opt/GSM/qosmo-grgsm/build && ninja 2>&1 | tail -15' | sed 's/^/ /'
fi
}
build_vol_args() {
local tmpdir=$1
local vol_args=""
[ -d "$tmpdir/osmocom" ] && vol_args="$vol_args -v $tmpdir/osmocom:/etc/osmocom"
for f in "$tmpdir/asterisk"/*.conf; do
[ -f "$f" ] || continue
vol_args="$vol_args -v $f:/etc/asterisk/$(basename "$f")"
done
if [ -f "$tmpdir/bb/mobile.cfg" ]; then
vol_args="$vol_args -v $tmpdir/bb/mobile.cfg:/root/.osmocom/bb/mobile.cfg"
fi
if [ -f "$tmpdir/bb/mobile_group1.cfg" ]; then
vol_args="$vol_args -v $tmpdir/bb/mobile_group1.cfg:/root/.osmocom/bb/mobile_group1.cfg"
fi
echo "$vol_args"
}
# ══════════════════════════════════════════════════════════════════════════════
# TUN hote
# ══════════════════════════════════════════════════════════════════════════════
prepare_host_tun() {
echo -e "${GREEN}[*] Configuration TUN sur l'hote...${NC}"
modprobe tun 2>/dev/null || true
mkdir -p /dev/net
if [ ! -c /dev/net/tun ]; then
mknod /dev/net/tun c 10 200
chmod 666 /dev/net/tun
fi
ip link del apn0 2>/dev/null || true
ip tuntap add dev apn0 mode tun
ip addr add 176.16.32.0/24 dev apn0
ip link set apn0 up
}
# ══════════════════════════════════════════════════════════════════════════════
# Inter-STP
# ══════════════════════════════════════════════════════════════════════════════
start_inter_stp() {
local n_operators=$1
local tmpdir
tmpdir=$(mktemp -d)
local inter_cfg="${tmpdir}/osmo-stp-interop.cfg"
if [ "${WAN_MESH:-0}" = "1" ]; then
# Plan WAN : un AS par couple (noeud, operateur), point codes
# 1.<noeud><op>.<role>. Le hub doit connaitre TOUS les noeuds, pas
# seulement les operateurs locaux - sinon les ASP distants s'attachent
# a un AS qui n'existe pas et sont rejetes sans explication.
echo -e "${GREEN}Generation config inter-STP WAN (${WAN_NODE_COUNT} noeuds × ${n_operators})...${NC}"
bash ./helpers/create_interop.sh --wan "$WAN_NODE_COUNT" "$n_operators" "$inter_cfg" > /dev/null
else
echo -e "${GREEN}Generation config inter-STP (${n_operators} operateurs)...${NC}"
bash ./helpers/create_interop.sh "$n_operators" "$inter_cfg" > /dev/null
fi
if [ ! -f "$inter_cfg" ]; then
echo -e "${RED}Echec generation config inter-STP${NC}"; exit 1
fi
# Le hub ne fait que router du M3UA : l'image osmocom-stp (Dockerfile.stp)
# lui suffit et pese une fraction d'osmocom-run. On ne la CONSTRUIT pas ici
# a l'insu de l'appelant - si elle manque, on reste sur l'image complete,
# qui contient aussi osmo-stp.
local stp_image="$IMAGE_RUN"
if [ "${BUILD_STP:-0}" = "1" ] && ! docker image inspect "$IMAGE_STP" >/dev/null 2>&1; then
echo -e " ${GREEN}Construction de ${IMAGE_STP} (Dockerfile.stp)...${NC}"
docker build -f "$(dirname "$0")/Dockerfile.stp" -t "$IMAGE_STP" "$(dirname "$0")" \
|| echo -e " ${YELLOW}⚠ echec - on restera sur ${IMAGE_RUN}${NC}"
fi
if docker image inspect "$IMAGE_STP" >/dev/null 2>&1; then
stp_image="$IMAGE_STP"
echo -e " ${CYAN}image ${IMAGE_STP} (STP seul)${NC}"
elif [ "${WAN_MESH:-0}" = "1" ]; then
echo -e " ${YELLOW}image ${IMAGE_STP} absente - on utilise ${IMAGE_RUN}.${NC}"
echo -e " ${CYAN}Pour la construire : docker build -f Dockerfile.stp -t ${IMAGE_STP} .${NC}"
fi
echo -e "${GREEN}Lancement inter-STP @ ${INTER_STP_IP}:2908 (PC 0.0.0)...${NC}"
docker rm -f "$INTER_STP_CONTAINER" &>/dev/null || true
# --restart, PAS --rm : avec --rm, un reboot de l'hote ne laissait pas le hub
# arrete, il le faisait DISPARAITRE - au retour, plus rien a redemarrer, et
# des ASP qui ouvrent leur SCTP vers personne. Les deux options sont
# exclusives (docker refuse la combinaison au parsing), et le "docker rm -f"
# ci-dessus tient deja le role de --rm : aucun reliquat du lancement d'avant.
# ── 2908 PUBLIE SUR LA BOUCLE LOCALE : LE RACCORD DE L OPERATEUR NATIF ──
# [2026-08-31] Sans cette ligne, un operateur NATIF (celui de
# start-direct.sh, hors conteneur) ne pouvait PAS s attacher au hub.
# Son /etc/osmocom/osmo-stp.cfg declare
# asp asp-to-inter 2908 2910 m3ua
# remote-ip 127.0.0.1
# ce qui est le bon choix - c est la seule adresse dont on soit sur - mais
# RIEN n ecoutait sur 127.0.0.1:2908 cote hote : le hub vit dans le
# conteneur, sur le reseau prive, et ne publiait aucun port
# ("docker port osmo-inter-stp" : vide). L ASP ouvrait donc son SCTP vers
# personne, et le diagnostic sortait tres loin de la cause -
# Aucune association SCTP vers ...:2908 (0/1)
# as-inter : AS_DOWN / Route par defaut -> inter-STP absente
# ce qui envoie chercher un hub en panne ou un filtrage, alors que le hub
# va bien et que rien ne le rendait joignable.
#
# /sctp, PAS /tcp : M3UA roule sur SCTP. Docker sait publier ce protocole
# (verifie sur ce banc : noyau avec module sctp, et
# "docker run -p 127.0.0.1:PORT:PORT/sctp" accepte).
#
# Lie a 127.0.0.1 et pas a 0.0.0.0 : ce point d entree ne sert QU au natif,
# sur la meme machine. L exposer sur toutes les interfaces ouvrirait le
# coeur SS7 au LAN - checks/wan_ss7_check.sh signale d ailleurs "un port
# M3UA expose sur l interface publique" comme un defaut a part entiere.
#
# L autre solution possible etait de repointer l ASP natif sur
# 172.20.0.10 avec local-ip 172.20.0.1. Ecartee : ces deux adresses
# n existent que TANT QUE le bridge docker est monte, et elles disparaissent
# avec les conteneurs - on aurait remplace une adresse absente par deux
# adresses intermittentes, dans une config que start-direct.sh regenere.
docker run -d \
--restart unless-stopped \
--name "$INTER_STP_CONTAINER" \
--hostname "$INTER_STP_CONTAINER" \
--network "$INTER_NET" \
--ip "$INTER_STP_IP" \
-p "127.0.0.1:2908:2908/sctp" \
--cap-add NET_ADMIN \
-v "${inter_cfg}:/etc/osmocom/osmo-stp-interop.cfg" \
--entrypoint bash \
"$stp_image" \
-c "sleep infinity" > /dev/null
# tmux si l'image en a un, sinon un simple processus detache.
#
# osmocom-stp est l'image "STP seul" : elle ne porte que osmo-stp et ses
# bibliotheques - c'est tout son interet, 877 Mo contre 11 Go. tmux n'y est
# pas, et l'exec echouait alors sur "tmux: executable file not found in
# $PATH". Le conteneur tournait pourtant (sleep infinity) : un hub bien
# demarre, mais SANS osmo-stp dedans - il n'ecoutait rien, et les noeuds ne
# s'attachaient a personne.
#
# tmux n'apportait ici que le confort de s'attacher a la session. La
# barriere qui suit ne lit que /tmp/osmo-stp.log : un processus detache qui
# redirige vers ce meme fichier remplit le contrat, et l'image complete
# garde son tmux quand elle en a un.
if docker exec "$INTER_STP_CONTAINER" sh -c 'command -v tmux >/dev/null 2>&1'; then
docker exec "$INTER_STP_CONTAINER" \
tmux new-session -d -s stp \
"osmo-stp -c /etc/osmocom/osmo-stp-interop.cfg 2>&1 | tee /tmp/osmo-stp.log"
else
docker exec -d "$INTER_STP_CONTAINER" \
sh -c 'exec osmo-stp -c /etc/osmocom/osmo-stp-interop.cfg > /tmp/osmo-stp.log 2>&1'
fi
echo -ne "${GREEN}[*] Attente demarrage inter-STP"
local retries=25
while [ $retries -gt 0 ]; do
if docker exec "$INTER_STP_CONTAINER" \
grep -qE "listening|m3ua.*[Ss]erver|bound" /tmp/osmo-stp.log 2>/dev/null; then
echo -e " ${GREEN}✓${NC}"
return 0
fi
if [ "$(docker inspect -f '{{.State.Running}}' "$INTER_STP_CONTAINER" 2>/dev/null)" != "true" ]; then
echo -e " ${RED}CRASH${NC}"
docker logs "$INTER_STP_CONTAINER"
exit 1
fi
echo -n "."
sleep 1
((retries--)) || true
done
echo -e " ${YELLOW}(timeout log, on continue)${NC}"
}
wait_inter_stp_ready() {
local n_operators=$1
echo -ne "${GREEN}[*] Verification stabilisation inter-STP (routes SS7)${NC}"
for i in {1..15}; do sleep 1; echo -n "."; done
echo -e " ${GREEN}✓${NC}"
return 0
}
wait_bb_vty() {
local container="$1"
local timeout=120
local elapsed=0
echo -ne " Attente BB VTY 4247 "
while ! docker exec "$container" bash -c "echo >/dev/tcp/127.0.0.1/4247" 2>/dev/null; do
sleep 2; elapsed=$((elapsed + 2)); echo -n "."
if [ "$elapsed" -ge "$timeout" ]; then echo -e " ${RED}TIMEOUT${NC}"; return 1; fi
done
echo -e " ${GREEN}OK${NC}"
}
wait_stp_vty() {
local container="$1"
local timeout=60
local elapsed=0
echo -ne " Attente STP VTY 4239 "
while ! docker exec "$container" bash -c "echo >/dev/tcp/127.0.0.1/4239" 2>/dev/null; do
sleep 2; elapsed=$((elapsed + 2)); echo -n "."
if [ "$elapsed" -ge "$timeout" ]; then echo -e " ${RED}TIMEOUT${NC}"; return 1; fi
done
echo -e " ${GREEN}OK${NC}"
}
# ══════════════════════════════════════════════════════════════════════════════
# WAN Interop
# ══════════════════════════════════════════════════════════════════════════════
setup_wan_interop() {
local n_local=$1
local n_remote=$2
local script_path
script_path="$(dirname "$0")/setup-wan-interop.sh"
if [ ! -f "$script_path" ]; then
echo -e "${RED}[WAN] network/setup-wan-interop.sh introuvable${NC}"
return 1
fi
chmod +x "$script_path"
bash "$script_path" "$WAN_LOCAL_IP" "$WAN_REMOTE_IP" "$n_local" "$n_remote"
}
# ══════════════════════════════════════════════════════════════════════════════
# WAN mesh (--wan)
# ══════════════════════════════════════════════════════════════════════════════
# Renseigne la table des noeuds : sans interaction si WAN_NODES est deja pose
# (option --wan-nodes ou variable d'environnement), par questions sinon.
wan_mesh_configure() {
local n_operators=$1
WAN_OPS="$n_operators"
# Le point code WAN vaut 1.<noeud><op>.<role> : le champ central du format
# ITU 3-8-3 tient sur 8 bits, soit 255. Avec 12 operateurs, le noeud 9
# donnerait "912" - un point code invalide, refuse par osmo-stp au
# demarrage avec une erreur de parsing qui ne dit rien du nombre
# d'operateurs. On arrete ici, en nommant la cause.
if [ "$n_operators" -gt 9 ]; then
echo -e "${RED}[WAN] ${n_operators} operateurs : le plan de point codes WAN${NC}" >&2
echo -e "${RED} (1.<noeud><op>.<role>) n'en admet que 9 par noeud.${NC}" >&2
echo -e "${CYAN} Repartissez-les sur plusieurs noeuds, ou lancez sans --wan.${NC}" >&2
exit 1
fi
# --virtualbox : le segment, les VM et la table sont fabriques AVANT tout le
# reste, puis relus comme n'importe quelle table. Le WAN qui suit ne sait
# pas - et n'a pas a savoir - que ses pairs sont des VM.
if [ "${VBOX_INTERCO:-0}" = "1" ] && [ -z "${WAN_NODES:-}" ]; then
local _vb=("$(dirname "$0")/network/setup-vbox-interco.sh"
--host-node "$VBOX_HOST_NODE" --conf "$WAN_CONF_FILE")
[ -n "${VBOX_NODES:-}" ] && _vb+=(--nodes "$VBOX_NODES")
bash "${_vb[@]}" || { echo -e "${RED}[WAN] interconnexion VirtualBox echouee${NC}" >&2; exit 1; }
wan_nodes_load "$WAN_CONF_FILE" || exit 1
WAN_NODES="$(wan_nodes_spec)"
WAN_NODE_ID="$VBOX_HOST_NODE"
fi
wan_menu_table
if [ -n "${WAN_NODES:-}" ]; then
wan_nodes_parse "$WAN_NODES" || exit 1
if [ "${WAN_NODE_ID:-0}" = "0" ]; then
# La detection par IP echoue par construction quand les premiers
# noeuds sont des CONTENEURS : leurs adresses (172.20.0.11, .12)
# appartiennent au backbone docker, pas a l'hote. L'hote n'est donc
# dans la table sous aucune de ses adresses - et il n'a pas a y
# etre : il porte des noeuds, il n'en est pas un.
#
# Son numero est celui de son PREMIER conteneur, c'est-a-dire 1
# dans le modele "conteneur i = noeud i". On ne s'arrete donc plus :
# on le pose, et on ne demande --wan-id que si la table ne contient
# meme pas ce noeud-la.
if wan_nodes_detect_self 2>/dev/null; then
:
elif [ -n "${WAN_IP[1]:-}" ]; then
WAN_NODE_ID=1
echo -e " ${CYAN}[WAN] cette machine porte les conteneurs : noeud de base 1${NC}"
else
echo -e "${RED}[WAN] table vide ou sans noeud 1 : passez --wan-id N${NC}" >&2
exit 1
fi
fi
wan_nodes_validate || exit 1
else
# Une table deja posee sur cette machine sert de valeurs par defaut :
# relancer le lab ne redemande donc pas tout, il propose l'existant.
wan_nodes_load "$WAN_CONF_FILE" 2>/dev/null || true
wan_nodes_prompt || exit 1
fi
# ── Ce que la table WAN ne dit pas, et qu'il faut pourtant savoir ───────
# Elle donne les noeuds et leurs adresses. Elle ne dit ni ou est le hub SS7,
# ni si les conteneurs de CETTE machine sont ses operateurs ou des noeuds a
# part entiere. Sans ces reponses, --wan partait sur ses defauts - hub
# local, conteneurs = operateurs - et sur un banc ou le hub est une VM
# dediee, cela donnait un second hub inutile et des ASP attaches au mauvais.
wan_menu_complements
WAN_ENABLED="false" # exclusif du WAN legacy
WAN_LOCAL_IP="$(wan_local_ip)"
echo ""
wan_nodes_summary
if [ -n "${WAN_HUB_IP:-}" ]; then
echo -e " inter-STP : ${CYAN}${WAN_HUB_IP}${NC} (hors table : ce n'est pas un noeud)"
else
echo -e " inter-STP : porte par le noeud ${CYAN}${WAN_HUB_NODE}${NC}"
fi
[ "${WAN_NODE_PER_OP:-0}" = "1" ] \
&& echo -e " conteneurs: ${CYAN}un noeud chacun${NC}, a partir du noeud ${CYAN}${WAN_NODE_ID}${NC}"
# Qui tourne ICI, et qui est seulement DECLARE. La distinction n'a rien de
# cosmetique : un noeud declare doit deja tourner ailleurs, sinon ses
# abonnes figurent dans les tables SMS et le dialplan sans que personne ne
# reponde - un appel qui sonne dans le vide se diagnostique mal.
for _nid in ${WAN_NODE_LIST[@]+"${WAN_NODE_LIST[@]}"}; do
if [ "$_nid" = "${WAN_NODE_ID:-0}" ] || \
{ [ "${WAN_NODE_PER_OP:-0}" = "1" ] && [ "$_nid" -gt "${WAN_NODE_ID:-0}" ]; }; then
printf ' noeud %-2s %-16s %s\n' "$_nid" "${WAN_IP[$_nid]}" "conteneur lance ici"
else
printf ' noeud %-2s %-16s %s\n' "$_nid" "${WAN_IP[$_nid]}" "declare (doit tourner ailleurs)"
fi
done
echo ""
}
# ── La table WAN, batie dans l'ordre ou on la pense ─────────────────────────
# D'abord COMBIEN d'operateurs en tout, puis combien tournent ICI en conteneur,
# puis les autres un par un. Les conteneurs prennent les premiers rangs -
# osmo-operator-1 est le noeud 1, -2 le noeud 2 - de sorte que le nom du
# conteneur donne son numero de noeud, sans table a consulter. Les operateurs
# distants (une VM deja demarree) prennent les rangs suivants : on ne demande
# que ce qu'on ne peut pas deduire, leur adresse et leur indicatif.
wan_menu_table() {
command -v whiptail >/dev/null 2>&1 || return 0
[ -t 0 ] || return 0
[ -n "${WAN_NODES:-}" ] && return 0 # deja donne en CLI
[ "${#OPERATOR_DECLS[@]}" -gt 0 ] && return 0 # deja declare par --operator
local total cont k ip ind spec=""
total=$(wt_input "WAN" "Nombre d'operateurs du WAN, tous sites confondus (1-9) :" "3") || return 0
[[ "$total" =~ ^[1-9]$ ]] || { wt_msg "Nombre invalide : $total"; return 0; }
cont=$(wt_input "WAN" "Combien tournent ICI, en conteneur ? (0-${total})" "$(( total > 1 ? total - 1 : total ))") || return 0
[[ "$cont" =~ ^[0-9]$ ]] && [ "$cont" -le "$total" ] \
|| { wt_msg "Entre 0 et ${total}."; return 0; }
# Les conteneurs : adresse du backbone, indicatif du rang. Rien a demander.
for k in $(seq 1 "$cont"); do
spec="${spec}${spec:+ }${k}:$(op_backbone_ip "$k"):$(( k * 11 ))"
done
# Les autres : eux seuls ont besoin d'une reponse.
for k in $(seq $(( cont + 1 )) "$total"); do
# Defaut : la VM du banc, seul noeud distant du montage courant.
_defip=""; [ "$k" = 3 ] && _defip="192.168.1.2"
ip=$(wt_input "Operateur distant ${k}/${total}" \
"Adresse du noeud ${k} (une VM, une autre machine) :" "$_defip") || return 0
[ -n "$ip" ] || { wt_msg "Adresse vide : noeud ${k} ignore."; continue; }
ind=$(wt_input "Operateur distant ${k}/${total}" \
"Indicatif (prefixe d'appel) du noeud ${k} :" "$(( k * 11 ))") || return 0
spec="${spec}${spec:+ }${k}:${ip}:${ind:-$(( k * 11 ))}"
done
WAN_NODES="$spec"
OPERATOR_COUNT_HINT="$cont"
WAN_NODE_ID="${WAN_NODE_ID:-1}"
[ "$cont" -ge 1 ] && WAN_NODE_PER_OP=1 && WAN_NODE_PER_OP_GIVEN=1
echo -e " ${CYAN}Table WAN :${NC} $WAN_NODES"
}
# ── Les questions WAN que la table ne porte pas ──────────────────────────────
# Chacune se saute des que l'option correspondante a ete donnee en ligne de
# commande : une invocation scriptee reste muette, une invocation a la main est
# guidee. Sans terminal (CI, cron), on garde les defauts plutot que d'attendre
# une reponse que personne ne donnera.
wan_menu_complements() {
command -v whiptail >/dev/null 2>&1 || return 0
[ -t 0 ] || return 0
if [ -z "${WAN_HUB_IP:-}" ] && [ "${WAN_HUB_NODE_GIVEN:-0}" != "1" ]; then
local _c
_c=$(wt_menu "Inter-STP" "Ou se trouve le hub SS7 (PC 0.0.0) ?" \
"ip" "sur une machine dediee, hors table (une VM par exemple)" \
"ici" "sur CETTE machine - un conteneur inter-STP sera lance" \
"noeud" "sur un autre noeud de la table WAN") || return 0
case "$_c" in
ip)
local _ip
_ip=$(wt_input "Inter-STP" "Adresse du hub (ex: 192.168.1.49) :" "${WAN_HUB_IP:-}") || return 0
if [ -n "$_ip" ]; then WAN_HUB_IP="$_ip"
else wt_msg "Adresse vide : le hub restera local."; fi ;;
noeud)
local _n
_n=$(wt_input "Inter-STP" "Numero du noeud qui porte le hub :" "${WAN_HUB_NODE:-1}") || return 0
[ -n "$_n" ] && WAN_HUB_NODE="$_n" ;;
*) WAN_HUB_NODE="$WAN_NODE_ID" ;;
esac
fi
if [ "${WAN_NODE_PER_OP_GIVEN:-0}" != "1" ]; then
if wt_yesno "Conteneurs" \
"Chaque conteneur doit-il etre un NOEUD a part entiere ?
Oui : conteneur 1 -> noeud ${WAN_NODE_ID}, conteneur 2 -> noeud $((WAN_NODE_ID + 1))...
(leurs adresses backbone doivent etre joignables des autres noeuds)
Non : ce sont les operateurs du noeud ${WAN_NODE_ID}."; then
WAN_NODE_PER_OP=1
fi
fi
}
# Applique la table (Asterisk, iptables, SMS) une fois les containers debout.
wan_mesh_apply() {
local n_operators=$1
local script_path="$(dirname "$0")/network/setup-wan-mesh.sh"
if [ ! -f "$script_path" ]; then
echo -e "${RED}[WAN] network/setup-wan-mesh.sh introuvable${NC}"; return 1
fi
# ── Le 4e champ de la table : combien d'operateurs porte chaque noeud ────
# WAN_NOPS n'avait AUCUN producteur dans le depot : la table sortait
# toujours a trois champs, setup-wan-mesh.sh se repliait sur une supposition
# et fabriquait - ou supprimait - des trunks au jugee. On l'ecrit ici, ou
# l'information est certaine : avec --op-is-node chaque noeud EST un
# conteneur et porte donc un operateur ; sinon, notre noeud les porte tous.
# wan_nodes_spec n'ecrit le champ que lorsqu'il vaut autre chose que 1, donc
# une table de conteneurs garde exactement la forme que les autres lecteurs
# (build-iso.sh, set_stp_ip.sh, les diagnostics) lui connaissent.
local _k
if [ "${WAN_NODE_PER_OP:-0}" = "1" ]; then
for _k in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
WAN_NOPS[$(( ${WAN_NODE_ID:-1} + _k - 1 ))]=1
done
else
WAN_NOPS[${WAN_NODE_ID:-1}]="$n_operators"
fi
if [ "${WAN_NODE_PER_OP:-0}" = "1" ]; then
# UN SEUL passage, avec --op-is-node : le script associe alors
# l'operateur i au noeud i et recalcule SES pairs. Un passage par noeud,
# comme on le faisait, ecrivait dans TOUS les conteneurs a chaque fois :
# le dernier gagnait, l'un d'eux se routait vers lui-meme et l'autre
# n'avait aucun maillage.
bash "$script_path" --docker --op-is-node \
--nodes "$(wan_nodes_spec)" --id 1 --ops "$n_operators" \
|| echo -e "${RED}[WAN] setup-wan-mesh.sh a echoue${NC}"
else
bash "$script_path" --docker \
--nodes "$(wan_nodes_spec)" --id "$WAN_NODE_ID" --ops "$n_operators" \
|| echo -e "${RED}[WAN] setup-wan-mesh.sh a echoue${NC}"
fi
}
# ── Rendre au trafic sortant son adresse d'origine ───────────────────────────
# Docker masque tout ce qui quitte un bridge : vues du LAN, les associations des
# conteneurs semblent toutes venir de l'hote. Ce n'est PAS ce qui empeche un ASP
# de rejoindre son AS : l'inter-STP est en "accept-asp-connections
# dynamic-permitted" (helpers/create_interop.sh) et apparie par ROUTING CONTEXT,
# pas par adresse source - il n'a aucun bloc "match=", contrairement a ce qui
# etait ecrit ici. Preserver l'adresse reste utile pour autre chose : un
# diagnostic qui nomme l'operateur au lieu de montrer trois fois l'hote
# (checks/diag-interstp.sh), et un pare-feu qui peut distinguer les sources.
# network/setup-docker-lan-route.sh pose donc un RETURN en tete de POSTROUTING.
#
# POURQUOI LE REJOUER ICI
# Docker REINSERE ses MASQUERADE en tete a chaque reseau cree - et start.sh en
# cree un par operateur. Le RETURN se retrouve alors DERRIERE eux et ne sert
# plus a rien : "iptables -L" le montre toujours, et pourtant le hub revoit
# l'adresse de l'hote. On le remet donc en premiere position une fois TOUS les
# reseaux crees.
reassert_docker_lan_return() {
command -v iptables >/dev/null 2>&1 || return 0
[ "$(id -u)" -eq 0 ] || return 0
local lan_ip lan_net
lan_ip="${HOST_LAN_IP:-}"
[ -n "$lan_ip" ] || lan_ip="$(ip route show default 2>/dev/null | awk '/^default/{print $9; exit}')"
[ -n "$lan_ip" ] || lan_ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
[ -n "$lan_ip" ] || return 0
lan_net="${lan_ip%.*}.0/24"
# Si LAN et backbone se recouvrent, la regle porterait sur du trafic interne
# au bridge : on s'abstient plutot que de deviner.
[ "${lan_net%%/*}" = "${INTER_NET_SUBNET%%/*}" ] && return 0
# ── LA REGLE RETURN EST RETIREE, PAS POSEE ──────────────────────────────
# [2026-08-26] Elle preservait l'adresse source des conteneurs vers le LAN :
# l'inter-STP voyait 172.20.0.11 et 172.20.0.12 au lieu de l'hote. C'etait
# l'intention. Le resultat etait l'inverse : ASP_DOWN des deux cotes et
# "connect failed (-110)" en boucle.
#
# MESURE, plutot que raisonnement. Capture pendant une reconnexion forcee :
# wlp0s20f3 Out 172.20.0.12.2910 > 192.168.1.49.2908 [INIT]
# wlp0s20f3 Out 192.168.1.49.2908 > 172.20.0.12.2910 [INIT ACK]
# L'INIT part, le hub repond - et l'INIT ACK n'est JAMAIS recu en entree par
# l'hote (tcpdump -Q in : rien). Il est adresse a 172.20.0.12, une adresse
# que l'hote doit ROUTER, et le chemin ne la lui remet pas.
#
# Ce n'est PAS le drop de Docker 29 dans la table raw, contrairement a ce
# qu'on a cru : ses compteurs sont restes a 0 pendant toute la mesure.
# ip daddr 172.20.0.12 iifname != "br-..." counter packets 0 bytes 0 drop
#
# Sans la regle, le trafic ressort masque en 192.168.1.31 : le hub repond a
# l'adresse PROPRE de l'hote, qui la recoit, et conntrack rend le paquet au
# conteneur. Verifie : as-inter AS_ACTIVE sur les deux operateurs, en huit
# secondes.
#
# Et l'on ne perd rien : l'inter-STP ne distingue pas ses noeuds par leur
# adresse source. Il est en "accept-asp-connections dynamic-permitted"
# (helpers/create_interop.sh) et les apparie par ROUTING CONTEXT - 1150,
# 2150, 3150. Deux noeuds derriere la meme adresse gardent des contextes
# distincts, donc des AS distincts.
#
# On RETIRE donc la regle si un lancement precedent l'a laissee.
iptables -t nat -D POSTROUTING -s "$INTER_NET_SUBNET" -d "$lan_net" -j RETURN 2>/dev/null \
&& echo -e " ${CYAN}[WAN] regle RETURN retiree : les noeuds sortent masques (l'inter-STP les apparie par routing context)${NC}" \
|| true
# ── Le FORWARD, sans quoi le routage ne sert a rien ─────────────────────
# Docker pose un FORWARD par defaut a DROP : un paquet du LAN vers le
# backbone (ou l'inverse) est jete avant d'atteindre le conteneur, sans
# refus ni trace - cote VM, on ne voit qu'un silence. DOCKER-USER est
# consultee AVANT les regles de docker et il ne la reecrit pas.
# network/setup-docker-lan-route.sh pose ces deux memes regles, mais aucun
# chemin automatique ne l'appelle : elles n'existaient donc sur le banc que
# si on avait pense a lancer ce script a la main. On les pose ici, ou la
# fonction est deja rejouee apres chaque reseau cree.
iptables -N DOCKER-USER 2>/dev/null || true
local _spec
for _spec in "-s ${lan_net} -d ${INTER_NET_SUBNET}" "-s ${INTER_NET_SUBNET} -d ${lan_net}"; do
# shellcheck disable=SC2086
if iptables -C DOCKER-USER $_spec -j ACCEPT 2>/dev/null; then
continue
fi
# shellcheck disable=SC2086
if iptables -I DOCKER-USER 1 $_spec -j ACCEPT 2>/dev/null; then
echo -e " ${CYAN}[WAN] FORWARD autorise (${_spec})${NC}"
fi
done
# ── Les traductions DEJA etablies ───────────────────────────────────────
# La regle ne vaut que pour les nouvelles associations. Celles qui ont ete
# masquees avant elle gardent leur traduction tant qu'elles vivent : le hub
# continue de les voir sous l'adresse de l'hote, et l'on croit la regle
# inoperante alors qu'elle est bien la. On efface donc ces entrees - les
# piles se reconnectent, cette fois avec leur vraie adresse.
#
# SAUF quand docker 29 a pose son DROP. Sa table "ip raw" jette, en
# prerouting, tout paquet entrant vers le backbone qui n'arrive pas par le
# bridge : une association qui repart avec sa vraie adresse source ne recoit
# alors plus aucune reponse et reste en COOKIE_WAIT. Purger conntrack dans
# cet etat coupe des ASP qui ne tiennent QUE par le masquage, pour les faire
# renaitre muets - c'est exactement la panne qu'on croit corriger.
local _nft_drop=0
if command -v nft >/dev/null 2>&1; then
if nft list table ip raw 2>/dev/null \
| grep -F "${INTER_NET_SUBNET%.*}." | grep -q 'drop'; then
_nft_drop=1
fi
fi
if [ "$_nft_drop" = "1" ]; then
echo -e " ${YELLOW}[WAN] DROP nft de docker actif sur ${INTER_NET_SUBNET} :${NC}"
echo -e " ${YELLOW} purge conntrack SAUTEE (elle couperait les ASP attaches).${NC}"
echo -e " ${CYAN} Backbone en mode routed pour le lever : OSMO_DOCKER_ROUTED=1${NC}"
elif command -v conntrack >/dev/null 2>&1; then
# BORNEE a la destination LAN. Sans -d, la meme commande effacait aussi
# les flux 172.20.0.x <-> 172.20.0.x, c'est-a-dire tout le SS7
# intra-backbone : les ASP du hub local tombaient d'un seul coup, et le
# message parlait pourtant de "traductions heritees".
local _purged
_purged="$(conntrack -D -s "$INTER_NET_SUBNET" -d "$lan_net" 2>/dev/null | grep -c . || true)"
echo -e " ${CYAN}[WAN] ${_purged} traduction(s) heritee(s) effacee(s) vers ${lan_net}${NC}"
else
# Le cas de l'hote du banc. Le bloc etait saute sans un mot : on croyait
# les traductions effacees alors qu'elles vivaient encore, et le hub
# continuait de voir l'adresse de l'hote sans que rien ne l'explique.
echo -e " ${YELLOW}[WAN] conntrack absent : les associations deja masquees gardent${NC}"
echo -e " ${YELLOW} leur traduction jusqu'a leur reconnexion (apt install conntrack)${NC}"
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# PoC QEMU - entree par defaut du premier menu
# ══════════════════════════════════════════════════════════════════════════════
start_qemu_poc() {
echo -e "${GREEN}PoC QEMU Calypso - bring-up minimal + qosmo-grgsm/run.sh...${NC}"
QEMU_POC=1
start_bridge_mode
}
# ══════════════════════════════════════════════════════════════════════════════
# Mode virtual (multi-operateurs SS7 - ex-bridge)
# ══════════════════════════════════════════════════════════════════════════════
start_bridge_mode() {
# Ce mode CREE les conteneurs : le segment prive se derive de l operateur,
# pas du noeud (voir _osmo_priv_index).
export OSMO_PRIV_BY_OP=1
local n_operators
declare -A OP_MCC OP_MNC OP_NAME OP_MS
if [ "${QEMU_POC:-0}" = "1" ]; then
# ── PoC QEMU ──────────────────────────────────────────────────────────
n_operators=1
# Meme base que les boucles : sans ca, un OP_ID_BASE=2 ferait chercher
# OP_MCC[2] alors que tout est pose en [1] - tableaux vides et
# operateur sans MCC, sans que rien ne le signale.
_b="${OP_ID_BASE:-1}"
OP_MCC[$_b]="001"; OP_MNC[$_b]="01"; OP_NAME[$_b]="OsmoQEMU"; OP_MS[$_b]=2
# PoC a un seul operateur : le PLMN de reference, celui des IMSI du
# depot. On ne le derive pas du noeud, il n'y a pas de WAN ici.
WAN_ENABLED="false"
# --wan reste valable en PoC QEMU : un noeud = un operateur, c'est
# exactement la maquette "N machines qui s'appellent".
[ "${WAN_MESH:-0}" = "1" ] && wan_mesh_configure 1
# BRIDGE_NO_PROCESS=0 : la radio et le mobile demarrent avec le coeur
# (voir le commentaire "NO-ATTACH, PLUS NO-PROCESS" plus bas).
PHY_MODE="faketrx"; BRIDGE_NO_PROCESS="${BRIDGE_NO_PROCESS:-0}"; BRIDGE_QEMU=1
ENCRYPTION="a5 0"
echo -e " ${CYAN}[PoC QEMU] 1 operateur · no-process + qosmo-grgsm/run.sh · A5/1${NC}"
else
# ── Mode interactif ──────────────────────────────────────────────────
# Deja repondu dans le menu WAN (« combien tournent ICI ») : on ne
# repose pas la question, deux reponses divergentes donneraient une
# table et un nombre de conteneurs qui ne se correspondent plus.
if [ -n "${OPERATOR_COUNT_HINT:-}" ]; then
n_operators="$OPERATOR_COUNT_HINT"
echo -e " ${CYAN}Conteneurs a lancer : ${n_operators} (choisis dans le menu WAN)${NC}"
else
# 3 par defaut : c'est la taille du banc (deux conteneurs et une
# VM). Un defaut qui correspond au montage courant evite la moitie
# des saisies, et surtout evite qu'un oubli produise une table plus
# courte que la realite - un noeud absent de la table n'existe pour
# personne, et son absence ne se voit qu'a l'appel qui n'aboutit pas.
n_operators=$(wt_input "Operateurs" "Nombre d'operateurs (1-36) :" "3") || exit 1
fi
n_operators=${n_operators:-2}
if ! [[ "$n_operators" =~ ^[0-9]+$ ]] || [ "$n_operators" -lt 1 ] || [ "$n_operators" -gt 36 ]; then
wt_msg "Nombre invalide (1-36)."; exit 1
fi
# ── LE PLAN DE NOEUDS SE DECIDE AVANT LE PLMN ────────────────────────
# Ce bloc etait 50 lignes plus bas, APRES le menu des operateurs. Or
# c'est lui qui repond a "chaque conteneur est-il un noeud a part
# entiere ?", donc lui qui fixe WAN_NODE_PER_OP - et le MCC vaut le
# numero de NOEUD. Le menu calculait donc les PLMN alors que la reponse
# n'etait pas encore connue : tout le monde recevait le MCC du noeud 1.
# Constate sur le banc - "Operateur 2 : MCC=001 MNC=02" annonce juste au-
# dessus de "STP PC 1.21.2", c'est-a-dire noeud 2. Deux plans dans le
# meme ecran, le point code juste et le PLMN faux.
# Les questions de WAN passent donc devant. Elles ne dependent que du
# nombre d'operateurs, connu ci-dessus.
# ── WAN ──────────────────────────────────────────────────────────────
if [ "${WAN_MESH:-0}" = "1" ]; then
wan_mesh_configure "$n_operators"
elif wt_yesno "WAN Interop" "Activer WAN vers un serveur distant (2 serveurs, prefixe 66) ?"; then
WAN_ENABLED="true"
local auto_ip=""
auto_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1) || true
[ -z "$auto_ip" ] && auto_ip=$(hostname -I 2>/dev/null | awk '{print $1}') || true
local wan_local wan_remote wan_nremote wan_pfx
wan_local=$(wt_input "WAN" "IP publique locale :" "$auto_ip") || exit 1
WAN_LOCAL_IP="${wan_local:-$auto_ip}"
[ -z "$WAN_LOCAL_IP" ] && WAN_ENABLED="false"
if [ "$WAN_ENABLED" = "true" ]; then
wan_remote=$(wt_input "WAN" "IP publique distante :" "") || exit 1
WAN_REMOTE_IP="$wan_remote"
[ -z "$WAN_REMOTE_IP" ] && WAN_ENABLED="false"
fi
if [ "$WAN_ENABLED" = "true" ]; then
wan_nremote=$(wt_input "WAN" "Nb operateurs distants :" "$n_operators") || exit 1
WAN_N_REMOTE="${wan_nremote:-$n_operators}"
wan_pfx=$(wt_input "WAN" "Prefixe WAN :" "$WAN_PREFIX") || exit 1
WAN_PREFIX="${wan_pfx:-$WAN_PREFIX}"
echo -e " ${GREEN}WAN: ${WAN_LOCAL_IP} ↔ ${WAN_REMOTE_IP} (local=${n_operators} remote=${WAN_N_REMOTE} prefix=${WAN_PREFIX})${NC}"
fi
fi
local use_defaults="N"
wt_yesno "Valeurs par defaut" "Valeurs par defaut pour tous (MCC=001/002/... MNC=01/02/...) ?" && use_defaults="O"
local same_ms_all="N"
if [ "$use_defaults" != "O" ]; then
wt_yesno "MS" "Meme nombre de MS pour tous les operateurs ?" && same_ms_all="O"
fi
declare -A OP_MCC OP_MNC OP_NAME OP_MS
if [ "$use_defaults" = "O" ]; then
local common_ms
common_ms=$(wt_input "MS" "MS par operateur (1-64) :" "8") || exit 1
common_ms=${common_ms:-8}
if ! [[ "$common_ms" =~ ^[0-9]+$ ]] || [ "$common_ms" -lt 1 ] || [ "$common_ms" -gt 64 ]; then common_ms=8; fi
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
# Un PLMN par operateur, sur les DEUX composantes. Le MCC
# etait fige a 001 pour tout le monde : seul le MNC separait
# les reseaux, et la moindre valeur ecrite en dur ailleurs
# (les IMSI des MS l'etaient) retombait sur un PLMN plausible
# mais etranger. 001/002/003 rend la confusion visible.
OP_MCC[$i]=$(op_mcc "$i"); OP_MNC[$i]=$(op_mnc "$i")
OP_NAME[$i]="$(op_default_name "$i")"; OP_MS[$i]=$common_ms
done
else
if [ "$same_ms_all" = "O" ]; then
local common_ms
common_ms=$(wt_input "MS" "MS par operateur (1-64) :" "8") || exit 1
common_ms=${common_ms:-8}
if ! [[ "$common_ms" =~ ^[0-9]+$ ]] || [ "$common_ms" -lt 1 ] || [ "$common_ms" -gt 64 ]; then common_ms=8; fi
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
local mcc mnc name dmcc dmnc dname
dmcc=$(op_mcc "$i"); dmnc=$(op_mnc "$i"); dname="$(op_default_name "$i")"
mcc=$(wt_input "Operateur ${i}" "MCC (pays = noeud $(op_plmn_node "$i"), $(op_country "$i")) :" "$dmcc") || exit 1; OP_MCC[$i]=${mcc:-$dmcc}
mnc=$(wt_input "Operateur ${i}" "MNC :" "$dmnc") || exit 1; OP_MNC[$i]=${mnc:-$dmnc}
name=$(wt_input "Operateur ${i}" "Nom :" "$dname") || exit 1; OP_NAME[$i]=${name:-$dname}
OP_MS[$i]=$common_ms
done
else
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
local mcc mnc name n_ms dmcc dmnc dname
dmcc=$(op_mcc "$i"); dmnc=$(op_mnc "$i"); dname="$(op_default_name "$i")"
mcc=$(wt_input "Operateur ${i}" "MCC (pays = noeud $(op_plmn_node "$i"), $(op_country "$i")) :" "$dmcc") || exit 1; OP_MCC[$i]=${mcc:-$dmcc}
mnc=$(wt_input "Operateur ${i}" "MNC :" "$dmnc") || exit 1; OP_MNC[$i]=${mnc:-$dmnc}
name=$(wt_input "Operateur ${i}" "Nom :" "$dname") || exit 1; OP_NAME[$i]=${name:-$dname}
# 2 par defaut, pas 1 : c est le minimum pour une radio
# MIXTE - un faketrx et un QEMU Calypso, comme le natif.
# A 1, l operateur n avait qu un seul MS et le mode hybride
# n avait plus rien a repartir.
n_ms=$(wt_input "Operateur ${i}" "Nombre de MS (1-64) :" "2") || exit 1; OP_MS[$i]=${n_ms:-2}
if ! [[ "${OP_MS[$i]}" =~ ^[0-9]+$ ]] || [ "${OP_MS[$i]}" -lt 1 ] || [ "${OP_MS[$i]}" -gt 64 ]; then OP_MS[$i]=1; fi
done
fi
fi
# ── RAN / Encryption ─────────────────────────────────────────────────
# BRIDGE_NO_PROCESS=0 : la radio et le mobile demarrent avec le coeur
# (voir le commentaire "NO-ATTACH, PLUS NO-PROCESS" plus bas).
PHY_MODE="faketrx"; BRIDGE_NO_PROCESS="${BRIDGE_NO_PROCESS:-0}"; BRIDGE_QEMU=1
ENCRYPTION="${ENCRYPTION:-a5 0}"
if [ -n "$HANDOFF_MODE_FROM_ENV" ]; then
# [2026-08-31] L APPELANT A IMPOSE LE MODE : on ne lui repose pas la
# question. Sans cette branche, seul --wan pouvait choisir l hybride
# et tout autre lancement retombait sur choose_ran_host - un menu.
# En non interactif ce menu s auto-repondait par sa PREMIERE entree,
# donc "full-grgsm" : un HANDOFF_MODE=faketrx-qemu passe en
# environnement etait lu (HANDOFF_MODE_FROM_ENV=1, l.137) puis
# ignore ici. On ne pouvait pas obtenir la radio mixte hors WAN.
echo -e " ${GREEN}[RAN] mode impose par l appelant : ${CYAN}${HANDOFF_MODE}${NC}"
elif [ "${WAN_MESH:-0}" = "1" ]; then
# Un noeud de WAN est une maquette complete : il lui faut une radio
# des DEUX types pour que l'appel inter-noeud soit vraiment porte de
# bout en bout. D'ou l'hybride par defaut - un faketrx + un QEMU
# Calypso - au lieu du menu. HANDOFF_MODE=... en prefixe le change.
HANDOFF_MODE="faketrx-qemu"; HANDOFF_QEMU_CHOICE="faketrx-qemu"
echo -e " ${GREEN}[RAN] WAN → hybride par defaut : ${CYAN}1 faketrx + 1 QEMU Calypso${NC}"
else
choose_ran_host
fi
fi
# ── Detection IP hote ────────────────────────────────────────────────────
HOST_IP=$(ip route get 1.1.1.1 2>/dev/null \
| awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1) || true
if [ -z "$HOST_IP" ]; then
HOST_IP=$(hostname -I 2>/dev/null | awk '{print $1}') || true
fi
HOST_IP="${HOST_IP:-127.0.0.1}"
REAL_UID=$(id -u "${SUDO_USER:-$(logname 2>/dev/null || echo root)}")
echo -e "${GREEN}IP hote : ${CYAN}${HOST_IP}${NC} (Linphone)${NC}"
echo ""
# ── Ou joindre le hub SS7, vu d'ici ──────────────────────────────────────
# Le hub est UNIQUE pour tout le WAN. Deux cas, et le mauvais donne un ASP
# qui ne s'attache jamais :
# ce noeud heberge le hub → les conteneurs le joignent en interne (172.20.0.10)
# un autre noeud l'heberge → ils sortent sur le WAN, vers SON adresse
WAN_STP_TARGET="$INTER_STP_IP"
HUB_IS_LOCAL=1
if [ -n "${WAN_HUB_IP:-}" ]; then
# Hub designe par son adresse : il n'est pas un noeud, et rien ne se
# lance ici. C'est le cas d'une VM inter-STP dediee.
WAN_STP_TARGET="$WAN_HUB_IP"; HUB_IS_LOCAL=0
elif [ "${WAN_MESH:-0}" = "1" ] && [ "${WAN_NODE_ID}" != "${WAN_HUB_NODE}" ]; then
WAN_STP_TARGET="${WAN_IP[$WAN_HUB_NODE]:-}"; HUB_IS_LOCAL=0
if [ -z "$WAN_STP_TARGET" ]; then
echo -e "${RED}[SS7] le noeud ${WAN_HUB_NODE} (hub) n'est pas dans la table WAN${NC}" >&2
echo -e "${RED} (ou designez-le par son adresse : --hub-ip ADRESSE)${NC}" >&2
exit 1
fi
fi
# ── Reseau backbone ──────────────────────────────────────────────────────
# OSMO_DOCKER_ROUTED=1 : bridge en mode "routed" (docker >= 28). Docker
# cesse alors de masquer ce qui sort du backbone, et surtout n'installe pas
# dans sa table "ip raw" le DROP qui jette, en prerouting, tout paquet
# entrant vers 172.20.0.x n'arrivant pas par le bridge. Avec ce DROP, un
# conteneur joint depuis le LAN a sa VRAIE adresse ne recoit jamais de
# reponse : son association SCTP reste en COOKIE_WAIT, sans un mot dans les
# journaux. C'est le seul reglage qui leve les deux a la fois.
# Optionnel et non fatal : docker < 28 ne connait pas l'option et refuse la
# creation, on retombe alors en silence sur le reseau ordinaire (masque),
# celui avec lequel le banc a toujours tourne.
if docker network inspect "$INTER_NET" &>/dev/null; then
echo -e " ${CYAN}Reseau backbone ${INTER_NET} deja present${NC}"
else
if [ "${OSMO_DOCKER_ROUTED:-0}" = "1" ] \
&& docker network create --subnet="$INTER_NET_SUBNET" --gateway="$INTER_NET_GATEWAY" \
-o com.docker.network.bridge.gateway_mode_ipv4=routed "$INTER_NET" &>/dev/null; then
echo -e " ${GREEN}✓ Reseau backbone ${INTER_NET} (${INTER_NET_SUBNET}) cree - mode routed${NC}"
elif docker network create --subnet="$INTER_NET_SUBNET" --gateway="$INTER_NET_GATEWAY" "$INTER_NET" &>/dev/null; then
[ "${OSMO_DOCKER_ROUTED:-0}" = "1" ] \
&& echo -e " ${YELLOW} (mode routed refuse par ce docker - reseau ordinaire)${NC}"
echo -e " ${GREEN}✓ Reseau backbone ${INTER_NET} (${INTER_NET_SUBNET}) cree${NC}"
else
echo -e " ${RED}✗ Echec creation reseau backbone ${INTER_NET} (${INTER_NET_SUBNET})${NC}"; exit 1
fi
fi
# ── SMS Routing ──────────────────────────────────────────────────────────
SMS_ROUTING_DIR=$(mktemp -d)
if declare -f sms_routing_generate_all > /dev/null 2>&1; then
local _ms_counts=()
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do _ms_counts+=("${OP_MS[$i]}"); done
sms_routing_generate_all "$n_operators" "$SMS_ROUTING_DIR" "${_ms_counts[@]}"
sms_routing_summary "$n_operators" "${_ms_counts[@]}"
fi
# ── Inter-STP ────────────────────────────────────────────────────────────
if [ "$HUB_IS_LOCAL" = "1" ]; then
start_inter_stp "$n_operators"
wait_inter_stp_ready "$n_operators"
else
echo -e "${CYAN}[SS7] hub porte par le noeud ${WAN_HUB_NODE} (${WAN_STP_TARGET}) - pas de hub ici.${NC}"
echo -e "${CYAN} Les ASP de ce noeud s'y attacheront en M3UA/SCTP 2908.${NC}"
fi
# ── HLR subscribers ──────────────────────────────────────────────────────
local all_subscribers_file
all_subscribers_file=$(mktemp)
echo "# op_id:ms_idx:imsi:msisdn:ki" > "$all_subscribers_file"
# ── L'annuaire : un etat civil pour chaque IMSI ──────────────────────────
# Un banc ne rend que des numeros a 15 chiffres, et l'on finit par lire
# "001010001000002" comme un mot de passe : impossible de dire de tete a
# quel operateur, a quel pays, a quel telephone il appartient. Cette table
# met un nom et une adresse en face de chaque abonne. Elle est FABRIQUEE -
# personnages et rues n'existent pas - mais elle n'invente pas l'IMSI :
# c'est exactement celui qui part au HLR trois lignes plus bas, dans la
# meme boucle, a partir de la meme variable.
# Les fiches sont tirees dans data/noms.txt et data/adresses.txt, melanges
# au chargement. On les parcourt EN ORDRE a partir de la liste melangee
# plutot que de retirer au sort a chaque abonne : deux abonnes du meme banc
# ne peuvent alors pas porter le meme nom tant que le fichier n'est pas
# epuise - et deux homonymes dans une trace, c'est une heure perdue.
local ANNUAIRE_FILE="${OSMO_ANNUAIRE:-/var/tmp/osmo-annuaire.csv}"
# ── L'annuaire du SS7 : le meme service, un etage plus bas ──────────────
# L'annuaire d'abonnes met un nom sur un MSISDN. Celui-ci met un nom sur un
# POINT CODE. On lit "1.21.2" dans une trace M3UA sans savoir de quelle
# machine il s'agit, ni quel operateur elle porte, ni vers quel hub elle
# s'attache - et les trois se deduisent d'un plan qui n'est ecrit nulle
# part ailleurs que dans la tete de celui qui a lance le banc.
# Il est produit par la MEME boucle que les configurations : ce n'est pas
# une documentation a cote, c'est le releve de ce qui vient d'etre ecrit.
local ANNUAIRE_SS7="${OSMO_ANNUAIRE_SS7:-/var/tmp/osmo-annuaire-ss7.csv}"
printf 'noeud;operateur;pays;mcc;mnc;pc_msc;pc_stp;pc_bsc;rctx_msc;rctx_stp;rctx_bsc;rctx_inter;hub;as_inter;asp_inter;backbone;prive\n' \
> "$ANNUAIRE_SS7"
local _pick=0
printf 'imsi;msisdn;operateur;pays;mcc;mnc;nom;adresse;ki\n' > "$ANNUAIRE_FILE"
for op_id in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
local mcc="${OP_MCC[$op_id]}" mnc="${OP_MNC[$op_id]}" n_ms="${OP_MS[$op_id]}"
local _pays; _pays="$(op_country "$op_id")"
for ms_idx in $(seq 1 "$n_ms"); do
local msin; msin=$(printf '%04d%06d' "${op_id}" "${ms_idx}")
local imsi="${mcc}${mnc}${msin}"
# MSISDN a SIX chiffres : <noeud>00<operateur><rang>.
# L'ancien plan, op * 10000 + rang, commencait a 10001 - cinq
# chiffres dont le premier EST le numero d'operateur, ce qui melait
# le plan d'abonnes au plan de composition (les services du dialplan
# sont a 100, 200, 500, 600, 700) et obligeait chaque motif local a
# dependre de l'operateur. Le prefixe 600 qui l'a remplace etait
# commun a TOUTES les machines : deux noeuds y revendiquaient les
# memes numeros. Le premier chiffre porte donc le noeud, et le
# routage se lit dans le numero : 100101, 100102, 200101...
local msisdn; msisdn=$(osmo_msisdn "$(osmo_node_id)" "$op_id" "$ms_idx")
local ki; ki=$(printf '00112233445566778899aabbccdd%02x%02x' "${ms_idx}" "${op_id}")
echo "${op_id}:${ms_idx}:${imsi}:${msisdn}:${ki}" >> "$all_subscribers_file"
printf '%s;%s;%s;%s;%s;%s;%s;%s %s;%s\n' \
"$imsi" "$msisdn" "${OP_NAME[$op_id]}" "$_pays" "$mcc" "$mnc" \
"${POOL_NOMS[$(( _pick % ${#POOL_NOMS[@]} ))]}" \
"$(( RANDOM % 97 + 1 ))" \
"${POOL_ADR[$(( _pick % ${#POOL_ADR[@]} ))]}" \
"$ki" >> "$ANNUAIRE_FILE"
_pick=$(( _pick + 1 ))
done
done
local total_subs; total_subs=$(( $(wc -l < "$all_subscribers_file") - 1 ))
echo -e "${GREEN}Abonnes total : ${total_subs}${NC}"
# Aperçu : les premieres fiches, puis le chemin du fichier complet. Une
# table de 8 MS par operateur noierait le reste du demarrage.
awk -F';' 'NR==1 {next}
NR<=9 {printf " %-16s %-6s %-16s %-12s %-22s %s\n", $1,$2,$3,$4,$7,$8}
END {if (NR>9) printf " ... et %d autres\n", NR-9}' "$ANNUAIRE_FILE"
echo -e " ${CYAN}Annuaire complet : ${ANNUAIRE_FILE}${NC} (aussi /etc/osmocom/annuaire.csv dans chaque conteneur)"
echo ""
# ── Table rase sur la serie d'operateurs ────────────────────────────────
# Le "docker rm -f" plus bas ne vise que le conteneur de MEME NOM. Un
# reliquat d'un lancement precedent - trois operateurs hier, deux
# aujourd'hui, ou une serie interrompue en cours de route - garde ses
# publications de ports et fait echouer la creation du suivant :
# Bind for 0.0.0.0:5082 failed: port is already allocated
# Le message nomme le port, jamais le conteneur qui le tient : on cherche
# longtemps. Cette fonction RECREE toute la serie, on efface donc d'abord.
local _stale
_stale="$(docker ps -aq --filter "name=^osmo-operator-" 2>/dev/null)"
if [ -n "$_stale" ]; then
echo -e " ${CYAN}Nettoyage : $(echo "$_stale" | wc -l) conteneur(s) operateur d'un lancement precedent${NC}"
# shellcheck disable=SC2086
docker rm -f $_stale >/dev/null 2>&1 || true
fi
# ── Demarrage sequentiel des operateurs ──────────────────────────────────
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
local container_name net_name subnet gateway container_ip inter_local_ip rctx_inter
local n_groups last_group_ip
container_name=$(op_container "$i")
net_name="gsm-net-op${i}"
subnet=$(op_private_net "$i")
gateway=$(op_private_gw "$i")
container_ip=$(op_private_ip "$i")
inter_local_ip=$(op_backbone_ip "$i")
rctx_inter=$(op_rctx_inter "$i")
n_groups=$(( (${OP_MS[$i]} + 7) / 8 ))
last_group_ip="127.0.0.${n_groups}"
echo -e "${CYAN}── Operateur ${i} : ${OP_NAME[$i]} ($(op_country "$i"), MCC=${OP_MCC[$i]} MNC=${OP_MNC[$i]}) ──${NC}"
echo -e " Backbone : ${CYAN}${inter_local_ip}${NC} Prive : ${CYAN}${container_ip}${NC}"
local _node_i="$WAN_NODE_ID" _op_i="$i"
if [ "${WAN_NODE_PER_OP:-0}" = "1" ]; then
_node_i=$(( WAN_NODE_ID + i - 1 ))
_op_i=1
# Le numero de noeud vit dans 1..9 : au-dela, le point code ecrit
# ici (1.<noeud><op>.<role>) sort du format ITU 3-8-3, aucun AS
# correspondant n'existe au hub (helpers/create_interop.sh borne a
# 9 noeuds), et network/set-node-id.sh comme start-direct.sh
# refusent l'argument. Mieux vaut le dire ici, avant de fabriquer
# une identite SS7 que personne ne connaitra.
if [ "$_node_i" -gt 9 ] || [ "$_node_i" -lt 1 ]; then
echo -e "${RED}[WAN] --node-per-op : l'operateur ${i} tomberait sur le noeud ${_node_i}${NC}" >&2
echo -e "${RED} Les numeros de noeud vont de 1 a 9. Avec --wan-id ${WAN_NODE_ID},"' '"${NC}" >&2
echo -e "${RED} le maximum est $(( 10 - WAN_NODE_ID )) operateur(s) : baissez --wan-id,"' '"${NC}" >&2
echo -e "${RED} reduisez --operators, ou retirez --node-per-op.${NC}" >&2
exit 1
fi
fi
local _pc_mid="$i" _rctx_inter=""
if [ "${WAN_MESH:-0}" = "1" ]; then
_pc_mid="${_node_i}${_op_i}"
_rctx_inter=$(( _node_i * 1000 + _op_i * 100 + 50 ))
fi
# L'identite AFFICHEE est celle qui sera ECRITE. Cette ligne montrait
# "1.<op>.2" en dur et le routing context du plan local : elle annoncait
# donc 1.1.2/150 alors que la config recevait 1.21.2/2150. On cherche
# longtemps une panne quand le journal dit le contraire du fichier.
echo -e " STP PC : 1.${_pc_mid}.2 RCTX : ${_rctx_inter:-$rctx_inter} MS : ${CYAN}${OP_MS[$i]}${NC}"
# Le releve SS7 de CET operateur, avec les valeurs qui partent dans sa
# configuration deux lignes plus bas - pas une reconstitution.
printf '%s;%s;%s;%s;%s;1.%s.1;1.%s.2;1.%s.3;%s;%s;%s;%s;%s;as-n%sop%s;asp-n%sop%s;%s;%s\n' \
"$_node_i" "${OP_NAME[$i]}" "$(op_country "$i")" \
"${OP_MCC[$i]}" "${OP_MNC[$i]}" \
"$_pc_mid" "$_pc_mid" "$_pc_mid" \
"$(op_rctx_msc "$i")" "$(op_rctx_stp "$i")" "$(op_rctx_bsc "$i")" \
"${_rctx_inter:-$rctx_inter}" "${WAN_STP_TARGET:-127.0.0.1}" \
"$_node_i" "$_op_i" "$_node_i" "$_op_i" \
"$inter_local_ip" "$container_ip" >> "$ANNUAIRE_SS7"
if docker network inspect "$net_name" &>/dev/null; then
echo -e " Reseau : ${CYAN}${net_name}${NC} (deja present)"
elif docker network create --subnet="$subnet" --gateway="$gateway" "$net_name" &>/dev/null; then
echo -e " Reseau : ${GREEN}✓ ${net_name} (${subnet}) cree${NC}"
else
echo -e " ${RED}✗ Echec creation reseau ${net_name} (${subnet})${NC}"; exit 1
fi
# ── La regle de non-masquage, AVANT que la pile ne se connecte ───────
# Une decision de NAT se prend sur le PREMIER paquet d'une association :
# conntrack la conserve ensuite pour toute sa duree. Poser la regle en
# fin de lancement arrivait donc trop tard - l'ASP s'etait deja attache,
# masque, et le hub le voyait sous l'adresse de l'hote jusqu'a sa
# prochaine reconnexion. Deux operateurs y devenaient indiscernables.
#
# Ici, elle est reaffirmee apres CHAQUE reseau cree (docker reinsere ses
# MASQUERADE en tete a chacun) et donc avant tout demarrage de pile.
reassert_docker_lan_return >/dev/null
local tmpdir
tmpdir=$(mktemp -d)
# Plan de point codes. Hors WAN : 1.<op>.<role>, comme toujours. En WAN :
# le numero de noeud entre dedans, sans quoi deux machines presentent au
# hub des adresses SS7 identiques - routage faux, et silencieux.
# Seul RCTX_INTER est surcharge : c'est le seul routing context que le
# hub voit. Ceux du MSC et du BSC restent internes au conteneur.
#
# UN NOEUD PAR CONTENEUR (--node-per-op)
# Par defaut, la machine EST le noeud et ses conteneurs en sont les
# operateurs : 1.<noeud><op>.<role>. C'est juste quand une machine
# heberge un operateur complet.
#
# Mais des que les conteneurs sont joignables chacun a SON adresse
# (172.20.0.11, .12... routees depuis le LAN), ce ne sont plus des
# operateurs d'un meme site : ce sont des noeuds a part entiere, au meme
# titre qu'une VM. Les traiter en operateurs obligeait alors a inscrire
# la MACHINE dans la table WAN - une entree qui ne correspond a aucun
# equipement, et qui recevait pourtant un indicatif et des routes.
#
# Avec --node-per-op, le conteneur i devient le noeud (base + i - 1),
# porte l'operateur 1 de ce noeud, et son point code suit : 1.<n>1.<role>.
RCTX_INTER_OVERRIDE="$_rctx_inter" \
apply_config_templates "$tmpdir" \
"$container_ip" "$gateway" \
"$i" "1.${_pc_mid}.1" "1.${_pc_mid}.2" "1.${_pc_mid}.3" \
"${OP_MCC[$i]}" "${OP_MNC[$i]}" "${OP_NAME[$i]}" \
"$WAN_STP_TARGET" "no shutdown" \
"$n_operators"
# L'annuaire voyage avec la pile : c'est dans le conteneur qu'on lit
# un IMSI dans un journal, pas sur l'hote.
[ -f "$ANNUAIRE_FILE" ] && cp "$ANNUAIRE_FILE" "$tmpdir/osmocom/annuaire.csv"
[ -f "$ANNUAIRE_SS7" ] && cp "$ANNUAIRE_SS7" "$tmpdir/osmocom/annuaire-ss7.csv"
# ── Les fiches du dialplan ─────────────────────────────────────────
# Asterisk ne sait pas lire un CSV. On lui ecrit donc les memes fiches
# sous la forme qu'il comprend : une extension par MSISDN dans le
# contexte sub-annuaire (voir configs/extensions.conf). L'appel arrive
# alors sur le combine avec un nom, et non avec cinq chiffres.
{
printf '; annuaire.conf - genere par start.sh, ne pas editer a la main.\n'
printf '; Une fiche par abonne du banc. Le repli _X. est dans extensions.conf.\n'
printf '[sub-annuaire]\n'
awk -F';' 'NR>1 && $2 != "" {
gsub(/[^A-Za-z0-9 ._-]/, "", $7); gsub(/[^A-Za-z0-9 ._-]/, "", $3)
gsub(/[^A-Za-z0-9 ._-]/, "", $4); gsub(/[^A-Za-z0-9 .,_-]/, "", $8)
printf "exten => %s,1,Set(CALLERID(name)=%s)\n", $2, $7
printf " same => n,Set(ANNU_OP=%s)\n", $3
printf " same => n,Set(ANNU_PAYS=%s)\n", $4
printf " same => n,Set(ANNU_ADR=%s)\n", $8
printf " same => n,Set(ANNU_IMSI=%s)\n", $1
printf " same => n,Set(CDR(userfield)=%s / %s / %s)\n", $7, $3, $4
printf " same => n,NoOp(=== ANNUAIRE %s = %s, %s (%s) ===)\n", $2, $7, $3, $4
printf " same => n,Return()\n"
}' "$ANNUAIRE_FILE"
} > "$tmpdir/asterisk/annuaire.conf" 2>/dev/null || true
local vol_args alsa_args
vol_args=$(build_vol_args "$tmpdir")
alsa_args=$(build_alsa_args)
# [2026-08-12] Le chown est OBLIGATOIRE, pas cosmetique. Ce repertoire est
# monte sur /var/log/osmocom et les demons tournent en osmocom (999:1000
# dans l'image). Cree en root:root, osmo-hlr ne peut pas creer son fichier
# de log → "% Unable to create file '/var/log/osmocom/osmo-hlr.log'" →
# echec du parse de sa config → boucle de restart systemd → osmo-start.sh
# `exit 1` ("HLR indispensable") → run.sh meurt (set -e) et n'atteint
# aucun de ses etages suivants. Un simple probleme de droits fait donc
# tomber tout le pipeline, audio compris.
mkdir -p /tmp/osmocom-logs/op${i}
chown 999:1000 /tmp/osmocom-logs/op${i} 2>/dev/null || true
# ── Ports a exposer ──────────────────────────────────────────────────
local port_args=""
# ── SIP/RTP : PLUS AUCUNE PUBLICATION SUR L HOTE ─────────────────
# [2026-08-31] On publiait le 5060 et la plage RTP de chaque conteneur
# sur l hote. C etait la SEULE source du conflit de ports : l operateur
# natif tient 5060/udp (Asterisk) et 5061 (osmo-sip-connector) en
# permanence, puisqu il tourne EN MEME TEMPS que les conteneurs.
#
# On a d abord decale les publications (+100). Ca marchait, mais ca
# reposait sur une sonde "5060 est-il pris ?" prise a un instant t -
# donc sur une course : lancee juste apres l arret du banc, la sonde
# voyait le port libre, ne decalait rien, et le conteneur mourait sur
# failed to bind host port 0.0.0.0:5061/udp: address already in use
# en restant en etat "Created", des que le natif remontait.
#
# La publication etait de toute facon INUTILE : les conteneurs sont sur
# un bridge routable depuis l hote. On les joint en direct, a leur
# propre adresse, sur le port nominal. Supprimer la publication
# supprime le conflit, l offset ET la course, d un coup.
local _cip; _cip="$(op_backbone_ip "$i")"
echo -e " Linphone : ${CYAN}${_cip}:5060${NC} RTP 30000-30199 (direct, sans publication)"
# WAN en plus si active (legacy ou mesh)
if wan_active; then
local sip_port rtp_start rtp_end sms_port
sip_port=$(wan_sip_port "$i")
rtp_start=$(wan_rtp_start "$i")
rtp_end=$(wan_rtp_end "$i")
port_args="${port_args} -p ${sip_port}:5060/udp -p ${sip_port}:5060/tcp -p ${rtp_start}-${rtp_end}:${rtp_start}-${rtp_end}/udp"
echo -e " WAN : SIP ${sip_port} RTP ${rtp_start}-${rtp_end}"
if [ "${WAN_MESH:-0}" = "1" ]; then
# Un port SMS PAR operateur : le relais distant doit joindre le
# container du BON operateur, or son injection MT passe par le
# HLR local a son container. Un port unique livrerait tous les
# SMS au premier operateur, qui ne connait pas les abonnes des
# autres - "MSISDN not found in HLR", sans autre trace.
sms_port=$(wan_sms_port "$i")
port_args="${port_args} -p ${sms_port}:7890/tcp"
echo -e " WAN SMS : ${sms_port} → relay 7890"
fi
fi
docker rm -f "$container_name" 2>/dev/null || true
ensure_host_audio_relay
# --restart, PAS --rm : au reboot, les conteneurs operateurs ne se
# retrouvaient pas arretes mais DISPARUS - avec leur base HLR alimentee
# et tout ce qui n'est pas monte depuis l'hote. Le banc etait a remonter
# entierement apres une simple coupure. Le "docker rm -f" juste au-dessus
# rend --rm inutile, et docker refuse --rm avec --restart au parsing.
#
# OSMO_ROLE=operator : le conteneur regenere ses configs quand run.sh
# rejoue apply_config_templates, et le rattrapage d'identite SS7 de
# generate_configs.sh s'arrete net sur un role vide - sans rien
# reappliquer et sans rien dire. Le conteneur repartait alors sur le
# point code du gabarit, 1.1.2 pour TOUS, donc deux equipements a la
# meme adresse SS7 et aucun ASP attache.
# shellcheck disable=SC2086
docker run -d \
--restart unless-stopped \
--name "$container_name" \
--hostname "$container_name" \
--network "$INTER_NET" \
--ip "$inter_local_ip" \
--cap-add NET_ADMIN \
--cap-add SYS_ADMIN \
--cap-add NET_RAW \
--ulimit rtprio=18 \
--shm-size=8g \
--cgroupns host \
--device /dev/net/tun:/dev/net/tun \
$alsa_args \
$port_args \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
-v /tmp/osmocom-logs/op${i}:/var/log/osmocom \
--tmpfs /tmp:exec,rw,mode=1777,size=2g \
--tmpfs /run:exec,size=64m \
--tmpfs /run/lock \
-e OPERATOR_ID="$i" \
-e OSMO_ROLE=operator \
-e N_MS="${OP_MS[$i]}" \
-e CONTAINER_IP="$container_ip" \
-e GATEWAY_IP="$gateway" \
-e INTER_STP_IP="${WAN_STP_TARGET:-$INTER_STP_IP}" \
-e OSMO_HUB_IP="${WAN_STP_TARGET:-$INTER_STP_IP}" \
-e WAN_NODE_ID="${_node_i}" \
-e OSMO_WAN_NODE="${_node_i}" \
-e HOST_IP="${HOST_IP}" \
-e SIP_HOST_PORT="5060" \
-e PHY_MODE="${PHY_MODE}" \
-e HOST_AUDIO_RELAY="${HOST_AUDIO_RELAY}" \
$vol_args \
"$IMAGE_RUN" \
sleep infinity
docker network connect --ip "$container_ip" "$net_name" "$container_name"
force_update_trees "$container_name"
# ── LE CONTENEUR S'ARRETE AU COEUR ──────────────────────────────────
# [2026-08-31] NO-ATTACH, PLUS NO-PROCESS.
# no-process arretait run.sh au coeur : STP, HLR, MSC, BSC, MGW, GGSN,
# SGSN, PCU montaient, puis plus rien - ni PHY, ni mobile, ni Asterisk,
# ni SMSC. Il fallait lancer start-direct.sh A LA MAIN derriere, et en
# attendant le conteneur affichait
# [no-process] mobile non lance - attente 4247 sautee
# c est-a-dire un operateur sans radio ni abonne joignable, alors que
# tout paraissait "pret".
#
# La pile complete demarre donc maintenant. Ce n est PAS un retour a
# l attachement bloquant : run.sh ne s attache jamais de lui-meme - il
# lance tout dans tmux et se contente d imprimer la commande d attache
# (scripts/run.sh, "Attach : tmux -S ... attach"). Le pipeline QEMU
# delegue, lui, prend CALYPSO_NO_ATTACH=1 (qosmo-grgsm/run.sh l.36).
#
# BRIDGE_NO_PROCESS=1 ./start.sh retablit l ancien comportement quand on
# veut vraiment le coeur seul.
local run_cmd="RUN_NO_PROCESS=${BRIDGE_NO_PROCESS:-0} CALYPSO_NO_ATTACH=1 /etc/osmocom/run.sh"
echo -e " ${GREEN}[*] Lancement run.sh...${NC}"
# DANS UN TMUX, pas en simple processus detache.
#
# Le terminal courant est reserve a osmo-operator-1 (handoff QEMU plus
# bas) : c'est celui qu'on regarde. Les autres tournaient en
# "docker exec -d", c'est-a-dire sans session a laquelle se rattacher -
# on ne pouvait plus que LIRE leur journal, jamais reprendre la main sur
# la pile qui tourne.
#
# Une session nommee "osmo" rend chaque operateur joignable a la
# demande, sans rien afficher tant qu'on ne le demande pas :
# docker exec -ti osmo-operator-2 tmux attach -t osmo
# (ou par tools/vty-menu.sh, qui liste les conteneurs)
#
# tee : le journal reste ecrit meme quand personne n'est attache, sinon
# un incident survenu hors session serait perdu. Repli sans tmux pour
# une image qui n'en aurait pas - le comportement d'avant, a l'identique.
# UN SEUL chemin de demarrage par conteneur.
# En mode QEMU, chaque conteneur est demarre par start-direct.sh - le
# premier dans le terminal courant, les autres en session tmux. Lancer
# run.sh EN PLUS donnait deux piles concurrentes dans le meme
# conteneur : la seconde commencait par "--stop" et coupait ce que la
# premiere venait de monter. L'ASP de l'operateur 2 s'attachait, puis
# tout s'arretait - "Client connected" suivi d'un silence.
# run.sh EST conservé : c'est lui qui monte le coeur, et c'est de son
# HLR que depend l'etape suivante - l'attente du port 4258 puis le
# provisionnement des abonnes. L'en priver figeait le lancement sur
# "Attente HLR" pour un demarrage prevu plus tard.
#
# Les conteneurs qui doivent porter la pile COMPLETE la relancent
# ensuite via start-direct.sh, une fois les abonnes en base : la base
# HLR survit au redemarrage, le travail n'est pas perdu.
docker exec -d "$container_name" bash -c "mkdir -p /var/log/osmocom && { ${run_cmd}; } > /var/log/osmocom/run.sh.log 2>&1"
# Attente HLR
echo -ne " ${GREEN}[*] Attente HLR (4258)${NC}"
local retry=0
while ! docker exec "$container_name" bash -c "echo >/dev/tcp/127.0.0.1/4258" 2>/dev/null; do
sleep 2; echo -n "."; retry=$((retry + 1))
if [ $retry -ge 45 ]; then echo -e " ${RED}TIMEOUT${NC}"; break; fi
done
echo -e " ${GREEN}✓${NC}"
# ── CHAQUE HLR NE PORTE QUE SES PROPRES ABONNES ─────────────────────
# On y versait TOUS les abonnes du banc, tous operateurs confondus. Cela
# simulait un roaming qui n'existe pas : un abonne de l'operateur 1
# inscrit dans le HLR de l'operateur 2 y est joignable sans qu'aucun
# lien MAP n'ait ete etabli. Et surtout, la liste devenait illisible -
# six abonnes chez un operateur qui n'en a que deux, dont on ne savait
# plus lesquels etaient les siens.
# Un vrai roaming passe par le SS7, pas par une copie de la base.
local _mine; _mine="$(awk -F: -v op="$i" '$1 == op' "$all_subscribers_file")"
local _n_mine; _n_mine="$(printf '%s\n' "$_mine" | grep -c . || true)"
echo -e " ${GREEN}[*] Alimentation HLR Op${i} (${_n_mine} abonnes)...${NC}"
# ── ET ON EFFACE CE QUI N'EST PLUS AU PLAN ──────────────────────────
# hlr.db survit aux relances. Un changement de plan - MCC par noeud,
# MSISDN a six chiffres - laisse donc derriere lui des abonnes d'un plan
# mort : IMSI que plus aucun mobile ne presente, et surtout MSISDN qu'ils
# RETIENNENT. osmo-hlr impose l'unicite du numero, si bien que le bon
# abonne ne peut plus recuperer le sien - l'erreur est alors
# "cannot update MSISDN", qui ne nomme pas celui qui le tient.
local _garder; _garder="$(printf '%s\n' "$_mine" | cut -d: -f3 | paste -sd'|' -)"
{
echo "#!/bin/bash"
echo "cat > /tmp/hlr_feed.vty << 'VTYCMDS'"
echo "enable"
if [ -n "$_garder" ]; then
echo "__PURGE__${_garder}"
fi
while IFS=: read -r feed_op feed_ms feed_imsi feed_msisdn feed_ki; do
[[ "$feed_op" =~ ^#.*$ ]] && continue
echo "subscriber imsi ${feed_imsi} create"
echo "subscriber imsi ${feed_imsi} update msisdn ${feed_msisdn}"
echo "subscriber imsi ${feed_imsi} update aud2g comp128v1 ki ${feed_ki}"
done <<< "$_mine"
echo "end"
echo "VTYCMDS"
} | docker exec -i "$container_name" bash
# ON NE COMPTE PAS LES REFUS. Le compte n'a pas de valeur ici : osmo-hlr
# prefixe d'un '%' TOUTES ses reponses, succes compris, et un
# "Subscriber already exists" est la reponse NORMALE d'une relance -
# le banc est reprovisionne a chaque demarrage. Toute variante de ce
# comptage finissait par signaler un probleme la ou il n'y en a pas,
# et un controle qui crie au loup ne sert plus a rien le jour ou il a
# raison.
#
# La seule preuve qu'un abonne est servi, c'est sa fiche : on RELIT la
# base. Un abonne sans cle est pire qu'un abonne absent - osmo-msc le
# trouve, reclame ses vecteurs d'authentification, n'obtient rien, et
# rejette le mobile avec un motif qui accuse la carte SIM.
local _sans_cle
# La purge : on LISTE avec sqlite (le VTY n'a pas de "show all" filtrable)
# et on SUPPRIME par le VTY - osmo-hlr tient un cache en memoire, un
# DELETE direct dans la base ne l'en previendrait pas.
docker exec "$container_name" bash -c '
garder="$(sed -n "s/^__PURGE__//p" /tmp/hlr_feed.vty)"
sed -i "/^__PURGE__/d" /tmp/hlr_feed.vty
if [ -n "$garder" ] && command -v sqlite3 >/dev/null 2>&1; then
sqlite3 /var/lib/osmocom/hlr.db "select imsi from subscriber;" 2>/dev/null \
| grep -vE "^($garder)$" \
| sed "s|^|subscriber imsi |; s|$| delete|" >> /tmp/hlr_purge.vty
fi
if [ -s /tmp/hlr_purge.vty ]; then
{ echo enable; cat /tmp/hlr_purge.vty; } > /tmp/hlr_purge_full.vty
if command -v nc >/dev/null 2>&1; then
(sleep 1; cat /tmp/hlr_purge_full.vty; sleep 2) | nc -q2 127.0.0.1 4258 2>/dev/null
else
(sleep 1; cat /tmp/hlr_purge_full.vty; sleep 3) | telnet 127.0.0.1 4258 2>/dev/null
fi
echo " purge : $(grep -c . /tmp/hlr_purge.vty) abonne(s) hors plan supprime(s)" >&2
fi
rm -f /tmp/hlr_purge.vty /tmp/hlr_purge_full.vty
if command -v nc >/dev/null 2>&1; then
(sleep 1; cat /tmp/hlr_feed.vty; sleep 2) | nc -q2 127.0.0.1 4258 2>/dev/null
else
(sleep 1; cat /tmp/hlr_feed.vty; sleep 3) | telnet 127.0.0.1 4258 2>/dev/null
fi
rm -f /tmp/hlr_feed.vty
' >/dev/null 2>&1 || true
_sans_cle="$(docker exec "$container_name" bash -c '
command -v sqlite3 >/dev/null 2>&1 || exit 0
sqlite3 /var/lib/osmocom/hlr.db "
select count(*) from subscriber s
left join auc_2g a on a.subscriber_id = s.id
where a.subscriber_id is null;" 2>/dev/null' 2>/dev/null)" || true
if [ "${_sans_cle:-0}" -gt 0 ]; then
echo -e " ${RED}✗ HLR Op${i} : ${_sans_cle} abonne(s) SANS CLE - ils ne pourront pas s'authentifier${NC}"
echo -e " ${CYAN}docker exec ${container_name} sqlite3 /var/lib/osmocom/hlr.db \"select s.imsi from subscriber s left join auc_2g a on a.subscriber_id=s.id where a.subscriber_id is null;\"${NC}"
else
echo -e " ${GREEN}✓ HLR Op${i} alimente (${_n_mine} abonnes, tous avec cle)${NC}"
fi
# Attente dernier groupe - sautee en no-process
if [ "${BRIDGE_NO_PROCESS:-0}" = "1" ]; then
echo -e " ${YELLOW}[no-process] mobile non lance - attente 4247 sautee${NC}"
else
echo -ne " ${GREEN}[*] Attente groupe (${last_group_ip}:4247)${NC}"
retry=0
while ! docker exec "$container_name" bash -c "echo >/dev/tcp/${last_group_ip}/4247" 2>/dev/null; do
sleep 2; echo -n "."; retry=$((retry + 1))
if [ $retry -ge 90 ]; then echo -e " ${RED}TIMEOUT${NC}"; break; fi
done
echo -e " ${GREEN}OK${NC}"
sleep 3
fi
echo -e " ${GREEN}✓${NC} ${container_name} pret"
echo ""
done
rm -f "$all_subscribers_file"
# ── Attente relays SMS ────────────────────────────────────────────────────
if [ "${BRIDGE_NO_PROCESS:-0}" != "1" ] && declare -f sms_routing_wait_ready > /dev/null 2>&1; then
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
sms_routing_wait_ready "$(op_container "$i")" 90 || true
done
fi
# ── WAN ────────────────────────────────────────────────────────────────────
if [ "${WAN_MESH:-0}" = "1" ]; then
wan_mesh_apply "$n_operators"
elif [ "$WAN_ENABLED" = "true" ]; then
setup_wan_interop "$n_operators" "$WAN_N_REMOTE"
fi
# ── Resume ─────────────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}${BOLD}Stack multi-operateurs demarree !${NC}"
echo ""
echo -e " Inter-STP @ ${CYAN}${INTER_STP_IP}:2908${NC} PC=0.0.0"
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
local rctx bb_ip n_groups
rctx=$(op_rctx_inter "$i"); bb_ip=$(op_backbone_ip "$i")
n_groups=$(( (${OP_MS[$i]} + 7) / 8 ))
echo -e " Op${i} ${OP_NAME[$i]} STP 1.${i}.2 @ ${bb_ip} RCTX ${rctx} [${OP_MS[$i]} MS]"
done
echo ""
echo -e " ${BOLD}Linphone (depuis l'hote) :${NC}"
for i in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
local bb_ip; bb_ip=$(op_backbone_ip "$i")
echo -e " Op${i}: ${CYAN}${bb_ip}:5060${NC}"
done
echo -e " linphone_A / tester → 100 | linphone_B / testerB → 200"
if [ "${WAN_MESH:-0}" = "1" ]; then
echo ""
echo -e " ${BOLD}WAN mesh :${NC} noeud ${CYAN}${WAN_NODE_ID}${NC}/${WAN_NODE_COUNT} - indicatif ${CYAN}$(wan_local_ind)${NC}"
for _r in "${WAN_NODE_LIST[@]}"; do
[ "$_r" = "$WAN_NODE_ID" ] && continue
echo -e " ${CYAN}${WAN_IND[$_r]}${NC}10001 → MS 10001 op1 du noeud ${_r} (${WAN_IP[$_r]})"
done
elif [ "$WAN_ENABLED" = "true" ]; then
echo ""
echo -e " ${BOLD}WAN :${NC} ${CYAN}${WAN_LOCAL_IP}${NC} (${n_operators} ops) ↔ ${CYAN}${WAN_REMOTE_IP}${NC} (${WAN_N_REMOTE} ops) prefix=${WAN_PREFIX}"
fi
echo ""
# ── LES CHECKS, JUSTE APRES LE LANCEMENT ────────────────────────────────
# [2026-08-31] Ils tournent ICI depuis que la pile complete demarre
# (no-attach au lieu de no-process). Avant, il n y avait rien a mesurer : le
# coeur seul tournait, ni PHY ni mobile ni Asterisk, et "Stack demarree !"
# etait tout ce qu on obtenait - un banc annonce pret dont personne
# n avait verifie qu il portait un appel.
#
# ss7_check.sh ASP, AS, routes, SSN - la couche qui porte les appels.
# operator_summary.sh ce que chaque operateur a REELLEMENT : abonnes
# HLR/VLR, BTS, contextes PDP. --live lui fait generer
# son propre dump ; sans ce drapeau il analyserait
# celui d un lab precedent et decrirait un autre banc.
#
# Chemins RELATIFS, comme ./helpers/prepare_host.sh plus haut : ce script
# suppose deja d etre lance depuis la racine du depot.
# Ni l un ni l autre ne doit faire echouer le lancement - la pile est
# debout, on RAPPORTE son etat, d ou le || true.
# OSMO_SKIP_CHECKS=1 les saute (start-multi.sh le pose : il enchaine ses
# propres verifications juste apres, inutile de jouer ss7_check deux fois).
if [ "${OSMO_SKIP_CHECKS:-0}" != "1" ]; then
if [ -r ./checks/ss7_check.sh ]; then
echo -e "${GREEN}${BOLD}── Check SS7 ──${NC}"
bash ./checks/ss7_check.sh || true
echo ""
else
echo -e " ${YELLOW}checks/ss7_check.sh introuvable - check SS7 saute${NC}"
fi
if [ -r ./checks/operator_summary.sh ]; then
echo -e "${GREEN}${BOLD}── Check operateurs ──${NC}"
bash ./checks/operator_summary.sh --live || true
echo ""
else
echo -e " ${YELLOW}checks/operator_summary.sh introuvable - resume saute${NC}"
fi
fi
# --virtualbox : le "set" ne se limite pas a cette machine. Les VM sont
# creees avant (setup-vbox-interco.sh) ; on les demarre ici, une fois que le
# hub et les operateurs locaux sont debout - un noeud qui monte avant le hub
# voit sa SCTP refusee.
if [ "${VBOX_INTERCO:-0}" = "1" ]; then
local _lab="$(dirname "$0")/tools/vbox-wan-lab.sh"
if [ -x "$_lab" ] || [ -r "$_lab" ]; then
echo ""
echo -e "${GREEN}[*] Demarrage des VM du WAN...${NC}"
bash "$_lab" start || echo -e " ${YELLOW}⚠ demarrage des VM incomplet${NC}"
fi
fi
# ICI, et pas plus tot : docker reinsere ses MASQUERADE en tete de
# POSTROUTING a CHAQUE reseau cree, et il y en a un par operateur. Rejouee
# depuis l'application du WAN, la regle se retrouvait derriere eux - donc
# inerte, alors qu'"iptables -L" la montrait toujours. A cet endroit, tous
# les reseaux existent : plus rien ne passera devant.
reassert_docker_lan_return
# L'adresse par laquelle les VM doivent router vers les conteneurs. Elle
# change avec l'interface active - un cable debranche, et c'est le wifi qui
# repond, sous une autre adresse. L'afficher ici evite d'aller la chercher
# apres coup, quand plus rien ne s'attache et que rien n'a "bouge".
local _hostip
_hostip="${HOST_LAN_IP:-$(ip route show default 2>/dev/null | awk '/^default/{print $9; exit}')}"
if [ -n "$_hostip" ] && [ "${WAN_MESH:-0}" = "1" ]; then
echo -e "\n ${BOLD}Cote VM${NC}, pour joindre les conteneurs :"
echo -e " ${CYAN}bash network/set_ip_vm.sh --via ${_hostip} --persist${NC}"
fi
echo -e "\n${GREEN}${BOLD}Stack prete !${NC}"
# ── Mode QEMU : handoff vers start-direct.sh ──────────────────────────────
if [ "${BRIDGE_QEMU:-0}" = "1" ]; then
local _qemu_container; _qemu_container=$(op_container 1)
echo -e " ${CYAN}[*] run.sh calypso → ${_qemu_container} (terminal courant)${NC}"
echo -e " ${CYAN}[*] mode=${HANDOFF_MODE} pipeline=${HANDOFF_QEMU_CHOICE} chiffrement='${ENCRYPTION}' (choisis sur l'hote, NO_MENU)${NC}"
# Le start-direct lance DANS le conteneur doit connaitre son noeud : il
# reverifie (et corrige) l'identite SS7 avant de demarrer la pile, au
# lieu de dependre de ce que start.sh a ecrit dans les configs.
# Ces arguments sont fabriques par la fonction _node_args() plus bas,
# une par conteneur. La variable `_node_args` qui vivait ici n'etait
# LUE nulle part : une seconde version, figee sur "--op 1", du meme
# calcul. Supprimee pour qu'il n'en reste qu'une.
local _wan_env=""
if [ "${WAN_MESH:-0}" = "1" ]; then
_wan_env="WAN_NODES='$(wan_nodes_spec)' WAN_NODE_ID='${WAN_NODE_ID}' WAN_OPS='${n_operators}'"
echo -e " ${CYAN}[*] WAN mesh transmis au conteneur (noeud ${WAN_NODE_ID}, indicatif $(wan_local_ind))${NC}"
elif [ "$WAN_ENABLED" = "true" ]; then
_wan_env="WAN_REMOTE_IP='${WAN_REMOTE_IP}' WAN_PREFIX='${WAN_PREFIX}' WAN_N_REMOTE='${WAN_N_REMOTE:-$n_operators}'"
echo -e " ${CYAN}[*] SMS inter-WAN → ${WAN_REMOTE_IP} (prefixe ${WAN_PREFIX})${NC}"
fi
# ── LANCEMENT DE LA PILE RADIO ──────────────────────────────────────
# Les conteneurs se sont arretes au COEUR (no-process) : STP, HLR, MSC,
# BSC, MGW, GGSN, SGSN, PCU sont debout, mais ni PHY, ni mobile, ni
# Asterisk, ni SMSC. start.sh enchaine maintenant start-direct.sh sur
# chaque conteneur pour monter la pile radio - la sortie [ OK ] defile
# dans le terminal courant, on voit ce qui se passe.
#
# LE COMPORTEMENT DEPEND DU NOMBRE DE CONTENEURS (voir plus bas) :
# - un seul : on lance PUIS on s'attache au tmux "calypso" ;
# - plusieurs : on lance chacun en NO-ATTACH, les [ OK ] defilent.
#
# Les commandes executees ne sont pas indicatives : ce sont EXACTEMENT
# celles que start.sh imprimait auparavant a recopier a la main - meme
# identite de noeud, meme hub, meme table WAN.
# Les arguments de noeud d'un conteneur : sa place dans le plan WAN.
_node_args() { # $1=indice du conteneur
# Le rang d'operateur n'est 1 que si CHAQUE conteneur est un noeud.
# Sinon les conteneurs partagent le noeud et se distinguent par
# leur rang : figer "--op 1" faisait reecrire par set-node-id.sh
# les trois configs du conteneur 2 avec l'identite du conteneur 1
# (meme point code, meme routing context) - le hub, en override,
# donnait alors tout le trafic au dernier attache. Meme convention
# que le plan ecrit plus haut (_node_i / _op_i).
local _n _op
if [ "${WAN_NODE_PER_OP:-0}" = "1" ]; then
_n=$(( ${WAN_NODE_ID:-1} + $1 - 1 )); _op=1
else
_n="${WAN_NODE_ID:-1}"; _op="$1"
fi
# Repli sur le noeud 1, TOUJOURS. Deux trous se referment ici :
# - sans WAN (`./start.sh` sans argument), la fonction n'imprimait
# RIEN : start-direct.sh partait sans identite et montait la
# pile sur les point codes du gabarit (1.1.2), les memes pour
# tout le monde ;
# - avec WAN mais WAN_NODE_ID vide, elle imprimait `--node --op 1`
# — bash recolle les mots, start-direct.sh lisait "--op" comme
# numero de noeud et sortait sur
# « [ KO ] --node (un chiffre de 1 a 9) ».
# Un noeud unique EST le noeud 1 : c'est deja ce que start.sh se
# donne lui-meme (WAN_NODE_ID="${WAN_NODE_ID:-1}" plus haut), on
# transmet donc la meme reponse au conteneur au lieu de rien.
[[ "$_n" =~ ^[1-9]$ ]] || _n=1
[[ "$_op" =~ ^[1-9]$ ]] || _op=1
printf -- '--node %s --op %s' "$_n" "$_op"
# Le hub n'a de sens qu'en mesh : hors mesh il n'y a personne a
# joindre, et un `--hub-ip` vide decalerait les arguments suivants.
[ "${WAN_MESH:-0}" = "1" ] && [ -n "${WAN_STP_TARGET:-}" ] && \
printf -- ' --hub-ip %s' "$WAN_STP_TARGET"
return 0
}
echo ""
# La commande start-direct.sh d'un conteneur : identite de noeud + hub
# + table WAN. Rien d'indicatif ici, ce sont EXACTEMENT les arguments
# que start.sh executait quand il imprimait les lignes a recopier.
_hcmd() { # $1=indice du conteneur -> imprime la commande
local _na; _na="$(_node_args "$1")"
local _cmd="cd /opt/GSM/osmo-operator && NO_MENU=1"
# [2026-08-31] CALYPSO_NO_ATTACH SUIT LE HANDOFF.
# L attache ne venait ni d ici ni de start-direct.sh, mais du
# qosmo-grgsm/run.sh qu il appelle en bout de chaine :
# NO_ATTACH="${CALYPSO_NO_ATTACH:-0}" (run.sh l.36)
# [ $TTY -eq 1 ] && [ "$NO_ATTACH" != 1 ] && exec tmux attach
# Le defaut est donc D ATTACHER, et `docker exec -t` fournit
# justement un TTY. Resultat : meme avec deux conteneurs - donc dans
# la branche NO-ATTACH de ce script - la fenetre finissait dans le
# tmux du conteneur, start-multi.sh ne reprenait jamais la main, et
# ni les checks ni le bilan ne s executaient.
# La variable posee plus haut sur /etc/osmocom/run.sh ne suffisait
# pas : elle ne traversait pas ce handoff-ci.
[ "${OSMO_NO_ATTACH:-0}" = "1" ] && _cmd="$_cmd CALYPSO_NO_ATTACH=1"
_cmd="$_cmd MODE='${HANDOFF_MODE}' QEMU_CHOICE='${HANDOFF_QEMU_CHOICE}'"
_cmd="$_cmd ENCRYPTION='a5 1' CALYPSO_BRIDGE=pont CALYPSO_MODE=shunt_legit"
[ -n "$_wan_env" ] && _cmd="$_cmd ${_wan_env}"
_cmd="$_cmd ./start-direct.sh ${_na} --force"
printf '%s' "$_cmd"
}
local _c _ct _cmd
if [ "$n_operators" -eq 1 ] && [ "${OSMO_NO_ATTACH:-0}" != "1" ]; then
# ── UN SEUL CONTENEUR : ON LANCE PUIS ON S'ATTACHE ──────────────
# [2026-08-31] ...SAUF si l appelant a demande le contraire.
# A un seul conteneur, ce bloc s attachait TOUJOURS a la session
# tmux du conteneur et n en ressortait plus. C est le bon geste
# quand on tape la commande soi-meme ; c est le mauvais quand le
# lancement vient d une icone ou d un script : le terminal reste
# bloque dans le tmux, le script appelant n a jamais la main, et
# rien de ce qui devait suivre - les checks, le bilan - ne
# s execute. Constate depuis start-multi.sh, dont la fenetre
# finissait sur les panneaux de osmo-operator-1.
# OSMO_NO_ATTACH=1 saute l attache ; la pile reste debout dans sa
# session calypso, on s y rattache a la main quand on veut.
# start.sh lance lui-meme la pile radio, dans le terminal courant :
# les [ OK ] de start-direct.sh puis de run.sh defilent sous les
# yeux, et une fois le coeur monte on S'ATTACHE a la session tmux
# "calypso" du conteneur - on reste dedans, on regarde la pile
# vivre. Ctrl-b puis d pour se detacher sans rien arreter.
_ct="$(op_container 1)"; _cmd="$(_hcmd 1)"
echo -e " ${BOLD}Lancement de la pile radio sur ${_ct} (terminal courant)...${NC}"
echo ""
docker exec -ti "$_ct" bash -c "$_cmd"
echo ""
echo -e " ${BOLD}Attache a la session tmux ${CYAN}calypso${NC}${BOLD} de ${_ct}${NC} ${CYAN}(Ctrl-b puis d pour se detacher)${NC}"
echo ""
docker exec -ti "$_ct" tmux attach -t calypso
else
# ── PLUSIEURS CONTENEURS : NO-ATTACH, ET DETACHE ────────────────
# On ne peut pas s'attacher a N sessions tmux dans un seul terminal.
#
# [2026-08-31] `docker exec -t` REMPLACE PAR `-d`. Le `-t` allouait
# un pty et le rendait au retour du script ; Docker le fermait, et
# le noyau envoyait SIGHUP a tout le groupe de premier plan - donc a
# TOUS les demons que run_modules lance en `cmd &` (un shell non
# interactif n'a pas de controle de tache : ils restent dans le
# groupe du script, dont ce pty est le terminal de controle). QEMU
# sortait sur "terminating on signal 1", fake_trx et tcpdump avec
# lui, et les deux `mobile` sur "Layer2 socket failed" - une
# quinzaine de secondes apres un demarrage annonce reussi, sans que
# rien ne relie la panne au lanceur. Les modules posent desormais
# `setsid` (voir run_modules/_lib/radio.sh, bloc SIGHUP) ; `-d`
# supprime la cause en amont : pas de pty, donc pas de hangup.
#
# CE QU'ON PERD : les [ OK ] ne defilent plus ici. Ils sont donc
# rediriges, DANS le conteneur, vers /var/log/osmocom/start-direct.log
# - chemin deja monte sur l'hote (/tmp/osmocom-logs/opN), rien n'est
# perdu. Meme convention que le lancement de run.sh plus haut.
# CE QU'ON GARDE : le sequencement. `wait_bb_vty` ne rend la main
# qu'une fois la pile du conteneur en ligne, donc un conteneur a la
# fois, et "Toutes les piles tournent" reste vrai a l'impression.
echo -e " ${BOLD}${n_operators} conteneurs : lancement detache - journal ${CYAN}/var/log/osmocom/start-direct.log${NC}${BOLD} dans chaque conteneur.${NC}"
echo ""
for _c in $(seq "${OP_ID_BASE:-1}" "$(( ${OP_ID_BASE:-1} + n_operators - 1 ))"); do
_ct="$(op_container "$_c")"; _cmd="$(_hcmd "$_c")"
echo -e " ${GREEN}────── ${_ct} ──────────────────────────────────${NC}"
docker exec -d "$_ct" bash -c "mkdir -p /var/log/osmocom && { ${_cmd}; } > /var/log/osmocom/start-direct.log 2>&1"
wait_bb_vty "$_ct" || echo -e " ${RED}pile non prete - voir /var/log/osmocom/start-direct.log${NC}"
echo ""
done
echo -e " ${BOLD}Toutes les piles tournent.${NC} Pour en suivre une :"
echo -e " ${CYAN}docker exec -ti <conteneur> tmux attach -t calypso${NC}"
echo -e " ${CYAN}Ctrl-b puis d${NC} pour se detacher sans rien arreter."
echo ""
fi
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# Mode net-host
# ══════════════════════════════════════════════════════════════════════════════
start_host_mode() {
echo -e "${GREEN}Demarrage en mode net-host (1 operateur)...${NC}"
local src_ip gw_ip
gw_ip=$(ip route get 1 | awk '{print $3; exit}')
src_ip=$(ip route get 1 | awk '{print $7; exit}')
[ -z "$src_ip" ] && { echo -e "${RED}Impossible de detecter l'IP hote.${NC}"; exit 1; }
HOST_IP="$src_ip"
local n_ms
n_ms=$(wt_input "net-host" "Nombre de MS :" "1") || exit 1
n_ms=${n_ms:-1}
prepare_host_tun
docker rm -f egprs &>/dev/null || true
SMS_ROUTING_DIR=$(mktemp -d)
if declare -f sms_routing_generate > /dev/null 2>&1; then
sms_routing_generate 1 1 "$SMS_ROUTING_DIR" "$n_ms"
fi
local tmpdir; tmpdir=$(mktemp -d)
apply_config_templates "$tmpdir" \
"$src_ip" "$gw_ip" \
"1" "1.1.1" "1.1.2" "1.1.3" \
"001" "01" "OsmoGSM" \
"127.0.0.1" "shutdown" "1"
local vol_args alsa_args
vol_args=$(build_vol_args "$tmpdir")
alsa_args=$(build_alsa_args)
ensure_host_audio_relay
local nethost_relay=""
[ -n "$HOST_AUDIO_RELAY" ] && nethost_relay="tcp:127.0.0.1:${WSLG_TCP_PORT}"
# --rm CONSERVE ici, contrairement aux conteneurs operateurs. Celui-la a
# pour commande run.sh et non "sleep infinity" : il s'arrete des que run.sh
# rend la main, et --restart le relancerait alors en boucle - sur le reseau
# de l'HOTE, en concurrence avec le gnome-terminal qui ouvre sa propre
# session juste apres. Il depend en outre de prepare_host_tun (apn0), qui
# n'existe plus apres un reboot : un redemarrage automatique repartirait
# dans un environnement incomplet. Ce mode se relance donc a la main.
# shellcheck disable=SC2086
docker run -d --rm --name egprs --net host \
--cap-add NET_ADMIN --cap-add SYS_ADMIN --cap-add NET_RAW \
--shm-size=8g \
--cgroupns host \
--device /dev/net/tun:/dev/net/tun \
$alsa_args \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
--tmpfs /run --tmpfs /run/lock --tmpfs /tmp:exec,rw,mode=1777,size=2g \
-e CONTAINER_IP="$src_ip" -e GATEWAY_IP="$gw_ip" \
-e OPERATOR_ID="1" -e N_MS="$n_ms" \
-e INTER_STP_IP="127.0.0.1" \
-e HOST_IP="${HOST_IP}" -e SIP_HOST_PORT="5060" \
-e ALSA_OUTPUT="${ALSA_OUTPUT}" -e ALSA_INPUT="${ALSA_INPUT}" \
-e HOST_AUDIO_RELAY="${nethost_relay}" \
$vol_args \
"$IMAGE_RUN" /root/run.sh
wait_bb_vty "egprs"
sleep 3
local _du="${SUDO_USER:-$(logname 2>/dev/null || echo "$USER")}"
local _duid; _duid=$(id -u "$_du" 2>/dev/null)
local _disp="${DISPLAY:-:0}"
local _xauth="${XAUTHORITY:-/home/$_du/.Xauthority}"
local _hscript="/tmp/osmo-gnome-host.sh"
cat > "$_hscript" <<'EOF'
#!/usr/bin/env bash
printf '\033]0;net-host - egprs\007'
echo "=== net-host - egprs run.sh ==="
exec sudo docker exec -ti egprs /bin/bash -c "/root/run.sh; exec bash"
EOF
chmod +x "$_hscript"
echo -e " ${CYAN}[*] run.sh → gnome-terminal${NC}"
sudo -u "$_du" \
DISPLAY="$_disp" XAUTHORITY="$_xauth" \
XDG_RUNTIME_DIR="/run/user/${_duid}" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${_duid}/bus" \
gnome-terminal -- bash "$_hscript" &
sleep 0.3
}
# ══════════════════════════════════════════════════════════════════════════════
stop_all() {
echo -e "${YELLOW}Arret de tous les containers Osmocom...${NC}"
docker ps -a --filter "name=osmo-" --format "{{.Names}}" | xargs -r docker rm -f 2>/dev/null || true
docker ps -a --filter "name=egprs" --format "{{.Names}}" | xargs -r docker rm -f 2>/dev/null || true
for _chain in OSMO_WAN_INTEROP OSMO_WAN_MESH; do
iptables -t nat -D PREROUTING -j "$_chain" 2>/dev/null || true
iptables -t nat -F "$_chain" 2>/dev/null || true
iptables -t nat -X "$_chain" 2>/dev/null || true
done
echo -e "${GREEN}Arrete.${NC}"
disable_user_loopback
}
# ══════════════════════════════════════════════════════════════════════════════
# Menus
# ══════════════════════════════════════════════════════════════════════════════
choose_build_mode() {
[ "${QUICK_EXPLICIT:-0}" = "1" ] && return 0
local choice
choice=$(wt_menu "Build de l'image" "Comment (re)construire l'image run ?" \
"quick" "Quick - reutilise le cache docker (rapide)" \
"normal" "Normal - docker build --no-cache (image a neuf)") \
|| { echo "Annule."; exit 1; }
case "$choice" in
quick) QUICK=1 ;;
normal) QUICK=0 ;;
esac
}
choose_ran_host() {
local r
r=$(wt_menu "RAN - Pipeline QEMU Calypso" \
"Pipeline radio (conteneur de l'operateur) :" \
"full-grgsm" "gr-gsm = le DSP (defaut, valide)" \
"start-clean" "Pipeline historique (start-clean.sh)" \
"full" "full radio + vrai c54x (WIP)" \
"shunt" "DSP shunt canned (bissection FBSB)" \
"bare" "QEMU + osmocon only" \
"faketrx-qemu" "COMBINE : fake_trx vivant + Calypso QEMU" \
"free" "menu complet run.sh (--menu)") || exit 1
if [ "$r" = "faketrx-qemu" ]; then
HANDOFF_MODE="faketrx-qemu"; HANDOFF_QEMU_CHOICE="faketrx-qemu"
else
HANDOFF_MODE="qemu"; HANDOFF_QEMU_CHOICE="$r"
fi
local e
e=$(wt_menu "Chiffrement A5" "Chiffrement :" \
"1" "A5/1 - chiffrement legacy (defaut)" \
"0" "A5/0 - pas de chiffrement" \
"2" "A5/2 - (casse, usage test)") || exit 1
case "$e" in 0) ENCRYPTION="a5 0" ;; 2) ENCRYPTION="a5 2" ;; *) ENCRYPTION="a5 0" ;; esac
echo -e " ${GREEN}[RAN] mode=${CYAN}${HANDOFF_MODE}${NC}${GREEN} pipeline=${CYAN}${HANDOFF_QEMU_CHOICE}${NC}${GREEN} chiffrement=${CYAN}${ENCRYPTION}${NC}"
}
choose_network_mode() {
local choice
choice=$(wt_menu "Que lancer ?" "Premier choix :" \
"qemu" "PoC QEMU Calypso - qosmo-grgsm/run.sh (defaut)" \
"virtual" "Reseau VIRTUEL multi-operateurs SS7 (config niveau 2)" \
"hw" "SDR physique (net-host) - hw (in dev)") || { echo "Annule."; exit 1; }
case "$choice" in
qemu) NETWORK_MODE="qemu" ;;
virtual) NETWORK_MODE="bridge" ;;
hw) NETWORK_MODE="host" ;;
*) echo -e "${RED}Choix invalide.${NC}"; exit 1 ;;
esac
}
# ══════════════════════════════════════════════════════════════════════════════
# Main
# ══════════════════════════════════════════════════════════════════════════════
# ── Options longues, extraites AVANT le mode positionnel ──────────────────────
# start.sh a toujours pris son mode en $1 (qemu|virtual|hw|stop) et son build en
# quick|normal. On garde ca intact : la pre-passe ne retire que les --wan* et
# rend la main avec les positionnels d'origine.
usage_wan() {
cat <<'USAGE'
Usage : sudo ./start.sh [quick|normal] [--wan ...] [qemu|virtual|hw|stop]
--wan active le WAN a N noeuds (1 a 9) et pose les questions :
• nombre de noeuds
• IP publique de chaque noeud
• indicatif de chaque noeud (prefixe d'appel)
• numero du noeud construit par CE lancement
--wan-nodes "1:IP:IND ..." meme chose sans question (scriptable)
--wan-id N numero du noeud local (sinon deduit des IP locales)
--wan-conf FICHIER table a lire/ecrire (defaut /etc/osmo-wan.conf)
--virtualbox[=N] monte le WAN entre CETTE machine et des VM VirtualBox
(implique --wan). Cette machine devient un noeud, les
autres sont des VM sur un segment host-only.
--vbox-node N numero de noeud porte par cette machine (defaut 1)
--hub-node N noeud qui heberge l'inter-STP, le hub SS7 (defaut 1).
--hub-ip ADRESSE inter-STP a une adresse a lui, hors table WAN. Le hub
n'est alors PAS un noeud : aucun indicatif, aucun
abonne, et rien n'est lance localement.
--node-per-op chaque conteneur est un NOEUD (base = --wan-id) et non
un operateur de la machine : conteneur 1 -> noeud N,
conteneur 2 -> noeud N+1... Point codes 1.<n>1.<role>.
--host ADRESSE adresse de CETTE machine vue des VM (defaut : deduite
de la route par defaut). Recours quand la machine a
plusieurs cartes ou vient de changer d'adresse : c'est
par elle que les VM routent vers les conteneurs.
--operator IP:PREFIXE declare un operateur du WAN par son adresse et son
prefixe d'appel. Repetable ; l'ordre donne les numeros
de noeud. Il DECLARE seulement : aucun conteneur n'est
lance pour lui - c'est ainsi qu'une VM deja demarree
entre dans la table SMS et le dialplan. Les conteneurs
restent gouvernes par le nombre d'operateurs demande.
--operator 192.168.1.2:11 (la VM, deja lancee)
--operator 172.20.0.11:22 (un conteneur d'ici)
UN SEUL noeud du WAN doit le porter.
--build-stp construit l'image legere osmocom-stp (Dockerfile.stp)
pour le hub. Sans elle, le hub tourne sur l'image
complete, qui contient aussi osmo-stp.
Sans --wan, rien ne change : aucun WAN n'est monte.
Avec --wan, la radio passe en HYBRIDE par defaut (1 faketrx + 1 QEMU Calypso).
Composer <indicatif><numero> joint ce numero sur le noeud correspondant.
ex. noeud 1 = indicatif 11, noeud 2 = 22 → depuis 2, "1110001" appelle
le MS 10001 de l'operateur 1 du noeud 1.
USAGE
}
_pos_args=()
while [ $# -gt 0 ]; do
case "$1" in
--wan) WAN_MESH=1 ;;
--wan=*) WAN_MESH=1; WAN_NODES="${1#*=}" ;;
--wan-nodes) WAN_MESH=1; WAN_NODES="${2:-}"; shift ;;
--wan-nodes=*) WAN_MESH=1; WAN_NODES="${1#*=}" ;;
--wan-id) WAN_MESH=1; WAN_NODE_ID="${2:-}"; shift ;;
--wan-id=*) WAN_MESH=1; WAN_NODE_ID="${1#*=}" ;;
--wan-conf) WAN_CONF_FILE="${2:-}"; shift ;;
--wan-conf=*) WAN_CONF_FILE="${1#*=}" ;;
--build-stp) BUILD_STP=1 ;;
--hub-node) WAN_HUB_NODE="${2:-1}"; WAN_HUB_NODE_GIVEN=1; shift ;;
# --hub-ip et --node-per-op IMPLIQUENT le plan WAN. Sans WAN_MESH=1,
# _pc_mid retombe sur le plan LOCAL (_pc_mid="$i") : les point codes
# deviennent 1.1.2 / 1.2.2 et les routing contexts 150 / 250, calcules
# a l'identique sur CHAQUE machine du WAN. Deux noeuds attaches au meme
# hub presentent alors le meme PC et le meme contexte ; le hub etant en
# traffic-mode override, le dernier ASP attache vole le trafic du
# premier, en silence. C'est exactement ce que le plan WAN, indexe par
# numero de noeud, existe pour eviter.
--hub-ip) WAN_HUB_IP="${2:-}"; WAN_MESH=1; shift ;;
--node-per-op) WAN_NODE_PER_OP=1; WAN_NODE_PER_OP_GIVEN=1; WAN_MESH=1 ;;
--host) HOST_LAN_IP="${2:-}"; shift ;;
--host=*) HOST_LAN_IP="${1#*=}" ;;
--operators) OPERATOR_COUNT_HINT="${2:-}"; shift ;;
--operators=*) OPERATOR_COUNT_HINT="${1#*=}" ;;
--operator) OPERATOR_DECLS+=("${2:-}"); WAN_MESH=1; shift ;;
--operator=*) OPERATOR_DECLS+=("${1#*=}"); WAN_MESH=1 ;;
--hub-ip=*) WAN_HUB_IP="${1#*=}"; WAN_MESH=1 ;;
--hub-node=*) WAN_HUB_NODE="${1#*=}" ;;
--virtualbox) WAN_MESH=1; VBOX_INTERCO=1 ;;
--virtualbox=*) WAN_MESH=1; VBOX_INTERCO=1; VBOX_NODES="${1#*=}" ;;
--vbox-node) VBOX_HOST_NODE="${2:-1}"; shift ;;
--vbox-node=*) VBOX_HOST_NODE="${1#*=}" ;;
-h|--help) usage_wan; exit 0 ;;
*) _pos_args+=("$1") ;;
esac
shift
done
# ── --operator : de la declaration a la table WAN ───────────────────────────
# "IP:PREFIXE" est ce qu'on a sous les yeux ; "id:ip:ind" est ce que le reste du
# code attend. La conversion se fait ici, une fois, plutot que de demander a
# l'utilisateur de tenir lui-meme la numerotation des noeuds.
if [ "${#OPERATOR_DECLS[@]}" -gt 0 ]; then
if [ -n "${WAN_NODES:-}" ]; then
echo -e "\033[0;31m--operator et --wan-nodes sont exclusifs : choisissez l'un des deux.\033[0m" >&2
exit 2
fi
# LES CONTENEURS D'ABORD, dans l'ordre : osmo-operator-1 est le noeud 1,
# -2 le noeud 2... Leur adresse est celle du backbone, leur indicatif le
# defaut du rang. Les operateurs declares en CLI prennent les rangs
# suivants. Numerotation lisible : le nom du conteneur donne son noeud.
_n=0; _spec=""
_ncont="${OPERATOR_COUNT_HINT:-0}"
while [ "$_n" -lt "$_ncont" ]; do
_n=$(( _n + 1 ))
_spec="${_spec}${_spec:+ }${_n}:172.20.0.$(( 10 + _n )):$(( _n * 11 ))"
done
for _d in "${OPERATOR_DECLS[@]}"; do
_ip="${_d%%:*}"; _ind="${_d##*:}"
if [ -z "$_ip" ] || [ "$_ip" = "$_d" ] || [ -z "$_ind" ]; then
echo -e "\033[0;31m--operator : attendu IP:PREFIXE (ex: 192.168.1.2:11), recu '$_d'\033[0m" >&2
exit 2
fi
_n=$(( _n + 1 ))
_spec="${_spec}${_spec:+ }${_n}:${_ip}:${_ind}"
done
WAN_NODES="$_spec"
echo -e "\033[0;36m Operateurs declares :\033[0m $WAN_NODES"
fi
set -- ${_pos_args[@]+"${_pos_args[@]}"}
banner
[ "${1:-}" = "stop" ] && { stop_all; exit 0; }
[ "$(id -u)" -ne 0 ] && { echo -e "${RED}Root requis${NC}"; exit 1; }
case "${1:-}" in
quick) QUICK=1; QUICK_EXPLICIT=1; shift ;;
normal) QUICK=0; QUICK_EXPLICIT=1; shift ;;
esac
docker rm -f $(docker ps -aq --filter "name=osmo-") 2>/dev/null || true
docker rm -f $(docker ps -aq --filter "name=egprs") 2>/dev/null || true
docker network ls --filter "name=gsm-" -q | xargs -r docker network rm 2>/dev/null || true
# Selection du mode
case "${1:-}" in
qemu) NETWORK_MODE="qemu" ;;
virtual) NETWORK_MODE="bridge" ;;
hw) NETWORK_MODE="host" ;;
"")
if [ "${WAN_MESH:-0}" = "1" ]; then
# --wan sans mode : on ne redemande pas "que lancer ?". Un WAN se
# monte sur des operateurs, donc mode virtuel, et le menu de build
# reste pose (quick/normal) s'il n'a pas ete tranche en ligne.
NETWORK_MODE="bridge"
[ "${QUICK_EXPLICIT:-0}" = "1" ] || { check_whiptail; choose_build_mode; }
elif [ "${OSMO_MULTI:-0}" = "1" ]; then
NETWORK_MODE="bridge"
elif [ "${OSMO_MENU:-1}" = "0" ]; then
NETWORK_MODE="qemu"
else
check_whiptail
choose_build_mode
choose_network_mode
fi
;;
*) echo -e "${RED}Mode inconnu : '$1' (attendu : qemu|virtual|hw|stop)${NC}"; exit 1 ;;
esac
[ "$NETWORK_MODE" = "bridge" ] && check_whiptail
echo -e "${GREEN}Mode : ${CYAN}${NETWORK_MODE}${NC} ${GREEN}Build : ${CYAN}$([ "${QUICK:-0}" = "1" ] && echo "quick (cache)" || echo "normal (--no-cache)")${NC}"
./helpers/prepare_host.sh
build_run_image
check_image
case "$NETWORK_MODE" in
qemu) start_qemu_poc ;;
host) start_host_mode ;;
bridge) start_bridge_mode ;;
esactools/daram_sweep.sh
tools/daram_sweep.sh
#!/bin/bash
# tools/daram_sweep.sh - balaye CALYPSO_BSP_DARAM_ADDR en full mode et compare
# l'activite du correlateur FB-det (fb0_att / snr / toa / pm).
# But : trancher empiriquement quelle zone DARAM debloque le FB-det.
#
# Robuste : run.sh est lance NO_ATTACH EN AVANT-PLAN (il rend la main seul
# apres avoir lance qemu, qui survit orphelin). On NE backgroude PAS run.sh
# avec & (ca cassait le pipe du log → qemu mourait). On verifie que qemu est
# vivant avant de mesurer ; sinon on le signale.
#
# Usage (conteneur) : /opt/GSM/daram_sweep.sh
# Env : ADDRS (defaut "0x0080 0x2a00 0x0060"), SETTLE (attente post-boot, 50s)
set -u
RUN=/opt/GSM/qosmo-grgsm/run.sh
QLOG=/root/qemu.log
ADDRS="${ADDRS:-0x0080 0x2a00 0x0060}"
SETTLE="${SETTLE:-50}"
# Profil d'emulation du balayage. "full" a disparu de environment/modes.env :
# les profils sont empty | shunt_legit | shunt_legit_no_inject | native |
# native_twl | native_twl_host_demod | native_helped.
MODE="${MODE:-${CALYPSO_MODE:-native}}"
declare -A R_FB R_SB R_NZ R_BEST R_ALIVE R_STATES R_TOT
kill_all() {
tmux kill-session -t calypso 2>/dev/null
for p in $(pgrep -f qemu-system-arm); do kill -9 "$p" 2>/dev/null; done
pkill -9 -f osmo-bts-trx 2>/dev/null; pkill -9 -f osmo-trx-ipc 2>/dev/null
pkill -9 -f "mobile" 2>/dev/null; pkill -9 -f osmocon 2>/dev/null
sleep 4
}
for a in $ADDRS; do
echo "############ DARAM_ADDR=$a (profil $MODE) ############ $(date +%H:%M:%S)"
kill_all
if pgrep -f qemu-system-arm >/dev/null; then
echo " !! un qemu survit a kill_all : la supervision le relance." >&2
echo " Arretez la pile (CALYPSO_NO_RESTART=1 ou ./run.sh --stop) avant le balayage." >&2
exit 3
fi
: > "$QLOG"
# run.sh NO_ATTACH rend la main seul apres le launch (~30-60s). timeout = filet.
echo " launch run.sh (NO_ATTACH, avant-plan, le boot prend ~30-60s)..."
timeout 150 env CALYPSO_NO_ATTACH=1 CALYPSO_MODE="$MODE" CALYPSO_ICOUNT=off \
CALYPSO_NO_RESTART=1 CALYPSO_BSP_DARAM_ADDR="$a" "$RUN" \
</dev/null >"/tmp/sweep_$a.out" 2>&1
rc_run=$?
# Le profil a-t-il seulement ete applique ? Sans ce controle, un mode
# inconnu faisait avorter run.sh a son deuxieme module, l'ancien qemu
# (relance par la supervision) continuait d'ecrire dans le MEME journal,
# et le balayage mesurait cette instance-la : les trois adresses rendaient
# alors la meme "premiere detection", au caractere pres.
if grep -qa "CALYPSO_MODE inconnu\|sequence aborted" "/tmp/sweep_$a.out"; then
echo " !! run.sh a avorte : profil '''$MODE''' refuse par environment/modes.env" >&2
sed -n '''/valeurs :/,+2p''' "/tmp/sweep_$a.out" | sed "s/^/ /" >&2
echo " Relancez avec MODE=<profil valide> $0" >&2
exit 2
fi
echo " run.sh rendu (code=$?). qemu doit tourner orphelin. settle ${SETTLE}s..."
alive=no
for i in $(seq 1 $((SETTLE/5))); do
sleep 5
if pgrep -f qemu-system-arm >/dev/null; then alive=yes; else alive=no; fi
# stop tot si on a deja du FB data non trivial
grep -qa "real DSP path" "$QLOG" 2>/dev/null && [ "$i" -ge 4 ] && break
done
fb=$(grep -a "real DSP path" "$QLOG" 2>/dev/null | grep -oE "fb0_att=[0-9]+" | grep -oE "[0-9]+" | sort -n | tail -1)
sb=$(grep -a "real DSP path" "$QLOG" 2>/dev/null | grep -oE "sb_att=[0-9]+" | grep -oE "[0-9]+" | sort -n | tail -1)
# Le format des lignes FBSB a change : le correlateur s'ecrit desormais
# data[](det=0 toa=0 pm=0 ang=0 snr=0x0000) api[](...)
# et non plus "last(snr=... toa=... ang=... pm=...)". Les deux motifs sont
# acceptes : sans cela le balayage annoncait "non0=0 / 1er_corr=none" pour
# TOUTES les adresses, y compris celles ou le correlateur voit tres bien
# l'I/Q - un faux negatif qui condamnait la bonne zone DARAM.
NUL='(det=0 toa=0 pm=0 ang=0 snr=0x0000)|snr=0 toa=0 ang=0 pm=0'
nz=$(grep -a "real DSP path" "$QLOG" 2>/dev/null | grep -avcE "$NUL")
best=$(grep -a "real DSP path" "$QLOG" 2>/dev/null | grep -avE "$NUL" \
| grep -oE "(data\[\]\(det=[0-9-]+ toa=[0-9-]+ pm=[0-9-]+ ang=[0-9-]+ snr=0x[0-9a-fA-F]+\))|(last\(snr=[0-9-]+ toa=[0-9-]+ ang=[0-9-]+ pm=[0-9a-fx]+\))" \
| head -1)
# Combien de passages en tout : le taux de detection compte autant que le
# premier succes (19 % ne suffit pas a verrouiller la synchro).
tot=$(grep -ac "real DSP path" "$QLOG" 2>/dev/null)
states=$(grep -aoE 'state=[A-Z0-9_]+' "$QLOG" 2>/dev/null | sort | uniq -c | tr '\n' ' ')
qlines=$(wc -l < "$QLOG" 2>/dev/null)
R_FB[$a]="${fb:-0}"; R_SB[$a]="${sb:-0}"; R_NZ[$a]="${nz:-0}"
R_BEST[$a]="${best:-none}"; R_ALIVE[$a]="$alive"; R_STATES[$a]="${states:-<aucun>}"
R_TOT[$a]="${tot:-0}"
echo " -> qemu_vivant=$alive qemu.log=${qlines}l fb0_att=${fb:-0} sb_att=${sb:-0} non0=${nz:-0}/${tot:-0}"
echo " -> etats: ${states:-<aucun>}"
echo " -> 1er correlateur non-nul: ${best:-none}"
done
kill_all
echo
echo "================= COMPARAISON ================="
printf "%-9s %-7s %-9s %-8s %-12s %s\n" "ADDR" "alive" "fb0_att" "sb_att" "non0/total" "1er_corr"
for a in $ADDRS; do
printf "%-9s %-7s %-9s %-8s %-12s %s\n" \
"$a" "${R_ALIVE[$a]}" "${R_FB[$a]}" "${R_SB[$a]}" \
"${R_NZ[$a]}/${R_TOT[$a]}" "${R_BEST[$a]}"
done
echo
echo "Gagnant = fb0_att>0 et/ou non0>0 (correlateur voit l'I/Q). alive=no => boot KO."tools/fft2.sh
tools/fft2.sh
#!/usr/bin/env bash
# tools/fft2.sh - FFT du 2e cfile. Reutilise tools/fft.sh avec un autre CFILE.
#
# tools/fft.sh = dsp_iq.cfile = MS (entree DSP Calypso : le mobile qui repond a la BTS)
# tools/fft2.sh = record.cfile = BTS (ce que la BTS emet)
# -> MEME signal aux deux bouts de la chaine (BTS TX <-> MS RX) ; la difference
# entre les deux spectres = ce que la chaine (shunt/gr-gsm) fait au signal.
#
# Usage : ./tools/fft2.sh (defaut CFILE=/dev/shm/record.cfile)
# CFILE=... ./tools/fft2.sh
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export CFILE="${CFILE:-/dev/shm/record.cfile}"
export ARFCN="${ARFCN:-BTS-record}"
exec bash "$HERE/fft.sh" "$@"tools/fft_global.sh
tools/fft_global.sh
#!/usr/bin/env bash
# tools/fft_global.sh - FFT "jolie" d'un cfile LOCAL passe en argument (fichier statique).
#
# Derive de tools/fft.sh (osmo-operator) mais SANS docker / ring / tail-live : lit
# directement un cfile sur l'hote, le balaye en boucle, et affiche
# PSD moyennee (Welch maison) + waterfall defilant (FuncAnimation).
#
# Usage :
# ./tools/fft_global.sh my.cfile
# ./tools/fft_global.sh my.cfile --save spectre.png
# RATE=1083333 NSAMP=32768 REFRESH=0.25 FORMAT=cf32 ARFCN=514 ./tools/fft_global.sh my.cfile
#
# FORMAT : cf32 (complex64, defaut, ce qu'ecrit le shunt) | cs8 | cs16
# (record.cfile d'osmo-trx est souvent cs8 -> FORMAT=cs8)
set -euo pipefail
CFILE="${1:?usage: tools/fft_global.sh <fichier.cfile> [--save out.png]}"
[ -f "$CFILE" ] || { echo "[fft] introuvable: $CFILE"; exit 1; }
export CFILE="$(readlink -f "$CFILE")"
export RATE="${RATE:-1083333}" # 4 SPS natif = 26e6/24
export NSAMP="${NSAMP:-32768}" # complex samples par fenetre
export REFRESH="${REFRESH:-0.25}" # periode de rafraichissement (s)
export ARFCN="${ARFCN:-514}" # juste pour le titre
export FORMAT="${FORMAT:-cf32}" # cf32 | cs8 | cs16
export DISPLAY="${DISPLAY:-:0}"
export FFT_SAVE=""
[ "${2:-}" = "--save" ] && export FFT_SAVE="${3:?--save needs a path}"
python3 - <<'PYEOF'
import os, numpy as np
import matplotlib
SAVE = os.environ.get("FFT_SAVE","")
if SAVE:
matplotlib.use("Agg")
else:
for bk in ("TkAgg","Qt5Agg","GTK3Agg"):
try: matplotlib.use(bk); break
except Exception: continue
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
CFILE = os.environ["CFILE"]
RATE = float(os.environ["RATE"]); NS = int(os.environ["NSAMP"])
REFRESH = float(os.environ["REFRESH"]); ARFCN = os.environ["ARFCN"]
FORMAT = os.environ.get("FORMAT","cf32").lower()
NFFT = 4096 # resolution FFT
WROWS = 120 # lignes du waterfall
# ---- format d'echantillon -> dtype brut + bytes/sample complexe ----
if FORMAT in ("cf32","fc32","complex64"):
RAWDT, ITEMS = np.float32, 2 # I,F float32
elif FORMAT in ("cs16","sc16","int16"):
RAWDT, ITEMS = np.int16, 2
elif FORMAT in ("cs8","sc8","int8"):
RAWDT, ITEMS = np.int8, 2
else:
raise SystemExit(f"FORMAT inconnu: {FORMAT} (cf32|cs16|cs8)")
SAMP_BYTES = np.dtype(RAWDT).itemsize * ITEMS # octets / sample complexe
FSIZE = os.path.getsize(CFILE)
NSAMP_TOT = FSIZE // SAMP_BYTES
print(f"[fft] {CFILE} {FSIZE} o format={FORMAT} {NSAMP_TOT} samples Fs={RATE/1e6:.4f} MHz")
if NSAMP_TOT < NFFT:
raise SystemExit("[fft] fichier trop court pour une FFT")
win = np.hanning(NFFT).astype(np.float32)
freqs = np.fft.fftshift(np.fft.fftfreq(NFFT, 1.0/RATE)) / 1e3 # kHz
_off = [0] # offset courant en SAMPLES (balayage en boucle)
def grab():
"""Lit NS samples complexes a l'offset courant, avance, boucle a la fin."""
start = _off[0]
if start + NS > NSAMP_TOT:
start = 0
raw = np.fromfile(CFILE, dtype=RAWDT, count=NS*ITEMS,
offset=start*SAMP_BYTES)
_off[0] = start + NS
if len(raw) < NFFT*ITEMS:
return None
raw = raw.astype(np.float32)
iq = raw[0::2] + 1j*raw[1::2]
if RAWDT == np.int8: iq /= 128.0
if RAWDT == np.int16: iq /= 32768.0
return iq.astype(np.complex64)
def welch_psd(iq):
"""PSD moyennee (segments NFFT, overlap 50%, Hann) en dB."""
step = NFFT//2; nseg = max(1,(len(iq)-NFFT)//step + 1)
acc = np.zeros(NFFT, dtype=np.float64)
used = 0
for i in range(nseg):
seg = iq[i*step:i*step+NFFT]
if len(seg) < NFFT: break
sp = np.fft.fftshift(np.fft.fft(seg*win))
acc += (sp.real**2 + sp.imag**2); used += 1
psd = acc/max(1,used)
return 10*np.log10(psd/(NFFT*RATE) + 1e-20)
# ---- figure "jolie" : PSD (haut) + waterfall (bas) alignees ----
plt.style.use("dark_background")
fig = plt.figure(figsize=(11,8))
gs = fig.add_gridspec(2, 2, width_ratios=[40, 1], height_ratios=[1, 1.4],
hspace=0.18, wspace=0.015)
axp = fig.add_subplot(gs[0, 0])
axw = fig.add_subplot(gs[1, 0], sharex=axp)
cax = fig.add_subplot(gs[1, 1])
fig.suptitle(f"FFT - {os.path.basename(CFILE)} | ARFCN={ARFCN} Fs={RATE/1e6:.4f} MHz fmt={FORMAT}",
fontsize=11, color="#7fd")
(line,) = axp.plot(freqs, np.full(NFFT,-120), lw=0.5, color="#39f")
axp.set_xlim(freqs[0],freqs[-1]); axp.set_ylim(-130,-30)
axp.set_ylabel("PSD (dB)"); axp.grid(alpha=0.25)
axp.axvline(0, color="#f55", lw=0.7, alpha=0.7)
plt.setp(axp.get_xticklabels(), visible=False)
wf = np.full((WROWS,NFFT), -120.0)
im = axw.imshow(wf, aspect="auto", origin="upper", cmap="turbo",
extent=[freqs[0],freqs[-1],0,WROWS], vmin=-120, vmax=-50,
interpolation="bilinear")
axw.set_ylabel("temps (fenetres)"); axw.set_xlabel("offset (kHz)")
axw.axvline(0, color="#fff", lw=0.7, alpha=0.5)
fig.colorbar(im, cax=cax, label="dB")
fig.subplots_adjust(left=0.08, right=0.92, top=0.92, bottom=0.08)
def update(_):
iq = grab()
if iq is None: return line, im
psd = welch_psd(iq)
line.set_ydata(psd)
lo,hi = np.percentile(psd,5), np.percentile(psd,99.5)
axp.set_ylim(lo-5, hi+8)
global wf
wf = np.roll(wf,1,axis=0); wf[0,:] = psd
im.set_data(wf); im.set_clim(lo, hi+5)
return line, im
if SAVE:
update(0)
fig.savefig(SAVE, dpi=110, facecolor=fig.get_facecolor())
print("ecrit", SAVE)
else:
ani = FuncAnimation(fig, update, interval=int(REFRESH*1000),
blit=False, cache_frame_data=False)
print("FFT - ferme la fenetre pour quitter.")
plt.show()
PYEOFtools/fft.sh
tools/fft.sh
#!/usr/bin/env bash
# tools/fft.sh - FFT "jolie" du flux I/Q Calypso, lue DIRECTEMENT dans le docker.
#
# Lit /tmp/record.cfile (le ring record CONTINU, ecrit par record_drain.py qui
# draine iq_record.fifo en O_RDWR - DECOUPLE de la boucle relay temps-reel) DANS
# le container, le rapatrie en fc32 (complex64), et affiche sur le X de l'HOTE :
# PSD moyennee (Welch maison) + waterfall defilant. Boucle (FuncAnimation).
#
# /!\ Le mode 'fifo' (tap LIVE de /tmp/iq_fft.fifo) a ete RETIRE : ouvrir ce fifo
# en lecture force l'IPC device a l'ecrire et perturbe le relai temps-reel ->
# le decode grgsm s'arrete (si-bridge 'fini') et le camping fige. record.cfile
# est near-live et n'a AUCUN effet sur le pipeline.
#
# Usage :
# ./tools/fft.sh # tail de record.cfile (defaut, non-perturbant)
# FFT_SRC=sweep ./tools/fft.sh # balaye tout le ring record
# ./tools/fft.sh --save spectre.png # une capture PNG (si pas de X)
# CONTAINER=osmo-operator-1 CFILE=/tmp/record.cfile RATE=1083333 \
# NSAMP=262144 REFRESH=1.0 ARFCN=514 ./tools/fft.sh
#
set -euo pipefail
export CONTAINER="${CONTAINER:-osmo-operator-1}"
export FFT_SRC="${FFT_SRC:-tail}" # tail (defaut, NON-perturbant) | sweep - lisent record.cfile
# NB: le mode 'fifo' (tap LIVE de /tmp/iq_fft.fifo) a ete RETIRE : ouvrir ce
# fifo en lecture force l'IPC device a l'ecrire, ce qui perturbe la boucle relay
# temps-reel et COUPE le decode grgsm (si-bridge 'fini') -> camping fige. On lit
# desormais record.cfile, alimente par record_drain.py (decouple, always-on).
export CFILE="${CFILE:-/dev/shm/dsp_iq.cfile}" # entree DSP shunt = MS (le mobile qui repond a la BTS ; cfile fc32 en /dev/shm RAM). Fichier qui GRANDIT -> tail-c lit le frais. (override CFILE=/dev/shm/record.cfile = BTS)
export RATE="${RATE:-1083333}" # 4 SPS natif = 26e6/24
export NSAMP="${NSAMP:-32768}" # complex samples par fenetre (256 KB) - court = ca gigote
export REFRESH="${REFRESH:-0.25}" # periode de rafraichissement (s) - court = fluide
export ARFCN="${ARFCN:-514}" # juste pour le titre (DCS1800)
export DISPLAY="${DISPLAY:-:0}"
export FFT_SAVE=""
[ "${1:-}" = "--save" ] && export FFT_SAVE="${2:?--save needs a path}"
# garde-fous
command -v docker >/dev/null || { echo "docker introuvable"; exit 1; }
case "$FFT_SRC" in
sweep|tail) ;;
*) echo "[fft] FFT_SRC='$FFT_SRC' invalide. Le mode 'fifo' (tap live) a ete retire"
echo "[fft] (il perturbe le relai temps-reel et coupe le decode grgsm). -> tail."
export FFT_SRC=tail ;;
esac
docker exec "$CONTAINER" test -f "$CFILE" \
|| { echo "record absent $CONTAINER:$CFILE (lance ./run.sh d'abord ; record_drain.py alimente record.cfile)"; exit 1; }
python3 - <<'PYEOF'
import os, sys, subprocess, numpy as np
import matplotlib
SAVE = os.environ.get("FFT_SAVE","")
if SAVE:
matplotlib.use("Agg")
else:
for bk in ("TkAgg","Qt5Agg","GTK3Agg"):
try: matplotlib.use(bk); break
except Exception: continue
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
CONT = os.environ["CONTAINER"]; CFILE = os.environ["CFILE"]
RATE = float(os.environ["RATE"]); NS = int(os.environ["NSAMP"])
REFRESH = float(os.environ["REFRESH"]); ARFCN = os.environ["ARFCN"]
NBYTES = NS*8 # complex64 = 8 o/sample
NFFT = 4096 # resolution FFT (segment Welch)
WROWS = 120 # lignes du waterfall
win = np.hanning(NFFT).astype(np.float32)
freqs = np.fft.fftshift(np.fft.fftfreq(NFFT, 1.0/RATE)) / 1e3 # kHz
SRC = os.environ.get("FFT_SRC","tail") # tail (defaut) | sweep - lisent record.cfile
if SRC not in ("sweep","tail"): SRC = "tail" # 'fifo' retire (tap live nocif)
BS = 4096; NBLK = max(1, NBYTES//BS)
# ---- modes fichier : balaye le ring (sweep) ou lit le tail de record.cfile
# (alimente par record_drain.py, decouple de la boucle relay temps-reel).
def _filesize():
try:
return int(subprocess.run(["docker","exec",CONT,"stat","-c","%s",CFILE],
capture_output=True,timeout=5).stdout or 0)
except Exception: return NBYTES
def _ringsize():
# taille du ring publiee par record_drain (modulo pour le wrap LIVE)
try:
v = int(subprocess.run(["docker","exec",CONT,"cat",CFILE+".ring"],
capture_output=True,timeout=5).stdout or 0)
if v > 0: return v
except Exception: pass
return _filesize()
RING = _ringsize()
_off = [0]
def _dd(skip_blk, count_blk):
return subprocess.run(["docker","exec",CONT,"dd","if="+CFILE,"bs="+str(BS),
"skip="+str(skip_blk),"count="+str(count_blk),"status=none"],
capture_output=True, timeout=8).stdout
def grab():
"""Rend NBYTES d'I/Q complex64 (le plus frais), depuis record.cfile.
SRC=tail : derniers octets (defaut, near-live, non-perturbant).
SRC=sweep : balaye le ring record (offset qui avance)."""
if SRC == "sweep":
skip = _off[0] // BS
try:
raw = subprocess.run(["docker","exec",CONT,"dd","if="+CFILE,
"bs="+str(BS),"skip="+str(skip),"count="+str(NBLK),"status=none"],
capture_output=True, timeout=8).stdout
except Exception:
return None
_off[0] += NBYTES
if _off[0] + NBYTES > RING: _off[0] = 0
else:
# LIVE (tail) : lit la fenetre fraiche FINISSANT a l'offset d'ecriture
# publie par record_drain (<cfile>.off). Corrige le freeze : avec le ring
# seek(0) le frais est a `w` (milieu), PAS au tail. Aligne blocs + wrap.
try:
w = int(subprocess.run(["docker","exec",CONT,"cat",CFILE+".off"],
capture_output=True, timeout=5).stdout or -1)
except Exception:
w = -1
try:
if w >= 0:
wblk = w // BS; start_blk = wblk - NBLK
if start_blk >= 0:
raw = _dd(start_blk, NBLK)
else: # fenetre a cheval sur le wrap
ring_blk = RING // BS; n_end = -start_blk
raw = _dd(ring_blk - n_end, n_end) + _dd(0, wblk)
else: # repli : pas d'offset publie
raw = subprocess.run(["docker","exec",CONT,"tail","-c",str(NBYTES),CFILE],
capture_output=True, timeout=8).stdout
except Exception:
return None
n = (len(raw)//8)*8
if n < NFFT*8: return None
return np.frombuffer(raw[:n], dtype=np.complex64)
def welch_psd(iq):
"""PSD moyennee (segments NFFT, overlap 50%, fenetre Hann) en dB"""
step = NFFT//2; nseg = max(1,(len(iq)-NFFT)//step + 1)
acc = np.zeros(NFFT, dtype=np.float64)
for i in range(nseg):
seg = iq[i*step:i*step+NFFT]
if len(seg) < NFFT: break
sp = np.fft.fftshift(np.fft.fft(seg*win))
acc += (sp.real**2 + sp.imag**2)
psd = acc/max(1,nseg)
return 10*np.log10(psd/ (NFFT*RATE) + 1e-20)
# ---- figure "jolie" : PSD (haut) + waterfall (bas) ALIGNEES verticalement.
# GridSpec 2x2 : col 0 = les 2 plots (MEME largeur), col 1 = colorbar (seulement
# sur la ligne waterfall) -> la colorbar ne retrecit plus la waterfall, donc x=0
# tombe sur la MEME verticale dans la PSD et la waterfall. sharex lie les axes. ----
plt.style.use("dark_background")
fig = plt.figure(figsize=(11,8))
gs = fig.add_gridspec(2, 2, width_ratios=[40, 1], height_ratios=[1, 1.4],
hspace=0.18, wspace=0.015)
axp = fig.add_subplot(gs[0, 0])
axw = fig.add_subplot(gs[1, 0], sharex=axp) # x partage -> aligne avec la PSD
cax = fig.add_subplot(gs[1, 1]) # colorbar dans sa colonne (n'affecte pas axw)
fig.suptitle(f"Calypso I/Q live - {CONT}:{CFILE} | ARFCN={ARFCN} Fs={RATE/1e6:.4f} MHz",
fontsize=11, color="#7fd")
(line,) = axp.plot(freqs, np.full(NFFT,-120), lw=0.5, color="#39f")
axp.set_xlim(freqs[0],freqs[-1]); axp.set_ylim(-130,-30)
axp.set_ylabel("PSD (dB)"); axp.grid(alpha=0.25)
axp.axvline(0, color="#f55", lw=0.7, alpha=0.7)
plt.setp(axp.get_xticklabels(), visible=False) # l'axe x est porte par la waterfall en bas
wf = np.full((WROWS,NFFT), -120.0)
im = axw.imshow(wf, aspect="auto", origin="upper", cmap="turbo",
extent=[freqs[0],freqs[-1],0,WROWS], vmin=-120, vmax=-50,
interpolation="bilinear")
axw.set_ylabel("temps (frames)"); axw.set_xlabel("offset (kHz)")
axw.axvline(0, color="#fff", lw=0.7, alpha=0.5) # meme 0 vertical que la PSD
fig.colorbar(im, cax=cax, label="dB")
fig.subplots_adjust(left=0.08, right=0.92, top=0.92, bottom=0.08)
def update(_):
iq = grab()
if iq is None: return line, im
psd = welch_psd(iq)
line.set_ydata(psd)
lo,hi = np.percentile(psd,5), np.percentile(psd,99.5)
axp.set_ylim(lo-5, hi+8)
global wf
wf = np.roll(wf,1,axis=0); wf[0,:] = psd
im.set_data(wf); im.set_clim(lo, hi+5)
return line, im
if SAVE:
update(0) # une passe
fig.savefig(SAVE, dpi=110, facecolor=fig.get_facecolor())
print("ecrit", SAVE)
else:
ani = FuncAnimation(fig, update, interval=int(REFRESH*1000),
blit=False, cache_frame_data=False)
print("FFT live - ferme la fenetre pour quitter.")
plt.show()
PYEOFtools/make-docker-image.sh
tools/make-docker-image.sh
#!/bin/bash
set -euo pipefail
# 0. Parsing des arguments
NO_CACHE=""
for arg in "$@"; do
case "$arg" in
--no-cache)
NO_CACHE="--no-cache"
;;
-h|--help)
echo "Usage: sudo $0 [--no-cache]"
echo " --no-cache Force la reconstruction complete de l'image (paquets propres)."
exit 0
;;
*)
echo -e "\033[0;33m[WARN] Argument inconnu ignore : $arg\033[0m"
;;
esac
done
# 1. Verification des privileges ROOT
if [[ $EUID -ne 0 ]]; then
echo -e "\033[0;31m[ERREUR] Ce script doit etre lance en tant que root (sudo).\033[0m"
exit 1
fi
echo "--- Preparation complete de l'hote (SDR & Docker) ---"
# 2. Installation de Docker (si non present)
if ! command -v docker &> /dev/null; then
echo "[*] Docker n'est pas installe. Installation en cours..."
apt-get update
apt-get install -y docker.io docker-compose-v2
fi
# 3. Installation des dependances critiques sur l'hote
# SCTP est vital pour les protocoles de signalisation Osmocom
echo "[*] Installation de SCTP, TUN et D-Bus sur l'hote..."
apt-get update
apt-get install -y lksctp-tools libsctp-dev dbus uml-utilities libusb-1.0-0-dev \
wireshark linphone* gnome-terminal whiptail expect netcat-openbsd \
telnet iproute2 pulseaudio-utils
# 4. Chargement des modules noyau
echo "[*] Chargement des modules noyau (SCTP & TUN)..."
modprobe sctp
modprobe tun
# Verification du module SCTP (lecture directe de /proc/modules : pas de tube,
# donc pas de SIGPIPE/pipefail, et ancrage sur le nom de module exact)
if grep -q '^sctp ' /proc/modules; then
echo -e "\033[0;32m[OK] Module SCTP charge sur l'hote.\033[0m"
else
echo -e "\033[0;31m[ERREUR] Impossible de charger SCTP.\033[0m"
fi
# 5. Lancement du build Docker
echo "--- Lancement du build de l'image osmocom-nitb ---"
if [[ -n "$NO_CACHE" ]]; then
echo "[*] Mode --no-cache actif : reconstruction complete, paquets propres."
fi
if docker build $NO_CACHE . -t osmocom-nitb; then
echo -e "\033[0;32m[OK] Image osmocom-nitb construite avec succes.\033[0m"
else
echo -e "\033[0;31m[ERREUR] Le build Docker a echoue.\033[0m"
exit 1
fitools/matrix.sh
tools/matrix.sh
#!/usr/bin/env bash
# tools/matrix.sh <burst|si> - MATRICE DYNAMIQUE du treillis 51-multiframe (cote mobile).
#
# Deux vues "temps/canal" complementaires des 2 FFT (frequence) :
# tools/matrix.sh burst -> dynamique des BURSTS (TRXD ts0 DL :5702) :
# lignes = FN%51 (structure canal), colonnes = temps,
# couleur = vide / dummy(idle) / reel(actif).
# tools/matrix.sh si -> dynamique des SI/L2 decodes (GSMTAP :4729/:4730) :
# lignes = FN%51, couleur = type de message (SI/IMM/UA/PAGING/?).
#
# Sniff PASSIF (AF_PACKET via gsm_sniff.py dans le conteneur, ne vole rien, aucune
# fifo) -> pipe -> rendu matplotlib sur l'hote (X). Ferme la fenetre pour quitter.
#
# Usage : ./tools/matrix.sh burst | ./tools/matrix.sh si
# CONTAINER=osmo-operator-66 TS=0 W=240 ./tools/matrix.sh burst
set -euo pipefail
MODE="${1:?usage: tools/matrix.sh <burst|si>}"
case "$MODE" in burst|si) ;; *) echo "mode invalide: $MODE (burst|si)"; exit 1;; esac
export MODE
export CONTAINER="${CONTAINER:-osmo-operator-66}"
export TS="${TS:-0}" # timeslot a afficher (0 = canal combine CCCH+SDCCH4)
export W="${W:-240}" # colonnes (multiframes d'historique)
export DISPLAY="${DISPLAY:-:0}"
PY="${PY:-/root/.env/bin/python3}"; command -v "$PY" >/dev/null 2>&1 || PY=python3
SNIFF=/opt/GSM/qosmo-grgsm/opt-gsm-scripts/gsm_sniff.py
docker exec "$CONTAINER" test -f "$SNIFF" || { echo "gsm_sniff absent dans $CONTAINER"; exit 1; }
# gsm_sniff (conteneur, passif) -> stdout -> python hote (rendu matrice live)
docker exec "$CONTAINER" sh -c "SNIFF_IFACE=lo stdbuf -oL python3 $SNIFF $MODE" \
| "$PY" - <<'PYEOF'
import sys, os, re, threading, collections
import numpy as np, matplotlib
matplotlib.use("TkAgg") if not os.environ.get("MATRIX_SAVE") else matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.colors import ListedColormap, BoundaryNorm
MODE = os.environ.get("MODE","burst"); TS = int(os.environ.get("TS","0")); W = int(os.environ.get("W","240"))
DUMMY = "00011111011011101100000101001001" # debut du dummy burst GSM (idle)
# --- type SI/L2 -> code couleur (mode si) ---
SI_CODE = {"?":1,"SI1":2,"SI2":3,"SI3":4,"SI4":5,"SI2bis":6,"SI2ter":7,"SI13":8,
"PAGING-REQ-1":9,"PAGING-REQ-2":9,"PAGING-REQ-3":9,
"IMM-ASSIGN":10,"IMM-ASSIGN-EXT":10,"IMM-ASSIGN-REJ":10}
# LAPDm (SDCCH/SACCH) detecte par octet de controle : on tag "DEDIE"
SI_LABELS = ["·","?","SI1","SI2","SI3","SI4","SI2b","SI2t","SI13","PAG","IMM","DEDIE"]
SI_COLORS = ["#101018","#888","#1f77b4","#2ca02c","#17becf","#9467bd","#8c564b","#e377c2",
"#7f7f7f","#ff7f0e","#d62728","#ffd700"]
mat = np.zeros((51, W)); curmf=[None]; lock=threading.Lock()
re_ts = re.compile(r"ts(\d)\b")
re_m51 = re.compile(r"%51=(\d+)")
re_fn = re.compile(r"\bfn=(\d+)|\bFN=(\d+)")
re_148 = re.compile(r"\b([01]{148})\b")
re_si = re.compile(r"\b(SI2bis|SI2ter|SI1[3]?|SI[2-4]|IMM-ASSIGN-?\w*|PAGING-REQ-\d)\b")
re_port= re.compile(r":(\d{4})\b")
def reader():
for line in sys.stdin:
m51 = re_m51.search(line); mts = re_ts.search(line)
if not m51: continue
if mts and int(mts.group(1)) != TS: # filtre timeslot (defaut ts0)
# en mode si les lignes :4730 n'ont pas toujours ts -> on garde
if MODE=="burst": continue
fnm = re_fn.search(line); fn = int(fnm.group(1) or fnm.group(2)) if fnm else None
r = int(m51.group(1)) % 51
if MODE=="burst":
mb = re_148.search(line)
if "NOPE" in line or "vide" in line: v = 0.0 # burst vide
elif mb and mb.group(1).startswith(DUMMY): v = 1.0 # dummy (idle)
elif mb: v = 2.0 # reel (actif)
else: v = 0.0
else:
ms = re_si.search(line)
if ms: v = float(SI_CODE.get(ms.group(1), 1))
elif "?" in line: # LAPDm dedie (SDCCH/SACCH UA/I)
v = 11.0 if (22<=r<=25 or 26<=r<=39 or 42<=r<=49) else 1.0
else: v = 1.0
mf = fn//51 if fn is not None else (curmf[0] or 0)
with lock:
if curmf[0] is None: curmf[0]=mf
while mf > curmf[0]: # nouvelle multiframe -> shift gauche
mat[:, :-1] = mat[:, 1:]; mat[:, -1] = 0; curmf[0]+=1
if mf == curmf[0]:
mat[r, -1] = v
threading.Thread(target=reader, daemon=True).start()
plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(12,7))
title = ("BURSTS ts%d (·=vide bleu=dummy/idle rouge=ACTIF)" % TS) if MODE=="burst" \
else "SI / L2 decodes (type par couleur)"
fig.suptitle("Matrice dynamique 51-multiframe - %s [%s]" % (title, os.environ.get("CONTAINER","")),
color="#7fd", fontsize=11)
if MODE=="burst":
cmap = ListedColormap(["#101018","#1f77b4","#d62728"]); norm = BoundaryNorm([-.5,.5,1.5,2.5], cmap.N)
else:
cmap = ListedColormap(SI_COLORS); norm = BoundaryNorm([i-.5 for i in range(len(SI_COLORS)+1)], cmap.N)
im = ax.imshow(mat, aspect="auto", origin="lower", cmap=cmap, norm=norm, interpolation="nearest")
ax.set_ylabel("FN % 51 (FCCH=0,10.. SCH=1,11.. BCCH=2-5 CCCH=6-19 SDCCH=22-39 SACCH=42-49)")
ax.set_xlabel("temps (multiframes 51) →")
# reperes des bandes canal (combine BCCH+SDCCH4 ts0)
for y in (0,10,20,30,40): ax.axhline(y, color="#444", lw=.4)
for y0,y1,lbl,c in [(2,5,"BCCH","#17becf"),(22,25,"SDCCH/0","#ffd700"),(42,45,"SACCH/0","#ff7f0e")]:
ax.axhspan(y0-.5,y1+.5, xmin=0, xmax=0.015, color=c, alpha=.8)
if MODE=="si":
from matplotlib.patches import Patch
ax.legend(handles=[Patch(color=SI_COLORS[i], label=SI_LABELS[i]) for i in range(1,len(SI_LABELS))],
loc="upper left", ncol=4, fontsize=7, framealpha=.3)
def update(_):
with lock: im.set_data(mat.copy())
return (im,)
ani = FuncAnimation(fig, update, interval=250, blit=False, cache_frame_data=False)
print("[matrix:%s] fenetre live - ferme pour quitter." % MODE, flush=True)
plt.show()
PYEOF