Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1220 minDébutant

Python et le réseau : ipaddress, ping et socket

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • calculer et vérifier des réseaux avec ipaddress ;
  • trouver les hôtes qui répondent au ping, sous Linux et Windows ;
  • accélérer un balayage avec des threads ;
  • tester un port TCP et résoudre un nom DNS avec socket.

3 exercices corrigés1 schéma

Python sait calculer et tester un réseau avec sa seule bibliothèque standard : le module ipaddress manipule les adresses et les réseaux IPv4 et IPv6 sans erreur de calcul, subprocess lance la commande ping, socket teste un port TCP et résout un nom DNS. Ce chapitre vérifie le plan d'adressage de Sorbier Logistique, écrit un script qui trouve les hôtes du réseau des serveurs qui répondent au ping, l'accélère avec des fils d'exécution (threads), puis teste les services un par un. Les calculs de masque et de sous-réseau sont expliqués dans le cours Réseaux ; ici, Python les fait pour toi.

Le module ipaddress

python
>>> import ipaddress
>>> serveurs = ipaddress.ip_network("192.168.10.176/28")
>>> serveurs.netmask, serveurs.broadcast_address
(IPv4Address('255.255.255.240'), IPv4Address('192.168.10.191'))
>>> serveurs.num_addresses
16
>>> hotes = list(serveurs.hosts())
>>> len(hotes), hotes[0], hotes[-1]
(14, IPv4Address('192.168.10.177'), IPv4Address('192.168.10.190'))
>>> ipaddress.ip_address("192.168.10.183") in serveurs
True
>>> ipaddress.ip_address("192.168.10.200") in serveurs
False
>>> ipaddress.ip_address("8.8.8.8").is_private
False
>>> interface = ipaddress.ip_interface("192.168.21.186/24")
>>> interface.network
IPv4Network('192.168.21.0/24')
>>> [str(s) for s in ipaddress.ip_network("192.168.21.0/24").subnets(new_prefix=26)]
['192.168.21.0/26', '192.168.21.64/26', '192.168.21.128/26', '192.168.21.192/26']
>>> ipaddress.ip_network("192.168.10.177/28")
Traceback (most recent call last):
  File "<python-input-12>", line 1, in <module>
    ipaddress.ip_network("192.168.10.177/28")
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.15/ipaddress.py", line 71, in ip_network
    return IPv4Network(address, strict)
  File "/usr/local/lib/python3.15/ipaddress.py", line 1541, in __init__
    raise ValueError('%s has host bits set' % self)
ValueError: 192.168.10.177/28 has host bits set
>>> ipaddress.ip_address("192.168.10.300")
Traceback (most recent call last):
  File "<python-input-13>", line 1, in <module>
    ipaddress.ip_address("192.168.10.300")
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.15/ipaddress.py", line 51, in ip_address
    raise ValueError(f'{address!r} does not appear to be an IPv4 or IPv6 address')
ValueError: '192.168.10.300' does not appear to be an IPv4 or IPv6 address
Objet ou attributRôle
ip_address("192.168.10.183")Une adresse ; elle se compare, s'incrémente (ip + 1) et se teste (is_private, is_loopback)
ip_network("192.168.10.176/28")Un réseau, donné par son adresse de réseau
reseau.netmask, reseau.broadcast_addressLe masque, l'adresse de diffusion
reseau.num_addressesLe nombre total d'adresses, réseau et diffusion comprises
reseau.hosts()Les adresses utilisables par des hôtes, une par une
ip in reseauL'adresse appartient-elle au réseau ?
reseau.subnets(new_prefix=26)Le découpage en sous-réseaux plus petits
reseau.overlaps(autre)Les deux réseaux se chevauchent-ils ?
ip_interface("192.168.21.186/24")Une adresse avec son masque, comme sur une carte réseau ; .network donne son réseau

Le module refuse ce qui est faux : 192.168.10.177/28 n'est pas une adresse de réseau (des bits d'hôte sont à 1), et 192.168.10.300 n'est pas une adresse. Une saisie se valide donc en une ligne, dans un try qui intercepte ValueError. Tout fonctionne de la même façon en IPv6 (ip_network("2001:db8:10::/64")).

Vérifier un plan d'adressage

Le plan d'adressage des VLAN de Sorbier est décrit dans le cours pfSense : chaque VLAN a son réseau et sa passerelle, portée par le pare-feu, à la dernière adresse utilisable. Un script vérifie ces deux règles et l'absence de chevauchement entre deux VLAN, l'erreur qui rend un réseau incompréhensible :

python
# plan.py : vérifier le plan d'adressage des VLAN de Sorbier Logistique
import ipaddress
from itertools import combinations

PLAN = {
    "10 POSTES": ("192.168.10.64/26", "192.168.10.126"),
    "20 SERVEURS": ("192.168.10.176/28", "192.168.10.190"),
    "21 APPLIS": ("192.168.21.0/24", "192.168.21.254"),
    "30 ENTREPOT": ("192.168.10.128/27", "192.168.10.158"),
    "40 VISITEURS": ("192.168.10.0/26", "192.168.10.62"),
    "50 TELEPHONIE": ("192.168.10.160/28", "192.168.10.174"),
    "60 DMZ": ("192.168.10.224/28", "192.168.10.238"),
    "99 ADMIN": ("192.168.10.208/28", "192.168.10.222"),
}

reseaux = {}
for vlan, (cidr, passerelle) in PLAN.items():
    reseau = ipaddress.ip_network(cidr)
    gw = ipaddress.ip_address(passerelle)
    derniere = list(reseau.hosts())[-1]
    remarque = "" if gw == derniere else f"  ATTENTION : passerelle {gw}, dernière adresse {derniere}"
    print(f"VLAN {vlan:<14}{str(reseau):<19}{reseau.num_addresses - 2:>4} hôtes{remarque}")
    reseaux[vlan] = reseau

for (v1, r1), (v2, r2) in combinations(reseaux.items(), 2):
    if r1.overlaps(r2):
        print(f"CHEVAUCHEMENT : VLAN {v1} et VLAN {v2}")
print("Contrôle des chevauchements terminé")
text
VLAN 10 POSTES     192.168.10.64/26     62 hôtes
VLAN 20 SERVEURS   192.168.10.176/28    14 hôtes
VLAN 21 APPLIS     192.168.21.0/24     254 hôtes
VLAN 30 ENTREPOT   192.168.10.128/27    30 hôtes
VLAN 40 VISITEURS  192.168.10.0/26      62 hôtes
VLAN 50 TELEPHONIE 192.168.10.160/28    14 hôtes
VLAN 60 DMZ        192.168.10.224/28    14 hôtes
VLAN 99 ADMIN      192.168.10.208/28    14 hôtes
Contrôle des chevauchements terminé

combinations(reseaux.items(), 2), du module itertools, donne chaque paire de VLAN une seule fois : 28 paires pour 8 VLAN. Le plan est cohérent : aucune remarque, aucun chevauchement. Change une passerelle ou un masque dans le dictionnaire pour voir le script réagir.

Quels hôtes répondent au ping ?

Python n'a pas de fonction ping toute faite : envoyer un paquet ICMP demande de le construire soi-même avec un socket « brut », ce qui exige en général les droits d'administrateur. Le plus simple est donc de lancer la commande ping du système avec subprocess, une fois par adresse, et de regarder son code de retour. Ses options diffèrent selon le système :

SystèmeUn seul paquetDélai d'attente
Linux-c 1-W 1 (en secondes)
Windows-n 1-w 1000 (en millisecondes)
python
#!/usr/bin/env python3
"""Teste quels hôtes d'un réseau répondent au ping."""
import argparse
import ipaddress
import platform
import subprocess
from concurrent.futures import ThreadPoolExecutor

WINDOWS = platform.system() == "Windows"


