Tubes nommés en pratique — l'IPC standard de Windows, de la conception à la sécurité

· · Windows, IPC, Développement Windows, C#, C++, Sécurité, Win32 API

« Je veux qu’un service résident et une interface de paramètres échangent des commandes. » « Je veux isoler uniquement le travail qui exige des privilèges administrateur dans un processus séparé. » « Je veux que des outils sur le même PC se transmettent des données. » — Lorsque ce type de communication inter-processus (IPC) devient nécessaire sous Windows, le standard à considérer en premier est un tube nommé.

L’article sur le choix de la communication inter-processus sous Windows positionnait les tubes nommés comme « le premier candidat pour l’IPC sur la même machine ». Cet article en est le traitement détaillé. Pourquoi ils sont le premier candidat, comment choisir les modes et les formes de serveur, et ce qu’il faut protéger lorsqu’un service privilégié les utilise — destiné aux développeurs qui écrivent des applications métier et des services sous Windows, il organise ce matériau, à partir des sources primaires, pour ces jugements de conception.

1. La conclusion, d’abord

  • Un tube nommé est un canal inter-processus bidirectionnel dont l’espace de noms a la forme \\.\pipe\name. Vous pouvez créer plusieurs instances sous le même nom et accepter plusieurs clients à la fois.1
  • La raison pour laquelle ils sont le premier candidat pour l’IPC sur la même machine est le modèle de sécurité. Vous pouvez contrôler qui se connecte avec une ACL, et le serveur peut inspecter et emprunter (usurper) le compte Windows du client. Le TCP localhost n’a ni l’un ni l’autre.2
  • Si vous voulez traiter « une écriture = un message », utilisez le mode message ; si vous avez déjà votre propre cadrage, utilisez le mode octet. Même en mode message, il faut encore traiter les lectures fractionnées sur un tampon trop court (ERROR_MORE_DATA).3
  • Gérez plusieurs clients avec « plusieurs instances + E/S overlapped » ou avec « async/await .NET ». L’exemple officiel montre une forme qui traite plusieurs instances sur un seul thread.4
  • Le minimum de sécurité tient en quatre points : refuser le distant (PIPE_REJECT_REMOTE_CLIENTS), rendre l’ACL explicite, détecter le détournement avec FILE_FLAG_FIRST_PIPE_INSTANCE, et minimiser le niveau d’emprunt d’identité côté client.56
  • Pour ImpersonateNamedPipeClient, vérifier la valeur de retour est la ligne de vie. Ignorer un échec et le traitement continue avec les privilèges du serveur.6

2. Qu’est-ce qu’un tube nommé — espace de noms, instances et fonctionnement des connexions

Un tube nommé est un canal identifié par un nom tel que \\.\pipe\MyCompany.MyApp.Control. Le serveur le crée avec CreateNamedPipe, et le client ouvre le même nom avec CreateFile. Une fois ouvert, les deux côtés lisent et écrivent avec ReadFile / WriteFile — le point distinctif est que vous pouvez l’utiliser sous la même forme que les E/S de fichiers.1

Le concept important est l’instance. Vous pouvez créer plusieurs instances d’un tube portant le même nom, et une instance est un canal avec un client. Le premier appel à CreateNamedPipe décide du nombre maximal d’instances (ou de l’illimité).3

La connexion côté client a une recette standard. Lorsque toutes les instances sont occupées, CreateFile échoue avec ERROR_PIPE_BUSY, donc vous attendez qu’une se libère avec WaitNamedPipe puis réessayez. De plus, l’accès spécifié à l’ouverture doit correspondre à la direction créée par le serveur — un tube bidirectionnel peut être ouvert avec lecture ou écriture, mais un tube sortant que le serveur n’écrit que doit être ouvert en lecture seule, et un tube entrant que le serveur ne lit que doit être ouvert en écriture seule, sinon CreateFile échoue.7

Direction du tube et accès spécifié par le clientUn client peut ouvrir un tube bidirectionnel en lecture ou en écriture, mais doit ouvrir un tube sortant que le serveur n'écrit que en lecture seule, et un tube entrant que le serveur ne lit que en écriture seuleBidirectionnelSortantEntrantDirection créée par le serveur ?Lecture ou écritureOuvrir en lecture seuleOuvrir en écriture seule

Figure 1 : un décalage entre direction et spécification d’accès devient un échec de CreateFile. En cas d’erreur de connexion, vérifier ici en premier.

Structure de base d'un tube nomméLe serveur crée plusieurs instances de tube du même nom et attend une connexion avec ConnectNamedPipe ; chaque client ouvre le nom avec CreateFile et dispose d'un canal bidirectionnel un-à-un avec une instanceServeurInstance 1Instance 2Instance 3Client AClient BClient C

Figure 2 : en détenant plusieurs instances du même nom, un serveur peut parler un-à-un avec plusieurs clients en même temps.

Les tubes nommés peuvent aussi être ouverts à distance via SMB (\\server\pipe\name), mais dans une conception moderne il n’y a presque aucune raison de l’utiliser activement ; le sujet est plutôt de ne pas le laisser ouvert quand on ne l’utilise pas (chapitre 5).

3. Mode octet et mode message

Un tube a deux modes de transfert.3

  • Mode octet (PIPE_TYPE_BYTE) : un « flux d’octets ininterrompu » comme TCP. Vous décidez vous-même où un message se termine (vous concevez un cadrage tel qu’un préfixe de longueur).
  • Mode message (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE) : une écriture est traitée comme un message, et le lecteur le reçoit dans cette unité. C’est plus simple pour les échanges requête/réponse.

Le mode message a aussi un compagnon commode, TransactNamedPipe, qui envoie une requête et reçoit la réponse en un seul appel.8 Il y a toutefois un piège. Si le tampon de réception est plus petit que le message entier, la lecture renvoie ERROR_MORE_DATA et devient une lecture fractionnée. Ne supposez pas que le mode message signifie « un Read ramène toujours le tout » ; il faut encore écrire une boucle qui lit le reste. Notez que le mode de lecture est un réglage par handle, et CreateNamedPipe ne le décide que côté serveur. Le client le spécifie avec SetNamedPipeHandleState après CreateFile (en .NET, ReadMode après la connexion).7

Boucle de lecture fractionnée en mode messageSi ReadFile réussit le message est complet ; s'il renvoie ERROR_MORE_DATA on lit le reste qui n'a pas tenu dans le tampon et on le concatène ; toute autre erreur est traitée comme une déconnexionSuccèsERROR_MORE_DATAToute autre erreurLire avec ReadFileRésultat ?Message completLire le reste et concaténerTraiter comme une déconnexion

Figure 3 : même en mode message, il faut une « boucle de lecture du reste » ; sans elle, seuls les gros messages cassent.

Différence entre mode octet et mode messageEn mode octet trois écritures deviennent un flux d'octets ininterrompu et le récepteur doit le découper ; en mode message l'unité de chaque écriture est préservée et arrive telle quelleMode octet : AAA, BB, CCCCReçu comme le flux AAABBCCCCVous concevez le cadrageMode message : les 3 mêmesReçu en 3 messages : AAA, BB, CCCCLes unités d'écriture restent

Figure 4 : le mode message préserve « l’unité d’une écriture » et la livre. La conception du cadrage devient inutile ; n’oubliez simplement pas de traiter les lectures fractionnées.

La règle pratique pour choisir est simple. Si l’échange a la forme « requête et réponse », mode message. Si vous transportez une forme qui a déjà un cadrage intégré (données sérialisées préfixées en longueur ou transfert de flux), utilisez le mode octet. En .NET, spécifier PipeTransmissionMode.Message correspond au premier cas.9

4. Conception du serveur — un thread par client, ou overlapped ?

L’opération de base du serveur est la boucle « créer une instance → attendre un client avec ConnectNamedPipe → lire et écrire → déconnecter et passer au client suivant ». Il y a deux formes pour parler à plusieurs clients à la fois.

Synchrone, un thread par instance. Vous assignez un thread à chaque instance, et chacune parle à son propre client en E/S synchrones. Le code est direct, mais vous consommez un thread par client, et il faut aussi un moyen de sortir des E/S bloquantes lors de l’arrêt global.

Overlapped (asynchrone). Vous créez les instances avec FILE_FLAG_OVERLAPPED, émettez ConnectNamedPipe / ReadFile / WriteFile de façon asynchrone, et un petit nombre de threads gère l’achèvement pour chaque instance. L’exemple officiel de Microsoft montre un serveur qui attend sur un tableau d’événements avec WaitForMultipleObjects et traite plusieurs instances sur un seul thread.4 Le récit général des E/S asynchrones est celui de l’article de la série I/O, et à plus grande échelle vous pouvez aussi attacher IOCP ou les E/S du pool de threads.

Structure d'un serveur overlappedLes achèvements des opérations asynchrones de chaque instance sont reçus sur un tableau d'événements, et un petit nombre de threads attend avec WaitForMultipleObjects et avance l'instance achevée, découplant le nombre de threads du nombre de clientsOpération async instance 1Tableau d'événementsOpération async instance 2Opération async instance 3Attendre avec WaitForMultipleObjectsAvancer l'instance achevée

Figure 5 : la forme overlapped découple le nombre de threads du nombre de clients. L’exemple officiel fait tourner ce cycle sur un seul thread.

.NET élimine presque ce choix. En utilisant WaitForConnectionAsync / ReadAsync / WriteAsync de NamedPipeServerStream avec async/await, vous obtenez l’efficacité overlapped dans un code aussi direct que la forme synchrone.9

// C#: skeleton of a server that accepts multiple clients
while (!token.IsCancellationRequested)
{
    var server = new NamedPipeServerStream(
        "MyCompany.MyApp.Control",
        PipeDirection.InOut,
        NamedPipeServerStream.MaxAllowedServerInstances,
        PipeTransmissionMode.Message,
        PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

    try
    {
        await server.WaitForConnectionAsync(token);
    }
    catch
    {
        await server.DisposeAsync();        // dispose yourself when leaving before a connection
        throw;
    }
    _ = HandleClientAsync(server, token);   // ownership after connect goes to the handler
}

PipeOptions.CurrentUserOnly est une spécification qui « n’autorise les connexions que depuis des processus du même utilisateur », un défaut commode et sûr qui vous évite d’écrire une ACL vous-même.10 Elle ne peut pas être utilisée dans une configuration qui traverse les utilisateurs (un service ↔ une application dans une session utilisateur, et analogues), donc dans ce cas vous passez à la conception d’ACL du chapitre suivant.

Boucle d'acceptation d'un serveur asynchrone .NETLa boucle d'acceptation crée un NamedPipeServerStream, attend une connexion avec WaitForConnectionAsync, et à l'arrivée détache le traitement du client de façon asynchrone puis revient immédiatement à l'acceptation suivante, de sorte que les connexions concurrentes sont gérées dans un code directCréer le flux serveurAttendre avec WaitForConnectionAsyncLa connexion arriveDétacher le traitement async

Figure 6 : la boucle d’acceptation s’en tient au cycle « attendre → détacher → suivant », et le traitement de chaque client avance en parallèle.

5. Sécurité — quatre obligations lorsqu’un service privilégié utilise des tubes

La plus grande raison pour laquelle les tubes nommés sont le premier candidat pour l’IPC sur la même machine est le modèle de sécurité, mais seulement si vous le configurez correctement. Surtout dans une conception de courtier du type « un service à privilèges administrateur + une application d’interface à bas privilèges », le tube est la frontière de privilège elle-même. Quatre points à figer.

(1) Refuser le distant. Qu’un tube destiné à de l’IPC local soit ouvrable depuis le réseau est, à lui seul, une surface d’attaque. Spécifiez PIPE_REJECT_REMOTE_CLIENTS sur CreateNamedPipe et les connexions de clients distants sont refusées automatiquement.5

(2) Rendre l’ACL explicite. Passez un descripteur de sécurité dans SECURITY_ATTRIBUTES et restreignez les utilisateurs et groupes autorisés à se connecter. Ne donnez pas GENERIC_WRITE au client — le droit FILE_CREATE_PIPE_INSTANCE qu’il inclut permettrait à un client autorisé de créer lui-même une instance serveur du même nom et de voler les connexions suivantes. Accordez lecture et écriture comme droits individuels, et ne passez pas le droit de création d’instance.11

(3) Empêcher le détournement de nom. Les noms de tubes vont au premier arrivé. Si un processus malveillant crée d’abord un tube du même nom et attend, les clients se connectent au faux serveur. Le serveur spécifie FILE_FLAG_FIRST_PIPE_INSTANCE lors de la création de la première instance, garantissant « je suis le premier », et si cela échoue il soupçonne un détournement et s’arrête. Ce drapeau n’est que pour la première instance qui revendique le nom ; le mettre sur la deuxième et les suivantes fait échouer la création.3

(4) Le client minimise le niveau d’emprunt d’identité au nécessaire. C’est une préparation au cas où le pair serait un faux serveur. Si le client spécifie **SECURITY_SQOS_PRESENT SECURITY_IDENTIFICATION** sur CreateFile, le serveur peut identifier le client mais ne peut pas emprunter ces privilèges et agir.2 C’est toutefois un compromis avec un flux d’emprunt d’identité — dans une conception de courtier où le serveur effectue un accès réel sous les privilèges du client, le niveau identification ne suffit pas pour que l’emprunt réussisse, et il faut autoriser SECURITY_IMPERSONATION. Cette autorisation est conditionnée à être sûr d’être connecté au véritable serveur. L’anti-détournement côté serveur n’est qu’un mécanisme qui le remarque via un échec de démarrage ; si le véritable service est absent et qu’un attaquant crée d’abord le tube du même nom, le client peut encore se connecter au faux serveur. Ne l’autorisez que lorsque vous pouvez confirmer le pair par un démarrage de service garanti ou une authentification mutuelle après connexion.

La vérification d’identité et l’emprunt de privilèges côté serveur, c’est ImpersonateNamedPipeClient. Appelez-le après avoir lu une requête depuis le tube et le thread appelant commence à s’exécuter dans le contexte de sécurité de l’émetteur du dernier message lu. Ouvrez un fichier avec les privilèges du client et le contrôle d’accès se fait contre le client — le mécanisme par lequel un service privilégié exécute « l’opération demandée, avec les privilèges du demandeur ».6 La condition absolue pour l’utiliser est de vérifier la valeur de retour. Continuer après un échec de l’emprunt et les opérations suivantes s’exécutent avec les privilèges élevés du serveur lui-même. La documentation officielle indique explicitement que « en cas d’échec, il ne faut pas exécuter la requête du client ». Avec RevertToSelf après le travail, les pratiques de l’article sur les jetons d’emprunt d’identité s’appliquent telles quelles.

Flux de traitement d'une requête qui utilise l'emprunt d'identitéLe serveur lit une requête depuis le tube, confirme que ImpersonateNamedPipeClient a réussi, puis effectue l'opération avec les privilèges du client et revient à son propre contexte avec RevertToSelf. Si l'emprunt échoue il refuse la requête sans l'exécuterServeurClientServeurClientEn cas d'échec, refuser sans exécuterEnvoyer une requêteLire la requêteImpersonateNamedPipeClientOpérer avec les privilèges du clientRevertToSelf pour restaurer le contexteRépondre avec le résultat

Figure 7 : confirmer que l’emprunt d’identité a réussi et un RevertToSelf fiable vont de pair. Continuer en cas d’échec et cela s’exécute avec les privilèges du serveur.

Quatre points qui protègent le tube d'un service privilégiéLe côté serveur durcit l'entrée par le refus du distant, une ACL explicite et une garantie de première instance ; le côté client spécifie le niveau d'emprunt d'identité minimal nécessaire afin qu'un faux serveur ne puisse pas emprunter les privilèges (le restreindre au niveau identification si la conception ne laisse pas le serveur emprunter les privilèges)Côté clientNiveau d'emprunt minimalCôté serveurPIPE_REJECT_REMOTE_CLIENTSRestreindre les connectés(ACL)FIRST_PIPE_INSTANCE(1re seule)Le tube comme frontière

Figure 8 : dans une conception où le tube est la frontière de privilège, implémentez les trois points côté serveur plus le point côté client comme un ensemble.

6. Pièges pratiques

Une course sur l’ordre de démarrage. Si un client vient se connecter avant que le serveur n’ait créé le tube, vous obtenez une erreur « le tube n’existe pas ». Le côté client intègre « n’existe pas → attendre un peu et réessayer ». Inversement, le principe côté serveur est de commencer à écouter avec ConnectNamedPipe avant que le client ne démarre.8

Flux de nouvelle tentative de connexion du clientOuvrir le tube avec CreateFile ; s'il n'existe pas attendre brièvement et réessayer ; si toutes les instances sont occupées (ERROR_PIPE_BUSY) attendre qu'une se libère avec WaitNamedPipe puis réessayer ; en cas de succès entrer en communicationSuccèsLe tube n'existe pasERROR_PIPE_BUSYOuvrir avec CreateFileRésultat ?Commencer à communiquerAttendre un peu(serveur non démarré)Attendre une instance libre

Figure 9 : le traitement de connexion du client distingue les deux types d’échec, « n’existe pas » et « plein », et les ramène tous deux à une nouvelle tentative.

Détecter une déconnexion. Lorsque le pair se termine, Read/Write échouent avec ERROR_BROKEN_PIPE et analogues. Ce n’est pas une anomalie ; c’est le quotidien de la communication. Le serveur détecte la déconnexion, fait DisconnectNamedPipe sur l’instance, et se prépare à la connexion suivante ; le client se reconnecte — l’idée de « reconnexion idempotente » décrite dans l’article sur la veille et la reprise s’applique ici aussi.

Hypothèses sur la taille des messages. En plus des lectures fractionnées du mode message (chapitre 3), si vous ne décidez pas dans le protocole « quelle est la taille maximale en octets d’un message », un pair malveillant (ou buggé) peut gaspiller votre mémoire avec un message énorme. Décider d’une borne supérieure, et déconnecter si elle est dépassée : c’est l’approche sûre.

L’achèvement de l’écriture et la réception par le pair sont deux choses différentes. Le succès de WriteFile ne signifie pas que l’application du pair a traité les données. Les opérations qui ont besoin de certitude s’appuient sur des conceptions telles que confirmer par un message de réponse, et inclure la correspondance requête/réponse dans le protocole.

7. Résumé

  • Les tubes nommés sont le premier candidat pour l’IPC sur la même machine. Les raisons sont la même facilité d’emploi que les E/S de fichiers, et l’intégration au modèle de sécurité Windows des ACL et de l’emprunt d’identité.
  • Le choix de mode est « mode message pour requête et réponse, mode octet si vous avez déjà votre propre cadrage ». Même en mode message, il faut encore traiter les lectures fractionnées (ERROR_MORE_DATA).
  • Plusieurs clients se gèrent par plusieurs instances + overlapped, ou async/await .NET. Pour un travail neuf, la forme asynchrone .NET est la plus directe.
  • Sur un tube qui est une frontière de privilège, prenez le refus du distant, une ACL explicite, FIRST_PIPE_INSTANCE (première instance seulement) et la minimisation du niveau d’emprunt d’identité côté client comme un ensemble.
  • Pour ImpersonateNamedPipeClient, vérifier la valeur de retour et RevertToSelf sont la ligne de vie.
  • Tissez le « quotidien de la communication » — ordre de démarrage, déconnexion, borne supérieure des messages, confirmation de réponse — dans la conception du protocole.

Les tubes nommés sont une API ancienne, mais pour l’usage « faire parler des processus sur la même machine tout en respectant les frontières de comptes Windows », ils restent l’outil le plus naturel. Les points de jugement de conception sont presque épuisés dans le périmètre de cet article. Ensuite, écrivez votre propre protocole sur une seule feuille avant de commencer à implémenter.

Articles connexes

Domaines de conseil associés

KomuraSoft LLC prend en charge la conception et l’implémentation qui impliquent la communication inter-processus — séparer un service d’une application d’interface, isoler les privilèges administrateur, et analogues —, le remplacement d’une IPC existante (mémoire partagée, socket maison, COM, etc.) par des tubes nommés, et les revues de sécurité de la communication par tube d’un service privilégié. Une consultation dès le rebond d’une conception de protocole est la bienvenue.

Références

  1. Microsoft Learn, Named Pipes. Sur le fait qu’un tube nommé est un canal unidirectionnel ou bidirectionnel entre un serveur de tube et un ou plusieurs clients de tube ; sur le fait que chaque instance partage le même nom tout en ayant des tampons et des handles indépendants ; et sur le fait d’être utilisable depuis des processus locaux et distants.  2

  2. Microsoft Learn, Impersonating a Named Pipe Client. Sur le fait que l’emprunt d’identité laisse le thread serveur opérer dans les privilèges du client ; sur le fait que le niveau d’emprunt par défaut est SecurityImpersonation ; et sur le fait que le client peut contrôler le niveau d’emprunt avec le drapeau SECURITY_SQOS_PRESENT au moment de CreateFile (SECURITY_IDENTIFICATION n’autorise que l’identification).  2

  3. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). Sur la direction du tube (entrant, sortant, bidirectionnel), le type octet et le type message (PIPE_TYPE_BYTE / PIPE_TYPE_MESSAGE) et le mode de lecture (PIPE_READMODE_MESSAGE), le nombre maximal d’instances (PIPE_UNLIMITED_INSTANCES), le mode asynchrone via FILE_FLAG_OVERLAPPED, une garantie de première instance via FILE_FLAG_FIRST_PIPE_INSTANCE, et le délai d’attente par défaut de WaitNamedPipe.  2 3 4

  4. Microsoft Learn, Named Pipe Server Using Overlapped I/O. Sur l’exemple officiel d’un serveur à un seul thread qui traite des connexions simultanées avec plusieurs clients via des opérations overlapped. Sur la forme qui attend la structure OVERLAPPED et l’événement de chaque instance avec WaitForMultipleObjects et avance la machine d’états de l’instance achevée, et sur la confirmation de l’achèvement des E/S en attente avec GetOverlappedResult.  2

  5. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). Sur les deux modes de client distant, PIPE_ACCEPT_REMOTE_CLIENTS (accepter les connexions distantes et les inspecter contre le descripteur de sécurité) et PIPE_REJECT_REMOTE_CLIENTS (refuser automatiquement les connexions de clients distants).  2

  6. Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). Sur le fait qu’un thread côté serveur commence l’emprunt d’identité dans le contexte de sécurité du client du dernier message lu depuis le tube ; sur le retour avec RevertToSelf après achèvement ; et sur le fait que continuer après un échec de l’emprunt provoque une exécution dans le contexte propre (privilégié) du processus serveur, donc la valeur de retour doit toujours être vérifiée et, en cas d’échec, la requête du client ne doit pas être exécutée.  2 3

  7. Microsoft Learn, Named Pipe Client. Sur le fait que le client ouvre le tube avec CreateFile ; sur ERROR_PIPE_BUSY lorsque toutes les instances sont occupées, en attendant qu’une se libère avec WaitNamedPipe ; et sur le fait que le handle ouvert est par défaut en lecture octet, bloquant et non overlapped, SetNamedPipeHandleState pouvant le passer en mode lecture message.  2

  8. Microsoft Learn, Named Pipe Operations. Sur les opérations overlapped via ReadFileEx / WriteFileEx, une lecture non consommatrice via PeekNamedPipe, TransactNamedPipe qui effectue l’envoi de requête et la réception de réponse en un appel sur un tube bidirectionnel de type message, et le fait qu’une lecture bloquante avant le démarrage du client puisse provoquer une course.  2

  9. Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). Sur la connexion et la lecture/écriture avec NamedPipeServerStream / NamedPipeClientStream, le transfert par unité de message via PipeTransmissionMode.Message, et la gestion de plusieurs clients avec des méthodes asynchrones.  2

  10. Microsoft Learn, PipeOptions Enum (System.IO.Pipes). Sur l’activation des E/S asynchrones avec Asynchronous, et sur le fait que CurrentUserOnly peut n’autoriser les connexions qu’avec des processus du même utilisateur (et du même niveau d’élévation). 

  11. Microsoft Learn, Named Pipe Security and Access Rights. Sur la composition des droits d’accès des tubes nommés ; sur le fait que GENERIC_WRITE inclut FILE_CREATE_PIPE_INSTANCE, de sorte que donner une écriture générique à un client autorise aussi à créer une instance serveur ; et sur le fait que lecture et écriture des données sont accordées comme droits d’accès individuels. 

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Comment choisir entre un tube nommé et TCP (un socket localhost) ?
Pour la communication inter-processus sur la même machine, un tube nommé est le premier candidat. La raison est le modèle de sécurité. Un tube peut contrôler « qui a le droit de se connecter » au niveau de l'OS avec un descripteur de sécurité Windows (ACL), et le serveur peut inspecter et emprunter le compte Windows du pair connecté avec ImpersonateNamedPipeClient. Cela contraste avec un port TCP localhost, auquel n'importe qui peut se connecter, de sorte qu'il faut établir qui est le pair avec sa propre authentification. En revanche, les options basées sur TCP sont avantageuses lorsque la communication distante est susceptible de se développer plus tard, lorsque l'on parle aussi à des processus sur d'autres OS, ou lorsque l'on veut réutiliser un protocole existant tel que gRPC. Ce jugement est également exposé dans l'article sur le choix de la communication inter-processus sous Windows.
Faut-il utiliser le mode octet ou le mode message ?
Si vous voulez traiter « une écriture = une unité de sens », le mode message (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE) est commode. Le récepteur peut lire dans les unités que l'émetteur a écrites, donc vous n'avez pas à gérer les frontières vous-même. Le mode octet est un « flux d'octets ininterrompu » comme TCP, et vous devez concevoir le cadrage vous-même — un préfixe de longueur, par exemple. Si vous transportez un protocole qui a déjà un cadrage (par exemple une forme sérialisée préfixée en longueur), le mode octet convient. Un point d'attention : même en mode message, si le tampon de réception est plus petit que le message, vous obtenez une lecture fractionnée (ERROR_MORE_DATA), donc il faut tout de même la traiter. De plus, le mode de lecture est un réglage par handle, et CreateNamedPipe ne le fixe que côté serveur. Le client doit spécifier PIPE_READMODE_MESSAGE avec SetNamedPipeHandleState après CreateFile. En .NET, le serveur spécifie PipeTransmissionMode.Message, et le client règle NamedPipeClientStream.ReadMode sur Message après la connexion.
Comment construire un serveur qui parle à plusieurs clients à la fois ?
Un tube nommé peut créer plusieurs instances sous le même nom, et une instance gère un client. Il y a deux formes. L'une est une conception synchrone qui assigne un thread par client ; l'implémentation est directe, mais elle consomme un thread par client. L'autre consiste à utiliser des E/S asynchrones avec FILE_FLAG_OVERLAPPED et à faire gérer ConnectNamedPipe, ReadFile et WriteFile de chaque instance par un petit nombre de threads ; l'exemple officiel de Microsoft montre aussi une implémentation qui traite plusieurs instances sur un seul thread. En .NET, vous pouvez écrire la forme asynchrone avec presque la même simplicité que la forme synchrone, en utilisant NamedPipeServerStream.WaitForConnectionAsync et async/await. Sauf raison particulière, la forme asynchrone .NET est celle que je recommande pour les nouvelles implémentations.
Quel est le minimum à faire pour la sécurité des tubes nommés ?
Quatre points. Premier : si les connexions distantes ne sont pas nécessaires, spécifier PIPE_REJECT_REMOTE_CLIENTS et refuser explicitement les connexions via le réseau. Deuxième : poser une ACL appropriée avec SECURITY_ATTRIBUTES et restreindre les utilisateurs et groupes qui peuvent se connecter (l'ACL par défaut est trop lâche pour certains usages). Troisième : spécifier FILE_FLAG_FIRST_PIPE_INSTANCE lors de la création de la première instance, afin de détecter le « détournement de nom » dans lequel un tube du même nom est créé en premier (ne pas mettre ce drapeau sur la deuxième instance et les suivantes). Quatrième : un client qui veut seulement que le serveur l'identifie doit spécifier SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION sur CreateFile, afin qu'un faux serveur ne puisse pas emprunter (usurper) ses privilèges. Dans une conception de courtier où le serveur effectue un accès réel sous les privilèges du client, l'emprunt d'identité doit être autorisé, donc on applique ou non cette restriction selon que la conception laisse le serveur emprunter les privilèges.
Y a-t-il des précautions à prendre avec ImpersonateNamedPipeClient ?
La plus importante est de vérifier la valeur de retour. Si vous continuez après un échec de l'emprunt d'identité, les opérations suivantes s'exécutent avec les privilèges propres (souvent élevés) du processus serveur, et des opérations qui n'auraient pas dû être permises au client passent. La documentation officielle indique aussi explicitement qu'en cas d'échec, il ne faut pas exécuter la requête du client. Il faut aussi ne l'appeler qu'après avoir lu quelque chose — l'emprunt d'identité s'effectue dans le contexte « du dernier message lu depuis le tube » — et revenir de façon fiable au contexte d'origine avec RevertToSelf une fois le travail terminé. Les mécanismes autour de l'emprunt d'identité (jetons, niveaux d'emprunt, SeImpersonatePrivilege) sont couverts en détail dans l'article sur les jetons d'emprunt d'identité.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog