Именованные каналы на практике — стандартный IPC Windows: от проектирования до безопасности

· · Windows, IPC, Разработка под Windows, C#, C++, Безопасность, Win32 API

«Хочу, чтобы резидентная служба и интерфейс настроек обменивались командами.» «Хочу вынести только ту работу, которой нужны права администратора, в отдельный процесс.» «Хочу, чтобы инструменты на одном ПК передавали друг другу данные.» — Когда на Windows возникает потребность в таком межпроцессном взаимодействии (IPC), стандарт, который стоит рассмотреть первым, — это именованный канал.

Статья о выборе межпроцессного взаимодействия в Windows ставила именованные каналы «первым кандидатом для IPC на одной машине». Эта статья — подробный разбор. Почему они первый кандидат, как выбирать режимы и формы сервера и что обязательно защищать, когда канал использует привилегированная служба, — материал для этих проектных решений собран по первичным источникам и рассчитан на разработчиков бизнес-приложений и служб под Windows.

1. Сначала вывод

  • Именованный канал — двунаправленный межпроцессный канал с пространством имён вида \\.\pipe\name. Под одним именем можно создать несколько экземпляров и принимать нескольких клиентов одновременно.1
  • Почему они первый кандидат для IPC на одной машине — модель безопасности. Кто подключается, можно контролировать ACL, а сервер может проверить и позаимствовать (олицетворить) учётную запись Windows клиента. У TCP на localhost нет ни того, ни другого.2
  • Если нужно трактовать «одна запись = одно сообщение», берите режим сообщений; если свой фрейминг уже есть — байтовый режим. Даже в режиме сообщений дробное чтение при коротком буфере (ERROR_MORE_DATA) всё равно нужно обрабатывать.3
  • Несколько клиентов обрабатывают схемой «несколько экземпляров + overlapped I/O» или «.NET async/await». Официальный пример показывает форму, которая обрабатывает несколько экземпляров в одном потоке.4
  • Минимум безопасности — четыре пункта: отказ удалённым (PIPE_REJECT_REMOTE_CLIENTS), явный ACL, обнаружение захвата через FILE_FLAG_FIRST_PIPE_INSTANCE и минимизация уровня олицетворения на стороне клиента.56
  • Для ImpersonateNamedPipeClient проверка возвращаемого значения — линия жизни. Проигнорировать сбой — и обработка продолжится с правами сервера.6

2. Что такое именованный канал — пространство имён, экземпляры и как устроены подключения

Именованный канал — это канал, который идентифицируется именем вроде \\.\pipe\MyCompany.MyApp.Control. Сервер создаёт его через CreateNamedPipe, клиент открывает то же имя через CreateFile. После открытия обе стороны читают и пишут через ReadFile / WriteFile — отличительная черта в том, что канал можно использовать в той же форме, что и файловый ввод-вывод.1

Важное понятие — экземпляр. Можно создать несколько экземпляров канала с одним именем, и один экземпляр — это один канал с одним клиентом. Первый вызов CreateNamedPipe задаёт максимальное число экземпляров (или без ограничения).3

Подключение на стороне клиента имеет стандартный рецепт. Когда все экземпляры заняты, CreateFile завершается с ERROR_PIPE_BUSY, поэтому ждут свободный через WaitNamedPipe и повторяют попытку. Кроме того, доступ, указанный при открытии, должен совпадать с направлением, в котором сервер создал канал: двунаправленный канал можно открыть, указав либо чтение, либо запись, но исходящий канал, в который сервер только пишет, нужно открывать только на чтение, а входящий, из которого сервер только читает, — только на запись, иначе CreateFile завершится сбоем.7

Направление канала и доступ клиентаКлиент может открыть двунаправленный канал с указанием чтения или записи, но исходящий канал, в который сервер только пишет, нужно открывать только на чтение, а входящий, из которого сервер только читает, — только на записьДвунаправленныйИсходящийВходящийНаправление, заданное сервером?Чтение или запись — окОткрыть только на чтениеОткрыть только на запись

Рис. 1: Несовпадение направления и прав доступа даёт сбой CreateFile. При разборе ошибки подключения проверяйте это в первую очередь.

Базовая структура именованного каналаСервер создаёт несколько экземпляров канала с одним именем и ждёт подключения через ConnectNamedPipe; каждый клиент открывает имя через CreateFile и получает взаимно однозначный двунаправленный канал с одним экземпляромСерверЭкземпляр 1Экземпляр 2Экземпляр 3Клиент AКлиент BКлиент C

Рис. 2: Держа несколько экземпляров с одним именем, один сервер одновременно говорит один-к-одному с несколькими клиентами.

Именованные каналы можно открывать и удалённо по SMB (\\server\pipe\name), но в современном проекте почти нет причин использовать это активно; вопрос скорее в том, чтобы не оставлять это открытым, когда вы этим не пользуетесь (глава 5).

3. Байтовый режим и режим сообщений

У канала два режима передачи.3

  • Байтовый режим (PIPE_TYPE_BYTE): «непрерывный поток байтов», как TCP. Где кончается одно сообщение, решаете вы сами (проектируете фрейминг вроде префикса длины).
  • Режим сообщений (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE): одна запись трактуется как одно сообщение, и читатель получает его в этой единице. Так проще обмениваться запросом и ответом.

У режима сообщений есть удобный спутник — TransactNamedPipe, который отправляет запрос и принимает ответ одним вызовом.8 Есть и ловушка. Если приёмный буфер меньше всего сообщения, чтение возвращает ERROR_MORE_DATA и становится дробным чтением. Не исходите из того, что режим сообщений значит «один Read всегда приносит всё целиком»; всё равно нужен цикл, который дочитывает остаток. Заметьте: режим чтения — настройка на уровне дескриптора, и CreateNamedPipe задаёт его только на стороне сервера. Клиент указывает его через SetNamedPipeHandleState после CreateFile (в .NET — ReadMode после подключения).7

Цикл дробного чтения в режиме сообщенийЕсли ReadFile успешен, сообщение полно; если вернулся ERROR_MORE_DATA, дочитывают остаток, не поместившийся в буфер, и склеивают; любая другая ошибка трактуется как разрывУспехERROR_MORE_DATAЛюбая другая ошибкаЧитать через ReadFileРезультат?Сообщение полноДочитать остаток и склеитьТрактовать как разрыв

Рис. 3: Даже в режиме сообщений нужен «цикл дочитывания»; без него ломаются только большие сообщения.

Разница между байтовым режимом и режимом сообщенийВ байтовом режиме три записи становятся непрерывным потоком байтов, и приёмник должен сам его резать; в режиме сообщений единица каждой записи сохраняется и приходит к приёмнику как естьБайтовый: запись AAA, BB, CCCCПринято как поток AAABBCCCCФрейминг проектируете самиРежим сообщений: те же три записиПринято как три сообщения: AAA, BB, CCCCЕдиницы записи сохранены

Рис. 4: Режим сообщений сохраняет «единицу записи» и доставляет её. Проектирование фрейминга становится ненужным; только не забудьте обработать дробное чтение.

Практическое правило выбора простое. Если обмен имеет форму «запрос и ответ» — режим сообщений. Если вы несёте форму, в которой фрейминг уже встроен (сериализованные данные с префиксом длины или потоковая передача), берите байтовый режим. В .NET указание PipeTransmissionMode.Message соответствует первому варианту.9

4. Проектирование сервера — один поток на клиента или overlapped?

Базовая операция сервера — цикл «создать экземпляр → ждать клиента через ConnectNamedPipe → читать и писать → отключиться и перейти к следующему клиенту». Для одновременного общения с несколькими клиентами есть две формы.

Синхронная, один поток на экземпляр. Каждому экземпляру назначают один поток, и каждый общается со своим клиентом синхронным вводом-выводом. Код прямолинеен, но вы потребляете один поток на клиента и ещё нужен способ вырваться из блокирующего ввода-вывода при остановке всего сервера.

Overlapped (асинхронная). Экземпляры создают с FILE_FLAG_OVERLAPPED, выдают ConnectNamedPipe / ReadFile / WriteFile асинхронно, и небольшое число потоков обрабатывает завершение для всех экземпляров. Официальный пример Microsoft показывает сервер, который ждёт массив событий через WaitForMultipleObjects и обрабатывает несколько экземпляров в одном потоке.4 Общий рассказ об асинхронном вводе-выводе — как в статье серии про I/O, а в большем масштабе можно ещё подключить IOCP или ввод-вывод пула потоков.

Структура overlapped-сервераЗавершения асинхронных операций каждого экземпляра принимают на массив событий, и небольшое число потоков ждёт через WaitForMultipleObjects и продвигает завершившийся экземпляр, отделяя число потоков от числа клиентовАсинхр. операция экз. 1Массив событийАсинхр. операция экз. 2Асинхр. операция экз. 3Ждать завершения через WaitForMultipleObjectsПродвинуть завершённый экземпляр

Рис. 5: Overlapped-форма отделяет число потоков от числа клиентов. Официальный пример крутит этот цикл в одном потоке.

.NET почти снимает этот выбор. Используя WaitForConnectionAsync / ReadAsync / WriteAsync у NamedPipeServerStream вместе с async/await, вы получаете эффективность overlapped в коде такой же прямолинейности, как у синхронной формы.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 — спецификация «разрешать подключения только от процессов того же пользователя», удобное и безопасное значение по умолчанию, которое избавляет от самостоятельного написания ACL.10 Его нельзя использовать в схеме, которая пересекает пользователей (служба ↔ приложение в пользовательском сеансе и тому подобное), поэтому в таком случае переходите к проектированию ACL в следующей главе.

Цикл приёма асинхронного сервера .NETЦикл приёма создаёт NamedPipeServerStream, ждёт подключения через WaitForConnectionAsync и по прибытии отцепляет обработку клиента асинхронно и сразу возвращается к следующему приёму, так что параллельные подключения обрабатываются прямолинейным кодомСоздать серверный потокЖдать через WaitForConnectionAsyncПришло подключениеОтцепить обработку клиента асинхронно

Рис. 6: Цикл приёма держится схемы «ждать → отцепить → следующий», а обработка каждого клиента идёт параллельно.

5. Безопасность — четыре обязательных шага, когда привилегированная служба использует каналы

Главная причина, почему именованные каналы — первый кандидат для IPC на одной машине, — модель безопасности, но только если её настроить правильно. Особенно в брокерской схеме «служба с правами администратора + UI-приложение с низкими правами» канал и есть граница прав. Нужно закрепить четыре пункта.

(1) Отказывать удалённым. Канал, задуманный как локальный IPC и при этом открываемый из сети, сам по себе — поверхность атаки. Укажите PIPE_REJECT_REMOTE_CLIENTS в CreateNamedPipe — и подключения удалённых клиентов будут автоматически отклоняться.5

(2) Сделать ACL явным. Передайте дескриптор безопасности в SECURITY_ATTRIBUTES и сузьте пользователей и группы, которым разрешено подключаться. Не давайте клиенту GENERIC_WRITE — входящее в него право FILE_CREATE_PIPE_INSTANCE позволило бы авторизованному клиенту самому создать серверный экземпляр того же имени и перехватить последующие подключения. Выдавайте чтение и запись как отдельные права и не передавайте право создания экземпляра.11

(3) Предотвратить захват имени. Имена каналов раздаются по принципу «кто первый». Если вредоносный процесс создаст канал с тем же именем раньше и будет ждать, клиенты подключатся к поддельному серверу. Сервер указывает FILE_FLAG_FIRST_PIPE_INSTANCE при создании первого экземпляра, гарантируя «я первый», и если это не удаётся — подозревает захват и останавливается. Этот флаг только для первого экземпляра, который заявляет имя; поставить его на второй и последующие экземпляры значит сорвать создание.3

(4) Клиент сводит уровень олицетворения к необходимому минимуму. Это подготовка к случаю, когда собеседник — поддельный сервер. Если клиент указывает **SECURITY_SQOS_PRESENT SECURITY_IDENTIFICATION** в CreateFile, сервер может опознать клиента, но не может позаимствовать эти права и действовать.2 Это, впрочем, компромисс с рабочим процессом олицетворения: в брокерской схеме, где сервер выполняет реальный доступ под правами клиента, уровня идентификации недостаточно, чтобы олицетворение удалось, и нужно разрешить SECURITY_IMPERSONATION. Это разрешение условно: нужно быть уверенным, что подключены к подлинному серверу. Антизахват на стороне сервера — лишь механизм, который замечает сбой старта; если подлинной службы нет и злоумышленник создаёт одноимённый канал первым, клиент всё равно может подключиться к поддельному серверу. Разрешайте только когда собеседника можно подтвердить гарантированным стартом службы или взаимной аутентификацией после подключения.

Проверка личности и заимствование прав на стороне сервера — это ImpersonateNamedPipeClient. Вызовите её после чтения запроса из канала — и вызывающий поток начинает работать в контексте безопасности отправителя последнего прочитанного сообщения. Откройте файл с правами клиента — и проверка доступа идёт против клиента: механизм, которым привилегированная служба выполняет «запрошенную операцию, но с правами запросившего».6 Абсолютное условие использования — проверка возвращаемого значения. Продолжить после сбоя олицетворения — и последующие операции идут с собственными высокими правами сервера. Официальная документация прямо говорит: «при сбое запрос клиента выполнять нельзя». Вместе с RevertToSelf после работы практики из статьи о токенах олицетворения применимы как есть.

Поток обработки запроса с олицетворениемСервер читает запрос из канала, подтверждает, что ImpersonateNamedPipeClient успешен, затем выполняет операцию с правами клиента и возвращается к своему контексту через RevertToSelf. Если олицетворение не удалось, запрос отклоняют, не выполняяСерверКлиентСерверКлиентПри сбое отклонить, не выполняя запросОтправить запросПрочитать запросImpersonateNamedPipeClientВыполнить операцию с правами клиентаRevertToSelf — вернуть исходный контекстОтветить результатом

Рис. 7: Подтверждение, что олицетворение удалось, и надёжный RevertToSelf идут комплектом. Продолжить при сбое — и работа идёт с правами сервера.

Четыре пункта, которые защищают канал привилегированной службыСторона сервера укрепляет вход отказом удалённым, явным ACL и гарантией первого экземпляра; сторона клиента указывает минимальный нужный уровень олицетворения, чтобы поддельный сервер не мог заимствовать права (сузьте до уровня идентификации, если проект не даёт серверу заимствовать права)Сторона клиентаУказать мин. уровень олицетворенияСторона сервераPIPE_REJECT_REMOTE_CLIENTSОграничить подключающихся ACLFIRST_PIPE_INSTANCE (только 1-й экз.)Канал как граница прав

Рис. 8: В проекте, где канал — граница прав, реализуйте три серверных пункта плюс один клиентский как комплект.

6. Практические ловушки

Гонка порядка запуска. Если клиент приходит подключаться до того, как сервер создал канал, получается ошибка «канал не существует». На стороне клиента закладывают «не существует → немного подождать и повторить». Наоборот, принцип на стороне сервера — начать слушать через ConnectNamedPipe до старта клиента.8

Поток повторных попыток подключения клиентаОткрыть канал через CreateFile; если канала нет — кратко подождать и повторить; если все экземпляры заняты (ERROR_PIPE_BUSY) — ждать свободный через WaitNamedPipe и затем повторить; при успехе войти в обменУспехКанала нетERROR_PIPE_BUSYОткрыть через CreateFileРезультат?Начать обменПодождать (сервер не стартовал)WaitNamedPipe: ждать свободный

Рис. 9: Обработка подключения клиента различает два вида сбоя — «не существует» и «заполнено» — и оба возвращает в повторную попытку.

Обнаружение разрыва. Когда собеседник завершается, Read/Write падают с ERROR_BROKEN_PIPE и подобным. Это не аномалия, а повседневность обмена. Сервер обнаруживает разрыв, делает DisconnectNamedPipe экземпляру и готовится к следующему подключению; клиент переподключается — идея «идемпотентного переподключения», описанная в статье о сне и пробуждении, применима и здесь.

Допущения о размере сообщения. Поверх дробного чтения режима сообщений (глава 3), если в протоколе не решить «сколько максимум байт в одном сообщении», вредоносный (или ошибочный) собеседник может тратить вашу память огромным сообщением. Задать верхнюю границу и отключаться при превышении — безопасный подход.

Завершение записи и приём у собеседника — разные вещи. Успех WriteFile не значит, что приложение собеседника обработало данные. Операции, которым нужна достоверность, подкрепляют схемами вроде подтверждения ответным сообщением и включения соответствия запроса и ответа в протокол.

7. Итоги

  • Именованные каналы — первый кандидат для IPC на одной машине. Причины — та же удобность, что у файлового ввода-вывода, и интеграция с моделью безопасности Windows: ACL и олицетворение.
  • Выбор режима: «режим сообщений для запроса и ответа, байтовый режим, если свой фрейминг уже есть». Даже в режиме сообщений дробное чтение (ERROR_MORE_DATA) всё равно нужно обрабатывать.
  • Несколько клиентов — несколько экземпляров + overlapped либо .NET async/await. Для новой работы асинхронная форма .NET — прямолинейная.
  • На канале, который есть граница прав, берите комплектом отказ удалённым, явный ACL, FIRST_PIPE_INSTANCE (только первый экземпляр) и минимизацию уровня олицетворения на стороне клиента.
  • Для ImpersonateNamedPipeClient проверка возвращаемого значения и RevertToSelf — линия жизни.
  • Вплетите «повседневность обмена» — порядок запуска, разрыв, верхнюю границу сообщения, подтверждение ответа — в проектирование протокола.

Именованные каналы — старый API, но для задачи «дать процессам на одной машине говорить друг с другом, уважая границы учётных записей Windows» они по-прежнему самый естественный инструмент. Точки проектного решения почти исчерпаны рамками этой статьи. Дальше — выпишите свой протокол на один лист бумаги, прежде чем начинать реализацию.

Похожие статьи

Смежные области консультирования

KomuraSoft LLC занимается проектированием и реализацией, где есть межпроцессное взаимодействие — отделение службы от UI-приложения, изоляция прав администратора и тому подобное, — заменой существующего IPC (разделяемая память, самодельный сокет, COM и так далее) на именованные каналы и ревью безопасности канального обмена привилегированной службы. Консультация начиная с проверки протокола приветствуется.

Справочные ссылки

  1. Microsoft Learn, Named Pipes. О том, что именованный канал — односторонний или двунаправленный канал между сервером канала и одним или несколькими клиентами канала; о том, что все экземпляры разделяют одно имя, но имеют независимые буферы и дескрипторы; и о том, что канал можно использовать из локальных и удалённых процессов.  2

  2. Microsoft Learn, Impersonating a Named Pipe Client. О том, что олицетворение позволяет серверному потоку работать в пределах прав клиента; о том, что уровень олицетворения по умолчанию — SecurityImpersonation; и о том, что клиент может управлять уровнем олицетворения флагом SECURITY_SQOS_PRESENT в момент CreateFile (SECURITY_IDENTIFICATION разрешает только идентификацию).  2

  3. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). О направлении канала (входящий, исходящий, двунаправленный), байтовом и сообщенийном типе (PIPE_TYPE_BYTE / PIPE_TYPE_MESSAGE) и режиме чтения (PIPE_READMODE_MESSAGE), максимальном числе экземпляров (PIPE_UNLIMITED_INSTANCES), асинхронном режиме через FILE_FLAG_OVERLAPPED, гарантии первого экземпляра через FILE_FLAG_FIRST_PIPE_INSTANCE и тайм-ауте по умолчанию для WaitNamedPipe.  2 3 4

  4. Microsoft Learn, Named Pipe Server Using Overlapped I/O. Об официальном примере однопоточного сервера, который обрабатывает одновременные подключения нескольких клиентов через overlapped-операции. О форме, которая ждёт OVERLAPPED-структуру и событие каждого экземпляра через WaitForMultipleObjects и продвигает конечный автомат завершившегося экземпляра, и о подтверждении завершения ожидающего ввода-вывода через GetOverlappedResult.  2

  5. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). О двух режимах для удалённых клиентов: PIPE_ACCEPT_REMOTE_CLIENTS (принимать удалённые подключения и проверять их по дескриптору безопасности) и PIPE_REJECT_REMOTE_CLIENTS (автоматически отказывать удалённым клиентам).  2

  6. Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). О том, что серверный поток начинает олицетворение в контексте безопасности клиента последнего сообщения, прочитанного из канала; о возврате через RevertToSelf после завершения; и о том, что продолжение после сбоя олицетворения ведёт к выполнению в собственном (привилегированном) контексте серверного процесса, поэтому возвращаемое значение нужно всегда проверять и при сбое запрос клиента выполнять нельзя.  2 3

  7. Microsoft Learn, Named Pipe Client. О том, что клиент открывает канал через CreateFile; о ERROR_PIPE_BUSY, когда все экземпляры заняты, и ожидании свободного через WaitNamedPipe; и о том, что открытый дескриптор по умолчанию байтовый, блокирующий и не-overlapped, а SetNamedPipeHandleState может переключить его в режим чтения сообщений.  2

  8. Microsoft Learn, Named Pipe Operations. Об overlapped-операциях через ReadFileEx / WriteFileEx, чтении без потребления через PeekNamedPipe, TransactNamedPipe, который на двунаправленном канале сообщенийного типа выполняет отправку запроса и приём ответа одним вызовом, и о том, что блокирующее чтение до старта клиента может вызвать гонку.  2

  9. Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). О подключении и чтении/записи через NamedPipeServerStream / NamedPipeClientStream, передаче единицами сообщений через PipeTransmissionMode.Message и обработке нескольких клиентов асинхронными методами.  2

  10. Microsoft Learn, PipeOptions Enum (System.IO.Pipes). О включении асинхронного ввода-вывода через Asynchronous и о том, что CurrentUserOnly может разрешать подключения только процессам того же пользователя (и того же уровня повышения прав). 

  11. Microsoft Learn, Named Pipe Security and Access Rights. О составе прав доступа именованного канала; о том, что GENERIC_WRITE включает FILE_CREATE_PIPE_INSTANCE, поэтому выдача клиенту generic write также разрешает создать серверный экземпляр; и о том, что чтение и запись данных выдаются как отдельные права доступа. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Как выбирать между именованным каналом и TCP (сокетом localhost)?
Для межпроцессного взаимодействия на одной машине именованный канал — первый кандидат. Причина — модель безопасности. Канал позволяет на уровне ОС контролировать «кто может подключиться» через дескриптор безопасности Windows (ACL), а сервер может проверить и позаимствовать учётную запись Windows подключившегося собеседника с помощью ImpersonateNamedPipeClient. Это контрастирует с портом TCP на localhost, к которому может подключиться кто угодно, поэтому личность собеседника приходится устанавливать собственной аутентификацией. С другой стороны, варианты на TCP выгодны, когда позже вероятно удалённое взаимодействие, когда нужно говорить и с процессами на других ОС, или когда хочется повторно использовать уже имеющийся протокол вроде gRPC. Это же суждение разобрано в статье о выборе межпроцессного взаимодействия в Windows.
Что использовать — байтовый режим или режим сообщений?
Если нужно трактовать «одна запись = одна смысловая единица», удобен режим сообщений (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE). Приёмник читает в тех единицах, которые записал отправитель, поэтому границы не приходится вести самостоятельно. Байтовый режим — это «непрерывный поток байтов», как TCP, и фрейминг нужно проектировать самому — например, префикс длины. Если вы несёте протокол, у которого фрейминг уже есть (например, сериализованная форма с префиксом длины), байтовый режим подходит. Оговорка: даже в режиме сообщений, если приёмный буфер меньше сообщения, получается дробное чтение (ERROR_MORE_DATA), так что его всё равно нужно обрабатывать. Кроме того, режим чтения — настройка на уровне дескриптора, и CreateNamedPipe задаёт его только на стороне сервера. Клиент должен указать PIPE_READMODE_MESSAGE через SetNamedPipeHandleState после CreateFile. В .NET сервер задаёт PipeTransmissionMode.Message, а клиент после подключения ставит NamedPipeClientStream.ReadMode в Message.
Как построить сервер, который одновременно общается с несколькими клиентами?
Именованный канал позволяет создать несколько экземпляров с одним именем, и один экземпляр обслуживает одного клиента. Есть две формы. Первая — синхронная схема с потоком на каждого клиента; реализация прямолинейна, но потребляет один поток на клиента. Вторая — асинхронный ввод-вывод с FILE_FLAG_OVERLAPPED, когда небольшое число потоков обрабатывает ConnectNamedPipe, ReadFile и WriteFile для всех экземпляров; официальный пример Microsoft тоже показывает реализацию, которая обрабатывает несколько экземпляров в одном потоке. В .NET асинхронную форму можно написать почти с той же прямолинейностью, что и синхронную, через NamedPipeServerStream.WaitForConnectionAsync и async/await. Если нет особых причин, для новой реализации я рекомендую асинхронную форму .NET.
Что минимум нужно сделать для безопасности именованных каналов?
Четыре пункта. Первый: если удалённые подключения не нужны, укажите PIPE_REJECT_REMOTE_CLIENTS и явно откажите в подключениях по сети. Второй: задайте подходящий ACL через SECURITY_ATTRIBUTES и сузьте пользователей и группы, которым разрешено подключаться (ACL по умолчанию для некоторых сценариев слишком свободен). Третий: при создании первого экземпляра укажите FILE_FLAG_FIRST_PIPE_INSTANCE, чтобы обнаружить «захват имени», когда канал с тем же именем создают раньше вас (этот флаг не ставьте на второй и последующие экземпляры). Четвёртый: клиент, которому нужно лишь чтобы сервер его опознал, должен указать SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION в CreateFile, чтобы поддельный сервер не мог позаимствовать (олицетворить) его права. В брокерской схеме, где сервер выполняет реальный доступ под правами клиента, олицетворение должно быть разрешено, поэтому это ограничение применяют или нет в зависимости от того, позволяет ли проект серверу заимствовать права.
Есть ли оговорки при использовании ImpersonateNamedPipeClient?
Самое важное — проверять возвращаемое значение. Если продолжить после сбоя олицетворения, последующие операции идут с собственными (часто высокими) правами серверного процесса, и проходят операции, которые клиенту не должны были быть разрешены. Официальная документация прямо говорит, что при сбое запрос клиента выполнять нельзя. Нужно также вызывать её только после того, как что-то прочитали — олицетворение выполняется в контексте «последнего сообщения, прочитанного из канала», — и надёжно возвращаться к исходному контексту через RevertToSelf, когда работа закончена. Механизмы вокруг олицетворения (токены, уровни олицетворения, SeImpersonatePrivilege) подробно разобраны в статье о токенах олицетворения.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог