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

Révision 2026-09-17 — camouflage Standalone : aucune association NAS/RADIUS n’est requise pour le camouflage manuel. Sans nas_id ou sans activation volontaire de la gestion Cloud, l’application applique et vérifie le changement de MAC localement, sans aucun appel aux routes Starlink MAC ou bridage. Les sections Cloud de ce document, y compris les consignes, déclarations, statuts et calendriers, ne s’appliquent qu’à un NAS existant que le gérant a choisi de gérer. L’application ne crée jamais un NAS pour lever un blocage de camouflage.

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

router_serial est obligatoire pour chaque appel de camouflage. Si l’application ne parvient pas à le lire, elle envoie UNKNOWN-SERIAL : le backend l’accepte sans 400, résout la série canonique si possible et garde sinon une identité interne stable par NAS.

{ "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
    &router_serial=HH40AF7YVNP
    &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.

Ajoutez &masquer_ttl=true pour effacer la trace du NAT. Chaque routeur traversé décrémente le TTL d’un paquet : un fournisseur qui voit arriver un TTL de 63 sait qu’un routeur se cache derrière. Avec ce paramètre, discretion.commandes contient une règle mangle de plus qui réécrit le TTL sortant à 64 sur chaque lien fournisseur — le MikroTik redevient un appareil comme les autres. C’est le seul des quatre signaux qui révèle la NATURE de l’appareil ; la MAC ne révèle que sa marque.

⚠️ Opt-in, et pour une raison. Réécrire le TTL casse le traceroute DEPUIS le LAN vers l’extérieur (les sauts ne se comptent plus). N’envoyez masquer_ttl=true que si le gérant veut la discrétion maximale, et dites-le-lui. Le champ de retour discretion.ttl_masque confirme si la règle est dans le lot. La règle porte le commentaire JSTTL — rejouable sans créer de doublon.

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}
{
  "router_serial": "HH40AF7YVNP",
  "resultat": "POSEE",
  "interface": "ether1",
  "mac_origine": "48:A9:8A:11:22:33",
  "camouflage_mac": "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?router_serial=HH40AF7YVNP

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. Cinq signaux trahissent un MikroTik ; quatre sont traités depuis le 2026-08-31 :

Signal État
Adresse MAC masquée. Sur cette infra, chaque routeur porte un préfixe d’un VRAI constructeur varié (Apple, Samsung, Intel, Google, Xiaomi…) : le parc ressemble à un mélange d’appareils du commerce, pas à un « fabricant inconnu » ni à une flotte homogène
Découverte de voisinage (MNDP/LLDP) limitée aux interfaces locales
Nom d’hôte DHCP retiré, ou remplacé
TTL réécrit à 64 si masquer_ttl=true — efface la trace du NAT
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_CAMOUFLAGE_NOT_UNICAST adresse de groupe : une carte ne peut pas la porter
MAC_COLLISION générer une autre MAC et rejouer la demande
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é
MAC_ALREADY_IN_PROGRESS une pose est déjà en cours : conserver l’écran de suivi
ROUTER_WRITE_FORBIDDEN ne devrait jamais arriver depuis l’app — signalez-le

Déviation documentée

UNKNOWN-SERIAL est accepté afin qu’une lecture RouterOS indisponible ne bloque pas le gérant. Le serveur résout d’abord la série canonique du NAS. S’il ne peut pas encore le faire, il utilise une identité stable propre à ce NAS pour éviter qu’une même MAC soit attribuée à tous les routeurs en repli. Cette identité ne prouve pas le matériel : l’application doit transmettre la vraie série dès qu’elle est lisible, afin que la reprise distante puisse cibler le routeur.

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 generation_par: "APP_LAN", discretion: null, discretion_motif: "INTERFACE_WAN_MANQUANTE"
2.2 avec interface=ether1 seul generation_par: "APP_LAN", 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 aucune MAC renvoyée : l’app la génère avant la déclaration
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é)

La MAC est désormais produite par l’app puis réservée par le serveur à la déclaration ; un 409 MAC_COLLISION impose de générer une nouvelle valeur avant le retry.

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 print → discover-interface-list: JSLOCAL, /ip dhcp-client print → dhcp-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 camouflage_mac = mac_origine 400 MAC_NO_EFFECT
4.2 camouflage_mac: "03:AB:CD:EF:01:02" 400 MAC_CAMOUFLAGE_NOT_UNICAST
4.3 camouflage_mac déjà enregistrée par un autre routeur 409 MAC_COLLISION
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