MSMQ remoto: por qué Send() no lanza excepción al quitar permisos y cómo detectarlo con ACK/NACK, Outgoing y Dead‑Letter

¿Quitaste permisos de una cola privada de MSMQ en otro servidor y aun así Send() “funciona”? No es un bug: es el diseño. Aquí verás por qué no lanza excepción, cómo instrumentar ACK/NACK, cómo forzar plazos con TTL y cómo diagnosticar correctamente el “Access denied”.

Índice

Contexto: por qué Send() no lanza excepción al quitar permisos en MSMQ

MSMQ es una tecnología store‑and‑forward. Cuando una aplicación cliente ejecuta Send() contra una cola remota, la operación solo garantiza que el mensaje fue aceptado por la cola de salida local del cliente. La autorización del destino (ACLs de la cola remota) se valida después, durante la transferencia. Si el servidor remoto niega “Send Message”, el error no se produce de forma síncrona en la llamada a Send(); se manifestará más tarde como:

  • Un NACK en la cola de administración si pediste confirmaciones.
  • Un mensaje atascado en Outgoing Queues del cliente.
  • Un mensaje en Dead‑Letter o Transactional Dead‑Letter si vence el TTL (TimeToReachQueue/TimeToBeReceived).
Aplicación cliente  ──>  MSMQ local (Outgoing) ──(transferencia)──>  MSMQ remoto (cola privada)
                               ▲                                              ▲
                            Send() OK                                     Verifica ACL

Conclusión clave: la ausencia de excepción en Send() es normal. El “forbidden” (Access denied) es asíncrono.

Cómo reproducir el escenario de forma segura

  1. En el servidor de destino, crea una cola privada (p. ej., MiCola) y retira el permiso Send Message para Everyone/Anonymous y para la cuenta de equipo del cliente (p. ej., CLIENTE$) o la cuenta de usuario de la app.
  2. Comprueba que el cliente usa una ruta DIRECT hacia el servidor: FormatName:DIRECT=OS:Servidor\private$\MiCola
  3. Envía un mensaje con el ejemplo de C# de más abajo. Verás que Send() devuelve sin error; el fallo aparecerá después.

Detectar el “Access denied” de forma fiable

Habilita confirmaciones (ACK/NACK)

Las confirmaciones se publican en una Administration Queue que tú eliges. Debes solicitar qué tipos de ACK/NACK quieres. Lo más práctico es pedir alcance y recepción en “modo completo” (positivos y negativos).

using System.Messaging;

// Cola de destino (ruta DIRECT)
var q = new MessageQueue(@"FormatName:DIRECT=OS:Servidor\private$\MiCola");

// Cola privada local para ACK/NACK
var adminAcks = new MessageQueue(@".\private$\Acks");
if (!MessageQueue.Exists(adminAcks.Path)) MessageQueue.Create(adminAcks.Path);

// Mensaje a enviar
var msg = new Message("payload")
{
    // Confirmaciones completas de alcance y recepción
    AcknowledgeType = AcknowledgeTypes.FullReachQueue | AcknowledgeTypes.FullReceive,
    AdministrationQueue = adminAcks,
    // Plazo máximo para alcanzar la cola remota
    TimeToReachQueue = TimeSpan.FromSeconds(30),
    // Enviar a Dead-Letter si no se entrega
    UseDeadLetterQueue = true,
    // Opcional: durabilidad
    Recoverable = true
};

// Envío (usa la transacción correcta si la cola es transaccional)
q.Send(msg /, MessageQueueTransactionType.Single /);

// Id del mensaje para correlacionar con ACK/NACK
var sentId = msg.Id;

Interpreta los ACK/NACK

Los mensajes de confirmación llevan el CorrelationId igual al Id del mensaje original. Lee la cola de administración y usa la propiedad Acknowledgment para conocer el resultado:

using System;
using System.Messaging;

var admin = new MessageQueue(@".\private$\Acks");
// El cuerpo de los ACKs suele ser irrelevante; lo importante es el metadato
admin.Formatter = new BinaryMessageFormatter();

// Espera hasta 60 s por una confirmación
try
{
    var ack = admin.Receive(TimeSpan.FromSeconds(60));
    if (!string.Equals(ack.CorrelationId, sentId, StringComparison.OrdinalIgnoreCase))
    {
        Console.WriteLine("ACK recibido, pero no corresponde a mi mensaje.");
    }

    switch (ack.Acknowledgment)
    {
        case Acknowledgment.ReachQueue:
            Console.WriteLine("ACK+: El mensaje alcanzó la cola remota.");
            break;
        case Acknowledgment.Receive:
            Console.WriteLine("ACK+: El mensaje fue recibido por un consumidor.");
            break;
        case Acknowledgment.NotAcknowledgeReachQueue:
            Console.WriteLine("ACK-: No alcanzó la cola. Posible Access denied o ruta inválida.");
            break;
        case Acknowledgment.NotAcknowledgeReceive:
            Console.WriteLine("ACK-: No se pudo completar la recepción.");
            break;
        default:
            Console.WriteLine($"ACK: {ack.Acknowledgment}");
            break;
    }
}
catch (MessageQueueException ex) when (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout)
{
    Console.WriteLine("No llegó ACK/NACK dentro del plazo.");
}

Supervisa Outgoing y Dead‑Letter

  • Outgoing Queues (cliente): si ves el mensaje estancado hacia Servidor\private$\MiCola, hay un bloqueo (ACL, red, DNS, etc.).
  • Dead‑Letter: si configuras UseDeadLetterQueue = true y vence el TTL, encontrarás ahí el mensaje (o en Transactional Dead‑Letter si el envío fue transaccional).
  • Visor de eventos: revisa los registros de MSMQ en ambos equipos para eventos de autorización, autenticación o conectividad.

Asegurar que el bloqueo de permisos sea efectivo

  • En la cola remota, quita Send Message a Everyone/Anonymous y a la identidad remitente (cuenta de servicio o CLIENTE$).
  • Aplica cambios y, si sospechas caché de tokens o sesiones persistentes, reinicia el servicio MSMQ en cliente y/o servidor, o vuelve a abrir la cola en la app.
  • Verifica que realmente usas la cola remota (ruta FormatName:DIRECT=OS:Servidor\private$\Cola), no una cola local homónima.
  • Si la cola es transaccional, asegúrate de enviar con transacción (MessageQueueTransaction o MessageQueueTransactionType.Single); de lo contrario, se rechaza por incompatibilidad.

Parámetros que aceleran la detección del fallo

Establece tiempos para que el error no quede oculto durante minutos u horas:

ParámetroDónde se defineEfectoRecomendación
TimeToReachQueueMessagePlazo para que el mensaje alcance la cola destino.Reduce a 15–60 s en pruebas para obtener NACK/Dead‑Letter rápido.
TimeToBeReceivedMessageTiempo total de vida del mensaje.Útil si necesitas que caduque incluso tras llegar a destino.
UseDeadLetterQueueMessageEnvía a Dead‑Letter al expirar o fallar la entrega.Actívalo para no perder fallos silenciosos.
AcknowledgeTypeMessageQué confirmaciones deseas (positivas/negativas).Usa FullReachQueue y, si procede, FullReceive.
AdministrationQueueMessageCola para recibir ACK/NACK.Configura una cola privada local dedicada.

Mapa rápido de confirmaciones

TipoCuándo apareceQué significaAcción sugerida
Acknowledgment.ReachQueueEl mensaje llegó a la cola destino.Autorización y ruta OK.Sin acción o esperar Receive.
Acknowledgment.ReceiveUn consumidor lo recibió (y confirmó).Fin del ciclo de entrega.Registrar métrica de entrega total.
Acknowledgment.NotAcknowledgeReachQueueNo pudo entrar en la cola.Access denied, ruta inválida, cola inexistente o caída.Revisar ACL, formato de nombre, DNS/puertos, estado de MSMQ.
Acknowledgment.NotAcknowledgeReceiveFallo tras llegar al destino.Error del receptor o tiempo de espera de recepción.Revisar consumidores, transacciones y logs del destino.

Ejemplos prácticos adicionales

Envío transaccional

using (var tx = new MessageQueueTransaction())
{
    tx.Begin();
    try
    {
        var q = new MessageQueue(@"FormatName:DIRECT=OS:Servidor\private$\MiColaTx");
        var admin = new MessageQueue(@".\private$\Acks");
        var m = new Message("payload tx")
        {
            AcknowledgeType = AcknowledgeTypes.FullReachQueue | AcknowledgeTypes.FullReceive,
            AdministrationQueue = admin,
            TimeToReachQueue = TimeSpan.FromSeconds(30),
            UseDeadLetterQueue = true
        };```
    q.Send(m, tx);
    tx.Commit();
}
catch
{
    tx.Abort();
    throw;
}
```
} </code></pre>

<h3>Mini monitor de ACKs por correlación</h3>
<pre><code class="language-csharp">static bool WaitForDelivery(string adminQueuePath, string messageId, TimeSpan timeout)
{
    var admin = new MessageQueue(adminQueuePath) { Formatter = new BinaryMessageFormatter() };
    var end = DateTime.UtcNow + timeout;```
while (DateTime.UtcNow &lt; end)
{
    var remaining = end - DateTime.UtcNow;
    if (remaining &lt; TimeSpan.Zero) break;

    try
    {
        var ack = admin.Receive(remaining);
        if (!string.Equals(ack.CorrelationId, messageId, StringComparison.OrdinalIgnoreCase))
            continue; // ACK de otro mensaje

        if (ack.Acknowledgment == Acknowledgment.ReachQueue ||
            ack.Acknowledgment == Acknowledgment.Receive)
            return true;

        if (ack.Acknowledgment == Acknowledgment.NotAcknowledgeReachQueue ||
            ack.Acknowledgment == Acknowledgment.NotAcknowledgeReceive)
            throw new InvalidOperationException($"Entrega fallida: {ack.Acknowledgment}");
    }
    catch (MessageQueueException ex) when (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout)
    {
        // intento de nuevo
    }
}
return false; // no llegó ACK a tiempo
```
} 

Escenarios y señales observables

CondiciónResultado de Send()Dónde se ve el errorACK típico
ACL remota sin “Send Message”Éxito inmediatoOutgoing del cliente; posible Dead‑LetterNotAcknowledgeReachQueue
Ruta mal formada (cola no existe)Éxito inmediatoOutgoing; Dead‑Letter al expirar TTLNotAcknowledgeReachQueue
Destino caído o sin conectividadÉxito inmediatoOutgoing con reintentos; Dead‑Letter si vence TTLNotAcknowledgeReachQueue o tiempo agotado
Cola transaccional pero envío no transaccionalPuede fallar posteriorACK negativo o registros del destinoNotAcknowledgeReachQueue
Mensaje cifrado/autenticado incompatibleÉxito inmediatoACK negativo y eventos de seguridadNotAcknowledgeReachQueue

Checklist de diagnóstico

  1. Confirma que envías a la cola remota correcta (ruta DIRECT).
  2. En el destino, retira “Send Message” de Everyone/Anonymous y del equipo o usuario remitente.
  3. Reinicia MSMQ o reconecta para evitar caché de permisos y sesiones.
  4. Activa ACK/NACK y apunta a una AdministrationQueue local.
  5. Observa Outgoing Queues del cliente y Dead‑Letter.
  6. Revisa el Visor de eventos → registros de MSMQ en ambos equipos.
  7. Prueba con otra cuenta/otro equipo para descartar permisos heredados o privilegios administrativos.

Buenas prácticas de diseño

  • Telemetría por defecto: envía siempre con ACKs y TTL moderados en entornos donde la latencia de diagnóstico importe.
  • Aislar cuentas de servicio: evita ejecutar pruebas con cuentas administradoras que eludan la restricción real.
  • Idempotencia y correlación: guarda Message.Id y correlación para cruzar con ACKs y trazas.
  • Observabilidad: expón métricas de Outgoing, Dead‑Letter, tasas de NACK y latencias de ReachQueue/Receive.
  • Compatibilidad .NET: System.Messaging es específico de Windows; valida el entorno si migras a .NET moderno.

Errores frecuentes y cómo solucionarlos

  • No usar ruta DIRECT: usar Servidor\private$\Cola sin FormatName:DIRECT=... puede depender de AD y enmascarar fallos. Solución: usa DIRECT=OS para pruebas directas.
  • Olvidar la AdministrationQueue: sin ella no verás ACK/NACK. Solución: crea una cola privada de ACKs y configúrala en cada mensaje.
  • TTL muy alto o no definido: los errores tardan en surfacear. Solución: fija TimeToReachQueue y, si aplica, TimeToBeReceived.
  • Confundir Dead‑Letter transaccional: si envías con transacción, los descartes aparecen en Transactional Dead‑Letter, no en la normal.
  • Probar con el usuario equivocado: la app corre como servicio y tú pruebas con tu sesión. Solución: verifica la identidad de ejecución real (cuenta de servicio) y dale/quítale permisos según el caso.
  • Incompatibilidad transaccional: enviar sin transacción a una cola transaccional (o viceversa) no “rompe” Send() local, pero fallará después. Ajusta el tipo de cola y el modo de envío.

Preguntas frecuentes

¿Se puede forzar que Send() lance excepción por permisos remotos?
No de forma general. Send() valida condiciones locales (cola accesible, formato correcto, servicio en marcha). La autorización remota se comprueba fuera de la transacción de llamada. La manera correcta de tratar errores de permisos es con ACK/NACK, Outgoing y Dead‑Letter.

¿MessageQueue.Exists sirve para verificar antes?
Puede ayudarte a detectar la existencia nominal, pero no sustituye a ACK/NACK: no valida tus permisos efectivos ni garantiza la llegada bajo condiciones reales.

¿Cómo diferencio “Access denied” de “servidor caído”?
Ambos producen NotAcknowledgeReachQueue. Para distinguirlos, correlaciona el NACK con los eventos de MSMQ del servidor (errores de autorización frente a errores de transporte) y observa si hay sesiones/intentonas en Outgoing. Reducir TTL y revisar logs del destino acorta el ciclo de diagnóstico.

Conclusión

Si al quitar permisos en una cola remota de MSMQ tu llamada a Send() no falla, todo funciona como se diseñó. MSMQ garantiza el depósito local y traslada la verificación remota a un paso asíncrono. La solución no es esperar una excepción inmediata, sino diseñar la entrega con telemetría (ACK/NACK), recortar tiempos (TimeToReachQueue/TimeToBeReceived), vigilar Outgoing/Dead‑Letter y aplicar correctamente las ACL (y refrescarlas). Con ese enfoque, los fallos de permisos dejan de ser silenciosos y se convierten en señales observables y accionables.

Índice