Задача
В одном из проектов пришлось подойти к незаслуженно заброшенному и игнорируемому снаряду. Среда функционирования:
Активная директория Windows.
Менеждер очередей IBM MQ 9.1.5
СУБД Microsoft SQL Server 2016
Сервер приложений на .NET
Всё под управлением ОС MS Windows Server 2016, само собой x64. Приложениям надо инициировать распределенные транзакции между IBM MQ и MS SQL, да ещё и запрещается хранить где-либо логины с паролями. А значит только NTLM/Kerberos. NTLM у нас тоже запрещён, но это уже мелочи. За координацию распределенных транзакций в такой среде отвечает служба MS DTC (Distributed Transaction Coordinator).
Лирика, жалобы на несовершенство мира
Отношение к IBM MQ на момент начала этих приключений было неплохое. Верилось, что это всё повидавший, хорошо отлаженный и матёрый продукт корпоративного класса. первые сомнения появились когда вместо простой и понятно пошаговой инструкции в документации к IBM MQ нашлись "туманные, плохо проветриваемые переулки, заполненные токсичными отбросами". Собственно, вниманию наивного читателя предлагается парочка статей
https://www.ibm.com/docs/en/ibm-mq/9.2?topic=windows-sspi-channel-exit-program
https://www.ibm.com/docs/en/ibm-mq/9.2?topic=multiplatforms-using-sspi-security-exit-windows
из которых становится ясно, что ничего не ясно.
Первое впечатление от этих статей можно кратко сформулировать так: "Простота - не наш метод. IBM MQ должен занимать в вашей жизни гораздо больше места, чем занимает сейчас. IBM MQ это не какй-то там инструмент. Это образ жизни, это философия, это путь!".
Основательно погрузившись в сладостный мир IBM MQ, отринув казульщину "бац, бац и в продакшн", раскаявшись в желании получить результат быстро и сполна насладившись непреступной академичностью документации, выяснил:
Для IWA-аутентификации между клиентом и менеджером очередей IBM MQ необходим "security exit". Вообще термин "exit" в мире IBM обычно означает некий плагин в какой-либо инфраструктуре. В частности, "security exit" это плагин, библиотека, публикующая два метода. Сигнатура методов одинаковая. Один предназначен для реализации NTLM, второй для Kerberos.
В результате расшифровки множества инструкций становится так же ясно, что плагин аутентификации необходим как на серверной, так и на клиентской стороне. Ну что же, сказано - сделано.
В параметрах серверного канала на стороне менеджера очередей указал необходимость использования плагина.
В параметрах подключения на стороне клиентского приложения указал указал необходимость использования плагина.
queueManagerProperties.Add(
MQC.TRANSPORT_PROPERTY,
MQC.TRANSPORT_MQSERIES_XACLIENT);
//Gives the same result.
//queueManagerProperties.Add(MQC.TRANSPORT_PROPERTY, MQC.TRANSPORT_MQSERIES_CLIENT);
//Not compatible with native "security exit" plugin implementation. More on that later.
//queueManagerProperties.Add(MQC.TRANSPORT_PROPERTY, MQC.TRANSPORT_MQSERIES_MANAGED);
queueManagerProperties.Add(
MQC.SECURITY_EXIT_PROPERTY,
"amqrspin(SCY_NTLM)");
queueManagerProperties.Add(
MQC.SECURITY_EXIT_USER_DATA_PROPERTY,
@"domain\client-app-user");
Конечно, "с ходу" ничего не получается. Получаем ошибку подключения типа 2539 (09EB) (RC2539): MQRC_CHANNEL_CONFIG_ERROR
или
2538 (09EA) (RC2538): MQRC_HOST_NOT_AVAILABLE
Исследование журналов на клиенте и на сервере навивает первую дрож и вселяет первую неуверенность в себе. По умолчанию журналы расположены в папках%ProgramData%\IBM\MQ\errors
на обеих сторонах. Что же обнаруживается в этих журналах?
AMQ4264.0.FDC
+-----------------------------------------------------------------------------+
| |
| IBM MQ First Failure Symptom Report |
| ========================================= |
| |
| Date/Time :- Thu July 21 2022 15:39:22 UTC |
| UTC Time :- 1658417962.217000 |
| UTC Time Offset :- 180 ((UNKNOWN)) |
| Host Name :- PHD-VS-MQ02 |
| Operating System :- Windows Server 2016 Server Datacenter Edition, Build |
| 14393 |
| PIDS :- 5724H7251 |
| LVLS :- 9.1.5.0 |
| Product Long Name :- IBM MQ for Windows (x64 platform) |
| Vendor :- IBM |
| O/S Registered :- 1 (amqxcs2.dll) |
| Data Path :- C:\ProgramData\IBM\MQ |
| Installation Path :- C:\Program Files\IBM\MQ |
| Installation Name :- Installation1 (1) |
| License Type :- Production |
| Probe Id :- XC130031 |
| Application Name :- MQM |
| Component :- xehExceptionHandler |
| SCCS Info :- F:\build\slot1\p910_P\src\lib\cs\pc\winnt\amqxerrn.c, |
| Line Number :- 766 |
| Build Date :- Mar 16 2020 |
| Build Level :- p915-L200316 |
| Build Type :- IKAP - (Production) |
| UserID :- ibmmq |
| Process Path :- C:\Program Files\IBM\MQ\bin64 |
| Process Name :- amqrmppa.exe |
| Arguments :- -m QTEST |
| Addressing mode :- 64-bit |
| Process :- 00004264 |
| Thread :- 00000003 RemoteResponder (6116) |
| Session :- 00000000 |
| UserApp :- FALSE |
| ConnId(1) IPCC :- 8924 |
| ConnId(3) QM-P :- 22793 |
| Last HQC :- 1.0.0-1747136 |
| Last HSHMEMB :- 0.0.0-0 |
| Last ObjectName :- |
| Major Errorcode :- xecF_E_UNEXPECTED_SYSTEM_RC |
| Minor Errorcode :- OK |
| Probe Type :- MSGAMQ6119 |
| Probe Severity :- 1 |
| Probe Description :- AMQ6109S: An internal IBM MQ error has occurred. |
| FDCSequenceNumber :- 0 |
| Comment1 :- Access Violation at address FFFFFFFFFFFFFFFF when |
| reading |
| |
+-----------------------------------------------------------------------------+
---> Stack dump for the faulting thread (0x17E4) <---
Stack Backtrace:
# ChildEBP RetAddr Param#1 Param#2 Param#3 Param#4 Fn-Loc'n : Module!Function+Offset [File Name # Line+Offset @ Address]
00 000000A40473E040 00007FFDA37B19A5 (000000A40473E220 0000020A5F376620 00000000C0000100 0000000000000000) 00007FFDA37B345A : amqrspin!terminateSecurityExit+0x9a<NLN:487>
01 000000A40473E0B0 00007FFDA37B15F5 (000000A40473E220 0000020A5F376620 000000A40473E1E0 000000A40473E190) 00007FFDA37B19A5 : amqrspin!SSPI+0x335<NLN:487>
02 000000A40473E100 00007FFD9E3B01C4 (0000020A5F350B60 0000020A659EABC0 0000020A5F376620 00007FFDB825E4FA) 00007FFDA37B15F5 : amqrspin!SCY_NTLM+0xc5<NLN:487>
03 000000A40473E1D0 00007FFD9E3B0DCA (0000020A00000000 0000020A5F376768 0000020A5F376620 0000020A5F376620) 00007FFD9E3B01C4 : amqrdlla!rriTermExit+0x3d4<NLN:487>
04 000000A40473E360 00007FFD9E3E6EF1 (0000020A5F353590 0000020A00000000 0000000000000000 0000020A00000002) 00007FFD9E3B0DCA : amqrdlla!rriTermExits+0x70a<NLN:487>
05 000000A40473EC50 00007FFD9E4B20E7 (00007FFD9E5CF530 000000A40473ED60 000000A40473ECE4 0000020A5F350B60) 00007FFD9E3E6EF1 : amqrdlla!rriFreeSess+0x781<NLN:487>
06 000000A40473EDA0 00007FFD9E4A7F47 (0000020A5F353590 0000020A5F353590 0000020A5F353F40 0000020A5F350B60) 00007FFD9E4B20E7 : amqrdlla!rriAsyncConvControl+0x327<NLN:487>
07 000000A40473EE10 00007FFD9E4B01F2 (0000020A5F379620 0000000000000000 0000020A5F353F40 0000020A5F353590) 00007FFD9E4A7F47 : amqrdlla!cciEndConv+0x287<NLN:487>
08 000000A40473EEC0 00007FFD9E4B0417 (0000020A5F370D60 0000020A5F379620 0000020A5F350B60 00007FFD00000000) 00007FFD9E4B01F2 : amqrdlla!ccxReceiveThreadCleanup+0x1f2<NLN:487>
09 000000A40473EEF0 00007FFD9E3FD4A1 (0000000000000000 000000A40473F000 0000000000000000 0000020A00000000) 00007FFD9E4B0417 : amqrdlla!ccxReceiveThreadFn+0x107<NLN:487>
0A 000000A40473F5E0 00007FFD9E44D0D3 (0000020A5F350B60 000000A400000000 0000020A5F350B60 0000020A5F3536F4) 00007FFD9E3FD4A1 : amqrdlla!rrxResponder+0x1f1<NLN:487>
0B 000000A40473F680 00007FFDAB885CD0 (0000020A00000001 0000020A00000001 0000000000000001 0000020A5F2FDAA0) 00007FFD9E44D0D3 : amqrdlla!cciResponderThread+0x293<NLN:487>
0C 000000A40473F7E0 00007FFDB80BFB80 (0000000000000000 0000000000000000 0000020A5F333EF0 0000000000000000) 00007FFDAB885CD0 : amqxcs2!ThreadMain+0x2c0<NLN:487>
0D 000000A40473F810 00007FFDBAFA84D4 (0000000000000000 0000000000000000 0000000000000000 0000000000000000) 00007FFDB80BFB80 : ucrtbase!o__realloc_base+0x60<NLN:487>
0E 000000A40473F840 00007FFDBBBF1781 (0000000000000000 0000000000000000 0000000000000000 0000000000000000) 00007FFDBAFA84D4 : KERNEL32!BaseThreadInitThunk+0x14<NLN:487>
0F 000000A40473F890 0000000000000000 (0000000000000000 0000000000000000 0000000000000000 0000000000000000) 00007FFDBBBF1781 : ntdll!RtlUserThreadStart+0x21<NLN:487>
ну и ещё много менее вразумительных деталей.
Видно, что функция указанного плагина была вызвана amqrspin!SCY_NTLM+0xc5NLN:487
и...Access Violation at address FFFFFFFFFFFFFFFF
приплыли.
В журнале ОС на стороне менеджера IBM MQ видны сообщения о падении процесса, отвечающего за работу канала, amqrmppa.exe
Журнал событий ОС, Application
- <Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
- <System>
<Provider Name="Application Error" />
<EventID Qualifiers="0">1000</EventID>
<Level>2</Level>
<Task>100</Task>
<Keywords>0x80000000000000</Keywords>
<TimeCreated SystemTime="2022-08-29T19:25:02.374175800Z" />
<EventRecordID>8583830</EventRecordID>
<Channel>Application</Channel>
<Computer>server.domain.local</Computer>
<Security />
</System>
- <EventData>
<Data>amqrmppa.exe</Data>
<Data>9.105.0.20076</Data>
<Data>5e6fa4d9</Data>
<Data>amqrspin.dll</Data>
<Data>9.105.0.20076</Data>
<Data>63060745</Data>
<Data>c0000005</Data>
<Data>00000000000139a6</Data>
<Data>2f4</Data>
<Data>01d8bbdd08c8fef0</Data>
<Data>C:\Program Files\IBM\MQ\bin64\amqrmppa.exe</Data>
<Data>C:\ProgramData\IBM\MQ\exits64\Installation1\amqrspin.dll</Data>
<Data>b10b2211-b70a-4e30-92da-d2489acc1f76</Data>
<Data />
<Data />
</EventData>
</Event>
Faulting application name: amqrmppa.exe, version: 9.105.0.20076, time stamp: 0x5e6fa4d9
Faulting module name: amqrspin.dll, version: 9.105.0.20076, time stamp: 0x63060745
Exception code: 0xc0000005
Fault offset: 0x00000000000139a6
Faulting process id: 0x2f4
Faulting application start time: 0x01d8bbdd08c8fef0
Faulting application path: C:\Program Files\IBM\MQ\bin64\amqrmppa.exe
Faulting module path: C:\ProgramData\IBM\MQ\exits64\Installation1\amqrspin.dll
Report Id: b10b2211-b70a-4e30-92da-d2489acc1f76
Faulting package full name:
Faulting package-relative application ID:
Fault bucket , type 0
Event Name: APPCRASH
Response: Not available
Cab Id: 0
Problem signature:
P1: amqrmppa.exe
P2: 9.105.0.20076
P3: 5e6fa4d9
P4: amqrspin.dll
P5: 9.105.0.20076
P6: 63060745
P7: c0000005
P8: 00000000000139a6
P9:
P10:
Attached files:
\\?\C:\Users\ibmmq\AppData\Local\Temp\WER8C45.tmp.appcompat.txt
\\?\C:\ProgramData\Microsoft\Windows\WER\Temp\WER8CD3.tmp.WERInternalMetadata.xml
C:\ProgramData\Microsoft\Windows\WER\ReportQueue\AppCrash_amqrmppa.exe_ae6ecfd69c49e99d32c25848168b3315947ddce5_92144070_cab_65f98cf0\memory.hdmp
C:\ProgramData\Microsoft\Windows\WER\ReportQueue\AppCrash_amqrmppa.exe_ae6ecfd69c49e99d32c25848168b3315947ddce5_92144070_cab_65f98cf0\triagedump.dmp
These files may be available here:
C:\ProgramData\Microsoft\Windows\WER\ReportQueue\AppCrash_amqrmppa.exe_ae6ecfd69c49e99d32c25848168b3315947ddce5_92144070_cab_65f98cf0
Analysis symbol:
Rechecking for solution: 0
Report Id: b10b2211-b70a-4e30-92da-d2489acc1f76
Report Status: 4
Hashed bucket:
Ну что же. По "лёгкой" не получится. Наладил стенд, на обеих сторонах, клиентской и серверной развернул Visual Studio. Собрал указанный в инструкции исходник amqsspin.c. Запускаю отладку. Проблема воспроизводится относительно легко и стабильно. Живём.
Много часов спустя...наконей обращаю внимание на комментарий в определении структуры
/* definition of the exit user area for the exit */
/* cannot be more than 16 bytes long */
typedef struct
{
MQBYTE expectedSecMsg;
PSecurityFunctionTable pSecurityInterface;
HINSTANCE DllHandle;
PSTATEDATA pStateData;
} SSPIEXITUSERAREA, * PSSPIEXITUSERAREA;
Подождите, у меня же архитектуры всех процессов x64. В них эта структура занимает не положенные 16 байт, а 32! Казалось бы, ну и что?! Откуда такое "ограничение"? С чего это "cannot be more than 16 bytes long". Смотрю, как эта структура используется:
pSecData = (PSSPIEXITUSERAREA)&(pParms->ExitUserArea);
Так так. Что же это за поле такое "pParms->ExitUserArea"?
https://www.ibm.com/docs/en/ibm-mq/9.1?topic=fields-exituserarea-mqbyte16
ExitUserArea (MQBYTE16)
ExitUserArea (MQBYTE16)
Last Updated: 2022-07-24
This field specifies the exit user area - a field available for the exit to use.
It is initialized to binary zero before the first invocation of the exit (which has an ExitReason
set to MQXR_INIT), and thereafter any changes made to this field by the exit are preserved across invocations of the exit.
The following value is defined:MQXUA_NONENo user information.
The value is binary zero for the length of the field.
For the C programming language, the constant MQXUA_NONE_ARRAY is also defined; this constant has the same value as MQXUA_NONE, but is an array of characters instead of a string.
The length of this field is given by MQ_EXIT_USER_AREA_LENGTH. This is an input/output field to the exit.
Диалог, я полагаю, состоялся примерно такой между программистами IT гиганта:
- хмм, на надо держать контекст плагина
- а какой размер структуры контекста?
- 16 байт для x86.
- а точно 16 байт?
- нет, не точно, стандарт языка не регламентирует, но на практике я посмотрел, 16 байт.
- в продакшн!
Что же сделали эти бандиты? Написали комментарий:
/* definition of the exit user area for the exit */
/* cannot be more than 16 bytes long */
Вы можете возразить: "это же просто пример!". Ну да, пример. А компонент amqrspin.dll, поставляемый с продуктом, тоже падает для примера?! Менеджер IBM MQ давно поставляется в варианте x64 и вместе с ним поставляется amqrspin.dll, собранная для x64.
Корректировка проста. Выделяю память для контекста, сохраняю адрес его указателя в отведённых плагину данных:
PSSPIEXITUSERAREA pSecData = malloc(sizeof(SSPIEXITUSERAREA));
memset(pSecData, 0, sizeof(SSPIEXITUSERAREA));
memcpy(&(pParms->ExitUserArea), &pSecData, sizeof(void*));
и конечно, не забываю освободить эту память, когда приходит время.
free(pSecData);
memset(pSecData, 0, MQ_EXIT_USER_AREA_LENGTH);
Этого достаточно для NTLM.
Однако, чтобы заработал Kerberos предстоит ещё разобраться с Service Principal Name и некорректным значением константы MQC.SECURITY_EXIT_USER_DATA_PROPERTY, которую я использовал для настройки клиентского подключения, на корректное значение, которое обнаруживается в исходнике amqsspin.c
queueManagerProperties.Add(
"securityUserData",
@"domain\client-app-user");
Настройка SPN, по большей части, за рамками данного опуса, однако следует отметить, что предлагаемая IBM реализация плагина формирует SPN по прошитым в её исходный код правилам:
ibmMQSeries/<queue manager name>
для моего случая:
ibmMQSeries/QTEST
Этого достаточно для Kerberos.
На сладкое
Во сторой части будут представлены результаты изысканий портирования amqsspin.c на .NET для сопряжения IWA и распределенных транзакций под координацией MS DTC. Ну и полные исходники примеров с пошаговой инструкцией.