Scanner de ports TCP

Nous allons réaliser un scanner de ports TCP. Celui-ci tentera de tester des ports sur des machines cibles et de déterminer s'ils sont ouverts ou non, et quel logiciel s'exécute derrière.

Le code du scanner se trouvera dans le module scanner de notre programme et dans ses sous-modules.

  1. Créez le module scanner. Étant donné qu'il aura des sous-modules, créez le dans le fichier src/scanner/mod.rs plutôt que dans src/scanner.rs afin de regrouper le module et ses sous-modules dans le même dossier.
  2. Créez un module scanner::net dans le fichier src/scanner/net.rs. Nous y mettrons le code spécifique aux interactions réseau.

Utilisation du type TcpStream

La bibliothèque standard de Rust fournit un type std::net::TcpStream qui représente un flux TCP connecté entrant ou sortant. Le crate tokio fournit un type similaire, tokio::net::TcpStream, dont l'interface est proche mais qui a comme caractéristique d'être entièrement asynchrone, c'est-à-dire que toutes les opérations retournent immédiatement, en général à travers un type implémentant Future ou Stream.

Le type tokio::net::TcpStream implémente les deux traits tokio::io::AsyncReadExt et tokio::io::AsyncWriteExt qui fournissent de nombreuses méthodes utilitaires telles que read_to_string() ou write_all(). Il est également possible de séparer le stream en deux parties, l'une en lecture et l'autre en écriture, pour les utiliser indépendamment.

  1. Implémentez une fonction asynchrone scanner::net::tcp_ping() qui renverra s'il est possible de se connecter à un port donné :
pub async fn tcp_ping(host: &str, port: u16) -> bool {
  todo!()
}
  1. Après avoir mis les droits nécessaires pour permettre à main() d'atteindre cette fonction, rajoutez une sous-commande ping qui prend un hôte et un numéro de port sur la ligne de commande et qui indique si le port est ouvert ou non, en fonction de si le TcpStream parvient ou non à se connecter.

On notera que TcpStream::connect() prend de nombreux formats d'arguments en entrée dont un couple (host, port).

$ cargo run -- ping 127.0.0.1 22
127.0.0.1:22 is open
$ cargo run -- ping 127.0.0.1 23
127.0.0.1:23 is closed
$ cargo run -- ping google.com 80
google.com:80 is open

Détection d'adresses non résolues

Cependant, notre code a un problème : l'impossibilité de se connecter n'est pas la seule cause d'erreur, il se peut que la cible ne soit pas trouvée.

$ cargo run -- ping google.kfdslkfj 80
google.kfdslkfj:80 is closed

Il serait légitime ici de signaler une erreur. Nous pouvons utiliser la fonction asynchrone tokio::net::lookup_host() qui, avec les mêmes arguments que TcpStream::connect(), renvoie les adresses à utiliser pour l'hôte recherché ou une erreur si elle n'en trouve pas. En cas d'erreur, elle retourne une erreur de type std::io::Error que nous pouvons encapsuler dans notre type error::Error.

  1. Modifiez la signature de la fonction scanner::net::tcp_ping() et son appel dans main() pour qu'elle renvoie une erreur si l'adresse sélectionnée n'existe pas :
pub async fn tcp_ping(host: &str, port: u16) -> Result<bool, Error> {
  todo!()
}

On utilisera avec profit l'idiomatique construction .await? qui combine .await pour attendre le résultat d'une Future et ? pour faire remonter de manière prématurée une erreur avec un appel implicite à .into() vers le type d'erreur attendu au niveau supérieur.

Cela donnera maintenant :

$ cargo run -- ping google.kfdslkfj 80
Error: IoError(Custom { kind: Uncategorized, error: "failed to lookup address information: Name or service not known" })

Bien entendu, rien n'empêche d'être plus agréable dans l'affichage du message d'erreur en remplaçant le await? dans main() par un match un peu plus respectueux de l'utilisateur, afin d'obtenir par exemple un message plus explicite si le type d'erreur encapsulée est std::io::Error dont il est connu que les descriptions sont pertinentes :

$ cargo run -- ping google.kfdslkfj 80
google.kfdslkfj: failed to lookup address information: Name or service not known

Timeout

L'utilisation d'une adresse non routable peut mener à des temps d'attente excessifs :

$ cargo run -- ping 10.101.102.103 80
[attente interminable]

Le crate tokio fournit une fonction utilitaire tokio::time::timeout qui enveloppe une Future dans un délai. Le résultat arrive enveloppé dans un Result, avec une erreur signalée en cas de dépassement du temps maximal.

  1. Modifiez le code de scanner::net::tcp_ping() pour qu'un délai maximal de 3 secondes soit utilisé. Une nouvelle valeur de l'énumération error::Error::Timeout permettra de représenter cette erreur spécifique.

Si vous avez modifié le code de main() pour représenter plus clairement les erreurs de résolution de nom, faîtes de même en cas de timeout.

$ cargo run -- ping 10.101.102.103 80
10.101.102.103:80 timed out