Skip to content

Instantly share code, notes, and snippets.

@bbaranoff
Created March 8, 2026 12:03
Show Gist options
  • Select an option

  • Save bbaranoff/e7c0c0fe42cb69ad640cd39549dda174 to your computer and use it in GitHub Desktop.

Select an option

Save bbaranoff/e7c0c0fe42cb69ad640cd39549dda174 to your computer and use it in GitHub Desktop.
#!/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