Created
March 8, 2026 12:03
-
-
Save bbaranoff/e7c0c0fe42cb69ad640cd39549dda174 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # vty-debug-dump.sh | |
| # | |
| # Parcourt tous les VTY Osmocom de chaque container osmo-operator-N, | |
| # active le niveau de log DEBUG sur chaque composant, exécute les | |
| # commandes "show" pertinentes, et consolide le tout dans un log.txt. | |
| # | |
| # VTY couverts par container : | |
| # OsmoSTP 4239 | |
| # OsmoHLR 4258 | |
| # OsmoMGW 4243 | |
| # OsmoMSC 4254 | |
| # OsmoBSC 4242 | |
| # OsmoBTS 4241 | |
| # OsmoPCU 4240 | |
| # OsmoSGSN 4245 | |
| # OsmoGGSN 4260 | |
| # OsmoSIP-conn 4255 | |
| # | |
| # Usage : | |
| # sudo ./vty-debug-dump.sh [--out /chemin/log.txt] | |
| set -euo pipefail | |
| GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' | |
| OUTFILE="${OUTFILE:-/tmp/vty-debug-dump.txt}" | |
| [[ "${1:-}" == "--out" && -n "${2:-}" ]] && OUTFILE="$2" | |
| # ── Table des VTY : "nom:port:commandes_show" ───────────────────────────────── | |
| # Les commandes show sont séparées par des pipes | (parsées plus bas). | |
| # Chaque composant a ses commandes "show" les plus utiles en debug. | |
| declare -A VTY_NAME VTY_SHOW | |
| register_vty() { | |
| local port="$1" name="$2" show_cmds="$3" | |
| VTY_NAME[$port]="$name" | |
| VTY_SHOW[$port]="$show_cmds" | |
| } | |
| register_vty 4239 "OsmoSTP" "show cs7 instance 0 users|show cs7 instance 0 as all|show cs7 instance 0 asp|show cs7 instance 0 route|show talloc-context all brief" | |
| register_vty 4258 "OsmoHLR" "show subscriber all|show talloc-context all brief" | |
| register_vty 4243 "OsmoMGW" "show mgcp stats|show talloc-context all brief" | |
| register_vty 4254 "OsmoMSC" "show network|show subscriber all|show transaction all|show call-legs|show cs7 instance 0 users|show talloc-context all brief" | |
| register_vty 4242 "OsmoBSC" "show network|show bts all|show paging all|show lchan all|show cs7 instance 0 users|show cs7 instance 0 asp|show talloc-context all brief" | |
| register_vty 4241 "OsmoBTS" "show bts 0|show trx 0 0|show ts all|show lchan all|show talloc-context all brief" | |
| register_vty 4240 "OsmoPCU" "show talloc-context all brief" | |
| register_vty 4245 "OsmoSGSN" "show sgsn|show pdp-context all|show talloc-context all brief" | |
| register_vty 4260 "OsmoGGSN" "show ggsn|show pdp-context all|show talloc-context all brief" | |
| register_vty 4255 "OsmoSIPconn" "show talloc-context all brief" | |
| VTY_PORTS=(4239 4258 4243 4254 4242 4241 4240 4245 4260 4255) | |
| # ── Détection des containers ────────────────────────────────────────────────── | |
| mapfile -t OP_CONTAINERS < <( | |
| docker ps --format '{{.Names}}' \ | |
| | grep -E '^osmo-operator-[0-9]+$' \ | |
| | sort -t '-' -k 3 -n | |
| ) | |
| if [ ${#OP_CONTAINERS[@]} -eq 0 ]; then | |
| echo -e "${RED}Aucun container osmo-operator-N trouvé.${NC}" | |
| echo -e " Containers actifs : $(docker ps --format '{{.Names}}' | tr '\n' ' ')" | |
| exit 1 | |
| fi | |
| N_OPS=${#OP_CONTAINERS[@]} | |
| echo -e "${GREEN}=== VTY Debug Dump — ${N_OPS} opérateur(s) → ${OUTFILE} ===${NC}" | |
| echo "" | |
| # ── Vider / initialiser le fichier de sortie ───────────────────────────────── | |
| : > "$OUTFILE" | |
| log() { echo -e "$*" | tee -a "$OUTFILE"; } | |
| logsep() { | |
| local title="$1" | |
| log "" | |
| log "$(printf '═%.0s' {1..72})" | |
| log " ${title}" | |
| log "$(printf '═%.0s' {1..72})" | |
| } | |
| logsubsep() { | |
| local title="$1" | |
| log "" | |
| log " $(printf '─%.0s' {1..68})" | |
| log " ${title}" | |
| log " $(printf '─%.0s' {1..68})" | |
| } | |
| log "VTY DEBUG DUMP — $(date '+%Y-%m-%d %H:%M:%S')" | |
| log "Opérateurs : ${N_OPS} Containers : ${OP_CONTAINERS[*]}" | |
| # ── Interroger un VTY dans un container ────────────────────────────────────── | |
| # Séquence envoyée : | |
| # enable | |
| # logging level all debug | |
| # logging filter all 1 | |
| # <commandes show> | |
| # end | |
| query_vty() { | |
| local container="$1" | |
| local port="$2" | |
| local name="${VTY_NAME[$port]}" | |
| local show_str="${VTY_SHOW[$port]}" | |
| # Test de connectivité rapide | |
| if ! docker exec "$container" bash -c \ | |
| "echo >/dev/tcp/127.0.0.1/${port}" 2>/dev/null; then | |
| echo -e " ${YELLOW}[SKIP]${NC} ${name} (port ${port}) — non disponible" | |
| log " [SKIP] ${name} port ${port} — non disponible" | |
| return 0 | |
| fi | |
| echo -ne " ${CYAN}${name}${NC} (${port}) … " | |
| # Construire le script VTY | |
| local vty_script | |
| vty_script="$(printf 'enable\nlogging level all debug\nlogging filter all 1\n')" | |
| # Ajouter chaque commande show | |
| IFS='|' read -ra SHOW_CMDS <<< "$show_str" | |
| for cmd in "${SHOW_CMDS[@]}"; do | |
| vty_script+="${cmd}"$'\n' | |
| done | |
| vty_script+=$'end\n' | |
| # Envoyer via nc ou telnet | |
| local raw_output | |
| raw_output=$(docker exec "$container" bash -c " | |
| VTY_SCRIPT=\$(cat << 'VTYEOF' | |
| ${vty_script} | |
| VTYEOF | |
| ) | |
| if command -v nc >/dev/null 2>&1; then | |
| ( sleep 0.5; printf '%s' \"\$VTY_SCRIPT\"; sleep 1 ) \ | |
| | nc -q2 127.0.0.1 ${port} 2>/dev/null || true | |
| else | |
| ( sleep 0.5; printf '%s' \"\$VTY_SCRIPT\"; sleep 1.5 ) \ | |
| | telnet 127.0.0.1 ${port} 2>/dev/null || true | |
| fi | |
| " 2>/dev/null || true) | |
| # Filtrer les lignes de bannière/prompt VTY | |
| local clean_output | |
| clean_output=$(echo "$raw_output" \ | |
| | grep -vE \ | |
| "^(Trying|Connected|Escape|Welcome|OsmoSTP|OsmoHLR|OsmoMSC|\ | |
| OsmoBSC|OsmoBTS|OsmoMGW|OsmoPCU|OsmoSGSN|OsmoGGSN|OsmoSIP|\ | |
| VTY server|Use.*help|Press.*tab|[A-Za-z0-9_-]+[#>] |\ | |
| Enter password|% Unknown|% Command incomplete)" \ | |
| | sed 's/\r//' \ | |
| | grep -v '^[[:space:]]*$' || true) | |
| local n_lines | |
| n_lines=$(echo "$clean_output" | wc -l) | |
| echo -e "${GREEN}${n_lines} lignes${NC}" | |
| # Écrire dans le log | |
| { | |
| echo "" | |
| echo " [${name}] port=${port}" | |
| echo " Commandes show : $(echo "$show_str" | tr '|' ',')" | |
| echo "" | |
| echo "$clean_output" | sed 's/^/ /' | |
| } >> "$OUTFILE" | |
| } | |
| # ── Boucle principale ───────────────────────────────────────────────────────── | |
| TOTAL_OK=0 | |
| TOTAL_SKIP=0 | |
| for container in "${OP_CONTAINERS[@]}"; do | |
| logsep "Container : ${container} — $(date '+%H:%M:%S')" | |
| echo -e "${GREEN}--- ${container} ---${NC}" | |
| for port in "${VTY_PORTS[@]}"; do | |
| logsubsep "${VTY_NAME[$port]} (port ${port})" | |
| query_vty "$container" "$port" | |
| # Compter disponibilité | |
| if docker exec "$container" bash -c \ | |
| "echo >/dev/tcp/127.0.0.1/${port}" 2>/dev/null; then | |
| TOTAL_OK=$(( TOTAL_OK + 1 )) | |
| else | |
| TOTAL_SKIP=$(( TOTAL_SKIP + 1 )) | |
| fi | |
| done | |
| echo "" | |
| done | |
| # ── Résumé ──────────────────────────────────────────────────────────────────── | |
| logsep "RÉSUMÉ — $(date '+%Y-%m-%d %H:%M:%S')" | |
| log " VTY interrogés : ${TOTAL_OK}" | |
| log " VTY non disponibles : ${TOTAL_SKIP}" | |
| log " Fichier : ${OUTFILE}" | |
| log " Taille : $(wc -c < "$OUTFILE") octets / $(wc -l < "$OUTFILE") lignes" | |
| echo "" | |
| echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}" | |
| echo -e "${GREEN}║ Dump complet → ${OUTFILE}${NC}" | |
| echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}" | |
| echo "" | |
| echo -e " ${CYAN}Consulter :${NC} less ${OUTFILE}" | |
| echo -e " ${CYAN}Filtrer :${NC} grep -A5 'OsmoBSC' ${OUTFILE}" | |
| echo -e " ${CYAN}Erreurs :${NC} grep -i 'error\|fail\|warn' ${OUTFILE}" | |
| # Copie locale si lancé depuis l'hôte | |
| if [ -w "$(pwd)" ]; then | |
| cp "$OUTFILE" "$(pwd)/vty-debug-dump-$(date '+%Y%m%d-%H%M%S').txt" 2>/dev/null && \ | |
| echo -e " ${CYAN}Copie locale :${NC} $(pwd)/vty-debug-dump-$(date '+%Y%m%d-%H%M%S').txt" || true | |
| fi | |
| ! | |
| mncc | |
| socket-path /tmp/msc_mncc | |
| sip | |
| local 127.0.0.1 5061 | |
| remote __INTER_LOCAL_IP__ 5060 | |
| ! | |
| ! osmo-bsc.cfg — BSC opérateur __OPERATOR_ID__ (MCC __MCC__ / MNC __MNC__) | |
| ! | |
| ! Pour opérateur 1: utiliser __CONTAINER_IP__ = 172.20.0.11 | |
| ! Pour opérateur 2: utiliser __CONTAINER_IP__ = 172.20.0.12 | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging print thread-id 0 | |
| logging timestamp 0 | |
| logging print level 1 | |
| logging print file basename last | |
| ! | |
| log gsmtap __GATEWAY_IP__ | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging print level 1 | |
| logging level rll notice | |
| logging level mm notice | |
| logging level rr notice | |
| logging level rsl notice | |
| logging level nm notice | |
| logging level pag notice | |
| logging level meas notice | |
| logging level msc notice | |
| logging level ho notice | |
| logging level hodec notice | |
| logging level ref notice | |
| logging level ctrl notice | |
| logging level filter notice | |
| logging level pcu notice | |
| logging level lcls notice | |
| logging level chan notice | |
| logging level ts notice | |
| logging level as notice | |
| logging level cbs notice | |
| logging level lcs notice | |
| logging level asci notice | |
| logging level reset notice | |
| logging level loop notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 notice | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua notice | |
| logging level lmgcp notice | |
| logging level ljibuf notice | |
| logging level lrspro notice | |
| logging level lns notice | |
| logging level lbssgp notice | |
| logging level lnsdata notice | |
| logging level lnssignal notice | |
| logging level liuup notice | |
| logging level lpfcp notice | |
| logging level lcsn1 notice | |
| logging level lio notice | |
| logging level ltcap notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| e1_input | |
| e1_line 0 driver ipa | |
| e1_line 0 port 0 | |
| ! | |
| cs7 instance 0 | |
| network-indicator international | |
| point-code __PC_BSC__ | |
| asp asp-to-stp 2905 2907 m3ua | |
| remote-ip 127.0.0.1 | |
| local-ip 127.0.0.1 | |
| role asp | |
| sctp-role client | |
| no shutdown | |
| as as-bsc m3ua | |
| asp asp-to-stp | |
| routing-key __RCTX_BSC__ __PC_BSC__ | |
| traffic-mode override | |
| sccp-address addr-msc | |
| routing-indicator PC | |
| point-code __PC_MSC__ | |
| subsystem-number 254 | |
| network | |
| network country code __MCC__ | |
| mobile network code __MNC__ | |
| encryption a5 1 | |
| neci 1 | |
| paging any use tch 0 | |
| handover 0 | |
| handover algorithm 1 | |
| handover1 window rxlev averaging 10 | |
| handover1 window rxqual averaging 1 | |
| handover1 window rxlev neighbor averaging 10 | |
| handover1 power budget interval 6 | |
| handover1 power budget hysteresis 3 | |
| handover1 maximum distance 9999 | |
| mgw 0 | |
| local-port 2727 | |
| remote-ip 127.0.0.1 | |
| remote-port 2427 | |
| bts 0 | |
| type osmo-bts | |
| band DCS1800 | |
| cell_identity __CELL_ID__ | |
| location_area_code 0x000__OPERATOR_ID__ | |
| base_station_id_code __BSIC__ | |
| ms max power 15 | |
| cell reselection hysteresis 4 | |
| rxlev access min 0 | |
| radio-link-timeout 32 | |
| channel allocator mode chan-req ascending | |
| channel allocator mode assignment ascending | |
| channel allocator mode handover ascending | |
| channel allocator mode vgcs-vbs ascending | |
| rach tx integer 9 | |
| rach max transmission 7 | |
| rach max-delay 63 | |
| rach expiry-timeout 32 | |
| channel-description attach 1 | |
| channel-description bs-pa-mfrms 5 | |
| channel-description bs-ag-blks-res 1 | |
| no nch-position | |
| no access-control-class-ramping | |
| early-classmark-sending forbidden | |
| early-classmark-sending-3g allowed | |
| ipa unit-id __IPA_UNIT_ID__ 0 | |
| oml ipa stream-id 255 line 0 | |
| neighbor-list mode automatic | |
| codec-support fr | |
| amr tch-f modes 0 2 5 7 | |
| amr tch-f threshold ms 13 25 37 | |
| amr tch-f hysteresis ms 4 4 4 | |
| amr tch-f threshold bts 13 25 37 | |
| amr tch-f hysteresis bts 4 4 4 | |
| amr tch-f start-mode auto | |
| amr tch-h modes 0 2 3 5 | |
| amr tch-h threshold ms 16 24 32 | |
| amr tch-h hysteresis ms 4 4 4 | |
| amr tch-h threshold bts 16 24 32 | |
| amr tch-h hysteresis bts 4 4 4 | |
| amr tch-h start-mode auto | |
| gprs mode egprs | |
| gprs routing area 0 | |
| gprs network-control-order nc0 | |
| gprs power-control alpha 0 | |
| gprs cell bvci __BVCI__ | |
| gprs cell timer blocking-timer 3 | |
| gprs cell timer blocking-retries 3 | |
| gprs cell timer unblocking-retries 3 | |
| gprs cell timer reset-timer 3 | |
| gprs cell timer reset-retries 3 | |
| gprs cell timer suspend-timer 10 | |
| gprs cell timer suspend-retries 3 | |
| gprs cell timer resume-timer 10 | |
| gprs cell timer resume-retries 3 | |
| gprs cell timer capability-update-timer 10 | |
| gprs cell timer capability-update-retries 3 | |
| gprs nsei __NSEI__ | |
| gprs ns timer tns-block 3 | |
| gprs ns timer tns-block-retries 3 | |
| gprs ns timer tns-reset 3 | |
| gprs ns timer tns-reset-retries 3 | |
| gprs ns timer tns-test 30 | |
| gprs ns timer tns-alive 3 | |
| gprs ns timer tns-alive-retries 10 | |
| gprs nsvc 0 nsvci __NSVCI__ | |
| gprs nsvc 0 local udp port 23001 | |
| gprs nsvc 0 remote ip __CONTAINER_IP__ | |
| gprs nsvc 0 remote udp port 23000 | |
| gprs nsvc 1 nsvci 0 | |
| gprs nsvc 1 local udp port 0 | |
| gprs nsvc 1 remote ip 0.0.0.0 | |
| gprs nsvc 1 remote udp port 0 | |
| bs-power-control | |
| mode static | |
| ms-power-control | |
| mode dyn-bts | |
| ctrl-interval 2 | |
| step-size inc 4 red 2 | |
| rxlev-thresh lower 32 upper 38 | |
| rxlev-thresh-comp lower 10 12 upper 19 20 | |
| rxqual-thresh lower 3 upper 0 | |
| rxqual-thresh-comp lower 5 7 upper 15 18 | |
| ci-thresh fr-efr disable | |
| ci-thresh fr-efr lower 13 upper 17 | |
| ci-thresh-comp fr-efr lower 5 7 upper 15 18 | |
| ci-thresh hr disable | |
| ci-thresh hr lower 16 upper 21 | |
| ci-thresh-comp hr lower 5 7 upper 15 18 | |
| ci-thresh amr-fr disable | |
| ci-thresh amr-fr lower 7 upper 11 | |
| ci-thresh-comp amr-fr lower 5 7 upper 15 18 | |
| ci-thresh amr-hr disable | |
| ci-thresh amr-hr lower 13 upper 17 | |
| ci-thresh-comp amr-hr lower 5 7 upper 15 18 | |
| ci-thresh sdcch disable | |
| ci-thresh sdcch lower 12 upper 16 | |
| ci-thresh-comp sdcch lower 5 7 upper 15 18 | |
| ci-thresh gprs disable | |
| ci-thresh gprs lower 18 upper 24 | |
| ci-thresh-comp gprs lower 5 7 upper 15 18 | |
| trx 0 | |
| rf_locked 0 | |
| arfcn __ARFCN__ | |
| nominal power 23 | |
| max_power_red 0 | |
| rsl e1 tei 0 | |
| timeslot 0 | |
| phys_chan_config CCCH+SDCCH4 | |
| hopping enabled 0 | |
| timeslot 1 | |
| phys_chan_config SDCCH8 | |
| hopping enabled 0 | |
| timeslot 2 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 3 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 4 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 5 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 6 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 7 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| msc 0 | |
| codec-list fr1 hr1 fr2 fr3 hr3 | |
| allow-emergency deny | |
| amr-config 12_2k allowed | |
| amr-config 10_2k allowed | |
| amr-config 7_95k allowed | |
| amr-config 7_40k allowed | |
| amr-config 6_70k allowed | |
| amr-config 5_90k allowed | |
| amr-config 5_15k allowed | |
| amr-config 4_75k allowed | |
| amr-payload octet-aligned | |
| msc-addr addr-msc | |
| asp-protocol m3ua | |
| lcls-mode bts-loop | |
| lcls-codec-mismatch allowed | |
| bsc | |
| mid-call-timeout 0 | |
| end | |
| pfcp | |
| local-addr __CONTAINER_IP__ | |
| nft | |
| # netfilter requires no specific configuration | |
| gtp | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category 1 | |
| logging timestamp 1 | |
| logging print file basename | |
| logging level set-all info | |
| ! | |
| line vty | |
| no login | |
| ! | |
| trx | |
| bind-ip 127.0.0.1 | |
| remote-ip 127.0.0.1 | |
| base-port 5700 | |
| egprs enable | |
| tx-sps 4 | |
| rx-sps 4 | |
| clock-ref internal | |
| chan 0 | |
| ! | |
| ! OsmoHLR configuration — Opérateur __OPERATOR_ID__ | |
| ! SMS-over-GSUP activé via gsup-smsc-proto | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging print extended-timestamp 1 | |
| logging print level 1 | |
| logging print file basename | |
| logging level main debug | |
| logging level db debug | |
| logging level auc debug | |
| logging level ss debug | |
| logging level lglobal debug | |
| logging level llapd debug | |
| logging level linp debug | |
| logging level lmux debug | |
| logging level lmi debug | |
| logging level lmib debug | |
| logging level lsms debug | |
| logging level lctrl debug | |
| logging level lgtp debug | |
| logging level lstats debug | |
| logging level lgsup debug | |
| logging level loap debug | |
| logging level lss7 debug | |
| logging level lsccp debug | |
| logging level lsua debug | |
| logging level lm3ua debug | |
| logging level lmgcp debug | |
| logging level ljibuf debug | |
| logging level lrspro debug | |
| logging filter all 0 | |
| logging color 1 | |
| logging print category-hex 1 | |
| logging print category 0 | |
| logging timestamp 0 | |
| logging print file 1 | |
| logging level main notice | |
| logging level db notice | |
| logging level auc notice | |
| logging level ss notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 notice | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua notice | |
| logging level lmgcp notice | |
| logging level ljibuf notice | |
| logging level lrspro notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| hlr | |
| subscriber-create-on-demand 5 cs+ps | |
| store-imei | |
| database /var/lib/osmocom/hlr.db | |
| gsup | |
| bind ip 127.0.0.2 | |
| ipa-name HLR-OP__OPERATOR_ID__ | |
| ussd route prefix *#100# internal own-msisdn | |
| ussd route prefix *#101# internal own-imsi | |
| ! | |
| ! ── SMS-over-GSUP : proto-SMSC connecté via GSUP ────────────────────────── | |
| ! Le proto-smsc-daemon se connecte au HLR avec l'IPA name SMSC-OP<N> | |
| ! SC-address = numéro fictif unique par opérateur (réservé NANP) | |
| ! | |
| smsc entity SMSC-OP__OPERATOR_ID__ | |
| smsc default-route SMSC-OP__OPERATOR_ID__ | |
| ! | |
| ! OsmoBTS (192.168.1.15-bc49-dirty) configuration saved from vty | |
| !! | |
| ! | |
| log stderr | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging timestamp 0 | |
| logging print file basename last | |
| logging print level 1 | |
| ! | |
| log gsmtap __GATEWAY_IP__ | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging print level 1 | |
| logging level rsl info | |
| logging level oml info | |
| logging level rll notice | |
| logging level rr notice | |
| logging level meas notice | |
| logging level pag info | |
| logging level l1c info | |
| logging level l1p info | |
| logging level dsp error | |
| logging level pcu notice | |
| logging level ho debug | |
| logging level trx notice | |
| logging level loop notice | |
| logging level abis debug | |
| logging level rtp notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats error | |
| ! | |
| line vty | |
| no login | |
| ! | |
| e1_input | |
| e1_line 0 driver ipa | |
| e1_line 0 port 0 | |
| no e1_line 0 keepalive | |
| ! | |
| phy 0 | |
| instance 0 | |
| osmotrx rx-gain 40 | |
| osmotrx tx-attenuation 50 | |
| osmotrx ip local 127.0.0.1 | |
| osmotrx ip remote 127.0.0.1 | |
| no osmotrx timing-advance-loop | |
| bts 0 | |
| band DCS1800 | |
| ipa unit-id __IPA_UNIT_ID__ 0 | |
| oml remote-ip 127.0.0.1 | |
| rtp jitter-buffer 100 | |
| paging queue-size 200 | |
| paging lifetime 0 | |
| min-qual-rach 50 | |
| min-qual-norm -5 | |
| gsmtap-sapi ccch | |
| gsmtap-sapi pdtch | |
| trx 0 | |
| power-ramp max-initial 23000 mdBm | |
| power-ramp step-size 2000 mdB | |
| power-ramp step-interval 1 | |
| ms-power-control osmo | |
| phy 0 instance 0 | |
| ! | |
| ! osmo-msc.cfg — MSC opérateur __OPERATOR_ID__ (MCC __MCC__ / MNC __MNC__) | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 1 | |
| logging print category 0 | |
| logging print thread-id 0 | |
| logging timestamp 0 | |
| logging print file 1 | |
| logging level rll notice | |
| logging level cc notice | |
| logging level bcc notice | |
| logging level gcc notice | |
| logging level mm notice | |
| logging level rr notice | |
| logging level mncc notice | |
| logging level pag notice | |
| logging level msc notice | |
| logging level mgcp notice | |
| logging level ho notice | |
| logging level db notice | |
| logging level ref notice | |
| logging level ctrl notice | |
| logging level smpp notice | |
| logging level ranap notice | |
| logging level vlr notice | |
| logging level iucs notice | |
| logging level bssap notice | |
| logging level sgs notice | |
| logging level ss notice | |
| logging level asci notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 info | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua info | |
| logging level lmgcp notice | |
| logging level ljibuf notice | |
| logging level lrspro notice | |
| logging level lns notice | |
| logging level lbssgp notice | |
| logging level lnsdata notice | |
| logging level lnssignal notice | |
| logging level liuup notice | |
| logging level lpfcp notice | |
| logging level lcsn1 notice | |
| logging level lio notice | |
| logging level ltcap notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| cs7 instance 0 | |
| network-indicator international | |
| point-code __PC_MSC__ | |
| ! | |
| ! MSC → STP : même container → 127.0.0.1 | |
| asp asp-to-stp 2905 2906 m3ua | |
| remote-ip 127.0.0.2 | |
| local-ip 127.0.0.1 | |
| role asp | |
| sctp-role client | |
| no shutdown | |
| ! | |
| as as-msc m3ua | |
| asp asp-to-stp | |
| routing-key __RCTX_MSC__ __PC_MSC__ | |
| traffic-mode override | |
| ! | |
| sccp-address addr-bsc | |
| routing-indicator PC | |
| point-code __PC_BSC__ | |
| subsystem-number 254 | |
| ! | |
| network | |
| network country code __MCC__ | |
| mobile network code __MNC__ | |
| short name __OP_NAME__ | |
| long name __OP_NAME__ | |
| encryption a5 1 | |
| authentication optional | |
| rrlp mode none | |
| mm info 0 | |
| mgw 0 | |
| remote-ip 127.0.0.1 | |
| msc | |
| mncc external /tmp/msc_mncc | |
| mncc guard-timeout 180 | |
| ncss guard-timeout 30 | |
| assign-tmsi | |
| lcls-permitted | |
| cs7-instance-a 0 | |
| check-imei-rqd early | |
| ! SMS-over-GSUP : redirige les SM-RP vers le HLR → proto-smsc-daemon | |
| sms-over-gsup | |
| mncc-int | |
| default-codec tch-f fr | |
| default-codec tch-h hr | |
| smpp | |
| local-tcp-port 2775 | |
| policy closed | |
| no smpp-first | |
| esme msc_tester | |
| password osmocom1 | |
| route prefix national isdn 33 | |
| hlr | |
| remote-ip __HLR_IP__ | |
| remote-port 4222 | |
| ipa-name VLR-SDR-OP__OPERATOR_ID__ | |
| sgs | |
| local-port 29118 | |
| local-ip 0.0.0.0 | |
| vlr-name vlr.op__OPERATOR_ID__.net | |
| asci | |
| disable | |
| gcr | |
| ; ============================================================================= | |
| ; extensions.conf — Asterisk dialplan — Opérateur __OPERATOR_ID__ | |
| ; | |
| ; ┌─────────────────────────────────────────────────────────────────────────┐ | |
| ; │ PLAN DE NUMÉROTATION │ | |
| ; │ 100 Linphone A (softphone local) │ | |
| ; │ 200 Linphone B (softphone local) │ | |
| ; │ 600 Echo test │ | |
| ; │ 700 Annonce de bienvenue │ | |
| ; │ NXXXX Abonnés GSM opérateur N (premier chiffre = identifiant op) │ | |
| ; ├─────────────────────────────────────────────────────────────────────────┤ | |
| ; │ CONTEXTES │ | |
| ; │ [internal] Appels depuis softphones locaux │ | |
| ; │ [gsm_in] Appels entrants depuis OsmoMSC (GSM → Asterisk) │ | |
| ; │ [interop_in] Appels entrants depuis un opérateur distant (SIP trunk) │ | |
| ; │ [gsm_out] Sous-contexte : sortie vers MSC local │ | |
| ; │ [interop_out] Routage vers l'opérateur distant approprié │ | |
| ; │ GÉNÉRÉ par start.sh selon le nombre d'opérateurs │ | |
| ; └─────────────────────────────────────────────────────────────────────────┘ | |
| ; | |
| ; SCÉNARIOS COUVERTS | |
| ; ────────────────── | |
| ; 1. MS → MS (même opérateur) : MS → MSC → MNCC → Asterisk → MNCC → MSC → MS | |
| ; 2. MS → Linphone : MS → MSC → Asterisk SIP → Linphone | |
| ; 3. Linphone → MS : Linphone SIP → Asterisk → MNCC → MSC → MS | |
| ; 4. Linphone A → Linphone B : SIP local | |
| ; 5. MS(OpX) → MS(OpY) inter-op : MNCC → AstX → SIP trunk → AstY → MNCC → MS | |
| ; 6. MS → echo / annonce : test audio | |
| ; | |
| ; NOTE : [interop_out] est ajouté en fin de fichier par start.sh. | |
| ; Il contient une extension par opérateur distant, routée vers | |
| ; le trunk interop_trunk_opX correspondant. | |
| ; ============================================================================= | |
| [general] | |
| static=yes | |
| writeprotect=no | |
| clearglobalvars=no | |
| [globals] | |
| MSC_LOCAL=127.0.0.1 | |
| OP_PREFIX=__OPERATOR_ID__ | |
| ; ============================================================================= | |
| ; [internal] — Appels DEPUIS les softphones locaux | |
| ; ============================================================================= | |
| [internal] | |
| ; ── Tests audio ────────────────────────────────────────────────────────────── | |
| exten => 600,1,NoOp(=== ECHO TEST ===) | |
| same => n,Answer() | |
| same => n,Playback(demo-echotest) | |
| same => n,Echo() | |
| same => n,Playback(demo-echodone) | |
| same => n,Hangup() | |
| exten => 700,1,NoOp(=== ANNONCE ===) | |
| same => n,Answer() | |
| same => n,Playback(hello-world) | |
| same => n,Hangup() | |
| ; ── Softphone → Softphone ──────────────────────────────────────────────────── | |
| exten => 100,1,NoOp(=== SIP: → Linphone A ===) | |
| same => n,Dial(PJSIP/linphone_A,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| exten => 200,1,NoOp(=== SIP: → Linphone B ===) | |
| same => n,Dial(PJSIP/linphone_B,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── Softphone → GSM local ──────────────────────────────────────────────────── | |
| ; Numéros opérateur local : __OPERATOR_ID__XXXX | |
| exten => ___OPERATOR_ID__XXXX,1,NoOp(=== SIP -> GSM local (Op__OPERATOR_ID__): ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── Softphone → GSM inter-opérateur ────────────────────────────────────────── | |
| ; Composer 9 + numéro complet (ex: 920001 pour joindre 20001 sur Op2) | |
| exten => _9.,1,NoOp(=== SIP -> INTER-OP: ${EXTEN:1} ===) | |
| same => n,Goto(interop_out,${EXTEN:1},1) | |
| ; ── Catch-all : essayer le MSC local ──────────────────────────────────────── | |
| exten => _X.,1,NoOp(=== SIP -> GSM fallback: ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ============================================================================= | |
| ; [gsm_in] — Appels entrants DEPUIS OsmoMSC (GSM → Asterisk) | |
| ; ============================================================================= | |
| [gsm_in] | |
| ; ── Tests audio ────────────────────────────────────────────────────────────── | |
| exten => 600,1,NoOp(=== GSM -> ECHO TEST ===) | |
| same => n,Answer() | |
| same => n,Playback(demo-echotest) | |
| same => n,Echo() | |
| same => n,Playback(demo-echodone) | |
| same => n,Hangup() | |
| ; ── MS → Linphone ──────────────────────────────────────────────────────────── | |
| exten => 100,1,NoOp(=== GSM -> Linphone A ===) | |
| same => n,Dial(PJSIP/linphone_A,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| exten => 200,1,NoOp(=== GSM -> Linphone B ===) | |
| same => n,Dial(PJSIP/linphone_B,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── MS → MS local (même opérateur) ────────────────────────────────────────── | |
| exten => ___OPERATOR_ID__XXXX,1,NoOp(=== GSM -> GSM local (Op__OPERATOR_ID__): ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── MS → MS inter-opérateur (routage par premier chiffre) ─────────────────── | |
| ; Si premier chiffre = identifiant opérateur local → MSC local | |
| ; Sinon → [interop_out] qui sélectionne le bon trunk SIP | |
| exten => _[0-9]XXXX,1,NoOp(=== GSM -> routage: ${EXTEN} ===) | |
| same => n,GotoIf($["${EXTEN:0:1}" = "__OPERATOR_ID__"]?local_gsm) | |
| same => n,Goto(interop_out,${EXTEN},1) | |
| same => n(local_gsm),NoOp(=== GSM -> GSM local: ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── Catch-all ──────────────────────────────────────────────────────────────── | |
| exten => _X.,1,NoOp(=== GSM -> numéro inconnu: ${EXTEN} ===) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ============================================================================= | |
| ; [interop_in] — Appels entrants DEPUIS un opérateur distant (SIP trunk) | |
| ; ============================================================================= | |
| [interop_in] | |
| ; MS distant → MS local | |
| exten => ___OPERATOR_ID__XXXX,1,NoOp(=== INTER-OP -> GSM local: ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; Opérateur distant → Linphone local | |
| exten => 100,1,NoOp(=== INTER-OP -> Linphone A ===) | |
| same => n,Dial(PJSIP/linphone_A,,rT) | |
| same => n,Hangup() | |
| exten => 200,1,NoOp(=== INTER-OP -> Linphone B ===) | |
| same => n,Dial(PJSIP/linphone_B,,rT) | |
| same => n,Hangup() | |
| ; Refus propre | |
| exten => _X.,1,NoOp(=== INTER-OP -> numéro inconnu: ${EXTEN} ===) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ============================================================================= | |
| ; [interop_out] — Routage sortant vers les opérateurs distants | |
| ; | |
| ; Ce contexte est GÉNÉRÉ et AJOUTÉ en fin de fichier par start.sh. | |
| ; Il contient une extension _NXXXX par opérateur distant (N ≠ __OPERATOR_ID__) | |
| ; qui route vers interop_trunk_opN via PJSIP. | |
| ; | |
| ; Exemple pour Op1 avec 3 opérateurs : | |
| ; exten => _2XXXX → interop_trunk_op2 | |
| ; exten => _3XXXX → interop_trunk_op3 | |
| ; exten => _X. → Congestion | |
| ; ============================================================================= | |
| ! | |
| ! OsmoMGW (1.7.0) configuration saved from vty | |
| !! | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 0 | |
| logging print category-hex 1 | |
| logging print category 0 | |
| logging timestamp 0 | |
| logging print file 1 | |
| logging level rtp notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 notice | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua notice | |
| logging level lmgcp notice | |
| logging level ljibuf notice | |
| logging level lrspro notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| ! | |
| mgcp | |
| domain mgw | |
| bind ip 127.0.0.1 | |
| bind port 2427 | |
| rtp port-range 4002 16001 | |
| rtp bind-ip 127.0.0.1 | |
| rtp ip-probing | |
| rtp keep-alive once | |
| rtcp-omit | |
| rtp-patch ssrc | |
| rtp-patch timestamp | |
| no rtp-patch rfc5993hr | |
| sdp audio-payload number 98 | |
| sdp audio-payload name GSM | |
| sdp audio-payload send-ptime | |
| sdp audio-payload send-name | |
| loop 0 | |
| number endpoints 31 | |
| allow-transcoding | |
| osmux off | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms-routing.conf — Table de routage inter-opérateur SMS | |
| # | |
| # Ce fichier est templaté par start.sh via apply_config_templates() | |
| # Les placeholders __XXX__ sont remplacés au démarrage du container. | |
| # | |
| # Pour ajouter un opérateur : | |
| # 1. Ajouter son IP dans [operators] | |
| # 2. Ajouter les préfixes MSISDN dans [routes] | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| [local] | |
| # ID de cet opérateur (remplacé dynamiquement) | |
| operator_id = __OPERATOR_ID__ | |
| [operators] | |
| # operator_id = container_ip | |
| # Ces IPs correspondent au réseau bridge Docker (172.20.0.0/24) | |
| 1 = 172.20.0.11 | |
| 2 = 172.20.0.12 | |
| # 3 = 172.20.0.13 | |
| [routes] | |
| # prefix = operator_id | |
| # Longest-prefix match : le préfixe le plus long gagne | |
| # | |
| # ── Préfixes MSISDN par opérateur ────────────────────────────────────────── | |
| # Adapter selon ton plan de numérotation | |
| # | |
| # Opérateur 1 : extensions 1xxx et MSISDN +33601xxxxxx | |
| 10000 = 1 | |
| 1001 = 1 | |
| 1002 = 1 | |
| 1003 = 1 | |
| 1004 = 1 | |
| 1005 = 1 | |
| 33601 = 1 | |
| # | |
| # Opérateur 2 : extensions 2xxx et MSISDN +33602xxxxxx | |
| 20000 = 2 | |
| 2001 = 2 | |
| 2002 = 2 | |
| 2003 = 2 | |
| 2004 = 2 | |
| 2005 = 2 | |
| 33602 = 2 | |
| # | |
| # ── Route par défaut (optionnel) ─────────────────────────────────────────── | |
| # Si aucun préfixe ne matche, le SMS n'est pas routé. | |
| # Décommenter pour un fallback : | |
| # * = 1 | |
| ; ============================================================================= | |
| ; pjsip.conf — Asterisk PJSIP — Opérateur __OPERATOR_ID__ | |
| ; IP container : __CONTAINER_IP__ | |
| ; IP inter-net : __INTER_LOCAL_IP__ (réseau gsm-inter, pour trunks inter-op) | |
| ; | |
| ; Ce fichier contient les éléments statiques (transport, softphones, MSC). | |
| ; Les trunks inter-opérateurs [interop-identify-opX] / [interop_trunk_opX] | |
| ; sont GÉNÉRÉS et AJOUTÉS en fin de fichier par start.sh selon N opérateurs. | |
| ; | |
| ; Plan de numérotation : | |
| ; 100 Linphone A (softphone local) | |
| ; 200 Linphone B (softphone local) | |
| ; 600 Echo test | |
| ; NXXXX Abonnés GSM opérateur N (ex: 10001 pour Op1, 20001 pour Op2…) | |
| ; | |
| ; Flux d'appels : | |
| ; MS → Asterisk via MNCC socket (/tmp/msc_mncc) → OsmoMSC → context gsm_in | |
| ; Linphone → Asterisk SIP → context internal | |
| ; Inter-op → trunk SIP sur gsm-inter → context interop_in | |
| ; ============================================================================= | |
| [global] | |
| type=global | |
| user_agent=OsmoAsterisk-Op__OPERATOR_ID__ | |
| ; ============================================================================= | |
| ; TRANSPORT | |
| ; ============================================================================= | |
| [transport-udp] | |
| type=transport | |
| protocol=udp | |
| bind=0.0.0.0:5060 | |
| ; ============================================================================= | |
| ; SOFTPHONE LOCAL A (Linphone / Baresip / autre) | |
| ; SIP account : linphone_A@__CONTAINER_IP__:5060 | Mot de passe : tester | |
| ; Numéro : 100 | |
| ; ============================================================================= | |
| [linphone_A] | |
| type=endpoint | |
| transport=transport-udp | |
| context=internal | |
| disallow=all | |
| allow=ulaw | |
| allow=gsm | |
| auth=linphone_A | |
| aors=linphone_A | |
| direct_media=no | |
| rtp_symmetric=yes | |
| force_rport=yes | |
| rewrite_contact=yes | |
| callerid=Linphone A <100> | |
| [linphone_A] | |
| type=auth | |
| auth_type=userpass | |
| username=linphone_A | |
| password=tester | |
| [linphone_A] | |
| type=aor | |
| max_contacts=1 | |
| remove_existing=yes | |
| qualify_frequency=30 | |
| ; ============================================================================= | |
| ; SOFTPHONE LOCAL B | |
| ; SIP account : linphone_B@__CONTAINER_IP__:5060 | Mot de passe : testerB | |
| ; Numéro : 200 | |
| ; ============================================================================= | |
| [linphone_B] | |
| type=endpoint | |
| transport=transport-udp | |
| context=internal | |
| disallow=all | |
| allow=ulaw | |
| allow=gsm | |
| auth=linphone_B | |
| aors=linphone_B | |
| direct_media=no | |
| rtp_symmetric=yes | |
| force_rport=yes | |
| rewrite_contact=yes | |
| callerid=Linphone B <200> | |
| [linphone_B] | |
| type=auth | |
| auth_type=userpass | |
| username=linphone_B | |
| password=testerB | |
| [linphone_B] | |
| type=aor | |
| max_contacts=1 | |
| remove_existing=yes | |
| qualify_frequency=30 | |
| ; ============================================================================= | |
| ; TRUNK GSM ← OsmoMSC (connexion MNCC/SIP depuis osmo-sip-connector) | |
| ; Accepte les appels entrants de 127.0.0.1:5061 | |
| ; Contexte d'arrivée : gsm_in | |
| ; ============================================================================= | |
| [gsm_msc-identify] | |
| type=identify | |
| endpoint=gsm_msc | |
| match=127.0.0.1 | |
| [gsm_msc] | |
| type=endpoint | |
| transport=transport-udp | |
| context=gsm_in | |
| disallow=all | |
| allow=gsm | |
| allow=ulaw | |
| aors=gsm_msc | |
| direct_media=no | |
| rtp_symmetric=yes | |
| force_rport=yes | |
| rewrite_contact=yes | |
| [gsm_msc] | |
| type=aor | |
| contact=sip:127.0.0.1:5061 | |
| qualify_frequency=10 | |
| ; ============================================================================= | |
| ; TRUNKS INTER-OPÉRATEURS — section générée automatiquement par start.sh | |
| ; | |
| ; Pour chaque opérateur distant (ID = X ≠ __OPERATOR_ID__) : | |
| ; [interop-identify-opX] → identification par IP source (172.20.0.(10+X)) | |
| ; [interop_trunk_opX] → endpoint PJSIP (context=interop_in) | |
| ; [interop_trunk_opX] → AOR (contact=sip:172.20.0.(10+X):5060) | |
| ; | |
| ; Les blocs ci-dessous sont ajoutés en append par start.sh : | |
| ; ============================================================================= | |
| [modules] | |
| autoload = no | |
| ; This is a minimal module load. We are loading only the modules | |
| ; required for the Asterisk features used in the "Super Awesome | |
| ; Company" configuration. | |
| ; Applications | |
| load = app_bridgewait.so | |
| load = app_dial.so | |
| load = app_playback.so | |
| load = app_stack.so | |
| load = app_verbose.so | |
| load = app_voicemail.so | |
| load = app_directory.so | |
| load = app_confbridge.so | |
| load = app_queue.so | |
| ; Bridging | |
| load = bridge_builtin_features.so | |
| load = bridge_builtin_interval_features.so | |
| load = bridge_holding.so | |
| load = bridge_native_rtp.so | |
| load = bridge_simple.so | |
| load = bridge_softmix.so | |
| ; Call Detail Records | |
| load = cdr_custom.so | |
| ; Channel Drivers | |
| load = chan_bridge_media.so | |
| load = chan_pjsip.so | |
| ; Codecs | |
| load = codec_gsm.so | |
| load = codec_resample.so | |
| load = codec_ulaw.so | |
| load = codec_g722.so | |
| ; Formats | |
| load = format_gsm.so | |
| load = format_pcm.so | |
| load = format_wav_gsm.so | |
| load = format_wav.so | |
| ; Functions | |
| load = func_callerid.so | |
| load = func_cdr.so | |
| load = func_pjsip_endpoint.so | |
| load = func_sorcery.so | |
| load = func_devstate.so | |
| load = func_strings.so | |
| ; Core/PBX | |
| load = pbx_config.so | |
| ; Resources | |
| load = res_http_websocket.so | |
| load = res_musiconhold.so | |
| load = res_pjproject.so | |
| load = res_pjsip_acl.so | |
| load = res_pjsip_authenticator_digest.so | |
| load = res_pjsip_caller_id.so | |
| load = res_pjsip_dialog_info_body_generator.so | |
| load = res_pjsip_diversion.so | |
| load = res_pjsip_dtmf_info.so | |
| load = res_pjsip_endpoint_identifier_anonymous.so | |
| load = res_pjsip_endpoint_identifier_ip.so | |
| load = res_pjsip_endpoint_identifier_user.so | |
| load = res_pjsip_exten_state.so | |
| load = res_pjsip_header_funcs.so | |
| load = res_pjsip_logger.so | |
| load = res_pjsip_messaging.so | |
| load = res_pjsip_mwi_body_generator.so | |
| load = res_pjsip_mwi.so | |
| load = res_pjsip_nat.so | |
| load = res_pjsip_notify.so | |
| load = res_pjsip_one_touch_record_info.so | |
| load = res_pjsip_outbound_authenticator_digest.so | |
| load = res_pjsip_outbound_publish.so | |
| load = res_pjsip_outbound_registration.so | |
| load = res_pjsip_path.so | |
| load = res_pjsip_pidf_body_generator.so | |
| load = res_pjsip_pidf_digium_body_supplement.so | |
| load = res_pjsip_pidf_eyebeam_body_supplement.so | |
| load = res_pjsip_publish_asterisk.so | |
| load = res_pjsip_pubsub.so | |
| load = res_pjsip_refer.so | |
| load = res_pjsip_registrar.so | |
| load = res_pjsip_rfc3326.so | |
| load = res_pjsip_sdp_rtp.so | |
| load = res_pjsip_send_to_voicemail.so | |
| load = res_pjsip_session.so | |
| load = res_pjsip.so | |
| load = res_pjsip_t38.so | |
| load = res_pjsip_transport_websocket.so | |
| load = res_pjsip_xpidf_body_generator.so | |
| load = res_rtp_asterisk.so | |
| load = res_sorcery_astdb.so | |
| load = res_sorcery_config.so | |
| load = res_sorcery_memory.so | |
| load = res_sorcery_realtime.so | |
| load = res_timing_timerfd.so | |
| ; Do not load res_hep and kin unless you are using HEP monitoring | |
| ; <http://sipcapture.org> in your network. | |
| noload = res_hep.so | |
| noload = res_hep_pjsip.so | |
| noload = res_hep_rtcp.so | |
| load = chan_iax2.so | |
| ! | |
| ! Osmocom SGSN configuration | |
| ! | |
| log stderr | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging timestamp 0 | |
| logging print file basename last | |
| logging print level 1 | |
| line vty | |
| no login | |
| ! | |
| sgsn | |
| gtp state-dir /var/lib/osmocom/osmo-sgsn | |
| gtp local-ip 127.0.0.1 | |
| ggsn 0 remote-ip __CONTAINER_IP__ | |
| ggsn 0 gtp-version 1 | |
| ggsn 0 echo-interval 60 | |
| authentication optional | |
| auth-policy remote | |
| gsup remote-ip __HLR_IP__ | |
| gsup remote-port 4222 | |
| ! | |
| ns | |
| timer tns-block 3 | |
| timer tns-block-retries 3 | |
| timer tns-reset 3 | |
| timer tns-reset-retries 3 | |
| timer tns-test 30 | |
| timer tns-alive 3 | |
| timer tns-alive-retries 10 | |
| bind udp local | |
| listen __CONTAINER_IP__ 23000 | |
| accept-ipaccess | |
| ! | |
| bssgp | |
| ! | |
| ! OsmoBTS (192.168.1.15-bc49-dirty) configuration saved from vty | |
| !! | |
| ! | |
| log stderr | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging timestamp 0 | |
| logging print file basename last | |
| logging print level 1 | |
| ! | |
| log gsmtap __GATEWAY_IP__ | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging print level 1 | |
| logging level rsl info | |
| logging level oml info | |
| logging level rll notice | |
| logging level rr notice | |
| logging level meas notice | |
| logging level pag info | |
| logging level l1c info | |
| logging level l1p info | |
| logging level dsp error | |
| logging level pcu notice | |
| logging level ho debug | |
| logging level trx notice | |
| logging level loop notice | |
| logging level abis debug | |
| logging level rtp notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats error | |
| ! | |
| line vty | |
| no login | |
| ! | |
| e1_input | |
| e1_line 0 driver ipa | |
| e1_line 0 port 0 | |
| no e1_line 0 keepalive | |
| ! | |
| phy 0 | |
| instance 0 | |
| osmotrx rx-gain 40 | |
| osmotrx tx-attenuation 50 | |
| osmotrx ip local 127.0.0.1 | |
| osmotrx ip remote 127.0.0.1 | |
| no osmotrx timing-advance-loop | |
| bts 0 | |
| band DCS1800 | |
| ipa unit-id __IPA_UNIT_ID__ 0 | |
| oml remote-ip 127.0.0.1 | |
| rtp jitter-buffer 100 | |
| paging queue-size 200 | |
| paging lifetime 0 | |
| min-qual-rach 50 | |
| min-qual-norm -5 | |
| gsmtap-sapi ccch | |
| gsmtap-sapi bcch | |
| gsmtap-sapi rach | |
| gsmtap-sapi agch | |
| gsmtap-sapi pch | |
| gsmtap-sapi sdcch | |
| gsmtap-sapi sacch | |
| trx 0 | |
| power-ramp max-initial 23000 mdBm | |
| power-ramp step-size 2000 mdB | |
| power-ramp step-interval 1 | |
| ms-power-control osmo | |
| phy 0 instance 0 | |
| # mobile.cfg.template - avec placeholders | |
| # KI format : "ki comp128 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX" | |
| # 16 octets séparés par espaces | |
| # Octet 15 = ms_idx, Octet 16 = op_id | |
| ! | |
| line vty | |
| no login | |
| bind 127.0.0.1 4247 | |
| ! | |
| gps device /dev/ttyACM0 | |
| gps baudrate default | |
| no gps enable | |
| ! | |
| no hide-default | |
| ! | |
| ! | |
| log gsmtap 127.0.0.1 | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging level mm debug | |
| logging level rr debug | |
| logging level cc debug | |
| logging level sms debug | |
| ! | |
| ms 1 | |
| layer2-socket /tmp/osmocom_l2 | |
| sim test | |
| network-selection-mode auto | |
| imei __IMEI__ 0 | |
| imei-fixed | |
| no emergency-imsi | |
| sms-service-center __SMS_SC__ | |
| no call-waiting | |
| no auto-answer | |
| no force-rekey | |
| no clip | |
| no clir | |
| tx-power auto | |
| no simulated-delay | |
| stick __ARFCN__ | |
| location-updating | |
| neighbour-measurement | |
| codec full-speed prefer | |
| codec half-speed | |
| no abbrev | |
| support | |
| sms | |
| a5/1 | |
| a5/2 | |
| p-gsm | |
| e-gsm | |
| r-gsm | |
| no gsm-850 | |
| dcs | |
| no pcs | |
| class-900 4 | |
| class-850 4 | |
| class-dcs 1 | |
| class-pcs 1 | |
| channel-capability sdcch+tchf+tchh | |
| full-speech-v1 | |
| full-speech-v2 | |
| half-speech-v1 | |
| min-rxlev -106 | |
| dsc-max 90 | |
| no skip-max-per-band | |
| test-sim | |
| imsi __IMSI__ | |
| ki comp128 __KI__ | |
| no barred-access | |
| rplmn __MCC__ __MNC__ | |
| no shutdown | |
| ! | |
| ! GSMTAP configuration | |
| ! | |
| gsmtap | |
| remote-host 127.0.0.1 | |
| no local-host | |
| lchan sacch | |
| lchan lsacch | |
| lchan sacch/4 | |
| lchan sacch/8 | |
| lchan sacch/f | |
| lchan sacch/h | |
| lchan unknown | |
| lchan bcch | |
| lchan ccch | |
| lchan rach | |
| lchan agch | |
| lchan pch | |
| lchan sdcch | |
| lchan sdcch/4 | |
| lchan sdcch/8 | |
| lchan facch/f | |
| lchan facch/h | |
| lchan pacch | |
| lchan cbch | |
| lchan pdch | |
| lchan pttch | |
| lchan tch/f | |
| lchan tch/h | |
| category gprs dl-unknown | |
| category gprs dl-dummy | |
| category gprs dl-ctrl | |
| category gprs dl-data-gprs | |
| category gprs dl-data-egprs | |
| category gprs ul-unknown | |
| category gprs ul-dummy | |
| category gprs ul-ctrl | |
| category gprs ul-data-gprs | |
| category gprs ul-data-egprs | |
| end | |
| ! | |
| ! OpenGGSN configuration | |
| ! | |
| log stderr | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging timestamp 0 | |
| logging print file basename last | |
| logging print level 1 | |
| logging level ip info | |
| logging level tun info | |
| logging level ggsn info | |
| logging level sgsn notice | |
| logging level icmp6 notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp info | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 notice | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua notice | |
| logging level lmgcp notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| ggsn ggsn0 | |
| gtp state-dir /var/lib/osmocom/osmo-ggsn | |
| gtp bind-ip __CONTAINER_IP__ | |
| apn internet | |
| gtpu-mode tun | |
| tun-device apn0 | |
| type-support v4 | |
| ip prefix dynamic 176.16.32.0/24 | |
| ip dns 0 __GATEWAY_IP__ | |
| ip dns 1 __CONTAINER_IP__ | |
| ip ifconfig 176.16.32.0/24 | |
| no shutdown | |
| default-apn internet | |
| no shutdown ggsn | |
| ! | |
| ! osmo-stp.cfg — Signal Transfer Point opérateur __OPERATOR_ID__ | |
| ! | |
| ! Architecture : | |
| ! [BSC __PC_BSC__] ─►┐ | |
| ! ├─ STP __PC_STP__ @ 127.0.0.1:2905 (local) | |
| ! [MSC __PC_MSC__] ─►┘ │ | |
| ! │ asp client → inter-STP __INTER_STP_IP__:2908 | |
| ! ▼ | |
| ! [Inter-STP 0.23.0] | |
| ! | |
| ! BSC/MSC se connectent au STP via 127.0.0.1 (même container). | |
| ! Le STP écoute sur 0.0.0.0:2905 pour accepter connexions locales ET | |
| ! éviter les race conditions Docker (l'IP réseau privé n'existe pas | |
| ! encore au moment du démarrage des services). | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging print extended-timestamp 1 | |
| logging print level 1 | |
| logging print file basename | |
| logging level lss7 info | |
| logging level lsccp info | |
| logging level lm3ua info | |
| logging level linp notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| cs7 instance 0 | |
| network-indicator international | |
| point-code __PC_STP__ | |
| ! | |
| ! ── Listener local ────────────────────────────────────────────────────── | |
| ! 0.0.0.0 : accepte les connexions sur toutes les interfaces | |
| ! → fonctionne immédiatement, avant même que Docker attache le réseau privé | |
| ! BSC/MSC se connectent via 127.0.0.1 | |
| xua rkm routing-key-allocation dynamic-permitted | |
| listen m3ua 2905 | |
| accept-asp-connections dynamic-permitted | |
| local-ip 127.0.0.1 | |
| local-ip 127.0.0.2 | |
| ! | |
| ! ── Lien client vers l'inter-STP central ──────────────────────────────── | |
| asp asp-to-inter 2908 2910 m3ua | |
| remote-ip __INTER_STP_IP__ | |
| local-ip __INTER_LOCAL_IP__ | |
| role asp | |
| sctp-role client | |
| __INTER_STP_SHUTDOWN__ | |
| ! | |
| as as-inter m3ua | |
| asp asp-to-inter | |
| routing-key __RCTX_INTER__ __PC_STP__ | |
| traffic-mode override | |
| ! | |
| ! ── Routage ───────────────────────────────────────────────────────────── | |
| ! Routes locales (BSC/MSC) : créées dynamiquement par accept-asp. | |
| ! Catch-all : tout le reste → inter-STP. | |
| route-table system | |
| update route 0.0.0 0.0.0 linkset as-inter | |
| log gsmtap __GATEWAY_IP__ | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging print level 1 | |
| ! | |
| pcu | |
| flow-control-interval 10 | |
| cs 2 | |
| alloc-algorithm dynamic | |
| alpha 0 | |
| gamma 0 | |
| #!/bin/bash | |
| # start.sh — Lance la stack Osmocom GSM multi-opérateurs | |
| # | |
| # Modes : net-host (1 opérateur, SDR physique) | bridge (N opérateurs SS7 inter-op) | |
| # | |
| # Topologie SS7 en mode bridge (exemple 3 opérateurs) : | |
| # | |
| # ┌──────────── Inter-STP (PC 0.23.0) ──────────────┐ | |
| # │ 172.20.0.10:2908 (dynamic-permitted) │ | |
| # └─────────────────────────────────────────────────┘ | |
| # ▲ RCTX 150 ▲ RCTX 250 ▲ RCTX 350 | |
| # ┌──────────┴───┐ ┌──────┴───────┐ ┌──┴────────────┐ | |
| # │ Op1:172.20.0.11│ │Op2:172.20.0.12│ │Op3:172.20.0.13│ | |
| # │ STP 1.23.2 │ │ STP 2.23.2 │ │ STP 3.23.2 │ | |
| # │ MSC 1.23.1 │ │ MSC 2.23.1 │ │ MSC 3.23.1 │ | |
| # │ BSC 1.23.3 │ │ BSC 2.23.3 │ │ BSC 3.23.3 │ | |
| # └───────────────┘ └──────────────┘ └──────────────┘ | |
| # | |
| # Réseau backbone : gsm-inter (172.20.0.0/24) | |
| # Réseau privé : gsm-net-opN (172.20.N.0/24) | |
| # Intra-container : 127.0.0.1 (MSC/BSC → STP local) | |
| # Inter-container : 172.20.0.X (STP → inter-STP) | |
| set -e | |
| # DEBUG mode | |
| if [[ -n "$DEBUG" ]]; then | |
| set -x | |
| PS4='[DEBUG] + ${BASH_SOURCE}:${LINENO}: ' | |
| echo "=== MODE DEBUG ACTIVÉ ===" | |
| echo "" | |
| fi | |
| IMAGE_BASE="osmocom-nitb" | |
| IMAGE_RUN="osmocom-run" | |
| INTER_NET="gsm-inter" | |
| INTER_NET_SUBNET="172.20.0.0/24" | |
| INTER_NET_GATEWAY="172.20.0.1" | |
| 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 — chargement du module dédié ─────────────────────────────────── | |
| SMS_ROUTING_SCRIPT="$(dirname "$0")/scripts/sms-routing-setup.sh" | |
| if [ -f "$SMS_ROUTING_SCRIPT" ]; then | |
| # shellcheck source=scripts/sms-routing-setup.sh | |
| source "$SMS_ROUTING_SCRIPT" | |
| else | |
| echo -e "${YELLOW}[WARN] sms-routing-setup.sh introuvable — routing SMS basique${NC}" >&2 | |
| fi | |
| # Répertoire global pour les configs SMS (initialisé dans start_bridge_mode / | |
| # start_host_mode, utilisé par build_vol_args) | |
| SMS_ROUTING_DIR="" | |
| # ── Helpers ──────────────────────────────────────────────────────────────────── | |
| op_backbone_ip() { echo "172.20.0.$((10 + $1))"; } | |
| op_private_ip() { echo "172.20.$1.10"; } | |
| op_private_gw() { echo "172.20.$1.1"; } | |
| op_private_net() { echo "172.20.$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 )); } | |
| banner() { | |
| echo -e "${CYAN}" | |
| echo "╔══════════════════════════════════════════════════════╗" | |
| echo "║ Osmocom GSM/EGPRS Virtual Network ║" | |
| echo "║ Multi-Operator SS7 + osmo-gapk Audio ║" | |
| echo "╚══════════════════════════════════════════════════════╝" | |
| echo -e "${NC}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # ALSA — construit les arguments Docker pour le passthrough audio | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| build_alsa_args() { | |
| local alsa_args="" | |
| local alsa_card="${ALSA_CARD:-default}" | |
| local target_user="${SUDO_USER:-$(logname 2>/dev/null || echo root)}" | |
| if [ -d /dev/snd ]; then | |
| alsa_args="--device /dev/snd" | |
| if getent group audio &>/dev/null; then | |
| alsa_args="${alsa_args} --group-add $(getent group audio | cut -d: -f3)" | |
| fi | |
| echo -e " ALSA : ${GREEN}/dev/snd passthrough${NC} (carte: ${CYAN}${alsa_card}${NC})" >&2 | |
| else | |
| echo -e " ALSA : ${YELLOW}absent${NC} (gapk en mode RTP/fichier uniquement)" >&2 | |
| fi | |
| local asoundrc="/home/${target_user}/.asoundrc" | |
| if [ -f "$asoundrc" ]; then | |
| alsa_args="${alsa_args} -v ${asoundrc}:/root/.asoundrc:ro" | |
| fi | |
| local pulse_socket="/run/user/$(id -u "${target_user}" 2>/dev/null || echo 1000)/pulse/native" | |
| if [ -S "$pulse_socket" ]; then | |
| alsa_args="${alsa_args} -v ${pulse_socket}:${pulse_socket}" | |
| alsa_args="${alsa_args} -e PULSE_SERVER=unix:${pulse_socket}" | |
| echo -e " PulseAudio: ${GREEN}socket disponible${NC}" >&2 | |
| fi | |
| alsa_args="${alsa_args} -e ALSA_CARD=${alsa_card} -e GAPK_ALSA_DEV=${alsa_card}" | |
| echo "$alsa_args" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Build | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| build_run_image() { | |
| echo -e "${GREEN}Build de l'image run (Dockerfile.run)...${NC}" | |
| docker build -f Dockerfile.run -t "$IMAGE_RUN" . | |
| echo -e "${GREEN}Image '$IMAGE_RUN' prête.${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 | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Génération dynamique — pjsip interop trunks | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| generate_pjsip_interop_trunks() { | |
| local op_id=$1 | |
| local n_operators=$2 | |
| for remote_op in $(seq 1 "$n_operators"); do | |
| [ "$remote_op" -eq "$op_id" ] && continue | |
| local remote_ip | |
| remote_ip=$(op_backbone_ip "$remote_op") | |
| cat <<EOF | |
| ; ── Trunk inter-op → Opérateur ${remote_op} (${remote_ip}) ────────────────────────────── | |
| [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 | |
| rewrite_contact=yes | |
| from_user=interop_op${op_id} | |
| [interop_trunk_op${remote_op}] | |
| type=aor | |
| contact=sip:${remote_ip}:5060 | |
| qualify_frequency=15 | |
| qualify_timeout=5.0 | |
| EOF | |
| done | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Génération dynamique — contexte [interop_out] du dialplan Asterisk | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| generate_extensions_interop_out() { | |
| local op_id=$1 | |
| local n_operators=$2 | |
| cat <<'EOF' | |
| ; ============================================================================= | |
| ; [interop_out] — Sortie vers un opérateur distant (SIP trunk inter-op) | |
| ; ============================================================================= | |
| [interop_out] | |
| EOF | |
| for remote_op in $(seq 1 "$n_operators"); do | |
| [ "$remote_op" -eq "$op_id" ] && continue | |
| cat <<EOF | |
| ; → Opérateur ${remote_op} | |
| exten => _${remote_op}XXXX,1,NoOp(=== INTEROP OUT Op${remote_op}: \${EXTEN} ===) | |
| same => n,Dial(PJSIP/\${EXTEN}@interop_trunk_op${remote_op},,rT) | |
| same => n,NoOp(Échec inter-op: \${DIALSTATUS}) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| EOF | |
| done | |
| cat <<'EOF' | |
| ; Destination inconnue → congestion | |
| exten => _X.,1,NoOp(=== INTEROP OUT: destination inconnue ${EXTEN} ===) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| EOF | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Génération dynamique — sms-routing.conf | |
| # | |
| # Délégué à scripts/sms-routing-setup.sh (sms_routing_generate_all). | |
| # Cette fonction de secours est utilisée uniquement si le module est absent. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| _generate_sms_routing_conf_fallback() { | |
| local op_id=$1 n_operators=$2 | |
| echo -e " ${YELLOW}[SMS] Fallback basique op${op_id} (sms-routing-setup.sh absent)${NC}" >&2 | |
| printf '# sms-routing.conf — Fallback (routes courtes)\n\n[local]\noperator_id = %s\nsc_address = 1999001%s444\n\n[operators]\n' \ | |
| "$op_id" "$op_id" | |
| for i in $(seq 1 "$n_operators"); do | |
| printf '%s = %s\n' "$i" "$(op_backbone_ip "$i")" | |
| done | |
| printf '\n[routes]\n' | |
| for i in $(seq 1 "$n_operators"); do | |
| printf '%s0000 = %s\n' "$i" "$i" | |
| for j in 001 002 003 004 005; do printf '%s%s = %s\n' "$i" "$j" "$i"; done | |
| printf '336%s0 = %s\n\n' "$i" "$i" | |
| done | |
| printf '[relay]\nport = 7890\nconnect_timeout = 10\nretry_count = 3\nretry_delay = 5\n' | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Substitution des placeholders | |
| # | |
| # Paramètres : | |
| # $1 dest Répertoire de sortie temporaire | |
| # $2 container_ip IP privée (172.20.N.10) | |
| # $3 gateway_ip Passerelle réseau privé (172.20.N.1) | |
| # $4 op_id Identifiant opérateur (1…N) | |
| # $5 pc_msc Point code MSC (N.23.1) | |
| # $6 pc_stp Point code STP (N.23.2) | |
| # $7 pc_bsc Point code BSC (N.23.3) | |
| # $8 mcc Mobile Country Code (ex: 001) | |
| # $9 mnc Mobile Network Code (ex: 01) | |
| # $10 op_name Nom opérateur | |
| # $11 inter_stp IP inter-STP (172.20.0.10 en bridge) | |
| # $12 inter_stp_shutdown "no shutdown" ou "shutdown" | |
| # $13 n_operators Nombre total d'opérateurs | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| apply_config_templates() { | |
| local dest=$1 | |
| local container_ip=$2 | |
| local gateway_ip=$3 | |
| local op_id=$4 | |
| local pc_msc=$5 pc_stp=$6 pc_bsc=$7 | |
| local mcc=$8 mnc=$9 op_name=${10} | |
| local inter_stp=${11} | |
| local inter_stp_shutdown=${12} | |
| local n_operators=${13} | |
| mkdir -p "$dest/osmocom" "$dest/asterisk" "$dest/bb" | |
| for f in configs/*.cfg; do | |
| [ "$(basename "$f")" = "osmo-stp-interop.cfg" ] && continue | |
| cp "$f" "$dest/osmocom/" | |
| done | |
| for f in configs/*.conf; do | |
| local bn | |
| bn=$(basename "$f") | |
| [ "$bn" = "sms-routing.conf" ] && continue | |
| cp "$f" "$dest/asterisk/" | |
| done | |
| for s in entrypoint.sh osmo-start.sh status.sh run.sh gapk-start.sh; do | |
| [ -f "scripts/$s" ] && cp "scripts/$s" "$dest/osmocom/$s" && chmod +x "$dest/osmocom/$s" | |
| done | |
| if [ -f "configs/mobile.cfg.template" ]; then | |
| cp "configs/mobile.cfg.template" "$dest/bb/mobile.cfg" | |
| elif [ -f "configs/mobile.cfg" ]; then | |
| cp "configs/mobile.cfg" "$dest/bb/mobile.cfg" | |
| fi | |
| local rctx_msc rctx_stp rctx_bsc rctx_inter | |
| rctx_msc=$(op_rctx_msc "$op_id") | |
| rctx_stp=$(op_rctx_stp "$op_id") | |
| rctx_bsc=$(op_rctx_bsc "$op_id") | |
| rctx_inter=$(op_rctx_inter "$op_id") | |
| local arfcn=$(( 512 + op_id * 2 )) | |
| local ipa_unit_id=$(( 6000 + op_id )) | |
| local cell_id=$(( 6000 + op_id )) | |
| local bsic=$(( 60 + op_id )) | |
| local bvci=$(( op_id * 10 + 2 )) | |
| local nsei=$(( op_id * 10 )) | |
| local nsvci=$(( op_id * 10 )) | |
| local imsi="${mcc}${mnc}$(printf '%010d' "${op_id}")" | |
| local imei="3589250059$(printf '%04d' "${op_id}")0" | |
| local ki="00 11 22 33 44 55 66 77 88 99 aa bb cc dd $(printf '%02x' "${op_id}") ff" | |
| local sms_sc="+336661234$(printf '%04d' "${op_id}")" | |
| local inter_local_ip | |
| inter_local_ip=$(op_backbone_ip "$op_id") | |
| for f in "$dest/osmocom"/*.cfg "$dest/asterisk"/*.conf "$dest/bb"/*.cfg; do | |
| [ -f "$f" ] || continue | |
| sed -i \ | |
| -e "s|__CONTAINER_IP__|${container_ip}|g" \ | |
| -e "s|__GATEWAY_IP__|${gateway_ip}|g" \ | |
| -e "s|__HLR_IP__|127.0.0.2|g" \ | |
| -e "s|__INTER_STP_IP__|${inter_stp}|g" \ | |
| -e "s|__INTER_STP_SHUTDOWN__|${inter_stp_shutdown}|g" \ | |
| -e "s|__INTER_LOCAL_IP__|${inter_local_ip}|g" \ | |
| -e "s|__OPERATOR_ID__|${op_id}|g" \ | |
| -e "s|__PC_MSC__|${pc_msc}|g" \ | |
| -e "s|__PC_STP__|${pc_stp}|g" \ | |
| -e "s|__PC_BSC__|${pc_bsc}|g" \ | |
| -e "s|__RCTX_MSC__|${rctx_msc}|g" \ | |
| -e "s|__RCTX_STP__|${rctx_stp}|g" \ | |
| -e "s|__RCTX_BSC__|${rctx_bsc}|g" \ | |
| -e "s|__RCTX_INTER__|${rctx_inter}|g" \ | |
| -e "s|__MCC__|${mcc}|g" \ | |
| -e "s|__MNC__|${mnc}|g" \ | |
| -e "s|__OP_NAME__|${op_name}|g" \ | |
| -e "s|__ARFCN__|${arfcn}|g" \ | |
| -e "s|__IPA_UNIT_ID__|${ipa_unit_id}|g" \ | |
| -e "s|__CELL_ID__|${cell_id}|g" \ | |
| -e "s|__BSIC__|${bsic}|g" \ | |
| -e "s|__BVCI__|${bvci}|g" \ | |
| -e "s|__NSEI__|${nsei}|g" \ | |
| -e "s|__NSVCI__|${nsvci}|g" \ | |
| -e "s|__IMSI__|${imsi}|g" \ | |
| -e "s|__IMEI__|${imei}|g" \ | |
| -e "s|__KI__|${ki}|g" \ | |
| -e "s|__SMS_SC__|${sms_sc}|g" \ | |
| "$f" | |
| done | |
| generate_pjsip_interop_trunks "$op_id" "$n_operators" \ | |
| >> "$dest/asterisk/pjsip.conf" | |
| generate_extensions_interop_out "$op_id" "$n_operators" \ | |
| >> "$dest/asterisk/extensions.conf" | |
| # ── SMS Routing ───────────────────────────────────────────────────────── | |
| # Si sms_routing_generate (module externe) est disponible ET que | |
| # SMS_ROUTING_DIR est défini, la config a déjà été générée globalement : | |
| # on la copie simplement dans le tmpdir de cet opérateur. | |
| # Sinon : fallback inline basique. | |
| if declare -f sms_routing_generate > /dev/null 2>&1 && \ | |
| [ -n "${SMS_ROUTING_DIR:-}" ] && \ | |
| [ -f "${SMS_ROUTING_DIR}/sms-routing-op${op_id}.conf" ]; then | |
| cp "${SMS_ROUTING_DIR}/sms-routing-op${op_id}.conf" \ | |
| "$dest/osmocom/sms-routing.conf" | |
| else | |
| _generate_sms_routing_conf_fallback "$op_id" "$n_operators" \ | |
| > "$dest/osmocom/sms-routing.conf" | |
| fi | |
| } | |
| build_vol_args() { | |
| local tmpdir=$1 | |
| local vol_args="" | |
| for f in "$tmpdir/osmocom"/*.cfg "$tmpdir/osmocom"/*.sh "$tmpdir/osmocom"/*.conf; do | |
| [ -f "$f" ] || continue | |
| vol_args="$vol_args -v $f:/etc/osmocom/$(basename "$f")" | |
| done | |
| 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 | |
| echo "$vol_args" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # TUN hôte | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| prepare_host_tun() { | |
| echo -e "${GREEN}[*] Configuration TUN sur l'hôte...${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 — hub SS7 central | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| start_inter_stp() { | |
| local n_operators=$1 | |
| local tmpdir | |
| tmpdir=$(mktemp -d) | |
| local inter_cfg="${tmpdir}/osmo-stp-interop.cfg" | |
| echo -e "${GREEN}Génération config inter-STP (${n_operators} opérateurs)...${NC}" | |
| bash ./create_interop.sh "$n_operators" "$inter_cfg" > /dev/null | |
| if [ ! -f "$inter_cfg" ]; then | |
| echo -e "${RED}Échec génération config inter-STP${NC}"; exit 1 | |
| fi | |
| echo -e "${GREEN}Lancement inter-STP @ ${INTER_STP_IP}:2908 (PC 0.23.0)...${NC}" | |
| docker rm -f "$INTER_STP_CONTAINER" &>/dev/null || true | |
| docker run -d \ | |
| --name "$INTER_STP_CONTAINER" \ | |
| --network "$INTER_NET" \ | |
| --ip "$INTER_STP_IP" \ | |
| --cap-add NET_ADMIN \ | |
| -v "${inter_cfg}:/etc/osmocom/osmo-stp-interop.cfg:ro" \ | |
| --entrypoint bash \ | |
| "$IMAGE_RUN" \ | |
| -c "exec sleep infinity" > /dev/null | |
| 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" | |
| echo -ne "${GREEN}[*] Attente démarrage 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}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # [PATCH] wait_inter_stp_ready — Attendre que l'inter-STP soit prêt | |
| # | |
| # osmo-stp démarre rapidement (~1-2s). On attend 15s pour être sûr. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| wait_inter_stp_ready() { | |
| local n_operators=$1 | |
| echo -ne "${GREEN}[*] Vérification stabilisation inter-STP (routes SS7)${NC}" | |
| # osmo-stp crée les routes rapidement | |
| # Attendre 15s pour stabilisation complète | |
| for i in {1..15}; do | |
| sleep 1 | |
| echo -n "." | |
| done | |
| echo -e " ${GREEN}✓${NC}" | |
| return 0 | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Démarrage d'un opérateur | |
| # | |
| # Paramètres : | |
| # $1 op_id | |
| # $2 mcc | |
| # $3 mnc | |
| # $4 op_name | |
| # $5 n_operators Nombre total d'opérateurs | |
| # $6 n_ms Nombre de Mobile Stations pour cet opérateur (défaut: 1) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| start_operator() { | |
| local op_id=$1 mcc=$2 mnc=$3 op_name=$4 n_operators=$5 n_ms=${6:-1} | |
| local container_name net_name subnet gateway container_ip inter_local_ip | |
| container_name=$(op_container "$op_id") | |
| net_name="gsm-net-op${op_id}" | |
| subnet=$(op_private_net "$op_id") | |
| gateway=$(op_private_gw "$op_id") | |
| container_ip=$(op_private_ip "$op_id") | |
| inter_local_ip=$(op_backbone_ip "$op_id") | |
| local rctx_inter | |
| rctx_inter=$(op_rctx_inter "$op_id") | |
| echo -e "${CYAN}── Opérateur ${op_id} : ${op_name} (MCC=${mcc} MNC=${mnc}) ──${NC}" | |
| echo -e " Backbone : ${CYAN}${inter_local_ip}${NC} Privé : ${CYAN}${container_ip}${NC}" | |
| echo -e " STP PC : ${op_id}.23.2 RCTX inter : ${rctx_inter} MS : ${CYAN}${n_ms}${NC}" | |
| # Réseau privé par opérateur | |
| docker network inspect "$net_name" &>/dev/null || \ | |
| docker network create --subnet="$subnet" --gateway="$gateway" "$net_name" &>/dev/null | |
| # Génération des configs | |
| local tmpdir | |
| tmpdir=$(mktemp -d) | |
| apply_config_templates "$tmpdir" \ | |
| "$container_ip" "$gateway" \ | |
| "$op_id" "${op_id}.23.1" "${op_id}.23.2" "${op_id}.23.3" \ | |
| "$mcc" "$mnc" "$op_name" \ | |
| "$INTER_STP_IP" "no shutdown" \ | |
| "$n_operators" | |
| local vol_args | |
| vol_args=$(build_vol_args "$tmpdir") | |
| local alsa_args | |
| alsa_args=$(build_alsa_args) | |
| docker rm -f "$container_name" &>/dev/null || true | |
| # shellcheck disable=SC2086 | |
| docker run -d \ | |
| --name "$container_name" \ | |
| --network "$INTER_NET" \ | |
| --ip "$inter_local_ip" \ | |
| --cap-add NET_ADMIN \ | |
| --cap-add SYS_ADMIN \ | |
| --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 \ | |
| -e OPERATOR_ID="$op_id" \ | |
| -e N_MS="$n_ms" \ | |
| -e CONTAINER_IP="$container_ip" \ | |
| -e GATEWAY_IP="$gateway" \ | |
| -e INTER_STP_IP="$INTER_STP_IP" \ | |
| $vol_args \ | |
| "$IMAGE_RUN" \ | |
| /etc/osmocom/run.sh > /dev/null | |
| # Attacher le réseau privé (après démarrage — évite la race condition) | |
| docker network connect --ip "$container_ip" "$net_name" "$container_name" | |
| echo -e " ${GREEN}✓${NC} ${container_name} démarré (${n_ms} MS)" | |
| # Lancer l'orchestrateur tmux en background avec délai. | |
| # | |
| # /etc/osmocom/run.sh (CMD docker run) = boot systemd + services Osmocom. | |
| # /root/run.sh = orchestrateur tmux (notre script). | |
| # On attend 8s que systemd ait stabilisé les services Osmocom avant tmux. | |
| local _cname="$container_name" | |
| local _opid="$op_id" | |
| local _nms="$n_ms" | |
| ( | |
| sleep 8 | |
| docker exec -d "$_cname" bash -c \ | |
| "OPERATOR_ID=${_opid} N_MS=${_nms} /root/run.sh \ | |
| >> /var/log/osmocom/run-orchestrator.log 2>&1" | |
| ) & | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Mode net-host (1 opérateur, SDR physique) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| start_host_mode() { | |
| echo -e "${GREEN}Démarrage en mode net-host (1 opérateur)...${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 détecter l'IP hôte.${NC}"; exit 1; } | |
| echo -e " IP hôte : ${CYAN}${src_ip}${NC} GW : ${CYAN}${gw_ip}${NC}" | |
| local n_ms | |
| read -rp "Nombre de MS [1] : " n_ms | |
| n_ms=${n_ms:-1} | |
| if ! [[ "$n_ms" =~ ^[0-9]+$ ]] || [ "$n_ms" -lt 1 ] || [ "$n_ms" -gt 9 ]; then | |
| echo -e "${YELLOW}Valeur invalide, forcée à 1${NC}" | |
| n_ms=1 | |
| fi | |
| prepare_host_tun | |
| docker rm -f egprs &>/dev/null || true | |
| # ── Génération config SMS routing (mode host, 1 opérateur) ──────────── | |
| SMS_ROUTING_DIR=$(mktemp -d) | |
| if declare -f sms_routing_generate > /dev/null 2>&1; then | |
| echo -e "${GREEN}Génération table SMS routing (1 op, ${n_ms} MS)...${NC}" | |
| sms_routing_generate 1 1 "$SMS_ROUTING_DIR" "$n_ms" | |
| sms_routing_summary 1 "$n_ms" | |
| fi | |
| local tmpdir | |
| tmpdir=$(mktemp -d) | |
| apply_config_templates "$tmpdir" \ | |
| "$src_ip" "$gw_ip" \ | |
| "1" "1.23.1" "1.23.2" "1.23.3" \ | |
| "001" "01" "OsmoGSM" \ | |
| "127.0.0.1" "shutdown" \ | |
| "1" | |
| local vol_args | |
| vol_args=$(build_vol_args "$tmpdir") | |
| local alsa_args | |
| alsa_args=$(build_alsa_args) | |
| # shellcheck disable=SC2086 | |
| docker run -d \ | |
| --rm \ | |
| --name egprs \ | |
| --net host \ | |
| --cap-add NET_ADMIN \ | |
| --cap-add SYS_ADMIN \ | |
| --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 \ | |
| -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" \ | |
| $vol_args \ | |
| "$IMAGE_RUN" \ | |
| /etc/osmocom/run.sh > /dev/null | |
| echo -e "${GREEN}[*] Attente démarrage (5 s)...${NC}" | |
| sleep 5 | |
| docker exec -it egprs /bin/bash -c "/root/run.sh" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Mode bridge (N opérateurs avec inter-STP central) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| start_bridge_mode() { | |
| # ── Saisie du nombre d'opérateurs ────────────────────────────────────── | |
| local n_operators | |
| read -rp "Nombre d'opérateurs [2] : " n_operators | |
| n_operators=${n_operators:-2} | |
| if ! [[ "$n_operators" =~ ^[0-9]+$ ]] || [ "$n_operators" -lt 1 ] || [ "$n_operators" -gt 9 ]; then | |
| echo -e "${RED}Nombre invalide (1–9).${NC}"; exit 1 | |
| fi | |
| # ── Saisie des paramètres par opérateur (MCC / MNC / Nom / MS count) ── | |
| declare -A OP_MCC OP_MNC OP_NAME OP_MS | |
| for i in $(seq 1 "$n_operators"); do | |
| echo -e "${CYAN}── Opérateur ${i} ──${NC}" | |
| read -rp " MCC [001] : " mcc; OP_MCC[$i]=${mcc:-001} | |
| read -rp " MNC [0${i}] : " mnc; OP_MNC[$i]=${mnc:-"0${i}"} | |
| read -rp " Nom [OsmoOP${i}]: " name; OP_NAME[$i]=${name:-"OsmoOP${i}"} | |
| read -rp " MS [1] : " n_ms; OP_MS[$i]=${n_ms:-1} | |
| if ! [[ "${OP_MS[$i]}" =~ ^[0-9]+$ ]] || \ | |
| [ "${OP_MS[$i]}" -lt 1 ] || [ "${OP_MS[$i]}" -gt 9 ]; then | |
| echo -e " ${YELLOW}Nombre de MS invalide, forcé à 1${NC}" | |
| OP_MS[$i]=1 | |
| fi | |
| echo -e " → MCC=${OP_MCC[$i]} MNC=${OP_MNC[$i]} Nom=${OP_NAME[$i]} MS=${CYAN}${OP_MS[$i]}${NC}" | |
| done | |
| # ── Réseau backbone partagé ──────────────────────────────────────────── | |
| echo -e "${GREEN}Création du réseau backbone ${INTER_NET}...${NC}" | |
| docker network inspect "$INTER_NET" &>/dev/null || \ | |
| docker network create \ | |
| --subnet="$INTER_NET_SUBNET" \ | |
| --gateway="$INTER_NET_GATEWAY" \ | |
| "$INTER_NET" &>/dev/null | |
| # ── Génération des configs SMS routing (avant le démarrage des containers) | |
| # Construit un sms-routing-op<N>.conf par opérateur avec les routes exactes | |
| # par MSISDN (pas seulement des préfixes courts) et les méta-données relay. | |
| SMS_ROUTING_DIR=$(mktemp -d) | |
| if declare -f sms_routing_generate_all > /dev/null 2>&1; then | |
| echo -e "${GREEN}Génération tables SMS routing (${n_operators} ops)...${NC}" | |
| # Construire le tableau des MS counts dans l'ordre op1..opN | |
| local _ms_counts=() | |
| for i in $(seq 1 "$n_operators"); do | |
| _ms_counts+=("${OP_MS[$i]}") | |
| done | |
| sms_routing_generate_all "$n_operators" "$SMS_ROUTING_DIR" "${_ms_counts[@]}" | |
| #sms_routing_validate "${SMS_ROUTING_DIR}/sms-routing-op1.conf" | |
| sms_routing_summary "$n_operators" "${_ms_counts[@]}" | |
| else | |
| echo -e " ${YELLOW}[SMS] sms-routing-setup.sh non chargé — fallback basique${NC}" | |
| fi | |
| # ── Inter-STP — doit être UP avant les opérateurs ───────────────────── | |
| start_inter_stp "$n_operators" | |
| # [PATCH] Vérifier que l'inter-STP est vraiment prêt avant les opérateurs | |
| wait_inter_stp_ready "$n_operators" | |
| # ── Opérateurs ──────────────────────────────────────────────────────── | |
| for i in $(seq 1 "$n_operators"); do | |
| start_operator "$i" \ | |
| "${OP_MCC[$i]}" "${OP_MNC[$i]}" "${OP_NAME[$i]}" \ | |
| "$n_operators" "${OP_MS[$i]}" | |
| done | |
| sudo bash hlr-feed-subscribers.sh | |
| # ── Attente relays SMS ───────────────────────────────────────────────── | |
| if declare -f sms_routing_wait_ready > /dev/null 2>&1; then | |
| echo -e "${GREEN}Vérification relays SMS (port 7890)...${NC}" | |
| for i in $(seq 1 "$n_operators"); do | |
| sms_routing_wait_ready "$(op_container "$i")" 90 || true | |
| done | |
| fi | |
| # ── Résumé ──────────────────────────────────────────────────────────── | |
| echo "" | |
| echo -e "${GREEN}${BOLD}Stack multi-opérateurs démarrée !${NC}" | |
| echo "" | |
| echo -e " Inter-STP @ ${CYAN}${INTER_STP_IP}:2908${NC} PC=0.23.0" | |
| for i in $(seq 1 "$n_operators"); do | |
| local rctx | |
| rctx=$(op_rctx_inter "$i") | |
| local bb_ip | |
| bb_ip=$(op_backbone_ip "$i") | |
| echo -e " Op${i} ${OP_NAME[$i]} STP ${i}.23.2 @ ${bb_ip} ──RCTX ${rctx}──► inter-STP [MS: ${OP_MS[$i]}]" | |
| done | |
| echo "" | |
| # ── Wireshark ───────────────────────────────────────────────────────── | |
| local bridge_if | |
| bridge_if=$(docker network inspect "$INTER_NET" -f '{{.Id}}' 2>/dev/null | cut -c1-12) | |
| if [ -n "$bridge_if" ] && command -v wireshark &>/dev/null; then | |
| TARGET_USER="${SUDO_USER:-$(logname 2>/dev/null || echo "$USER")}" | |
| DISPLAY="${DISPLAY:-:0}" | |
| XAUTHORITY="${XAUTHORITY:-/home/$TARGET_USER/.Xauthority}" | |
| DISPLAY="$DISPLAY" XAUTHORITY="$XAUTHORITY" \ | |
| wireshark -k -i "br-${bridge_if}" -f "sctp or udp port 4729" \ | |
| >/dev/null 2>&1 & true | |
| echo -e " Wireshark lancé sur ${CYAN}br-${bridge_if}${NC}" | |
| fi | |
| # ── Terminaux xterm ─────────────────────────────────────────────────── | |
| TARGET_USER="${SUDO_USER:-$(logname 2>/dev/null || echo "$USER")}" | |
| DISPLAY="${DISPLAY:-:0}" | |
| XAUTHORITY="${XAUTHORITY:-/home/$TARGET_USER/.Xauthority}" | |
| # Ouvre un xterm à partir d'un script tmp → évite tout problème de quoting/ | |
| # évaluation prématurée des variables (WAIT, etc.) dans les chaînes inline. | |
| _open_term_script() { | |
| local title="$1" script_file="$2" | |
| chmod +x "$script_file" | |
| DISPLAY="$DISPLAY" XAUTHORITY="$XAUTHORITY" \ | |
| xterm -title "$title" \ | |
| -fa 'Monospace' -fs 10 \ | |
| -bg '#1e1e1e' -fg '#d4d4d4' \ | |
| -e bash "$script_file" \ | |
| 2>/dev/null & | |
| sleep 0.3 | |
| } | |
| for i in $(seq 1 "$n_operators"); do | |
| local cname | |
| cname=$(op_container "$i") | |
| local tmpscript="/tmp/osmo-xterm-op${i}.sh" | |
| # Le heredoc écrit le script tel quel ; les variables du shell courant | |
| # (cname, OP_NAME, etc.) sont interpolées maintenant (sans $). | |
| # Les variables exécutées DANS xterm (WAIT) sont échappées avec \$. | |
| cat > "$tmpscript" << XTERM_SCRIPT | |
| #!/bin/bash | |
| echo "=== Op${i} — ${OP_NAME[$i]} (${OP_MS[$i]} MS) ===" | |
| echo "" | |
| echo " STP : sudo docker exec -it ${cname} telnet 127.0.0.1 4239" | |
| echo " MSC : sudo docker exec -it ${cname} telnet 127.0.0.1 4254" | |
| echo " BSC : sudo docker exec -it ${cname} telnet 127.0.0.1 4242" | |
| echo " HLR : sudo docker exec -it ${cname} telnet 127.0.0.1 4258" | |
| echo "" | |
| echo "En attente de la session tmux (max 120s)..." | |
| WAIT=0 | |
| until sudo docker exec ${cname} tmux has-session 2>/dev/null; do | |
| sleep 2 | |
| WAIT=\$((WAIT + 2)) | |
| if [ "\$WAIT" -ge 120 ]; then | |
| echo "TIMEOUT : tmux non disponible apres 120s." | |
| echo "Verifier : sudo docker logs ${cname}" | |
| break | |
| fi | |
| echo " ... \${WAIT}s" | |
| done | |
| exec sudo docker exec -ti ${cname} tmux attach | |
| XTERM_SCRIPT | |
| _open_term_script "Op${i} — ${OP_NAME[$i]} [${OP_MS[$i]} MS]" "$tmpscript" | |
| done | |
| # Inter-STP | |
| local tmpscript_stp="/tmp/osmo-xterm-stp.sh" | |
| cat > "$tmpscript_stp" << XTERM_STP | |
| #!/bin/bash | |
| echo "=== Inter-STP 0.23.0 @ ${INTER_STP_IP}:2908 ===" | |
| echo " VTY : sudo docker exec -it ${INTER_STP_CONTAINER} telnet 127.0.0.1 4239" | |
| echo "" | |
| sleep 3 | |
| exec sudo docker exec -ti ${INTER_STP_CONTAINER} tmux attach -t stp | |
| XTERM_STP | |
| _open_term_script "Inter-STP (PC 0.23.0)" "$tmpscript_stp" | |
| } | |
| # Arrêt | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| stop_all() { | |
| echo -e "${YELLOW}Arrêt 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 | |
| echo -e "${GREEN}Arrêté.${NC}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Mode réseau | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| choose_network_mode() { | |
| echo -e "${BOLD}Mode réseau :${NC}" | |
| echo " 1) net-host — SDR physique, 1 opérateur, accès direct au hardware" | |
| echo " 2) bridge — Multi-opérateurs (N≥1), SS7 inter-op via inter-STP" | |
| echo "" | |
| read -rp "Choix [1/2] : " NET_CHOICE | |
| case "$NET_CHOICE" in | |
| 1) NETWORK_MODE="host" ;; | |
| 2) NETWORK_MODE="bridge" ;; | |
| *) echo -e "${RED}Choix invalide.${NC}"; exit 1 ;; | |
| esac | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Main | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| banner | |
| [ "${1:-}" = "stop" ] && { stop_all; exit 0; } | |
| if [ "$(id -u)" -ne 0 ]; then | |
| echo -e "${RED}Doit être lancé en root (sudo).${NC}"; exit 1 | |
| fi | |
| build_run_image | |
| check_image | |
| choose_network_mode | |
| ./prepare_host.sh | |
| case "$NETWORK_MODE" in | |
| host) start_host_mode ;; | |
| bridge) start_bridge_mode ;; | |
| esac | |
| FROM ubuntu:22.04 as osmocom-nitb | |
| ARG DEBIAN_FRONTEND=noninteractive | |
| ARG ROOT=/opt/GSM | |
| ENV container=docker | |
| ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig | |
| ENV LD_LIBRARY_PATH=/usr/local/lib | |
| # 1. Dépendances système complètes (Infrastructure + Osmocom-BB) | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| # Outils de build | |
| build-essential git gcc g++ make cmake autoconf automake libtool pkg-config wget curl \ | |
| # Dépendances Osmocom Core & Network | |
| libtalloc-dev libpcsclite-dev libsctp-dev libmnl-dev liburing-dev \ | |
| libdbi-dev libdbd-sqlite3 libsqlite3-dev sqlite3 libc-ares-dev libgnutls28-dev \ | |
| # Audio, Radio & SIP | |
| libortp-dev libfftw3-dev libusb-1.0-0-dev libsofia-sip-ua-dev libsofia-sip-ua-glib-dev \ | |
| # Python & Outils système | |
| python3 python3-dev python3-scapy ca-certificates tmux systemd systemd-sysv \ | |
| # ALSA — requis par osmo-gapk pour l'I/O audio matériel | |
| libasound2-dev libasound2 alsa-utils \ | |
| # libgsm — codec GSM-FR natif (accélère gapk en mode gsmfr) | |
| libgsm1-dev libgsm1 \ | |
| iptables iproute2 asterisk | |
| SHELL ["/bin/bash", "-c"] | |
| COPY configs/*conf /etc/asterisk/ | |
| WORKDIR ${ROOT} | |
| # 2. Création de l'utilisateur osmocom | |
| RUN groupadd osmocom && useradd -r -g osmocom -s /sbin/nologin -d /var/lib/osmocom osmocom && \ | |
| mkdir -p /var/lib/osmocom && chown osmocom:osmocom /var/lib/osmocom | |
| # 3. Compilation de la pile Osmocom (Ordre respecté) | |
| RUN for repo in \ | |
| libosmocore:1.12.1 \ | |
| libosmo-netif:1.7.0 \ | |
| libosmo-abis:2.1.0 \ | |
| libosmo-sigtran:2.2.1 \ | |
| libsmpp34:1.14.5 \ | |
| libgtpnl:1.3.3 \ | |
| osmo-hlr:1.9.2 \ | |
| osmo-mgw:1.15.0 \ | |
| osmo-ggsn:1.14.0 \ | |
| osmo-sgsn:1.13.1 \ | |
| osmo-msc:1.15.0 \ | |
| osmo-bsc:1.14.0 \ | |
| osmo-pcu:1.5.2 \ | |
| osmo-bts:1.10.0 \ | |
| osmo-sip-connector:1.7.2 \ | |
| libosmo-gprs:0.2.1; \ | |
| do \ | |
| name=$(echo $repo | cut -d: -f1) && \ | |
| version=$(echo $repo | cut -d: -f2) && \ | |
| \ | |
| if [ "$name" = "libosmocore" ]; then \ | |
| GIT_URL="https://github.com/osmocom/$name"; \ | |
| elif [[ "$name" =~ "libosmo" ]]; then \ | |
| GIT_URL="https://gitea.osmocom.org/osmocom/$name"; \ | |
| else \ | |
| GIT_URL="https://gitea.osmocom.org/cellular-infrastructure/$name"; \ | |
| fi && \ | |
| \ | |
| cd ${ROOT} && \ | |
| git clone "$GIT_URL" && cd "$name" && \ | |
| git checkout "$version" && \ | |
| \ | |
| autoreconf -fi && \ | |
| EXTRA_FLAGS="" && \ | |
| if [ "$name" = "libosmo-abis" ]; then EXTRA_FLAGS="--disable-dahdi"; fi && \ | |
| if [ "$name" = "osmo-msc" ]; then EXTRA_FLAGS="--enable-smpp"; fi && \ | |
| if [ "$name" = "osmo-mgw" ]; then EXTRA_FLAGS="--enable-alsa"; fi && \ | |
| if [ "$name" = "osmo-bts" ]; then EXTRA_FLAGS="--enable-virtual --enable-trx"; fi && \ | |
| if [ "$name" = "osmo-ggsn" ]; then EXTRA_FLAGS="--enable-gtp-linux"; fi && \ | |
| \ | |
| ./configure $EXTRA_FLAGS && \ | |
| make -j$(nproc) && \ | |
| make install && \ | |
| ldconfig; \ | |
| done | |
| RUN cd ${ROOT} && \ | |
| git clone https://gitea.osmocom.org/osmocom/gapk osmo-gapk && \ | |
| cd osmo-gapk && \ | |
| autoreconf -fi && \ | |
| ./configure --enable-alsa && \ | |
| make -j$(nproc) && \ | |
| make install && \ | |
| ldconfig | |
| RUN cd ${ROOT} && \ | |
| git clone https://gitea.osmocom.org/phone-side/osmocom-bb && \ | |
| cd osmocom-bb/src && \ | |
| # nofirmware désactive la compilation des firmwares .bin pour les téléphones | |
| make nofirmware -j$(nproc) | |
| # ── gsup-smsc-proto : SMSC externe connecté à OsmoHLR via GSUP ──────────────── | |
| # Programmes : proto-smsc-daemon (réception MO SMS + relai MT via GSUP) | |
| # proto-smsc-sendmt (injection MT SMS via socket UNIX local) | |
| # Dépendances build : libosmocore, libosmogsm, libosmo-gsup-client | |
| RUN cd ${ROOT} && \ | |
| git clone https://gitea.osmocom.org/themwi/gsup-smsc-proto && \ | |
| cd gsup-smsc-proto && \ | |
| ./configure --with-osmo=/usr/local && \ | |
| make -j$(nproc) && \ | |
| make install && \ | |
| ldconfig | |
| # ── sms-coding-utils : encodage/décodage SMS PDU (GSM 03.40) ────────────────── | |
| # sms-encode-text, gen-sms-deliver-pdu, sms-pdu-decode, etc. | |
| RUN cd ${ROOT} && \ | |
| wget -q https://www.freecalypso.org/pub/GSM/FreeCalypso/sms-coding-utils-latest.tar.bz2 && \ | |
| tar xf sms-coding-utils-latest.tar.bz2 && \ | |
| cd sms-coding-utils-r1 && \ | |
| ./configure && \ | |
| make -j$(nproc) && \ | |
| make install INSTDIR=/usr/local/bin | |
| # 4. Installation des fichiers du projet | |
| WORKDIR /etc/osmocom | |
| COPY scripts/. /etc/osmocom/ | |
| COPY configs/*cfg /etc/osmocom/ | |
| RUN mv /etc/osmocom/run.sh /root/run.sh | |
| # Copie des binaires vers /usr/bin pour systemd et installation des .service | |
| COPY scripts/gapk-start.sh /etc/osmocom/gapk-start.sh | |
| RUN chmod +x /etc/osmocom/gapk-start.sh && \ | |
| ln -sf /etc/osmocom/gapk-start.sh /usr/local/bin/gapk-start.sh && \ | |
| mkdir -p /var/lib/gapk | |
| RUN cp -f /usr/local/bin/osmo* /usr/bin/ || true && \ | |
| cp -f /usr/local/bin/proto-smsc-* /usr/bin/ || true && \ | |
| cp -f /usr/local/bin/sms-* /usr/bin/ || true && \ | |
| cp -f /usr/local/bin/gen-sms-* /usr/bin/ || true && \ | |
| # Si tu as des fichiers .service dans configs/ | |
| cp /etc/osmocom/configs/*.service /lib/systemd/system/ 2>/dev/null || true | |
| # 5. Fix Permissions & Systemd (Status 214/217) | |
| RUN sed -i 's/^CPUScheduling/#CPUScheduling/g' /lib/systemd/system/osmo-*.service && \ | |
| sed -i 's/User=osmocom/User=root/g' /lib/systemd/system/osmo-ggsn.service && \ | |
| sed -i 's/User=osmocom/User=root/g' /lib/systemd/system/osmo-sgsn.service && \ | |
| chmod +x /etc/osmocom/*.sh | |
| # Activation du service et nettoyage | |
| RUN systemctl enable osmo-bts-trx.service && \ | |
| passwd -d root && \ | |
| systemctl mask getty@tty1.service serial-getty@tty1.service | |
| # Point d'entrée pour systemd | |
| STOPSIGNAL SIGRTMIN+3 | |
| ENTRYPOINT ["/etc/osmocom/entrypoint.sh"] | |
| RUN mkdir -p /root/.osmocom/bb/ | |
| RUN cp /opt/GSM/osmocom-bb/src/host/trxcon/src/trxcon /usr/local/bin | |
| RUN cp /opt/GSM/osmocom-bb/src/host/layer23/src/mobile/mobile /usr/local/bin | |
| RUN cp /opt/GSM/osmocom-bb/src/host/virt_phy/src/virtphy /usr/local/bin | |
| RUN cp /opt/GSM/osmocom-bb/src/host/layer23/src/misc/ccch_scan /usr/local/bin | |
| RUN echo "alias faketrx='python3 /opt/GSM/osmocom-bb/src/target/trx_toolkit/fake_trx.py'" >> ~/.bashrc && source ~/.bashrc | |
| COPY configs/mobile.cfg /root/.osmocom/bb/mobile.cfg | |
| RUN chmod +x /root/run.sh | |
| # Répertoires pour le proto-SMSC | |
| RUN mkdir -p /var/log/osmocom /var/run/smsc | |
| CMD ["/bin/bash"] | |
| #!/usr/bin/env bash | |
| # force_stack.sh | |
| # À lancer en root (sudo). | |
| # But : | |
| # - FORCER (kill + relance) Linphone en user | |
| # - FORCER (kill + relance) Wireshark en root sur UDP/4729 (GSMTAP) | |
| # - Restart du service docker | |
| # Affichage: | |
| # [....] action en cours -> se transforme en -> [ OK ] action finie (même ligne) | |
| set -euo pipefail | |
| GSMTAP_PORT="${GSMTAP_PORT:-4729}" | |
| # ---------- helpers ---------- | |
| step() { printf "[....] %s" "$1"; } | |
| step_ok() { printf "\r[ OK ] %s\n" "$1"; } | |
| fail() { printf "\r[FAIL] %s\n" "$1" >&2; exit 1; } | |
| need() { command -v "$1" >/dev/null 2>&1 || fail "Commande manquante: $1"; } | |
| kill_name_as_user() { | |
| local user="$1" name="$2" | |
| pkill -u "$user" -x "$name" 2>/dev/null || true | |
| } | |
| kill_name_root() { | |
| local name="$1" | |
| pkill -x "$name" 2>/dev/null || true | |
| } | |
| # ---------- préflight ---------- | |
| [[ "${EUID}" -eq 0 ]] || fail "Lance en root: sudo $0" | |
| need sudo | |
| need docker | |
| need ss | |
| need systemctl | |
| TARGET_USER="${SUDO_USER:-}" | |
| if [[ -z "${TARGET_USER}" || "${TARGET_USER}" == "root" ]]; then | |
| fail "SUDO_USER absent — lance via sudo (ex: sudo $0)" | |
| fi | |
| id -u "${TARGET_USER}" >/dev/null 2>&1 || fail "User invalide: ${TARGET_USER}" | |
| # ---------- 1) Linphone : kill + relance en user ---------- | |
| step "Linphone: kill (user=${TARGET_USER})" | |
| kill_name_as_user "${TARGET_USER}" "linphone" | |
| sleep 0.2 | |
| step_ok "Linphone: kill (user=${TARGET_USER})" | |
| step "Linphone: relance (user=${TARGET_USER})" | |
| TARGET_USER="${SUDO_USER:-$(logname 2>/dev/null || echo "$USER")}" | |
| TARGET_UID=$(id -u "$TARGET_USER") | |
| DISPLAY="${DISPLAY:-:0}" | |
| XAUTHORITY="${XAUTHORITY:-/home/$TARGET_USER/.Xauthority}" | |
| sudo -u "$TARGET_USER" \ | |
| env DISPLAY="$DISPLAY" XAUTHORITY="$XAUTHORITY" \ | |
| DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/${TARGET_UID}/bus" \ | |
| nohup linphone >/dev/null 2>&1 & true | |
| step_ok "Linphone: relance (user=${TARGET_USER})" | |
| # ---------- 3) Xterm : kill---------- | |
| step "Xterm kill" | |
| kill_name_root "xterm" | |
| sleep 0.2 | |
| step_ok "Xterm: kill (root)" | |
| # ---------- 4) Docker: restart service ---------- | |
| step "Restart docker service" | |
| systemctl restart docker >/dev/null 2>&1 || fail "Restart docker service" | |
| step_ok "Restart docker service" | |
| # ---------- 2) Wireshark : kill + relance en root sur GSMTAP/4729 ---------- | |
| step "Wireshark: kill (root)" | |
| kill_name_root "wireshark" | |
| sleep 0.2 | |
| step_ok "Wireshark: kill (root)" | |
| # ---------- 5) Mini-check (optionnel) ---------- | |
| step "Check: listeners UDP/${GSMTAP_PORT}" | |
| ss -lunp | awk -v p=":${GSMTAP_PORT}" '$0 ~ p {print}' >/dev/null 2>&1 || true | |
| step_ok "Check: listeners UDP/${GSMTAP_PORT}" | |
| step "Stack forcée" | |
| step_ok "Stack forcée" | |
| #!/bin/bash | |
| # 1. Vérification des privilèges ROOT | |
| if [[ $EUID -ne 0 ]]; then | |
| echo -e "\033[0;31m[ERREUR] Ce script doit être lancé en tant que root (sudo).\033[0m" | |
| exit 1 | |
| fi | |
| echo "--- Préparation complète de l'hôte (SDR & Docker) ---" | |
| # 2. Installation de Docker (si non présent) | |
| if ! command -v docker &> /dev/null; then | |
| echo "[*] Docker n'est pas installé. Installation en cours..." | |
| apt-get update | |
| apt-get install -y docker.io docker-compose-v2 wireshark linphone-desktop xterm | |
| fi | |
| # 3. Installation des dépendances critiques sur l'hôte | |
| # SCTP est vital pour les protocoles de signalisation Osmocom | |
| echo "[*] Installation de SCTP, TUN et D-Bus sur l'hôte..." | |
| apt-get update | |
| apt-get install -y lksctp-tools libsctp-dev dbus tunctl libusb-1.0-0-dev | |
| # 4. Chargement des modules noyau | |
| echo "[*] Chargement des modules noyau (SCTP & TUN)..." | |
| modprobe sctp | |
| modprobe tun | |
| # Vérification du module SCTP | |
| if lsmod | grep -q sctp; then | |
| echo -e "\033[0;32m[OK] Module SCTP chargé sur l'hôte.\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 ---" | |
| # On utilise --no-cache si tu veux une installation propre des paquets dans le container | |
| docker build . -t osmocom-nitb | |
| # 6. Vérification du succès | |
| if [ $? -eq 0 ]; then | |
| echo -e "\033[0;32m[OK] Image osmocom-nitb construite avec succès.\033[0m" | |
| else | |
| echo -e "\033[0;31m[ERREUR] Le build Docker a échoué.\033[0m" | |
| exit 1 | |
| fi | |
| #!/bin/bash | |
| # hlr-feed-subscribers.sh | |
| # | |
| # Injecte les abonnés de TOUS les opérateurs dans le HLR de CHAQUE opérateur. | |
| # | |
| # Formules communes (identiques à run.sh et start.sh) : | |
| # MSIN = printf('%04d%06d', op_id, ms_idx) → 10 chiffres | |
| # IMSI = MCC(3) + MNC(2) + MSIN(10) → 15 chiffres | |
| # MSISDN = op_id * 10000 + ms_idx | |
| # KI HLR VTY = 00112233445566778899aabbccdd<ms_hex><op_hex> (sans espaces) | |
| # Octet 15 = ms_idx | Octet 16 = op_id | |
| # KI mobile = "00 11 22 33 44 55 66 77 88 99 aa bb cc dd <ms_hex> <op_hex>" | |
| # (même valeur, avec espaces — format mobile.cfg) | |
| # | |
| # Commandes VTY osmo-hlr par abonné : | |
| # subscriber imsi IMSI create | |
| # subscriber imsi IMSI update msisdn MSISDN | |
| # subscriber imsi IMSI update aud2g comp128v1 ki KI | |
| # | |
| # Usage : | |
| # sudo ./hlr-feed-subscribers.sh (après start.sh bridge mode) | |
| # sudo ./hlr-feed-subscribers.sh --dry-run | |
| set -euo pipefail | |
| GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' | |
| DRY_RUN=0 | |
| [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1 | |
| MCC="${MCC:-001}" | |
| # ── Détection des containers opérateurs ────────────────────────────────────── | |
| mapfile -t OP_CONTAINERS < <( | |
| docker ps --format '{{.Names}}' \ | |
| | grep -E '^osmo-operator-[0-9]+$' \ | |
| | sort -t '-' -k 3 -n | |
| ) | |
| if [ ${#OP_CONTAINERS[@]} -eq 0 ]; then | |
| echo -e "${RED}Aucun container osmo-operator-N trouvé.${NC}" | |
| echo -e " Containers actifs : $(docker ps --format '{{.Names}}' | tr '\n' ' ')" | |
| exit 1 | |
| fi | |
| N_OPS=${#OP_CONTAINERS[@]} | |
| echo -e "${GREEN}=== HLR Feed — ${N_OPS} opérateur(s) ===${NC}" | |
| # ── N_MS et MNC par opérateur ──────────────────────────────────────────────── | |
| # MNC lu depuis la variable d'env du container (injectée par start.sh via -e MNC=). | |
| # Fallback : lire dans le mobile.cfg résolu (ligne "rplmn MCC MNC"). | |
| declare -A OP_MS OP_MNC | |
| for i in $(seq 1 "${N_OPS}"); do | |
| cname="osmo-operator-${i}" | |
| n=$(docker exec "$cname" bash -c 'echo "${N_MS:-1}"' 2>/dev/null || echo 1) | |
| mnc=$(docker exec "$cname" bash -c ' | |
| # Priorité 1 : variable env MNC injectée par start.sh | |
| if [ -n "${MNC:-}" ]; then echo "$MNC"; exit 0; fi | |
| # Priorité 2 : lire dans le mobile.cfg résolu (rplmn MCC MNC) | |
| for f in /root/.osmocom/bb/mobile_ms1.cfg \ | |
| /root/.osmocom/bb/mobile.cfg; do | |
| [ -f "$f" ] || continue | |
| mnc=$(grep -E "^\s+rplmn " "$f" \ | |
| | awk "{print \$3}" | head -1) | |
| [ -n "$mnc" ] && echo "$mnc" && exit 0 | |
| done | |
| echo "01" | |
| ' 2>/dev/null || echo "01") | |
| OP_MS[$i]="$n" | |
| OP_MNC[$i]="$mnc" | |
| echo -e " Op${i} (${cname}) : ${n} MS MNC=${mnc}" | |
| done | |
| echo "" | |
| # ── Liste globale des abonnés ───────────────────────────────────────────────── | |
| # Format interne : "op_id:ms_idx:imsi:msisdn:ki_nospaces" | |
| # | |
| # KI sans espaces pour le VTY osmo-hlr (aud2g comp128v1 ki) : | |
| # 00112233445566778899aabbccdd<ms_hex><op_hex> | |
| # → 32 caractères hex, pas d'espaces | |
| # → octet 15 = ms_idx, octet 16 = op_id | |
| SUBSCRIBERS=() | |
| for op_id in $(seq 1 "${N_OPS}"); do | |
| mnc="${OP_MNC[$op_id]}" | |
| n_ms="${OP_MS[$op_id]}" | |
| for ms_idx in $(seq 1 "${n_ms}"); do | |
| msin=$(printf '%04d%06d' "${op_id}" "${ms_idx}") | |
| imsi="${MCC}${mnc}${msin}" | |
| msisdn=$(( op_id * 10000 + ms_idx )) | |
| # KI sans espaces : octet 15=ms_idx octet 16=op_id | |
| ki=$(printf '00112233445566778899aabbccdd%02xff' \ | |
| "${ms_idx}") | |
| SUBSCRIBERS+=("${op_id}:${ms_idx}:${imsi}:${msisdn}:${ki}") | |
| done | |
| done | |
| total=${#SUBSCRIBERS[@]} | |
| echo -e "${GREEN}${total} abonnés à injecter dans chaque HLR :${NC}" | |
| for s in "${SUBSCRIBERS[@]}"; do | |
| IFS=':' read -r op ms imsi msisdn ki <<< "$s" | |
| # Afficher le KI avec espaces pour lisibilité du log (2 chars = 1 octet) | |
| ki_display=$(echo "$ki" | sed 's/../& /g' | sed 's/[[:space:]]*$//') | |
| echo -e " Op${op}/MS${ms} IMSI=${imsi} MSISDN=${msisdn} KI=${ki_display}" | |
| done | |
| echo "" | |
| wait_hlr_vty() { | |
| local container="$1" | |
| local timeout=60 | |
| local elapsed=0 | |
| echo -ne " Attente HLR VTY 127.0.0.1:4258 " | |
| while ! docker exec "$container" bash -c \ | |
| "echo >/dev/tcp/127.0.0.1/4258" 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}" | |
| return 0 | |
| } | |
| # ── Injection dans un HLR ───────────────────────────────────────────────────── | |
| # Stratégie : | |
| # 1. Vérifier que le VTY HLR (port 4258) est accessible | |
| # 2. Écrire toutes les commandes VTY dans /tmp/hlr_feed.vty dans le container | |
| # 3. Piper ce fichier vers osmo-hlr via netcat (ou telnet en fallback) | |
| inject_to_hlr() { | |
| local container="$1" | |
| echo -e "${CYAN}=== ${container} ===${NC}" | |
| # Vérifier VTY HLR | |
| if ! wait_hlr_vty "$container"; then | |
| echo -e "${RED} HLR VTY indisponible — skip${NC}" | |
| return 1 | |
| fi | |
| if [ "$DRY_RUN" -eq 1 ]; then | |
| echo -e "${YELLOW} [DRY-RUN] commandes VTY :${NC}" | |
| for s in "${SUBSCRIBERS[@]}"; do | |
| IFS=':' read -r _ _ imsi msisdn ki <<< "$s" | |
| printf " subscriber imsi %s create\n" "$imsi" | |
| printf " subscriber imsi %s update msisdn %s\n" "$imsi" "$msisdn" | |
| printf " subscriber imsi %s update aud2g comp128v1 ki %s\n" "$imsi" "$ki" | |
| done | |
| return 0 | |
| fi | |
| # Écrire le script VTY dans le container via stdin | |
| { | |
| echo "#!/bin/bash" | |
| echo "cat > /tmp/hlr_feed.vty << 'VTYCMDS'" | |
| echo "enable" | |
| for s in "${SUBSCRIBERS[@]}"; do | |
| IFS=':' read -r _ _ imsi msisdn ki <<< "$s" | |
| # 3 commandes par abonné : | |
| echo "subscriber imsi ${imsi} create" | |
| echo "subscriber imsi ${imsi} update msisdn ${msisdn}" | |
| # aud2g (pas aug2g) — ki sans espaces, 32 hex chars | |
| echo "subscriber imsi ${imsi} update aud2g comp128v1 ki ${ki}" | |
| done | |
| echo "end" | |
| echo "VTYCMDS" | |
| } | docker exec -i "$container" bash | |
| # Envoyer le script VTY via netcat (ou telnet en fallback) | |
| docker exec "$container" bash -c ' | |
| if command -v nc >/dev/null 2>&1; then | |
| (sleep 1; cat /tmp/hlr_feed.vty; sleep 1) \ | |
| | nc -q2 127.0.0.1 4258 2>/dev/null \ | |
| | grep -vE "^(OsmoHLR|Welcome|VTY|\s*$)" \ | |
| | sed "s/^/ /" || true | |
| else | |
| # Fallback telnet | |
| (sleep 1; cat /tmp/hlr_feed.vty; sleep 2) \ | |
| | telnet 127.0.0.1 4258 2>/dev/null \ | |
| | grep -vE "^(Trying|Connected|Escape|OsmoHLR|Welcome)" \ | |
| | sed "s/^/ /" || true | |
| fi | |
| rm -f /tmp/hlr_feed.vty | |
| ' | |
| echo -e " ${GREEN}✓ ${total} abonnés injectés${NC}" | |
| } | |
| # ── Boucle ──────────────────────────────────────────────────────────────────── | |
| FAIL=0 | |
| for i in $(seq 1 "${N_OPS}"); do | |
| inject_to_hlr "osmo-operator-${i}" || FAIL=$(( FAIL + 1 )) | |
| echo "" | |
| done | |
| # ── Résumé ──────────────────────────────────────────────────────────────────── | |
| if [ "$FAIL" -eq 0 ]; then | |
| echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}" | |
| echo -e "${GREEN}║ HLR Feed complet — ${total} abonnés × ${N_OPS} HLR${NC}" | |
| echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}" | |
| echo "" | |
| echo -e "Vérifier :" | |
| echo -e " ${CYAN}docker exec osmo-operator-1 bash -c \\" | |
| echo -e " 'echo -e \"enable\\\\nshow subscriber all\\\\nend\" | nc -q2 127.0.0.1 4258 2>/dev/null'${NC}" | |
| else | |
| echo -e "${RED}${FAIL} HLR(s) en échec. Relancer : sudo bash hlr-feed-subscribers.sh${NC}" | |
| exit 1 | |
| fi | |
| #!/bin/bash | |
| # smsc-start.sh — Lance le proto-smsc-daemon + le relay interop | |
| # | |
| # 1. proto-smsc-daemon : connecté au HLR via GSUP | |
| # - MO SMS → log fichier | |
| # - MT SMS ← socket UNIX | |
| # | |
| # 2. sms-interop-relay.py : pont entre opérateurs | |
| # - Surveille le log MO, parse les TPDU | |
| # - Route vers l'opérateur cible via TCP (port 7890) | |
| # - Écoute en TCP pour les MT venant d'autres opérateurs | |
| # - Résout MSISDN→IMSI via HLR VTY, injecte les MT localement | |
| set -euo pipefail | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| HLR_IP="${HLR_IP:-127.0.0.2}" | |
| SMSC_IPA_NAME="SMSC-OP${OPERATOR_ID}" | |
| MO_LOG="/var/log/osmocom/mo-sms-op${OPERATOR_ID}.log" | |
| SENDMT_SOCKET="/tmp/sendmt_socket" | |
| ROUTING_CONF="/etc/osmocom/sms-routing.conf" | |
| RELAY_PORT="${RELAY_PORT:-7890}" | |
| GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[0;33m'; NC='\033[0m' | |
| echo -e "${GREEN}╔══════════════════════════════════════════════════╗${NC}" | |
| echo -e "${GREEN}║ SMS Gateway — Opérateur ${OPERATOR_ID} ║${NC}" | |
| echo -e "${GREEN}╚══════════════════════════════════════════════════╝${NC}" | |
| echo -e " HLR : ${CYAN}${HLR_IP}${NC}" | |
| echo -e " IPA name : ${CYAN}${SMSC_IPA_NAME}${NC}" | |
| echo -e " MO log : ${CYAN}${MO_LOG}${NC}" | |
| echo -e " MT socket : ${CYAN}${SENDMT_SOCKET}${NC}" | |
| echo -e " Relay port : ${CYAN}${RELAY_PORT}${NC} (TCP interop)" | |
| echo -e " Routing : ${CYAN}${ROUTING_CONF}${NC}" | |
| # Nettoyage | |
| rm -f "$SENDMT_SOCKET" | |
| mkdir -p /var/log/osmocom | |
| # Attente OsmoHLR GSUP (port 4222) | |
| echo -ne "${YELLOW}[SMSC] Attente OsmoHLR GSUP" | |
| retries=30 | |
| while [ $retries -gt 0 ]; do | |
| if ss -tln | grep -q ":4222 "; then | |
| echo -e " ✓${NC}" | |
| break | |
| fi | |
| echo -n "." | |
| sleep 1 | |
| retries=$(( retries - 1 )) | |
| done | |
| [ $retries -eq 0 ] && echo -e " timeout${NC}" | |
| sleep 2 | |
| # ── 1. proto-smsc-daemon (background) ──────────────────────────────────────── | |
| echo -e "${GREEN}[1/2] proto-smsc-daemon${NC}" | |
| proto-smsc-daemon "$HLR_IP" "$SMSC_IPA_NAME" "$MO_LOG" "$SENDMT_SOCKET" & | |
| DAEMON_PID=$! | |
| echo -e " PID: ${CYAN}${DAEMON_PID}${NC}" | |
| sleep 2 | |
| [ -S "$SENDMT_SOCKET" ] && echo -e " Socket: ${GREEN}✓${NC}" || echo -e " Socket: ${YELLOW}attente...${NC}" | |
| # ── 2. Relay interop (background) ──────────────────────────────────────────── | |
| echo -e "${GREEN}[2/2] sms-interop-relay${NC}" | |
| python3 /etc/osmocom/sms-interop-relay.py \ | |
| --config "$ROUTING_CONF" \ | |
| --port "$RELAY_PORT" \ | |
| --mo-log "$MO_LOG" \ | |
| --operator-id "$OPERATOR_ID" & | |
| RELAY_PID=$! | |
| echo -e " PID: ${CYAN}${RELAY_PID}${NC}" | |
| echo -e "" | |
| echo -e "${GREEN}══════════════════════════════════════════════════${NC}" | |
| echo -e "${GREEN} SMS Gateway actif${NC}" | |
| echo -e "${GREEN} daemon=${DAEMON_PID} relay=${RELAY_PID}${NC}" | |
| echo -e "${GREEN} MT local : send-mt-sms.sh <imsi> 'msg'${NC}" | |
| echo -e "${GREEN} MO log : tail -f ${MO_LOG}${NC}" | |
| echo -e "${GREEN}══════════════════════════════════════════════════${NC}" | |
| # Cleanup | |
| cleanup() { | |
| echo -e "\n${YELLOW}[SMSC] Arrêt...${NC}" | |
| kill $DAEMON_PID 2>/dev/null || true | |
| kill $RELAY_PID 2>/dev/null || true | |
| rm -f "$SENDMT_SOCKET" | |
| exit 0 | |
| } | |
| trap cleanup SIGINT SIGTERM | |
| # Attendre fin d'un processus | |
| wait -n $DAEMON_PID $RELAY_PID 2>/dev/null || true | |
| echo -e "${YELLOW}[SMSC] Processus terminé, arrêt...${NC}" | |
| cleanup | |
| [Unit] | |
| Description=Osmocom GSM Base Transceiver Station (BTS) | |
| # S'assure que le réseau et l'interface de bouclage sont prêts | |
| After=network.target network-online.target | |
| Wants=network-online.target | |
| # Si tu utilises osmo-bsc sur la même machine, décommente la ligne suivante | |
| # After=osmo-bsc.service | |
| [Service] | |
| Type=simple | |
| # Adapte l'utilisateur (ex: root ou ton utilisateur actuel 'nirvana') | |
| User=root | |
| Group=root | |
| # Chemin vers l'exécutable (vérifie avec 'which osmo-bts-trx') | |
| # -c spécifie le fichier de configuration | |
| # -r 1 permet d'activer la priorité temps réel si le noyau le permet | |
| ExecStart=/usr/bin/osmo-bts-trx -c /etc/osmocom/osmo-bts-trx.cfg -r 1 | |
| # Redémarrage automatique en cas de crash (utile pour la stabilité radio) | |
| Restart=always | |
| RestartSec=5 | |
| # Limites de ressources pour éviter les "hangs" système | |
| LimitNOFILE=65535 | |
| CPUSchedulingPolicy=rr | |
| CPUSchedulingPriority=1 | |
| [Install] | |
| WantedBy=multi-user.target | |
| #!/bin/bash | |
| set -e | |
| # --- AJOUT DE LA PARTIE TUN --- | |
| echo "[*] Initialisation du périphérique TUN pour osmo-ggsn..." | |
| mkdir -p /dev/net | |
| if [ ! -c /dev/net/tun ]; then | |
| # Création du nœud de périphérique (c = caractère, 10 = majeur, 200 = mineur) | |
| mknod /dev/net/tun c 10 200 | |
| chmod 666 /dev/net/tun | |
| fi | |
| # --- CONFIGURATION ENVIRONNEMENT --- | |
| container=docker | |
| export container | |
| # Vérification de la présence d'une commande | |
| if [ $# -eq 0 ]; then | |
| echo >&2 'ERROR: No command specified. You probably want to run bash or a script.' | |
| exit 1 | |
| fi | |
| # Export des variables pour les sessions systemd | |
| env > /etc/docker-entrypoint-env | |
| # Création du service de démarrage | |
| quoted_args="$(printf " %q" "${@}")" | |
| echo "${quoted_args}" > /etc/docker-entrypoint-cmd | |
| cat >/etc/systemd/system/docker-entrypoint.service <<EOT | |
| [Unit] | |
| Description=Lancement de la stack Osmocom Simulee | |
| After=network.target | |
| [Service] | |
| ExecStart=/bin/bash -exc "source /etc/docker-entrypoint-cmd" | |
| StandardInput=tty-force | |
| StandardOutput=inherit | |
| StandardError=inherit | |
| WorkingDirectory=/etc/osmocom | |
| EnvironmentFile=/etc/docker-entrypoint-env | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOT | |
| # Désactivation des services systemd conflictuels | |
| systemctl mask systemd-firstboot.service systemd-udevd.service systemd-modules-load.service \ | |
| systemd-udevd-kernel systemd-udevd-control 2>/dev/null || true | |
| systemctl enable docker-entrypoint.service | |
| # Localisation et exécution de systemd | |
| if [ -x /lib/systemd/systemd ]; then | |
| exec /lib/systemd/systemd --show-status=false --unit=multi-user.target | |
| elif [ -x /usr/lib/systemd/systemd ]; then | |
| exec /usr/lib/systemd/systemd --show-status=false --unit=multi-user.target | |
| else | |
| echo >&2 'ERROR: systemd is not installed' | |
| exit 1 | |
| fi | |
| # NOTE : Les directives RUN/COPY/EXPOSE qui étaient ici sont des instructions | |
| # Dockerfile et doivent être dans le Dockerfile, pas dans ce script. | |
| # exec systemd remplace le process — rien après cette ligne n'est exécuté. | |
| #!/usr/bin/env python3 | |
| """ | |
| sms-interop-relay.py — Relay SMS inter-opérateur via proto-smsc-proto | |
| Architecture : | |
| Chaque container opérateur exécute ce relay qui : | |
| 1. Surveille le fichier MO SMS log (proto-smsc-daemon) | |
| 2. Parse le TPDU SMS-SUBMIT pour extraire le numéro destinataire | |
| 3. Détermine si le destinataire est local ou sur un autre opérateur | |
| 4. Si distant : envoie via TCP au relay de l'opérateur cible | |
| 5. Écoute aussi en TCP pour recevoir les MT SMS d'autres opérateurs | |
| 6. Résout MSISDN → IMSI via le HLR local (VTY telnet) | |
| 7. Injecte le MT SMS localement via proto-smsc-sendmt | |
| Flux inter-opérateur : | |
| MS(Op1) → MSC(Op1) → HLR(Op1) → proto-smsc-daemon(Op1) → MO log | |
| → sms-interop-relay(Op1) [parse TPDU, route] | |
| → TCP → sms-interop-relay(Op2) [MSISDN→IMSI lookup, inject MT] | |
| → proto-smsc-sendmt(Op2) → HLR(Op2) → MSC(Op2) → MS(Op2) | |
| Usage : | |
| python3 sms-interop-relay.py --config /etc/osmocom/sms-routing.conf | |
| Env vars : | |
| OPERATOR_ID : identifiant opérateur (1, 2, ...) | |
| CONTAINER_IP : IP du container (172.20.0.11, .12, ...) | |
| HLR_VTY_PORT : port VTY du HLR (défaut 4258) | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import socket | |
| import struct | |
| import select | |
| import signal | |
| import logging | |
| import argparse | |
| import threading | |
| import subprocess | |
| from pathlib import Path | |
| from typing import Optional, Dict, Tuple | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Configuration | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| RELAY_TCP_PORT = 7890 # Port TCP d'écoute pour les MT entrants | |
| MO_LOG_POLL_INTERVAL = 0.5 # Intervalle polling du log MO (sec) | |
| HLR_VTY_HOST = "127.0.0.1" | |
| HLR_VTY_PORT = 4258 | |
| SENDMT_SOCKET = "/tmp/sendmt_socket" | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s [RELAY-OP%(operator_id)s] %(levelname)s %(message)s', | |
| datefmt='%Y-%m-%dT%H:%M:%S' | |
| ) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # TPDU Parser — décode SMS-SUBMIT (GSM 03.40) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def decode_bcd_number(hex_bytes: bytes, num_digits: int) -> str: | |
| """Décode un numéro BCD swapped (format GSM standard).""" | |
| number = "" | |
| for byte in hex_bytes: | |
| lo = byte & 0x0F | |
| hi = (byte >> 4) & 0x0F | |
| if lo <= 9: | |
| number += str(lo) | |
| if hi <= 9 and len(number) < num_digits: | |
| number += str(hi) | |
| return number[:num_digits] | |
| def parse_sms_submit_tpdu(hex_str: str) -> Optional[Dict]: | |
| """ | |
| Parse un SMS-SUBMIT TPDU (direction MO : MS → SMSC). | |
| Extrait le numéro destinataire (TP-DA). | |
| Format SMS-SUBMIT (GSM 03.40 §9.2.2) : | |
| Octet 0 : First Octet (MTI=01 pour SUBMIT) | |
| Octet 1 : TP-MR (Message Reference) | |
| Octet 2 : TP-DA length (nombre de chiffres) | |
| Octet 3 : TP-DA Type of Address | |
| Octets 4+ : TP-DA digits (BCD swapped) | |
| ...suite : TP-PID, TP-DCS, TP-VP (optionnel), TP-UDL, TP-UD | |
| """ | |
| try: | |
| data = bytes.fromhex(hex_str.strip()) | |
| except ValueError: | |
| return None | |
| if len(data) < 4: | |
| return None | |
| first_octet = data[0] | |
| mti = first_octet & 0x03 | |
| # MTI=01 = SMS-SUBMIT | |
| if mti != 0x01: | |
| return None | |
| # TP-VPF (Validity Period Format) : bits 4-3 | |
| vpf = (first_octet >> 3) & 0x03 | |
| mr = data[1] # Message Reference | |
| da_len = data[2] # Nombre de chiffres dans TP-DA | |
| da_toa = data[3] # Type of Address | |
| # Nombre d'octets pour les chiffres BCD : ceil(da_len / 2) | |
| da_bytes = (da_len + 1) // 2 | |
| if len(data) < 4 + da_bytes: | |
| return None | |
| da_digits = decode_bcd_number(data[4:4 + da_bytes], da_len) | |
| # Type of Number (bits 6-4 du TOA) | |
| ton = (da_toa >> 4) & 0x07 | |
| # Si TON=1 (international), ajouter le + | |
| prefix = "+" if ton == 1 else "" | |
| # Extraire aussi le corps du message si possible | |
| offset = 4 + da_bytes | |
| result = { | |
| "mti": "SMS-SUBMIT", | |
| "mr": mr, | |
| "da_number": f"{prefix}{da_digits}", | |
| "da_ton": ton, | |
| "da_npi": da_toa & 0x0F, | |
| "da_raw": da_digits, | |
| } | |
| # Parser TP-PID, TP-DCS, TP-VP, TP-UDL, TP-UD | |
| if offset < len(data): | |
| result["tp_pid"] = data[offset] | |
| offset += 1 | |
| if offset < len(data): | |
| result["tp_dcs"] = data[offset] | |
| offset += 1 | |
| # VP dépend de vpf | |
| if vpf == 2 and offset < len(data): # Relative | |
| result["tp_vp"] = data[offset] | |
| offset += 1 | |
| elif vpf == 3 and offset + 7 <= len(data): # Absolute | |
| offset += 7 | |
| elif vpf == 1 and offset + 7 <= len(data): # Enhanced | |
| offset += 7 | |
| if offset < len(data): | |
| udl = data[offset] | |
| result["tp_udl"] = udl | |
| offset += 1 | |
| # Décoder le texte si DCS=0x00 (GSM 7-bit default alphabet) | |
| dcs = result.get("tp_dcs", 0) | |
| if dcs == 0x00 and offset < len(data): | |
| # GSM 7-bit → on laisse sms-pdu-decode s'en charger | |
| result["user_data_hex"] = data[offset:].hex() | |
| return result | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MO SMS Log Parser | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| class MOLogEntry: | |
| """Représente un SMS MO reçu par le proto-smsc-daemon.""" | |
| def __init__(self): | |
| self.timestamp = "" | |
| self.imsi = "" | |
| self.sm_rp_mr = "" | |
| self.sm_rp_da = "" # SMSC address | |
| self.sm_rp_oa = "" # Sender MSISDN | |
| self.sm_rp_oa_number = "" | |
| self.sm_rp_ui_len = 0 | |
| self.tpdu_hex = "" | |
| self.parsed_tpdu = None | |
| @property | |
| def destination_number(self) -> str: | |
| """Numéro destinataire extrait du TPDU.""" | |
| if self.parsed_tpdu: | |
| return self.parsed_tpdu.get("da_raw", "") | |
| return "" | |
| @property | |
| def sender_number(self) -> str: | |
| """Numéro expéditeur (SM-RP-OA).""" | |
| return self.sm_rp_oa_number | |
| def __repr__(self): | |
| return (f"MOLogEntry(from={self.sender_number}, " | |
| f"to={self.destination_number}, imsi={self.imsi})") | |
| def parse_mo_log_block(lines: list) -> Optional[MOLogEntry]: | |
| """Parse un bloc de log MO SMS (multi-lignes).""" | |
| entry = MOLogEntry() | |
| for line in lines: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| if line.endswith("Rx MO SM"): | |
| entry.timestamp = line.replace(" Rx MO SM", "") | |
| elif line.startswith("IMSI:"): | |
| entry.imsi = line.split(":", 1)[1].strip() | |
| elif line.startswith("SM-RP-MR:"): | |
| entry.sm_rp_mr = line.split(":", 1)[1].strip() | |
| elif line.startswith("SM-RP-DA:"): | |
| entry.sm_rp_da = line.split(":", 1)[1].strip() | |
| elif line.startswith("SM-RP-OA:"): | |
| parts = line.split() | |
| entry.sm_rp_oa = line.split(":", 1)[1].strip() | |
| # Dernier élément = numéro | |
| if parts: | |
| entry.sm_rp_oa_number = parts[-1] | |
| elif line.startswith("SM-RP-UI:"): | |
| try: | |
| entry.sm_rp_ui_len = int(line.split(":")[1].strip().split()[0]) | |
| except (ValueError, IndexError): | |
| pass | |
| elif all(c in "0123456789abcdefABCDEF" for c in line) and len(line) >= 4: | |
| # Ligne hex = TPDU brut | |
| entry.tpdu_hex = line | |
| entry.parsed_tpdu = parse_sms_submit_tpdu(line) | |
| if entry.imsi and entry.tpdu_hex: | |
| return entry | |
| return None | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Routing Table | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| class RoutingTable: | |
| """ | |
| Table de routage MSISDN → opérateur. | |
| Format du fichier sms-routing.conf : | |
| # Commentaires | |
| [operators] | |
| 1 = 172.20.0.11 # Op1 container IP | |
| 2 = 172.20.0.12 # Op2 container IP | |
| [routes] | |
| # prefix = operator_id | |
| 33601 = 1 # +33601xxxxxx → Op1 | |
| 33602 = 2 # +33602xxxxxx → Op2 | |
| 1001 = 1 # Extensions courtes Op1 | |
| 2001 = 2 # Extensions courtes Op2 | |
| [local] | |
| operator_id = 1 # ID de cet opérateur | |
| """ | |
| def __init__(self, config_path: str): | |
| self.operators: Dict[str, str] = {} # id → IP | |
| self.routes: list = [] # (prefix, operator_id) trié par longueur desc | |
| self.local_operator_id = "1" | |
| self._load(config_path) | |
| def _load(self, path: str): | |
| section = None | |
| try: | |
| with open(path, 'r') as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line or line.startswith('#'): | |
| continue | |
| if line.startswith('[') and line.endswith(']'): | |
| section = line[1:-1].lower() | |
| continue | |
| if '=' not in line: | |
| continue | |
| key, val = line.split('=', 1) | |
| key = key.strip() | |
| val = val.split('#')[0].strip() # Retirer commentaires | |
| if section == 'operators': | |
| self.operators[key] = val | |
| elif section == 'routes': | |
| self.routes.append((key, val)) | |
| elif section == 'local': | |
| if key == 'operator_id': | |
| self.local_operator_id = val | |
| # Trier les routes par longueur de préfixe décroissante (longest match) | |
| self.routes.sort(key=lambda x: len(x[0]), reverse=True) | |
| except FileNotFoundError: | |
| logging.warning(f"Routing config not found: {path}, using defaults") | |
| def lookup(self, msisdn: str) -> Optional[str]: | |
| """Retourne l'operator_id pour un MSISDN donné, ou None.""" | |
| # Nettoyer le numéro | |
| clean = msisdn.lstrip('+') | |
| for prefix, op_id in self.routes: | |
| if clean.startswith(prefix): | |
| return op_id | |
| return None | |
| def is_local(self, msisdn: str) -> bool: | |
| """Le destinataire est-il sur cet opérateur ?""" | |
| op = self.lookup(msisdn) | |
| return op == self.local_operator_id or op is None | |
| def get_operator_ip(self, op_id: str) -> Optional[str]: | |
| """IP du container de l'opérateur.""" | |
| return self.operators.get(op_id) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # HLR VTY — résolution MSISDN → IMSI | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def hlr_msisdn_to_imsi(msisdn: str, | |
| hlr_host: str = HLR_VTY_HOST, | |
| hlr_port: int = HLR_VTY_PORT) -> Optional[str]: | |
| clean = msisdn.lstrip('+') | |
| try: | |
| s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| s.settimeout(5) | |
| s.connect((hlr_host, hlr_port)) | |
| def recv_until_prompt(timeout=3): | |
| data = b"" | |
| deadline = time.time() + timeout | |
| while time.time() < deadline: | |
| try: | |
| chunk = s.recv(4096) | |
| if not chunk: | |
| break | |
| data += chunk | |
| decoded = data.decode("ascii", errors="replace") | |
| lines = decoded.strip().splitlines() | |
| if lines: | |
| last = lines[-1].strip() | |
| if last.endswith(">") or last.endswith("#"): | |
| break | |
| except socket.timeout: | |
| break | |
| return data.decode("ascii", errors="replace") | |
| # Banner | |
| recv_until_prompt() | |
| # Enable | |
| s.sendall(b"enable\r\n") | |
| recv_until_prompt() | |
| # Commande exacte supportée par ta version | |
| cmd = f"show subscriber msisdn {clean}\r\n" | |
| s.sendall(cmd.encode()) | |
| response = recv_until_prompt() | |
| s.close() | |
| logging.info(f"HLR lookup MSISDN={clean}") | |
| # Parse uniquement IMSI | |
| for line in response.splitlines(): | |
| stripped = line.strip() | |
| if stripped.startswith("IMSI:"): | |
| imsi = stripped.split(":", 1)[1].strip() | |
| if imsi.isdigit(): | |
| logging.info(f"HLR match: MSISDN {clean} → IMSI {imsi}") | |
| return imsi | |
| logging.warning(f"MSISDN {clean} not found in HLR") | |
| return None | |
| except Exception as e: | |
| logging.error(f"HLR VTY error for MSISDN {clean}: {e}") | |
| return None | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MT SMS Injection (local) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def inject_mt_sms(dest_imsi: str, message_text: str, from_number: str, | |
| sc_address: str, sendmt_socket: str = SENDMT_SOCKET) -> bool: | |
| """ | |
| Injecte un MT SMS localement via proto-smsc-sendmt. | |
| Pipeline : sms-encode-text | gen-sms-deliver-pdu | proto-smsc-sendmt | |
| """ | |
| try: | |
| # Pipeline shell | |
| cmd = ( | |
| f"sms-encode-text '{message_text}' " | |
| f"| gen-sms-deliver-pdu {from_number} " | |
| f"| proto-smsc-sendmt {sc_address} {dest_imsi} {sendmt_socket}" | |
| ) | |
| result = subprocess.run( | |
| cmd, shell=True, capture_output=True, text=True, timeout=10 | |
| ) | |
| if result.returncode == 0: | |
| logging.info(f"MT SMS injected: IMSI={dest_imsi}") | |
| return True | |
| else: | |
| logging.error(f"MT SMS injection failed: {result.stderr}") | |
| return False | |
| except Exception as e: | |
| logging.error(f"MT SMS injection error: {e}") | |
| return False | |
| def inject_mt_sms_raw(dest_imsi: str, tpdu_hex: str, sc_address: str, | |
| sendmt_socket: str = SENDMT_SOCKET) -> bool: | |
| """ | |
| Injecte un MT SMS à partir d'un TPDU brut (déjà encodé). | |
| On doit convertir le SMS-SUBMIT en SMS-DELIVER avant injection. | |
| Pour simplifier, on re-décode le texte et re-encode en SMS-DELIVER | |
| via les sms-coding-utils. | |
| """ | |
| # Pour l'instant, on utilise la méthode texte via inject_mt_sms() | |
| # Une version future pourrait manipuler le TPDU directement | |
| return False | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # TCP Relay Server — reçoit les MT SMS d'autres opérateurs | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| class RelayServer(threading.Thread): | |
| """ | |
| Serveur TCP qui reçoit les requêtes MT SMS des autres opérateurs. | |
| Protocole simplifié (JSON sur TCP) : | |
| { | |
| "type": "mt_sms", | |
| "dest_msisdn": "33602000001", | |
| "from_number": "33601000001", | |
| "message_text": "Bonjour depuis Op1!", | |
| "sc_address": "19990011444", | |
| "sender_imsi": "001010000000001" | |
| } | |
| """ | |
| def __init__(self, port: int, operator_id: str, sc_address: str): | |
| super().__init__(daemon=True) | |
| self.port = port | |
| self.operator_id = operator_id | |
| self.sc_address = sc_address | |
| self.running = True | |
| def run(self): | |
| server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
| server.bind(('0.0.0.0', self.port)) | |
| server.listen(5) | |
| server.settimeout(1.0) | |
| logging.info(f"Relay server listening on port {self.port}") | |
| while self.running: | |
| try: | |
| conn, addr = server.accept() | |
| threading.Thread( | |
| target=self._handle_client, | |
| args=(conn, addr), | |
| daemon=True | |
| ).start() | |
| except socket.timeout: | |
| continue | |
| except Exception as e: | |
| logging.error(f"Server error: {e}") | |
| server.close() | |
| def _handle_client(self, conn: socket.socket, addr: tuple): | |
| try: | |
| conn.settimeout(10) | |
| data = b"" | |
| while True: | |
| chunk = conn.recv(4096) | |
| if not chunk: | |
| break | |
| data += chunk | |
| # Messages JSON terminés par newline | |
| if b'\n' in data: | |
| break | |
| if not data: | |
| conn.close() | |
| return | |
| msg = json.loads(data.decode('utf-8').strip()) | |
| if msg.get("type") == "mt_sms": | |
| self._handle_mt_sms(msg, conn) | |
| else: | |
| response = {"status": "error", "reason": "unknown message type"} | |
| conn.sendall((json.dumps(response) + '\n').encode()) | |
| except Exception as e: | |
| logging.error(f"Client handler error: {e}") | |
| finally: | |
| conn.close() | |
| def _handle_mt_sms(self, msg: dict, conn: socket.socket): | |
| dest_msisdn = msg.get("dest_msisdn", "") | |
| from_number = msg.get("from_number", "") | |
| message_text = msg.get("message_text", "") | |
| logging.info(f"Incoming interop MT: {from_number} → {dest_msisdn}") | |
| # Résoudre MSISDN → IMSI dans notre HLR local | |
| dest_imsi = hlr_msisdn_to_imsi(dest_msisdn) | |
| if not dest_imsi: | |
| response = {"status": "error", "reason": f"MSISDN {dest_msisdn} not found"} | |
| conn.sendall((json.dumps(response) + '\n').encode()) | |
| logging.warning(f"MSISDN {dest_msisdn} not found in local HLR") | |
| return | |
| # Injecter le MT SMS localement | |
| success = inject_mt_sms( | |
| dest_imsi=dest_imsi, | |
| message_text=message_text, | |
| from_number=from_number, | |
| sc_address=self.sc_address | |
| ) | |
| response = { | |
| "status": "ok" if success else "error", | |
| "dest_imsi": dest_imsi, | |
| "reason": "" if success else "injection failed" | |
| } | |
| conn.sendall((json.dumps(response) + '\n').encode()) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # TCP Relay Client — envoie les MT SMS vers d'autres opérateurs | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def send_interop_mt(target_ip: str, target_port: int, dest_msisdn: str, | |
| from_number: str, message_text: str, sc_address: str, | |
| sender_imsi: str = "") -> bool: | |
| """Envoie une requête MT SMS au relay d'un autre opérateur via TCP.""" | |
| msg = { | |
| "type": "mt_sms", | |
| "dest_msisdn": dest_msisdn, | |
| "from_number": from_number, | |
| "message_text": message_text, | |
| "sc_address": sc_address, | |
| "sender_imsi": sender_imsi, | |
| } | |
| try: | |
| s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| s.settimeout(10) | |
| s.connect((target_ip, target_port)) | |
| s.sendall((json.dumps(msg) + '\n').encode()) | |
| # Attendre la réponse | |
| data = b"" | |
| while True: | |
| chunk = s.recv(4096) | |
| if not chunk: | |
| break | |
| data += chunk | |
| if b'\n' in data: | |
| break | |
| s.close() | |
| if data: | |
| response = json.loads(data.decode('utf-8').strip()) | |
| if response.get("status") == "ok": | |
| logging.info(f"Interop MT delivered: {from_number} → {dest_msisdn}") | |
| return True | |
| else: | |
| logging.error(f"Interop MT failed: {response.get('reason')}") | |
| return False | |
| return False | |
| except Exception as e: | |
| logging.error(f"Interop send error to {target_ip}: {e}") | |
| return False | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # GSM 7-bit Default Alphabet Decoder (basique) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| GSM7_BASIC = ( | |
| "@£$¥èéùìòÇ\nØø\rÅå" | |
| "Δ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ" | |
| " !\"#¤%&'()*+,-./" | |
| "0123456789:;<=>?" | |
| "¡ABCDEFGHIJKLMNO" | |
| "PQRSTUVWXYZÄÖÑܧ" | |
| "¿abcdefghijklmno" | |
| "pqrstuvwxyzäöñüà" | |
| ) | |
| def decode_gsm7(data: bytes, num_chars: int) -> str: | |
| """Décode du GSM 7-bit packed en texte.""" | |
| chars = [] | |
| bit_pos = 0 | |
| for i in range(num_chars): | |
| byte_idx = (bit_pos * 7) // 8 # Correction: bit_pos is char index | |
| # Calcul correct pour GSM 7-bit unpacking | |
| byte_offset = (i * 7) // 8 | |
| bit_offset = (i * 7) % 8 | |
| if byte_offset >= len(data): | |
| break | |
| val = (data[byte_offset] >> bit_offset) & 0x7F | |
| if bit_offset > 1 and byte_offset + 1 < len(data): | |
| val = ((data[byte_offset] >> bit_offset) | | |
| (data[byte_offset + 1] << (8 - bit_offset))) & 0x7F | |
| if val < len(GSM7_BASIC): | |
| chars.append(GSM7_BASIC[val]) | |
| else: | |
| chars.append('?') | |
| return ''.join(chars) | |
| def extract_text_from_tpdu(tpdu_hex: str) -> str: | |
| """ | |
| Tente d'extraire le texte d'un TPDU SMS-SUBMIT. | |
| Méthode de secours si sms-pdu-decode n'est pas disponible. | |
| """ | |
| parsed = parse_sms_submit_tpdu(tpdu_hex) | |
| if not parsed: | |
| return "" | |
| dcs = parsed.get("tp_dcs", 0) | |
| udl = parsed.get("tp_udl", 0) | |
| ud_hex = parsed.get("user_data_hex", "") | |
| if dcs == 0x00 and ud_hex: | |
| # GSM 7-bit | |
| try: | |
| ud_bytes = bytes.fromhex(ud_hex) | |
| return decode_gsm7(ud_bytes, udl) | |
| except Exception: | |
| pass | |
| return "" | |
| def extract_text_via_sms_decode(tpdu_hex: str) -> str: | |
| """ | |
| Utilise sms-pdu-decode (sms-coding-utils) pour extraire le texte. | |
| Plus fiable que notre décodeur maison. | |
| """ | |
| try: | |
| # Créer un fichier temporaire avec le format attendu | |
| # sms-pdu-decode -n attend juste le hex brut | |
| result = subprocess.run( | |
| ['sms-pdu-decode', '-n'], | |
| input=tpdu_hex + '\n', | |
| capture_output=True, text=True, timeout=5 | |
| ) | |
| if result.returncode == 0: | |
| # Le texte du message est après la dernière ligne vide | |
| lines = result.stdout.strip().split('\n') | |
| # Trouver la ligne "Length:" et prendre tout après | |
| text_lines = [] | |
| found_length = False | |
| for line in lines: | |
| if found_length and line.strip(): | |
| text_lines.append(line) | |
| if line.strip().startswith('Length:'): | |
| found_length = True | |
| if text_lines: | |
| return '\n'.join(text_lines).strip() | |
| except (FileNotFoundError, subprocess.TimeoutExpired): | |
| pass | |
| # Fallback vers notre décodeur | |
| return extract_text_from_tpdu(tpdu_hex) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MO Log Watcher — surveille le log MO et déclenche le routage | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| class MOLogWatcher(threading.Thread): | |
| """ | |
| Surveille le fichier log MO du proto-smsc-daemon. | |
| Parse les nouveaux blocs et les route vers le bon opérateur. | |
| """ | |
| def __init__(self, log_path: str, routing: RoutingTable, | |
| operator_id: str, sc_address: str): | |
| super().__init__(daemon=True) | |
| self.log_path = log_path | |
| self.routing = routing | |
| self.operator_id = operator_id | |
| self.sc_address = sc_address | |
| self.running = True | |
| self._position = 0 | |
| def run(self): | |
| logging.info(f"Watching MO log: {self.log_path}") | |
| # Attendre que le fichier existe | |
| while self.running and not Path(self.log_path).exists(): | |
| time.sleep(2) | |
| if not self.running: | |
| return | |
| # Se positionner à la fin du fichier (ne pas traiter l'historique) | |
| with open(self.log_path, 'r') as f: | |
| f.seek(0, 2) # EOF | |
| self._position = f.tell() | |
| logging.info(f"MO log watcher started (pos={self._position})") | |
| while self.running: | |
| try: | |
| self._check_new_entries() | |
| except Exception as e: | |
| logging.error(f"MO watcher error: {e}") | |
| time.sleep(MO_LOG_POLL_INTERVAL) | |
| def _check_new_entries(self): | |
| try: | |
| with open(self.log_path, 'r') as f: | |
| f.seek(self._position) | |
| new_data = f.read() | |
| self._position = f.tell() | |
| except FileNotFoundError: | |
| return | |
| if not new_data: | |
| return | |
| # Découper en blocs (séparés par des lignes contenant "Rx MO SM") | |
| blocks = [] | |
| current_block = [] | |
| for line in new_data.split('\n'): | |
| if "Rx MO SM" in line and current_block: | |
| blocks.append(current_block) | |
| current_block = [line] | |
| else: | |
| current_block.append(line) | |
| if current_block: | |
| blocks.append(current_block) | |
| for block in blocks: | |
| entry = parse_mo_log_block(block) | |
| if entry: | |
| self._route_mo_sms(entry) | |
| def _route_mo_sms(self, entry: MOLogEntry): | |
| dest = entry.destination_number | |
| sender = entry.sender_number | |
| if not dest: | |
| logging.warning(f"MO SMS sans destination parsable: {entry}") | |
| return | |
| logging.info(f"MO SMS: {sender} → {dest} (from IMSI {entry.imsi})") | |
| # Déterminer l'opérateur cible | |
| target_op = self.routing.lookup(dest) | |
| if target_op is None: | |
| logging.warning(f"No route for destination {dest}") | |
| return | |
| if target_op == self.operator_id: | |
| # Local delivery — le proto-smsc-daemon l'a déjà loggé | |
| # Pour un vrai SMSC, il faudrait aussi faire le MT delivery local | |
| logging.info(f"Local delivery for {dest} (same operator)") | |
| self._deliver_local(entry, dest) | |
| return | |
| # Inter-opérateur : envoyer au relay distant | |
| target_ip = self.routing.get_operator_ip(target_op) | |
| if not target_ip: | |
| logging.error(f"No IP for operator {target_op}") | |
| return | |
| logging.info(f"Interop route: {dest} → Op{target_op} ({target_ip})") | |
| # Extraire le texte du message | |
| message_text = extract_text_via_sms_decode(entry.tpdu_hex) | |
| if not message_text: | |
| message_text = extract_text_from_tpdu(entry.tpdu_hex) | |
| if not message_text: | |
| logging.warning(f"Could not decode message text from TPDU: {entry.tpdu_hex}") | |
| message_text = "[message non décodable]" | |
| # Envoyer via TCP au relay distant | |
| success = send_interop_mt( | |
| target_ip=target_ip, | |
| target_port=RELAY_TCP_PORT, | |
| dest_msisdn=dest, | |
| from_number=sender, | |
| message_text=message_text, | |
| sc_address=self.sc_address, | |
| sender_imsi=entry.imsi | |
| ) | |
| if success: | |
| logging.info(f"✓ Interop SMS delivered: {sender} → {dest} (Op{target_op})") | |
| else: | |
| logging.error(f"✗ Interop SMS failed: {sender} → {dest} (Op{target_op})") | |
| def _deliver_local(self, entry: MOLogEntry, dest_msisdn: str): | |
| """Livraison locale — résout MSISDN→IMSI et injecte MT.""" | |
| dest_imsi = hlr_msisdn_to_imsi(dest_msisdn) | |
| if not dest_imsi: | |
| logging.warning(f"Local MSISDN {dest_msisdn} not found in HLR") | |
| return | |
| message_text = extract_text_via_sms_decode(entry.tpdu_hex) | |
| if not message_text: | |
| message_text = extract_text_from_tpdu(entry.tpdu_hex) | |
| if not message_text: | |
| message_text = "[message non décodable]" | |
| inject_mt_sms( | |
| dest_imsi=dest_imsi, | |
| message_text=message_text, | |
| from_number=entry.sender_number, | |
| sc_address=self.sc_address | |
| ) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Main | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='SMS Interop Relay — routage inter-opérateur via proto-smsc-proto' | |
| ) | |
| parser.add_argument( | |
| '--config', '-c', | |
| default='/etc/osmocom/sms-routing.conf', | |
| help='Fichier de configuration du routage SMS' | |
| ) | |
| parser.add_argument( | |
| '--port', '-p', | |
| type=int, default=RELAY_TCP_PORT, | |
| help=f'Port TCP du relay (défaut: {RELAY_TCP_PORT})' | |
| ) | |
| parser.add_argument( | |
| '--mo-log', | |
| help='Chemin du log MO (défaut: /var/log/osmocom/mo-sms-op<N>.log)' | |
| ) | |
| parser.add_argument( | |
| '--operator-id', | |
| default=os.environ.get('OPERATOR_ID', '1'), | |
| help='ID opérateur (défaut: $OPERATOR_ID ou 1)' | |
| ) | |
| args = parser.parse_args() | |
| op_id = args.operator_id | |
| sc_address = f"1999001{op_id}444" | |
| mo_log = args.mo_log or f"/var/log/osmocom/mo-sms-op{op_id}.log" | |
| # Injecter l'operator_id dans le format de log | |
| old_factory = logging.getLogRecordFactory() | |
| def record_factory(*a, **kw): | |
| record = old_factory(*a, **kw) | |
| record.operator_id = op_id | |
| return record | |
| logging.setLogRecordFactory(record_factory) | |
| logging.info(f"SMS Interop Relay starting") | |
| logging.info(f" Operator : {op_id}") | |
| logging.info(f" SC-address : {sc_address}") | |
| logging.info(f" MO log : {mo_log}") | |
| logging.info(f" TCP port : {args.port}") | |
| logging.info(f" Config : {args.config}") | |
| # Charger la table de routage | |
| routing = RoutingTable(args.config) | |
| routing.local_operator_id = op_id | |
| logging.info(f" Operators : {routing.operators}") | |
| logging.info(f" Routes : {routing.routes}") | |
| # Démarrer le serveur TCP (réception MT inter-op) | |
| server = RelayServer( | |
| port=args.port, | |
| operator_id=op_id, | |
| sc_address=sc_address | |
| ) | |
| server.start() | |
| # Démarrer le watcher MO log (routage MO inter-op) | |
| watcher = MOLogWatcher( | |
| log_path=mo_log, | |
| routing=routing, | |
| operator_id=op_id, | |
| sc_address=sc_address | |
| ) | |
| watcher.start() | |
| # Boucle principale (attente signal) | |
| def signal_handler(sig, frame): | |
| logging.info("Shutting down...") | |
| watcher.running = False | |
| server.running = False | |
| sys.exit(0) | |
| signal.signal(signal.SIGINT, signal_handler) | |
| signal.signal(signal.SIGTERM, signal_handler) | |
| logging.info("Relay running. Ctrl+C to stop.") | |
| while True: | |
| time.sleep(1) | |
| if __name__ == '__main__': | |
| main() | |
| # ── INTER-STP ──────────────────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-inter-stp telnet 127.0.0.1 4239 | |
| # show cs7 instance 0 asp | |
| # show cs7 instance 0 as | |
| # show cs7 instance 0 route | |
| # ── OPÉRATEUR 1 — STP intra ────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-operator-1 telnet 127.0.0.1 4239 | |
| # show cs7 instance 0 asp | |
| # show cs7 instance 0 as | |
| # show cs7 instance 0 sccp users | |
| # show cs7 instance 0 sccp connections | |
| # ── OPÉRATEUR 1 — MSC ──────────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-operator-1 telnet 127.0.0.1 4254 | |
| # en | |
| # show cs7 instance 0 asp | |
| # show cs7 instance 0 sccp users | |
| # show cs7 instance 0 sccp connections | |
| # show subscriber all | |
| # show connection all | |
| # ── OPÉRATEUR 1 — BSC ──────────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-operator-1 telnet 127.0.0.1 4242 | |
| # en | |
| # show cs7 instance 0 asp | |
| # show cs7 instance 0 sccp users | |
| # show cs7 instance 0 sccp connections | |
| # show bts 0 | |
| # show trx 0 0 | |
| # ── OPÉRATEUR 2 — même chose ────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-operator-2 telnet 127.0.0.1 4239 | |
| sudo docker exec -it osmo-operator-2 telnet 127.0.0.1 4254 | |
| sudo docker exec -it osmo-operator-2 telnet 127.0.0.1 4242 | |
| # ── ONE-LINER : dump tout sans interaction ──────────────────────────────────── | |
| for container in osmo-operator-1 osmo-operator-2; do | |
| echo "=== $container STP ===" | |
| sudo docker exec $container sh -c \ | |
| 'echo -e "show cs7 instance 0 asp\nshow cs7 instance 0 sccp users\nshow cs7 instance 0 sccp connections\n" | telnet 127.0.0.1 4239 2>/dev/null' \ | |
| | grep -v "^Trying\|^Connected\|^Escape" | |
| echo "=== $container MSC ===" | |
| sudo docker exec $container sh -c \ | |
| 'echo -e "enable\nshow cs7 instance 0 sccp users\nshow cs7 instance 0 sccp connections\nshow subscriber all\n" | telnet 127.0.0.1 4254 2>/dev/null' \ | |
| | grep -v "^Trying\|^Connected\|^Escape" | |
| done | |
| echo "=== inter-STP ASP ===" | |
| sudo docker exec osmo-inter-stp sh -c \ | |
| 'echo -e "show cs7 instance 0 asp\nshow cs7 instance 0 as\n" | telnet 127.0.0.1 4239 2>/dev/null' \ | |
| | grep -v "^Trying\|^Connected\|^Escape" | |
| #!/bin/bash | |
| # run.sh — Orchestrateur intra-container Osmocom | |
| # | |
| # Séquence garantie : | |
| # 1. Reset | |
| # 2. generate_ms_configs → UN SEUL mobile_combined.cfg avec N blocs ms | |
| # 3. osmo-start.sh → STP HLR MGW MSC BSC GGSN SGSN PCU | |
| # (PAS bts-trx ni sip-connector) | |
| # 4. fake_trx démarre → bind UDP 5700 (+trx additionnels via --trx) | |
| # 5. osmo-bts-trx start → se connecte à fake_trx déjà prêt ✓ | |
| # 6. trxcon × N (chacun -C) → N process, N sockets L1CTL | |
| # 7. mobile (UN SEUL) → charge tous les ms depuis le combined cfg | |
| # 8. sip-connector → après MNCC socket prêt | |
| # 9. Asterisk → après sip-connector | |
| # 10. SMSC + gapk | |
| # | |
| # Architecture OsmocomBB multi-MS : | |
| # | |
| # fake_trx (1 process) | |
| # │ (N TRX internes : --trx pour chaque MS ≥ 2) | |
| # │ | |
| # ┌────┼────┐ | |
| # trxcon1 trxcon2 trxcon3 ← chacun avec -C, son port TRX, son socket L1CTL | |
| # │ │ │ | |
| # └────┼────┘ | |
| # mobile (1 process) | |
| # mobile_combined.cfg : ms 1 / ms 2 / ms 3 | |
| # | |
| # OsmocomBB a été conçu pour UN téléphone Calypso physique → une seule | |
| # couche radio. fake_trx a gardé cette architecture : 1 transceiver, | |
| # N MS logiques au-dessus. Donc : 1 mobile process, 1 fichier cfg, N blocs ms. | |
| # | |
| # DEBUG MODE: DEBUG=1 ./run.sh | |
| set -euo pipefail | |
| # ── LOGGING ──────────────────────────────────────────────────────────────── | |
| # Tout stdout+stderr → fichier log + console | |
| # Lire depuis le host : docker exec <container> cat /var/log/osmocom/run.sh.log | |
| LOG_FILE="/var/log/osmocom/run.sh.log" | |
| mkdir -p "$(dirname "$LOG_FILE")" | |
| exec > >(tee -a "$LOG_FILE") 2>&1 | |
| echo "" | |
| echo "========================================" | |
| echo " run.sh démarré à $(date '+%Y-%m-%d %H:%M:%S')" | |
| echo "========================================" | |
| echo "" | |
| # ── DEBUG MODE ───────────────────────────────────────────────────────────── | |
| if [[ -n "${DEBUG:-}" ]]; then | |
| set -x | |
| PS4='[DEBUG-run] + ${BASH_SOURCE}:${LINENO}: ' | |
| echo "=== RUN.SH: MODE DEBUG ACTIVÉ (Op${OPERATOR_ID:-?}) ===" | |
| echo "" | |
| fi | |
| SESSION="osmocom" | |
| GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' | |
| FAKETRX_PY="${FAKETRX_PY:-/opt/GSM/osmocom-bb/src/target/trx_toolkit/fake_trx.py}" | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| N_MS="${N_MS:-1}" | |
| # Base du socket L1CTL — chaque trxcon -C -s crée un socket | |
| # trxcon MS1 → ${L2_SOCK_BASE}_1, MS2 → ${L2_SOCK_BASE}_2, etc. | |
| L2_SOCK_BASE="/tmp/osmocom_l2" | |
| SAP_SOCK_BASE="/tmp/osmocom_sap" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] OPERATOR_ID=$OPERATOR_ID N_MS=$N_MS FAKETRX_PY=$FAKETRX_PY" | |
| if ! [[ "$N_MS" =~ ^[0-9]+$ ]] || [ "$N_MS" -lt 1 ] || [ "$N_MS" -gt 9 ]; then | |
| echo -e "${YELLOW}[WARN] N_MS='${N_MS}' invalide → forcé à 1${NC}" | |
| N_MS=1 | |
| fi | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Génération d'UN SEUL mobile_combined.cfg contenant N blocs ms | |
| # | |
| # Format VTY OsmocomBB : | |
| # ┌── partie globale (line vty, gps, no hide-default) | |
| # ├── ms 1 | |
| # │ ├── layer2-socket, sap-socket, sim, imei, support {...exit}, test-sim {...exit} | |
| # │ └── exit ← sans indentation = ferme le bloc ms | |
| # ├── ms 2 (optionnel) | |
| # │ └── exit | |
| # └── ! | |
| # | |
| # Format ancien (configs/) : le bloc ms se termine par 'end' au lieu de 'exit' | |
| # | |
| # On extrait SEULEMENT le premier bloc ms comme template, puis on le | |
| # duplique N fois avec des credentials/sockets différents. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| generate_ms_configs() { | |
| local base_cfg="/root/.osmocom/bb/mobile.cfg" | |
| local combined_cfg="/root/.osmocom/bb/mobile_combined.cfg" | |
| echo -e " ${CYAN}[cfg]${NC} Source : ${base_cfg}" | |
| echo -e " ${CYAN}[cfg]${NC} Sortie : ${combined_cfg}" | |
| if [ ! -f "$base_cfg" ]; then | |
| echo -e "${RED}[ERR] mobile.cfg introuvable : ${base_cfg}${NC}" | |
| echo -e "${RED} ls /root/.osmocom/bb/ :${NC}" | |
| ls -la /root/.osmocom/bb/ 2>&1 | sed 's/^/ /' | |
| return 1 | |
| fi | |
| echo -e " ${CYAN}[cfg]${NC} Taille : $(wc -l < "$base_cfg") lignes, $(wc -c < "$base_cfg") octets" | |
| # ── Dump rapide du fichier source ───────────────────────────────────── | |
| echo -e " ${CYAN}[cfg]${NC} Contenu (premières / dernières lignes) :" | |
| head -5 "$base_cfg" | sed 's/^/ | /' | |
| echo " | ..." | |
| tail -5 "$base_cfg" | sed 's/^/ | /' | |
| # ── Extraction des valeurs de référence ─────────────────────────────── | |
| # IMPORTANT: || true sur chaque grep — sinon set -e tue le script | |
| # silencieusement si un pattern ne matche pas (grep retourne 1) | |
| local ref_imsi ref_imei ref_ki ref_l2 ref_sap | |
| ref_imsi=$(grep -oP '^\s+imsi\s+\K[0-9]+' "$base_cfg" 2>/dev/null | head -1 || true) | |
| ref_imei=$(grep -oP '^\s+imei \K[0-9]+' "$base_cfg" 2>/dev/null | head -1 || true) | |
| ref_ki=$(grep -oP '^\s+ki comp128 \K[0-9a-f ]+' "$base_cfg" 2>/dev/null | head -1 \ | |
| | sed 's/[[:space:]]*$//' || true) | |
| ref_l2=$(grep -oP 'layer2-socket \K\S+' "$base_cfg" 2>/dev/null | head -1 || true) | |
| ref_l2="${ref_l2:-/tmp/osmocom_l2}" | |
| ref_sap=$(grep -oP 'sap-socket \K\S+' "$base_cfg" 2>/dev/null | head -1 || true) | |
| # Fallback si grep -P (PCRE) pas dispo dans le container | |
| if [ -z "$ref_imsi" ]; then | |
| echo -e " ${YELLOW}[ref] grep -oP échoué → fallback grep -E${NC}" | |
| ref_imsi=$(grep -E 'imsi [0-9]+' "$base_cfg" 2>/dev/null | head -1 | awk '{print $NF}' || true) | |
| ref_imei=$(grep -E 'imei [0-9]+' "$base_cfg" 2>/dev/null | head -1 | awk '{print $2}' || true) | |
| ref_ki=$(grep -E 'ki comp128' "$base_cfg" 2>/dev/null | head -1 \ | |
| | sed 's/.*ki comp128 //' | sed 's/[[:space:]]*$//' || true) | |
| ref_l2=$(grep -E 'layer2-socket' "$base_cfg" 2>/dev/null | head -1 | awk '{print $2}' || true) | |
| ref_l2="${ref_l2:-/tmp/osmocom_l2}" | |
| ref_sap=$(grep -E 'sap-socket' "$base_cfg" 2>/dev/null | head -1 | awk '{print $2}' || true) | |
| fi | |
| echo -e " ${CYAN}[ref]${NC} IMSI : '${ref_imsi:-${RED}VIDE${NC}}'" | |
| echo -e " ${CYAN}[ref]${NC} IMEI : '${ref_imei:-${RED}VIDE${NC}}'" | |
| echo -e " ${CYAN}[ref]${NC} KI : '${ref_ki:-${RED}VIDE${NC}}'" | |
| echo -e " ${CYAN}[ref]${NC} L2 sock : '${ref_l2}'" | |
| echo -e " ${CYAN}[ref]${NC} SAP sock: '${ref_sap:-ABSENT → injection auto}'" | |
| if [ -z "$ref_imsi" ] || [ -z "$ref_imei" ] || [ -z "$ref_ki" ]; then | |
| echo -e "${RED}[ERR] Impossible d'extraire IMSI/IMEI/KI de ${base_cfg}${NC}" | |
| echo -e "${RED} Lignes contenant imsi/imei/ki :${NC}" | |
| grep -inE '(imsi|imei|ki comp128)' "$base_cfg" 2>/dev/null | head -10 | sed 's/^/ /' || true | |
| echo -e "${RED} → Les __PLACEHOLDERS__ ont-ils été substitués par start.sh ?${NC}" | |
| return 1 | |
| fi | |
| local mcc mnc | |
| mcc=$(grep -E '^\s+rplmn ' "$base_cfg" 2>/dev/null | awk '{print $2}' | head -1 || true) | |
| mnc=$(grep -E '^\s+rplmn ' "$base_cfg" 2>/dev/null | awk '{print $3}' | head -1 || true) | |
| mcc="${mcc:-001}"; mnc="${mnc:-01}" | |
| echo -e " ${CYAN}[ref]${NC} MCC/MNC : ${mcc} / ${mnc}" | |
| # ── Extraction partie globale (tout avant le premier 'ms N') ────────── | |
| local global_line | |
| global_line=$(grep -n '^ms [0-9]' "$base_cfg" 2>/dev/null | head -1 | cut -d: -f1 || true) | |
| if [ -z "$global_line" ]; then | |
| echo -e "${RED}[ERR] Pas de bloc 'ms' trouvé dans ${base_cfg}${NC}" | |
| echo -e "${RED} Le fichier ne contient pas de ligne commençant par 'ms [0-9]'${NC}" | |
| echo -e "${RED} grep 'ms ' résultat :${NC}" | |
| grep -n 'ms ' "$base_cfg" 2>/dev/null | head -5 | sed 's/^/ /' || true | |
| return 1 | |
| fi | |
| echo -e " ${CYAN}[parse]${NC} Premier 'ms' trouvé ligne ${global_line}" | |
| echo -e " ${CYAN}[parse]${NC} Partie globale : lignes 1-$(( global_line - 1 ))" | |
| head -n $(( global_line - 1 )) "$base_cfg" > "$combined_cfg" | |
| # ── Template = premier bloc ms SEULEMENT ────────────────────────────── | |
| # S'arrête au premier ^exit$ ou ^end$ ou au prochain ^ms [0-9] | |
| # Les ' exit' indentés (support/test-sim) sont ignorés. | |
| local ms_template | |
| ms_template=$(awk ' | |
| /^ms [0-9]/ { | |
| if (found) { exit } | |
| found=1; printing=1 | |
| } | |
| printing { print } | |
| /^(exit|end)$/ && printing && found { printing=0; exit } | |
| ' "$base_cfg") | |
| if [ -z "$ms_template" ]; then | |
| echo -e "${RED}[ERR] Extraction du bloc ms échouée (awk n'a rien trouvé)${NC}" | |
| echo -e "${RED} Lignes autour de 'ms 1' :${NC}" | |
| sed -n "$(( global_line - 2 )),$(( global_line + 5 ))p" "$base_cfg" | sed 's/^/ | /' | |
| return 1 | |
| fi | |
| local tpl_lines | |
| tpl_lines=$(echo "$ms_template" | wc -l) | |
| local tpl_first | |
| tpl_first=$(echo "$ms_template" | head -1) | |
| local tpl_last | |
| tpl_last=$(echo "$ms_template" | tail -1) | |
| echo -e " ${CYAN}[parse]${NC} Template ms : ${tpl_lines} lignes" | |
| echo -e " ${CYAN}[parse]${NC} première : '${tpl_first}'" | |
| echo -e " ${CYAN}[parse]${NC} dernière : '${tpl_last}'" | |
| # Vérifier que le template se termine bien par exit ou end | |
| if ! echo "$tpl_last" | grep -qE '^(exit|end)$'; then | |
| echo -e " ${YELLOW}[WARN] Template ne se termine pas par exit/end → dernière ligne : '${tpl_last}'${NC}" | |
| fi | |
| # ── Écriture des N blocs ms ─────────────────────────────────────────── | |
| echo "" | |
| for ms_idx in $(seq 1 "${N_MS}"); do | |
| local l2_sock="${L2_SOCK_BASE}_${ms_idx}" | |
| local sap_sock="${SAP_SOCK_BASE}_${ms_idx}" | |
| local msin; msin=$(printf '%04d%06d' "${OPERATOR_ID}" "${ms_idx}") | |
| local new_imsi="${mcc}${mnc}${msin}" | |
| local new_imei; new_imei="3589250059$(printf '%02d%02d' "${OPERATOR_ID}" "${ms_idx}")0" | |
| local new_ki; new_ki=$(printf '00 11 22 33 44 55 66 77 88 99 aa bb cc dd %02x ff' \ | |
| "${ms_idx}") | |
| local ms_block | |
| ms_block=$(echo "$ms_template" \ | |
| | sed \ | |
| -e "s|^ms [0-9]\+|ms ${ms_idx}|" \ | |
| -e "s|layer2-socket ${ref_l2}|layer2-socket ${l2_sock}|" \ | |
| -e "s|imsi ${ref_imsi}|imsi ${new_imsi}|" \ | |
| -e "s|imei ${ref_imei}[[:space:]]*[0-9]*|imei ${new_imei}|" \ | |
| -e "s|ki comp128 ${ref_ki}|ki comp128 ${new_ki}|" \ | |
| ) | |
| # sap-socket : remplacer si présent, sinon injecter après layer2-socket | |
| if [ -n "$ref_sap" ]; then | |
| ms_block=$(echo "$ms_block" \ | |
| | sed "s|sap-socket ${ref_sap}|sap-socket ${sap_sock}|") | |
| else | |
| ms_block=$(echo "$ms_block" \ | |
| | sed "/layer2-socket/a\\ sap-socket ${sap_sock}") | |
| fi | |
| echo "$ms_block" >> "$combined_cfg" | |
| echo "" >> "$combined_cfg" | |
| echo -e " ${CYAN}[MS${ms_idx}]${NC} IMSI=${new_imsi} IMEI=${new_imei} KI=…$(printf '%02x ff' "${ms_idx}")" | |
| echo -e " ${CYAN}[MS${ms_idx}]${NC} L1CTL=${l2_sock} SAP=${sap_sock}" | |
| # Vérification post-sed : les substitutions ont-elles marché ? | |
| if echo "$ms_block" | grep -q "${ref_imsi}"; then | |
| echo -e " ${YELLOW}[WARN MS${ms_idx}] IMSI ref '${ref_imsi}' encore présent après sed !${NC}" | |
| fi | |
| if echo "$ms_block" | grep -q "layer2-socket ${ref_l2}\$"; then | |
| echo -e " ${YELLOW}[WARN MS${ms_idx}] layer2-socket ref '${ref_l2}' encore présent après sed !${NC}" | |
| fi | |
| done | |
| # ── Vérification finale ─────────────────────────────────────────────── | |
| echo "" | |
| local n_ms_found n_exit_found | |
| n_ms_found=$(grep -c '^ms [0-9]' "$combined_cfg") | |
| n_exit_found=$(grep -cE '^(exit|end)$' "$combined_cfg") | |
| echo -e " ${CYAN}[check]${NC} Blocs ms dans combined : ${n_ms_found} (attendu: ${N_MS})" | |
| echo -e " ${CYAN}[check]${NC} Terminateurs exit/end : ${n_exit_found} (attendu: ${N_MS})" | |
| echo -e " ${CYAN}[check]${NC} Taille combined : $(wc -l < "$combined_cfg") lignes" | |
| if [ "$n_ms_found" -ne "$N_MS" ]; then | |
| echo -e " ${RED}[ERR] Nombre de blocs ms incorrect !${NC}" | |
| fi | |
| if [ "$n_exit_found" -ne "$N_MS" ]; then | |
| echo -e " ${YELLOW}[WARN] Nombre de exit/end != N_MS${NC}" | |
| fi | |
| echo -e " ${GREEN}→ ${combined_cfg} (${N_MS} blocs ms)${NC}" | |
| } | |
| # ── Helper : reset TRX via VTY OsmoBTS (port 4236) ────────────────────────── | |
| reset_trx() { | |
| local vty_host="127.0.0.1" vty_port=4236 | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] reset_trx: attente ${vty_host}:${vty_port}" | |
| echo -ne " Attente VTY OsmoBTS (${vty_host}:${vty_port})" | |
| elapsed=0 | |
| while ! bash -c "echo >/dev/tcp/${vty_host}/${vty_port}" 2>/dev/null; do | |
| sleep 1; elapsed=$((elapsed + 1)); echo -n "." | |
| if [ "$elapsed" -ge 60 ]; then | |
| echo -e " ${RED}TIMEOUT — reset TRX ignoré${NC}"; return 1 | |
| fi | |
| done | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| cat > /tmp/trx_reset.vty << 'VTYCMDS' | |
| enable | |
| trx 0 reset | |
| end | |
| VTYCMDS | |
| if command -v nc >/dev/null 2>&1; then | |
| (sleep 1; cat /tmp/trx_reset.vty; sleep 1) \ | |
| | nc -q2 "${vty_host}" "${vty_port}" 2>/dev/null \ | |
| | grep -vE "^(OsmoBTS|Welcome|VTY|\s*$)" \ | |
| | sed "s/^/ /" || true | |
| else | |
| (sleep 1; cat /tmp/trx_reset.vty; sleep 2) \ | |
| | telnet "${vty_host}" "${vty_port}" 2>/dev/null \ | |
| | grep -vE "^(Trying|Connected|Escape|OsmoBTS|Welcome)" \ | |
| | sed "s/^/ /" || true | |
| fi | |
| rm -f /tmp/trx_reset.vty | |
| echo -e " ${GREEN}✓ TRX reset envoyé${NC}" | |
| } | |
| # ── Helper : attendre qu'un port TCP soit ouvert ────────────────────────────── | |
| wait_port() { | |
| local host="$1" port="$2" label="$3" timeout="${4:-60}" | |
| elapsed=0 | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] wait_port: $host:$port ($label)" | |
| echo -ne " Attente ${label} (${host}:${port})" | |
| while ! bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null; do | |
| sleep 1; elapsed=$((elapsed + 1)) | |
| echo -n "." | |
| if [ "$elapsed" -ge "$timeout" ]; then | |
| echo -e " ${RED}TIMEOUT${NC}"; return 1 | |
| fi | |
| done | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| } | |
| # ── Helper : attendre qu'un port UDP soit bindé ─────────────────────────────── | |
| wait_udp() { | |
| local port="$1" label="$2" timeout="${3:-30}" | |
| elapsed=0 | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] wait_udp: UDP $port ($label)" | |
| echo -ne " Attente ${label} (UDP ${port})" | |
| while ! ss -unlp | grep -q ":${port} "; do | |
| sleep 1; elapsed=$((elapsed + 1)) | |
| echo -n "." | |
| if [ "$elapsed" -ge "$timeout" ]; then | |
| echo -e " ${RED}TIMEOUT${NC}"; return 1 | |
| fi | |
| done | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # MAIN | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # ── 1. Reset ────────────────────────────────────────────────────────────────── | |
| echo -e "${GREEN}=== [1/10] Reset ===${NC}" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] Killing old processes..." | |
| usermod -u 0 -o osmocom 2>/dev/null || true | |
| pkill -9 -f fake_trx.py 2>/dev/null || true | |
| pkill -9 -f "mobile -c" 2>/dev/null || true | |
| pkill -9 -f trxcon 2>/dev/null || true | |
| sleep 1 | |
| tmux kill-server 2>/dev/null || true | |
| sleep 0.3 | |
| tmux start-server | |
| tmux new-session -d -s "$SESSION" -n main | |
| # ── 2. Génération config MS combinée ───────────────────────────────────────── | |
| echo -e "${GREEN}=== [2/10] Génération config MS combinée (N=${N_MS}) ===${NC}" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] Calling generate_ms_configs..." | |
| generate_ms_configs | |
| echo "" | |
| # ── 3. Core Osmocom (sans bts-trx ni sip-connector) ────────────────────────── | |
| echo -e "${GREEN}=== [3/10] Core Osmocom ===${NC}" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] Launching /etc/osmocom/osmo-start.sh..." | |
| /etc/osmocom/osmo-start.sh | |
| # ── 4. FakeTRX ─────────────────────────────────────────────────────────────── | |
| echo -e "${GREEN}=== [4/10] FakeTRX ===${NC}" | |
| tmux new-window -t "$SESSION" -n faketrx | |
| # Bind explicite sur 127.0.0.1, adresses BTS et BB explicites | |
| # -b = fake_trx bind address | |
| # -R = BTS remote (osmo-bts-trx) | |
| # -r = BB remote (trxcon) | |
| # -P = BTS base port (défaut 5700, osmo-bts-trx s'y connecte) | |
| # -p = BB base port (défaut 6700, trxcon MS1 s'y connecte) | |
| FAKETRX_CMD="python3 ${FAKETRX_PY} -b 127.0.0.1 -R 127.0.0.1 -r 127.0.0.1 -P 5700 -p 6700" | |
| for ms_idx in $(seq 2 "${N_MS}"); do | |
| trx_port=$(( 6700 + (ms_idx - 1) * 20 )) | |
| FAKETRX_CMD="${FAKETRX_CMD} --trx 127.0.0.1:${trx_port}" | |
| done | |
| echo -e " ${CYAN}Cmd :${NC} ${FAKETRX_CMD}" | |
| tmux send-keys -t "${SESSION}:faketrx" "${FAKETRX_CMD}" C-m | |
| wait_udp 5700 "fake_trx UDP" 30 || true | |
| # ── 5. OsmoBTS-TRX (APRÈS fake_trx confirmé) ───────────────────────────────── | |
| echo -e "${GREEN}=== [5/10] OsmoBTS-TRX ===${NC}" | |
| systemctl start osmo-bts-trx | |
| wait_port 127.0.0.1 4241 "OsmoBTS VTY" 60 || true | |
| # ── 6. trxcon — N instances séparées, chacune avec -C ───────────────────────── | |
| # | |
| # OsmocomBB : 1 trxcon par MS, chacun -C 1 (1 seul client L1CTL) : | |
| # trxcon -C 1 -b 127.0.0.1 -i 127.0.0.1 -p 6700 -s /tmp/osmocom_l2_1 (MS1) | |
| # trxcon -C 1 -b 127.0.0.1 -i 127.0.0.1 -p 6720 -s /tmp/osmocom_l2_2 (MS2) | |
| # trxcon -C 1 -b 127.0.0.1 -i 127.0.0.1 -p 6740 -s /tmp/osmocom_l2_3 (MS3) | |
| # | |
| # -b = trxcon bind address (écoute réponses de fake_trx) | |
| # -i = TRX remote address (envoie vers fake_trx) | |
| # -p = TRX base port (doit matcher le port BB de fake_trx pour ce MS) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo -e "${GREEN}=== [6/10] trxcon (${N_MS} instances, -C 1) ===${NC}" | |
| tmux new-window -t "$SESSION" -n trxcon | |
| for ms_idx in $(seq 1 "${N_MS}"); do | |
| l2_sock="${L2_SOCK_BASE}_${ms_idx}" | |
| trx_port=$(( 6700 + (ms_idx - 1) * 20 )) | |
| # Toujours explicite : bind, remote, port, max-clients=1 | |
| trxcon_cmd="trxcon -C 1 -b 127.0.0.1 -i 127.0.0.1 -p ${trx_port} -s ${l2_sock} -F 100" | |
| echo -e " ${CYAN}[trxcon${ms_idx}]${NC} ${trxcon_cmd}" | |
| # Premier dans le pane existant, suivants dans des splits | |
| if [ "$ms_idx" -gt 1 ]; then | |
| tmux split-window -v -t "${SESSION}:trxcon" | |
| tmux select-layout -t "${SESSION}:trxcon" even-vertical | |
| fi | |
| pane_idx=$(( ms_idx - 1 )) | |
| tmux send-keys -t "${SESSION}:trxcon.${pane_idx}" \ | |
| "echo '=== trxcon MS${ms_idx} ===' && sleep 2 && ${trxcon_cmd}" C-m | |
| done | |
| # ── 7. mobile (UN SEUL process, tous les ms) ────────────────────────────────── | |
| echo -e "${GREEN}=== [7/10] mobile (1 process, ${N_MS} MS) ===${NC}" | |
| combined_cfg="/root/.osmocom/bb/mobile_combined.cfg" | |
| # Attendre que les sockets L1CTL existent (créés par trxcon) | |
| echo -ne " Attente sockets L1CTL" | |
| elapsed=0 | |
| all_ready=false | |
| while [ "$elapsed" -lt 30 ]; do | |
| all_ready=true | |
| for ms_idx in $(seq 1 "${N_MS}"); do | |
| if [ ! -S "${L2_SOCK_BASE}_${ms_idx}" ]; then | |
| all_ready=false | |
| break | |
| fi | |
| done | |
| if $all_ready; then break; fi | |
| sleep 1; elapsed=$((elapsed + 1)); echo -n "." | |
| done | |
| if $all_ready; then | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| else | |
| # Diagnostic : lister ce que trxcon a réellement créé | |
| echo -e " ${YELLOW}partiel${NC}" | |
| echo -e " ${CYAN}Sockets L1CTL présents :${NC}" | |
| ls -la ${L2_SOCK_BASE}* 2>/dev/null | sed 's/^/ /' || echo " (aucun)" | |
| echo -e " ${YELLOW}→ Si nommage différent, ajuster L2_SOCK_BASE dans run.sh${NC}" | |
| fi | |
| tmux new-window -t "$SESSION" -n mobile | |
| tmux send-keys -t "${SESSION}:mobile" \ | |
| "echo '=== mobile — ${N_MS} MS ===' && sleep 3 && mobile -c ${combined_cfg}" C-m | |
| echo -e " ${GREEN}→ mobile -c ${combined_cfg}${NC}" | |
| echo -e " ${CYAN}Contrôle VTY :${NC} enable → show ms → ms 1 → network select 1" | |
| # ── 8. SIP-Connector (AVANT Asterisk — MNCC doit être prêt) ────────────────── | |
| echo -e "${GREEN}=== [8/10] SIP-Connector ===${NC}" | |
| echo -ne " Attente MNCC socket /tmp/msc_mncc" | |
| elapsed=0 | |
| while [ ! -S /tmp/msc_mncc ] && [ $elapsed -lt 30 ]; do | |
| sleep 1; elapsed=$((elapsed + 1)); echo -n "." | |
| done | |
| [ -S /tmp/msc_mncc ] && echo -e " ${GREEN}OK${NC}" || echo -e " ${YELLOW}absent${NC}" | |
| systemctl start osmo-sip-connector | |
| wait_port 127.0.0.1 4255 "SIP-Connector VTY" 30 || true | |
| # ── 9. Asterisk (APRÈS SIP-Connector) ──────────────────────────────────────── | |
| echo -e "${GREEN}=== [9/10] Asterisk ===${NC}" | |
| tmux new-window -t "$SESSION" -n asterisk | |
| tmux send-keys -t "${SESSION}:asterisk" \ | |
| "pkill asterisk 2>/dev/null; sleep 2; pkill -9 asterisk 2>/dev/null; sleep 1; rm -f /var/lib/asterisk/astdb.sqlite3; asterisk -cvvv" C-m | |
| # ── 10. SMSC + gapk ────────────────────────────────────────────────────────── | |
| echo -e "${GREEN}=== [10/10] SMSC + gapk ===${NC}" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] Starting SMSC and gapk..." | |
| tmux new-window -t "$SESSION" -n smsc | |
| if [ -f /etc/osmocom/smsc-start.sh ]; then | |
| tmux send-keys -t "${SESSION}:smsc" "/etc/osmocom/smsc-start.sh" C-m | |
| else | |
| tmux send-keys -t "${SESSION}:smsc" "echo 'SMSC non disponible'" C-m | |
| fi | |
| tmux new-window -t "$SESSION" -n gapk | |
| tmux send-keys -t "${SESSION}:gapk" \ | |
| "echo '=== gapk audio ===' && sleep 3 && (gapk-start.sh auto 2>/dev/null || echo 'gapk non disponible')" C-m | |
| tmux select-window -t "${SESSION}:mobile" | |
| echo "" | |
| echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}" | |
| echo -e "${GREEN}║ Opérateur ${OPERATOR_ID} — Stack prête (${N_MS} MS) ║${NC}" | |
| echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}" | |
| echo "" | |
| echo -e " ${CYAN}Fenêtres :${NC} faketrx trxcon mobile asterisk smsc gapk" | |
| echo "" | |
| echo -e " ${CYAN}Contrôle MS :${NC} dans la fenêtre mobile (VTY) :" | |
| echo -e " enable → show ms → ms 1 → network select 1" | |
| echo "" | |
| echo -e " ${CYAN}Vérif TRX :${NC} fenêtre faketrx → chercher 'RSP POWERON'" | |
| echo -e " ${CYAN}Vérif TCH :${NC} telnet 127.0.0.1 4242 → show bts 0" | |
| echo -e " ${CYAN}Vérif PJSIP:${NC} asterisk -rx 'pjsip show endpoints'" | |
| echo -e " ${CYAN}VTY MSC :${NC} telnet 127.0.0.1 4254" | |
| echo -e " ${CYAN}VTY HLR :${NC} telnet 127.0.0.1 4258" | |
| echo "" | |
| echo -e " ${CYAN}Navigation :${NC} Ctrl-b w Ctrl-b n/p" | |
| echo "" | |
| tmux select-window -t "${SESSION}:mobile" | |
| tmux attach-session -t "$SESSION" | |
| #!/bin/bash | |
| # gapk-start.sh — Gestionnaire audio GSM (osmo-gapk ↔ ALSA/RTP) | |
| # | |
| # Intégration native dans la stack osmo_egprs : | |
| # • Mode `auto` : se greffe sur OsmoMGW pour détecter les appels actifs | |
| # et démarre/arrête gapk automatiquement via hook MGCP. | |
| # • Mode `call` : appel bidirectionnel ALSA ↔ RTP (RX + TX en background). | |
| # • Mode `monitor`: écoute passive RTP → ALSA (foreground). | |
| # • Mode `record` : enregistre flux RTP → fichier .gsm (foreground). | |
| # • Mode `playback`: injecte fichier .gsm → RTP (foreground). | |
| # • Mode `loopback`: boucle codec ALSA micro → GSM → HP (test). | |
| # • Mode `mgw-ports`: liste les endpoints/ports RTP OsmoMGW actifs. | |
| # • Mode `stop` : arrête toutes les instances en background. | |
| # • Mode `status`: état des instances en cours. | |
| # | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # ARCHITECTURE AUDIO | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # | |
| # Micro ALSA ──[PCM 8kHz]──► gapk-TX ──[GSM FR]──► RTP ──► OsmoMGW | |
| # OsmoMGW ──► RTP ──[GSM FR]──► gapk-RX ──[PCM 8kHz]──► HP ALSA | |
| # | |
| # OsmoMGW alloue les ports RTP dynamiquement (plage 4002–16001). | |
| # Les endpoints actifs sont visibles via : | |
| # echo "show mgcp" | telnet 127.0.0.1 4243 | |
| # | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # INTÉGRATION NATIVE (mode auto) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # | |
| # gapk-start.sh auto [FORMAT] [ALSA_DEV] | |
| # | |
| # Surveille OsmoMGW toutes les 2 secondes. | |
| # Dès qu'un endpoint passe en état "active" (connexion RTP ouverte), | |
| # démarre gapk RX+TX sur les ports détectés. | |
| # Dès que l'endpoint redevient idle, arrête les instances gapk. | |
| # | |
| # Ce mode tourne en foreground — idéal pour la fenêtre tmux [4]. | |
| # | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| set -euo pipefail | |
| GAPK_DEFAULT_FORMAT="${GAPK_FORMAT:-gsmfr}" | |
| GAPK_DEFAULT_DEVICE="${GAPK_ALSA_DEV:-${ALSA_CARD:-default}}" | |
| GAPK_LOG_DIR="${GAPK_LOG_DIR:-/var/log/osmocom}" | |
| GAPK_REC_DIR="${GAPK_REC_DIR:-/var/lib/gapk}" | |
| GAPK_FRAME_MS=20 | |
| MGW_VTY_HOST="127.0.0.1" | |
| MGW_VTY_PORT="4243" | |
| AUTO_POLL_INTERVAL="${GAPK_POLL_INTERVAL:-2}" | |
| GAPK_PID_RX="/var/run/gapk-rx.pid" | |
| GAPK_PID_TX="/var/run/gapk-tx.pid" | |
| GAPK_AUTO_LOCK="/var/run/gapk-auto.lock" | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m'; NC='\033[0m'; BOLD='\033[1m' | |
| mkdir -p "$GAPK_LOG_DIR" "$GAPK_REC_DIR" | |
| log_info() { echo -e "${GREEN}[gapk]${NC} $*"; } | |
| log_warn() { echo -e "${YELLOW}[gapk]${NC} $*"; } | |
| log_error() { echo -e "${RED}[gapk ERROR]${NC} $*" >&2; } | |
| log_auto() { echo -e "${CYAN}[gapk-auto]${NC} $(date '+%H:%M:%S') $*"; } | |
| # ── Vérifications ────────────────────────────────────────────────────────────── | |
| check_gapk() { | |
| command -v osmo-gapk >/dev/null 2>&1 || { | |
| log_error "osmo-gapk introuvable." | |
| log_error "Vérifier que le Dockerfile inclut l'étape osmo-gapk --enable-alsa." | |
| exit 1 | |
| } | |
| } | |
| check_alsa() { | |
| local dev="${1:-default}" | |
| if [ ! -d /dev/snd ]; then | |
| log_warn "/dev/snd absent — container lancé sans --device /dev/snd" | |
| log_warn "Modes record/playback fonctionnent sans ALSA." | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # ── Cycle de vie background ──────────────────────────────────────────────────── | |
| run_bg() { | |
| local pid_file="$1"; shift | |
| local log_file="$1"; shift | |
| osmo-gapk "$@" >> "$log_file" 2>&1 & | |
| echo $! > "$pid_file" | |
| log_info "PID $(cat "$pid_file") → $(basename "$log_file")" | |
| } | |
| stop_pid() { | |
| local pid_file="$1" | |
| [ -f "$pid_file" ] || return 0 | |
| local pid; pid=$(cat "$pid_file") | |
| if kill -0 "$pid" 2>/dev/null; then | |
| kill "$pid" 2>/dev/null && log_info "Arrêté PID $pid" || true | |
| fi | |
| rm -f "$pid_file" | |
| } | |
| is_running() { | |
| local pid_file="$1" | |
| [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # mgw-ports — interroge OsmoMGW VTY et retourne les ports RTP actifs | |
| # | |
| # Sortie : une ligne par endpoint actif "RX_PORT TX_IP:TX_PORT" | |
| # (RX = port local MGW pour downlink ; TX = destination uplink MGW) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_mgw_ports() { | |
| local quiet="${1:-}" | |
| local mgw_out | |
| mgw_out=$(echo "show mgcp" | telnet "$MGW_VTY_HOST" "$MGW_VTY_PORT" 2>/dev/null \ | |
| | grep -v "^Trying\|^Connected\|^Escape\|Welcome\|OsmoMGW[>#]") || true | |
| if [ -z "$mgw_out" ]; then | |
| [ -z "$quiet" ] && echo "(MGW non disponible ou aucun endpoint)" | |
| return 1 | |
| fi | |
| # Extrait les endpoints actifs avec leur port RTP local | |
| # Format VTY OsmoMGW : | |
| # Endpoint: rtpbridge/*@mgw | |
| # Conn: 0x... (RHCF) port: 4002 ... | |
| # Conn: 0x... (LCLS) port: 4004 RTP-IP: 127.0.0.1 RTP-Port: 4006 | |
| if [ -z "$quiet" ]; then | |
| echo -e "${CYAN}── Endpoints OsmoMGW actifs ──${NC}" | |
| echo "$mgw_out" | grep -E "Endpoint:|port:|RTP" | sed 's/^/ /' || true | |
| else | |
| # Mode machine : sortie parsée "RX_PORT TX_IP:TX_PORT" | |
| echo "$mgw_out" | awk ' | |
| /Endpoint:/ { ep=$0 } | |
| /port:/ { | |
| match($0, /port: ([0-9]+)/, p) | |
| match($0, /RTP-IP: ([0-9.]+) RTP-Port: ([0-9]+)/, r) | |
| if (p[1] != "" && r[1] != "") | |
| print p[1], r[1]":"r[2] | |
| } | |
| ' | |
| fi | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # call — appel bidirectionnel ALSA ↔ RTP | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_call() { | |
| local rx_port="${1:?RTP_RX_PORT requis (port MGW downlink)}" | |
| local tx_dest="${2:?RTP_TX_DEST:PORT requis (dest MGW uplink)}" | |
| local fmt="${3:-$GAPK_DEFAULT_FORMAT}" | |
| local dev="${4:-$GAPK_DEFAULT_DEVICE}" | |
| check_gapk | |
| check_alsa "$dev" || true | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| echo -e "${CYAN}${BOLD}" | |
| echo "╔══════════════════════════════════════════╗" | |
| echo "║ gapk — Appel bidirectionnel ALSA↔RTP ║" | |
| echo "╚══════════════════════════════════════════╝" | |
| echo -e "${NC}" | |
| printf " Codec : ${CYAN}%s${NC}\n" "$fmt" | |
| printf " Périph. : ${CYAN}%s${NC}\n" "$dev" | |
| printf " RX écoute : ${CYAN}0.0.0.0:%s${NC} (downlink MGW → HP)\n" "$rx_port" | |
| printf " TX envoi : ${CYAN}%s${NC} (micro → uplink MGW)\n" "$tx_dest" | |
| echo "" | |
| local log_rx="${GAPK_LOG_DIR}/gapk-rx.log" | |
| local log_tx="${GAPK_LOG_DIR}/gapk-tx.log" | |
| log_info "RX : rtp://0.0.0.0:${rx_port} → alsa://${dev}" | |
| run_bg "$GAPK_PID_RX" "$log_rx" \ | |
| -f "$fmt" \ | |
| -i "rtp://0.0.0.0:${rx_port}/${GAPK_FRAME_MS}" \ | |
| -o "alsa://${dev}/${GAPK_FRAME_MS}" | |
| sleep 0.3 | |
| log_info "TX : alsa://${dev} → rtp://${tx_dest}" | |
| run_bg "$GAPK_PID_TX" "$log_tx" \ | |
| -f "$fmt" \ | |
| -i "alsa://${dev}/${GAPK_FRAME_MS}" \ | |
| -o "rtp://${tx_dest}/${GAPK_FRAME_MS}" | |
| echo "" | |
| log_info "Appel actif. Arrêter : gapk-start.sh stop" | |
| log_info "Logs RX : tail -f $log_rx" | |
| log_info "Logs TX : tail -f $log_tx" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # auto — intégration native : surveille MGW et gère gapk automatiquement | |
| # | |
| # Démarre gapk dès qu'un endpoint MGW passe actif (connexion RTP ouverte). | |
| # Arrête gapk quand l'endpoint redevient idle. | |
| # Tourne en foreground dans la fenêtre tmux [4]. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_auto() { | |
| local fmt="${1:-$GAPK_DEFAULT_FORMAT}" | |
| local dev="${2:-$GAPK_DEFAULT_DEVICE}" | |
| check_gapk | |
| check_alsa "$dev" || log_warn "ALSA absent — mode RTP-only si appel détecté" | |
| echo -e "${CYAN}${BOLD}" | |
| echo "╔══════════════════════════════════════════════════╗" | |
| echo "║ gapk-auto — Intégration native OsmoMGW ║" | |
| echo "║ Surveille les endpoints RTP actifs ║" | |
| echo "╚══════════════════════════════════════════════════╝" | |
| echo -e "${NC}" | |
| log_auto "Codec=${fmt} Périph=${dev} Poll=${AUTO_POLL_INTERVAL}s" | |
| log_auto "Ctrl+C pour arrêter" | |
| echo "" | |
| touch "$GAPK_AUTO_LOCK" | |
| local prev_rx_port="" | |
| cleanup_auto() { | |
| log_auto "Arrêt..." | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| rm -f "$GAPK_AUTO_LOCK" | |
| exit 0 | |
| } | |
| trap cleanup_auto SIGINT SIGTERM | |
| while [ -f "$GAPK_AUTO_LOCK" ]; do | |
| # Lire le premier endpoint actif (port RX + TX dest) | |
| local first_active | |
| first_active=$(mode_mgw_ports "quiet" 2>/dev/null | head -1) || first_active="" | |
| if [ -n "$first_active" ]; then | |
| local rx_port tx_dest | |
| rx_port=$(echo "$first_active" | awk '{print $1}') | |
| tx_dest=$(echo "$first_active" | awk '{print $2}') | |
| if [ "$rx_port" != "$prev_rx_port" ]; then | |
| if [ -n "$prev_rx_port" ]; then | |
| log_auto "Changement d'endpoint (${prev_rx_port} → ${rx_port}) — restart gapk" | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| fi | |
| log_auto "Endpoint actif détecté : RX=${rx_port} TX=${tx_dest}" | |
| log_auto "Démarrage gapk (${fmt}) ..." | |
| local log_rx="${GAPK_LOG_DIR}/gapk-rx.log" | |
| local log_tx="${GAPK_LOG_DIR}/gapk-tx.log" | |
| # RX : MGW → ALSA (downlink) | |
| run_bg "$GAPK_PID_RX" "$log_rx" \ | |
| -f "$fmt" \ | |
| -i "rtp://0.0.0.0:${rx_port}/${GAPK_FRAME_MS}" \ | |
| -o "alsa://${dev}/${GAPK_FRAME_MS}" | |
| sleep 0.3 | |
| # TX : ALSA → MGW (uplink) | |
| run_bg "$GAPK_PID_TX" "$log_tx" \ | |
| -f "$fmt" \ | |
| -i "alsa://${dev}/${GAPK_FRAME_MS}" \ | |
| -o "rtp://${tx_dest}/${GAPK_FRAME_MS}" | |
| prev_rx_port="$rx_port" | |
| log_auto "Audio actif (PID RX=$(cat $GAPK_PID_RX 2>/dev/null) TX=$(cat $GAPK_PID_TX 2>/dev/null))" | |
| fi | |
| # Vérifier que les processus sont toujours vivants | |
| if ! is_running "$GAPK_PID_RX" || ! is_running "$GAPK_PID_TX"; then | |
| log_warn "Processus gapk mort de façon inattendue — restart" | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| prev_rx_port="" | |
| fi | |
| else | |
| # Pas d'endpoint actif | |
| if [ -n "$prev_rx_port" ]; then | |
| log_auto "Endpoint disparu (port ${prev_rx_port}) — arrêt gapk" | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| prev_rx_port="" | |
| fi | |
| fi | |
| sleep "$AUTO_POLL_INTERVAL" | |
| done | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # monitor — écoute passive RTP → ALSA (foreground) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_monitor() { | |
| local rx_port="${1:?RTP_RX_PORT requis}" | |
| local fmt="${2:-$GAPK_DEFAULT_FORMAT}" | |
| local dev="${3:-$GAPK_DEFAULT_DEVICE}" | |
| check_gapk | |
| check_alsa "$dev" || true | |
| stop_pid "$GAPK_PID_RX" | |
| log_info "Monitor : rtp://0.0.0.0:${rx_port} → alsa://${dev} [${fmt}]" | |
| log_info "Ctrl+C pour arrêter" | |
| exec osmo-gapk \ | |
| -f "$fmt" \ | |
| -i "rtp://0.0.0.0:${rx_port}/${GAPK_FRAME_MS}" \ | |
| -o "alsa://${dev}/${GAPK_FRAME_MS}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # record — enregistre flux RTP → fichier .gsm brut | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_record() { | |
| local rx_port="${1:?RTP_RX_PORT requis}" | |
| local outfile="${2:-${GAPK_REC_DIR}/record_$(date '+%Y%m%d_%H%M%S').gsm}" | |
| local fmt="${3:-$GAPK_DEFAULT_FORMAT}" | |
| check_gapk | |
| stop_pid "$GAPK_PID_RX" | |
| log_info "Record : rtp://0.0.0.0:${rx_port} → ${outfile} [${fmt}]" | |
| log_info "Ctrl+C pour arrêter" | |
| log_info "Relire : gapk-start.sh playback ${outfile} <DEST:PORT>" | |
| exec osmo-gapk \ | |
| -f "$fmt" \ | |
| -i "rtp://0.0.0.0:${rx_port}/${GAPK_FRAME_MS}" \ | |
| -o "rawfile://${outfile}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # playback — injecte fichier .gsm dans un flux RTP | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_playback() { | |
| local infile="${1:?INPUT_FILE requis}" | |
| local tx_dest="${2:?RTP_TX_DEST:PORT requis}" | |
| local fmt="${3:-$GAPK_DEFAULT_FORMAT}" | |
| check_gapk | |
| [ -f "$infile" ] || { log_error "Fichier introuvable : $infile"; exit 1; } | |
| log_info "Playback : ${infile} → rtp://${tx_dest} [${fmt}]" | |
| log_info "Ctrl+C pour arrêter" | |
| exec osmo-gapk \ | |
| -f "$fmt" \ | |
| -i "rawfile://${infile}" \ | |
| -o "rtp://${tx_dest}/${GAPK_FRAME_MS}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # loopback — ALSA micro → encode → décode → ALSA HP (test codec) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_loopback() { | |
| local fmt="${1:-$GAPK_DEFAULT_FORMAT}" | |
| local dev="${2:-$GAPK_DEFAULT_DEVICE}" | |
| check_gapk | |
| check_alsa "$dev" || { log_error "ALSA requis pour le loopback."; exit 1; } | |
| echo -e "${YELLOW}${BOLD}⚠ UTILISEZ DES ÉCOUTEURS — risque de larsen !${NC}" | |
| echo "" | |
| log_info "Loopback : alsa://${dev} → [${fmt}] → alsa://${dev}" | |
| log_info "Ctrl+C pour arrêter" | |
| echo "" | |
| exec osmo-gapk \ | |
| -f "$fmt" \ | |
| -i "alsa://${dev}/${GAPK_FRAME_MS}" \ | |
| -o "alsa://${dev}/${GAPK_FRAME_MS}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # stop / status / list | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_stop() { | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| rm -f "$GAPK_AUTO_LOCK" | |
| pkill -x osmo-gapk 2>/dev/null && log_info "Toutes instances arrêtées" \ | |
| || log_info "Aucune instance active" | |
| } | |
| mode_status() { | |
| echo -e "${CYAN}── État osmo-gapk ──${NC}" | |
| local any=0 | |
| for f in "$GAPK_PID_RX" "$GAPK_PID_TX"; do | |
| [ -f "$f" ] || continue | |
| local pid; pid=$(cat "$f") | |
| if kill -0 "$pid" 2>/dev/null; then | |
| local cmdline; cmdline=$(cat "/proc/${pid}/cmdline" 2>/dev/null \ | |
| | tr '\0' ' ' | cut -c1-80 || echo "?") | |
| echo -e " ${GREEN}●${NC} PID ${pid} [$(basename "$f")] ${cmdline}" | |
| any=1 | |
| else | |
| echo -e " ${RED}✗${NC} PID ${pid} mort"; rm -f "$f" | |
| fi | |
| done | |
| [ -f "$GAPK_AUTO_LOCK" ] && echo -e " ${CYAN}●${NC} mode AUTO actif" && any=1 | |
| [ $any -eq 0 ] && echo -e " ${YELLOW}Aucune instance active${NC}" | |
| if pgrep -x osmo-gapk >/dev/null 2>&1; then | |
| echo "" | |
| echo -e "${CYAN}Processus détectés :${NC}" | |
| ps -o pid,args --no-headers -C osmo-gapk 2>/dev/null | sed 's/^/ /' || true | |
| fi | |
| } | |
| mode_list_codecs() { | |
| check_gapk | |
| echo -e "${CYAN}Codecs disponibles :${NC}" | |
| osmo-gapk --list-codecs 2>&1 || \ | |
| osmo-gapk --help 2>&1 | grep -A30 -i "format\|codec" || true | |
| } | |
| mode_list_devices() { | |
| echo -e "${CYAN}── Périphériques ALSA ──${NC}" | |
| echo "Lecture (HP) :" | |
| aplay -L 2>/dev/null | head -20 || echo " (aplay indisponible)" | |
| echo "" | |
| echo "Capture (micro) :" | |
| arecord -L 2>/dev/null | head -20 || echo " (arecord indisponible)" | |
| echo "" | |
| echo "Cartes son :" | |
| cat /proc/asound/cards 2>/dev/null || echo " (relancer avec --device /dev/snd)" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Usage | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| usage() { | |
| cat <<'EOF' | |
| Usage: gapk-start.sh <mode> [options] | |
| auto [FORMAT] [ALSA_DEV] | |
| Intégration native : surveille MGW, démarre/arrête gapk auto. | |
| Tourne en foreground (idéal fenêtre tmux). | |
| ex: gapk-start.sh auto gsmfr default | |
| call <RX_PORT> <TX_DEST:PORT> [FORMAT] [ALSA_DEV] | |
| Appel bidirectionnel ALSA ↔ RTP (background). | |
| ex: gapk-start.sh call 4002 127.0.0.1:4004 gsmfr | |
| monitor <RX_PORT> [FORMAT] [ALSA_DEV] | |
| Écoute passive RTP → ALSA (foreground). | |
| ex: gapk-start.sh monitor 4002 | |
| record <RX_PORT> [FICHIER] [FORMAT] | |
| Enregistre flux RTP → fichier .gsm. | |
| ex: gapk-start.sh record 4002 /var/lib/gapk/appel.gsm | |
| playback <FICHIER> <TX_DEST:PORT> [FORMAT] | |
| Injecte fichier .gsm → RTP. | |
| ex: gapk-start.sh playback /var/lib/gapk/appel.gsm 127.0.0.1:4002 | |
| loopback [FORMAT] [ALSA_DEV] | |
| Loopback codec ALSA (ÉCOUTEURS OBLIGATOIRES). | |
| mgw-ports Liste les endpoints/ports RTP OsmoMGW actifs. | |
| stop Arrête toutes les instances. | |
| status État des instances. | |
| list-codecs Codecs disponibles. | |
| list-devices Périphériques ALSA. | |
| Formats : gsmfr (défaut) | gsmefr | gsmhr | pcm8 | pcm16 | |
| Variables : GAPK_FORMAT, GAPK_ALSA_DEV, ALSA_CARD, GAPK_POLL_INTERVAL | |
| Ports RTP OsmoMGW (manuel) : | |
| echo "show mgcp" | telnet 127.0.0.1 4243 | |
| EOF | |
| } | |
| # ── Dispatch ────────────────────────────────────────────────────────────────── | |
| MODE="${1:-help}"; shift 2>/dev/null || true | |
| case "$MODE" in | |
| auto) mode_auto "$@" ;; | |
| call) mode_call "$@" ;; | |
| monitor) mode_monitor "$@" ;; | |
| record) mode_record "$@" ;; | |
| playback) mode_playback "$@" ;; | |
| loopback) mode_loopback "$@" ;; | |
| mgw-ports) mode_mgw_ports "" ;; | |
| stop) mode_stop ;; | |
| status) mode_status ;; | |
| list-codecs) mode_list_codecs ;; | |
| list-devices) mode_list_devices ;; | |
| help|-h|--help) usage ;; | |
| *) log_error "Mode inconnu : $MODE"; echo ""; usage; exit 1 ;; | |
| esac | |
| #!/bin/bash | |
| cmd="${1:-status}" | |
| set -ex | |
| systemctl daemon-reload | |
| systemctl $cmd osmo-hlr \ | |
| osmo-msc \ | |
| osmo-mgw \ | |
| osmo-stp \ | |
| osmo-bsc \ | |
| osmo-ggsn \ | |
| osmo-sgsn \ | |
| osmo-sip-connector \ | |
| osmo-bts-trx \ | |
| osmo-pcu | |
| #!/bin/bash | |
| # send-mt-sms.sh — Envoi de SMS MT local via proto-smsc-sendmt | |
| # Usage : ./send-mt-sms.sh <imsi> <message> [from_number] | |
| set -euo pipefail | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| SC_ADDRESS="1999001${OPERATOR_ID}444" | |
| SENDMT_SOCKET="/tmp/sendmt_socket" | |
| if [ $# -lt 2 ]; then | |
| echo "Usage: $0 <imsi> <message> [from_number]" | |
| echo " Ex: $0 001010000000001 'Bonjour!'" | |
| exit 1 | |
| fi | |
| DEST_IMSI="$1"; MESSAGE="$2"; FROM="${3:-${SC_ADDRESS}}" | |
| [ ! -S "$SENDMT_SOCKET" ] && { echo "ERREUR: $SENDMT_SOCKET absent"; exit 1; } | |
| sms-encode-text "$MESSAGE" \ | |
| | gen-sms-deliver-pdu "$FROM" \ | |
| | proto-smsc-sendmt "$SC_ADDRESS" "$DEST_IMSI" "$SENDMT_SOCKET" | |
| echo "MT SMS envoyé → IMSI=$DEST_IMSI" | |
| #!/bin/bash | |
| # osmo-start.sh — Démarrage séquencé du Core Osmocom | |
| # | |
| # Ordre de dépendance : | |
| # STP (4239) → HLR (4258) → MGW (4243) | |
| # ↓ | |
| # MSC (4254) → BSC (4242) | |
| # ↓ | |
| # GGSN (4260) → SGSN (4245) → PCU (4239 bts) | |
| # | |
| # Chaque service attend le VTY du précédent avant de démarrer. | |
| # BTS-TRX et SIP-connector sont gérés par run.sh (dépendent de fake_trx / Asterisk). | |
| # | |
| # Appelé par : run.sh (étape 3) | |
| set -e | |
| GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m'; NC='\033[0m' | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| # ── Helper : attendre qu'un port TCP soit ouvert ────────────────────────────── | |
| wait_port() { | |
| local host="$1" port="$2" label="$3" timeout="${4:-30}" | |
| local elapsed=0 | |
| echo -ne " Attente ${label} (${host}:${port})" | |
| while ! bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null; do | |
| sleep 1; elapsed=$((elapsed + 1)) | |
| echo -n "." | |
| if [ "$elapsed" -ge "$timeout" ]; then | |
| echo -e " ${RED}TIMEOUT${NC}" | |
| return 1 | |
| fi | |
| done | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| } | |
| # ── Helper : démarrer un service et vérifier ────────────────────────────────── | |
| start_svc() { | |
| local svc="$1" vty_port="$2" label="$3" timeout="${4:-30}" | |
| echo -e " ${CYAN}${label}${NC}" | |
| systemctl start "$svc" || { | |
| echo -e " ${RED}✗ systemctl start ${svc} échoué${NC}" | |
| journalctl -u "$svc" -n 20 --no-pager >&2 | |
| return 1 | |
| } | |
| if [ -n "$vty_port" ]; then | |
| wait_port 127.0.0.1 "$vty_port" "$label VTY" "$timeout" || { | |
| echo -e " ${YELLOW}[WARN] VTY :${vty_port} non accessible après ${timeout}s${NC}" >&2 | |
| journalctl -u "$svc" -n 10 --no-pager >&2 | |
| return 1 | |
| } | |
| fi | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo -e "${GREEN}=== Core Osmocom — Op${OPERATOR_ID} ===${NC}" | |
| echo "" | |
| # ── 1. Réseau TUN (APN0 pour GGSN) ─────────────────────────────────────────── | |
| echo -e "${CYAN}[1/4] Interface TUN${NC}" | |
| if ip link show apn0 > /dev/null 2>&1; then | |
| ip link del dev apn0 | |
| fi | |
| ip tuntap add dev apn0 mode tun | |
| ip addr add 176.16.32.0/24 dev apn0 | |
| ip link set dev apn0 up | |
| echo -e " ${GREEN}✓${NC} apn0 up" | |
| # ── 2. Signalisation (STP → HLR → MGW) ─────────────────────────────────────── | |
| # | |
| # STP doit être prêt en premier : tout le SS7 passe par lui. | |
| # HLR doit être prêt avant MSC : MSC se connecte au HLR au démarrage. | |
| # MGW doit être prêt avant MSC : MSC ouvre un MGCP vers MGW. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo "" | |
| echo -e "${CYAN}[2/4] Signalisation (STP → HLR → MGW)${NC}" | |
| start_svc osmo-stp 4239 "OsmoSTP" 30 || true | |
| start_svc osmo-hlr 4258 "OsmoHLR" 30 || { | |
| echo -e " ${RED}[ERR] HLR indispensable pour MSC — abandon${NC}" | |
| exit 1 | |
| } | |
| start_svc osmo-mgw 4243 "OsmoMGW" 20 || true | |
| # ── 3. Core Network (MSC → BSC) ────────────────────────────────────────────── | |
| # | |
| # MSC doit être prêt avant BSC : BSC se connecte au MSC via A-interface. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo "" | |
| echo -e "${CYAN}[3/4] Core Network (MSC → BSC)${NC}" | |
| start_svc osmo-msc 4254 "OsmoMSC" 30 || true | |
| start_svc osmo-bsc 4242 "OsmoBSC" 30 || true | |
| # ── 4. Data (GGSN → SGSN → PCU) ────────────────────────────────────────────── | |
| echo "" | |
| echo -e "${CYAN}[4/4] Data (GGSN → SGSN → PCU)${NC}" | |
| start_svc osmo-ggsn 4260 "OsmoGGSN" 20 || true | |
| start_svc osmo-sgsn 4245 "OsmoSGSN" 20 || true | |
| start_svc osmo-pcu "" "OsmoPCU" 0 || true | |
| sleep 1 | |
| chmod 777 /tmp/pcu_bts 2>/dev/null || true | |
| # NOTE : osmo-bts-trx et osmo-sip-connector sont intentionnellement | |
| # absents ici. run.sh les démarre dans l'ordre correct : | |
| # fake_trx → wait_udp 5700 → osmo-bts-trx (évite la race condition TRX) | |
| # Asterisk → osmo-sip-connector (évite le MNCC connect avant SIP UP) | |
| # ── Résumé ──────────────────────────────────────────────────────────────────── | |
| echo "" | |
| echo -e "${GREEN}=== Vérification ===${NC}" | |
| SERVICES="osmo-stp osmo-hlr osmo-mgw osmo-msc osmo-bsc osmo-ggsn osmo-sgsn osmo-pcu" | |
| for svc in $SERVICES; do | |
| if systemctl is-active --quiet "$svc"; then | |
| echo -e " ${GREEN}✓${NC} ${svc}" | |
| else | |
| echo -e " ${RED}✗${NC} ${svc}" | |
| fi | |
| done | |
| echo "" | |
| echo -e "${GREEN}Core Osmocom prêt. BTS et SIP connector gérés par run.sh.${NC}" | |
| #!/bin/bash | |
| # sms-routing-setup.sh — Gestion complète du routage SMS inter-opérateur | |
| # | |
| # Fonctions exportables appelées depuis start.sh : | |
| # sms_routing_generate <op_id> <n_ops> <destdir> [op1_nms op2_nms ...] | |
| # sms_routing_validate <conf_file> | |
| # sms_routing_summary <n_ops> [op1_nms op2_nms ...] | |
| # | |
| # Formules COMMUNES (identiques à run.sh, hlr-feed-subscribers.sh) : | |
| # MSISDN = op_id * 10000 + ms_idx | |
| # IMSI = MCC(3) + MNC(2) + printf('%04d%06d', op_id, ms_idx) | |
| # KI = 00 11 22 33 44 55 66 77 88 99 aa bb cc dd <ms_hex> <op_hex> | |
| # | |
| # Architecture de routage : | |
| # ┌─────────────────────────────────────────────────────────────────────┐ | |
| # │ MS (op1, ms1) MSISDN=10001 envoie à MSISDN=20002 (op2, ms2) │ | |
| # │ → proto-smsc-daemon (op1) → MO log │ | |
| # │ → sms-interop-relay.py (op1) : lookup prefix 20002 → op2 │ | |
| # │ → TCP 172.20.0.12:7890 │ | |
| # │ → sms-interop-relay.py (op2) : MSISDN→IMSI via HLR VTY │ | |
| # │ → proto-smsc-sendmt (op2) → HLR (op2) → MS (op2, ms2) │ | |
| # └─────────────────────────────────────────────────────────────────────┘ | |
| # | |
| # Usage autonome (test/debug) : | |
| # bash sms-routing-setup.sh generate 1 3 /tmp/sms-cfg 2 2 3 | |
| # bash sms-routing-setup.sh validate /tmp/sms-cfg/sms-routing-op1.conf | |
| # bash sms-routing-setup.sh summary 3 2 2 3 | |
| set -euo pipefail | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Helpers internes | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| _sms_op_backbone_ip() { echo "172.20.0.$((10 + $1))"; } | |
| _sms_op_msisdn() { echo $(( $1 * 10000 + $2 )); } # op_id ms_idx | |
| _sms_op_ms_imsi() { # mcc mnc op_id ms_idx | |
| local mcc=$1 mnc=$2 op=$3 ms=$4 | |
| printf '%s%s%04d%06d' "$mcc" "$mnc" "$op" "$ms" | |
| } | |
| _sms_sc_address() { printf '1999001%s444' "$1"; } # op_id | |
| _log_ok() { echo -e " ${GREEN}✓${NC} $*"; } | |
| _log_warn() { echo -e " ${YELLOW}⚠${NC} $*"; } | |
| _log_err() { echo -e " ${RED}✗${NC} $*" >&2; } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_generate | |
| # | |
| # Génère configs/sms-routing-op<N>.conf pour chaque opérateur. | |
| # Le fichier de chaque opérateur contient TOUTES les routes (locales + distantes) | |
| # et est monté dans /etc/osmocom/sms-routing.conf du container. | |
| # | |
| # Usage : | |
| # sms_routing_generate <op_id> <n_ops> <destdir> [ms_counts...] | |
| # ms_counts : nombre de MS par opérateur (1 valeur par opérateur) | |
| # ex: "2 3 1" → op1=2MS, op2=3MS, op3=1MS | |
| # | |
| # Sortie : | |
| # <destdir>/sms-routing-op<op_id>.conf | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_generate() { | |
| local op_id="$1" | |
| local n_ops="$2" | |
| local destdir="$3" | |
| shift 3 | |
| local ms_counts=("$@") # tableau : ms_counts[0]=nMS_op1, [1]=nMS_op2, ... | |
| mkdir -p "$destdir" | |
| local outfile="${destdir}/sms-routing-op${op_id}.conf" | |
| # Valeurs par défaut si ms_counts non fourni | |
| local -a nms | |
| for i in $(seq 1 "$n_ops"); do | |
| nms[$i]=${ms_counts[$((i-1))]:-1} | |
| done | |
| local mcc="${MCC:-001}" | |
| cat > "$outfile" << HEADER | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms-routing-op${op_id}.conf | |
| # Généré par sms-routing-setup.sh — $(date '+%Y-%m-%d %H:%M:%S') | |
| # | |
| # Opérateur local : ${op_id} | |
| # Nombre d'opérat. : ${n_ops} | |
| # MS par opérateur : $(for i in $(seq 1 "$n_ops"); do printf 'Op%s=%s ' "$i" "${nms[$i]}"; done) | |
| # | |
| # Routage MSISDN : | |
| # MSISDN = op_id × 10000 + ms_idx | |
| # Règle longest-prefix match (préfixe le plus long l'emporte) | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| [local] | |
| operator_id = ${op_id} | |
| sc_address = $(_sms_sc_address "$op_id") | |
| hlr_vty_ip = 127.0.0.1 | |
| hlr_vty_port = 4258 | |
| sendmt_socket = /tmp/sendmt_socket | |
| mo_log = /var/log/osmocom/mo-sms-op${op_id}.log | |
| [operators] | |
| # operator_id = container_ip (réseau backbone 172.20.0.0/24) | |
| HEADER | |
| for i in $(seq 1 "$n_ops"); do | |
| printf '%s = %s\n' "$i" "$(_sms_op_backbone_ip "$i")" >> "$outfile" | |
| done | |
| cat >> "$outfile" << ROUTES_HEADER | |
| [routes] | |
| # Format : prefix = operator_id | |
| # Longest-prefix match : le préfixe le plus long l'emporte. | |
| # Stratégie : | |
| # 1. MSISDN exact de chaque MS → routage précis (priorité max) | |
| # 2. Préfixe court par opérateur → fallback pour MSISDN inconnus | |
| # 3. Préfixes E.164 (+336XX…) → routage international | |
| # | |
| ROUTES_HEADER | |
| # ── Routes exactes par MS (priorité maximale) ───────────────────────────── | |
| for i in $(seq 1 "$n_ops"); do | |
| local mnc; mnc=$(printf '%02d' "$i") | |
| printf '\n# ── Opérateur %s (SC=%s) ──────────────────────────────────────\n' \ | |
| "$i" "$(_sms_sc_address "$i")" >> "$outfile" | |
| printf '# %s MS déclarés\n' "${nms[$i]}" >> "$outfile" | |
| for ms in $(seq 1 "${nms[$i]}"); do | |
| local msisdn; msisdn=$(_sms_op_msisdn "$i" "$ms") | |
| local imsi; imsi=$(_sms_op_ms_imsi "$mcc" "$mnc" "$i" "$ms") | |
| # Commentaire IMSI pour traçabilité | |
| printf '%-8s = %s # IMSI=%s\n' "$msisdn" "$i" "$imsi" >> "$outfile" | |
| done | |
| # Préfixe court de l'opérateur (fallback MSISDN hors liste) | |
| local op_prefix="${i}0000" | |
| printf '# Fallback opérateur %s (MSISDN inconnu)\n' "$i" >> "$outfile" | |
| printf '%-8s = %s\n' "$op_prefix" "$i" >> "$outfile" | |
| # Préfixe E.164 fictif : +336<op>X... (format français simulé) | |
| local e164_prefix; e164_prefix=$(printf '336%02d' "$i") | |
| printf '%-8s = %s # E.164 +33 6%02d...\n' "$e164_prefix" "$i" "$i" >> "$outfile" | |
| done | |
| cat >> "$outfile" << ROUTES_FOOTER | |
| [relay] | |
| # Port TCP sur lequel ce relay écoute les MT entrants d'autres opérateurs. | |
| port = 7890 | |
| # Timeout connexion vers un relay distant (secondes) | |
| connect_timeout = 10 | |
| # Tentatives de réémission si le relay distant est indisponible | |
| retry_count = 3 | |
| retry_delay = 5 | |
| ROUTES_FOOTER | |
| _log_ok "sms-routing-op${op_id}.conf → ${outfile}" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_generate_all | |
| # | |
| # Génère les configs pour TOUS les opérateurs en une seule passe. | |
| # Appelé depuis start.sh en mode bridge. | |
| # | |
| # Usage : | |
| # sms_routing_generate_all <n_ops> <destdir> [ms_counts...] | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_generate_all() { | |
| local n_ops="$1" | |
| local destdir="$2" | |
| shift 2 | |
| local ms_counts=("$@") | |
| echo -e "${CYAN}${BOLD}── SMS Routing — génération configs (${n_ops} opérateurs) ──${NC}" | |
| for i in $(seq 1 "$n_ops"); do | |
| sms_routing_generate "$i" "$n_ops" "$destdir" "${ms_counts[@]}" | |
| done | |
| echo -e " ${GREEN}✓ Configs générées dans ${destdir}${NC}" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_validate | |
| # | |
| # Vérifie la cohérence d'un fichier sms-routing.conf : | |
| # • Section [local] présente | |
| # • Section [operators] non vide | |
| # • Section [routes] non vide | |
| # • Aucune collision de préfixe exact entre deux opérateurs différents | |
| # • MSISDN locaux routés vers l'opérateur local | |
| # | |
| # Retourne 0 si OK, 1 si erreurs. | |
| # | |
| # Usage : | |
| # sms_routing_validate <conf_file> | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_validate() { | |
| local conf="$1" | |
| local errors=0 | |
| echo -e "${CYAN}Validation : ${conf}${NC}" | |
| if [ ! -f "$conf" ]; then | |
| _log_err "Fichier introuvable : $conf" | |
| return 1 | |
| fi | |
| # ── Sections obligatoires ───────────────────────────────────────────────── | |
| for section in local operators routes; do | |
| if ! grep -q "^\[${section}\]" "$conf"; then | |
| _log_err "Section manquante : [${section}]" | |
| errors=$(( errors + 1 )) | |
| fi | |
| done | |
| # ── operator_id dans [local] ────────────────────────────────────────────── | |
| local local_op | |
| local_op=$(awk '/^\[local\]/,/^\[/' "$conf" \ | |
| | grep -E '^\s*operator_id\s*=' \ | |
| | head -1 | cut -d= -f2 | tr -d ' ') | |
| if [ -z "$local_op" ]; then | |
| _log_err "[local] : operator_id manquant" | |
| errors=$(( errors + 1 )) | |
| else | |
| _log_ok "[local] operator_id = ${local_op}" | |
| fi | |
| # ── Au moins 1 opérateur déclaré ───────────────────────────────────────── | |
| local op_count | |
| op_count=$(awk '/^\[operators\]/,/^\[/' "$conf" \ | |
| | grep -cE '^\s*[0-9]+\s*=' || echo 0) | |
| if [ "$op_count" -eq 0 ]; then | |
| _log_err "[operators] : aucun opérateur déclaré" | |
| errors=$(( errors + 1 )) | |
| else | |
| _log_ok "[operators] : ${op_count} opérateur(s)" | |
| fi | |
| # ── Routes non vides ────────────────────────────────────────────────────── | |
| local route_count | |
| route_count=$(awk '/^\[routes\]/,/^\[/' "$conf" \ | |
| | grep -cE '^\s*[0-9]+\s*=' || echo 0) | |
| if [ "$route_count" -eq 0 ]; then | |
| _log_err "[routes] : aucune route définie" | |
| errors=$(( errors + 1 )) | |
| else | |
| _log_ok "[routes] : ${route_count} entrée(s)" | |
| fi | |
| # ── Collision de préfixes : même MSISDN → deux opérateurs différents ───── | |
| local collision_count=0 | |
| while IFS= read -r line; do | |
| [[ "$line" =~ ^[[:space:]]*# ]] && continue # commentaire | |
| [[ "$line" =~ ^[[:space:]]*$ ]] && continue # vide | |
| [[ "$line" =~ ^\[ ]] && continue # section | |
| local prefix op | |
| prefix=$(echo "$line" | cut -d= -f1 | tr -d ' ') | |
| op=$(echo "$line" | cut -d= -f2 | cut -d'#' -f1 | tr -d ' ') | |
| # Chercher si ce même préfixe apparaît avec un opérateur différent | |
| local other_op | |
| other_op=$(awk -v p="$prefix" -v o="$op" ' | |
| /^\[routes\]/,/^\[/ { | |
| if ($0 ~ "^[[:space:]]*" p "[[:space:]]*=") { | |
| split($0, a, "=") | |
| gsub(/[[:space:]]/, "", a[2]) | |
| sub(/#.*/, "", a[2]) | |
| if (a[2] != o) { print a[2]; exit } | |
| } | |
| }' "$conf") | |
| if [ -n "$other_op" ]; then | |
| _log_err "Collision : préfixe ${prefix} → op${op} ET op${other_op}" | |
| collision_count=$(( collision_count + 1 )) | |
| errors=$(( errors + 1 )) | |
| fi | |
| done < <(awk '/^\[routes\]/,/^\[/' "$conf") | |
| [ "$collision_count" -eq 0 ] && _log_ok "Pas de collision de préfixes" | |
| # ── Routes MSISDN locaux → opérateur local ──────────────────────────────── | |
| if [ -n "$local_op" ]; then | |
| local wrong_local=0 | |
| while IFS= read -r line; do | |
| [[ "$line" =~ ^[[:space:]]*# ]] && continue | |
| [[ "$line" =~ ^[[:space:]]*$ ]] && continue | |
| [[ "$line" =~ ^\[ ]] && continue | |
| local prefix op | |
| prefix=$(echo "$line" | cut -d= -f1 | tr -d ' ') | |
| op=$(echo "$line" | cut -d= -f2 | cut -d'#' -f1 | tr -d ' ') | |
| # Un MSISDN commençant par op_id devrait router vers op_id | |
| if [[ "$prefix" =~ ^${local_op}[0-9]* ]] && [ "$op" != "$local_op" ]; then | |
| _log_warn "MSISDN local ${prefix} routé vers op${op} ≠ op${local_op}" | |
| wrong_local=$(( wrong_local + 1 )) | |
| fi | |
| done < <(awk '/^\[routes\]/,/^\[/' "$conf") | |
| [ "$wrong_local" -eq 0 ] && _log_ok "MSISDN locaux correctement routés" | |
| fi | |
| # ── Résultat ────────────────────────────────────────────────────────────── | |
| echo "" | |
| if [ "$errors" -eq 0 ]; then | |
| echo -e " ${GREEN}${BOLD}✓ Validation OK${NC}" | |
| return 0 | |
| else | |
| echo -e " ${RED}${BOLD}✗ ${errors} erreur(s)${NC}" | |
| return 1 | |
| fi | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_summary | |
| # | |
| # Affiche la table de routage complète en format lisible (debug / audit). | |
| # Montre tous les MSISDN enregistrés et leur opérateur cible. | |
| # | |
| # Usage : | |
| # sms_routing_summary <n_ops> [ms_counts...] | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_summary() { | |
| local n_ops="$1" | |
| shift | |
| local ms_counts=("$@") | |
| local mcc="${MCC:-001}" | |
| echo -e "${CYAN}${BOLD}" | |
| echo "╔══════════════════════════════════════════════════════════════════╗" | |
| echo "║ SMS Routing — Table complète ║" | |
| printf "║ %d opérateur(s) ║\n" "$n_ops" | |
| echo "╚══════════════════════════════════════════════════════════════════╝" | |
| echo -e "${NC}" | |
| # En-tête tableau | |
| printf "${BOLD}%-12s %-8s %-20s %-18s %-15s${NC}\n" \ | |
| "MSISDN" "Op→" "IMSI" "Container IP" "SC-address" | |
| printf "%-12s %-8s %-20s %-18s %-15s\n" \ | |
| "────────────" "────────" "────────────────────" "──────────────────" "───────────────" | |
| local total_ms=0 | |
| for i in $(seq 1 "$n_ops"); do | |
| local n_ms=${ms_counts[$((i-1))]:-1} | |
| local mnc; mnc=$(printf '%02d' "$i") | |
| local container_ip; container_ip=$(_sms_op_backbone_ip "$i") | |
| local sc; sc=$(_sms_sc_address "$i") | |
| for ms in $(seq 1 "$n_ms"); do | |
| local msisdn; msisdn=$(_sms_op_msisdn "$i" "$ms") | |
| local imsi; imsi=$(_sms_op_ms_imsi "$mcc" "$mnc" "$i" "$ms") | |
| printf "%-12s ${CYAN}Op%-6s${NC} %-20s %-18s %-15s\n" \ | |
| "$msisdn" "$i" "$imsi" "$container_ip" "$sc" | |
| total_ms=$(( total_ms + 1 )) | |
| done | |
| # Ligne de séparation entre opérateurs | |
| [ "$i" -lt "$n_ops" ] && \ | |
| printf "${YELLOW}%-12s %-8s %-20s %-18s %-15s${NC}\n" \ | |
| "──────────" "↓ Op$((i+1))" "" "" "" | |
| done | |
| echo "" | |
| printf "${BOLD}Total : %d MS | %d opérateur(s)${NC}\n" "$total_ms" "$n_ops" | |
| echo "" | |
| echo -e "${CYAN}── Réseau inter-opérateurs ──${NC}" | |
| for i in $(seq 1 "$n_ops"); do | |
| local n_ms=${ms_counts[$((i-1))]:-1} | |
| printf " Op%-2s %s ← TCP 7890 (relay) %d MS\n" \ | |
| "$i" "$(_sms_op_backbone_ip "$i")" "$n_ms" | |
| done | |
| echo "" | |
| echo -e "${CYAN}── Exemple de flux SMS inter-op ──${NC}" | |
| if [ "$n_ops" -ge 2 ]; then | |
| local src_msisdn; src_msisdn=$(_sms_op_msisdn 1 1) | |
| local dst_msisdn; dst_msisdn=$(_sms_op_msisdn 2 1) | |
| printf " %s (Op1) ──GSUP──► HLR(Op1) ──► proto-smsc-daemon\n" "$src_msisdn" | |
| printf " ──► sms-interop-relay(Op1) [lookup %s → Op2]\n" "$dst_msisdn" | |
| printf " ──TCP──► sms-interop-relay(Op2) @ %s:7890\n" "$(_sms_op_backbone_ip 2)" | |
| printf " ──► HLR(Op2) VTY → IMSI lookup\n" | |
| printf " ──► proto-smsc-sendmt(Op2) → MS\n" | |
| fi | |
| echo "" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_mount_args | |
| # | |
| # Retourne les arguments -v Docker pour monter la config SMS du bon opérateur. | |
| # Utilisé dans start_operator() de start.sh. | |
| # | |
| # Usage : | |
| # vol_args+=$(sms_routing_mount_args <op_id> <destdir>) | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_mount_args() { | |
| local op_id="$1" | |
| local destdir="$2" | |
| local conf="${destdir}/sms-routing-op${op_id}.conf" | |
| if [ -f "$conf" ]; then | |
| echo "-v ${conf}:/etc/osmocom/sms-routing.conf" | |
| else | |
| echo "" # pas de volume si le fichier n'existe pas encore | |
| fi | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_wait_ready | |
| # | |
| # Attend que le relay SMS d'un opérateur soit prêt (port TCP 7890 en écoute). | |
| # Appelé optionnellement après start_operator() pour s'assurer que le relay | |
| # est disponible avant d'injecter des SMS de test. | |
| # | |
| # Usage : | |
| # sms_routing_wait_ready <container_name> [timeout_sec] | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_wait_ready() { | |
| local container="$1" | |
| local timeout="${2:-60}" | |
| local elapsed=0 | |
| echo -ne " Attente relay SMS (${container}) " | |
| while [ "$elapsed" -lt "$timeout" ]; do | |
| if docker exec "$container" \ | |
| bash -c "ss -tlnp | grep -q ':7890'" 2>/dev/null; then | |
| echo -e " ${GREEN}✓${NC}" | |
| return 0 | |
| fi | |
| echo -n "." | |
| sleep 2 | |
| elapsed=$(( elapsed + 2 )) | |
| done | |
| echo -e " ${YELLOW}timeout${NC}" | |
| return 1 | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_test_send | |
| # | |
| # Envoie un SMS de test entre deux MS de deux opérateurs différents. | |
| # Nécessite que les containers soient démarrés et les HLR alimentés. | |
| # | |
| # Usage : | |
| # sms_routing_test_send <src_container> <src_imsi> <dst_msisdn> <message> | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_test_send() { | |
| local src_container="$1" | |
| local src_imsi="$2" | |
| local dst_msisdn="$3" | |
| local message="$4" | |
| echo -e "${CYAN}Test SMS :${NC} ${src_container} IMSI=${src_imsi} → MSISDN=${dst_msisdn}" | |
| if ! docker ps --format '{{.Names}}' | grep -q "^${src_container}$"; then | |
| _log_err "Container ${src_container} non démarré" | |
| return 1 | |
| fi | |
| docker exec "$src_container" bash -c " | |
| OPERATOR_ID=\${OPERATOR_ID:-1} | |
| SC_ADDRESS=\"\$(_sms_sc_address \$OPERATOR_ID)\" 2>/dev/null \ | |
| || SC_ADDRESS=\"1999001\${OPERATOR_ID}444\" | |
| sms-encode-text '${message}' \ | |
| | gen-sms-deliver-pdu \"\$SC_ADDRESS\" \ | |
| | proto-smsc-sendmt \"\$SC_ADDRESS\" '${src_imsi}' /tmp/sendmt_socket | |
| " && _log_ok "SMS envoyé" || _log_err "Échec envoi SMS" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Point d'entrée CLI (appel direct en ligne de commande) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| _usage() { | |
| cat << 'EOF' | |
| Usage: sms-routing-setup.sh <commande> [args...] | |
| Commandes : | |
| generate <op_id> <n_ops> <destdir> [ms_counts...] | |
| Génère sms-routing-op<op_id>.conf dans <destdir>. | |
| ms_counts : nombre de MS par opérateur (ex: 2 3 1 pour 3 ops). | |
| generate-all <n_ops> <destdir> [ms_counts...] | |
| Génère les configs pour TOUS les opérateurs. | |
| validate <conf_file> | |
| Valide un fichier sms-routing.conf et affiche les erreurs. | |
| summary <n_ops> [ms_counts...] | |
| Affiche la table de routage complète (audit / debug). | |
| wait-ready <container_name> [timeout_sec] | |
| Attend que le relay SMS soit actif dans un container. | |
| test-send <container> <src_imsi> <dst_msisdn> <message> | |
| Envoie un SMS de test via proto-smsc-sendmt. | |
| Variables d'environnement : | |
| MCC : Mobile Country Code (défaut: 001) | |
| Exemples : | |
| bash sms-routing-setup.sh generate-all 3 /tmp/sms-cfg 2 2 3 | |
| bash sms-routing-setup.sh validate /tmp/sms-cfg/sms-routing-op1.conf | |
| bash sms-routing-setup.sh summary 2 2 3 | |
| EOF | |
| } | |
| if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then | |
| # Exécuté directement (pas sourcé) | |
| CMD="${1:-help}"; shift 2>/dev/null || true | |
| case "$CMD" in | |
| generate) sms_routing_generate "$@" ;; | |
| generate-all) sms_routing_generate_all "$@" ;; | |
| validate) sms_routing_validate "$@" ;; | |
| summary) sms_routing_summary "$@" ;; | |
| wait-ready) sms_routing_wait_ready "$@" ;; | |
| test-send) sms_routing_test_send "$@" ;; | |
| help|-h|--help) _usage ;; | |
| *) | |
| echo -e "${RED}Commande inconnue : ${CMD}${NC}" >&2 | |
| _usage; exit 1 ;; | |
| esac | |
| fi | |
| #!/bin/bash | |
| # create_interop.sh — Génère la configuration inter-STP (hub SS7 central) | |
| # | |
| # Paramètres : | |
| # $1 n_operators Nombre d'opérateurs (défaut: 2) | |
| # $2 outfile Chemin fichier config de sortie (défaut: osmo-stp-interop.cfg) | |
| # | |
| # Topologie : | |
| # Inter-STP (PC 0.23.0) @ 0.0.0.0:2908 | |
| # ├── Reçoit connexion ASP Op1 (PC 1.23.2, RCTX 150) | |
| # ├── Reçoit connexion ASP Op2 (PC 2.23.2, RCTX 250) | |
| # └── Reçoit connexion ASP Op3 (PC 3.23.2, RCTX 350) | |
| # | |
| # IMPORTANT : Les routing-keys doivent matcher exactement ce que les STP locaux envoient | |
| # STP local Op1 envoie : routing-key 150 1.23.2 | |
| # Inter-STP AS doit recevoir : routing-key 150 1.23.2 (MATCH!) | |
| set -e | |
| n_operators="${1:-2}" | |
| outfile="${2:-osmo-stp-interop.cfg}" | |
| if ! [[ "$n_operators" =~ ^[0-9]+$ ]] || [ "$n_operators" -lt 1 ] || [ "$n_operators" -gt 9 ]; then | |
| echo "Erreur : n_operators doit être 1..9" >&2 | |
| exit 1 | |
| fi | |
| cat > "$outfile" <<'EOFCONFIG' | |
| ! | |
| ! osmo-stp-interop.cfg — Configuration inter-STP centrale | |
| ! | |
| ! PC 0.23.0 : hub de routage SS7 pour N opérateurs | |
| ! Écoute les connexions ASP des STP locaux sur 0.0.0.0:2908 | |
| ! Route les messages vers les destinations appropriées via les AS | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging print extended-timestamp 1 | |
| logging print level 1 | |
| logging print file basename | |
| logging level lss7 info | |
| logging level lsccp info | |
| logging level lm3ua info | |
| logging level linp notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| cs7 instance 0 | |
| network-indicator international | |
| point-code 0.23.0 | |
| ! | |
| ! ── Server : écoute les connexions ASP des opérateurs ──────────────── | |
| ! Les STP locaux se connectent ici avec leurs ASP client | |
| xua rkm routing-key-allocation dynamic-permitted | |
| listen m3ua 2908 | |
| accept-asp-connections dynamic-permitted | |
| local-ip 0.0.0.0 | |
| ! | |
| EOFCONFIG | |
| # Générer dynamiquement les AS pour chaque opérateur | |
| # Les routing-keys DOIVENT matcher ce que les STP locaux envoient | |
| # STP local Op i envoie : routing-key (i*100+50) i.23.2 | |
| for i in $(seq 1 "$n_operators"); do | |
| rctx_inter=$(( i * 100 + 50 )) | |
| pc_stp="${i}.23.2" # PC du STP local (IMPORTANT!) | |
| cat >> "$outfile" <<EOF | |
| ! ── Application Server pour Opérateur ${i} ────────────────────────── | |
| ! Routing-key DOIT matcher ce que l'ASP Op${i} envoie : ${rctx_inter} ${pc_stp} | |
| as as-op${i} m3ua | |
| routing-key ${rctx_inter} ${pc_stp} | |
| traffic-mode override | |
| EOF | |
| done | |
| # Routes vers toutes les destinations | |
| cat >> "$outfile" <<'EOFROUTES' | |
| ! ── Routage vers les destinations des opérateurs ────────────────────── | |
| ! Les routes sont mappées aux AS via les routing-keys | |
| route-table system | |
| EOFROUTES | |
| for i in $(seq 1 "$n_operators"); do | |
| msc_pc="${i}.23.1" | |
| bsc_pc="${i}.23.3" | |
| cat >> "$outfile" <<EOF | |
| update route ${msc_pc} ${msc_pc} linkset as-op${i} | |
| update route ${bsc_pc} ${bsc_pc} linkset as-op${i} | |
| EOF | |
| done | |
| cat >> "$outfile" <<'EOF' | |
| ! | |
| EOF | |
| echo "✓ Config inter-STP générée : $outfile" >&2 | |
| echo " Opérateurs : $n_operators" >&2 | |
| echo " Application Servers : $(seq 1 "$n_operators" | xargs -I{} echo -n "as-op{} ")" >&2 | |
| echo " Routes : $((n_operators * 2)) destinations (MSC + BSC par opérateur)" >&2 | |
| #!/bin/bash | |
| # osmo-start.sh — Démarrage synchronisé des services Osmocom (systemd) | |
| # | |
| # Utilisé par systemd pour lancer les services Osmocom dans un ordre cohérent | |
| # avec dépendances explicites et vérifications d'état. | |
| # | |
| # Appelé par : run.sh (après boot systemd via entrypoint.sh) | |
| set -e | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' | |
| LOG_DIR="/var/log/osmocom" | |
| mkdir -p "$LOG_DIR" | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| N_MS="${N_MS:-1}" | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Helpers | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| vty_check() { | |
| local port=$1 name=$2 timeout=${3:-30} | |
| local elapsed=0 | |
| echo -ne " ${YELLOW}…${NC} VTY ${CYAN}:${port}${NC} (${name})" | |
| while [ $elapsed -lt $timeout ]; do | |
| if timeout 2 bash -c "echo 'help' | telnet 127.0.0.1 $port &>/dev/null"; then | |
| echo -e " ${GREEN}✓${NC}" | |
| return 0 | |
| fi | |
| sleep 1 | |
| elapsed=$((elapsed + 1)) | |
| echo -ne "." | |
| done | |
| echo -e " ${YELLOW}[timeout]${NC}" | |
| return 1 | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Démarrage des services | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo -e "${BOLD}Démarrage services Osmocom — Op${OPERATOR_ID}${NC}" | |
| echo "" | |
| # ─ OsmoBTS ─────────────────────────────────────────────────────────────────── | |
| echo -e "${CYAN}1. OsmoBTS-TRX${NC}" | |
| systemctl start osmo-bts-trx || { | |
| echo -e " ${RED}✗ Démarrage échoué${NC}"; | |
| journalctl -u osmo-bts-trx -n 20 >&2 | |
| exit 1 | |
| } | |
| vty_check 4241 "OsmoBTS" 30 || { | |
| echo -e " ${YELLOW}[WARN]${NC} VTY non accessible après 30s, on continue..." >&2 | |
| } | |
| # ─ OsmoBSC ─────────────────────────────────────────────────────────────────── | |
| echo -e "${CYAN}2. OsmoBSC${NC}" | |
| systemctl start osmo-bsc || { | |
| echo -e " ${RED}✗ Démarrage échoué${NC}"; | |
| journalctl -u osmo-bsc -n 20 >&2 | |
| exit 1 | |
| } | |
| vty_check 4242 "OsmoBSC" 30 || { | |
| echo -e " ${YELLOW}[WARN]${NC} VTY non accessible après 30s, on continue..." >&2 | |
| } | |
| # ─ OsmoMSC ─────────────────────────────────────────────────────────────────── | |
| echo -e "${CYAN}3. OsmoMSC${NC}" | |
| systemctl start osmo-msc || { | |
| echo -e " ${RED}✗ Démarrage échoué${NC}"; | |
| journalctl -u osmo-msc -n 20 >&2 | |
| exit 1 | |
| } | |
| vty_check 4254 "OsmoMSC" 30 || { | |
| echo -e " ${YELLOW}[WARN]${NC} VTY non accessible après 30s, on continue..." >&2 | |
| } | |
| # ─ OsmoHLR ─────────────────────────────────────────────────────────────────── | |
| # En mode bridge, le HLR est centralisé (127.0.0.2) | |
| # En mode net-host (INTER_STP_IP = 127.0.0.1), lancer une instance locale | |
| if [ "${INTER_STP_IP:-127.0.0.1}" = "127.0.0.1" ]; then | |
| echo -e "${CYAN}4. OsmoHLR${NC}" | |
| systemctl start osmo-hlr || { | |
| echo -e " ${RED}✗ Démarrage échoué${NC}"; | |
| journalctl -u osmo-hlr -n 20 >&2 | |
| exit 1 | |
| } | |
| vty_check 4258 "OsmoHLR" 30 || { | |
| echo -e " ${YELLOW}[WARN]${NC} VTY non accessible après 30s, on continue..." >&2 | |
| } | |
| else | |
| echo -e "${CYAN}4. OsmoHLR${NC}" | |
| echo -e " ${YELLOW}[*]${NC} Mode bridge → HLR centralisé @ 127.0.0.2 (skip local)" | |
| fi | |
| # ─ Asterisk (VoIP/SIP) ─────────────────────────────────────────────────────── | |
| echo -e "${CYAN}5. Asterisk (VoIP/SIP)${NC}" | |
| if systemctl is-enabled asterisk &>/dev/null; then | |
| systemctl start asterisk || { | |
| echo -e " ${YELLOW}[WARN]${NC} Astérisk non disponible ou échoué" >&2 | |
| } | |
| else | |
| echo -e " ${YELLOW}[*]${NC} Astérisk désactivé" | |
| fi | |
| # ─ gapk-alsa (Audio GSM) ───────────────────────────────────────────────────── | |
| echo -e "${CYAN}6. osmo-gapk (Audio GSM)${NC}" | |
| if systemctl is-enabled gapk-alsa &>/dev/null; then | |
| systemctl start gapk-alsa || { | |
| echo -e " ${YELLOW}[WARN]${NC} gapk-alsa non disponible ou échoué" >&2 | |
| } | |
| else | |
| echo -e " ${YELLOW}[*]${NC} gapk-alsa désactivé (mode sans audio)" | |
| fi | |
| # ── SMS Relay ───────────────────────────────────────────────────────────────── | |
| echo -e "${CYAN}7. SMS Relay${NC}" | |
| if [ -f /etc/osmocom/sms-routing.conf ]; then | |
| systemctl start sms-relay || { | |
| echo -e " ${YELLOW}[WARN]${NC} sms-relay non disponible" >&2 | |
| } | |
| else | |
| echo -e " ${YELLOW}[*]${NC} sms-routing.conf absent" | |
| fi | |
| echo "" | |
| echo -e "${GREEN}[✓] Services Osmocom démarrés — Op${OPERATOR_ID}${NC}" | |
| echo "" | |
| echo " VTY Status :" | |
| for svc in BTS BSC MSC HLR; do | |
| case "$svc" in | |
| BTS) port=4241 ;; | |
| BSC) port=4242 ;; | |
| MSC) port=4254 ;; | |
| HLR) port=4258 ;; | |
| esac | |
| if timeout 2 bash -c "echo 'help' | telnet 127.0.0.1 $port &>/dev/null"; then | |
| echo -e " ${GREEN}✓${NC} ${svc} (telnet 127.0.0.1:$port)" | |
| else | |
| echo -e " ${YELLOW}○${NC} ${svc} (démarrage en cours...)" | |
| fi | |
| done | |
| echo "" | |
| echo " Logs → ${LOG_DIR}/" | |
| # Dockerfile.run — Image d'exécution ; repose sur l'image de base | |
| FROM osmocom-nitb:latest as osmocom-run | |
| # ── Répertoires nécessaires ──────────────────────────────────────────────────── | |
| RUN mkdir -p \ | |
| /etc/osmocom \ | |
| /etc/asterisk \ | |
| /var/lib/asterisk \ | |
| /var/log/asterisk \ | |
| /var/run/asterisk \ | |
| /var/log/osmocom \ | |
| /var/run/smsc \ | |
| /root/.osmocom/bb \ | |
| /scripts && \ | |
| chmod 755 \ | |
| /etc/asterisk \ | |
| /var/lib/asterisk \ | |
| /var/log/asterisk \ | |
| /var/run/asterisk \ | |
| /var/log/osmocom | |
| RUN apt-get install -y --no-install-recommends telnet iproute2 tmux && \ | |
| rm -rf /var/lib/apt/lists/* | |
| # ── Patch fake_trx : forcer TRXD v0 ───────────────────────────────────────── | |
| # osmo-bts-trx 1.10+ négocie TRXD v1 via SETFORMAT, mais trxcon (OsmocomBB) | |
| # ne supporte que v0. Le header v1 a 2 octets de plus → décalage des soft-bits | |
| # → BER constant 55/456 sur chaque frame → FBSB_SEARCH échoue en boucle. | |
| # Fix : forcer ver_req=0 dans la réponse SETFORMAT → le BTS reste en v0. | |
| RUN sed -i 's/ver_req = int(request\[1\])/ver_req = 0 # PATCHED: force TRXD v0/' \ | |
| /opt/GSM/osmocom-bb/src/target/trx_toolkit/ctrl_if_trx.py | |
| # ── Scripts ──────────────────────────────────────────────────────────────────── | |
| COPY scripts/ /scripts/ | |
| RUN chmod +x /scripts/*.sh | |
| # ── Configs Osmocom (templates avec placeholders __XXX__) ───────────────────── | |
| # Toutes les configs sont copiées ; les valeurs réelles sont injectées | |
| # au démarrage via start.sh (apply_config_templates) et montées en volume. | |
| COPY configs/*.conf /etc/asterisk/ | |
| COPY configs/*.cfg /etc/osmocom/ | |
| COPY configs/sms-routing.conf /etc/osmocom/ | |
| COPY configs/mobile.cfg /root/.osmocom/bb/mobile.cfg | |
| # ── Scripts de démarrage ────────────────────────────────────────────────────── | |
| COPY scripts/sms-interop-relay.py /etc/osmocom/sms-interop-relay.py | |
| COPY scripts/gapk-start.sh /etc/osmocom/gapk-start.sh | |
| COPY scripts/osmo-start.sh /root/osmo-start.sh | |
| COPY scripts/run.sh /root/run.sh | |
| COPY scripts/smsc-start.sh /etc/osmocom/smsc-start.sh | |
| COPY scripts/send-mt-sms.sh /etc/osmocom/send-mt-sms.sh | |
| RUN chmod +x /root/osmo-start.sh /root/run.sh \ | |
| /etc/osmocom/smsc-start.sh /etc/osmocom/send-mt-sms.sh | |
| # ── Vérification binaires proto-SMSC ────────────────────────────────────────── | |
| RUN which proto-smsc-daemon && which proto-smsc-sendmt && \ | |
| echo "proto-smsc binaries OK" || \ | |
| echo "WARNING: proto-smsc binaries not found in PATH" | |
| # ── Entrypoint ──────────────────────────────────────────────────────────────── | |
| ENTRYPOINT ["/scripts/entrypoint.sh"] | |
| CMD ["bash"] | |
| #!/bin/bash | |
| # smsc-start.sh — Lance le proto-smsc-daemon + le relay interop | |
| # | |
| # 1. proto-smsc-daemon : connecté au HLR via GSUP | |
| # - MO SMS → log fichier | |
| # - MT SMS ← socket UNIX | |
| # | |
| # 2. sms-interop-relay.py : pont entre opérateurs | |
| # - Surveille le log MO, parse les TPDU | |
| # - Route vers l'opérateur cible via TCP (port 7890) | |
| # - Écoute en TCP pour les MT venant d'autres opérateurs | |
| # - Résout MSISDN→IMSI via HLR VTY, injecte les MT localement | |
| set -euo pipefail | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| HLR_IP="${HLR_IP:-127.0.0.2}" | |
| SMSC_IPA_NAME="SMSC-OP${OPERATOR_ID}" | |
| MO_LOG="/var/log/osmocom/mo-sms-op${OPERATOR_ID}.log" | |
| SENDMT_SOCKET="/tmp/sendmt_socket" | |
| ROUTING_CONF="/etc/osmocom/sms-routing.conf" | |
| RELAY_PORT="${RELAY_PORT:-7890}" | |
| GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[0;33m'; NC='\033[0m' | |
| echo -e "${GREEN}╔══════════════════════════════════════════════════╗${NC}" | |
| echo -e "${GREEN}║ SMS Gateway — Opérateur ${OPERATOR_ID} ║${NC}" | |
| echo -e "${GREEN}╚══════════════════════════════════════════════════╝${NC}" | |
| echo -e " HLR : ${CYAN}${HLR_IP}${NC}" | |
| echo -e " IPA name : ${CYAN}${SMSC_IPA_NAME}${NC}" | |
| echo -e " MO log : ${CYAN}${MO_LOG}${NC}" | |
| echo -e " MT socket : ${CYAN}${SENDMT_SOCKET}${NC}" | |
| echo -e " Relay port : ${CYAN}${RELAY_PORT}${NC} (TCP interop)" | |
| echo -e " Routing : ${CYAN}${ROUTING_CONF}${NC}" | |
| # Nettoyage | |
| rm -f "$SENDMT_SOCKET" | |
| mkdir -p /var/log/osmocom | |
| # Attente OsmoHLR GSUP (port 4222) | |
| echo -ne "${YELLOW}[SMSC] Attente OsmoHLR GSUP" | |
| retries=30 | |
| while [ $retries -gt 0 ]; do | |
| if ss -tln | grep -q ":4222 "; then | |
| echo -e " ✓${NC}" | |
| break | |
| fi | |
| echo -n "." | |
| sleep 1 | |
| retries=$(( retries - 1 )) | |
| done | |
| [ $retries -eq 0 ] && echo -e " timeout${NC}" | |
| sleep 2 | |
| # ── 1. proto-smsc-daemon (background) ──────────────────────────────────────── | |
| echo -e "${GREEN}[1/2] proto-smsc-daemon${NC}" | |
| proto-smsc-daemon "$HLR_IP" "$SMSC_IPA_NAME" "$MO_LOG" "$SENDMT_SOCKET" & | |
| DAEMON_PID=$! | |
| echo -e " PID: ${CYAN}${DAEMON_PID}${NC}" | |
| sleep 2 | |
| [ -S "$SENDMT_SOCKET" ] && echo -e " Socket: ${GREEN}✓${NC}" || echo -e " Socket: ${YELLOW}attente...${NC}" | |
| # ── 2. Relay interop (background) ──────────────────────────────────────────── | |
| echo -e "${GREEN}[2/2] sms-interop-relay${NC}" | |
| python3 /etc/osmocom/sms-interop-relay.py \ | |
| --config "$ROUTING_CONF" \ | |
| --port "$RELAY_PORT" \ | |
| --mo-log "$MO_LOG" \ | |
| --operator-id "$OPERATOR_ID" & | |
| RELAY_PID=$! | |
| echo -e " PID: ${CYAN}${RELAY_PID}${NC}" | |
| echo -e "" | |
| echo -e "${GREEN}══════════════════════════════════════════════════${NC}" | |
| echo -e "${GREEN} SMS Gateway actif${NC}" | |
| echo -e "${GREEN} daemon=${DAEMON_PID} relay=${RELAY_PID}${NC}" | |
| echo -e "${GREEN} MT local : send-mt-sms.sh <imsi> 'msg'${NC}" | |
| echo -e "${GREEN} MO log : tail -f ${MO_LOG}${NC}" | |
| echo -e "${GREEN}══════════════════════════════════════════════════${NC}" | |
| # Cleanup | |
| cleanup() { | |
| echo -e "\n${YELLOW}[SMSC] Arrêt...${NC}" | |
| kill $DAEMON_PID 2>/dev/null || true | |
| kill $RELAY_PID 2>/dev/null || true | |
| rm -f "$SENDMT_SOCKET" | |
| exit 0 | |
| } | |
| trap cleanup SIGINT SIGTERM | |
| # Attendre fin d'un processus | |
| wait -n $DAEMON_PID $RELAY_PID 2>/dev/null || true | |
| echo -e "${YELLOW}[SMSC] Processus terminé, arrêt...${NC}" | |
| cleanup | |
| [Unit] | |
| Description=Osmocom GSM Base Transceiver Station (BTS) | |
| # S'assure que le réseau et l'interface de bouclage sont prêts | |
| After=network.target network-online.target | |
| Wants=network-online.target | |
| # Si tu utilises osmo-bsc sur la même machine, décommente la ligne suivante | |
| # After=osmo-bsc.service | |
| [Service] | |
| Type=simple | |
| # Adapte l'utilisateur (ex: root ou ton utilisateur actuel 'nirvana') | |
| User=root | |
| Group=root | |
| # Chemin vers l'exécutable (vérifie avec 'which osmo-bts-trx') | |
| # -c spécifie le fichier de configuration | |
| # -r 1 permet d'activer la priorité temps réel si le noyau le permet | |
| ExecStart=/usr/bin/osmo-bts-trx -c /etc/osmocom/osmo-bts-trx.cfg -r 1 | |
| # Redémarrage automatique en cas de crash (utile pour la stabilité radio) | |
| Restart=always | |
| RestartSec=5 | |
| # Limites de ressources pour éviter les "hangs" système | |
| LimitNOFILE=65535 | |
| CPUSchedulingPolicy=rr | |
| CPUSchedulingPriority=1 | |
| [Install] | |
| WantedBy=multi-user.target | |
| #!/bin/bash | |
| set -e | |
| # --- AJOUT DE LA PARTIE TUN --- | |
| echo "[*] Initialisation du périphérique TUN pour osmo-ggsn..." | |
| mkdir -p /dev/net | |
| if [ ! -c /dev/net/tun ]; then | |
| # Création du nœud de périphérique (c = caractère, 10 = majeur, 200 = mineur) | |
| mknod /dev/net/tun c 10 200 | |
| chmod 666 /dev/net/tun | |
| fi | |
| # --- CONFIGURATION ENVIRONNEMENT --- | |
| container=docker | |
| export container | |
| # Vérification de la présence d'une commande | |
| if [ $# -eq 0 ]; then | |
| echo >&2 'ERROR: No command specified. You probably want to run bash or a script.' | |
| exit 1 | |
| fi | |
| # Export des variables pour les sessions systemd | |
| env > /etc/docker-entrypoint-env | |
| # Création du service de démarrage | |
| quoted_args="$(printf " %q" "${@}")" | |
| echo "${quoted_args}" > /etc/docker-entrypoint-cmd | |
| cat >/etc/systemd/system/docker-entrypoint.service <<EOT | |
| [Unit] | |
| Description=Lancement de la stack Osmocom Simulee | |
| After=network.target | |
| [Service] | |
| ExecStart=/bin/bash -exc "source /etc/docker-entrypoint-cmd" | |
| StandardInput=tty-force | |
| StandardOutput=inherit | |
| StandardError=inherit | |
| WorkingDirectory=/etc/osmocom | |
| EnvironmentFile=/etc/docker-entrypoint-env | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOT | |
| # Désactivation des services systemd conflictuels | |
| systemctl mask systemd-firstboot.service systemd-udevd.service systemd-modules-load.service \ | |
| systemd-udevd-kernel systemd-udevd-control 2>/dev/null || true | |
| systemctl enable docker-entrypoint.service | |
| # Localisation et exécution de systemd | |
| if [ -x /lib/systemd/systemd ]; then | |
| exec /lib/systemd/systemd --show-status=false --unit=multi-user.target | |
| elif [ -x /usr/lib/systemd/systemd ]; then | |
| exec /usr/lib/systemd/systemd --show-status=false --unit=multi-user.target | |
| else | |
| echo >&2 'ERROR: systemd is not installed' | |
| exit 1 | |
| fi | |
| # NOTE : Les directives RUN/COPY/EXPOSE qui étaient ici sont des instructions | |
| # Dockerfile et doivent être dans le Dockerfile, pas dans ce script. | |
| # exec systemd remplace le process — rien après cette ligne n'est exécuté. | |
| #!/usr/bin/env python3 | |
| """ | |
| sms-interop-relay.py — Relay SMS inter-opérateur via proto-smsc-proto | |
| Architecture : | |
| Chaque container opérateur exécute ce relay qui : | |
| 1. Surveille le fichier MO SMS log (proto-smsc-daemon) | |
| 2. Parse le TPDU SMS-SUBMIT pour extraire le numéro destinataire | |
| 3. Détermine si le destinataire est local ou sur un autre opérateur | |
| 4. Si distant : envoie via TCP au relay de l'opérateur cible | |
| 5. Écoute aussi en TCP pour recevoir les MT SMS d'autres opérateurs | |
| 6. Résout MSISDN → IMSI via le HLR local (VTY telnet) | |
| 7. Injecte le MT SMS localement via proto-smsc-sendmt | |
| Flux inter-opérateur : | |
| MS(Op1) → MSC(Op1) → HLR(Op1) → proto-smsc-daemon(Op1) → MO log | |
| → sms-interop-relay(Op1) [parse TPDU, route] | |
| → TCP → sms-interop-relay(Op2) [MSISDN→IMSI lookup, inject MT] | |
| → proto-smsc-sendmt(Op2) → HLR(Op2) → MSC(Op2) → MS(Op2) | |
| Usage : | |
| python3 sms-interop-relay.py --config /etc/osmocom/sms-routing.conf | |
| Env vars : | |
| OPERATOR_ID : identifiant opérateur (1, 2, ...) | |
| CONTAINER_IP : IP du container (172.20.0.11, .12, ...) | |
| HLR_VTY_PORT : port VTY du HLR (défaut 4258) | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import socket | |
| import struct | |
| import select | |
| import signal | |
| import logging | |
| import argparse | |
| import threading | |
| import subprocess | |
| from pathlib import Path | |
| from typing import Optional, Dict, Tuple | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Configuration | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| RELAY_TCP_PORT = 7890 # Port TCP d'écoute pour les MT entrants | |
| MO_LOG_POLL_INTERVAL = 0.5 # Intervalle polling du log MO (sec) | |
| HLR_VTY_HOST = "127.0.0.1" | |
| HLR_VTY_PORT = 4258 | |
| SENDMT_SOCKET = "/tmp/sendmt_socket" | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s [RELAY-OP%(operator_id)s] %(levelname)s %(message)s', | |
| datefmt='%Y-%m-%dT%H:%M:%S' | |
| ) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # TPDU Parser — décode SMS-SUBMIT (GSM 03.40) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def decode_bcd_number(hex_bytes: bytes, num_digits: int) -> str: | |
| """Décode un numéro BCD swapped (format GSM standard).""" | |
| number = "" | |
| for byte in hex_bytes: | |
| lo = byte & 0x0F | |
| hi = (byte >> 4) & 0x0F | |
| if lo <= 9: | |
| number += str(lo) | |
| if hi <= 9 and len(number) < num_digits: | |
| number += str(hi) | |
| return number[:num_digits] | |
| def parse_sms_submit_tpdu(hex_str: str) -> Optional[Dict]: | |
| """ | |
| Parse un SMS-SUBMIT TPDU (direction MO : MS → SMSC). | |
| Extrait le numéro destinataire (TP-DA). | |
| Format SMS-SUBMIT (GSM 03.40 §9.2.2) : | |
| Octet 0 : First Octet (MTI=01 pour SUBMIT) | |
| Octet 1 : TP-MR (Message Reference) | |
| Octet 2 : TP-DA length (nombre de chiffres) | |
| Octet 3 : TP-DA Type of Address | |
| Octets 4+ : TP-DA digits (BCD swapped) | |
| ...suite : TP-PID, TP-DCS, TP-VP (optionnel), TP-UDL, TP-UD | |
| """ | |
| try: | |
| data = bytes.fromhex(hex_str.strip()) | |
| except ValueError: | |
| return None | |
| if len(data) < 4: | |
| return None | |
| first_octet = data[0] | |
| mti = first_octet & 0x03 | |
| # MTI=01 = SMS-SUBMIT | |
| if mti != 0x01: | |
| return None | |
| # TP-VPF (Validity Period Format) : bits 4-3 | |
| vpf = (first_octet >> 3) & 0x03 | |
| mr = data[1] # Message Reference | |
| da_len = data[2] # Nombre de chiffres dans TP-DA | |
| da_toa = data[3] # Type of Address | |
| # Nombre d'octets pour les chiffres BCD : ceil(da_len / 2) | |
| da_bytes = (da_len + 1) // 2 | |
| if len(data) < 4 + da_bytes: | |
| return None | |
| da_digits = decode_bcd_number(data[4:4 + da_bytes], da_len) | |
| # Type of Number (bits 6-4 du TOA) | |
| ton = (da_toa >> 4) & 0x07 | |
| # Si TON=1 (international), ajouter le + | |
| prefix = "+" if ton == 1 else "" | |
| # Extraire aussi le corps du message si possible | |
| offset = 4 + da_bytes | |
| result = { | |
| "mti": "SMS-SUBMIT", | |
| "mr": mr, | |
| "da_number": f"{prefix}{da_digits}", | |
| "da_ton": ton, | |
| "da_npi": da_toa & 0x0F, | |
| "da_raw": da_digits, | |
| } | |
| # Parser TP-PID, TP-DCS, TP-VP, TP-UDL, TP-UD | |
| if offset < len(data): | |
| result["tp_pid"] = data[offset] | |
| offset += 1 | |
| if offset < len(data): | |
| result["tp_dcs"] = data[offset] | |
| offset += 1 | |
| # VP dépend de vpf | |
| if vpf == 2 and offset < len(data): # Relative | |
| result["tp_vp"] = data[offset] | |
| offset += 1 | |
| elif vpf == 3 and offset + 7 <= len(data): # Absolute | |
| offset += 7 | |
| elif vpf == 1 and offset + 7 <= len(data): # Enhanced | |
| offset += 7 | |
| if offset < len(data): | |
| udl = data[offset] | |
| result["tp_udl"] = udl | |
| offset += 1 | |
| # Décoder le texte si DCS=0x00 (GSM 7-bit default alphabet) | |
| dcs = result.get("tp_dcs", 0) | |
| if dcs == 0x00 and offset < len(data): | |
| # GSM 7-bit → on laisse sms-pdu-decode s'en charger | |
| result["user_data_hex"] = data[offset:].hex() | |
| return result | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MO SMS Log Parser | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| class MOLogEntry: | |
| """Représente un SMS MO reçu par le proto-smsc-daemon.""" | |
| def __init__(self): | |
| self.timestamp = "" | |
| self.imsi = "" | |
| self.sm_rp_mr = "" | |
| self.sm_rp_da = "" # SMSC address | |
| self.sm_rp_oa = "" # Sender MSISDN | |
| self.sm_rp_oa_number = "" | |
| self.sm_rp_ui_len = 0 | |
| self.tpdu_hex = "" | |
| self.parsed_tpdu = None | |
| @property | |
| def destination_number(self) -> str: | |
| """Numéro destinataire extrait du TPDU.""" | |
| if self.parsed_tpdu: | |
| return self.parsed_tpdu.get("da_raw", "") | |
| return "" | |
| @property | |
| def sender_number(self) -> str: | |
| """Numéro expéditeur (SM-RP-OA).""" | |
| return self.sm_rp_oa_number | |
| def __repr__(self): | |
| return (f"MOLogEntry(from={self.sender_number}, " | |
| f"to={self.destination_number}, imsi={self.imsi})") | |
| def parse_mo_log_block(lines: list) -> Optional[MOLogEntry]: | |
| """Parse un bloc de log MO SMS (multi-lignes).""" | |
| entry = MOLogEntry() | |
| for line in lines: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| if line.endswith("Rx MO SM"): | |
| entry.timestamp = line.replace(" Rx MO SM", "") | |
| elif line.startswith("IMSI:"): | |
| entry.imsi = line.split(":", 1)[1].strip() | |
| elif line.startswith("SM-RP-MR:"): | |
| entry.sm_rp_mr = line.split(":", 1)[1].strip() | |
| elif line.startswith("SM-RP-DA:"): | |
| entry.sm_rp_da = line.split(":", 1)[1].strip() | |
| elif line.startswith("SM-RP-OA:"): | |
| parts = line.split() | |
| entry.sm_rp_oa = line.split(":", 1)[1].strip() | |
| # Dernier élément = numéro | |
| if parts: | |
| entry.sm_rp_oa_number = parts[-1] | |
| elif line.startswith("SM-RP-UI:"): | |
| try: | |
| entry.sm_rp_ui_len = int(line.split(":")[1].strip().split()[0]) | |
| except (ValueError, IndexError): | |
| pass | |
| elif all(c in "0123456789abcdefABCDEF" for c in line) and len(line) >= 4: | |
| # Ligne hex = TPDU brut | |
| entry.tpdu_hex = line | |
| entry.parsed_tpdu = parse_sms_submit_tpdu(line) | |
| if entry.imsi and entry.tpdu_hex: | |
| return entry | |
| return None | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Routing Table | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| class RoutingTable: | |
| """ | |
| Table de routage MSISDN → opérateur. | |
| Format du fichier sms-routing.conf : | |
| # Commentaires | |
| [operators] | |
| 1 = 172.20.0.11 # Op1 container IP | |
| 2 = 172.20.0.12 # Op2 container IP | |
| [routes] | |
| # prefix = operator_id | |
| 33601 = 1 # +33601xxxxxx → Op1 | |
| 33602 = 2 # +33602xxxxxx → Op2 | |
| 1001 = 1 # Extensions courtes Op1 | |
| 2001 = 2 # Extensions courtes Op2 | |
| [local] | |
| operator_id = 1 # ID de cet opérateur | |
| """ | |
| def __init__(self, config_path: str): | |
| self.operators: Dict[str, str] = {} # id → IP | |
| self.routes: list = [] # (prefix, operator_id) trié par longueur desc | |
| self.local_operator_id = "1" | |
| self._load(config_path) | |
| def _load(self, path: str): | |
| section = None | |
| try: | |
| with open(path, 'r') as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line or line.startswith('#'): | |
| continue | |
| if line.startswith('[') and line.endswith(']'): | |
| section = line[1:-1].lower() | |
| continue | |
| if '=' not in line: | |
| continue | |
| key, val = line.split('=', 1) | |
| key = key.strip() | |
| val = val.split('#')[0].strip() # Retirer commentaires | |
| if section == 'operators': | |
| self.operators[key] = val | |
| elif section == 'routes': | |
| self.routes.append((key, val)) | |
| elif section == 'local': | |
| if key == 'operator_id': | |
| self.local_operator_id = val | |
| # Trier les routes par longueur de préfixe décroissante (longest match) | |
| self.routes.sort(key=lambda x: len(x[0]), reverse=True) | |
| except FileNotFoundError: | |
| logging.warning(f"Routing config not found: {path}, using defaults") | |
| def lookup(self, msisdn: str) -> Optional[str]: | |
| """Retourne l'operator_id pour un MSISDN donné, ou None.""" | |
| # Nettoyer le numéro | |
| clean = msisdn.lstrip('+') | |
| for prefix, op_id in self.routes: | |
| if clean.startswith(prefix): | |
| return op_id | |
| return None | |
| def is_local(self, msisdn: str) -> bool: | |
| """Le destinataire est-il sur cet opérateur ?""" | |
| op = self.lookup(msisdn) | |
| return op == self.local_operator_id or op is None | |
| def get_operator_ip(self, op_id: str) -> Optional[str]: | |
| """IP du container de l'opérateur.""" | |
| return self.operators.get(op_id) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # HLR VTY — résolution MSISDN → IMSI | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def hlr_msisdn_to_imsi(msisdn: str, | |
| hlr_host: str = HLR_VTY_HOST, | |
| hlr_port: int = HLR_VTY_PORT) -> Optional[str]: | |
| clean = msisdn.lstrip('+') | |
| try: | |
| s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| s.settimeout(5) | |
| s.connect((hlr_host, hlr_port)) | |
| def recv_until_prompt(timeout=3): | |
| data = b"" | |
| deadline = time.time() + timeout | |
| while time.time() < deadline: | |
| try: | |
| chunk = s.recv(4096) | |
| if not chunk: | |
| break | |
| data += chunk | |
| decoded = data.decode("ascii", errors="replace") | |
| lines = decoded.strip().splitlines() | |
| if lines: | |
| last = lines[-1].strip() | |
| if last.endswith(">") or last.endswith("#"): | |
| break | |
| except socket.timeout: | |
| break | |
| return data.decode("ascii", errors="replace") | |
| # Banner | |
| recv_until_prompt() | |
| # Enable | |
| s.sendall(b"enable\r\n") | |
| recv_until_prompt() | |
| # Commande exacte supportée par ta version | |
| cmd = f"show subscriber msisdn {clean}\r\n" | |
| s.sendall(cmd.encode()) | |
| response = recv_until_prompt() | |
| s.close() | |
| logging.info(f"HLR lookup MSISDN={clean}") | |
| # Parse uniquement IMSI | |
| for line in response.splitlines(): | |
| stripped = line.strip() | |
| if stripped.startswith("IMSI:"): | |
| imsi = stripped.split(":", 1)[1].strip() | |
| if imsi.isdigit(): | |
| logging.info(f"HLR match: MSISDN {clean} → IMSI {imsi}") | |
| return imsi | |
| logging.warning(f"MSISDN {clean} not found in HLR") | |
| return None | |
| except Exception as e: | |
| logging.error(f"HLR VTY error for MSISDN {clean}: {e}") | |
| return None | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MT SMS Injection (local) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def inject_mt_sms(dest_imsi: str, message_text: str, from_number: str, | |
| sc_address: str, sendmt_socket: str = SENDMT_SOCKET) -> bool: | |
| """ | |
| Injecte un MT SMS localement via proto-smsc-sendmt. | |
| Pipeline : sms-encode-text | gen-sms-deliver-pdu | proto-smsc-sendmt | |
| """ | |
| try: | |
| # Pipeline shell | |
| cmd = ( | |
| f"sms-encode-text '{message_text}' " | |
| f"| gen-sms-deliver-pdu {from_number} " | |
| f"| proto-smsc-sendmt {sc_address} {dest_imsi} {sendmt_socket}" | |
| ) | |
| result = subprocess.run( | |
| cmd, shell=True, capture_output=True, text=True, timeout=10 | |
| ) | |
| if result.returncode == 0: | |
| logging.info(f"MT SMS injected: IMSI={dest_imsi}") | |
| return True | |
| else: | |
| logging.error(f"MT SMS injection failed: {result.stderr}") | |
| return False | |
| except Exception as e: | |
| logging.error(f"MT SMS injection error: {e}") | |
| return False | |
| def inject_mt_sms_raw(dest_imsi: str, tpdu_hex: str, sc_address: str, | |
| sendmt_socket: str = SENDMT_SOCKET) -> bool: | |
| """ | |
| Injecte un MT SMS à partir d'un TPDU brut (déjà encodé). | |
| On doit convertir le SMS-SUBMIT en SMS-DELIVER avant injection. | |
| Pour simplifier, on re-décode le texte et re-encode en SMS-DELIVER | |
| via les sms-coding-utils. | |
| """ | |
| # Pour l'instant, on utilise la méthode texte via inject_mt_sms() | |
| # Une version future pourrait manipuler le TPDU directement | |
| return False | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # TCP Relay Server — reçoit les MT SMS d'autres opérateurs | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| class RelayServer(threading.Thread): | |
| """ | |
| Serveur TCP qui reçoit les requêtes MT SMS des autres opérateurs. | |
| Protocole simplifié (JSON sur TCP) : | |
| { | |
| "type": "mt_sms", | |
| "dest_msisdn": "33602000001", | |
| "from_number": "33601000001", | |
| "message_text": "Bonjour depuis Op1!", | |
| "sc_address": "19990011444", | |
| "sender_imsi": "001010000000001" | |
| } | |
| """ | |
| def __init__(self, port: int, operator_id: str, sc_address: str): | |
| super().__init__(daemon=True) | |
| self.port = port | |
| self.operator_id = operator_id | |
| self.sc_address = sc_address | |
| self.running = True | |
| def run(self): | |
| server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
| server.bind(('0.0.0.0', self.port)) | |
| server.listen(5) | |
| server.settimeout(1.0) | |
| logging.info(f"Relay server listening on port {self.port}") | |
| while self.running: | |
| try: | |
| conn, addr = server.accept() | |
| threading.Thread( | |
| target=self._handle_client, | |
| args=(conn, addr), | |
| daemon=True | |
| ).start() | |
| except socket.timeout: | |
| continue | |
| except Exception as e: | |
| logging.error(f"Server error: {e}") | |
| server.close() | |
| def _handle_client(self, conn: socket.socket, addr: tuple): | |
| try: | |
| conn.settimeout(10) | |
| data = b"" | |
| while True: | |
| chunk = conn.recv(4096) | |
| if not chunk: | |
| break | |
| data += chunk | |
| # Messages JSON terminés par newline | |
| if b'\n' in data: | |
| break | |
| if not data: | |
| conn.close() | |
| return | |
| msg = json.loads(data.decode('utf-8').strip()) | |
| if msg.get("type") == "mt_sms": | |
| self._handle_mt_sms(msg, conn) | |
| else: | |
| response = {"status": "error", "reason": "unknown message type"} | |
| conn.sendall((json.dumps(response) + '\n').encode()) | |
| except Exception as e: | |
| logging.error(f"Client handler error: {e}") | |
| finally: | |
| conn.close() | |
| def _handle_mt_sms(self, msg: dict, conn: socket.socket): | |
| dest_msisdn = msg.get("dest_msisdn", "") | |
| from_number = msg.get("from_number", "") | |
| message_text = msg.get("message_text", "") | |
| logging.info(f"Incoming interop MT: {from_number} → {dest_msisdn}") | |
| # Résoudre MSISDN → IMSI dans notre HLR local | |
| dest_imsi = hlr_msisdn_to_imsi(dest_msisdn) | |
| if not dest_imsi: | |
| response = {"status": "error", "reason": f"MSISDN {dest_msisdn} not found"} | |
| conn.sendall((json.dumps(response) + '\n').encode()) | |
| logging.warning(f"MSISDN {dest_msisdn} not found in local HLR") | |
| return | |
| # Injecter le MT SMS localement | |
| success = inject_mt_sms( | |
| dest_imsi=dest_imsi, | |
| message_text=message_text, | |
| from_number=from_number, | |
| sc_address=self.sc_address | |
| ) | |
| response = { | |
| "status": "ok" if success else "error", | |
| "dest_imsi": dest_imsi, | |
| "reason": "" if success else "injection failed" | |
| } | |
| conn.sendall((json.dumps(response) + '\n').encode()) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # TCP Relay Client — envoie les MT SMS vers d'autres opérateurs | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def send_interop_mt(target_ip: str, target_port: int, dest_msisdn: str, | |
| from_number: str, message_text: str, sc_address: str, | |
| sender_imsi: str = "") -> bool: | |
| """Envoie une requête MT SMS au relay d'un autre opérateur via TCP.""" | |
| msg = { | |
| "type": "mt_sms", | |
| "dest_msisdn": dest_msisdn, | |
| "from_number": from_number, | |
| "message_text": message_text, | |
| "sc_address": sc_address, | |
| "sender_imsi": sender_imsi, | |
| } | |
| try: | |
| s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| s.settimeout(10) | |
| s.connect((target_ip, target_port)) | |
| s.sendall((json.dumps(msg) + '\n').encode()) | |
| # Attendre la réponse | |
| data = b"" | |
| while True: | |
| chunk = s.recv(4096) | |
| if not chunk: | |
| break | |
| data += chunk | |
| if b'\n' in data: | |
| break | |
| s.close() | |
| if data: | |
| response = json.loads(data.decode('utf-8').strip()) | |
| if response.get("status") == "ok": | |
| logging.info(f"Interop MT delivered: {from_number} → {dest_msisdn}") | |
| return True | |
| else: | |
| logging.error(f"Interop MT failed: {response.get('reason')}") | |
| return False | |
| return False | |
| except Exception as e: | |
| logging.error(f"Interop send error to {target_ip}: {e}") | |
| return False | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # GSM 7-bit Default Alphabet Decoder (basique) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| GSM7_BASIC = ( | |
| "@£$¥èéùìòÇ\nØø\rÅå" | |
| "Δ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ" | |
| " !\"#¤%&'()*+,-./" | |
| "0123456789:;<=>?" | |
| "¡ABCDEFGHIJKLMNO" | |
| "PQRSTUVWXYZÄÖÑܧ" | |
| "¿abcdefghijklmno" | |
| "pqrstuvwxyzäöñüà" | |
| ) | |
| def decode_gsm7(data: bytes, num_chars: int) -> str: | |
| """Décode du GSM 7-bit packed en texte.""" | |
| chars = [] | |
| bit_pos = 0 | |
| for i in range(num_chars): | |
| byte_idx = (bit_pos * 7) // 8 # Correction: bit_pos is char index | |
| # Calcul correct pour GSM 7-bit unpacking | |
| byte_offset = (i * 7) // 8 | |
| bit_offset = (i * 7) % 8 | |
| if byte_offset >= len(data): | |
| break | |
| val = (data[byte_offset] >> bit_offset) & 0x7F | |
| if bit_offset > 1 and byte_offset + 1 < len(data): | |
| val = ((data[byte_offset] >> bit_offset) | | |
| (data[byte_offset + 1] << (8 - bit_offset))) & 0x7F | |
| if val < len(GSM7_BASIC): | |
| chars.append(GSM7_BASIC[val]) | |
| else: | |
| chars.append('?') | |
| return ''.join(chars) | |
| def extract_text_from_tpdu(tpdu_hex: str) -> str: | |
| """ | |
| Tente d'extraire le texte d'un TPDU SMS-SUBMIT. | |
| Méthode de secours si sms-pdu-decode n'est pas disponible. | |
| """ | |
| parsed = parse_sms_submit_tpdu(tpdu_hex) | |
| if not parsed: | |
| return "" | |
| dcs = parsed.get("tp_dcs", 0) | |
| udl = parsed.get("tp_udl", 0) | |
| ud_hex = parsed.get("user_data_hex", "") | |
| if dcs == 0x00 and ud_hex: | |
| # GSM 7-bit | |
| try: | |
| ud_bytes = bytes.fromhex(ud_hex) | |
| return decode_gsm7(ud_bytes, udl) | |
| except Exception: | |
| pass | |
| return "" | |
| def extract_text_via_sms_decode(tpdu_hex: str) -> str: | |
| """ | |
| Utilise sms-pdu-decode (sms-coding-utils) pour extraire le texte. | |
| Plus fiable que notre décodeur maison. | |
| """ | |
| try: | |
| # Créer un fichier temporaire avec le format attendu | |
| # sms-pdu-decode -n attend juste le hex brut | |
| result = subprocess.run( | |
| ['sms-pdu-decode', '-n'], | |
| input=tpdu_hex + '\n', | |
| capture_output=True, text=True, timeout=5 | |
| ) | |
| if result.returncode == 0: | |
| # Le texte du message est après la dernière ligne vide | |
| lines = result.stdout.strip().split('\n') | |
| # Trouver la ligne "Length:" et prendre tout après | |
| text_lines = [] | |
| found_length = False | |
| for line in lines: | |
| if found_length and line.strip(): | |
| text_lines.append(line) | |
| if line.strip().startswith('Length:'): | |
| found_length = True | |
| if text_lines: | |
| return '\n'.join(text_lines).strip() | |
| except (FileNotFoundError, subprocess.TimeoutExpired): | |
| pass | |
| # Fallback vers notre décodeur | |
| return extract_text_from_tpdu(tpdu_hex) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MO Log Watcher — surveille le log MO et déclenche le routage | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| class MOLogWatcher(threading.Thread): | |
| """ | |
| Surveille le fichier log MO du proto-smsc-daemon. | |
| Parse les nouveaux blocs et les route vers le bon opérateur. | |
| """ | |
| def __init__(self, log_path: str, routing: RoutingTable, | |
| operator_id: str, sc_address: str): | |
| super().__init__(daemon=True) | |
| self.log_path = log_path | |
| self.routing = routing | |
| self.operator_id = operator_id | |
| self.sc_address = sc_address | |
| self.running = True | |
| self._position = 0 | |
| def run(self): | |
| logging.info(f"Watching MO log: {self.log_path}") | |
| # Attendre que le fichier existe | |
| while self.running and not Path(self.log_path).exists(): | |
| time.sleep(2) | |
| if not self.running: | |
| return | |
| # Se positionner à la fin du fichier (ne pas traiter l'historique) | |
| with open(self.log_path, 'r') as f: | |
| f.seek(0, 2) # EOF | |
| self._position = f.tell() | |
| logging.info(f"MO log watcher started (pos={self._position})") | |
| while self.running: | |
| try: | |
| self._check_new_entries() | |
| except Exception as e: | |
| logging.error(f"MO watcher error: {e}") | |
| time.sleep(MO_LOG_POLL_INTERVAL) | |
| def _check_new_entries(self): | |
| try: | |
| with open(self.log_path, 'r') as f: | |
| f.seek(self._position) | |
| new_data = f.read() | |
| self._position = f.tell() | |
| except FileNotFoundError: | |
| return | |
| if not new_data: | |
| return | |
| # Découper en blocs (séparés par des lignes contenant "Rx MO SM") | |
| blocks = [] | |
| current_block = [] | |
| for line in new_data.split('\n'): | |
| if "Rx MO SM" in line and current_block: | |
| blocks.append(current_block) | |
| current_block = [line] | |
| else: | |
| current_block.append(line) | |
| if current_block: | |
| blocks.append(current_block) | |
| for block in blocks: | |
| entry = parse_mo_log_block(block) | |
| if entry: | |
| self._route_mo_sms(entry) | |
| def _route_mo_sms(self, entry: MOLogEntry): | |
| dest = entry.destination_number | |
| sender = entry.sender_number | |
| if not dest: | |
| logging.warning(f"MO SMS sans destination parsable: {entry}") | |
| return | |
| logging.info(f"MO SMS: {sender} → {dest} (from IMSI {entry.imsi})") | |
| # Déterminer l'opérateur cible | |
| target_op = self.routing.lookup(dest) | |
| if target_op is None: | |
| logging.warning(f"No route for destination {dest}") | |
| return | |
| if target_op == self.operator_id: | |
| # Local delivery — le proto-smsc-daemon l'a déjà loggé | |
| # Pour un vrai SMSC, il faudrait aussi faire le MT delivery local | |
| logging.info(f"Local delivery for {dest} (same operator)") | |
| self._deliver_local(entry, dest) | |
| return | |
| # Inter-opérateur : envoyer au relay distant | |
| target_ip = self.routing.get_operator_ip(target_op) | |
| if not target_ip: | |
| logging.error(f"No IP for operator {target_op}") | |
| return | |
| logging.info(f"Interop route: {dest} → Op{target_op} ({target_ip})") | |
| # Extraire le texte du message | |
| message_text = extract_text_via_sms_decode(entry.tpdu_hex) | |
| if not message_text: | |
| message_text = extract_text_from_tpdu(entry.tpdu_hex) | |
| if not message_text: | |
| logging.warning(f"Could not decode message text from TPDU: {entry.tpdu_hex}") | |
| message_text = "[message non décodable]" | |
| # Envoyer via TCP au relay distant | |
| success = send_interop_mt( | |
| target_ip=target_ip, | |
| target_port=RELAY_TCP_PORT, | |
| dest_msisdn=dest, | |
| from_number=sender, | |
| message_text=message_text, | |
| sc_address=self.sc_address, | |
| sender_imsi=entry.imsi | |
| ) | |
| if success: | |
| logging.info(f"✓ Interop SMS delivered: {sender} → {dest} (Op{target_op})") | |
| else: | |
| logging.error(f"✗ Interop SMS failed: {sender} → {dest} (Op{target_op})") | |
| def _deliver_local(self, entry: MOLogEntry, dest_msisdn: str): | |
| """Livraison locale — résout MSISDN→IMSI et injecte MT.""" | |
| dest_imsi = hlr_msisdn_to_imsi(dest_msisdn) | |
| if not dest_imsi: | |
| logging.warning(f"Local MSISDN {dest_msisdn} not found in HLR") | |
| return | |
| message_text = extract_text_via_sms_decode(entry.tpdu_hex) | |
| if not message_text: | |
| message_text = extract_text_from_tpdu(entry.tpdu_hex) | |
| if not message_text: | |
| message_text = "[message non décodable]" | |
| inject_mt_sms( | |
| dest_imsi=dest_imsi, | |
| message_text=message_text, | |
| from_number=entry.sender_number, | |
| sc_address=self.sc_address | |
| ) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Main | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='SMS Interop Relay — routage inter-opérateur via proto-smsc-proto' | |
| ) | |
| parser.add_argument( | |
| '--config', '-c', | |
| default='/etc/osmocom/sms-routing.conf', | |
| help='Fichier de configuration du routage SMS' | |
| ) | |
| parser.add_argument( | |
| '--port', '-p', | |
| type=int, default=RELAY_TCP_PORT, | |
| help=f'Port TCP du relay (défaut: {RELAY_TCP_PORT})' | |
| ) | |
| parser.add_argument( | |
| '--mo-log', | |
| help='Chemin du log MO (défaut: /var/log/osmocom/mo-sms-op<N>.log)' | |
| ) | |
| parser.add_argument( | |
| '--operator-id', | |
| default=os.environ.get('OPERATOR_ID', '1'), | |
| help='ID opérateur (défaut: $OPERATOR_ID ou 1)' | |
| ) | |
| args = parser.parse_args() | |
| op_id = args.operator_id | |
| sc_address = f"1999001{op_id}444" | |
| mo_log = args.mo_log or f"/var/log/osmocom/mo-sms-op{op_id}.log" | |
| # Injecter l'operator_id dans le format de log | |
| old_factory = logging.getLogRecordFactory() | |
| def record_factory(*a, **kw): | |
| record = old_factory(*a, **kw) | |
| record.operator_id = op_id | |
| return record | |
| logging.setLogRecordFactory(record_factory) | |
| logging.info(f"SMS Interop Relay starting") | |
| logging.info(f" Operator : {op_id}") | |
| logging.info(f" SC-address : {sc_address}") | |
| logging.info(f" MO log : {mo_log}") | |
| logging.info(f" TCP port : {args.port}") | |
| logging.info(f" Config : {args.config}") | |
| # Charger la table de routage | |
| routing = RoutingTable(args.config) | |
| routing.local_operator_id = op_id | |
| logging.info(f" Operators : {routing.operators}") | |
| logging.info(f" Routes : {routing.routes}") | |
| # Démarrer le serveur TCP (réception MT inter-op) | |
| server = RelayServer( | |
| port=args.port, | |
| operator_id=op_id, | |
| sc_address=sc_address | |
| ) | |
| server.start() | |
| # Démarrer le watcher MO log (routage MO inter-op) | |
| watcher = MOLogWatcher( | |
| log_path=mo_log, | |
| routing=routing, | |
| operator_id=op_id, | |
| sc_address=sc_address | |
| ) | |
| watcher.start() | |
| # Boucle principale (attente signal) | |
| def signal_handler(sig, frame): | |
| logging.info("Shutting down...") | |
| watcher.running = False | |
| server.running = False | |
| sys.exit(0) | |
| signal.signal(signal.SIGINT, signal_handler) | |
| signal.signal(signal.SIGTERM, signal_handler) | |
| logging.info("Relay running. Ctrl+C to stop.") | |
| while True: | |
| time.sleep(1) | |
| if __name__ == '__main__': | |
| main() | |
| # ── INTER-STP ──────────────────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-inter-stp telnet 127.0.0.1 4239 | |
| # show cs7 instance 0 asp | |
| # show cs7 instance 0 as | |
| # show cs7 instance 0 route | |
| # ── OPÉRATEUR 1 — STP intra ────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-operator-1 telnet 127.0.0.1 4239 | |
| # show cs7 instance 0 asp | |
| # show cs7 instance 0 as | |
| # show cs7 instance 0 sccp users | |
| # show cs7 instance 0 sccp connections | |
| # ── OPÉRATEUR 1 — MSC ──────────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-operator-1 telnet 127.0.0.1 4254 | |
| # en | |
| # show cs7 instance 0 asp | |
| # show cs7 instance 0 sccp users | |
| # show cs7 instance 0 sccp connections | |
| # show subscriber all | |
| # show connection all | |
| # ── OPÉRATEUR 1 — BSC ──────────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-operator-1 telnet 127.0.0.1 4242 | |
| # en | |
| # show cs7 instance 0 asp | |
| # show cs7 instance 0 sccp users | |
| # show cs7 instance 0 sccp connections | |
| # show bts 0 | |
| # show trx 0 0 | |
| # ── OPÉRATEUR 2 — même chose ────────────────────────────────────────────────── | |
| sudo docker exec -it osmo-operator-2 telnet 127.0.0.1 4239 | |
| sudo docker exec -it osmo-operator-2 telnet 127.0.0.1 4254 | |
| sudo docker exec -it osmo-operator-2 telnet 127.0.0.1 4242 | |
| # ── ONE-LINER : dump tout sans interaction ──────────────────────────────────── | |
| for container in osmo-operator-1 osmo-operator-2; do | |
| echo "=== $container STP ===" | |
| sudo docker exec $container sh -c \ | |
| 'echo -e "show cs7 instance 0 asp\nshow cs7 instance 0 sccp users\nshow cs7 instance 0 sccp connections\n" | telnet 127.0.0.1 4239 2>/dev/null' \ | |
| | grep -v "^Trying\|^Connected\|^Escape" | |
| echo "=== $container MSC ===" | |
| sudo docker exec $container sh -c \ | |
| 'echo -e "enable\nshow cs7 instance 0 sccp users\nshow cs7 instance 0 sccp connections\nshow subscriber all\n" | telnet 127.0.0.1 4254 2>/dev/null' \ | |
| | grep -v "^Trying\|^Connected\|^Escape" | |
| done | |
| echo "=== inter-STP ASP ===" | |
| sudo docker exec osmo-inter-stp sh -c \ | |
| 'echo -e "show cs7 instance 0 asp\nshow cs7 instance 0 as\n" | telnet 127.0.0.1 4239 2>/dev/null' \ | |
| | grep -v "^Trying\|^Connected\|^Escape" | |
| #!/bin/bash | |
| # run.sh — Orchestrateur intra-container Osmocom | |
| # | |
| # Séquence garantie : | |
| # 1. Reset | |
| # 2. generate_ms_configs → UN SEUL mobile_combined.cfg avec N blocs ms | |
| # 3. osmo-start.sh → STP HLR MGW MSC BSC GGSN SGSN PCU | |
| # (PAS bts-trx ni sip-connector) | |
| # 4. fake_trx démarre → bind UDP 5700 (+trx additionnels via --trx) | |
| # 5. osmo-bts-trx start → se connecte à fake_trx déjà prêt ✓ | |
| # 6. trxcon × N (chacun -C) → N process, N sockets L1CTL | |
| # 7. mobile (UN SEUL) → charge tous les ms depuis le combined cfg | |
| # 8. sip-connector → après MNCC socket prêt | |
| # 9. Asterisk → après sip-connector | |
| # 10. SMSC + gapk | |
| # | |
| # Architecture OsmocomBB multi-MS : | |
| # | |
| # fake_trx (1 process) | |
| # │ (N TRX internes : --trx pour chaque MS ≥ 2) | |
| # │ | |
| # ┌────┼────┐ | |
| # trxcon1 trxcon2 trxcon3 ← chacun avec -C, son port TRX, son socket L1CTL | |
| # │ │ │ | |
| # └────┼────┘ | |
| # mobile (1 process) | |
| # mobile_combined.cfg : ms 1 / ms 2 / ms 3 | |
| # | |
| # OsmocomBB a été conçu pour UN téléphone Calypso physique → une seule | |
| # couche radio. fake_trx a gardé cette architecture : 1 transceiver, | |
| # N MS logiques au-dessus. Donc : 1 mobile process, 1 fichier cfg, N blocs ms. | |
| # | |
| # DEBUG MODE: DEBUG=1 ./run.sh | |
| set -euo pipefail | |
| # ── LOGGING ──────────────────────────────────────────────────────────────── | |
| # Tout stdout+stderr → fichier log + console | |
| # Lire depuis le host : docker exec <container> cat /var/log/osmocom/run.sh.log | |
| LOG_FILE="/var/log/osmocom/run.sh.log" | |
| mkdir -p "$(dirname "$LOG_FILE")" | |
| exec > >(tee -a "$LOG_FILE") 2>&1 | |
| echo "" | |
| echo "========================================" | |
| echo " run.sh démarré à $(date '+%Y-%m-%d %H:%M:%S')" | |
| echo "========================================" | |
| echo "" | |
| # ── DEBUG MODE ───────────────────────────────────────────────────────────── | |
| if [[ -n "${DEBUG:-}" ]]; then | |
| set -x | |
| PS4='[DEBUG-run] + ${BASH_SOURCE}:${LINENO}: ' | |
| echo "=== RUN.SH: MODE DEBUG ACTIVÉ (Op${OPERATOR_ID:-?}) ===" | |
| echo "" | |
| fi | |
| SESSION="osmocom" | |
| GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' | |
| FAKETRX_PY="${FAKETRX_PY:-/opt/GSM/osmocom-bb/src/target/trx_toolkit/fake_trx.py}" | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| N_MS="${N_MS:-1}" | |
| # Base du socket L1CTL — chaque trxcon -C -s crée un socket | |
| # trxcon MS1 → ${L2_SOCK_BASE}_1, MS2 → ${L2_SOCK_BASE}_2, etc. | |
| L2_SOCK_BASE="/tmp/osmocom_l2" | |
| SAP_SOCK_BASE="/tmp/osmocom_sap" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] OPERATOR_ID=$OPERATOR_ID N_MS=$N_MS FAKETRX_PY=$FAKETRX_PY" | |
| if ! [[ "$N_MS" =~ ^[0-9]+$ ]] || [ "$N_MS" -lt 1 ] || [ "$N_MS" -gt 9 ]; then | |
| echo -e "${YELLOW}[WARN] N_MS='${N_MS}' invalide → forcé à 1${NC}" | |
| N_MS=1 | |
| fi | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Génération d'UN SEUL mobile_combined.cfg contenant N blocs ms | |
| # | |
| # Format VTY OsmocomBB : | |
| # ┌── partie globale (line vty, gps, no hide-default) | |
| # ├── ms 1 | |
| # │ ├── layer2-socket, sap-socket, sim, imei, support {...exit}, test-sim {...exit} | |
| # │ └── exit ← sans indentation = ferme le bloc ms | |
| # ├── ms 2 (optionnel) | |
| # │ └── exit | |
| # └── ! | |
| # | |
| # Format ancien (configs/) : le bloc ms se termine par 'end' au lieu de 'exit' | |
| # | |
| # On extrait SEULEMENT le premier bloc ms comme template, puis on le | |
| # duplique N fois avec des credentials/sockets différents. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| generate_ms_configs() { | |
| local base_cfg="/root/.osmocom/bb/mobile.cfg" | |
| local combined_cfg="/root/.osmocom/bb/mobile_combined.cfg" | |
| echo -e " ${CYAN}[cfg]${NC} Source : ${base_cfg}" | |
| echo -e " ${CYAN}[cfg]${NC} Sortie : ${combined_cfg}" | |
| if [ ! -f "$base_cfg" ]; then | |
| echo -e "${RED}[ERR] mobile.cfg introuvable : ${base_cfg}${NC}" | |
| echo -e "${RED} ls /root/.osmocom/bb/ :${NC}" | |
| ls -la /root/.osmocom/bb/ 2>&1 | sed 's/^/ /' | |
| return 1 | |
| fi | |
| echo -e " ${CYAN}[cfg]${NC} Taille : $(wc -l < "$base_cfg") lignes, $(wc -c < "$base_cfg") octets" | |
| # ── Dump rapide du fichier source ───────────────────────────────────── | |
| echo -e " ${CYAN}[cfg]${NC} Contenu (premières / dernières lignes) :" | |
| head -5 "$base_cfg" | sed 's/^/ | /' | |
| echo " | ..." | |
| tail -5 "$base_cfg" | sed 's/^/ | /' | |
| # ── Extraction des valeurs de référence ─────────────────────────────── | |
| # IMPORTANT: || true sur chaque grep — sinon set -e tue le script | |
| # silencieusement si un pattern ne matche pas (grep retourne 1) | |
| local ref_imsi ref_imei ref_ki ref_l2 ref_sap | |
| ref_imsi=$(grep -oP '^\s+imsi\s+\K[0-9]+' "$base_cfg" 2>/dev/null | head -1 || true) | |
| ref_imei=$(grep -oP '^\s+imei \K[0-9]+' "$base_cfg" 2>/dev/null | head -1 || true) | |
| ref_ki=$(grep -oP '^\s+ki comp128 \K[0-9a-f ]+' "$base_cfg" 2>/dev/null | head -1 \ | |
| | sed 's/[[:space:]]*$//' || true) | |
| ref_l2=$(grep -oP 'layer2-socket \K\S+' "$base_cfg" 2>/dev/null | head -1 || true) | |
| ref_l2="${ref_l2:-/tmp/osmocom_l2}" | |
| ref_sap=$(grep -oP 'sap-socket \K\S+' "$base_cfg" 2>/dev/null | head -1 || true) | |
| # Fallback si grep -P (PCRE) pas dispo dans le container | |
| if [ -z "$ref_imsi" ]; then | |
| echo -e " ${YELLOW}[ref] grep -oP échoué → fallback grep -E${NC}" | |
| ref_imsi=$(grep -E 'imsi [0-9]+' "$base_cfg" 2>/dev/null | head -1 | awk '{print $NF}' || true) | |
| ref_imei=$(grep -E 'imei [0-9]+' "$base_cfg" 2>/dev/null | head -1 | awk '{print $2}' || true) | |
| ref_ki=$(grep -E 'ki comp128' "$base_cfg" 2>/dev/null | head -1 \ | |
| | sed 's/.*ki comp128 //' | sed 's/[[:space:]]*$//' || true) | |
| ref_l2=$(grep -E 'layer2-socket' "$base_cfg" 2>/dev/null | head -1 | awk '{print $2}' || true) | |
| ref_l2="${ref_l2:-/tmp/osmocom_l2}" | |
| ref_sap=$(grep -E 'sap-socket' "$base_cfg" 2>/dev/null | head -1 | awk '{print $2}' || true) | |
| fi | |
| echo -e " ${CYAN}[ref]${NC} IMSI : '${ref_imsi:-${RED}VIDE${NC}}'" | |
| echo -e " ${CYAN}[ref]${NC} IMEI : '${ref_imei:-${RED}VIDE${NC}}'" | |
| echo -e " ${CYAN}[ref]${NC} KI : '${ref_ki:-${RED}VIDE${NC}}'" | |
| echo -e " ${CYAN}[ref]${NC} L2 sock : '${ref_l2}'" | |
| echo -e " ${CYAN}[ref]${NC} SAP sock: '${ref_sap:-ABSENT → injection auto}'" | |
| if [ -z "$ref_imsi" ] || [ -z "$ref_imei" ] || [ -z "$ref_ki" ]; then | |
| echo -e "${RED}[ERR] Impossible d'extraire IMSI/IMEI/KI de ${base_cfg}${NC}" | |
| echo -e "${RED} Lignes contenant imsi/imei/ki :${NC}" | |
| grep -inE '(imsi|imei|ki comp128)' "$base_cfg" 2>/dev/null | head -10 | sed 's/^/ /' || true | |
| echo -e "${RED} → Les __PLACEHOLDERS__ ont-ils été substitués par start.sh ?${NC}" | |
| return 1 | |
| fi | |
| local mcc mnc | |
| mcc=$(grep -E '^\s+rplmn ' "$base_cfg" 2>/dev/null | awk '{print $2}' | head -1 || true) | |
| mnc=$(grep -E '^\s+rplmn ' "$base_cfg" 2>/dev/null | awk '{print $3}' | head -1 || true) | |
| mcc="${mcc:-001}"; mnc="${mnc:-01}" | |
| echo -e " ${CYAN}[ref]${NC} MCC/MNC : ${mcc} / ${mnc}" | |
| # ── Extraction partie globale (tout avant le premier 'ms N') ────────── | |
| local global_line | |
| global_line=$(grep -n '^ms [0-9]' "$base_cfg" 2>/dev/null | head -1 | cut -d: -f1 || true) | |
| if [ -z "$global_line" ]; then | |
| echo -e "${RED}[ERR] Pas de bloc 'ms' trouvé dans ${base_cfg}${NC}" | |
| echo -e "${RED} Le fichier ne contient pas de ligne commençant par 'ms [0-9]'${NC}" | |
| echo -e "${RED} grep 'ms ' résultat :${NC}" | |
| grep -n 'ms ' "$base_cfg" 2>/dev/null | head -5 | sed 's/^/ /' || true | |
| return 1 | |
| fi | |
| echo -e " ${CYAN}[parse]${NC} Premier 'ms' trouvé ligne ${global_line}" | |
| echo -e " ${CYAN}[parse]${NC} Partie globale : lignes 1-$(( global_line - 1 ))" | |
| head -n $(( global_line - 1 )) "$base_cfg" > "$combined_cfg" | |
| # ── Template = premier bloc ms SEULEMENT ────────────────────────────── | |
| # S'arrête au premier ^exit$ ou ^end$ ou au prochain ^ms [0-9] | |
| # Les ' exit' indentés (support/test-sim) sont ignorés. | |
| local ms_template | |
| ms_template=$(awk ' | |
| /^ms [0-9]/ { | |
| if (found) { exit } | |
| found=1; printing=1 | |
| } | |
| printing { print } | |
| /^(exit|end)$/ && printing && found { printing=0; exit } | |
| ' "$base_cfg") | |
| if [ -z "$ms_template" ]; then | |
| echo -e "${RED}[ERR] Extraction du bloc ms échouée (awk n'a rien trouvé)${NC}" | |
| echo -e "${RED} Lignes autour de 'ms 1' :${NC}" | |
| sed -n "$(( global_line - 2 )),$(( global_line + 5 ))p" "$base_cfg" | sed 's/^/ | /' | |
| return 1 | |
| fi | |
| local tpl_lines | |
| tpl_lines=$(echo "$ms_template" | wc -l) | |
| local tpl_first | |
| tpl_first=$(echo "$ms_template" | head -1) | |
| local tpl_last | |
| tpl_last=$(echo "$ms_template" | tail -1) | |
| echo -e " ${CYAN}[parse]${NC} Template ms : ${tpl_lines} lignes" | |
| echo -e " ${CYAN}[parse]${NC} première : '${tpl_first}'" | |
| echo -e " ${CYAN}[parse]${NC} dernière : '${tpl_last}'" | |
| # Vérifier que le template se termine bien par exit ou end | |
| if ! echo "$tpl_last" | grep -qE '^(exit|end)$'; then | |
| echo -e " ${YELLOW}[WARN] Template ne se termine pas par exit/end → dernière ligne : '${tpl_last}'${NC}" | |
| fi | |
| # ── Écriture des N blocs ms ─────────────────────────────────────────── | |
| echo "" | |
| for ms_idx in $(seq 1 "${N_MS}"); do | |
| local l2_sock="${L2_SOCK_BASE}_${ms_idx}" | |
| local sap_sock="${SAP_SOCK_BASE}_${ms_idx}" | |
| local msin; msin=$(printf '%04d%06d' "${OPERATOR_ID}" "${ms_idx}") | |
| local new_imsi="${mcc}${mnc}${msin}" | |
| local new_imei; new_imei="3589250059$(printf '%02d%02d' "${OPERATOR_ID}" "${ms_idx}")0" | |
| local new_ki; new_ki=$(printf '00 11 22 33 44 55 66 77 88 99 aa bb cc dd %02x ff' \ | |
| "${ms_idx}") | |
| local ms_block | |
| ms_block=$(echo "$ms_template" \ | |
| | sed \ | |
| -e "s|^ms [0-9]\+|ms ${ms_idx}|" \ | |
| -e "s|layer2-socket ${ref_l2}|layer2-socket ${l2_sock}|" \ | |
| -e "s|imsi ${ref_imsi}|imsi ${new_imsi}|" \ | |
| -e "s|imei ${ref_imei}[[:space:]]*[0-9]*|imei ${new_imei}|" \ | |
| -e "s|ki comp128 ${ref_ki}|ki comp128 ${new_ki}|" \ | |
| ) | |
| # sap-socket : remplacer si présent, sinon injecter après layer2-socket | |
| if [ -n "$ref_sap" ]; then | |
| ms_block=$(echo "$ms_block" \ | |
| | sed "s|sap-socket ${ref_sap}|sap-socket ${sap_sock}|") | |
| else | |
| ms_block=$(echo "$ms_block" \ | |
| | sed "/layer2-socket/a\\ sap-socket ${sap_sock}") | |
| fi | |
| echo "$ms_block" >> "$combined_cfg" | |
| echo "" >> "$combined_cfg" | |
| echo -e " ${CYAN}[MS${ms_idx}]${NC} IMSI=${new_imsi} IMEI=${new_imei} KI=…$(printf '%02x ff' "${ms_idx}")" | |
| echo -e " ${CYAN}[MS${ms_idx}]${NC} L1CTL=${l2_sock} SAP=${sap_sock}" | |
| # Vérification post-sed : les substitutions ont-elles marché ? | |
| if echo "$ms_block" | grep -q "${ref_imsi}"; then | |
| echo -e " ${YELLOW}[WARN MS${ms_idx}] IMSI ref '${ref_imsi}' encore présent après sed !${NC}" | |
| fi | |
| if echo "$ms_block" | grep -q "layer2-socket ${ref_l2}\$"; then | |
| echo -e " ${YELLOW}[WARN MS${ms_idx}] layer2-socket ref '${ref_l2}' encore présent après sed !${NC}" | |
| fi | |
| done | |
| # ── Vérification finale ─────────────────────────────────────────────── | |
| echo "" | |
| local n_ms_found n_exit_found | |
| n_ms_found=$(grep -c '^ms [0-9]' "$combined_cfg") | |
| n_exit_found=$(grep -cE '^(exit|end)$' "$combined_cfg") | |
| echo -e " ${CYAN}[check]${NC} Blocs ms dans combined : ${n_ms_found} (attendu: ${N_MS})" | |
| echo -e " ${CYAN}[check]${NC} Terminateurs exit/end : ${n_exit_found} (attendu: ${N_MS})" | |
| echo -e " ${CYAN}[check]${NC} Taille combined : $(wc -l < "$combined_cfg") lignes" | |
| if [ "$n_ms_found" -ne "$N_MS" ]; then | |
| echo -e " ${RED}[ERR] Nombre de blocs ms incorrect !${NC}" | |
| fi | |
| if [ "$n_exit_found" -ne "$N_MS" ]; then | |
| echo -e " ${YELLOW}[WARN] Nombre de exit/end != N_MS${NC}" | |
| fi | |
| echo -e " ${GREEN}→ ${combined_cfg} (${N_MS} blocs ms)${NC}" | |
| } | |
| # ── Helper : reset TRX via VTY OsmoBTS (port 4236) ────────────────────────── | |
| reset_trx() { | |
| local vty_host="127.0.0.1" vty_port=4236 | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] reset_trx: attente ${vty_host}:${vty_port}" | |
| echo -ne " Attente VTY OsmoBTS (${vty_host}:${vty_port})" | |
| elapsed=0 | |
| while ! bash -c "echo >/dev/tcp/${vty_host}/${vty_port}" 2>/dev/null; do | |
| sleep 1; elapsed=$((elapsed + 1)); echo -n "." | |
| if [ "$elapsed" -ge 60 ]; then | |
| echo -e " ${RED}TIMEOUT — reset TRX ignoré${NC}"; return 1 | |
| fi | |
| done | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| cat > /tmp/trx_reset.vty << 'VTYCMDS' | |
| enable | |
| trx 0 reset | |
| end | |
| VTYCMDS | |
| if command -v nc >/dev/null 2>&1; then | |
| (sleep 1; cat /tmp/trx_reset.vty; sleep 1) \ | |
| | nc -q2 "${vty_host}" "${vty_port}" 2>/dev/null \ | |
| | grep -vE "^(OsmoBTS|Welcome|VTY|\s*$)" \ | |
| | sed "s/^/ /" || true | |
| else | |
| (sleep 1; cat /tmp/trx_reset.vty; sleep 2) \ | |
| | telnet "${vty_host}" "${vty_port}" 2>/dev/null \ | |
| | grep -vE "^(Trying|Connected|Escape|OsmoBTS|Welcome)" \ | |
| | sed "s/^/ /" || true | |
| fi | |
| rm -f /tmp/trx_reset.vty | |
| echo -e " ${GREEN}✓ TRX reset envoyé${NC}" | |
| } | |
| # ── Helper : attendre qu'un port TCP soit ouvert ────────────────────────────── | |
| wait_port() { | |
| local host="$1" port="$2" label="$3" timeout="${4:-60}" | |
| elapsed=0 | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] wait_port: $host:$port ($label)" | |
| echo -ne " Attente ${label} (${host}:${port})" | |
| while ! bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null; do | |
| sleep 1; elapsed=$((elapsed + 1)) | |
| echo -n "." | |
| if [ "$elapsed" -ge "$timeout" ]; then | |
| echo -e " ${RED}TIMEOUT${NC}"; return 1 | |
| fi | |
| done | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| } | |
| # ── Helper : attendre qu'un port UDP soit bindé ─────────────────────────────── | |
| wait_udp() { | |
| local port="$1" label="$2" timeout="${3:-30}" | |
| elapsed=0 | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] wait_udp: UDP $port ($label)" | |
| echo -ne " Attente ${label} (UDP ${port})" | |
| while ! ss -unlp | grep -q ":${port} "; do | |
| sleep 1; elapsed=$((elapsed + 1)) | |
| echo -n "." | |
| if [ "$elapsed" -ge "$timeout" ]; then | |
| echo -e " ${RED}TIMEOUT${NC}"; return 1 | |
| fi | |
| done | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # MAIN | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # ── 1. Reset ────────────────────────────────────────────────────────────────── | |
| echo -e "${GREEN}=== [1/10] Reset ===${NC}" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] Killing old processes..." | |
| usermod -u 0 -o osmocom 2>/dev/null || true | |
| pkill -9 -f fake_trx.py 2>/dev/null || true | |
| pkill -9 -f "mobile -c" 2>/dev/null || true | |
| pkill -9 -f trxcon 2>/dev/null || true | |
| sleep 1 | |
| tmux kill-server 2>/dev/null || true | |
| sleep 0.3 | |
| tmux start-server | |
| tmux new-session -d -s "$SESSION" -n main | |
| # ── 2. Génération config MS combinée ───────────────────────────────────────── | |
| echo -e "${GREEN}=== [2/10] Génération config MS combinée (N=${N_MS}) ===${NC}" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] Calling generate_ms_configs..." | |
| generate_ms_configs | |
| echo "" | |
| # ── 3. Core Osmocom (sans bts-trx ni sip-connector) ────────────────────────── | |
| echo -e "${GREEN}=== [3/10] Core Osmocom ===${NC}" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] Launching /etc/osmocom/osmo-start.sh..." | |
| /etc/osmocom/osmo-start.sh | |
| # ── 4. FakeTRX ─────────────────────────────────────────────────────────────── | |
| echo -e "${GREEN}=== [4/10] FakeTRX ===${NC}" | |
| tmux new-window -t "$SESSION" -n faketrx | |
| # Bind explicite sur 127.0.0.1, adresses BTS et BB explicites | |
| # -b = fake_trx bind address | |
| # -R = BTS remote (osmo-bts-trx) | |
| # -r = BB remote (trxcon) | |
| # -P = BTS base port (défaut 5700, osmo-bts-trx s'y connecte) | |
| # -p = BB base port (défaut 6700, trxcon MS1 s'y connecte) | |
| FAKETRX_CMD="python3 ${FAKETRX_PY} -b 127.0.0.1 -R 127.0.0.1 -r 127.0.0.1 -P 5700 -p 6700" | |
| for ms_idx in $(seq 2 "${N_MS}"); do | |
| trx_port=$(( 6700 + (ms_idx - 1) * 20 )) | |
| FAKETRX_CMD="${FAKETRX_CMD} --trx 127.0.0.1:${trx_port}" | |
| done | |
| echo -e " ${CYAN}Cmd :${NC} ${FAKETRX_CMD}" | |
| tmux send-keys -t "${SESSION}:faketrx" "${FAKETRX_CMD}" C-m | |
| wait_udp 5700 "fake_trx UDP" 30 || true | |
| # ── 5. OsmoBTS-TRX (APRÈS fake_trx confirmé) ───────────────────────────────── | |
| echo -e "${GREEN}=== [5/10] OsmoBTS-TRX ===${NC}" | |
| systemctl start osmo-bts-trx | |
| wait_port 127.0.0.1 4241 "OsmoBTS VTY" 60 || true | |
| # ── 6. trxcon — N instances séparées, chacune avec -C ───────────────────────── | |
| # | |
| # OsmocomBB : 1 trxcon par MS, chacun -C 1 (1 seul client L1CTL) : | |
| # trxcon -C 1 -b 127.0.0.1 -i 127.0.0.1 -p 6700 -s /tmp/osmocom_l2_1 (MS1) | |
| # trxcon -C 1 -b 127.0.0.1 -i 127.0.0.1 -p 6720 -s /tmp/osmocom_l2_2 (MS2) | |
| # trxcon -C 1 -b 127.0.0.1 -i 127.0.0.1 -p 6740 -s /tmp/osmocom_l2_3 (MS3) | |
| # | |
| # -b = trxcon bind address (écoute réponses de fake_trx) | |
| # -i = TRX remote address (envoie vers fake_trx) | |
| # -p = TRX base port (doit matcher le port BB de fake_trx pour ce MS) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo -e "${GREEN}=== [6/10] trxcon (${N_MS} instances, -C 1) ===${NC}" | |
| tmux new-window -t "$SESSION" -n trxcon | |
| for ms_idx in $(seq 1 "${N_MS}"); do | |
| l2_sock="${L2_SOCK_BASE}_${ms_idx}" | |
| trx_port=$(( 6700 + (ms_idx - 1) * 20 )) | |
| # Toujours explicite : bind, remote, port, max-clients=1 | |
| trxcon_cmd="trxcon -C 1 -b 127.0.0.1 -i 127.0.0.1 -p ${trx_port} -s ${l2_sock} -F 100" | |
| echo -e " ${CYAN}[trxcon${ms_idx}]${NC} ${trxcon_cmd}" | |
| # Premier dans le pane existant, suivants dans des splits | |
| if [ "$ms_idx" -gt 1 ]; then | |
| tmux split-window -v -t "${SESSION}:trxcon" | |
| tmux select-layout -t "${SESSION}:trxcon" even-vertical | |
| fi | |
| pane_idx=$(( ms_idx - 1 )) | |
| tmux send-keys -t "${SESSION}:trxcon.${pane_idx}" \ | |
| "echo '=== trxcon MS${ms_idx} ===' && sleep 2 && ${trxcon_cmd}" C-m | |
| done | |
| # ── 7. mobile (UN SEUL process, tous les ms) ────────────────────────────────── | |
| echo -e "${GREEN}=== [7/10] mobile (1 process, ${N_MS} MS) ===${NC}" | |
| combined_cfg="/root/.osmocom/bb/mobile_combined.cfg" | |
| # Attendre que les sockets L1CTL existent (créés par trxcon) | |
| echo -ne " Attente sockets L1CTL" | |
| elapsed=0 | |
| all_ready=false | |
| while [ "$elapsed" -lt 30 ]; do | |
| all_ready=true | |
| for ms_idx in $(seq 1 "${N_MS}"); do | |
| if [ ! -S "${L2_SOCK_BASE}_${ms_idx}" ]; then | |
| all_ready=false | |
| break | |
| fi | |
| done | |
| if $all_ready; then break; fi | |
| sleep 1; elapsed=$((elapsed + 1)); echo -n "." | |
| done | |
| if $all_ready; then | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| else | |
| # Diagnostic : lister ce que trxcon a réellement créé | |
| echo -e " ${YELLOW}partiel${NC}" | |
| echo -e " ${CYAN}Sockets L1CTL présents :${NC}" | |
| ls -la ${L2_SOCK_BASE}* 2>/dev/null | sed 's/^/ /' || echo " (aucun)" | |
| echo -e " ${YELLOW}→ Si nommage différent, ajuster L2_SOCK_BASE dans run.sh${NC}" | |
| fi | |
| tmux new-window -t "$SESSION" -n mobile | |
| tmux send-keys -t "${SESSION}:mobile" \ | |
| "echo '=== mobile — ${N_MS} MS ===' && sleep 3 && mobile -c ${combined_cfg}" C-m | |
| echo -e " ${GREEN}→ mobile -c ${combined_cfg}${NC}" | |
| echo -e " ${CYAN}Contrôle VTY :${NC} enable → show ms → ms 1 → network select 1" | |
| # ── 8. SIP-Connector (AVANT Asterisk — MNCC doit être prêt) ────────────────── | |
| echo -e "${GREEN}=== [8/10] SIP-Connector ===${NC}" | |
| echo -ne " Attente MNCC socket /tmp/msc_mncc" | |
| elapsed=0 | |
| while [ ! -S /tmp/msc_mncc ] && [ $elapsed -lt 30 ]; do | |
| sleep 1; elapsed=$((elapsed + 1)); echo -n "." | |
| done | |
| [ -S /tmp/msc_mncc ] && echo -e " ${GREEN}OK${NC}" || echo -e " ${YELLOW}absent${NC}" | |
| systemctl start osmo-sip-connector | |
| wait_port 127.0.0.1 4255 "SIP-Connector VTY" 30 || true | |
| # ── 9. Asterisk (APRÈS SIP-Connector) ──────────────────────────────────────── | |
| echo -e "${GREEN}=== [9/10] Asterisk ===${NC}" | |
| tmux new-window -t "$SESSION" -n asterisk | |
| tmux send-keys -t "${SESSION}:asterisk" \ | |
| "pkill asterisk 2>/dev/null; sleep 2; pkill -9 asterisk 2>/dev/null; sleep 1; rm -f /var/lib/asterisk/astdb.sqlite3; asterisk -cvvv" C-m | |
| # ── 10. SMSC + gapk ────────────────────────────────────────────────────────── | |
| echo -e "${GREEN}=== [10/10] SMSC + gapk ===${NC}" | |
| [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] Starting SMSC and gapk..." | |
| tmux new-window -t "$SESSION" -n smsc | |
| if [ -f /etc/osmocom/smsc-start.sh ]; then | |
| tmux send-keys -t "${SESSION}:smsc" "/etc/osmocom/smsc-start.sh" C-m | |
| else | |
| tmux send-keys -t "${SESSION}:smsc" "echo 'SMSC non disponible'" C-m | |
| fi | |
| tmux new-window -t "$SESSION" -n gapk | |
| tmux send-keys -t "${SESSION}:gapk" \ | |
| "echo '=== gapk audio ===' && sleep 3 && (gapk-start.sh auto 2>/dev/null || echo 'gapk non disponible')" C-m | |
| tmux select-window -t "${SESSION}:mobile" | |
| echo "" | |
| echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}" | |
| echo -e "${GREEN}║ Opérateur ${OPERATOR_ID} — Stack prête (${N_MS} MS) ║${NC}" | |
| echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}" | |
| echo "" | |
| echo -e " ${CYAN}Fenêtres :${NC} faketrx trxcon mobile asterisk smsc gapk" | |
| echo "" | |
| echo -e " ${CYAN}Contrôle MS :${NC} dans la fenêtre mobile (VTY) :" | |
| echo -e " enable → show ms → ms 1 → network select 1" | |
| echo "" | |
| echo -e " ${CYAN}Vérif TRX :${NC} fenêtre faketrx → chercher 'RSP POWERON'" | |
| echo -e " ${CYAN}Vérif TCH :${NC} telnet 127.0.0.1 4242 → show bts 0" | |
| echo -e " ${CYAN}Vérif PJSIP:${NC} asterisk -rx 'pjsip show endpoints'" | |
| echo -e " ${CYAN}VTY MSC :${NC} telnet 127.0.0.1 4254" | |
| echo -e " ${CYAN}VTY HLR :${NC} telnet 127.0.0.1 4258" | |
| echo "" | |
| echo -e " ${CYAN}Navigation :${NC} Ctrl-b w Ctrl-b n/p" | |
| echo "" | |
| tmux select-window -t "${SESSION}:mobile" | |
| tmux attach-session -t "$SESSION" | |
| #!/bin/bash | |
| # gapk-start.sh — Gestionnaire audio GSM (osmo-gapk ↔ ALSA/RTP) | |
| # | |
| # Intégration native dans la stack osmo_egprs : | |
| # • Mode `auto` : se greffe sur OsmoMGW pour détecter les appels actifs | |
| # et démarre/arrête gapk automatiquement via hook MGCP. | |
| # • Mode `call` : appel bidirectionnel ALSA ↔ RTP (RX + TX en background). | |
| # • Mode `monitor`: écoute passive RTP → ALSA (foreground). | |
| # • Mode `record` : enregistre flux RTP → fichier .gsm (foreground). | |
| # • Mode `playback`: injecte fichier .gsm → RTP (foreground). | |
| # • Mode `loopback`: boucle codec ALSA micro → GSM → HP (test). | |
| # • Mode `mgw-ports`: liste les endpoints/ports RTP OsmoMGW actifs. | |
| # • Mode `stop` : arrête toutes les instances en background. | |
| # • Mode `status`: état des instances en cours. | |
| # | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # ARCHITECTURE AUDIO | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # | |
| # Micro ALSA ──[PCM 8kHz]──► gapk-TX ──[GSM FR]──► RTP ──► OsmoMGW | |
| # OsmoMGW ──► RTP ──[GSM FR]──► gapk-RX ──[PCM 8kHz]──► HP ALSA | |
| # | |
| # OsmoMGW alloue les ports RTP dynamiquement (plage 4002–16001). | |
| # Les endpoints actifs sont visibles via : | |
| # echo "show mgcp" | telnet 127.0.0.1 4243 | |
| # | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # INTÉGRATION NATIVE (mode auto) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # | |
| # gapk-start.sh auto [FORMAT] [ALSA_DEV] | |
| # | |
| # Surveille OsmoMGW toutes les 2 secondes. | |
| # Dès qu'un endpoint passe en état "active" (connexion RTP ouverte), | |
| # démarre gapk RX+TX sur les ports détectés. | |
| # Dès que l'endpoint redevient idle, arrête les instances gapk. | |
| # | |
| # Ce mode tourne en foreground — idéal pour la fenêtre tmux [4]. | |
| # | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| set -euo pipefail | |
| GAPK_DEFAULT_FORMAT="${GAPK_FORMAT:-gsmfr}" | |
| GAPK_DEFAULT_DEVICE="${GAPK_ALSA_DEV:-${ALSA_CARD:-default}}" | |
| GAPK_LOG_DIR="${GAPK_LOG_DIR:-/var/log/osmocom}" | |
| GAPK_REC_DIR="${GAPK_REC_DIR:-/var/lib/gapk}" | |
| GAPK_FRAME_MS=20 | |
| MGW_VTY_HOST="127.0.0.1" | |
| MGW_VTY_PORT="4243" | |
| AUTO_POLL_INTERVAL="${GAPK_POLL_INTERVAL:-2}" | |
| GAPK_PID_RX="/var/run/gapk-rx.pid" | |
| GAPK_PID_TX="/var/run/gapk-tx.pid" | |
| GAPK_AUTO_LOCK="/var/run/gapk-auto.lock" | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m'; NC='\033[0m'; BOLD='\033[1m' | |
| mkdir -p "$GAPK_LOG_DIR" "$GAPK_REC_DIR" | |
| log_info() { echo -e "${GREEN}[gapk]${NC} $*"; } | |
| log_warn() { echo -e "${YELLOW}[gapk]${NC} $*"; } | |
| log_error() { echo -e "${RED}[gapk ERROR]${NC} $*" >&2; } | |
| log_auto() { echo -e "${CYAN}[gapk-auto]${NC} $(date '+%H:%M:%S') $*"; } | |
| # ── Vérifications ────────────────────────────────────────────────────────────── | |
| check_gapk() { | |
| command -v osmo-gapk >/dev/null 2>&1 || { | |
| log_error "osmo-gapk introuvable." | |
| log_error "Vérifier que le Dockerfile inclut l'étape osmo-gapk --enable-alsa." | |
| exit 1 | |
| } | |
| } | |
| check_alsa() { | |
| local dev="${1:-default}" | |
| if [ ! -d /dev/snd ]; then | |
| log_warn "/dev/snd absent — container lancé sans --device /dev/snd" | |
| log_warn "Modes record/playback fonctionnent sans ALSA." | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| # ── Cycle de vie background ──────────────────────────────────────────────────── | |
| run_bg() { | |
| local pid_file="$1"; shift | |
| local log_file="$1"; shift | |
| osmo-gapk "$@" >> "$log_file" 2>&1 & | |
| echo $! > "$pid_file" | |
| log_info "PID $(cat "$pid_file") → $(basename "$log_file")" | |
| } | |
| stop_pid() { | |
| local pid_file="$1" | |
| [ -f "$pid_file" ] || return 0 | |
| local pid; pid=$(cat "$pid_file") | |
| if kill -0 "$pid" 2>/dev/null; then | |
| kill "$pid" 2>/dev/null && log_info "Arrêté PID $pid" || true | |
| fi | |
| rm -f "$pid_file" | |
| } | |
| is_running() { | |
| local pid_file="$1" | |
| [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # mgw-ports — interroge OsmoMGW VTY et retourne les ports RTP actifs | |
| # | |
| # Sortie : une ligne par endpoint actif "RX_PORT TX_IP:TX_PORT" | |
| # (RX = port local MGW pour downlink ; TX = destination uplink MGW) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_mgw_ports() { | |
| local quiet="${1:-}" | |
| local mgw_out | |
| mgw_out=$(echo "show mgcp" | telnet "$MGW_VTY_HOST" "$MGW_VTY_PORT" 2>/dev/null \ | |
| | grep -v "^Trying\|^Connected\|^Escape\|Welcome\|OsmoMGW[>#]") || true | |
| if [ -z "$mgw_out" ]; then | |
| [ -z "$quiet" ] && echo "(MGW non disponible ou aucun endpoint)" | |
| return 1 | |
| fi | |
| # Extrait les endpoints actifs avec leur port RTP local | |
| # Format VTY OsmoMGW : | |
| # Endpoint: rtpbridge/*@mgw | |
| # Conn: 0x... (RHCF) port: 4002 ... | |
| # Conn: 0x... (LCLS) port: 4004 RTP-IP: 127.0.0.1 RTP-Port: 4006 | |
| if [ -z "$quiet" ]; then | |
| echo -e "${CYAN}── Endpoints OsmoMGW actifs ──${NC}" | |
| echo "$mgw_out" | grep -E "Endpoint:|port:|RTP" | sed 's/^/ /' || true | |
| else | |
| # Mode machine : sortie parsée "RX_PORT TX_IP:TX_PORT" | |
| echo "$mgw_out" | awk ' | |
| /Endpoint:/ { ep=$0 } | |
| /port:/ { | |
| match($0, /port: ([0-9]+)/, p) | |
| match($0, /RTP-IP: ([0-9.]+) RTP-Port: ([0-9]+)/, r) | |
| if (p[1] != "" && r[1] != "") | |
| print p[1], r[1]":"r[2] | |
| } | |
| ' | |
| fi | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # call — appel bidirectionnel ALSA ↔ RTP | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_call() { | |
| local rx_port="${1:?RTP_RX_PORT requis (port MGW downlink)}" | |
| local tx_dest="${2:?RTP_TX_DEST:PORT requis (dest MGW uplink)}" | |
| local fmt="${3:-$GAPK_DEFAULT_FORMAT}" | |
| local dev="${4:-$GAPK_DEFAULT_DEVICE}" | |
| check_gapk | |
| check_alsa "$dev" || true | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| echo -e "${CYAN}${BOLD}" | |
| echo "╔══════════════════════════════════════════╗" | |
| echo "║ gapk — Appel bidirectionnel ALSA↔RTP ║" | |
| echo "╚══════════════════════════════════════════╝" | |
| echo -e "${NC}" | |
| printf " Codec : ${CYAN}%s${NC}\n" "$fmt" | |
| printf " Périph. : ${CYAN}%s${NC}\n" "$dev" | |
| printf " RX écoute : ${CYAN}0.0.0.0:%s${NC} (downlink MGW → HP)\n" "$rx_port" | |
| printf " TX envoi : ${CYAN}%s${NC} (micro → uplink MGW)\n" "$tx_dest" | |
| echo "" | |
| local log_rx="${GAPK_LOG_DIR}/gapk-rx.log" | |
| local log_tx="${GAPK_LOG_DIR}/gapk-tx.log" | |
| log_info "RX : rtp://0.0.0.0:${rx_port} → alsa://${dev}" | |
| run_bg "$GAPK_PID_RX" "$log_rx" \ | |
| -f "$fmt" \ | |
| -i "rtp://0.0.0.0:${rx_port}/${GAPK_FRAME_MS}" \ | |
| -o "alsa://${dev}/${GAPK_FRAME_MS}" | |
| sleep 0.3 | |
| log_info "TX : alsa://${dev} → rtp://${tx_dest}" | |
| run_bg "$GAPK_PID_TX" "$log_tx" \ | |
| -f "$fmt" \ | |
| -i "alsa://${dev}/${GAPK_FRAME_MS}" \ | |
| -o "rtp://${tx_dest}/${GAPK_FRAME_MS}" | |
| echo "" | |
| log_info "Appel actif. Arrêter : gapk-start.sh stop" | |
| log_info "Logs RX : tail -f $log_rx" | |
| log_info "Logs TX : tail -f $log_tx" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # auto — intégration native : surveille MGW et gère gapk automatiquement | |
| # | |
| # Démarre gapk dès qu'un endpoint MGW passe actif (connexion RTP ouverte). | |
| # Arrête gapk quand l'endpoint redevient idle. | |
| # Tourne en foreground dans la fenêtre tmux [4]. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_auto() { | |
| local fmt="${1:-$GAPK_DEFAULT_FORMAT}" | |
| local dev="${2:-$GAPK_DEFAULT_DEVICE}" | |
| check_gapk | |
| check_alsa "$dev" || log_warn "ALSA absent — mode RTP-only si appel détecté" | |
| echo -e "${CYAN}${BOLD}" | |
| echo "╔══════════════════════════════════════════════════╗" | |
| echo "║ gapk-auto — Intégration native OsmoMGW ║" | |
| echo "║ Surveille les endpoints RTP actifs ║" | |
| echo "╚══════════════════════════════════════════════════╝" | |
| echo -e "${NC}" | |
| log_auto "Codec=${fmt} Périph=${dev} Poll=${AUTO_POLL_INTERVAL}s" | |
| log_auto "Ctrl+C pour arrêter" | |
| echo "" | |
| touch "$GAPK_AUTO_LOCK" | |
| local prev_rx_port="" | |
| cleanup_auto() { | |
| log_auto "Arrêt..." | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| rm -f "$GAPK_AUTO_LOCK" | |
| exit 0 | |
| } | |
| trap cleanup_auto SIGINT SIGTERM | |
| while [ -f "$GAPK_AUTO_LOCK" ]; do | |
| # Lire le premier endpoint actif (port RX + TX dest) | |
| local first_active | |
| first_active=$(mode_mgw_ports "quiet" 2>/dev/null | head -1) || first_active="" | |
| if [ -n "$first_active" ]; then | |
| local rx_port tx_dest | |
| rx_port=$(echo "$first_active" | awk '{print $1}') | |
| tx_dest=$(echo "$first_active" | awk '{print $2}') | |
| if [ "$rx_port" != "$prev_rx_port" ]; then | |
| if [ -n "$prev_rx_port" ]; then | |
| log_auto "Changement d'endpoint (${prev_rx_port} → ${rx_port}) — restart gapk" | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| fi | |
| log_auto "Endpoint actif détecté : RX=${rx_port} TX=${tx_dest}" | |
| log_auto "Démarrage gapk (${fmt}) ..." | |
| local log_rx="${GAPK_LOG_DIR}/gapk-rx.log" | |
| local log_tx="${GAPK_LOG_DIR}/gapk-tx.log" | |
| # RX : MGW → ALSA (downlink) | |
| run_bg "$GAPK_PID_RX" "$log_rx" \ | |
| -f "$fmt" \ | |
| -i "rtp://0.0.0.0:${rx_port}/${GAPK_FRAME_MS}" \ | |
| -o "alsa://${dev}/${GAPK_FRAME_MS}" | |
| sleep 0.3 | |
| # TX : ALSA → MGW (uplink) | |
| run_bg "$GAPK_PID_TX" "$log_tx" \ | |
| -f "$fmt" \ | |
| -i "alsa://${dev}/${GAPK_FRAME_MS}" \ | |
| -o "rtp://${tx_dest}/${GAPK_FRAME_MS}" | |
| prev_rx_port="$rx_port" | |
| log_auto "Audio actif (PID RX=$(cat $GAPK_PID_RX 2>/dev/null) TX=$(cat $GAPK_PID_TX 2>/dev/null))" | |
| fi | |
| # Vérifier que les processus sont toujours vivants | |
| if ! is_running "$GAPK_PID_RX" || ! is_running "$GAPK_PID_TX"; then | |
| log_warn "Processus gapk mort de façon inattendue — restart" | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| prev_rx_port="" | |
| fi | |
| else | |
| # Pas d'endpoint actif | |
| if [ -n "$prev_rx_port" ]; then | |
| log_auto "Endpoint disparu (port ${prev_rx_port}) — arrêt gapk" | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| prev_rx_port="" | |
| fi | |
| fi | |
| sleep "$AUTO_POLL_INTERVAL" | |
| done | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # monitor — écoute passive RTP → ALSA (foreground) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_monitor() { | |
| local rx_port="${1:?RTP_RX_PORT requis}" | |
| local fmt="${2:-$GAPK_DEFAULT_FORMAT}" | |
| local dev="${3:-$GAPK_DEFAULT_DEVICE}" | |
| check_gapk | |
| check_alsa "$dev" || true | |
| stop_pid "$GAPK_PID_RX" | |
| log_info "Monitor : rtp://0.0.0.0:${rx_port} → alsa://${dev} [${fmt}]" | |
| log_info "Ctrl+C pour arrêter" | |
| exec osmo-gapk \ | |
| -f "$fmt" \ | |
| -i "rtp://0.0.0.0:${rx_port}/${GAPK_FRAME_MS}" \ | |
| -o "alsa://${dev}/${GAPK_FRAME_MS}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # record — enregistre flux RTP → fichier .gsm brut | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_record() { | |
| local rx_port="${1:?RTP_RX_PORT requis}" | |
| local outfile="${2:-${GAPK_REC_DIR}/record_$(date '+%Y%m%d_%H%M%S').gsm}" | |
| local fmt="${3:-$GAPK_DEFAULT_FORMAT}" | |
| check_gapk | |
| stop_pid "$GAPK_PID_RX" | |
| log_info "Record : rtp://0.0.0.0:${rx_port} → ${outfile} [${fmt}]" | |
| log_info "Ctrl+C pour arrêter" | |
| log_info "Relire : gapk-start.sh playback ${outfile} <DEST:PORT>" | |
| exec osmo-gapk \ | |
| -f "$fmt" \ | |
| -i "rtp://0.0.0.0:${rx_port}/${GAPK_FRAME_MS}" \ | |
| -o "rawfile://${outfile}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # playback — injecte fichier .gsm dans un flux RTP | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_playback() { | |
| local infile="${1:?INPUT_FILE requis}" | |
| local tx_dest="${2:?RTP_TX_DEST:PORT requis}" | |
| local fmt="${3:-$GAPK_DEFAULT_FORMAT}" | |
| check_gapk | |
| [ -f "$infile" ] || { log_error "Fichier introuvable : $infile"; exit 1; } | |
| log_info "Playback : ${infile} → rtp://${tx_dest} [${fmt}]" | |
| log_info "Ctrl+C pour arrêter" | |
| exec osmo-gapk \ | |
| -f "$fmt" \ | |
| -i "rawfile://${infile}" \ | |
| -o "rtp://${tx_dest}/${GAPK_FRAME_MS}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # loopback — ALSA micro → encode → décode → ALSA HP (test codec) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_loopback() { | |
| local fmt="${1:-$GAPK_DEFAULT_FORMAT}" | |
| local dev="${2:-$GAPK_DEFAULT_DEVICE}" | |
| check_gapk | |
| check_alsa "$dev" || { log_error "ALSA requis pour le loopback."; exit 1; } | |
| echo -e "${YELLOW}${BOLD}⚠ UTILISEZ DES ÉCOUTEURS — risque de larsen !${NC}" | |
| echo "" | |
| log_info "Loopback : alsa://${dev} → [${fmt}] → alsa://${dev}" | |
| log_info "Ctrl+C pour arrêter" | |
| echo "" | |
| exec osmo-gapk \ | |
| -f "$fmt" \ | |
| -i "alsa://${dev}/${GAPK_FRAME_MS}" \ | |
| -o "alsa://${dev}/${GAPK_FRAME_MS}" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # stop / status / list | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| mode_stop() { | |
| stop_pid "$GAPK_PID_RX" | |
| stop_pid "$GAPK_PID_TX" | |
| rm -f "$GAPK_AUTO_LOCK" | |
| pkill -x osmo-gapk 2>/dev/null && log_info "Toutes instances arrêtées" \ | |
| || log_info "Aucune instance active" | |
| } | |
| mode_status() { | |
| echo -e "${CYAN}── État osmo-gapk ──${NC}" | |
| local any=0 | |
| for f in "$GAPK_PID_RX" "$GAPK_PID_TX"; do | |
| [ -f "$f" ] || continue | |
| local pid; pid=$(cat "$f") | |
| if kill -0 "$pid" 2>/dev/null; then | |
| local cmdline; cmdline=$(cat "/proc/${pid}/cmdline" 2>/dev/null \ | |
| | tr '\0' ' ' | cut -c1-80 || echo "?") | |
| echo -e " ${GREEN}●${NC} PID ${pid} [$(basename "$f")] ${cmdline}" | |
| any=1 | |
| else | |
| echo -e " ${RED}✗${NC} PID ${pid} mort"; rm -f "$f" | |
| fi | |
| done | |
| [ -f "$GAPK_AUTO_LOCK" ] && echo -e " ${CYAN}●${NC} mode AUTO actif" && any=1 | |
| [ $any -eq 0 ] && echo -e " ${YELLOW}Aucune instance active${NC}" | |
| if pgrep -x osmo-gapk >/dev/null 2>&1; then | |
| echo "" | |
| echo -e "${CYAN}Processus détectés :${NC}" | |
| ps -o pid,args --no-headers -C osmo-gapk 2>/dev/null | sed 's/^/ /' || true | |
| fi | |
| } | |
| mode_list_codecs() { | |
| check_gapk | |
| echo -e "${CYAN}Codecs disponibles :${NC}" | |
| osmo-gapk --list-codecs 2>&1 || \ | |
| osmo-gapk --help 2>&1 | grep -A30 -i "format\|codec" || true | |
| } | |
| mode_list_devices() { | |
| echo -e "${CYAN}── Périphériques ALSA ──${NC}" | |
| echo "Lecture (HP) :" | |
| aplay -L 2>/dev/null | head -20 || echo " (aplay indisponible)" | |
| echo "" | |
| echo "Capture (micro) :" | |
| arecord -L 2>/dev/null | head -20 || echo " (arecord indisponible)" | |
| echo "" | |
| echo "Cartes son :" | |
| cat /proc/asound/cards 2>/dev/null || echo " (relancer avec --device /dev/snd)" | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Usage | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| usage() { | |
| cat <<'EOF' | |
| Usage: gapk-start.sh <mode> [options] | |
| auto [FORMAT] [ALSA_DEV] | |
| Intégration native : surveille MGW, démarre/arrête gapk auto. | |
| Tourne en foreground (idéal fenêtre tmux). | |
| ex: gapk-start.sh auto gsmfr default | |
| call <RX_PORT> <TX_DEST:PORT> [FORMAT] [ALSA_DEV] | |
| Appel bidirectionnel ALSA ↔ RTP (background). | |
| ex: gapk-start.sh call 4002 127.0.0.1:4004 gsmfr | |
| monitor <RX_PORT> [FORMAT] [ALSA_DEV] | |
| Écoute passive RTP → ALSA (foreground). | |
| ex: gapk-start.sh monitor 4002 | |
| record <RX_PORT> [FICHIER] [FORMAT] | |
| Enregistre flux RTP → fichier .gsm. | |
| ex: gapk-start.sh record 4002 /var/lib/gapk/appel.gsm | |
| playback <FICHIER> <TX_DEST:PORT> [FORMAT] | |
| Injecte fichier .gsm → RTP. | |
| ex: gapk-start.sh playback /var/lib/gapk/appel.gsm 127.0.0.1:4002 | |
| loopback [FORMAT] [ALSA_DEV] | |
| Loopback codec ALSA (ÉCOUTEURS OBLIGATOIRES). | |
| mgw-ports Liste les endpoints/ports RTP OsmoMGW actifs. | |
| stop Arrête toutes les instances. | |
| status État des instances. | |
| list-codecs Codecs disponibles. | |
| list-devices Périphériques ALSA. | |
| Formats : gsmfr (défaut) | gsmefr | gsmhr | pcm8 | pcm16 | |
| Variables : GAPK_FORMAT, GAPK_ALSA_DEV, ALSA_CARD, GAPK_POLL_INTERVAL | |
| Ports RTP OsmoMGW (manuel) : | |
| echo "show mgcp" | telnet 127.0.0.1 4243 | |
| EOF | |
| } | |
| # ── Dispatch ────────────────────────────────────────────────────────────────── | |
| MODE="${1:-help}"; shift 2>/dev/null || true | |
| case "$MODE" in | |
| auto) mode_auto "$@" ;; | |
| call) mode_call "$@" ;; | |
| monitor) mode_monitor "$@" ;; | |
| record) mode_record "$@" ;; | |
| playback) mode_playback "$@" ;; | |
| loopback) mode_loopback "$@" ;; | |
| mgw-ports) mode_mgw_ports "" ;; | |
| stop) mode_stop ;; | |
| status) mode_status ;; | |
| list-codecs) mode_list_codecs ;; | |
| list-devices) mode_list_devices ;; | |
| help|-h|--help) usage ;; | |
| *) log_error "Mode inconnu : $MODE"; echo ""; usage; exit 1 ;; | |
| esac | |
| #!/bin/bash | |
| cmd="${1:-status}" | |
| set -ex | |
| systemctl daemon-reload | |
| systemctl $cmd osmo-hlr \ | |
| osmo-msc \ | |
| osmo-mgw \ | |
| osmo-stp \ | |
| osmo-bsc \ | |
| osmo-ggsn \ | |
| osmo-sgsn \ | |
| osmo-sip-connector \ | |
| osmo-bts-trx \ | |
| osmo-pcu | |
| #!/bin/bash | |
| # send-mt-sms.sh — Envoi de SMS MT local via proto-smsc-sendmt | |
| # Usage : ./send-mt-sms.sh <imsi> <message> [from_number] | |
| set -euo pipefail | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| SC_ADDRESS="1999001${OPERATOR_ID}444" | |
| SENDMT_SOCKET="/tmp/sendmt_socket" | |
| if [ $# -lt 2 ]; then | |
| echo "Usage: $0 <imsi> <message> [from_number]" | |
| echo " Ex: $0 001010000000001 'Bonjour!'" | |
| exit 1 | |
| fi | |
| DEST_IMSI="$1"; MESSAGE="$2"; FROM="${3:-${SC_ADDRESS}}" | |
| [ ! -S "$SENDMT_SOCKET" ] && { echo "ERREUR: $SENDMT_SOCKET absent"; exit 1; } | |
| sms-encode-text "$MESSAGE" \ | |
| | gen-sms-deliver-pdu "$FROM" \ | |
| | proto-smsc-sendmt "$SC_ADDRESS" "$DEST_IMSI" "$SENDMT_SOCKET" | |
| echo "MT SMS envoyé → IMSI=$DEST_IMSI" | |
| #!/bin/bash | |
| # osmo-start.sh — Démarrage séquencé du Core Osmocom | |
| # | |
| # Ordre de dépendance : | |
| # STP (4239) → HLR (4258) → MGW (4243) | |
| # ↓ | |
| # MSC (4254) → BSC (4242) | |
| # ↓ | |
| # GGSN (4260) → SGSN (4245) → PCU (4239 bts) | |
| # | |
| # Chaque service attend le VTY du précédent avant de démarrer. | |
| # BTS-TRX et SIP-connector sont gérés par run.sh (dépendent de fake_trx / Asterisk). | |
| # | |
| # Appelé par : run.sh (étape 3) | |
| set -e | |
| GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m'; NC='\033[0m' | |
| OPERATOR_ID="${OPERATOR_ID:-1}" | |
| # ── Helper : attendre qu'un port TCP soit ouvert ────────────────────────────── | |
| wait_port() { | |
| local host="$1" port="$2" label="$3" timeout="${4:-30}" | |
| local elapsed=0 | |
| echo -ne " Attente ${label} (${host}:${port})" | |
| while ! bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null; do | |
| sleep 1; elapsed=$((elapsed + 1)) | |
| echo -n "." | |
| if [ "$elapsed" -ge "$timeout" ]; then | |
| echo -e " ${RED}TIMEOUT${NC}" | |
| return 1 | |
| fi | |
| done | |
| echo -e " ${GREEN}OK${NC} (${elapsed}s)" | |
| } | |
| # ── Helper : démarrer un service et vérifier ────────────────────────────────── | |
| start_svc() { | |
| local svc="$1" vty_port="$2" label="$3" timeout="${4:-30}" | |
| echo -e " ${CYAN}${label}${NC}" | |
| systemctl start "$svc" || { | |
| echo -e " ${RED}✗ systemctl start ${svc} échoué${NC}" | |
| journalctl -u "$svc" -n 20 --no-pager >&2 | |
| return 1 | |
| } | |
| if [ -n "$vty_port" ]; then | |
| wait_port 127.0.0.1 "$vty_port" "$label VTY" "$timeout" || { | |
| echo -e " ${YELLOW}[WARN] VTY :${vty_port} non accessible après ${timeout}s${NC}" >&2 | |
| journalctl -u "$svc" -n 10 --no-pager >&2 | |
| return 1 | |
| } | |
| fi | |
| } | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo -e "${GREEN}=== Core Osmocom — Op${OPERATOR_ID} ===${NC}" | |
| echo "" | |
| # ── 1. Réseau TUN (APN0 pour GGSN) ─────────────────────────────────────────── | |
| echo -e "${CYAN}[1/4] Interface TUN${NC}" | |
| if ip link show apn0 > /dev/null 2>&1; then | |
| ip link del dev apn0 | |
| fi | |
| ip tuntap add dev apn0 mode tun | |
| ip addr add 176.16.32.0/24 dev apn0 | |
| ip link set dev apn0 up | |
| echo -e " ${GREEN}✓${NC} apn0 up" | |
| # ── 2. Signalisation (STP → HLR → MGW) ─────────────────────────────────────── | |
| # | |
| # STP doit être prêt en premier : tout le SS7 passe par lui. | |
| # HLR doit être prêt avant MSC : MSC se connecte au HLR au démarrage. | |
| # MGW doit être prêt avant MSC : MSC ouvre un MGCP vers MGW. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo "" | |
| echo -e "${CYAN}[2/4] Signalisation (STP → HLR → MGW)${NC}" | |
| start_svc osmo-stp 4239 "OsmoSTP" 30 || true | |
| start_svc osmo-hlr 4258 "OsmoHLR" 30 || { | |
| echo -e " ${RED}[ERR] HLR indispensable pour MSC — abandon${NC}" | |
| exit 1 | |
| } | |
| start_svc osmo-mgw 4243 "OsmoMGW" 20 || true | |
| # ── 3. Core Network (MSC → BSC) ────────────────────────────────────────────── | |
| # | |
| # MSC doit être prêt avant BSC : BSC se connecte au MSC via A-interface. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| echo "" | |
| echo -e "${CYAN}[3/4] Core Network (MSC → BSC)${NC}" | |
| start_svc osmo-msc 4254 "OsmoMSC" 30 || true | |
| start_svc osmo-bsc 4242 "OsmoBSC" 30 || true | |
| # ── 4. Data (GGSN → SGSN → PCU) ────────────────────────────────────────────── | |
| echo "" | |
| echo -e "${CYAN}[4/4] Data (GGSN → SGSN → PCU)${NC}" | |
| start_svc osmo-ggsn 4260 "OsmoGGSN" 20 || true | |
| start_svc osmo-sgsn 4245 "OsmoSGSN" 20 || true | |
| start_svc osmo-pcu "" "OsmoPCU" 0 || true | |
| sleep 1 | |
| chmod 777 /tmp/pcu_bts 2>/dev/null || true | |
| # NOTE : osmo-bts-trx et osmo-sip-connector sont intentionnellement | |
| # absents ici. run.sh les démarre dans l'ordre correct : | |
| # fake_trx → wait_udp 5700 → osmo-bts-trx (évite la race condition TRX) | |
| # Asterisk → osmo-sip-connector (évite le MNCC connect avant SIP UP) | |
| # ── Résumé ──────────────────────────────────────────────────────────────────── | |
| echo "" | |
| echo -e "${GREEN}=== Vérification ===${NC}" | |
| SERVICES="osmo-stp osmo-hlr osmo-mgw osmo-msc osmo-bsc osmo-ggsn osmo-sgsn osmo-pcu" | |
| for svc in $SERVICES; do | |
| if systemctl is-active --quiet "$svc"; then | |
| echo -e " ${GREEN}✓${NC} ${svc}" | |
| else | |
| echo -e " ${RED}✗${NC} ${svc}" | |
| fi | |
| done | |
| echo "" | |
| echo -e "${GREEN}Core Osmocom prêt. BTS et SIP connector gérés par run.sh.${NC}" | |
| #!/bin/bash | |
| # sms-routing-setup.sh — Gestion complète du routage SMS inter-opérateur | |
| # | |
| # Fonctions exportables appelées depuis start.sh : | |
| # sms_routing_generate <op_id> <n_ops> <destdir> [op1_nms op2_nms ...] | |
| # sms_routing_validate <conf_file> | |
| # sms_routing_summary <n_ops> [op1_nms op2_nms ...] | |
| # | |
| # Formules COMMUNES (identiques à run.sh, hlr-feed-subscribers.sh) : | |
| # MSISDN = op_id * 10000 + ms_idx | |
| # IMSI = MCC(3) + MNC(2) + printf('%04d%06d', op_id, ms_idx) | |
| # KI = 00 11 22 33 44 55 66 77 88 99 aa bb cc dd <ms_hex> <op_hex> | |
| # | |
| # Architecture de routage : | |
| # ┌─────────────────────────────────────────────────────────────────────┐ | |
| # │ MS (op1, ms1) MSISDN=10001 envoie à MSISDN=20002 (op2, ms2) │ | |
| # │ → proto-smsc-daemon (op1) → MO log │ | |
| # │ → sms-interop-relay.py (op1) : lookup prefix 20002 → op2 │ | |
| # │ → TCP 172.20.0.12:7890 │ | |
| # │ → sms-interop-relay.py (op2) : MSISDN→IMSI via HLR VTY │ | |
| # │ → proto-smsc-sendmt (op2) → HLR (op2) → MS (op2, ms2) │ | |
| # └─────────────────────────────────────────────────────────────────────┘ | |
| # | |
| # Usage autonome (test/debug) : | |
| # bash sms-routing-setup.sh generate 1 3 /tmp/sms-cfg 2 2 3 | |
| # bash sms-routing-setup.sh validate /tmp/sms-cfg/sms-routing-op1.conf | |
| # bash sms-routing-setup.sh summary 3 2 2 3 | |
| set -euo pipefail | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Helpers internes | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| _sms_op_backbone_ip() { echo "172.20.0.$((10 + $1))"; } | |
| _sms_op_msisdn() { echo $(( $1 * 10000 + $2 )); } # op_id ms_idx | |
| _sms_op_ms_imsi() { # mcc mnc op_id ms_idx | |
| local mcc=$1 mnc=$2 op=$3 ms=$4 | |
| printf '%s%s%04d%06d' "$mcc" "$mnc" "$op" "$ms" | |
| } | |
| _sms_sc_address() { printf '1999001%s444' "$1"; } # op_id | |
| _log_ok() { echo -e " ${GREEN}✓${NC} $*"; } | |
| _log_warn() { echo -e " ${YELLOW}⚠${NC} $*"; } | |
| _log_err() { echo -e " ${RED}✗${NC} $*" >&2; } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_generate | |
| # | |
| # Génère configs/sms-routing-op<N>.conf pour chaque opérateur. | |
| # Le fichier de chaque opérateur contient TOUTES les routes (locales + distantes) | |
| # et est monté dans /etc/osmocom/sms-routing.conf du container. | |
| # | |
| # Usage : | |
| # sms_routing_generate <op_id> <n_ops> <destdir> [ms_counts...] | |
| # ms_counts : nombre de MS par opérateur (1 valeur par opérateur) | |
| # ex: "2 3 1" → op1=2MS, op2=3MS, op3=1MS | |
| # | |
| # Sortie : | |
| # <destdir>/sms-routing-op<op_id>.conf | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_generate() { | |
| local op_id="$1" | |
| local n_ops="$2" | |
| local destdir="$3" | |
| shift 3 | |
| local ms_counts=("$@") # tableau : ms_counts[0]=nMS_op1, [1]=nMS_op2, ... | |
| mkdir -p "$destdir" | |
| local outfile="${destdir}/sms-routing-op${op_id}.conf" | |
| # Valeurs par défaut si ms_counts non fourni | |
| local -a nms | |
| for i in $(seq 1 "$n_ops"); do | |
| nms[$i]=${ms_counts[$((i-1))]:-1} | |
| done | |
| local mcc="${MCC:-001}" | |
| cat > "$outfile" << HEADER | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms-routing-op${op_id}.conf | |
| # Généré par sms-routing-setup.sh — $(date '+%Y-%m-%d %H:%M:%S') | |
| # | |
| # Opérateur local : ${op_id} | |
| # Nombre d'opérat. : ${n_ops} | |
| # MS par opérateur : $(for i in $(seq 1 "$n_ops"); do printf 'Op%s=%s ' "$i" "${nms[$i]}"; done) | |
| # | |
| # Routage MSISDN : | |
| # MSISDN = op_id × 10000 + ms_idx | |
| # Règle longest-prefix match (préfixe le plus long l'emporte) | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| [local] | |
| operator_id = ${op_id} | |
| sc_address = $(_sms_sc_address "$op_id") | |
| hlr_vty_ip = 127.0.0.1 | |
| hlr_vty_port = 4258 | |
| sendmt_socket = /tmp/sendmt_socket | |
| mo_log = /var/log/osmocom/mo-sms-op${op_id}.log | |
| [operators] | |
| # operator_id = container_ip (réseau backbone 172.20.0.0/24) | |
| HEADER | |
| for i in $(seq 1 "$n_ops"); do | |
| printf '%s = %s\n' "$i" "$(_sms_op_backbone_ip "$i")" >> "$outfile" | |
| done | |
| cat >> "$outfile" << ROUTES_HEADER | |
| [routes] | |
| # Format : prefix = operator_id | |
| # Longest-prefix match : le préfixe le plus long l'emporte. | |
| # Stratégie : | |
| # 1. MSISDN exact de chaque MS → routage précis (priorité max) | |
| # 2. Préfixe court par opérateur → fallback pour MSISDN inconnus | |
| # 3. Préfixes E.164 (+336XX…) → routage international | |
| # | |
| ROUTES_HEADER | |
| # ── Routes exactes par MS (priorité maximale) ───────────────────────────── | |
| for i in $(seq 1 "$n_ops"); do | |
| local mnc; mnc=$(printf '%02d' "$i") | |
| printf '\n# ── Opérateur %s (SC=%s) ──────────────────────────────────────\n' \ | |
| "$i" "$(_sms_sc_address "$i")" >> "$outfile" | |
| printf '# %s MS déclarés\n' "${nms[$i]}" >> "$outfile" | |
| for ms in $(seq 1 "${nms[$i]}"); do | |
| local msisdn; msisdn=$(_sms_op_msisdn "$i" "$ms") | |
| local imsi; imsi=$(_sms_op_ms_imsi "$mcc" "$mnc" "$i" "$ms") | |
| # Commentaire IMSI pour traçabilité | |
| printf '%-8s = %s # IMSI=%s\n' "$msisdn" "$i" "$imsi" >> "$outfile" | |
| done | |
| # Préfixe court de l'opérateur (fallback MSISDN hors liste) | |
| local op_prefix="${i}0000" | |
| printf '# Fallback opérateur %s (MSISDN inconnu)\n' "$i" >> "$outfile" | |
| printf '%-8s = %s\n' "$op_prefix" "$i" >> "$outfile" | |
| # Préfixe E.164 fictif : +336<op>X... (format français simulé) | |
| local e164_prefix; e164_prefix=$(printf '336%02d' "$i") | |
| printf '%-8s = %s # E.164 +33 6%02d...\n' "$e164_prefix" "$i" "$i" >> "$outfile" | |
| done | |
| cat >> "$outfile" << ROUTES_FOOTER | |
| [relay] | |
| # Port TCP sur lequel ce relay écoute les MT entrants d'autres opérateurs. | |
| port = 7890 | |
| # Timeout connexion vers un relay distant (secondes) | |
| connect_timeout = 10 | |
| # Tentatives de réémission si le relay distant est indisponible | |
| retry_count = 3 | |
| retry_delay = 5 | |
| ROUTES_FOOTER | |
| _log_ok "sms-routing-op${op_id}.conf → ${outfile}" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_generate_all | |
| # | |
| # Génère les configs pour TOUS les opérateurs en une seule passe. | |
| # Appelé depuis start.sh en mode bridge. | |
| # | |
| # Usage : | |
| # sms_routing_generate_all <n_ops> <destdir> [ms_counts...] | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_generate_all() { | |
| local n_ops="$1" | |
| local destdir="$2" | |
| shift 2 | |
| local ms_counts=("$@") | |
| echo -e "${CYAN}${BOLD}── SMS Routing — génération configs (${n_ops} opérateurs) ──${NC}" | |
| for i in $(seq 1 "$n_ops"); do | |
| sms_routing_generate "$i" "$n_ops" "$destdir" "${ms_counts[@]}" | |
| done | |
| echo -e " ${GREEN}✓ Configs générées dans ${destdir}${NC}" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_validate | |
| # | |
| # Vérifie la cohérence d'un fichier sms-routing.conf : | |
| # • Section [local] présente | |
| # • Section [operators] non vide | |
| # • Section [routes] non vide | |
| # • Aucune collision de préfixe exact entre deux opérateurs différents | |
| # • MSISDN locaux routés vers l'opérateur local | |
| # | |
| # Retourne 0 si OK, 1 si erreurs. | |
| # | |
| # Usage : | |
| # sms_routing_validate <conf_file> | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_validate() { | |
| local conf="$1" | |
| local errors=0 | |
| echo -e "${CYAN}Validation : ${conf}${NC}" | |
| if [ ! -f "$conf" ]; then | |
| _log_err "Fichier introuvable : $conf" | |
| return 1 | |
| fi | |
| # ── Sections obligatoires ───────────────────────────────────────────────── | |
| for section in local operators routes; do | |
| if ! grep -q "^\[${section}\]" "$conf"; then | |
| _log_err "Section manquante : [${section}]" | |
| errors=$(( errors + 1 )) | |
| fi | |
| done | |
| # ── operator_id dans [local] ────────────────────────────────────────────── | |
| local local_op | |
| local_op=$(awk '/^\[local\]/,/^\[/' "$conf" \ | |
| | grep -E '^\s*operator_id\s*=' \ | |
| | head -1 | cut -d= -f2 | tr -d ' ') | |
| if [ -z "$local_op" ]; then | |
| _log_err "[local] : operator_id manquant" | |
| errors=$(( errors + 1 )) | |
| else | |
| _log_ok "[local] operator_id = ${local_op}" | |
| fi | |
| # ── Au moins 1 opérateur déclaré ───────────────────────────────────────── | |
| local op_count | |
| op_count=$(awk '/^\[operators\]/,/^\[/' "$conf" \ | |
| | grep -cE '^\s*[0-9]+\s*=' || echo 0) | |
| if [ "$op_count" -eq 0 ]; then | |
| _log_err "[operators] : aucun opérateur déclaré" | |
| errors=$(( errors + 1 )) | |
| else | |
| _log_ok "[operators] : ${op_count} opérateur(s)" | |
| fi | |
| # ── Routes non vides ────────────────────────────────────────────────────── | |
| local route_count | |
| route_count=$(awk '/^\[routes\]/,/^\[/' "$conf" \ | |
| | grep -cE '^\s*[0-9]+\s*=' || echo 0) | |
| if [ "$route_count" -eq 0 ]; then | |
| _log_err "[routes] : aucune route définie" | |
| errors=$(( errors + 1 )) | |
| else | |
| _log_ok "[routes] : ${route_count} entrée(s)" | |
| fi | |
| # ── Collision de préfixes : même MSISDN → deux opérateurs différents ───── | |
| local collision_count=0 | |
| while IFS= read -r line; do | |
| [[ "$line" =~ ^[[:space:]]*# ]] && continue # commentaire | |
| [[ "$line" =~ ^[[:space:]]*$ ]] && continue # vide | |
| [[ "$line" =~ ^\[ ]] && continue # section | |
| local prefix op | |
| prefix=$(echo "$line" | cut -d= -f1 | tr -d ' ') | |
| op=$(echo "$line" | cut -d= -f2 | cut -d'#' -f1 | tr -d ' ') | |
| # Chercher si ce même préfixe apparaît avec un opérateur différent | |
| local other_op | |
| other_op=$(awk -v p="$prefix" -v o="$op" ' | |
| /^\[routes\]/,/^\[/ { | |
| if ($0 ~ "^[[:space:]]*" p "[[:space:]]*=") { | |
| split($0, a, "=") | |
| gsub(/[[:space:]]/, "", a[2]) | |
| sub(/#.*/, "", a[2]) | |
| if (a[2] != o) { print a[2]; exit } | |
| } | |
| }' "$conf") | |
| if [ -n "$other_op" ]; then | |
| _log_err "Collision : préfixe ${prefix} → op${op} ET op${other_op}" | |
| collision_count=$(( collision_count + 1 )) | |
| errors=$(( errors + 1 )) | |
| fi | |
| done < <(awk '/^\[routes\]/,/^\[/' "$conf") | |
| [ "$collision_count" -eq 0 ] && _log_ok "Pas de collision de préfixes" | |
| # ── Routes MSISDN locaux → opérateur local ──────────────────────────────── | |
| if [ -n "$local_op" ]; then | |
| local wrong_local=0 | |
| while IFS= read -r line; do | |
| [[ "$line" =~ ^[[:space:]]*# ]] && continue | |
| [[ "$line" =~ ^[[:space:]]*$ ]] && continue | |
| [[ "$line" =~ ^\[ ]] && continue | |
| local prefix op | |
| prefix=$(echo "$line" | cut -d= -f1 | tr -d ' ') | |
| op=$(echo "$line" | cut -d= -f2 | cut -d'#' -f1 | tr -d ' ') | |
| # Un MSISDN commençant par op_id devrait router vers op_id | |
| if [[ "$prefix" =~ ^${local_op}[0-9]* ]] && [ "$op" != "$local_op" ]; then | |
| _log_warn "MSISDN local ${prefix} routé vers op${op} ≠ op${local_op}" | |
| wrong_local=$(( wrong_local + 1 )) | |
| fi | |
| done < <(awk '/^\[routes\]/,/^\[/' "$conf") | |
| [ "$wrong_local" -eq 0 ] && _log_ok "MSISDN locaux correctement routés" | |
| fi | |
| # ── Résultat ────────────────────────────────────────────────────────────── | |
| echo "" | |
| if [ "$errors" -eq 0 ]; then | |
| echo -e " ${GREEN}${BOLD}✓ Validation OK${NC}" | |
| return 0 | |
| else | |
| echo -e " ${RED}${BOLD}✗ ${errors} erreur(s)${NC}" | |
| return 1 | |
| fi | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_summary | |
| # | |
| # Affiche la table de routage complète en format lisible (debug / audit). | |
| # Montre tous les MSISDN enregistrés et leur opérateur cible. | |
| # | |
| # Usage : | |
| # sms_routing_summary <n_ops> [ms_counts...] | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_summary() { | |
| local n_ops="$1" | |
| shift | |
| local ms_counts=("$@") | |
| local mcc="${MCC:-001}" | |
| echo -e "${CYAN}${BOLD}" | |
| echo "╔══════════════════════════════════════════════════════════════════╗" | |
| echo "║ SMS Routing — Table complète ║" | |
| printf "║ %d opérateur(s) ║\n" "$n_ops" | |
| echo "╚══════════════════════════════════════════════════════════════════╝" | |
| echo -e "${NC}" | |
| # En-tête tableau | |
| printf "${BOLD}%-12s %-8s %-20s %-18s %-15s${NC}\n" \ | |
| "MSISDN" "Op→" "IMSI" "Container IP" "SC-address" | |
| printf "%-12s %-8s %-20s %-18s %-15s\n" \ | |
| "────────────" "────────" "────────────────────" "──────────────────" "───────────────" | |
| local total_ms=0 | |
| for i in $(seq 1 "$n_ops"); do | |
| local n_ms=${ms_counts[$((i-1))]:-1} | |
| local mnc; mnc=$(printf '%02d' "$i") | |
| local container_ip; container_ip=$(_sms_op_backbone_ip "$i") | |
| local sc; sc=$(_sms_sc_address "$i") | |
| for ms in $(seq 1 "$n_ms"); do | |
| local msisdn; msisdn=$(_sms_op_msisdn "$i" "$ms") | |
| local imsi; imsi=$(_sms_op_ms_imsi "$mcc" "$mnc" "$i" "$ms") | |
| printf "%-12s ${CYAN}Op%-6s${NC} %-20s %-18s %-15s\n" \ | |
| "$msisdn" "$i" "$imsi" "$container_ip" "$sc" | |
| total_ms=$(( total_ms + 1 )) | |
| done | |
| # Ligne de séparation entre opérateurs | |
| [ "$i" -lt "$n_ops" ] && \ | |
| printf "${YELLOW}%-12s %-8s %-20s %-18s %-15s${NC}\n" \ | |
| "──────────" "↓ Op$((i+1))" "" "" "" | |
| done | |
| echo "" | |
| printf "${BOLD}Total : %d MS | %d opérateur(s)${NC}\n" "$total_ms" "$n_ops" | |
| echo "" | |
| echo -e "${CYAN}── Réseau inter-opérateurs ──${NC}" | |
| for i in $(seq 1 "$n_ops"); do | |
| local n_ms=${ms_counts[$((i-1))]:-1} | |
| printf " Op%-2s %s ← TCP 7890 (relay) %d MS\n" \ | |
| "$i" "$(_sms_op_backbone_ip "$i")" "$n_ms" | |
| done | |
| echo "" | |
| echo -e "${CYAN}── Exemple de flux SMS inter-op ──${NC}" | |
| if [ "$n_ops" -ge 2 ]; then | |
| local src_msisdn; src_msisdn=$(_sms_op_msisdn 1 1) | |
| local dst_msisdn; dst_msisdn=$(_sms_op_msisdn 2 1) | |
| printf " %s (Op1) ──GSUP──► HLR(Op1) ──► proto-smsc-daemon\n" "$src_msisdn" | |
| printf " ──► sms-interop-relay(Op1) [lookup %s → Op2]\n" "$dst_msisdn" | |
| printf " ──TCP──► sms-interop-relay(Op2) @ %s:7890\n" "$(_sms_op_backbone_ip 2)" | |
| printf " ──► HLR(Op2) VTY → IMSI lookup\n" | |
| printf " ──► proto-smsc-sendmt(Op2) → MS\n" | |
| fi | |
| echo "" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_mount_args | |
| # | |
| # Retourne les arguments -v Docker pour monter la config SMS du bon opérateur. | |
| # Utilisé dans start_operator() de start.sh. | |
| # | |
| # Usage : | |
| # vol_args+=$(sms_routing_mount_args <op_id> <destdir>) | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_mount_args() { | |
| local op_id="$1" | |
| local destdir="$2" | |
| local conf="${destdir}/sms-routing-op${op_id}.conf" | |
| if [ -f "$conf" ]; then | |
| echo "-v ${conf}:/etc/osmocom/sms-routing.conf" | |
| else | |
| echo "" # pas de volume si le fichier n'existe pas encore | |
| fi | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_wait_ready | |
| # | |
| # Attend que le relay SMS d'un opérateur soit prêt (port TCP 7890 en écoute). | |
| # Appelé optionnellement après start_operator() pour s'assurer que le relay | |
| # est disponible avant d'injecter des SMS de test. | |
| # | |
| # Usage : | |
| # sms_routing_wait_ready <container_name> [timeout_sec] | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_wait_ready() { | |
| local container="$1" | |
| local timeout="${2:-60}" | |
| local elapsed=0 | |
| echo -ne " Attente relay SMS (${container}) " | |
| while [ "$elapsed" -lt "$timeout" ]; do | |
| if docker exec "$container" \ | |
| bash -c "ss -tlnp | grep -q ':7890'" 2>/dev/null; then | |
| echo -e " ${GREEN}✓${NC}" | |
| return 0 | |
| fi | |
| echo -n "." | |
| sleep 2 | |
| elapsed=$(( elapsed + 2 )) | |
| done | |
| echo -e " ${YELLOW}timeout${NC}" | |
| return 1 | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms_routing_test_send | |
| # | |
| # Envoie un SMS de test entre deux MS de deux opérateurs différents. | |
| # Nécessite que les containers soient démarrés et les HLR alimentés. | |
| # | |
| # Usage : | |
| # sms_routing_test_send <src_container> <src_imsi> <dst_msisdn> <message> | |
| # | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| sms_routing_test_send() { | |
| local src_container="$1" | |
| local src_imsi="$2" | |
| local dst_msisdn="$3" | |
| local message="$4" | |
| echo -e "${CYAN}Test SMS :${NC} ${src_container} IMSI=${src_imsi} → MSISDN=${dst_msisdn}" | |
| if ! docker ps --format '{{.Names}}' | grep -q "^${src_container}$"; then | |
| _log_err "Container ${src_container} non démarré" | |
| return 1 | |
| fi | |
| docker exec "$src_container" bash -c " | |
| OPERATOR_ID=\${OPERATOR_ID:-1} | |
| SC_ADDRESS=\"\$(_sms_sc_address \$OPERATOR_ID)\" 2>/dev/null \ | |
| || SC_ADDRESS=\"1999001\${OPERATOR_ID}444\" | |
| sms-encode-text '${message}' \ | |
| | gen-sms-deliver-pdu \"\$SC_ADDRESS\" \ | |
| | proto-smsc-sendmt \"\$SC_ADDRESS\" '${src_imsi}' /tmp/sendmt_socket | |
| " && _log_ok "SMS envoyé" || _log_err "Échec envoi SMS" | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # Point d'entrée CLI (appel direct en ligne de commande) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| _usage() { | |
| cat << 'EOF' | |
| Usage: sms-routing-setup.sh <commande> [args...] | |
| Commandes : | |
| generate <op_id> <n_ops> <destdir> [ms_counts...] | |
| Génère sms-routing-op<op_id>.conf dans <destdir>. | |
| ms_counts : nombre de MS par opérateur (ex: 2 3 1 pour 3 ops). | |
| generate-all <n_ops> <destdir> [ms_counts...] | |
| Génère les configs pour TOUS les opérateurs. | |
| validate <conf_file> | |
| Valide un fichier sms-routing.conf et affiche les erreurs. | |
| summary <n_ops> [ms_counts...] | |
| Affiche la table de routage complète (audit / debug). | |
| wait-ready <container_name> [timeout_sec] | |
| Attend que le relay SMS soit actif dans un container. | |
| test-send <container> <src_imsi> <dst_msisdn> <message> | |
| Envoie un SMS de test via proto-smsc-sendmt. | |
| Variables d'environnement : | |
| MCC : Mobile Country Code (défaut: 001) | |
| Exemples : | |
| bash sms-routing-setup.sh generate-all 3 /tmp/sms-cfg 2 2 3 | |
| bash sms-routing-setup.sh validate /tmp/sms-cfg/sms-routing-op1.conf | |
| bash sms-routing-setup.sh summary 2 2 3 | |
| EOF | |
| } | |
| if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then | |
| # Exécuté directement (pas sourcé) | |
| CMD="${1:-help}"; shift 2>/dev/null || true | |
| case "$CMD" in | |
| generate) sms_routing_generate "$@" ;; | |
| generate-all) sms_routing_generate_all "$@" ;; | |
| validate) sms_routing_validate "$@" ;; | |
| summary) sms_routing_summary "$@" ;; | |
| wait-ready) sms_routing_wait_ready "$@" ;; | |
| test-send) sms_routing_test_send "$@" ;; | |
| help|-h|--help) _usage ;; | |
| *) | |
| echo -e "${RED}Commande inconnue : ${CMD}${NC}" >&2 | |
| _usage; exit 1 ;; | |
| esac | |
| fi | |
| ! | |
| mncc | |
| socket-path /tmp/msc_mncc | |
| sip | |
| local 127.0.0.1 5061 | |
| remote __INTER_LOCAL_IP__ 5060 | |
| ! | |
| ! osmo-bsc.cfg — BSC opérateur __OPERATOR_ID__ (MCC __MCC__ / MNC __MNC__) | |
| ! | |
| ! Pour opérateur 1: utiliser __CONTAINER_IP__ = 172.20.0.11 | |
| ! Pour opérateur 2: utiliser __CONTAINER_IP__ = 172.20.0.12 | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging print thread-id 0 | |
| logging timestamp 0 | |
| logging print level 1 | |
| logging print file basename last | |
| ! | |
| log gsmtap __GATEWAY_IP__ | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging print level 1 | |
| logging level rll notice | |
| logging level mm notice | |
| logging level rr notice | |
| logging level rsl notice | |
| logging level nm notice | |
| logging level pag notice | |
| logging level meas notice | |
| logging level msc notice | |
| logging level ho notice | |
| logging level hodec notice | |
| logging level ref notice | |
| logging level ctrl notice | |
| logging level filter notice | |
| logging level pcu notice | |
| logging level lcls notice | |
| logging level chan notice | |
| logging level ts notice | |
| logging level as notice | |
| logging level cbs notice | |
| logging level lcs notice | |
| logging level asci notice | |
| logging level reset notice | |
| logging level loop notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 notice | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua notice | |
| logging level lmgcp notice | |
| logging level ljibuf notice | |
| logging level lrspro notice | |
| logging level lns notice | |
| logging level lbssgp notice | |
| logging level lnsdata notice | |
| logging level lnssignal notice | |
| logging level liuup notice | |
| logging level lpfcp notice | |
| logging level lcsn1 notice | |
| logging level lio notice | |
| logging level ltcap notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| e1_input | |
| e1_line 0 driver ipa | |
| e1_line 0 port 0 | |
| ! | |
| cs7 instance 0 | |
| network-indicator international | |
| point-code __PC_BSC__ | |
| asp asp-to-stp 2905 2907 m3ua | |
| remote-ip 127.0.0.1 | |
| local-ip 127.0.0.1 | |
| role asp | |
| sctp-role client | |
| no shutdown | |
| as as-bsc m3ua | |
| asp asp-to-stp | |
| routing-key __RCTX_BSC__ __PC_BSC__ | |
| traffic-mode override | |
| sccp-address addr-msc | |
| routing-indicator PC | |
| point-code __PC_MSC__ | |
| subsystem-number 254 | |
| network | |
| network country code __MCC__ | |
| mobile network code __MNC__ | |
| encryption a5 1 | |
| neci 1 | |
| paging any use tch 0 | |
| handover 0 | |
| handover algorithm 1 | |
| handover1 window rxlev averaging 10 | |
| handover1 window rxqual averaging 1 | |
| handover1 window rxlev neighbor averaging 10 | |
| handover1 power budget interval 6 | |
| handover1 power budget hysteresis 3 | |
| handover1 maximum distance 9999 | |
| mgw 0 | |
| local-port 2727 | |
| remote-ip 127.0.0.1 | |
| remote-port 2427 | |
| bts 0 | |
| type osmo-bts | |
| band DCS1800 | |
| cell_identity __CELL_ID__ | |
| location_area_code 0x000__OPERATOR_ID__ | |
| base_station_id_code __BSIC__ | |
| ms max power 15 | |
| cell reselection hysteresis 4 | |
| rxlev access min 0 | |
| radio-link-timeout 32 | |
| channel allocator mode chan-req ascending | |
| channel allocator mode assignment ascending | |
| channel allocator mode handover ascending | |
| channel allocator mode vgcs-vbs ascending | |
| rach tx integer 9 | |
| rach max transmission 7 | |
| rach max-delay 63 | |
| rach expiry-timeout 32 | |
| channel-description attach 1 | |
| channel-description bs-pa-mfrms 5 | |
| channel-description bs-ag-blks-res 1 | |
| no nch-position | |
| no access-control-class-ramping | |
| early-classmark-sending forbidden | |
| early-classmark-sending-3g allowed | |
| ipa unit-id __IPA_UNIT_ID__ 0 | |
| oml ipa stream-id 255 line 0 | |
| neighbor-list mode automatic | |
| codec-support fr | |
| amr tch-f modes 0 2 5 7 | |
| amr tch-f threshold ms 13 25 37 | |
| amr tch-f hysteresis ms 4 4 4 | |
| amr tch-f threshold bts 13 25 37 | |
| amr tch-f hysteresis bts 4 4 4 | |
| amr tch-f start-mode auto | |
| amr tch-h modes 0 2 3 5 | |
| amr tch-h threshold ms 16 24 32 | |
| amr tch-h hysteresis ms 4 4 4 | |
| amr tch-h threshold bts 16 24 32 | |
| amr tch-h hysteresis bts 4 4 4 | |
| amr tch-h start-mode auto | |
| gprs mode egprs | |
| gprs routing area 0 | |
| gprs network-control-order nc0 | |
| gprs power-control alpha 0 | |
| gprs cell bvci __BVCI__ | |
| gprs cell timer blocking-timer 3 | |
| gprs cell timer blocking-retries 3 | |
| gprs cell timer unblocking-retries 3 | |
| gprs cell timer reset-timer 3 | |
| gprs cell timer reset-retries 3 | |
| gprs cell timer suspend-timer 10 | |
| gprs cell timer suspend-retries 3 | |
| gprs cell timer resume-timer 10 | |
| gprs cell timer resume-retries 3 | |
| gprs cell timer capability-update-timer 10 | |
| gprs cell timer capability-update-retries 3 | |
| gprs nsei __NSEI__ | |
| gprs ns timer tns-block 3 | |
| gprs ns timer tns-block-retries 3 | |
| gprs ns timer tns-reset 3 | |
| gprs ns timer tns-reset-retries 3 | |
| gprs ns timer tns-test 30 | |
| gprs ns timer tns-alive 3 | |
| gprs ns timer tns-alive-retries 10 | |
| gprs nsvc 0 nsvci __NSVCI__ | |
| gprs nsvc 0 local udp port 23001 | |
| gprs nsvc 0 remote ip __CONTAINER_IP__ | |
| gprs nsvc 0 remote udp port 23000 | |
| gprs nsvc 1 nsvci 0 | |
| gprs nsvc 1 local udp port 0 | |
| gprs nsvc 1 remote ip 0.0.0.0 | |
| gprs nsvc 1 remote udp port 0 | |
| bs-power-control | |
| mode static | |
| ms-power-control | |
| mode dyn-bts | |
| ctrl-interval 2 | |
| step-size inc 4 red 2 | |
| rxlev-thresh lower 32 upper 38 | |
| rxlev-thresh-comp lower 10 12 upper 19 20 | |
| rxqual-thresh lower 3 upper 0 | |
| rxqual-thresh-comp lower 5 7 upper 15 18 | |
| ci-thresh fr-efr disable | |
| ci-thresh fr-efr lower 13 upper 17 | |
| ci-thresh-comp fr-efr lower 5 7 upper 15 18 | |
| ci-thresh hr disable | |
| ci-thresh hr lower 16 upper 21 | |
| ci-thresh-comp hr lower 5 7 upper 15 18 | |
| ci-thresh amr-fr disable | |
| ci-thresh amr-fr lower 7 upper 11 | |
| ci-thresh-comp amr-fr lower 5 7 upper 15 18 | |
| ci-thresh amr-hr disable | |
| ci-thresh amr-hr lower 13 upper 17 | |
| ci-thresh-comp amr-hr lower 5 7 upper 15 18 | |
| ci-thresh sdcch disable | |
| ci-thresh sdcch lower 12 upper 16 | |
| ci-thresh-comp sdcch lower 5 7 upper 15 18 | |
| ci-thresh gprs disable | |
| ci-thresh gprs lower 18 upper 24 | |
| ci-thresh-comp gprs lower 5 7 upper 15 18 | |
| trx 0 | |
| rf_locked 0 | |
| arfcn __ARFCN__ | |
| nominal power 23 | |
| max_power_red 0 | |
| rsl e1 tei 0 | |
| timeslot 0 | |
| phys_chan_config CCCH+SDCCH4 | |
| hopping enabled 0 | |
| timeslot 1 | |
| phys_chan_config SDCCH8 | |
| hopping enabled 0 | |
| timeslot 2 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 3 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 4 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 5 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 6 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| timeslot 7 | |
| phys_chan_config DYNAMIC/OSMOCOM | |
| hopping enabled 0 | |
| msc 0 | |
| codec-list fr1 hr1 fr2 fr3 hr3 | |
| allow-emergency deny | |
| amr-config 12_2k allowed | |
| amr-config 10_2k allowed | |
| amr-config 7_95k allowed | |
| amr-config 7_40k allowed | |
| amr-config 6_70k allowed | |
| amr-config 5_90k allowed | |
| amr-config 5_15k allowed | |
| amr-config 4_75k allowed | |
| amr-payload octet-aligned | |
| msc-addr addr-msc | |
| asp-protocol m3ua | |
| lcls-mode bts-loop | |
| lcls-codec-mismatch allowed | |
| bsc | |
| mid-call-timeout 0 | |
| end | |
| pfcp | |
| local-addr __CONTAINER_IP__ | |
| nft | |
| # netfilter requires no specific configuration | |
| gtp | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category 1 | |
| logging timestamp 1 | |
| logging print file basename | |
| logging level set-all info | |
| ! | |
| line vty | |
| no login | |
| ! | |
| trx | |
| bind-ip 127.0.0.1 | |
| remote-ip 127.0.0.1 | |
| base-port 5700 | |
| egprs enable | |
| tx-sps 4 | |
| rx-sps 4 | |
| clock-ref internal | |
| chan 0 | |
| ! | |
| ! OsmoHLR configuration — Opérateur __OPERATOR_ID__ | |
| ! SMS-over-GSUP activé via gsup-smsc-proto | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging print extended-timestamp 1 | |
| logging print level 1 | |
| logging print file basename | |
| logging level main debug | |
| logging level db debug | |
| logging level auc debug | |
| logging level ss debug | |
| logging level lglobal debug | |
| logging level llapd debug | |
| logging level linp debug | |
| logging level lmux debug | |
| logging level lmi debug | |
| logging level lmib debug | |
| logging level lsms debug | |
| logging level lctrl debug | |
| logging level lgtp debug | |
| logging level lstats debug | |
| logging level lgsup debug | |
| logging level loap debug | |
| logging level lss7 debug | |
| logging level lsccp debug | |
| logging level lsua debug | |
| logging level lm3ua debug | |
| logging level lmgcp debug | |
| logging level ljibuf debug | |
| logging level lrspro debug | |
| logging filter all 0 | |
| logging color 1 | |
| logging print category-hex 1 | |
| logging print category 0 | |
| logging timestamp 0 | |
| logging print file 1 | |
| logging level main notice | |
| logging level db notice | |
| logging level auc notice | |
| logging level ss notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 notice | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua notice | |
| logging level lmgcp notice | |
| logging level ljibuf notice | |
| logging level lrspro notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| hlr | |
| subscriber-create-on-demand 5 cs+ps | |
| store-imei | |
| database /var/lib/osmocom/hlr.db | |
| gsup | |
| bind ip 127.0.0.2 | |
| ipa-name HLR-OP__OPERATOR_ID__ | |
| ussd route prefix *#100# internal own-msisdn | |
| ussd route prefix *#101# internal own-imsi | |
| ! | |
| ! ── SMS-over-GSUP : proto-SMSC connecté via GSUP ────────────────────────── | |
| ! Le proto-smsc-daemon se connecte au HLR avec l'IPA name SMSC-OP<N> | |
| ! SC-address = numéro fictif unique par opérateur (réservé NANP) | |
| ! | |
| smsc entity SMSC-OP__OPERATOR_ID__ | |
| smsc default-route SMSC-OP__OPERATOR_ID__ | |
| ! | |
| ! OsmoBTS (192.168.1.15-bc49-dirty) configuration saved from vty | |
| !! | |
| ! | |
| log stderr | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging timestamp 0 | |
| logging print file basename last | |
| logging print level 1 | |
| ! | |
| log gsmtap __GATEWAY_IP__ | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging print level 1 | |
| logging level rsl info | |
| logging level oml info | |
| logging level rll notice | |
| logging level rr notice | |
| logging level meas notice | |
| logging level pag info | |
| logging level l1c info | |
| logging level l1p info | |
| logging level dsp error | |
| logging level pcu notice | |
| logging level ho debug | |
| logging level trx notice | |
| logging level loop notice | |
| logging level abis debug | |
| logging level rtp notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats error | |
| ! | |
| line vty | |
| no login | |
| ! | |
| e1_input | |
| e1_line 0 driver ipa | |
| e1_line 0 port 0 | |
| no e1_line 0 keepalive | |
| ! | |
| phy 0 | |
| instance 0 | |
| osmotrx rx-gain 40 | |
| osmotrx tx-attenuation 50 | |
| osmotrx ip local 127.0.0.1 | |
| osmotrx ip remote 127.0.0.1 | |
| no osmotrx timing-advance-loop | |
| bts 0 | |
| band DCS1800 | |
| ipa unit-id __IPA_UNIT_ID__ 0 | |
| oml remote-ip 127.0.0.1 | |
| rtp jitter-buffer 100 | |
| paging queue-size 200 | |
| paging lifetime 0 | |
| min-qual-rach 50 | |
| min-qual-norm -5 | |
| gsmtap-sapi ccch | |
| gsmtap-sapi pdtch | |
| trx 0 | |
| power-ramp max-initial 23000 mdBm | |
| power-ramp step-size 2000 mdB | |
| power-ramp step-interval 1 | |
| ms-power-control osmo | |
| phy 0 instance 0 | |
| ! | |
| ! osmo-msc.cfg — MSC opérateur __OPERATOR_ID__ (MCC __MCC__ / MNC __MNC__) | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 1 | |
| logging print category 0 | |
| logging print thread-id 0 | |
| logging timestamp 0 | |
| logging print file 1 | |
| logging level rll notice | |
| logging level cc notice | |
| logging level bcc notice | |
| logging level gcc notice | |
| logging level mm notice | |
| logging level rr notice | |
| logging level mncc notice | |
| logging level pag notice | |
| logging level msc notice | |
| logging level mgcp notice | |
| logging level ho notice | |
| logging level db notice | |
| logging level ref notice | |
| logging level ctrl notice | |
| logging level smpp notice | |
| logging level ranap notice | |
| logging level vlr notice | |
| logging level iucs notice | |
| logging level bssap notice | |
| logging level sgs notice | |
| logging level ss notice | |
| logging level asci notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 info | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua info | |
| logging level lmgcp notice | |
| logging level ljibuf notice | |
| logging level lrspro notice | |
| logging level lns notice | |
| logging level lbssgp notice | |
| logging level lnsdata notice | |
| logging level lnssignal notice | |
| logging level liuup notice | |
| logging level lpfcp notice | |
| logging level lcsn1 notice | |
| logging level lio notice | |
| logging level ltcap notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| cs7 instance 0 | |
| network-indicator international | |
| point-code __PC_MSC__ | |
| ! | |
| ! MSC → STP : même container → 127.0.0.1 | |
| asp asp-to-stp 2905 2906 m3ua | |
| remote-ip 127.0.0.2 | |
| local-ip 127.0.0.1 | |
| role asp | |
| sctp-role client | |
| no shutdown | |
| ! | |
| as as-msc m3ua | |
| asp asp-to-stp | |
| routing-key __RCTX_MSC__ __PC_MSC__ | |
| traffic-mode override | |
| ! | |
| sccp-address addr-bsc | |
| routing-indicator PC | |
| point-code __PC_BSC__ | |
| subsystem-number 254 | |
| ! | |
| network | |
| network country code __MCC__ | |
| mobile network code __MNC__ | |
| short name __OP_NAME__ | |
| long name __OP_NAME__ | |
| encryption a5 1 | |
| authentication optional | |
| rrlp mode none | |
| mm info 0 | |
| mgw 0 | |
| remote-ip 127.0.0.1 | |
| msc | |
| mncc external /tmp/msc_mncc | |
| mncc guard-timeout 180 | |
| ncss guard-timeout 30 | |
| assign-tmsi | |
| lcls-permitted | |
| cs7-instance-a 0 | |
| check-imei-rqd early | |
| ! SMS-over-GSUP : redirige les SM-RP vers le HLR → proto-smsc-daemon | |
| sms-over-gsup | |
| mncc-int | |
| default-codec tch-f fr | |
| default-codec tch-h hr | |
| smpp | |
| local-tcp-port 2775 | |
| policy closed | |
| no smpp-first | |
| esme msc_tester | |
| password osmocom1 | |
| route prefix national isdn 33 | |
| hlr | |
| remote-ip __HLR_IP__ | |
| remote-port 4222 | |
| ipa-name VLR-SDR-OP__OPERATOR_ID__ | |
| sgs | |
| local-port 29118 | |
| local-ip 0.0.0.0 | |
| vlr-name vlr.op__OPERATOR_ID__.net | |
| asci | |
| disable | |
| gcr | |
| ; ============================================================================= | |
| ; extensions.conf — Asterisk dialplan — Opérateur __OPERATOR_ID__ | |
| ; | |
| ; ┌─────────────────────────────────────────────────────────────────────────┐ | |
| ; │ PLAN DE NUMÉROTATION │ | |
| ; │ 100 Linphone A (softphone local) │ | |
| ; │ 200 Linphone B (softphone local) │ | |
| ; │ 600 Echo test │ | |
| ; │ 700 Annonce de bienvenue │ | |
| ; │ NXXXX Abonnés GSM opérateur N (premier chiffre = identifiant op) │ | |
| ; ├─────────────────────────────────────────────────────────────────────────┤ | |
| ; │ CONTEXTES │ | |
| ; │ [internal] Appels depuis softphones locaux │ | |
| ; │ [gsm_in] Appels entrants depuis OsmoMSC (GSM → Asterisk) │ | |
| ; │ [interop_in] Appels entrants depuis un opérateur distant (SIP trunk) │ | |
| ; │ [gsm_out] Sous-contexte : sortie vers MSC local │ | |
| ; │ [interop_out] Routage vers l'opérateur distant approprié │ | |
| ; │ GÉNÉRÉ par start.sh selon le nombre d'opérateurs │ | |
| ; └─────────────────────────────────────────────────────────────────────────┘ | |
| ; | |
| ; SCÉNARIOS COUVERTS | |
| ; ────────────────── | |
| ; 1. MS → MS (même opérateur) : MS → MSC → MNCC → Asterisk → MNCC → MSC → MS | |
| ; 2. MS → Linphone : MS → MSC → Asterisk SIP → Linphone | |
| ; 3. Linphone → MS : Linphone SIP → Asterisk → MNCC → MSC → MS | |
| ; 4. Linphone A → Linphone B : SIP local | |
| ; 5. MS(OpX) → MS(OpY) inter-op : MNCC → AstX → SIP trunk → AstY → MNCC → MS | |
| ; 6. MS → echo / annonce : test audio | |
| ; | |
| ; NOTE : [interop_out] est ajouté en fin de fichier par start.sh. | |
| ; Il contient une extension par opérateur distant, routée vers | |
| ; le trunk interop_trunk_opX correspondant. | |
| ; ============================================================================= | |
| [general] | |
| static=yes | |
| writeprotect=no | |
| clearglobalvars=no | |
| [globals] | |
| MSC_LOCAL=127.0.0.1 | |
| OP_PREFIX=__OPERATOR_ID__ | |
| ; ============================================================================= | |
| ; [internal] — Appels DEPUIS les softphones locaux | |
| ; ============================================================================= | |
| [internal] | |
| ; ── Tests audio ────────────────────────────────────────────────────────────── | |
| exten => 600,1,NoOp(=== ECHO TEST ===) | |
| same => n,Answer() | |
| same => n,Playback(demo-echotest) | |
| same => n,Echo() | |
| same => n,Playback(demo-echodone) | |
| same => n,Hangup() | |
| exten => 700,1,NoOp(=== ANNONCE ===) | |
| same => n,Answer() | |
| same => n,Playback(hello-world) | |
| same => n,Hangup() | |
| ; ── Softphone → Softphone ──────────────────────────────────────────────────── | |
| exten => 100,1,NoOp(=== SIP: → Linphone A ===) | |
| same => n,Dial(PJSIP/linphone_A,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| exten => 200,1,NoOp(=== SIP: → Linphone B ===) | |
| same => n,Dial(PJSIP/linphone_B,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── Softphone → GSM local ──────────────────────────────────────────────────── | |
| ; Numéros opérateur local : __OPERATOR_ID__XXXX | |
| exten => ___OPERATOR_ID__XXXX,1,NoOp(=== SIP -> GSM local (Op__OPERATOR_ID__): ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── Softphone → GSM inter-opérateur ────────────────────────────────────────── | |
| ; Composer 9 + numéro complet (ex: 920001 pour joindre 20001 sur Op2) | |
| exten => _9.,1,NoOp(=== SIP -> INTER-OP: ${EXTEN:1} ===) | |
| same => n,Goto(interop_out,${EXTEN:1},1) | |
| ; ── Catch-all : essayer le MSC local ──────────────────────────────────────── | |
| exten => _X.,1,NoOp(=== SIP -> GSM fallback: ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ============================================================================= | |
| ; [gsm_in] — Appels entrants DEPUIS OsmoMSC (GSM → Asterisk) | |
| ; ============================================================================= | |
| [gsm_in] | |
| ; ── Tests audio ────────────────────────────────────────────────────────────── | |
| exten => 600,1,NoOp(=== GSM -> ECHO TEST ===) | |
| same => n,Answer() | |
| same => n,Playback(demo-echotest) | |
| same => n,Echo() | |
| same => n,Playback(demo-echodone) | |
| same => n,Hangup() | |
| ; ── MS → Linphone ──────────────────────────────────────────────────────────── | |
| exten => 100,1,NoOp(=== GSM -> Linphone A ===) | |
| same => n,Dial(PJSIP/linphone_A,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| exten => 200,1,NoOp(=== GSM -> Linphone B ===) | |
| same => n,Dial(PJSIP/linphone_B,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── MS → MS local (même opérateur) ────────────────────────────────────────── | |
| exten => ___OPERATOR_ID__XXXX,1,NoOp(=== GSM -> GSM local (Op__OPERATOR_ID__): ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── MS → MS inter-opérateur (routage par premier chiffre) ─────────────────── | |
| ; Si premier chiffre = identifiant opérateur local → MSC local | |
| ; Sinon → [interop_out] qui sélectionne le bon trunk SIP | |
| exten => _[0-9]XXXX,1,NoOp(=== GSM -> routage: ${EXTEN} ===) | |
| same => n,GotoIf($["${EXTEN:0:1}" = "__OPERATOR_ID__"]?local_gsm) | |
| same => n,Goto(interop_out,${EXTEN},1) | |
| same => n(local_gsm),NoOp(=== GSM -> GSM local: ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ── Catch-all ──────────────────────────────────────────────────────────────── | |
| exten => _X.,1,NoOp(=== GSM -> numéro inconnu: ${EXTEN} ===) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ============================================================================= | |
| ; [interop_in] — Appels entrants DEPUIS un opérateur distant (SIP trunk) | |
| ; ============================================================================= | |
| [interop_in] | |
| ; MS distant → MS local | |
| exten => ___OPERATOR_ID__XXXX,1,NoOp(=== INTER-OP -> GSM local: ${EXTEN} ===) | |
| same => n,Dial(PJSIP/${EXTEN}@gsm_msc,,rT) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; Opérateur distant → Linphone local | |
| exten => 100,1,NoOp(=== INTER-OP -> Linphone A ===) | |
| same => n,Dial(PJSIP/linphone_A,,rT) | |
| same => n,Hangup() | |
| exten => 200,1,NoOp(=== INTER-OP -> Linphone B ===) | |
| same => n,Dial(PJSIP/linphone_B,,rT) | |
| same => n,Hangup() | |
| ; Refus propre | |
| exten => _X.,1,NoOp(=== INTER-OP -> numéro inconnu: ${EXTEN} ===) | |
| same => n,Congestion() | |
| same => n,Hangup() | |
| ; ============================================================================= | |
| ; [interop_out] — Routage sortant vers les opérateurs distants | |
| ; | |
| ; Ce contexte est GÉNÉRÉ et AJOUTÉ en fin de fichier par start.sh. | |
| ; Il contient une extension _NXXXX par opérateur distant (N ≠ __OPERATOR_ID__) | |
| ; qui route vers interop_trunk_opN via PJSIP. | |
| ; | |
| ; Exemple pour Op1 avec 3 opérateurs : | |
| ; exten => _2XXXX → interop_trunk_op2 | |
| ; exten => _3XXXX → interop_trunk_op3 | |
| ; exten => _X. → Congestion | |
| ; ============================================================================= | |
| ! | |
| ! OsmoMGW (1.7.0) configuration saved from vty | |
| !! | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 0 | |
| logging print category-hex 1 | |
| logging print category 0 | |
| logging timestamp 0 | |
| logging print file 1 | |
| logging level rtp notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 notice | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua notice | |
| logging level lmgcp notice | |
| logging level ljibuf notice | |
| logging level lrspro notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| ! | |
| mgcp | |
| domain mgw | |
| bind ip 127.0.0.1 | |
| bind port 2427 | |
| rtp port-range 4002 16001 | |
| rtp bind-ip 127.0.0.1 | |
| rtp ip-probing | |
| rtp keep-alive once | |
| rtcp-omit | |
| rtp-patch ssrc | |
| rtp-patch timestamp | |
| no rtp-patch rfc5993hr | |
| sdp audio-payload number 98 | |
| sdp audio-payload name GSM | |
| sdp audio-payload send-ptime | |
| sdp audio-payload send-name | |
| loop 0 | |
| number endpoints 31 | |
| allow-transcoding | |
| osmux off | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # sms-routing.conf — Table de routage inter-opérateur SMS | |
| # | |
| # Ce fichier est templaté par start.sh via apply_config_templates() | |
| # Les placeholders __XXX__ sont remplacés au démarrage du container. | |
| # | |
| # Pour ajouter un opérateur : | |
| # 1. Ajouter son IP dans [operators] | |
| # 2. Ajouter les préfixes MSISDN dans [routes] | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| [local] | |
| # ID de cet opérateur (remplacé dynamiquement) | |
| operator_id = __OPERATOR_ID__ | |
| [operators] | |
| # operator_id = container_ip | |
| # Ces IPs correspondent au réseau bridge Docker (172.20.0.0/24) | |
| 1 = 172.20.0.11 | |
| 2 = 172.20.0.12 | |
| # 3 = 172.20.0.13 | |
| [routes] | |
| # prefix = operator_id | |
| # Longest-prefix match : le préfixe le plus long gagne | |
| # | |
| # ── Préfixes MSISDN par opérateur ────────────────────────────────────────── | |
| # Adapter selon ton plan de numérotation | |
| # | |
| # Opérateur 1 : extensions 1xxx et MSISDN +33601xxxxxx | |
| 10000 = 1 | |
| 1001 = 1 | |
| 1002 = 1 | |
| 1003 = 1 | |
| 1004 = 1 | |
| 1005 = 1 | |
| 33601 = 1 | |
| # | |
| # Opérateur 2 : extensions 2xxx et MSISDN +33602xxxxxx | |
| 20000 = 2 | |
| 2001 = 2 | |
| 2002 = 2 | |
| 2003 = 2 | |
| 2004 = 2 | |
| 2005 = 2 | |
| 33602 = 2 | |
| # | |
| # ── Route par défaut (optionnel) ─────────────────────────────────────────── | |
| # Si aucun préfixe ne matche, le SMS n'est pas routé. | |
| # Décommenter pour un fallback : | |
| # * = 1 | |
| ; ============================================================================= | |
| ; pjsip.conf — Asterisk PJSIP — Opérateur __OPERATOR_ID__ | |
| ; IP container : __CONTAINER_IP__ | |
| ; IP inter-net : __INTER_LOCAL_IP__ (réseau gsm-inter, pour trunks inter-op) | |
| ; | |
| ; Ce fichier contient les éléments statiques (transport, softphones, MSC). | |
| ; Les trunks inter-opérateurs [interop-identify-opX] / [interop_trunk_opX] | |
| ; sont GÉNÉRÉS et AJOUTÉS en fin de fichier par start.sh selon N opérateurs. | |
| ; | |
| ; Plan de numérotation : | |
| ; 100 Linphone A (softphone local) | |
| ; 200 Linphone B (softphone local) | |
| ; 600 Echo test | |
| ; NXXXX Abonnés GSM opérateur N (ex: 10001 pour Op1, 20001 pour Op2…) | |
| ; | |
| ; Flux d'appels : | |
| ; MS → Asterisk via MNCC socket (/tmp/msc_mncc) → OsmoMSC → context gsm_in | |
| ; Linphone → Asterisk SIP → context internal | |
| ; Inter-op → trunk SIP sur gsm-inter → context interop_in | |
| ; ============================================================================= | |
| [global] | |
| type=global | |
| user_agent=OsmoAsterisk-Op__OPERATOR_ID__ | |
| ; ============================================================================= | |
| ; TRANSPORT | |
| ; ============================================================================= | |
| [transport-udp] | |
| type=transport | |
| protocol=udp | |
| bind=0.0.0.0:5060 | |
| ; ============================================================================= | |
| ; SOFTPHONE LOCAL A (Linphone / Baresip / autre) | |
| ; SIP account : linphone_A@__CONTAINER_IP__:5060 | Mot de passe : tester | |
| ; Numéro : 100 | |
| ; ============================================================================= | |
| [linphone_A] | |
| type=endpoint | |
| transport=transport-udp | |
| context=internal | |
| disallow=all | |
| allow=ulaw | |
| allow=gsm | |
| auth=linphone_A | |
| aors=linphone_A | |
| direct_media=no | |
| rtp_symmetric=yes | |
| force_rport=yes | |
| rewrite_contact=yes | |
| callerid=Linphone A <100> | |
| [linphone_A] | |
| type=auth | |
| auth_type=userpass | |
| username=linphone_A | |
| password=tester | |
| [linphone_A] | |
| type=aor | |
| max_contacts=1 | |
| remove_existing=yes | |
| qualify_frequency=30 | |
| ; ============================================================================= | |
| ; SOFTPHONE LOCAL B | |
| ; SIP account : linphone_B@__CONTAINER_IP__:5060 | Mot de passe : testerB | |
| ; Numéro : 200 | |
| ; ============================================================================= | |
| [linphone_B] | |
| type=endpoint | |
| transport=transport-udp | |
| context=internal | |
| disallow=all | |
| allow=ulaw | |
| allow=gsm | |
| auth=linphone_B | |
| aors=linphone_B | |
| direct_media=no | |
| rtp_symmetric=yes | |
| force_rport=yes | |
| rewrite_contact=yes | |
| callerid=Linphone B <200> | |
| [linphone_B] | |
| type=auth | |
| auth_type=userpass | |
| username=linphone_B | |
| password=testerB | |
| [linphone_B] | |
| type=aor | |
| max_contacts=1 | |
| remove_existing=yes | |
| qualify_frequency=30 | |
| ; ============================================================================= | |
| ; TRUNK GSM ← OsmoMSC (connexion MNCC/SIP depuis osmo-sip-connector) | |
| ; Accepte les appels entrants de 127.0.0.1:5061 | |
| ; Contexte d'arrivée : gsm_in | |
| ; ============================================================================= | |
| [gsm_msc-identify] | |
| type=identify | |
| endpoint=gsm_msc | |
| match=127.0.0.1 | |
| [gsm_msc] | |
| type=endpoint | |
| transport=transport-udp | |
| context=gsm_in | |
| disallow=all | |
| allow=gsm | |
| allow=ulaw | |
| aors=gsm_msc | |
| direct_media=no | |
| rtp_symmetric=yes | |
| force_rport=yes | |
| rewrite_contact=yes | |
| [gsm_msc] | |
| type=aor | |
| contact=sip:127.0.0.1:5061 | |
| qualify_frequency=10 | |
| ; ============================================================================= | |
| ; TRUNKS INTER-OPÉRATEURS — section générée automatiquement par start.sh | |
| ; | |
| ; Pour chaque opérateur distant (ID = X ≠ __OPERATOR_ID__) : | |
| ; [interop-identify-opX] → identification par IP source (172.20.0.(10+X)) | |
| ; [interop_trunk_opX] → endpoint PJSIP (context=interop_in) | |
| ; [interop_trunk_opX] → AOR (contact=sip:172.20.0.(10+X):5060) | |
| ; | |
| ; Les blocs ci-dessous sont ajoutés en append par start.sh : | |
| ; ============================================================================= | |
| [modules] | |
| autoload = no | |
| ; This is a minimal module load. We are loading only the modules | |
| ; required for the Asterisk features used in the "Super Awesome | |
| ; Company" configuration. | |
| ; Applications | |
| load = app_bridgewait.so | |
| load = app_dial.so | |
| load = app_playback.so | |
| load = app_stack.so | |
| load = app_verbose.so | |
| load = app_voicemail.so | |
| load = app_directory.so | |
| load = app_confbridge.so | |
| load = app_queue.so | |
| ; Bridging | |
| load = bridge_builtin_features.so | |
| load = bridge_builtin_interval_features.so | |
| load = bridge_holding.so | |
| load = bridge_native_rtp.so | |
| load = bridge_simple.so | |
| load = bridge_softmix.so | |
| ; Call Detail Records | |
| load = cdr_custom.so | |
| ; Channel Drivers | |
| load = chan_bridge_media.so | |
| load = chan_pjsip.so | |
| ; Codecs | |
| load = codec_gsm.so | |
| load = codec_resample.so | |
| load = codec_ulaw.so | |
| load = codec_g722.so | |
| ; Formats | |
| load = format_gsm.so | |
| load = format_pcm.so | |
| load = format_wav_gsm.so | |
| load = format_wav.so | |
| ; Functions | |
| load = func_callerid.so | |
| load = func_cdr.so | |
| load = func_pjsip_endpoint.so | |
| load = func_sorcery.so | |
| load = func_devstate.so | |
| load = func_strings.so | |
| ; Core/PBX | |
| load = pbx_config.so | |
| ; Resources | |
| load = res_http_websocket.so | |
| load = res_musiconhold.so | |
| load = res_pjproject.so | |
| load = res_pjsip_acl.so | |
| load = res_pjsip_authenticator_digest.so | |
| load = res_pjsip_caller_id.so | |
| load = res_pjsip_dialog_info_body_generator.so | |
| load = res_pjsip_diversion.so | |
| load = res_pjsip_dtmf_info.so | |
| load = res_pjsip_endpoint_identifier_anonymous.so | |
| load = res_pjsip_endpoint_identifier_ip.so | |
| load = res_pjsip_endpoint_identifier_user.so | |
| load = res_pjsip_exten_state.so | |
| load = res_pjsip_header_funcs.so | |
| load = res_pjsip_logger.so | |
| load = res_pjsip_messaging.so | |
| load = res_pjsip_mwi_body_generator.so | |
| load = res_pjsip_mwi.so | |
| load = res_pjsip_nat.so | |
| load = res_pjsip_notify.so | |
| load = res_pjsip_one_touch_record_info.so | |
| load = res_pjsip_outbound_authenticator_digest.so | |
| load = res_pjsip_outbound_publish.so | |
| load = res_pjsip_outbound_registration.so | |
| load = res_pjsip_path.so | |
| load = res_pjsip_pidf_body_generator.so | |
| load = res_pjsip_pidf_digium_body_supplement.so | |
| load = res_pjsip_pidf_eyebeam_body_supplement.so | |
| load = res_pjsip_publish_asterisk.so | |
| load = res_pjsip_pubsub.so | |
| load = res_pjsip_refer.so | |
| load = res_pjsip_registrar.so | |
| load = res_pjsip_rfc3326.so | |
| load = res_pjsip_sdp_rtp.so | |
| load = res_pjsip_send_to_voicemail.so | |
| load = res_pjsip_session.so | |
| load = res_pjsip.so | |
| load = res_pjsip_t38.so | |
| load = res_pjsip_transport_websocket.so | |
| load = res_pjsip_xpidf_body_generator.so | |
| load = res_rtp_asterisk.so | |
| load = res_sorcery_astdb.so | |
| load = res_sorcery_config.so | |
| load = res_sorcery_memory.so | |
| load = res_sorcery_realtime.so | |
| load = res_timing_timerfd.so | |
| ; Do not load res_hep and kin unless you are using HEP monitoring | |
| ; <http://sipcapture.org> in your network. | |
| noload = res_hep.so | |
| noload = res_hep_pjsip.so | |
| noload = res_hep_rtcp.so | |
| load = chan_iax2.so | |
| ! | |
| ! Osmocom SGSN configuration | |
| ! | |
| log stderr | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging timestamp 0 | |
| logging print file basename last | |
| logging print level 1 | |
| line vty | |
| no login | |
| ! | |
| sgsn | |
| gtp state-dir /var/lib/osmocom/osmo-sgsn | |
| gtp local-ip 127.0.0.1 | |
| ggsn 0 remote-ip __CONTAINER_IP__ | |
| ggsn 0 gtp-version 1 | |
| ggsn 0 echo-interval 60 | |
| authentication optional | |
| auth-policy remote | |
| gsup remote-ip __HLR_IP__ | |
| gsup remote-port 4222 | |
| ! | |
| ns | |
| timer tns-block 3 | |
| timer tns-block-retries 3 | |
| timer tns-reset 3 | |
| timer tns-reset-retries 3 | |
| timer tns-test 30 | |
| timer tns-alive 3 | |
| timer tns-alive-retries 10 | |
| bind udp local | |
| listen __CONTAINER_IP__ 23000 | |
| accept-ipaccess | |
| ! | |
| bssgp | |
| ! | |
| ! OsmoBTS (192.168.1.15-bc49-dirty) configuration saved from vty | |
| !! | |
| ! | |
| log stderr | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging timestamp 0 | |
| logging print file basename last | |
| logging print level 1 | |
| ! | |
| log gsmtap __GATEWAY_IP__ | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging print level 1 | |
| logging level rsl info | |
| logging level oml info | |
| logging level rll notice | |
| logging level rr notice | |
| logging level meas notice | |
| logging level pag info | |
| logging level l1c info | |
| logging level l1p info | |
| logging level dsp error | |
| logging level pcu notice | |
| logging level ho debug | |
| logging level trx notice | |
| logging level loop notice | |
| logging level abis debug | |
| logging level rtp notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp notice | |
| logging level lstats error | |
| ! | |
| line vty | |
| no login | |
| ! | |
| e1_input | |
| e1_line 0 driver ipa | |
| e1_line 0 port 0 | |
| no e1_line 0 keepalive | |
| ! | |
| phy 0 | |
| instance 0 | |
| osmotrx rx-gain 40 | |
| osmotrx tx-attenuation 50 | |
| osmotrx ip local 127.0.0.1 | |
| osmotrx ip remote 127.0.0.1 | |
| no osmotrx timing-advance-loop | |
| bts 0 | |
| band DCS1800 | |
| ipa unit-id __IPA_UNIT_ID__ 0 | |
| oml remote-ip 127.0.0.1 | |
| rtp jitter-buffer 100 | |
| paging queue-size 200 | |
| paging lifetime 0 | |
| min-qual-rach 50 | |
| min-qual-norm -5 | |
| gsmtap-sapi ccch | |
| gsmtap-sapi bcch | |
| gsmtap-sapi rach | |
| gsmtap-sapi agch | |
| gsmtap-sapi pch | |
| gsmtap-sapi sdcch | |
| gsmtap-sapi sacch | |
| trx 0 | |
| power-ramp max-initial 23000 mdBm | |
| power-ramp step-size 2000 mdB | |
| power-ramp step-interval 1 | |
| ms-power-control osmo | |
| phy 0 instance 0 | |
| # mobile.cfg.template - avec placeholders | |
| # KI format : "ki comp128 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX" | |
| # 16 octets séparés par espaces | |
| # Octet 15 = ms_idx, Octet 16 = op_id | |
| ! | |
| line vty | |
| no login | |
| bind 127.0.0.1 4247 | |
| ! | |
| gps device /dev/ttyACM0 | |
| gps baudrate default | |
| no gps enable | |
| ! | |
| no hide-default | |
| ! | |
| ! | |
| log gsmtap 127.0.0.1 | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging level mm debug | |
| logging level rr debug | |
| logging level cc debug | |
| logging level sms debug | |
| ! | |
| ms 1 | |
| layer2-socket /tmp/osmocom_l2 | |
| sim test | |
| network-selection-mode auto | |
| imei __IMEI__ 0 | |
| imei-fixed | |
| no emergency-imsi | |
| sms-service-center __SMS_SC__ | |
| no call-waiting | |
| no auto-answer | |
| no force-rekey | |
| no clip | |
| no clir | |
| tx-power auto | |
| no simulated-delay | |
| stick __ARFCN__ | |
| location-updating | |
| neighbour-measurement | |
| codec full-speed prefer | |
| codec half-speed | |
| no abbrev | |
| support | |
| sms | |
| a5/1 | |
| a5/2 | |
| p-gsm | |
| e-gsm | |
| r-gsm | |
| no gsm-850 | |
| dcs | |
| no pcs | |
| class-900 4 | |
| class-850 4 | |
| class-dcs 1 | |
| class-pcs 1 | |
| channel-capability sdcch+tchf+tchh | |
| full-speech-v1 | |
| full-speech-v2 | |
| half-speech-v1 | |
| min-rxlev -106 | |
| dsc-max 90 | |
| no skip-max-per-band | |
| test-sim | |
| imsi __IMSI__ | |
| ki comp128 __KI__ | |
| no barred-access | |
| rplmn __MCC__ __MNC__ | |
| no shutdown | |
| ! | |
| ! GSMTAP configuration | |
| ! | |
| gsmtap | |
| remote-host 127.0.0.1 | |
| no local-host | |
| lchan sacch | |
| lchan lsacch | |
| lchan sacch/4 | |
| lchan sacch/8 | |
| lchan sacch/f | |
| lchan sacch/h | |
| lchan unknown | |
| lchan bcch | |
| lchan ccch | |
| lchan rach | |
| lchan agch | |
| lchan pch | |
| lchan sdcch | |
| lchan sdcch/4 | |
| lchan sdcch/8 | |
| lchan facch/f | |
| lchan facch/h | |
| lchan pacch | |
| lchan cbch | |
| lchan pdch | |
| lchan pttch | |
| lchan tch/f | |
| lchan tch/h | |
| category gprs dl-unknown | |
| category gprs dl-dummy | |
| category gprs dl-ctrl | |
| category gprs dl-data-gprs | |
| category gprs dl-data-egprs | |
| category gprs ul-unknown | |
| category gprs ul-dummy | |
| category gprs ul-ctrl | |
| category gprs ul-data-gprs | |
| category gprs ul-data-egprs | |
| end | |
| ! | |
| ! OpenGGSN configuration | |
| ! | |
| log stderr | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging timestamp 0 | |
| logging print file basename last | |
| logging print level 1 | |
| logging level ip info | |
| logging level tun info | |
| logging level ggsn info | |
| logging level sgsn notice | |
| logging level icmp6 notice | |
| logging level lglobal notice | |
| logging level llapd notice | |
| logging level linp notice | |
| logging level lmux notice | |
| logging level lmi notice | |
| logging level lmib notice | |
| logging level lsms notice | |
| logging level lctrl notice | |
| logging level lgtp info | |
| logging level lstats notice | |
| logging level lgsup notice | |
| logging level loap notice | |
| logging level lss7 notice | |
| logging level lsccp notice | |
| logging level lsua notice | |
| logging level lm3ua notice | |
| logging level lmgcp notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| ggsn ggsn0 | |
| gtp state-dir /var/lib/osmocom/osmo-ggsn | |
| gtp bind-ip __CONTAINER_IP__ | |
| apn internet | |
| gtpu-mode tun | |
| tun-device apn0 | |
| type-support v4 | |
| ip prefix dynamic 176.16.32.0/24 | |
| ip dns 0 __GATEWAY_IP__ | |
| ip dns 1 __CONTAINER_IP__ | |
| ip ifconfig 176.16.32.0/24 | |
| no shutdown | |
| default-apn internet | |
| no shutdown ggsn | |
| ! | |
| ! osmo-stp.cfg — Signal Transfer Point opérateur __OPERATOR_ID__ | |
| ! | |
| ! Architecture : | |
| ! [BSC __PC_BSC__] ─►┐ | |
| ! ├─ STP __PC_STP__ @ 127.0.0.1:2905 (local) | |
| ! [MSC __PC_MSC__] ─►┘ │ | |
| ! │ asp client → inter-STP __INTER_STP_IP__:2908 | |
| ! ▼ | |
| ! [Inter-STP 0.23.0] | |
| ! | |
| ! BSC/MSC se connectent au STP via 127.0.0.1 (même container). | |
| ! Le STP écoute sur 0.0.0.0:2905 pour accepter connexions locales ET | |
| ! éviter les race conditions Docker (l'IP réseau privé n'existe pas | |
| ! encore au moment du démarrage des services). | |
| ! | |
| log stderr | |
| logging filter all 1 | |
| logging color 1 | |
| logging print category-hex 0 | |
| logging print category 1 | |
| logging print extended-timestamp 1 | |
| logging print level 1 | |
| logging print file basename | |
| logging level lss7 info | |
| logging level lsccp info | |
| logging level lm3ua info | |
| logging level linp notice | |
| ! | |
| stats interval 5 | |
| ! | |
| line vty | |
| no login | |
| ! | |
| cs7 instance 0 | |
| network-indicator international | |
| point-code __PC_STP__ | |
| ! | |
| ! ── Listener local ────────────────────────────────────────────────────── | |
| ! 0.0.0.0 : accepte les connexions sur toutes les interfaces | |
| ! → fonctionne immédiatement, avant même que Docker attache le réseau privé | |
| ! BSC/MSC se connectent via 127.0.0.1 | |
| xua rkm routing-key-allocation dynamic-permitted | |
| listen m3ua 2905 | |
| accept-asp-connections dynamic-permitted | |
| local-ip 127.0.0.1 | |
| local-ip 127.0.0.2 | |
| ! | |
| ! ── Lien client vers l'inter-STP central ──────────────────────────────── | |
| asp asp-to-inter 2908 2910 m3ua | |
| remote-ip __INTER_STP_IP__ | |
| local-ip __INTER_LOCAL_IP__ | |
| role asp | |
| sctp-role client | |
| __INTER_STP_SHUTDOWN__ | |
| ! | |
| as as-inter m3ua | |
| asp asp-to-inter | |
| routing-key __RCTX_INTER__ __PC_STP__ | |
| traffic-mode override | |
| ! | |
| ! ── Routage ───────────────────────────────────────────────────────────── | |
| ! Routes locales (BSC/MSC) : créées dynamiquement par accept-asp. | |
| ! Catch-all : tout le reste → inter-STP. | |
| route-table system | |
| update route 0.0.0 0.0.0 linkset as-inter | |
| log gsmtap __GATEWAY_IP__ | |
| logging filter all 1 | |
| logging color 0 | |
| logging timestamp 0 | |
| logging print category 1 | |
| logging print level 1 | |
| ! | |
| pcu | |
| flow-control-interval 10 | |
| cs 2 | |
| alloc-algorithm dynamic | |
| alpha 0 | |
| gamma 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment