Canalizaciones con nombre en la práctica — El IPC estándar de Windows, del diseño a la seguridad

· · Windows, IPC, Desarrollo en Windows, C#, C++, Seguridad, Win32 API

«Quiero que un servicio residente y una interfaz de configuración intercambien comandos.» «Quiero aislar en un proceso distinto solo el trabajo que necesita privilegios de administrador.» «Quiero que las herramientas del mismo PC se pasen datos entre sí.» — Cuando en Windows hace falta este tipo de comunicación entre procesos (IPC), el estándar que conviene considerar primero es una canalización con nombre.

El artículo sobre cómo elegir la comunicación entre procesos en Windows situó las canalizaciones con nombre como «la primera candidata para la IPC en la misma máquina». Este artículo es el tratamiento detallado. Por qué son la primera candidata, cómo se eligen los modos y las formas de servidor, y qué hay que proteger cuando las usa un servicio privilegiado —dirigido a desarrolladores que escriben aplicaciones empresariales y servicios en Windows, organiza el material para esos juicios de diseño a partir de fuentes primarias.

1. Conclusión principal

  • Una canalización con nombre es un canal bidireccional entre procesos con un espacio de nombres de la forma \\.\pipe\name. Se pueden crear varias instancias bajo el mismo nombre y aceptar varios clientes a la vez.1
  • La razón de que sean la primera candidata para la IPC en la misma máquina es el modelo de seguridad. Se puede controlar quién se conecta con un ACL, y el servidor puede inspeccionar y tomar prestada (suplantar) la cuenta de Windows del cliente. El TCP localhost no tiene ninguna de las dos cosas.2
  • Si se quiere tratar «una escritura = un mensaje», use el modo mensaje; si ya tiene su propio framing, use el modo byte. Incluso en modo mensaje hay que tratar las lecturas partidas cuando el búfer es corto (ERROR_MORE_DATA).3
  • Atienda a varios clientes con «varias instancias + E/S superpuesta» o con «async/await de .NET». El ejemplo oficial muestra una forma que procesa varias instancias en un solo hilo.4
  • El mínimo de seguridad son cuatro puntos: rechazar lo remoto (PIPE_REJECT_REMOTE_CLIENTS), hacer explícito el ACL, detectar el secuestro con FILE_FLAG_FIRST_PIPE_INSTANCE y minimizar el nivel de suplantación en el lado cliente.56
  • Para ImpersonateNamedPipeClient, comprobar el valor de retorno es el salvavidas. Si se ignora un fallo, el procesamiento sigue con los privilegios del servidor.6

2. Qué es una canalización con nombre — Espacio de nombres, instancias y cómo funcionan las conexiones

Una canalización con nombre es un canal identificado por un nombre como \\.\pipe\MyCompany.MyApp.Control. El servidor la crea con CreateNamedPipe y el cliente abre el mismo nombre con CreateFile. Una vez abierta, ambos lados leen y escriben con ReadFile / WriteFile: lo distintivo es que se puede usar con la misma forma que la E/S de archivos.1

El concepto importante es la instancia. Se pueden crear varias instancias de una canalización con el mismo nombre, y una instancia es un canal con un cliente. La primera llamada a CreateNamedPipe decide el número máximo de instancias (o ilimitado).3

La conexión en el lado cliente tiene una receta estándar. Cuando todas las instancias están en uso, CreateFile falla con ERROR_PIPE_BUSY, así que se espera a que haya una libre con WaitNamedPipe y se reintenta. Además, el acceso que se indica al abrir tiene que coincidir con la dirección que creó el servidor: una canalización bidireccional se puede abrir indicando lectura o escritura, pero una canalización de salida que el servidor solo escribe debe abrirse en solo lectura, y una de entrada que el servidor solo lee debe abrirse en solo escritura, o CreateFile falla.7

Dirección de la canalización y acceso del clienteUn cliente puede abrir una canalización bidireccional indicando lectura o escritura, pero debe abrir como solo lectura una de salida que el servidor solo escribe, y como solo escritura una de entrada que el servidor solo leeBidireccionalSalidaEntrada¿Dirección que creó el servidor?Vale lectura o escrituraAbrir en solo lecturaAbrir en solo escritura

Figura 1: Un desajuste entre dirección y especificación de acceso se convierte en un fallo de CreateFile. Al investigar un error de conexión, compruebe esto primero.

Estructura básica de una canalización con nombreEl servidor crea varias instancias de canalización con el mismo nombre y espera una conexión con ConnectNamedPipe; cada cliente abre el nombre con CreateFile y tiene un canal bidireccional uno a uno con una instanciaServidorInstancia 1Instancia 2Instancia 3Cliente ACliente BCliente C

Figura 2: Al mantener varias instancias del mismo nombre, un servidor puede hablar uno a uno con varios clientes al mismo tiempo.

Las canalizaciones con nombre también se pueden abrir en remoto por SMB (\\server\pipe\name), pero en un diseño moderno casi no hay motivo para usarlo de forma activa; el problema es más bien no dejarlo abierto cuando no se usa (capítulo 5).

3. Modo byte y modo mensaje

Una canalización tiene dos modos de transferencia.3

  • Modo byte (PIPE_TYPE_BYTE): un «flujo de bytes ininterrumpido» como TCP. Uno decide por sí mismo dónde termina un mensaje (se diseña el framing, por ejemplo un prefijo de longitud).
  • Modo mensaje (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE): una escritura se trata como un mensaje, y el lector lo recibe en esa unidad. Esto es más fácil para intercambios de petición/respuesta.

El modo mensaje también tiene un compañero cómodo, TransactNamedPipe, que envía una petición y recibe la respuesta en una sola llamada.8 Hay, no obstante, una trampa. Si el búfer de recepción es más pequeño que el mensaje completo, la lectura devuelve ERROR_MORE_DATA y se convierte en una lectura partida. No asuma que el modo mensaje significa «un Read siempre trae el todo»; sigue haciendo falta un bucle que lea el resto. Tenga en cuenta que el modo de lectura es un ajuste por identificador, y CreateNamedPipe lo decide solo en el lado servidor. El cliente lo indica con SetNamedPipeHandleState después de CreateFile (en .NET, ReadMode después de conectar).7

Bucle de lectura partida en modo mensajeSi ReadFile tiene éxito el mensaje está completo; si devuelve ERROR_MORE_DATA se lee el resto que no cupo en el búfer y se concatena; cualquier otro error se trata como una desconexiónÉxitoERROR_MORE_DATACualquier otro errorLeer con ReadFile¿Resultado?Mensaje completoLeer el resto y concatenarTratar como desconexión

Figura 3: Incluso en modo mensaje hace falta un «bucle de leer el resto»; sin él, solo se rompen los mensajes grandes.

Diferencia entre modo byte y modo mensajeEn modo byte tres escrituras se convierten en un flujo de bytes ininterrumpido y el receptor tiene que partirlo; en modo mensaje se conserva la unidad de cada escritura y llega al receptor tal cualModo byte: escribe AAA, BB, CCCCSe recibe el flujo AAABBCCCCEl framing lo diseña ustedModo mensaje: las mismas tres escriturasSe reciben tres mensajes: AAA, BB, CCCCSe conservan las unidades de escritura

Figura 4: El modo mensaje conserva «la unidad de una escritura» y la entrega. El diseño de framing deja de ser necesario; solo no olvide tratar las lecturas partidas.

La regla práctica para elegir es sencilla. Si el intercambio tiene forma de «petición y respuesta», modo mensaje. Si se transporta una forma que ya lleva el framing incorporado (datos serializados con prefijo de longitud o una transferencia en flujo), use el modo byte. En .NET, indicar PipeTransmissionMode.Message corresponde a lo primero.9

4. Diseño del servidor — ¿Un hilo por cliente, o superpuesto?

La operación básica del servidor es el bucle «crear una instancia → esperar a un cliente con ConnectNamedPipe → leer y escribir → desconectar e ir al siguiente cliente». Hay dos formas de hablar con varios clientes a la vez.

Síncrono, un hilo por instancia. Se asigna un hilo a cada instancia, y cada uno habla con su cliente con E/S síncrona. El código es directo, pero se consume un hilo por cliente, y además hace falta una forma de salir de la E/S bloqueante al apagar el conjunto.

Superpuesto (asíncrono). Se crean instancias con FILE_FLAG_OVERLAPPED, se emiten ConnectNamedPipe / ReadFile / WriteFile de forma asíncrona, y un número reducido de hilos gestiona la finalización de todas las instancias. El ejemplo oficial de Microsoft muestra un servidor que espera en una matriz de eventos con WaitForMultipleObjects y procesa varias instancias en un solo hilo.4 El relato general de la E/S asíncrona es el de el artículo de la serie de E/S, y a mayor escala también se puede acoplar IOCP o E/S del grupo de hilos.

Estructura de un servidor superpuestoLas finalizaciones de las operaciones asíncronas de cada instancia se reciben en una matriz de eventos, y unos pocos hilos esperan con WaitForMultipleObjects y avanzan la instancia completada, desacoplando el número de hilos del de clientesOp. asíncrona instancia 1Matriz de eventosOp. asíncrona instancia 2Op. asíncrona instancia 3Esperar finalización con WaitForMultipleObjectsAvanzar la instancia completada

Figura 5: La forma superpuesta desacopla el número de hilos del de clientes. El ejemplo oficial hace girar este ciclo en un solo hilo.

.NET casi elimina esta elección. Usando WaitForConnectionAsync / ReadAsync / WriteAsync de NamedPipeServerStream junto con async/await, se obtiene la eficiencia superpuesta en un código tan directo como la forma síncrona.9

// C#: esqueleto de un servidor que acepta varios clientes
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 es una especificación que «permite conexiones solo desde procesos del mismo usuario», un valor predeterminado cómodo y seguro que ahorra escribir un ACL uno mismo.10 No se puede usar en una configuración que cruza usuarios (un servicio ↔ una aplicación en una sesión de usuario, y similares), así que en ese caso se pasa al diseño de ACL del capítulo siguiente.

Bucle de aceptación de un servidor asíncrono .NETEl bucle de aceptación crea un NamedPipeServerStream, espera una conexión con WaitForConnectionAsync y, al llegar, despega el manejo del cliente de forma asíncrona y vuelve de inmediato a la siguiente aceptación, de modo que las conexiones concurrentes se gestionan con código directoCrear el flujo servidorEsperar con WaitForConnectionAsyncLlega la conexiónDespegar el manejo del cliente

Figura 6: El bucle de aceptación se atiene al ciclo «esperar → despegar → siguiente», y el procesamiento de cada cliente avanza en paralelo.

5. Seguridad — Cuatro obligaciones cuando un servicio privilegiado usa canalizaciones

La razón más importante de que las canalizaciones con nombre sean la primera candidata para la IPC en la misma máquina es el modelo de seguridad, pero eso solo vale si se configura correctamente. Sobre todo en un diseño de broker de «un servicio con privilegios de administrador + una aplicación de interfaz de privilegios bajos», la canalización es la frontera de privilegios en sí. Hay cuatro puntos que conviene fijar.

(1) Rechazar lo remoto. Que una canalización pensada como IPC local se pueda abrir desde la red es, por sí solo, superficie de ataque. Si se indica PIPE_REJECT_REMOTE_CLIENTS en CreateNamedPipe, las conexiones de clientes remotos se rechazan automáticamente.5

(2) Hacer explícito el ACL. Se pasa un descriptor de seguridad en SECURITY_ATTRIBUTES y se restringen los usuarios y grupos autorizados a conectar. No dé al cliente GENERIC_WRITE: el derecho FILE_CREATE_PIPE_INSTANCE incluido en él permitiría que un cliente autorizado creara él mismo una instancia servidor del mismo nombre y robara las conexiones siguientes. Conceda lectura y escritura como derechos individuales, y no pase el derecho de crear instancias.11

(3) Impedir el secuestro de nombre. Los nombres de canalización son por orden de llegada. Si un proceso malicioso crea primero una canalización del mismo nombre y espera, los clientes se conectan al servidor falso. El servidor indica FILE_FLAG_FIRST_PIPE_INSTANCE al crear la primera instancia, garantizando que «yo soy el primero», y si eso falla sospecha un secuestro y se detiene. Esta marca es solo para la primera instancia que reclama el nombre; ponerla en la segunda y posteriores hace fallar la creación.3

(4) El cliente minimiza el nivel de suplantación a lo necesario. Esto es una preparación para el caso en que el interlocutor sea un servidor falso. Si el cliente indica **SECURITY_SQOS_PRESENT SECURITY_IDENTIFICATION** en CreateFile, el servidor puede identificar al cliente pero no tomar prestados esos privilegios y actuar.2 Es, no obstante, un compromiso frente a un flujo de suplantación: en un diseño de broker en el que el servidor realiza un acceso real con los privilegios del cliente, el nivel de identificación no basta para que la suplantación tenga éxito, y hay que permitir SECURITY_IMPERSONATION. Esa autorización está condicionada a estar seguro de que se está conectado al servidor genuino. La protección contra el secuestro en el lado servidor es solo un mecanismo que se entera por un arranque fallido; si el servicio genuino no está y un atacante crea primero la canalización del mismo nombre, el cliente aún puede conectarse al servidor falso. Permítalo solo cuando pueda confirmar al interlocutor mediante un arranque garantizado del servicio o una autenticación mutua después de conectar.

La comprobación de identidad y el préstamo de privilegios en el lado servidor es ImpersonateNamedPipeClient. Llámese esto después de leer una petición de la canalización y el hilo que llama empieza a ejecutarse en el contexto de seguridad del emisor del último mensaje leído. Abrir un archivo con los privilegios del cliente hace que la comprobación de acceso se haga contra el cliente: el mecanismo por el que un servicio privilegiado ejecuta «la operación pedida, con los privilegios de quien la pide».6 La condición absoluta para usarlo es comprobar el valor de retorno. Continuar después de que la suplantación falle hace que las operaciones siguientes se ejecuten con los privilegios altos propios del servidor. La documentación oficial indica de forma explícita que «si falla, no se debe ejecutar la petición del cliente». Junto con RevertToSelf después del trabajo, las prácticas del artículo sobre tokens de suplantación se aplican tal cual.

Flujo de manejo de una petición que usa suplantaciónEl servidor lee una petición de la canalización, confirma que ImpersonateNamedPipeClient tuvo éxito, realiza la operación con los privilegios del cliente y vuelve a su propio contexto con RevertToSelf. Si la suplantación falla, rechaza la petición sin ejecutarlaServidorClienteServidorClienteSi falla, rechazar sin ejecutar la peticiónEnviar una peticiónLeer la peticiónImpersonateNamedPipeClientRealizar la operación con los privilegios del clienteRevertToSelf para restaurar el contextoResponder con el resultado

Figura 7: Confirmar que la suplantación tuvo éxito y un RevertToSelf fiable van en el mismo paquete. Continuar si falla y se ejecuta con los privilegios del servidor.

Cuatro puntos que protegen la canalización de un servicio privilegiadoEl lado servidor endurece la entrada con rechazo remoto, un ACL explícito y una garantía de primera instancia; el lado cliente indica el nivel mínimo de suplantación necesario para que un servidor falso no pueda tomar prestados privilegios (restringirlo a identificación si el diseño no deja que el servidor tome prestados privilegios)Lado clienteIndicar el nivel mínimo de suplantaciónLado servidorPIPE_REJECT_REMOTE_CLIENTSRestringir conectores con un ACLFIRST_PIPE_INSTANCE (solo 1.ª)La canalización como frontera de privilegios

Figura 8: En un diseño en el que la canalización es la frontera de privilegios, implemente los tres puntos del lado servidor más el del lado cliente como un conjunto.

6. Trampas prácticas

Una carrera en el orden de arranque. Si un cliente llega a conectar antes de que el servidor haya creado la canalización, se produce un error de «la canalización no existe». El lado cliente incorpora «no existe → esperar un poco y reintentar». A la inversa, el principio en el lado servidor es empezar a escuchar con ConnectNamedPipe antes de que arranque el cliente.8

Flujo de reintento de conexión del clienteAbrir la canalización con CreateFile; si no existe esperar un momento y reintentar; si todas las instancias están en uso (ERROR_PIPE_BUSY) esperar a una libre con WaitNamedPipe y luego reintentar; si tiene éxito, entrar en comunicaciónÉxitoNo existeERROR_PIPE_BUSYAbrir con CreateFile¿Resultado?Empezar a comunicarEsperar (servidor no listo)WaitNamedPipe (instancia libre)

Figura 9: El manejo de conexión del cliente distingue los dos tipos de fallo, «no existe» y «llena», y ambos vuelven a un reintento.

Detectar una desconexión. Cuando el interlocutor sale, Read/Write fallan con ERROR_BROKEN_PIPE y similares. Eso no es una anomalía; es el día a día de la comunicación. El servidor detecta la desconexión, hace DisconnectNamedPipe de la instancia y se prepara para la siguiente conexión; el cliente se reconecta: la idea de «reconexión idempotente» descrita en el artículo sobre suspensión/reanudación se aplica también aquí.

Supuestos sobre el tamaño del mensaje. Además de las lecturas partidas del modo mensaje (capítulo 3), si no se decide como parte del protocolo «cuál es el máximo de bytes de un mensaje», un interlocutor malicioso (o defectuoso) puede desperdiciar memoria con un mensaje enorme. Decidir un tope y desconectar si se supera es el enfoque seguro.

La finalización de la escritura y que el interlocutor reciba son cosas distintas. El éxito de WriteFile no significa que la aplicación del interlocutor haya procesado los datos. Las operaciones que necesitan certeza se respaldan con diseños como confirmar con un mensaje de respuesta e incluir la correspondencia de petición y respuesta en el protocolo.

7. Resumen

  • Las canalizaciones con nombre son la primera candidata para la IPC en la misma máquina. Las razones son la misma facilidad de uso que la E/S de archivos, y la integración con el modelo de seguridad de Windows de ACL y suplantación.
  • La elección de modo es «modo mensaje para petición y respuesta, modo byte si ya tiene su propio framing». Incluso en modo mensaje hay que tratar las lecturas partidas (ERROR_MORE_DATA).
  • Varios clientes son varias instancias + superpuesto, o async/await de .NET. Para trabajo nuevo, la forma asíncrona de .NET es la más directa.
  • En una canalización que es frontera de privilegios, tome como conjunto el rechazo remoto, un ACL explícito, FIRST_PIPE_INSTANCE (solo la primera instancia) y minimizar el nivel de suplantación en el lado cliente.
  • Para ImpersonateNamedPipeClient, comprobar el valor de retorno y RevertToSelf son el salvavidas.
  • Teja en el diseño del protocolo el «día a día de la comunicación»: orden de arranque, desconexión, tope de mensaje, confirmación de respuesta.

Las canalizaciones con nombre son una API antigua, pero para el uso de «hacer que los procesos hablen entre sí en la misma máquina respetando los límites de las cuentas de Windows», siguen siendo la herramienta más natural para el trabajo. Los puntos de juicio de diseño casi se agotan en el alcance de este artículo. Después, escriba su propio protocolo en una sola hoja antes de empezar a implementar.

Artículos relacionados

Áreas de consultoría relacionadas

En KomuraSoft LLC nos encargamos del diseño e implementación que involucran comunicación entre procesos —separar un servicio de una aplicación de interfaz, aislar privilegios de administrador y similares—, de sustituir IPC existente (memoria compartida, un socket casero, COM, etc.) por canalizaciones con nombre, y de revisiones de seguridad de la comunicación por canalización de un servicio privilegiado. La consulta que empieza por contrastar un diseño de protocolo es bienvenida.

Referencias

  1. Microsoft Learn, Named Pipes. Sobre que una canalización con nombre es un canal unidireccional o bidireccional entre un servidor de canalización y uno o más clientes; sobre que todas las instancias comparten el mismo nombre pero tienen búferes e identificadores independientes; y sobre que se puede usar desde procesos locales y remotos.  2

  2. Microsoft Learn, Impersonating a Named Pipe Client. Sobre que la suplantación deja que el hilo del servidor opere dentro de los privilegios del cliente; sobre que el nivel de suplantación predeterminado es SecurityImpersonation; y sobre que el cliente puede controlar el nivel de suplantación con la marca SECURITY_SQOS_PRESENT en el momento de CreateFile (SECURITY_IDENTIFICATION permite solo la identificación).  2

  3. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). Sobre la dirección de la canalización (entrada, salida, bidireccional), el tipo byte y el tipo mensaje (PIPE_TYPE_BYTE / PIPE_TYPE_MESSAGE) y el modo de lectura (PIPE_READMODE_MESSAGE), el número máximo de instancias (PIPE_UNLIMITED_INSTANCES), el modo asíncrono mediante FILE_FLAG_OVERLAPPED, la garantía de primera instancia mediante FILE_FLAG_FIRST_PIPE_INSTANCE, y el tiempo de espera predeterminado de WaitNamedPipe.  2 3 4

  4. Microsoft Learn, Named Pipe Server Using Overlapped I/O. Sobre el ejemplo oficial de un servidor de un solo hilo que procesa conexiones simultáneas con varios clientes mediante operaciones superpuestas. Sobre la forma que espera en la estructura OVERLAPPED y el evento de cada instancia con WaitForMultipleObjects y avanza la máquina de estados de la instancia completada, y sobre confirmar la finalización de la E/S pendiente con GetOverlappedResult.  2

  5. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). Sobre los dos modos de cliente remoto, PIPE_ACCEPT_REMOTE_CLIENTS (aceptar conexiones remotas e inspeccionarlas contra el descriptor de seguridad) y PIPE_REJECT_REMOTE_CLIENTS (rechazar automáticamente las conexiones de clientes remotos).  2

  6. Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). Sobre que un hilo del lado servidor comienza la suplantación en el contexto de seguridad del cliente del último mensaje leído de la canalización; sobre volver con RevertToSelf tras terminar; y sobre que continuar después de que la suplantación falle provoca la ejecución en el contexto propio (privilegiado) del proceso servidor, de modo que hay que comprobar siempre el valor de retorno y, si falla, no ejecutar la petición del cliente.  2 3

  7. Microsoft Learn, Named Pipe Client. Sobre que el cliente abre la canalización con CreateFile; sobre ERROR_PIPE_BUSY cuando todas las instancias están en uso, esperando a una libre con WaitNamedPipe; y sobre que el identificador abierto parte de lectura en byte, bloqueante y no superpuesta, y SetNamedPipeHandleState puede cambiarlo a modo de lectura de mensaje.  2

  8. Microsoft Learn, Named Pipe Operations. Sobre operaciones superpuestas mediante ReadFileEx / WriteFileEx, una lectura no consumidora mediante PeekNamedPipe, TransactNamedPipe realizando el envío de la petición y la recepción de la respuesta en una sola llamada en una canalización bidireccional de tipo mensaje, y sobre que una lectura bloqueante antes de que arranque el cliente puede provocar una carrera.  2

  9. Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). Sobre conectar y leer/escribir con NamedPipeServerStream / NamedPipeClientStream, la transferencia por unidades de mensaje mediante PipeTransmissionMode.Message, y el manejo de varios clientes con métodos asíncronos.  2

  10. Microsoft Learn, PipeOptions Enum (System.IO.Pipes). Sobre habilitar la E/S asíncrona con Asynchronous, y sobre que CurrentUserOnly puede permitir conexiones solo con procesos del mismo usuario (y el mismo nivel de elevación). 

  11. Microsoft Learn, Named Pipe Security and Access Rights. Sobre la composición de los derechos de acceso de una canalización con nombre; sobre que GENERIC_WRITE incluye FILE_CREATE_PIPE_INSTANCE, de modo que dar a un cliente escritura genérica también permite crear una instancia servidor; y sobre que la lectura y la escritura de datos se conceden como derechos de acceso individuales. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Cómo debo elegir entre canalizaciones con nombre y TCP (un socket localhost)?
Para la comunicación entre procesos en la misma máquina, la canalización con nombre es la primera candidata. El motivo es el modelo de seguridad. Una canalización puede controlar «quién puede conectarse» a nivel del sistema operativo con un descriptor de seguridad de Windows (ACL), y el servidor puede inspeccionar y tomar prestada la cuenta de Windows del interlocutor con ImpersonateNamedPipeClient. Eso contrasta con un puerto TCP localhost, al que cualquiera puede conectarse, de modo que hay que establecer quién es el interlocutor con una autenticación propia. Por otro lado, las opciones basadas en TCP son ventajosas cuando es probable que más adelante haya comunicación remota, cuando también se habla con procesos de otros sistemas operativos, o cuando se quiere reutilizar un protocolo ya existente como gRPC. Este criterio también está desarrollado en el artículo sobre cómo elegir la comunicación entre procesos en Windows.
¿Debo usar el modo byte o el modo mensaje?
Si se quiere tratar «una escritura = una unidad de significado», el modo mensaje (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE) resulta cómodo. El receptor puede leer en las unidades que escribió el emisor, así que no hay que gestionar los límites por cuenta propia. El modo byte es un «flujo de bytes ininterrumpido» como TCP, y hay que diseñar el framing uno mismo —por ejemplo, un prefijo de longitud—. Si se transporta un protocolo que ya tiene framing (por ejemplo, una forma serializada con prefijo de longitud), el modo byte es válido. Un aviso: incluso en modo mensaje, si el búfer de recepción es más pequeño que el mensaje se produce una lectura partida (ERROR_MORE_DATA), así que hay que tratarla. Además, el modo de lectura es un ajuste por identificador, y CreateNamedPipe lo fija solo en el lado servidor. El cliente debe indicar PIPE_READMODE_MESSAGE con SetNamedPipeHandleState después de CreateFile. En .NET el servidor indica PipeTransmissionMode.Message y el cliente asigna NamedPipeClientStream.ReadMode a Message después de conectar.
¿Cómo construyo un servidor que hable con varios clientes a la vez?
Una canalización con nombre puede crear varias instancias bajo el mismo nombre, y una instancia atiende a un cliente. Hay dos formas. Una es un diseño síncrono que asigna un hilo por cliente; la implementación es directa, pero consume un hilo por cliente. La otra es usar E/S asíncrona con FILE_FLAG_OVERLAPPED y que un número reducido de hilos gestione ConnectNamedPipe, ReadFile y WriteFile de todas las instancias; el ejemplo oficial de Microsoft también muestra una implementación que procesa varias instancias en un solo hilo. En .NET se puede escribir la forma asíncrona con casi la misma naturalidad que la síncrona, usando NamedPipeServerStream.WaitForConnectionAsync y async/await. Salvo que haya un motivo concreto, la forma asíncrona de .NET es la que recomiendo para implementaciones nuevas.
¿Cuál es el mínimo que debo hacer para la seguridad de una canalización con nombre?
Cuatro puntos. Primero, si no se necesitan conexiones remotas, indicar PIPE_REJECT_REMOTE_CLIENTS y rechazar de forma explícita las conexiones por la red. Segundo, fijar un ACL adecuado con SECURITY_ATTRIBUTES y restringir los usuarios y grupos que pueden conectarse (el ACL predeterminado es demasiado laxo para algunos usos). Tercero, indicar FILE_FLAG_FIRST_PIPE_INSTANCE al crear la primera instancia, para detectar el «secuestro de nombre» en el que se crea antes una canalización del mismo nombre (no ponga esta marca en la segunda instancia ni en las posteriores). Cuarto, un cliente que solo quiere que el servidor lo identifique debe indicar SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION en CreateFile, para que un servidor falso no pueda tomar prestados (suplantar) sus privilegios. En un diseño de broker en el que el servidor realiza un acceso real con los privilegios del cliente, la suplantación tiene que estar permitida, así que se usa o no esta restricción según el diseño deje o no que el servidor tome prestados los privilegios.
¿Hay precauciones al usar ImpersonateNamedPipeClient?
La más importante es comprobar el valor de retorno. Si se continúa después de que la suplantación falle, las operaciones siguientes se ejecutan con los privilegios propios del proceso servidor (a menudo altos), y se dejan pasar operaciones que no deberían haberse permitido al cliente. La documentación oficial también indica de forma explícita que, si falla, no se debe ejecutar la petición del cliente. También hay que llamarla solo después de haber leído algo —la suplantación se realiza en el contexto de «el último mensaje leído de la canalización»— y volver de forma fiable al contexto original con RevertToSelf cuando el trabajo termina. La maquinaria en torno a la suplantación (tokens, niveles de suplantación, SeImpersonatePrivilege) se trata en detalle en el artículo sobre tokens de suplantación.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog