MSMQ remoto: perché MessageQueue.Send() non lancia eccezioni e come diagnosticarlo davvero

Inviare a una coda privata MSMQ remota senza avere più i permessi e non vedere alcuna eccezione in C# è un comportamento sorprendente ma previsto. In questo articolo spiego perché succede, come riprodurlo e soprattutto come diagnosticarlo con ACK/NACK, log e verifiche affidabili.

Indice

Scenario e sintomi

Hai due VM Windows nello stesso dominio. Il client usa System.Messaging per contattare una coda privata su un server remoto. Finché il tuo account (o il computer account) ha Full Control sulla coda, invio e ricezione funzionano. Dopo la rimozione dei permessi, ti aspetti un’eccezione (ad es. “forbidden”), ma MessageQueue.Send() ritorna comunque senza errori.

Perché Send() non solleva eccezioni

La chiamata Send() dialoga con il servizio MSMQ locale. Se la coda remota è una destinazione raggiungibile “in teoria” (path valido, formato corretto, runtime MSMQ attivo), il servizio locale accetta il messaggio e lo sposta in un’outgoing queue. Da lì viene spedito via rete (TCP 1801 o HTTP/SRMP) al servizio MSMQ del server di destinazione. Il controllo dei permessi (ACL) della coda avviene solo sul nodo remoto, spesso in modo asincrono rispetto alla tua chiamata. Il risultato è che:

  • La chiamata locale ha successo ⇒ nessuna eccezione immediata.
  • Se la destinazione rifiuta il messaggio (ACL, autenticazione, coda inesistente, ecc.), il server remoto può emettere un NACK (acknowledgement negativo). Ma se non l’hai richiesto esplicitamente, il client non lo riceve e tu non vedi alcun errore.

Anatomia di un invio MSMQ

App .NET → MSMQ client API → Servizio MSMQ locale → Outgoing queue → Rete (1801) → 
→ Servizio MSMQ remoto → Valutazione ACL → Coda di destinazione
                                  ↘
                                   ↘ (opzionale) ACK/NACK → Administration/Ack Queue del mittente

Quando invece le eccezioni arrivano davvero

Ci sono casi in cui Send() può sollevare eccezione immediata perché l’errore è determinabile in locale:

  • Percorso o format name non valido (es. digitazione errata nell’URI della coda).
  • Mismatch transazionale: invii con transazione verso una coda non transazionale (o viceversa).
  • Servizio MSMQ locale fermo o impossibile creare la sessione locale.
  • Timeout locali (ad es. DNS irraggiungibile durante la risoluzione del nome per format non diretti).

Questi errori sono diversi dalla negazione dei permessi su una coda valida e raggiungibile, che viene valutata dal nodo remoto.

Cause principali del “nessuna eccezione” con permessi rimossi

Qui sotto una tabella estesa che riprende e approfondisce le cause tipiche.

AreaDettagliSintomo tipicoCome verificarla
Cache di sicurezza MSMQACL e ticket Kerberos possono rimanere in memoria nel servizio. Finché non si invalida la cache, il server continua a considerare valido un permesso revocato.Subito dopo aver rimosso i permessi gli invii continuano a riuscire per un po’.Riavviare il servizio MSMQ su entrambe le macchine e riprovare.
Controllo lato destinazioneIl controllo ACL avviene sul server remoto, dopo che il messaggio è stato accettato dal runtime locale.Send() torna OK; l’evento di rifiuto lo vede solo il server.Abilitare ACK/NACK e controllare NegativeArrival nell’AckQueue.
Modalità “insicure”Se la coda o il server accettano connessioni non autenticate, alcune verifiche vengono ridotte o bypassate.Invii “troppo facili”, identità non coerenti nei log.Impostare autenticazione/cripto sulla coda, disabilitare “Everyone → Send”.
Credenziali del processoL’account del servizio (es. LocalSystem o un service account di dominio) può avere privilegi sul server che l’utente interattivo non ha.Con la tua utenza fallisce, col servizio funziona (o viceversa).Esegui il test con runas o esegui il servizio con un account privo di privilegi extra.
Modifiche ACL non propagateCambiare i permessi da MMC non invalida immediatamente i token usati dalle sessioni esistenti.Comportamento incoerente fino a riavvio/refresh delle sessioni.Riavvia servizio o sistema; chiudi le sessioni aperte; klist purge (se pertinente).
Firewall e reteLa porta 1801 è aperta, quindi il pacchetto “arriva”; il rifiuto avviene a livello applicativo.Nessun errore TCP; solo NACK applicativi lato remoto.Monitorare AckQueue e i log “MSMQ” sul server di destinazione.
Formato del pathFormatName:DIRECT=OS:server\private$\coda influenza autenticazione e risoluzione.Con path AD vs DIRECT comportamenti diversi su auth.Provare sia format name diretto sia quello AD-integrated.
TransazionalitàInvio transazionale verso coda non transazionale non passa; ma il controllo “vero” è remoto.Eccezione se rilevata in locale; altrimenti NACK.Allineare Transactional tra coda e invio.
Timeout e TTLTimeToReachQueue e TimeToBeReceived troppo bassi generano NACK/Dead Letter invece che eccezioni immediate.Messaggi nella Dead-Letter; nessuna eccezione.Impostare TTL adeguati durante i test.
Dead-Letter disabilitataSenza Dead-Letter non hai traccia locale dei fallimenti di recapito.“Messaggi spariti”.Abilitare UseDeadLetterQueue per i test.
Workgroup vs dominioIn workgroup l’autenticazione Kerberos non è disponibile; ACL basate su identità AD non funzionano.Permessi apparentemente ignorati.Verificare il modello di sicurezza (workgroup vs domain).
Everyone/AnonymousACE permissive residue (Everyone→Send, Anonymous→Send) vanificano il test.Invio sempre OK nonostante rimozioni “mirate”.Controllare e rimuovere ACE permissive residue; test con ACE Deny.

Strategia di troubleshooting efficace

  1. Reset delle cache:
    • Riavvia il servizio MSMQ su client e server (Services.msc o Restart-Service MSMQ in PowerShell).
    • Chiudi sessioni/servizi che mantengono handle aperti alla coda; in ambienti di dominio può aiutare un klist purge (con cautela) o un riavvio.
  2. Abilita ACK/NACK:
    • Specifica una Administration Queue locale.
    • Richiedi NegativeArrival e/o NegativeReceive.
    • Monitora l’AckQueue e filtra gli Acknowledgment negativi.
  3. Event Viewer su entrambi i nodi:
    • Applications and Services Logs → Microsoft → Windows → MSMQ (Operational, Applications, ecc.).
    • Cerca eventi di access denied, rifiuti di autenticazione, queue not found, expired.
  4. Verifica della sicurezza:
    • MMC: Computer Management → Services and Applications → Message Queuing → Private Queues → [coda] → Properties → Security.
    • Rimuovi Everyone/Anonymous e, per test, aggiungi un ACE Deny Send Message sul tuo SID per forzare un rifiuto.
  5. Test con identità diverse:
    • Esegui il processo client con un account senza privilegi aggiuntivi sul server (ad esempio con runas /user:DOMINIO\utente).
    • Verifica se il service account ha diritti che bypassano l’ACL di coda.
  6. Forza la sicurezza “stretta”:
    • Abilita Authenticated ed eventualmente Encryption sulla coda.
    • Rimuovi “Everyone → Send”, lascia solo i gruppi/utenti necessari.
  7. Ispeziona le Outgoing Queues:
    • MMC: Message Queuing → Outgoing Queues sul client. Vedi se i messaggi restano lì, se ci sono errori o retry continui.
  8. Imposta TTL e Dead-Letter:
    • Per i test usa UseDeadLetterQueue e TTL ragionevoli; i messaggi scaduti finiscono tracciati.

Implementazione C#: invio con ACK/NACK e correlazione

Questo frammento invia un messaggio verso una coda remota e configura un’AckQueue locale per intercettare i rifiuti (NegativeArrival/NegativeReceive). Nota: System.Messaging è disponibile su .NET Framework e Windows.

// NuGet: <nessuno> - Namespace: System.Messaging (Full Framework)
// Attenzione: usare una coda di ACK privata locale esistente (AckQueue)
using System;
using System.Messaging;

class MsmqAckDemo
{
    static void Main()
    {
        // Format name diretto per evitare dipendenze AD durante i test.
        // Esempio: "FormatName:DIRECT=OS:SERVER01\\private$\\TargetQueue"
        var remoteFormatName = @"FormatName:DIRECT=OS:SERVER01\private$\TargetQueue";

        // Coda di acknowledgement locale
        var ackQueuePath = @".\private$\AckQueue";
        using var ackQueue = new MessageQueue(ackQueuePath);
        // Filtra per leggere l'acknowledgment
        ackQueue.MessageReadPropertyFilter.Acknowledgment = true;
        ackQueue.Formatter = new ActiveXMessageFormatter(); // Ack non ha un corpo tipizzato

        using var remoteQueue = new MessageQueue(remoteFormatName);

        // Messaggio da inviare
        var msg = new Message("test di accesso");
        msg.Label = "Probe-Access";
        msg.UseJournalQueue = false;

        // Abilita Dead-Letter solo per test (utile nei fallimenti di recapito)
        msg.UseDeadLetterQueue = true;

        // TTL (facoltativo): tempo massimo per raggiungere la coda
        msg.TimeToReachQueue = TimeSpan.FromMinutes(2);

        // Imposta la coda di amministrazione (per ACK/NACK)
        msg.AdministrationQueue = ackQueue;

        // Chiedi i soli NACK (arrivo/receive). Per prove complete usa anche i positivi.
        msg.AcknowledgeType = AcknowledgeTypes.NegativeArrival | AcknowledgeTypes.NegativeReceive;

        // Se la coda remota è transazionale, invia in transazione
        var tx = new MessageQueueTransaction();
        try
        {
            tx.Begin();
            remoteQueue.Send(msg, tx);
            tx.Commit();
        }
        catch
        {
            tx.Abort();
            throw;
        }

        // L'ACK/NACK usa CorrelationId = Id del messaggio originale
        var correlationId = msg.Id;

        Console.WriteLine($"Inviato. CorrelationId: {correlationId}");
        Console.WriteLine("In attesa di ACK/NACK...");

        // Attesa ACK/NACK con timeout
        var timeout = TimeSpan.FromSeconds(30);
        Message ack = null;
        var deadline = DateTime.UtcNow + timeout;

        while (DateTime.UtcNow < deadline)
        {
            try
            {
                ack = ackQueue.Receive(TimeSpan.FromSeconds(2));
                if (ack != null && ack.CorrelationId == correlationId)
                {
                    break;
                }
            }
            catch (MessageQueueException ex) when (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout)
            {
                // loop finché non scade il tempo totale
            }
        }

        if (ack == null)
        {
            Console.WriteLine("Nessun ACK/NACK ricevuto (controlla dead-letter e outgoing queue).");
            return;
        }

        switch (ack.Acknowledgment)
        {
            case Acknowledgment.NegativeArrival:
                Console.WriteLine("NACK: il messaggio NON è arrivato alla coda di destinazione (possibile accesso negato, coda assente, rete).");
                break;
            case Acknowledgment.NegativeReceive:
                Console.WriteLine("NACK: il destinatario NON ha ricevuto il messaggio (deny alla ricezione o timeout).");
                break;
            case Acknowledgment.ReachQueue:
                Console.WriteLine("ACK: il messaggio è arrivato alla coda di destinazione.");
                break;
            case Acknowledgment.Receive:
                Console.WriteLine("ACK: il messaggio è stato ricevuto dal consumatore.");
                break;
            case Acknowledgment.ReachQueueTimeout:
                Console.WriteLine("NACK: timeout nel raggiungere la coda.");
                break;
            default:
                Console.WriteLine($"ACK/NACK: {ack.Acknowledgment}");
                break;
        }
    }
}

Per un test minimo, puoi anche utilizzare questa forma sintetica (senza loop di correlazione) proposta come “quick probe”:

MessageQueue msgQ = new MessageQueue(@".\Private$\AckQueue");
msgQ.MessageReadPropertyFilter.Acknowledgment = true;

Message sent = new Message("test");
sent.UseJournalQueue = false;
sent.AdministrationQueue = msgQ;
sent.AcknowledgeType = AcknowledgeTypes.NegativeReceive | AcknowledgeTypes.NegativeArrival;

var remoteQueue = new MessageQueue(@"FormatName:DIRECT=OS:SERVER01\private$\TargetQueue");
// Se la coda remota è transazionale:
remoteQueue.Send(sent, MessageQueueTransactionType.Single);

// Poi monitora AckQueue per messaggi con Acknowledgment = NegativeArrival o ReachQueueTimeout

Riprodurre il problema in modo controllato

  1. Prepara le code: crea Private$\TargetQueue sul server e Private$\AckQueue sul client.
  2. Concedi permessi iniziali al tuo utente o computer (Send Message sulla coda target).
  3. Esegui il client e verifica ACK positivo (ReachQueue).
  4. Rimuovi/nega i permessi sulla coda (aggiungi temporaneamente un ACE Deny Send per il tuo SID).
  5. Riavvia MSMQ su entrambi i nodi per evitare cache residue.
  6. Rilancia il client: Send() terminerà OK, ma dopo pochi istanti riceverai NegativeArrival o vedrai eventi di “accesso negato” sul server.

Checklist rapida: problema → segnale → azione

Problema sospettoSegnale da cercareAzione immediata
Permessi rimossi ma invii “OK”NACK in AckQueue; eventi “access denied” sul serverAbilita ACK/NACK; riavvia MSMQ lato server
Messaggi “spariti”Dead-Letter sul client; Outgoing Queue accodataAbilita Dead-Letter; ispeziona Outgoing Queues
Mismatch transazionaleEccezione in locale o NACK specificoAllinea Transactional tra coda e invio
Autenticazione deboleIdentità inattesa nei log MSMQForza coda autenticata + encryption
Cache di credenzialiComportamento incoerente post-cambio ACLRiavvia MSMQ, chiudi sessioni, flush ticket

Differenze tra Positive/Negative ACK

  • PositiveArrival (ReachQueue): la coda di destinazione ha accettato il messaggio (non significa che sia già stato elaborato).
  • PositiveReceive (Receive): un consumatore ha effettivamente ricevuto e rimosso il messaggio dalla coda.
  • NegativeArrival: il messaggio non è arrivato alla coda; cause tipiche: accesso negato, coda inesistente, rete bloccata, TTL scaduto.
  • NegativeReceive: nessun consumatore ha ricevuto il messaggio (deny alla ricezione, coda in errore, timeout).

Per diagnosi complete in ambienti di produzione è comune richiedere sia i positivi che i negativi (AcknowledgeTypes.FullReachQueue | AcknowledgeTypes.FullReceive), regolando i TTL per evitare flood di acknowledgements.

Impostazioni consigliate per test e produzione

  • Ammettere il design asincrono: non affidarti mai alla sola assenza di eccezioni. Usa ACK/NACK, Dead-Letter e log come source of truth.
  • Administration/Ack Queue dedicata: separa gli ack dagli altri flussi; monitora gli Acknowledgment negativi e notifica.
  • Dead-Letter attivata per tutti i messaggi critici; in produzione valuta di mantenerla on solo sui flussi sensibili.
  • TTL coerenti (TimeToReachQueue e TimeToBeReceived) per evitare false diagnosi.
  • Transazionalità allineata: se la coda è transazionale, usa MessageQueueTransaction o TransactionScope.
  • Sicurezza forte:
    • Coda autenticata; rimuovere “Everyone/Anonymous → Send”.
    • Valutare l’uso di encryption per confidenzialità end‑to‑end.
    • Isolare gli account di servizio e limitare i privilegi a “Send”/“Receive” minimi.
  • Osservabilità:
    • Abilitare log MSMQ su client e server; collezionare eventi in SIEM.
    • Esportare metriche di backlog dalle Outgoing/Incoming queues.

Domande frequenti

Si può “forzare” Send() a fallire subito in caso di ACL negate?

No. L’architettura di MSMQ separa l’accettazione locale dalla decisione remota. Puoi simulare un “fail-fast” richiedendo NACK e impostando timeout corti, ma il meccanismo resta asincrono.

Ho rimosso i permessi, ma l’invio funziona ancora: è un bug?

Quasi sempre è dovuto a cache/token ancora validi o ad ACE permissive residue (Everyone/Anonymous). Riavvia MSMQ, verifica la Security della coda e aggiungi un ACE Deny esplicito sul tuo SID per validare il test.

Uso .NET moderno: posso usare System.Messaging?

System.Messaging è un’API del .NET Framework per Windows. Se stai su .NET Core/5+/moderno, valuta alternative o interoperabilità via COM/WMI/interop, ma il comportamento lato MSMQ (servizio) resta lo stesso.

È meglio usare format name diretto o AD-integrato?

Per test di sicurezza e per ridurre variabili, il format name diretto (es. FormatName:DIRECT=OS:server\private$\coda) è spesso più prevedibile. In produzione, in ambienti di dominio, l’integrazione AD può semplificare discovery e gestione, ma introduce la variabile della risoluzione e dei permessi AD.

Script e verifiche utili

PowerShell – riavvio servizi e ispezione rapida

# Riavvia il servizio MSMQ su client e server
Restart-Service -Name MSMQ -Force

Verifica che la coda di ACK esista localmente

Get-MsmqQueue -Name "private$\AckQueue"

Controlla le outgoing queues (sul client)

$mqPath = "Computer Management\System Tools\Message Queuing\Outgoing Queues"

Vista GUI: apri mmc e ispeziona ritardi/errori. In PS non c'è un cmdlet nativo per lo stato dettagliato.

Verifica credenziali effettive del processo

whoami
whoami /groups
whoami /priv

Confronta l’identità del processo client con gli ACE della coda sul server. Spesso il servizio gira con un account con più privilegi del tuo utente interattivo.

Approfondimento: dove “vive” l’errore

Il punto chiave è distinguere tra errori determinabili in locale (che possono generare eccezioni immediate) ed errori che richiedono l’esito remoto (che generano ACK/NACK). L’accesso negato per ACL appartiene alla seconda categoria. A livello operativo questo significa:

  • Nei test: considera obbligatori gli ACK/NACK, soprattutto quando stai validando i permessi.
  • In produzione: monitora sempre le code di amministrazione, le Outgoing Queues e i log MSMQ; emetti alert sul tasso di NACK.

Modello mentale riassuntivo

Puoi pensare a Send() come all’inserimento di una busta in una casella postale di quartiere (il servizio locale). Se l’ufficio postale centrale (il server remoto) rifiuta la busta perché il destinatario non è autorizzato, la tua casella non ti strappa la busta dalle mani: la accetta e più tardi ti recapitano un avviso di mancato recapito (NACK) — ma solo se hai chiesto di essere avvisato.

In sintesi

La mancanza di eccezioni quando rimuovi i permessi su una coda MSMQ remota è un comportamento previsto dell’architettura: la chiamata locale va a buon fine, la negazione avviene sul nodo remoto e diventa visibile solo tramite ACK/NACK, Dead-Letter o log di sistema. Per test affidabili:

  • Riavvia il servizio MSMQ su client e server dopo ogni cambio ACL (eviti cache e token “stanchi”).
  • Usa una Administration Queue e richiedi NegativeArrival/NegativeReceive.
  • Controlla l’Event Viewer su entrambi i nodi.
  • Forza la sicurezza (autenticazione e ACE minimi), rimuovi “Everyone → Send”.
  • Valuta Dead-Letter e TTL ragionevoli per catturare fallimenti non immediati.

Appendice: matrice di mappatura cause-soluzioni

CauseImpatto su Send()VerificaRimedi
Cache sicurezza MSMQNessuna eccezione; permessi “persistono”Riavvio MSMQ cambia l’esitoRiavviare MSMQ su entrambi i nodi
ACL valutate lato serverNessuna eccezioneNACK in AckQueueAbilitare ACK/NACK e monitorarli
Modalità non autenticataAccessi eccessivamente permissiviLog senza identità attendibiliAutenticazione + encryption
Credenziali del processoEsiti diversi tra utente e serviziowhoami, confronto SID/ACEAllineare account di esecuzione
Modifiche ACL non propagateNessuna eccezione, comportamento incoerentePersistenza post-cambioRiavvio servizio/sistema
Firewall solo di reteNessuna eccezione di trasportoEventi MSMQ lato serverRegole applicative, non solo TCP
ACE permissive residueInvii sempre OKSecurity tab della codaRimuovere Everyone/Anonymous; usare Deny

Nota tecnica aggiuntiva

  • Perché non arriva un’eccezione? MessageQueue.Send() parla con il servizio locale, che accetta e instrada il pacchetto senza attendere la decisione remota. L’ACL è valutata solo dal servizio MSMQ della macchina di destinazione; se il messaggio è respinto, questo genererà un NACK che, se non richiesto, il mittente ignora.
  • Come intercettare i fallimenti veri? Usa una Administration Queue locale, abilita NegativeArrival/NegativeReceive e monitora l’AckQueue per messaggi con Acknowledgment negativo o timeout (ReachQueueTimeout).
Indice