def hote_repond(ip, delai=1.0):
    """Envoie un seul ping à ip et renvoie True si une réponse arrive dans le délai."""
    if WINDOWS:
        commande = ["ping", "-n", "1", "-w", str(int(delai * 1000)), str(ip)]
    else:
        commande = ["ping", "-c", "1", "-W", str(delai), str(ip)]
    try:
        resultat = subprocess.run(commande, capture_output=True, timeout=delai + 2)
    except subprocess.TimeoutExpired:
        return False
    if WINDOWS:
        # Sous Windows, « Impossible de joindre l'hôte » renvoie aussi le code 0 :
        # seule une vraie réponse d'écho contient « TTL= ».
        return resultat.returncode == 0 and b"TTL=" in resultat.stdout
    return resultat.returncode == 0


def balayer(reseau, delai=1.0, fils=32):
    """Renvoie la liste des (adresse, répond) pour chaque hôte du réseau, dans l'ordre."""
    hotes = list(ipaddress.ip_network(reseau).hosts())
    with ThreadPoolExecutor(max_workers=fils) as executeur:
        reponses = executeur.map(lambda ip: hote_repond(ip, delai), hotes)
        return list(zip(hotes, reponses))


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("reseau", help="réseau à tester, par exemple 192.168.10.176/28")
    parser.add_argument("-d", "--delai", type=float, default=1.0, metavar="S",
                        help="attente maximale par hôte, en secondes (1 par défaut)")
    parser.add_argument("--actifs", action="store_true", help="n'affiche que les hôtes qui répondent")
    args = parser.parse_args()

    resultats = balayer(args.reseau, args.delai)
    for ip, repond in resultats:
        if repond or not args.actifs:
            print(f"{str(ip):<16}{'répond' if repond else '-'}")
    actifs = sum(1 for _, repond in resultats if repond)
    print(f"{actifs} hôte(s) sur {len(resultats)} répondent au ping")


if __name__ == "__main__":
    main()

Lancé depuis le poste d'administration (192.168.10.209, VLAN 99 ADMIN) sur le réseau des serveurs :

bash
python balayage.py 192.168.10.176/28
text
192.168.10.177  répond
192.168.10.178  répond
192.168.10.179  répond
192.168.10.180  répond
192.168.10.181  répond
192.168.10.182  répond
192.168.10.183  répond
192.168.10.184  répond
192.168.10.185  répond
192.168.10.186  répond
192.168.10.187  -
192.168.10.188  répond
192.168.10.189  répond
192.168.10.190  répond
13 hôte(s) sur 14 répondent au ping
192.168.10.176/28, réseau des serveurs (VLAN 20)les 14 hôtes testés, de .177 à .190176177178179180181182183184185186187188189190191réseaulibrediffusionrépond au ping (13)ne répond pas (1)non testé (réseau, diffusion)177 srv-ad · 178 srv-fichiers · 179 srv-gestion · 180 srv-journaux · 181 srv-ad2182 srv-collecte · 183 srv-nas · 184 srv-glpi · 185 srv-partages · 186 srv-deploiement188 pf1 · 189 pf2 · 190 passerelle, adresse virtuelle des deux pare-feu
Le balayage du réseau des serveurs : 14 hôtes testés, 13 répondent

Treize machines sur quatorze répondent. Seule .187, la dernière adresse libre du VLAN 20, reste muette : c'est pour cela que les nouvelles machines vont désormais dans le VLAN 21 APPLIS.

Trois points du script méritent l'attention.

  • Windows et le code de retour. Sous Windows, ping renvoie parfois le code 0 quand le routeur répond « Impossible de joindre l'hôte de destination » à la place de la machine. Seule une vraie réponse d'écho contient TTL=, dans toutes les langues de Windows : le script la cherche dans la sortie, lue en octets (b"TTL="), ce qui évite tout problème d'encodage de la console.
  • Les threads. Un hôte éteint fait attendre le délai complet : 14 hôtes testés un par un, c'est jusqu'à 14 secondes, et un /24 de 254 hôtes plus de 4 minutes. ThreadPoolExecutor, du module concurrent.futures, lance jusqu'à 32 pings en même temps ; executeur.map() applique la fonction à chaque adresse et rend les résultats dans l'ordre des adresses. Le /24 se teste alors en une dizaine de secondes.
  • Une réponse absente ne prouve rien. Le pare-feu de Windows peut bloquer l'écho ICMP entrant (la règle « Partage de fichiers et d'imprimantes (Demande d'écho - Trafic entrant ICMPv4) » n'est pas toujours activée), et un pare-feu entre deux VLAN peut filtrer ICMP. Une machine qui ne répond pas au ping peut très bien fonctionner : vérifie un port TCP qu'elle doit ouvrir.

Tester un port TCP avec socket

Un socket est l'extrémité d'une connexion réseau. socket.create_connection((hote, port), timeout) tente d'établir une connexion TCP : si elle s'établit, un service écoute sur ce port ; sinon, elle est refusée (rien n'écoute), ou le délai expire (un pare-feu filtre, ou la machine est éteinte). C'est ce que fait Test-NetConnection en PowerShell ou nc -zv sous Linux (les ports TCP et UDP).

python
# ports.py : tester si un service TCP accepte les connexions
import socket


def port_ouvert(hote, port, delai=2.0):
    """Renvoie True si une connexion TCP vers hote:port s'établit dans le délai."""
    try:
        with socket.create_connection((hote, port), timeout=delai):
            return True
    except OSError:          # refusée, délai dépassé, nom inconnu, réseau injoignable
        return False


SERVICES = [("127.0.0.1", 8000, "web de test"), ("127.0.0.1", 3306, "MySQL"), ("127.0.0.1", 22, "SSH")]
for hote, port, nom in SERVICES:
    etat = "ouvert" if port_ouvert(hote, port) else "fermé ou filtré"
    print(f"{hote}:{port:<6}{nom:<13}{etat}")

Pour l'essayer sur ta machine, lance dans un autre terminal le petit serveur web de Python, python -m http.server 8000 --bind 127.0.0.1, puis le script :

text
127.0.0.1:8000  web de test  ouvert
127.0.0.1:3306  MySQL        fermé ou filtré
127.0.0.1:22    SSH          fermé ou filtré

Sur le labo, la liste SERVICES contiendrait les services attendus : srv-ad sur les ports 53 (DNS) et 389 (LDAP), srv-web sur le port 443, srv-glpi sur le port 443. Le bloc with ferme la connexion aussitôt établie : le test ne fait qu'ouvrir et refermer, sans rien envoyer.

Résoudre un nom DNS

python
# dns.py : résoudre des noms avec le résolveur du système
import socket

for nom in ["localhost", "srv-inconnu.sorbier.example"]:
    try:
        print(nom, "->", socket.gethostbyname(nom))
    except socket.gaierror:
        print(nom, "-> nom introuvable")
text
localhost -> 127.0.0.1
srv-inconnu.sorbier.example -> nom introuvable

socket.gethostbyname() passe par le résolveur du système, comme n'importe quel programme : le fichier hosts, puis les serveurs DNS configurés sur la carte réseau. Sur un poste du domaine de Sorbier, srv-ad.ad.sorbier.example donnerait 192.168.10.177, résolu par le DNS de l'Active Directory. Un nom inconnu lève socket.gaierror. Pour interroger un serveur DNS précis ou lire d'autres enregistrements (MX, SRV, TXT), il faut un paquet tiers comme dnspython, ou les outils nslookup et dig (dépanner DNS et DHCP).

Exercices

Exercice 1 sur 3 Le réseau des postes Réussi À revoir

Avec ipaddress, réponds pour le VLAN 10 POSTES, 192.168.10.64/26 : masque, adresse de diffusion, nombre d'hôtes, première et dernière adresse utilisables. La passerelle 192.168.10.126 est-elle bien la dernière adresse utilisable ? Le terminal de scan 192.168.10.130 est-il dans ce réseau ?

Voir la réponseMasquer la réponse de l'exercice 1
python
>>> import ipaddress
>>> postes = ipaddress.ip_network("192.168.10.64/26")
>>> postes.netmask, postes.broadcast_address
(IPv4Address('255.255.255.192'), IPv4Address('192.168.10.127'))
>>> postes.num_addresses - 2
62
>>> hotes = list(postes.hosts())
>>> hotes[0], hotes[-1]
(IPv4Address('192.168.10.65'), IPv4Address('192.168.10.126'))
>>> ipaddress.ip_address("192.168.10.126") == hotes[-1]
True
>>> ipaddress.ip_address("192.168.10.130") in postes
False

Le masque /26 laisse 6 bits d'hôte : 2⁶ = 64 adresses, dont 62 utilisables, de .65 à .126. La passerelle est bien la dernière. .130 appartient au VLAN 30 ENTREPOT (192.168.10.128/27) : un terminal de scan, c'est logique.

Tu avais trouvé ?

Exercice 2 sur 3 Une adresse libre dans le VLAN 21 Réussi À revoir

Dans le VLAN 21 APPLIS (192.168.21.0/24, passerelle .254), les adresses .10, .20, .21, .22, .23, .182 et .186 sont prises. Écris une fonction adresses_libres(reseau, utilisees, reservees, nombre) qui renvoie les nombre premières adresses libres, et affiche les cinq premières, puis le nombre total d'adresses libres.

Voir la réponseMasquer la réponse de l'exercice 2
python
# ex_libres.py : les premières adresses libres du VLAN 21 APPLIS
import ipaddress

reseau = ipaddress.ip_network("192.168.21.0/24")
passerelle = ipaddress.ip_address("192.168.21.254")
utilisees = {ipaddress.ip_address(f"192.168.21.{n}") for n in [10, 20, 21, 22, 23, 182, 186]}


def adresses_libres(reseau, utilisees, reservees, nombre):
    libres = []
    for ip in reseau.hosts():
        if ip not in utilisees and ip not in reservees:
            libres.append(ip)
            if len(libres) == nombre:
                break
    return libres


print([str(ip) for ip in adresses_libres(reseau, utilisees, {passerelle}, 5)])
print(len(list(reseau.hosts())) - len(utilisees) - 1, "adresses libres en tout")
text
['192.168.21.1', '192.168.21.2', '192.168.21.3', '192.168.21.4', '192.168.21.5']
246 adresses libres en tout

Les adresses sont comparées comme objets IPv4Address, pas comme chaînes : "192.168.21.5" < "192.168.21.20" est faux en texte, vrai en adresses. 254 hôtes, moins 7 adresses prises et la passerelle : 246. Une adresse « libre » dans le fichier peut être occupée en réalité : avant de l'attribuer, vérifie qu'elle ne répond pas au ping et qu'elle n'est pas dans une étendue DHCP.

Tu avais trouvé ?

Exercice 3 sur 3 Ping ou port ? Réussi À revoir

Le script de balayage dit que srv-ad2 (192.168.10.181) ne répond plus au ping depuis la mise à jour de la veille, mais les utilisateurs s'authentifient normalement. Propose une explication et une vérification en Python.

Voir la réponseMasquer la réponse de l'exercice 3

La mise à jour a probablement réinitialisé une règle du pare-feu de Windows : l'écho ICMP entrant est de nouveau bloqué. Le serveur fonctionne, il ne répond simplement plus au ping. On vérifie un service qu'il doit rendre, avec la fonction port_ouvert() du chapitre : port_ouvert("192.168.10.181", 389) pour LDAP, ou port_ouvert("192.168.10.181", 53) pour le DNS. Si ces ports répondent, le serveur est vivant, et la règle ICMP se rétablit par GPO pour ne plus dépendre d'une mise à jour. La supervision (Zabbix) teste d'ailleurs les services, pas seulement le ping (cours Supervision).

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Appeler une API REST en Python : urllib et requestsRevoir le sommaire du cours