🔌 Diagnostic réseau OT/IT · au shell Linux

Chapitre 03
Découvrir qui est là, sans IP

Objectifs du chapitre

1. Pourquoi la découverte L2 d'abord

En OT, on arrive rarement dans un réseau documenté. On branche un câble sur un port libre d'un switch d'armoire et… on ne connaît ni le plan d'adressage, ni les IP des automates, ni même le masque du sous-réseau. Les réflexes IT (« je scanne le /24 ») supposent déjà de savoir quel /24. Or ici, on ne le sait pas encore.

La bonne nouvelle : les équipements d'automatisme parlent avant même d'avoir une IP compatible avec la tienne. Deux protocoles de couche 2 le permettent :

Ces deux moyens sont aussi les plus non-intrusifs qui soient : LLDP est purement passif (on lit ce qui passe déjà), et un DCP Identify ne fait qu'une question à laquelle les devices répondent de toute façon en fonctionnement normal. C'est le point de départ de référence, avant tout ping, tout scan, toute config IP.

LLDP te dit est branché chaque équipement (topologie physique, port par port). DCP te dit qui il est (nom de station, IP, type), même sans adresse compatible. Combinés, ils reconstruisent la vue « Topologie » de TIA Portal — sauf que là, tu la mesures sur le fil au lieu de la lire dans un projet d'ingénierie qui n'est peut-être plus à jour.

2. LLDP — le voisinage

Le démon lldpd, installé au chapitre 1, tourne en tâche de fond. Il émet les annonces LLDP de ton poste et, surtout, il collecte celles des voisins directs : switches Scalance, CPU, IO-devices. On l'interroge avec lldpctl (ou lldpcli show neighbors, équivalent).

$ lldpctl
-------------------------------------------------------------------------------
LLDP neighbors:
-------------------------------------------------------------------------------
Interface:    eth0, via: LLDP
  Chassis:
    ChassisID:    mac 00:1b:1b:2a:1f:40
    SysName:      SW-A
    SysDescr:     Siemens, SIMATIC NET, SCALANCE XC208, HW: 4, FW: V4.3
    MgmtIP:       192.168.0.240
    Capability:   Bridge, on
  Port:
    PortID:       ifname port-0003
    PortDescr:    Ethernet Port 3
    TTL:          120
-------------------------------------------------------------------------------
Interface:    eth0, via: LLDP
  Chassis:
    ChassisID:    mac 00:1b:1b:2a:10:01
    SysName:      cpu1500
    SysDescr:     Siemens, SIMATIC S7-1500, CPU 1516-3 PN/DP
    MgmtIP:       192.168.0.1
    Capability:   Station, on
  Port:
    PortID:       local port-001
    PortDescr:    X1 P1
    TTL:          20
-------------------------------------------------------------------------------

Lecture de cette sortie. Sur notre eth0, lldpd voit deux annonces : un switch SW-A (un Scalance XC208, MAC 00:1b:1b:…, IP de gestion .240) et le CPU cpu1500 (.1). Le champ décisif pour la topologie est le PortID distant : le voisin nous dit sur quel port de lui-même nous sommes branchés. Ici, SW-A annonce port-0003 — donc notre poste est câblé sur le port 3 du switch A. En interrogeant chaque équipement de la sorte, on reconstitue l'arbre complet : quel device sur quel port de quel switch.

lldpctl ne montre que les voisins directs (un saut). Pour la topologie complète, on interroge chaque switch (souvent via SNMP, cf. chapitre ultérieur, ou en s'y branchant tour à tour). Le TTL indique depuis combien de temps l'annonce est valide : une entrée qui disparaît de la liste = un câble débranché ou un voisin qui ne parle pas LLDP.

port-0001 port-0003 SW-A · Scalance .240 · MRM cpu1500 .1 · S7-1500 poste diag 🔎 .71 · eth0
Figure 3.1. La topologie déduite de la sortie lldpctl ci-dessus. Le PortID distant de chaque voisin (traits) place l'équipement sur un port précis du switch. On obtient ainsi la carte physique mesurée, sans avoir configuré la moindre IP.

Le SysName annoncé par un device Siemens en LLDP est son nom de station Profinet (ici cpu1500, et200sp-1…). Ce nom n'est pas cosmétique : c'est la clé d'identité par laquelle le CPU reconnaît et adresse chaque IO-device au démarrage (on y reviendra au chapitre 7). Voir ce nom sur le fil, c'est voir exactement ce que le contrôleur utilise pour attribuer les IP.

3. Profinet DCP — « qui es-tu ? » sans IP

LLDP est passif et ne voit que les voisins directs. Le DCP va plus loin : c'est un protocole de couche 2 (Ethertype 0x8892, comme le Profinet temps réel), diffusé en multicast sur tout le segment. Un message spécial, Identify All, demande à tous les devices Profinet de se présenter. Comme c'est de la couche 2 pure, ils répondent même si leur IP est dans un autre sous-réseau que le tien — ou s'ils n'ont aucune IP (device neuf sorti du carton).

En Python, la bibliothèque pnio-dcp (installée au chapitre 1) fait tout le travail de trame :

import pnio_dcp

# On lie le DCP à TON interface : IP locale ou nom d'interface ("eth0").
dcp = pnio_dcp.DCP("192.168.0.71")

# "Identify All" : tous les devices Profinet du segment répondent.
for d in dcp.identify_all():
    print(f"{d.name_of_station:14} {d.IPAddress:15} {d.MAC}")

Sur notre cellule, on obtient :

$ python3 dcp_identify.py
cpu1500        192.168.0.1     00:1b:1b:2a:10:01
et200sp-1      192.168.0.20    00:1b:1b:2a:10:20
et200sp-2      192.168.0.21    00:1b:1b:2a:10:21

En une requête, on a l'inventaire Profinet complet : nom de station, IP, masque, passerelle (attributs d.NetworkMask, d.Gateway), MAC et type de device. Aucune config préalable, aucune connaissance du plan d'adressage.

Mais le DCP ne fait pas que lire — il permet aussi d'écrire. Sur l'objet renvoyé pour un device, on dispose de :

Ce sont des outils de mise en service redoutables : donner nom et IP à un device neuf, ou faire clignoter « celui du milieu de la rangée » pour lever un doute de câblage.

Repérer physiquement un device dont on connaît la MAC (par exemple et200sp-2) :

$ python3 -c "import pnio_dcp; \
pnio_dcp.DCP('192.168.0.71').blink('00:1b:1b:2a:10:21')"

La LED du device se met à clignoter quelques secondes. Idéal pour dire « c'est celui-là » sans compter les câbles.

identify_all() et blink() ne dérangent rien : ce sont des lectures et un voyant. Mais set_ip_address, set_name_of_station et reset_to_factory écrivent dans le device. Renommer ou ré-adresser un IO-device en production, c'est rompre la clé d'identité que le CPU attend : le device sort de l'échange, l'API passe l'entrée en défaut, la machine peut s'arrêter. Ces trois appels sont réservés au banc ou à une intervention planifiée, jamais lancés « pour voir ».

4. Capturer le DCP au vol

On n'est pas obligé d'émettre soi-même du DCP : on peut aussi écouter celui que les autres génèrent. Dès qu'un outil d'ingénierie (TIA Portal, PRONETA, un autre poste) lance une découverte, des trames DCP Identify et leurs réponses passent sur le fil. On les capture avec tshark en filtrant sur l'Ethertype Profinet :

$ sudo tshark -i eth0 -f 'ether proto 0x8892' -Y pn_dcp
    1  0.000000  00:1b:1b:2a:10:71 → 01:0e:cf:00:00:00  PN-DCP Ident Req, Xid:0x1, All
    2  0.004120  00:1b:1b:2a:10:01 → 00:1b:1b:2a:10:71  PN-DCP Ident Ok, NameOfStation:"cpu1500"
    3  0.004980  00:1b:1b:2a:10:20 → 00:1b:1b:2a:10:71  PN-DCP Ident Ok, NameOfStation:"et200sp-1"
    4  0.005610  00:1b:1b:2a:10:21 → 00:1b:1b:2a:10:71  PN-DCP Ident Ok, NameOfStation:"et200sp-2"

Le filtre de capture -f 'ether proto 0x8892' ne garde que les trames Profinet (DCP et temps réel partagent cet Ethertype), et le filtre d'affichage -Y pn_dcp ne montre que le DCP. On voit ici une requête Identify All (destinée à l'adresse multicast 01:0e:cf:00:00:00) suivie des trois réponses unicast. Utile pour diagnostiquer « qui scanne le réseau », ou pour confirmer qu'un device répond bien quand notre propre identify_all() reste muet.

DCP et Profinet RT utilisent le même Ethertype 0x8892. Le filtre de capture ether proto 0x8892 attrape donc aussi le trafic temps réel cyclique — qui peut être massif (une trame toutes les 1 à 4 ms par device). D'où le second filtre -Y pn_dcp : sans lui, le DCP se noie dans le flot RT. En capture longue, préfère écrire un .pcap puis filtrer à la lecture.

5. Table ARP / voisins

Une fois qu'un peu de trafic IP a circulé (un ping, une session), le noyau Linux garde la correspondance IP↔MAC de ses voisins dans le cache ARP. On le lit avec ip neigh :

$ ip neigh
192.168.0.1    dev eth0 lladdr 00:1b:1b:2a:10:01 REACHABLE
192.168.0.20   dev eth0 lladdr 00:1b:1b:2a:10:20 STALE
192.168.0.240  dev eth0 lladdr 00:1b:1b:2a:1f:40 REACHABLE
192.168.0.21   dev eth0                          FAILED

Les états renseignent :

Contrairement à LLDP/DCP, ip neigh a besoin de trafic IP pour se remplir : c'est une photo de « avec qui j'ai parlé en IP récemment », pas une découverte active. Si ton poste est un pont (bridge), bridge fdb show montre en plus la table d'apprentissage MAC↔port de couche 2.

6. arp-scan — l'inventaire éclair

Pour peupler activement cet inventaire sur un sous-réseau connu, arp-scan envoie une requête ARP à chaque adresse du subnet et liste qui répond :

$ sudo arp-scan --interface=eth0 --localnet
Interface: eth0, type: EN10MB, MAC: 00:1b:1b:2a:10:71, IPv4: 192.168.0.71
Starting arp-scan 1.10.0 with 256 hosts
192.168.0.1     00:1b:1b:2a:10:01   Siemens AG
192.168.0.20    00:1b:1b:2a:10:20   Siemens AG
192.168.0.21    00:1b:1b:2a:10:21   Siemens AG
192.168.0.240   00:1b:1b:2a:1f:40   Siemens AG
192.168.0.241   00:1b:1b:2a:1f:41   Siemens AG
192.168.0.242   00:1b:1b:2a:1f:42   Siemens AG

6 packets received by filter, 0 packets dropped
6 responded

La troisième colonne est le constructeur, déduit de l'OUI — les trois premiers octets de la MAC, attribués par l'IEEE à chaque fabricant. Le préfixe 00:1b:1b est un OUI Siemens : tous nos équipements ressortent instantanément comme « Siemens AG ». Siemens possède plusieurs OUI (00:1b:1b, 8c:f3:19, ac:64:17…) ; arp-scan les reconnaît via sa base OUI intégrée. En un scan, on distingue les automates du reste (un PC, une imprimante, un équipement tiers ressortiraient avec un autre constructeur).

arp-scan --localnet émet 256 requêtes ARP d'un coup. Sur un banc, c'est anodin. Sur une cellule en production, cette rafale de broadcast reste légère mais n'est pas nulle : privilégie une plage restreinte (--interface=eth0 192.168.0.1-192.168.0.50) et évite les scans répétés en boucle sur un réseau temps réel sensible.

Récapitulatif

Exercices

Exercice 1 — Reconstruire la topologie depuis LLDP

Un collègue te transmet cette sortie lldpctl prise sur son poste (branché sur eth0). Décris précisément : quels équipements sont visibles, sur quel port de quel switch chacun est câblé, et où se trouve le poste du collègue.

Interface: eth0, via: LLDP
  Chassis: SysName: SW-B   MgmtIP: 192.168.0.241   Capability: Bridge
  Port:    PortID: ifname port-0005
Interface: eth0, via: LLDP
  Chassis: SysName: et200sp-1   MgmtIP: 192.168.0.20   Capability: Station
  Port:    PortID: local port-002
Voir la solution

Deux voisins directs sont visibles depuis eth0 :

  • Le switch SW-B (Scalance, IP de gestion 192.168.0.241), qui annonce nous voir arriver sur son port 5 (port-0005). Autrement dit, le poste du collègue est branché sur le port 5 de SW-B.
  • L'IO-device et200sp-1 (192.168.0.20, capacité « Station » = c'est un device, pas un switch), joignable directement — il partage donc le même domaine de collision/port voisin. Son PortID port-002 est son port à lui.

Topologie déduite : poste → port-5 de SW-B, et et200sp-1 voisin direct sur ce segment. Le PortID distant d'un switch localise toujours ton point de branchement ; le SysName + MgmtIP identifient l'équipement. En interrogeant SW-B lui-même (SNMP), on obtiendrait la liste complète de ce qui pend à ses autres ports.

Exercice 2 — Identify All puis clignotement ciblé

Écris un court script pnio-dcp qui liste tous les devices Profinet (nom, IP, MAC) depuis l'interface 192.168.0.71, puis fais clignoter la LED de la station nommée et200sp-2 pour la repérer dans l'armoire.

Voir la solution
import pnio_dcp

dcp = pnio_dcp.DCP("192.168.0.71")

# 1) Inventaire complet
devices = dcp.identify_all()
for d in devices:
    print(f"{d.name_of_station:14} {d.IPAddress:15} {d.MAC}")

# 2) Retrouver et200sp-2, puis faire clignoter sa LED
cible = next(d for d in devices if d.name_of_station == "et200sp-2")
dcp.blink(cible.MAC)
print(f"Clignotement de {cible.name_of_station} ({cible.MAC})…")
$ python3 exo2.py
cpu1500        192.168.0.1     00:1b:1b:2a:10:01
et200sp-1      192.168.0.20    00:1b:1b:2a:10:20
et200sp-2      192.168.0.21    00:1b:1b:2a:10:21
Clignotement de et200sp-2 (00:1b:1b:2a:10:21)…

On récupère la MAC via identify_all() plutôt que de la coder en dur : c'est plus robuste si le device a été remplacé. blink() ne modifie rien dans le device (juste un voyant), donc c'est sûr même en production — contrairement à set_name_of_station.

Exercice 3 — arp-scan, OUI Siemens, et portée vs DCP

Lance un arp-scan sur ton segment, repère les équipements Siemens à leur OUI, puis explique en quoi la portée de arp-scan diffère de celle d'un DCP Identify All. Dans quel cas un device apparaît-il en DCP mais pas en arp-scan ?

Voir la solution
$ sudo arp-scan --interface=eth0 --localnet
192.168.0.1     00:1b:1b:2a:10:01   Siemens AG
192.168.0.20    00:1b:1b:2a:10:20   Siemens AG
192.168.0.240   00:1b:1b:2a:1f:40   Siemens AG
...

Les équipements dont la MAC commence par 00:1b:1b (ou 8c:f3:19, ac:64:17) sont des Siemens : la colonne constructeur, tirée de la base OUI, le dit directement.

Différence de portée :

  • arp-scan travaille en IP sur le subnet local : il envoie un ARP à chaque adresse du même sous-réseau que ton interface. Un device dont l'IP est dans un autre plan (par ex. 10.0.0.5 alors que tu es en 192.168.0.71/24) ne répondra pas à ton ARP : il est invisible pour arp-scan. De même, un device neuf sans IP configurée reste muet.
  • Le DCP Identify All est de la couche 2 pure (multicast Ethernet, sans IP). Il fait répondre tout device Profinet du segment, quelle que soit son IP — voire sans IP du tout. C'est justement pour ça qu'on l'utilise pour retrouver un device « perdu » sur un mauvais plan d'adressage : DCP le voit, arp-scan non.

Conclusion : un device Profinet mal adressé (IP hors subnet) ou vierge apparaît en DCP mais pas en arp-scan. Les deux sont complémentaires : DCP pour l'identité L2 exhaustive, arp-scan pour un inventaire IP rapide avec constructeur une fois le plan d'adressage connu.