Documentation J+SERVICES Guides Référence API

Note d’intégration — Anti-Restriction, côté application mobile

Cette note complète CONTRAT-API-BRIDAGE (la référence complète des routes). Ici, seulement ce que l’application doit faire, dans quel ordre, et ce qu’il ne faut surtout pas faire.

Base : https://live.jmoai.net/api/v1/radius


1. Le partage des rôles, en une phrase

Le backend calcule et trace. L’application pose, depuis le LAN. Le backend reprend ensuite la main sur le routeur et le dit au gérant.

L’application est la seule à pouvoir poser ces commandes, et ce n’est pas un manque de temps côté serveur. Changer la MAC du WAN renégocie le bail DHCP : si Starlink lie la session à l’adresse, le lien tombe — et le tunnel de gestion comme la voie de secours BTH empruntent ce même lien. Le serveur perdrait le seul chemin qui lui permettrait de revenir en arrière. Depuis le LAN, vous en gardez toujours un. C’est pour ça que le serveur refuse ces écritures même à lui-même (routerWriteGuard).


1 bis. Jusqu’où le masquage porte — à lire avant de dessiner l’écran

Un MikroTik n’est pas toujours branché sur la parabole. Le cas le plus fréquent du parc : il est derrière un routeur du client (Omada, box opérateur, routeur bi-WAN). Son WAN a alors une adresse privée, et ce que le fournisseur voit, c’est la carte du routeur placé devant — jamais celle du MikroTik. Masquer sa MAC ne change alors rien côté fournisseur.

Envoyez adresse_wan (lue dans /ip dhcp-client print) et le serveur tranche :

masquage.portee Sens Ce que l’app affiche
FOURNISSEUR adresse publique ou CGNAT 100.64/10 → le routeur fait face au fournisseur rien de spécial
ROUTEUR_INTERMEDIAIRE adresse privée (10/8, 172.16/12, 192.168/16, 169.254/16) masquage.avertissement, avant le bouton
INCONNUE adresse absente ou illisible ne rien affirmer

⚠️ La CGNAT n’est PAS un routeur intermédiaire. Une parabole Starlink délivre par défaut du 100.64.0.0/10, qui n’est ni public ni privé : le routeur fait bel et bien face au fournisseur, et le masquage porte. Traiter « non publique » comme « derrière un routeur » ferait dire à l’application que la fonction ne sert à rien précisément chez les clients Starlink directs — c’est-à-dire sa cible.

On ne bloque pas : masquer un MikroTik derrière un Omada reste défendable, le gérant peut vouloir la discrétion sur son réseau intermédiaire. On le dit, il décide.

2. Avant tout : le droit

GET /bandwidth/program et GET /starlink/mac/{nasId}/consigne renvoient un objet droit :

{ "autorise": false, "motif": "STARLINK_X_REQUIRED", "enforced": false, "prix_xof": 25000 }

3. Préparer la pose : ce qu’il faut lire sur le routeur

L’application est sur le LAN, elle voit ce que le serveur ne voit pas. Le serveur ne devine rien de tout ceci, et c’est délibéré : se tromper d’interface coupe le gérant de son routeur.

À lire Commande RouterOS Sert à
tous les liens fournisseur /ip dhcp-client print → une ligne par client DHCP interface=, interfaces_wan=
l’adresse du WAN même sortie, colonne address adresse_wan=
la MAC actuelle de l’interface masquée /interface ethernet get [find name=…] mac-address mac_origine
les ponts /interface bridge print ponts=
les membres des ponts /interface bridge port print vérifier qu’aucun WAN n’y est
les interfaces locales /interface print interfaces_locales=

Le WAN ne s’appelle pas toujours ether1. Relevé sur un routeur du parc : il s’appelle ether1-TP-LinkGiga. Lisez le nom, ne le supposez pas.

Un routeur peut avoir plusieurs liens fournisseur. Ce même routeur en a deux : la fibre sur ether1-TP-LinkGiga et le Starlink de secours sur ether2-Bakup. La découverte se règle globalement, donc la liste locale les couvre tous ; mais le nom d’hôte se retire client DHCP par client DHCP. Envoyez-les tous dans interfaces_wan, sinon le routeur continuera d’annoncer son identité au second fournisseur pendant que l’écran affichera « appliqué ».

interface = celui dont on masque la MAC. interfaces_wan = tous les liens fournisseur.

Envoyez la liste des ponts. Ne comptez pas sur le serveur pour les deviner : sur ce routeur ils s’appellent HOTSPOT et Container-bridge. Un gérant nomme ses ponts par leur fonction, pas par leur type — sans ponts=, l’avertissement sur la visibilité côté clients ne s’affiche pas, précisément là où il sert.

Pour les interfaces locales, le cas normal est un seul pont. Envoyez ce pont, rien d’autre : la découverte y atteint tous ses membres, ports et Wi-Fi compris.

⚠️ Vérifiez que ce pont ne contient aucun WAN (/interface bridge port print). S’il en contient un, la découverte ressortirait par le WAN — exactement ce qu’on veut empêcher. Dans ce cas, n’envoyez pas ce pont.


4. La consigne

GET /starlink/mac/{nasId}/consigne
    ?interface=ether2-Bakup
    &interfaces_wan=ether1-TP-LinkGiga
    &interfaces_locales=HOTSPOT
    &ponts=HOTSPOT,Container-bridge
    &adresse_wan=10.100.4.2/24

Consigne réellement produite pour un routeur bi-WAN du parc :

1. :if ([:len [/interface list find name=JSLOCAL]] = 0) do={/interface list add name=JSLOCAL comment=JSLOCAL}
2. :if ([:len [/interface list member find list=JSLOCAL interface=HOTSPOT]] = 0) do={/interface list member add list=JSLOCAL interface=HOTSPOT}
3. /ip neighbor discovery-settings set discover-interface-list=JSLOCAL
4. /ip dhcp-client set [find interface=ether2-Bakup] dhcp-options=clientid
5. /ip dhcp-client set [find interface=ether1-TP-LinkGiga] dhcp-options=clientid
6. /interface ethernet set [find name=ether2-Bakup] mac-address=02:20:F9:E9:9B:50   ← EN DERNIER

Ajoutez &nom_hote=box-4f21 si le gérant veut un nom d’hôte de substitution. Sans ce paramètre, aucun nom d’hôte n’est envoyé au fournisseur — c’est le réglage le plus discret.

Vous récupérez commande (la MAC), discretion.commandes (la discrétion WAN), precautions, et discretion.avertissement.

Affichez precautions avant le bouton, pas dans un écran d’aide. Et affichez discretion.avertissement quand il n’est pas null : c’est le seul moment où le gérant peut encore choisir une interface de gestion hors du pont plutôt que d’être visible de ses clients.


5. L’ORDRE de pose — le point le plus important de cette note

1. discretion.commandes   (toutes, dans l'ordre reçu)
2. commande               (la MAC)  ← EN DERNIER

La MAC se pose en dernier, et c’est la seule règle qu’il ne faut pas inverser. Les commandes de discrétion ne coupent rien : elles se posent tranquillement, sur un lien qui fonctionne. La MAC, elle, fait tomber le lien le temps de la renégociation du bail. Poser la discrétion après la MAC reviendrait à écrire sur un routeur qui est justement en train de perdre son WAN.

Les commandes de discrétion sont idempotentes : si l’une échoue, rejouez tout le bloc, rien ne sera dupliqué.

Gardez commande_retour en local, sur le téléphone, avant de poser quoi que ce soit. C’est la commande qui rétablit l’adresse d’origine. Elle doit rester lisible hors ligne : le moment où on en a besoin est précisément celui où le routeur n’a plus Internet.


5 bis. Le routeur refuse les commandes ? Les trois causes, dans l’ordre

Cause n°1 — vous envoyez du script à une API qui n’en veut pas

Les commandes de discretion.commandes sont du script RouterOS :

:if ([:len [/interface list find name=JSLOCAL]] = 0) do={/interface list add name=JSLOCAL comment=JSLOCAL}

Cette forme fonctionne en terminal — SSH, Telnet, terminal WinBox. Elle n’existe pas en API REST (/rest/...) ni en API binaire (8728) : ces deux-là ne connaissent que des ressources et des champs, pas :if, pas :len, pas do={}. Envoyer ça à REST donne une erreur de syntaxe qui n’a rien à voir avec la commande elle-même.

Si votre app parle en SSH : collez les commandes telles quelles, une par ligne. Rien à faire.

Si votre app parle en REST : il faut porter l’idempotence côté client. Une commande :if ([:len [X find …]] = 0) do={X add …} se traduit par « je fais un GET pour voir si ça existe, et je ne fais le PUT que si la liste est vide ». Le tableau du §5 ter donne la correspondance.

Cause n°2 — la cible n’existe pas, et le lot entier tombe

/ip dhcp-client set [find interface=ether1] sur une interface qui n’a pas de client DHCP fait échouer la commande. Et comme un lot fautif échoue en entier, toute la pose tombe — alors qu’il n’y avait simplement rien à masquer sur ce lien.

C’est fréquent : un WAN en PPPoE ou en IP fixe n’a pas de client DHCP. Depuis le 2026-08-29 le serveur garde donc chaque écriture sur l’existence de sa cible. Si vous aviez copié une ancienne version des commandes, redemandez la consigne : la forme actuelle est

:if ([:len [/ip dhcp-client find interface=ether1]] > 0) do={/ip dhcp-client set [find interface=ether1] dhcp-options=clientid}

Note : sur un WAN PPPoE il n’y a de toute façon pas de nom d’hôte DHCP à cacher — PPPoE s’annonce par un identifiant, pas par l’option 12.

Cause n°3 — le nom de l’interface

Un nom mal recopié fait find sur du vide, donc rien, donc l’erreur ci-dessus. Vérifié sur un routeur du parc : les noms contiennent des tirets (ether1-TP-LinkGiga), et [find name=…] les accepte tels quels sans guillemets. En revanche, un nom contenant un espace doit être entre guillemets : [find name="mon interface"].

Les messages de RouterOS, traduits

Ce que dit le routeur Ce que ça veut dire
expected end of command / syntax error du script envoyé à REST ou à l’API binaire — cause n°1
no such item find n’a rien trouvé : cause n°2 ou n°3
already have such name vous avez retiré le :if : la liste existe déjà
input does not match any value of interface nom d’interface inexistant — cause n°3
not enough permissions l’utilisateur RouterOS n’a pas la politique write

5 ter. Correspondance REST, si votre app ne passe pas par SSH

Commande script Ressource REST Idempotence à faire vous-même
/interface list add name=JSLOCAL PUT /rest/interface/list GET /rest/interface/list?name=JSLOCAL d’abord
/interface list member add … PUT /rest/interface/list/member GET /rest/interface/list/member?list=JSLOCAL&interface=HOTSPOT
/ip neighbor discovery-settings set … réglage unique : POST /rest/ip/neighbor/discovery-settings/set aucune, c’est un set
/ip dhcp-client set [find interface=X] … GET /rest/ip/dhcp-client?interface=X puis PATCH /rest/ip/dhcp-client/<.id> le GET est la garde
/interface ethernet set [find name=X] mac-address=… GET /rest/interface/ethernet?name=X puis PATCH /rest/interface/ethernet/<.id> le GET est la garde

Les chemins REST exacts varient un peu selon la version de RouterOS : validez-les par un GET avant d’écrire. En cas de doute, le terminal SSH reste la voie la plus sûre — c’est celle pour laquelle les commandes sont écrites.

6. Vérifier qu’Internet est revenu — vraiment

Après la pose, attendez. Deux vérifications, les deux nécessaires :

1. :put [/ip dhcp-client get [find interface=ether1] status]     → doit valoir "bound"
2. /ping 1.1.1.1 count=3                                          → doit répondre

Le premier dit que le bail a été renégocié, le second que le trafic passe. Un bail obtenu ne prouve pas qu’Internet fonctionne.

Laissez jusqu’à 2 minutes. Au-delà, proposez la restauration (commande_retour) — ne la déclenchez pas toute seule : le gérant doit décider, il est peut-être en train de regarder la parabole.


7. Déclarer, puis suivre

POST /starlink/mac/{nasId}
{
  "resultat": "POSEE",
  "interface": "ether1",
  "mac_origine": "48:A9:8A:11:22:33",
  "mac_posee": "02:AB:CD:EF:01:02",
  "internet_confirme": true,
  "discretion_appliquee": true,
  "interfaces_locales": ["bridge-hotspot"]
}

internet_confirme n’est pas une case à cocher par principe. C’est lui qui déclenche la reprise en main côté serveur. Un true optimiste la ferait démarrer sur un routeur encore en train de renégocier son bail, et le gérant lirait un échec là où il n’y avait qu’une attente.

La réponse porte déjà service_etat: "INITIALISATION" et son titre : enchaînez sur l’écran de suivi sans second appel.

Puis sondez toutes les 5 s :

GET /starlink/mac/{nasId}/statut

En cas d’échec de pose, déclarez-le : resultat: "ECHEC", avec mac_origine quand même. C’est justement quand ça a raté qu’il faut savoir vers quoi revenir.

Pour revenir en arrière : resultat: "RESTAUREE". Le service repasse à EN_ATTENTE.


8. Les notifications

Type STARLINK_X. Elles arrivent en plus de l’écran, pour le gérant qui a quitté l’app :

event_type Texte
SERVICE_INITIALIZING Service Anti-Restriction en cours d’initialisation…
SERVICE_READY Système Anti-Restriction actif sur SN……
SERVICE_DEGRADED L’adresse est posée, mais le routeur n’a pas encore répondu au serveur

Un appui dessus doit ouvrir l’écran de statut du bon nas_id (il est dans metadata).


9. Ce qu’il faut dire au gérant, et ne pas embellir

Le camouflage est bon, il n’est pas total. Quatre signaux trahissent un MikroTik ; trois sont traités :

Signal État
Adresse MAC masquée — « fabricant inconnu », comme tout téléphone Android ou iPhone récent
Découverte de voisinage (MNDP/LLDP) limitée aux interfaces locales
Nom d’hôte DHCP retiré, ou remplacé
Empreinte DHCP (option 55) reste reconnaissable — ne se change pas

Le champ discretion.limites porte cette liste. Affichez-la. Un camouflage annoncé comme total et qui ne l’est pas ferait prendre au gérant des risques qu’il croit couverts.

Et dites-lui aussi ce qui ne change pas : le nom du routeur (/system identity) reste le vrai. C’est ce que son technicien lit dans WinBox. Seul le nom envoyé au fournisseur est masqué.


10. Les erreurs, et ce qu’elles veulent dire pour le gérant

Code Message à afficher
MAC_SERIAL_UNKNOWN « Ce routeur n’est pas encore reconnu par la plateforme. » (bloquant)
MAC_INTERFACE_INVALID interface mal lue — refaites l’étape 3
MAC_ORIGIN_INVALID l’adresse d’origine n’a pas été lue avant la pose
MAC_NO_EFFECT l’adresse posée est identique à l’origine — rien n’aurait été masqué
MAC_POSED_NOT_UNICAST adresse de groupe : une carte ne peut pas la porter
MAC_MISMATCH l’adresse posée n’est pas celle calculée — mauvais routeur ?
MAC_LOCAL_IFACES_REQUIRED discrétion déclarée sans dire sur quelles interfaces
MAC_HOSTNAME_INVALID nom d’hôte : lettres, chiffres et tirets uniquement
MAC_NOT_POSED relance demandée sur un routeur qui n’est pas masqué
ROUTER_WRITE_FORBIDDEN ne devrait jamais arriver depuis l’app — signalez-le

11. Protocole de test — les 12 routes, dans l’ordre

Toutes les routes sont sous https://live.jmoai.net/api/v1/radius. Chaque appel porte Authorization: Bearer <jeton du gérant>. Un compte client lié à un NAS est requis : requireClientRole + requireLinkedClient + requireRadiusLicense sont posés sur toutes.

Posez d’abord :

API=https://live.jmoai.net/api/v1/radius
T="Bearer <jeton>"
NAS=<uuid du NAS>

Phase 0 — les gardes (à faire une fois, sans jeton)

# Appel Attendu
0.1 n’importe quelle route sans Authorization 401
0.2 une route avec le jeton d’un autre client + votre NAS 404 MAC_NAS_NOT_OWNED
0.3 PUT /bandwidth/program 6 fois en moins d’une minute la 6ᵉ en 429

Un 403 en 0.1 au lieu d’un 401, ou un 200 en 0.2, sont bloquants : arrêtez la recette.

Phase 1 — lectures, sans rien changer

# Appel Vérifier
1.1 GET $API/bandwidth/classes la liste des classes, chacune avec plages > 0
1.2 GET $API/bandwidth/program droit.enforced présent ; programmes (vide au début)
1.3 GET $API/bandwidth/activity?limite=5 activites[] + suivant ; texte en français
1.4 GET $API/bandwidth/impact?jours=30 si fiabilite: "INSUFFISANTE"reduction_pct null
1.5 GET $API/starlink/mac substitutions: [] au premier passage
1.6 GET $API/starlink/mac/$NAS/statut service_etat: "EN_ATTENTE", peut_relancer: false

1.4 est le piège classique : si l’app affiche un pourcentage alors que fiabilite vaut INSUFFISANTE, c’est un défaut bloquant — elle inventerait un chiffre.

Phase 2 — la consigne, sans rien poser

# Appel Vérifier
2.1 /starlink/mac/$NAS/consigne sans interface commande: null, discretion: null, discretion_motif: "INTERFACE_WAN_MANQUANTE"
2.2 avec interface=ether1 seul commande remplie, discretion_motif: "INVENTAIRE_INTERFACES_MANQUANT"
2.3 avec interface + interfaces_locales + ponts discretion.commandes (≥ 4), portee: "PONT", avertissement non nul
2.4 même appel, deux fois mac_substitution identique — l’adresse est déterministe
2.5 avec adresse_wan=10.0.0.2 masquage.portee: "ROUTEUR_INTERMEDIAIRE"
2.6 avec adresse_wan=100.75.1.2 masquage.portee: "FOURNISSEUR" (CGNAT Starlink)
2.7 avec interface=ether1;/system reboot 400 MAC_INTERFACE_INVALID
2.8 avec nom_hote=nom impossible ! discretion: null (nom refusé)

2.4 est la vérification qui compte : une adresse qui change d’un appel à l’autre serait reposée à chaque passage, et changer la MAC coupe le lien à chaque fois.

Phase 3 — la pose réelle, sur un routeur d’essai, depuis le LAN

Ne faites cette phase que sur un routeur que vous pouvez atteindre physiquement.

  1. Notez commande_retour — ou l’adresse d’origine — sur papier ou hors ligne.
  2. Posez discretion.commandes dans l’ordre. Vérifiez : /ip neighbor discovery-settings printdiscover-interface-list: JSLOCAL, /ip dhcp-client printdhcp-options=clientid sur chaque WAN.
  3. Rejouez le même bloc une seconde fois → aucune erreur, aucun doublon dans /interface list member print (les commandes sont idempotentes).
  4. Posez commande (la MAC) en dernier. Attendez le bail : status: bound, puis un ping.
  5. POST $API/starlink/mac/$NAS avec resultat: "POSEE", internet_confirme: true, discretion_appliquee: true, interfaces_locales.
# Vérifier
3.1 la réponse porte service_etat: "INITIALISATION" et un titre
3.2 une notification push SERVICE_INITIALIZING arrive
3.3 GET /statut passe ROUTEUR à OK en moins de 30 s
3.4 service_etat finit à OPERATIONNEL, operationnel_depuis renseigné
3.5 une notification SERVICE_READY nomme le numéro de série
3.6 GET /bandwidth/activity contient MAC_SUBSTITUTED et WAN_DISCRETION_APPLIED, racontés en français
3.7 GET /starlink/mac liste le routeur avec discretion_appliquee: true

Phase 4 — les refus

# Corps envoyé à POST /starlink/mac/$NAS Attendu
4.1 mac_posee = mac_origine 400 MAC_NO_EFFECT
4.2 mac_posee: "03:AB:CD:EF:01:02" 400 MAC_POSED_NOT_UNICAST
4.3 mac_posee autre que celle de la consigne 409 MAC_MISMATCH
4.4 resultat: "ECHEC" sans mac_origine 400 MAC_ORIGIN_INVALID
4.5 discretion_appliquee: true sans interfaces_locales 400 MAC_LOCAL_IFACES_REQUIRED
4.6 nom_hote: "nom impossible !" 400 MAC_HOSTNAME_INVALID
4.7 resultat: "OK" 400 MAC_RESULT_INVALID

Phase 5 — reprise et retour arrière

# Appel Attendu
5.1 POST /starlink/mac/$NAS/relancer pendant une reprise en cours relance: false (pas deux sauvegardes concurrentes)
5.2 POST …/relancer sur un routeur non masqué 409 MAC_NOT_POSED
5.3 posez commande_retour sur le routeur, puis POST avec resultat: "RESTAUREE" service_etat repasse à EN_ATTENTE
5.4 GET /statut après restauration plus de « actif », peut_relancer: false
5.5 GET /bandwidth/activity une ligne MAC_RESTORED

Phase 6 — le bridage, si le gérant en a un

# Appel Attendu
6.1 PUT /bandwidth/program {nas_id, limit_mbps: 10} 200, application: "PLANIFIEE"
6.2 PUT avec mode: "SCHEDULED" sans dates 400 BW_SCHEDULE_EMPTY
6.3 PUT sans limit_mbps 400 BW_LIMIT_REQUIRED
6.4 attendre un passage du cron (≤ 5 min), puis GET /bandwidth/program etat_constate renseigné, en_erreur: false
6.5 sur le routeur : /queue simple print where comment=JSBRIDAGE les files sont là
6.6 DELETE /bandwidth/program/{nasId} 200, puis les files disparaissent du routeur

Ce qui doit rester vrai après toute la recette


12. Recette — à cocher avant publication