Named pipe trong thực tiễn — IPC chuẩn của Windows, từ thiết kế đến bảo mật

· · Windows, IPC, Phát triển Windows, C#, C++, Bảo mật, Win32 API

“Tôi muốn một dịch vụ thường trú và giao diện cài đặt trao đổi lệnh.” “Tôi muốn cô lập chỉ phần việc cần đặc quyền quản trị vào một tiến trình riêng.” “Tôi muốn các công cụ trên cùng một PC truyền dữ liệu cho nhau.” — Khi loại giao tiếp giữa các tiến trình (IPC) này trở nên cần thiết trên Windows, chuẩn bạn nên cân nhắc trước tiên là named pipe.

Bài viết về việc chọn cơ chế giao tiếp giữa các tiến trình trên Windows đã đặt named pipe ở vị trí “ứng viên đầu tiên cho IPC trên cùng một máy”. Bài này là phần xử lý chi tiết. Vì sao chúng là ứng viên đầu tiên, cách chọn chế độ và hình dạng máy chủ, và những gì bạn phải bảo vệ khi một dịch vụ đặc quyền dùng chúng — hướng tới các nhà phát triển viết ứng dụng nghiệp vụ và dịch vụ trên Windows, bài viết sắp xếp tư liệu cho những phán đoán thiết kế đó từ nguồn gốc.

1. Kết luận trước

  • Named pipe là kênh hai chiều giữa các tiến trình, với không gian tên dạng \\.\pipe\name. Bạn có thể tạo nhiều thực thể dưới cùng một tên và chấp nhận vài client cùng lúc.1
  • Lý do chúng là ứng viên đầu tiên cho IPC trên cùng một máy là mô hình bảo mật. Bạn kiểm soát ai được kết nối bằng ACL, và máy chủ có thể xem xét rồi mượn (mạo danh) tài khoản Windows của client. TCP localhost không có cả hai.2
  • Nếu muốn coi “một lần ghi = một thông điệp”, dùng chế độ thông điệp; nếu đã có framing riêng, dùng chế độ byte. Ngay cả trong chế độ thông điệp bạn vẫn phải xử lý lần đọc bị tách khi bộ đệm ngắn (ERROR_MORE_DATA).3
  • Xử lý nhiều client bằng “nhiều thực thể + I/O chồng lấp” hoặc “.NET async/await”. Mẫu chính thức cho thấy hình dạng xử lý nhiều thực thể trên một luồng duy nhất.4
  • Mức tối thiểu về bảo mật gồm bốn điểm: từ chối từ xa (PIPE_REJECT_REMOTE_CLIENTS), làm ACL rõ ràng, phát hiện chiếm đoạt bằng FILE_FLAG_FIRST_PIPE_INSTANCE, và giảm mức mạo danh ở phía client xuống mức cần thiết.56
  • Với ImpersonateNamedPipeClient, kiểm tra giá trị trả về là dây cứu sinh. Bỏ qua thất bại thì xử lý tiếp tục với đặc quyền của máy chủ.6

2. Named pipe là gì — Không gian tên, thực thể, và cách kết nối hoạt động

Named pipe là kênh được nhận diện bằng một tên như \\.\pipe\MyCompany.MyApp.Control. Máy chủ tạo nó bằng CreateNamedPipe, và client mở cùng tên đó bằng CreateFile. Một khi đã mở, cả hai phía đọc và ghi bằng ReadFile / WriteFile — điểm đặc trưng là bạn dùng nó theo cùng hình dạng với I/O tệp.1

Khái niệm quan trọng là thực thể (instance). Bạn có thể tạo nhiều thực thể của một pipe cùng tên, và một thực thể là một kênh với một client. Lần gọi CreateNamedPipe đầu tiên quyết định số thực thể tối đa (hoặc không giới hạn).3

Kết nối phía client có một công thức chuẩn. Khi mọi thực thể đều đang dùng, CreateFile thất bại với ERROR_PIPE_BUSY, nên bạn chờ một thực thể trống bằng WaitNamedPipe rồi thử lại. Ngoài ra, quyền truy cập bạn chỉ định khi mở phải khớp hướng mà máy chủ đã tạo — một pipe hai chiều có thể mở với đọc hoặc ghi, nhưng một pipe outbound mà máy chủ chỉ ghi phải được mở chỉ đọc, và một pipe inbound mà máy chủ chỉ đọc phải được mở chỉ ghi, nếu không CreateFile thất bại.7

Hướng của pipe và quyền truy cập phía clientClient có thể mở pipe hai chiều với đọc hoặc ghi, nhưng phải mở pipe outbound mà máy chủ chỉ ghi ở chế độ chỉ đọc, và pipe inbound mà máy chủ chỉ đọc ở chế độ chỉ ghiHai chiềuOutboundInboundHướng máy chủ đã tạo?Đọc hoặc ghi đều đượcMở chỉ đọcMở chỉ ghi

Hình 1: Lệch giữa hướng và quyền truy cập trở thành lỗi CreateFile. Khi điều tra lỗi kết nối, hãy kiểm tra chỗ này trước.

Cấu trúc cơ bản của named pipeMáy chủ tạo nhiều thực thể pipe cùng tên và chờ kết nối bằng ConnectNamedPipe; mỗi client mở tên đó bằng CreateFile và có kênh hai chiều một-một với một thực thểMáy chủThực thể 1Thực thể 2Thực thể 3Client AClient BClient C

Hình 2: Bằng cách giữ vài thực thể cùng tên, một máy chủ có thể nói chuyện một-một với vài client cùng lúc.

Named pipe cũng có thể được mở từ xa qua SMB (\\server\pipe\name), nhưng trong thiết kế hiện đại gần như không có lý do để chủ động dùng cách đó; vấn đề đúng hơn là đừng để nó mở khi bạn không dùng (Chương 5).

3. Chế độ byte và chế độ thông điệp

Một pipe có hai chế độ truyền.3

  • Chế độ byte (PIPE_TYPE_BYTE): một “luồng byte không ngắt” giống TCP. Bạn tự quyết định thông điệp kết thúc ở đâu (bạn thiết kế framing như tiền tố độ dài).
  • Chế độ thông điệp (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE): một lần ghi được coi là một thông điệp, và bên đọc nhận theo đơn vị đó. Cách này dễ hơn cho trao đổi yêu cầu/phản hồi.

Chế độ thông điệp còn có một bạn đồng hành tiện lợi, TransactNamedPipe, gửi yêu cầu và nhận phản hồi trong một lần gọi.8 Tuy nhiên có một cái bẫy. Nếu bộ đệm nhận nhỏ hơn toàn bộ thông điệp, lần đọc trả về ERROR_MORE_DATA và trở thành lần đọc bị tách. Đừng giả định rằng chế độ thông điệp nghĩa là “một Read luôn mang về toàn bộ”; bạn vẫn phải viết vòng lặp đọc phần còn lại. Lưu ý rằng chế độ đọc là thiết lập theo từng handle, và CreateNamedPipe chỉ quyết định nó ở phía máy chủ. Client chỉ định nó bằng SetNamedPipeHandleState sau CreateFile (trong .NET, ReadMode sau khi kết nối).7

Vòng lặp đọc phần còn lại ở chế độ thông điệpNếu ReadFile thành công thì thông điệp đã đủ; nếu trả về ERROR_MORE_DATA bạn đọc phần còn lại không vừa bộ đệm rồi nối vào; mọi lỗi khác được coi là ngắt kết nốiThành côngERROR_MORE_DATAMọi lỗi khácĐọc bằng ReadFileKết quả?Thông điệp đủĐọc phần còn lại rồi nốiCoi là ngắt kết nối

Hình 3: Ngay cả ở chế độ thông điệp bạn vẫn cần “vòng lặp đọc phần còn lại”; thiếu nó thì chỉ các thông điệp lớn bị hỏng.

Khác biệt giữa chế độ byte và chế độ thông điệpỞ chế độ byte ba lần ghi trở thành một luồng byte không ngắt và bên nhận phải tự tách; ở chế độ thông điệp đơn vị mỗi lần ghi được giữ nguyên và tới bên nhận như vậyChế độ byte: ghi AAA, BB, CCCCNhận thành luồng byte AAABBCCCCBạn tự thiết kế framingChế độ thông điệp: cùng ba lần ghiNhận thành ba thông điệp: AAA, BB, CCCCĐơn vị ghi được giữ nguyên

Hình 4: Chế độ thông điệp giữ “đơn vị của một lần ghi” và chuyển giao nó. Thiết kế framing trở nên không cần; chỉ đừng quên xử lý lần đọc bị tách.

Quy tắc thực tiễn để chọn rất đơn giản. Nếu trao đổi có hình dạng “yêu cầu và phản hồi”, chế độ thông điệp. Nếu bạn đang mang một dạng đã có framing sẵn (dữ liệu tuần tự hóa có tiền tố độ dài hoặc truyền luồng), dùng chế độ byte. Trong .NET, chỉ định PipeTransmissionMode.Message tương ứng với cách trước.9

4. Thiết kế máy chủ — Một luồng mỗi client, hay chồng lấp?

Thao tác cơ bản của máy chủ là vòng lặp “tạo thực thể → chờ client bằng ConnectNamedPipe → đọc và ghi → ngắt kết nối rồi sang client tiếp”. Có hai hình dạng để nói chuyện với vài client cùng lúc.

Đồng bộ, một luồng mỗi thực thể. Bạn gán một luồng cho mỗi thực thể, và mỗi luồng nói chuyện với client của mình bằng I/O đồng bộ. Mã thẳng thắn, nhưng bạn tiêu thụ một luồng mỗi client, và cũng cần cách thoát khỏi I/O chặn khi tắt toàn bộ hệ thống.

Chồng lấp (bất đồng bộ). Bạn tạo thực thể với FILE_FLAG_OVERLAPPED, phát ConnectNamedPipe / ReadFile / WriteFile bất đồng bộ, và để một số ít luồng xử lý hoàn tất cho mọi thực thể. Mẫu chính thức của Microsoft cho thấy một máy chủ chờ trên mảng sự kiện bằng WaitForMultipleObjectsxử lý nhiều thực thể trên một luồng duy nhất.4 Phần trình bày chung về I/O bất đồng bộ như đã giải thích trong bài loạt về I/O, và ở quy mô lớn hơn bạn cũng có thể gắn IOCP hoặc I/O của thread pool.

Cấu trúc máy chủ chồng lấpHoàn tất các thao tác bất đồng bộ của từng thực thể được nhận trên mảng sự kiện, và một số ít luồng chờ bằng WaitForMultipleObjects rồi đẩy thực thể đã hoàn tất, tách số luồng khỏi số clientThao tác bất đồng bộ thực thể 1Mảng sự kiệnThao tác bất đồng bộ thực thể 2Thao tác bất đồng bộ thực thể 3Chờ hoàn tất bằng WaitForMultipleObjectsĐẩy thực thể đã hoàn tất

Hình 5: Hình dạng chồng lấp tách số luồng khỏi số client. Mẫu chính thức chạy vòng này trên một luồng duy nhất.

.NET gần như xóa lựa chọn này. Dùng WaitForConnectionAsync / ReadAsync / WriteAsync của NamedPipeServerStream cùng async/await, bạn có hiệu quả chồng lấp trong mã thẳng thắn như hình dạng đồng bộ.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 là chỉ định “chỉ cho phép kết nối từ các tiến trình của cùng người dùng”, một mặc định tiện và an toàn giúp bạn khỏi phải tự viết ACL.10 Nó không dùng được trong cấu hình xuyên người dùng (một dịch vụ ↔ một ứng dụng trong phiên người dùng, và tương tự), nên khi đó bạn chuyển sang thiết kế ACL ở chương tiếp theo.

Vòng chấp nhận của máy chủ bất đồng bộ .NETVòng chấp nhận tạo NamedPipeServerStream, chờ kết nối bằng WaitForConnectionAsync, và khi tới thì tách xử lý client bất đồng bộ rồi ngay lập tức trở lại lần chấp nhận tiếp, nên các kết nối đồng thời được xử lý bằng mã thẳng thắnTạo luồng máy chủChờ bằng WaitForConnectionAsyncKết nối tớiTách xử lý client bất đồng bộ

Hình 6: Vòng chấp nhận bám chu kỳ “chờ → tách → tiếp”, và xử lý của từng client tiến hành song song.

5. Bảo mật — Bốn việc bắt buộc khi dịch vụ đặc quyền dùng pipe

Lý do lớn nhất khiến named pipe là ứng viên đầu tiên cho IPC trên cùng một máy là mô hình bảo mật, nhưng điều đó chỉ đúng nếu bạn cấu hình đúng. Đặc biệt trong thiết kế broker kiểu “dịch vụ đặc quyền quản trị + ứng dụng UI đặc quyền thấp”, pipe chính là ranh giới đặc quyền. Có bốn điểm cần chốt.

(1) Từ chối từ xa. Một pipe bạn định dùng làm IPC cục bộ mà lại mở được từ mạng, bản thân đã là bề mặt tấn công. Chỉ định PIPE_REJECT_REMOTE_CLIENTS trên CreateNamedPipe thì kết nối client từ xa bị từ chối tự động.5

(2) Làm ACL rõ ràng. Truyền security descriptor trong SECURITY_ATTRIBUTES và thu hẹp người dùng cùng nhóm được phép kết nối. Đừng cấp GENERIC_WRITE cho client — quyền FILE_CREATE_PIPE_INSTANCE nằm trong đó sẽ để một client đã được phép tự tạo thực thể máy chủ cùng tên và cướp các kết nối sau. Cấp đọc và ghi như các quyền riêng, và đừng truyền quyền tạo thực thể.11

(3) Ngăn chiếm tên. Tên pipe theo kiểu ai đến trước được trước. Nếu một tiến trình độc hại tạo pipe cùng tên trước rồi chờ, client kết nối tới máy chủ giả. Máy chủ chỉ định FILE_FLAG_FIRST_PIPE_INSTANCE khi tạo thực thể đầu tiên, bảo đảm “tôi là người đầu”, và nếu việc đó thất bại thì nghi chiếm đoạt rồi dừng. Cờ này chỉ dành cho thực thể đầu tiên tuyên bố tên; gắn nó vào thực thể thứ hai trở đi làm việc tạo thất bại.3

(4) Client giảm mức mạo danh xuống mức cần thiết. Đây là sự chuẩn bị cho trường hợp đối phương là máy chủ giả. Nếu client chỉ định **SECURITY_SQOS_PRESENT SECURITY_IDENTIFICATION** trên CreateFile, máy chủ có thể nhận diện client nhưng không thể mượn những đặc quyền đó rồi hành động.2 Đây là sự đánh đổi với luồng làm việc mạo danh — trong thiết kế broker, nơi máy chủ thực hiện truy cập thật dưới đặc quyền của client, mức nhận diện không đủ để mạo danh thành công, và bạn cần cho phép SECURITY_IMPERSONATION. Việc cho phép đó có điều kiện là chắc chắn bạn đang kết nối tới máy chủ thật. Chống chiếm đoạt phía máy chủ chỉ là cơ chế nhận ra qua lần khởi động thất bại; nếu dịch vụ thật vắng mặt và kẻ tấn công tạo pipe cùng tên trước, client vẫn có thể kết nối tới máy chủ giả. Chỉ cho phép khi bạn xác nhận đối phương qua khởi động dịch vụ được bảo đảm hoặc xác thực hai chiều sau khi kết nối.

Việc kiểm tra danh tính và mượn đặc quyền phía máy chủ là ImpersonateNamedPipeClient. Gọi hàm này sau khi đọc một yêu cầu từ pipe thì luồng gọi bắt đầu chạy trong ngữ cảnh bảo mật của người gửi thông điệp cuối cùng đã đọc. Mở một tệp với đặc quyền của client thì việc kiểm tra truy cập được thực hiện đối với client — cơ chế để một dịch vụ đặc quyền thực thi “thao tác được yêu cầu, với đặc quyền của người yêu cầu”.6 Điều kiện tuyệt đối khi dùng nó là kiểm tra giá trị trả về. Tiếp tục sau khi mạo danh thất bại thì các thao tác sau chạy với đặc quyền cao của chính máy chủ. Tài liệu chính thức nêu rõ rằng “khi thất bại bạn không được thực thi yêu cầu của client”. Cùng với RevertToSelf sau khi xong việc, các thực hành trong bài viết về token mạo danh áp dụng nguyên như vậy.

Luồng xử lý yêu cầu dùng mạo danhMáy chủ đọc yêu cầu từ pipe, xác nhận ImpersonateNamedPipeClient thành công, rồi thực hiện thao tác với đặc quyền của client và trở về ngữ cảnh riêng bằng RevertToSelf. Nếu mạo danh thất bại thì từ chối yêu cầu mà không thực thiMáy chủClientMáy chủClientKhi thất bại, từ chối mà không thực thi yêu cầuGửi yêu cầuĐọc yêu cầuImpersonateNamedPipeClientThực hiện thao tác với đặc quyền của clientRevertToSelf để khôi phục ngữ cảnh gốcTrả lời kết quả

Hình 7: Xác nhận mạo danh thành công và RevertToSelf đáng tin cậy đi thành một gói. Tiếp tục khi thất bại thì chạy với đặc quyền của máy chủ.

Bốn điểm bảo vệ pipe của dịch vụ đặc quyềnPhía máy chủ củng cố cửa vào bằng từ chối từ xa, ACL rõ ràng và bảo đảm thực thể đầu; phía client chỉ định mức mạo danh tối thiểu cần thiết để máy chủ giả không mượn được đặc quyền(thu hẹp về mức nhận diện nếu thiết kế không để máy chủ mượn đặc quyền)Phía clientChỉ định mức mạo danh tối thiểuPhía máy chủPIPE_REJECT_REMOTE_CLIENTSHạn chế người kết nối bằng ACLFIRST_PIPE_INSTANCE(chỉ thực thể đầu)Pipe như ranh giới đặc quyền

Hình 8: Trong thiết kế mà pipe là ranh giới đặc quyền, hãy triển khai ba điểm phía máy chủ cộng một điểm phía client như một bộ.

6. Những cái bẫy thực tiễn

Cuộc đua thứ tự khởi động. Nếu client tới kết nối trước khi máy chủ tạo pipe, bạn gặp lỗi “pipe không tồn tại”. Phía client xây sẵn “không tồn tại → chờ một chút rồi thử lại”. Ngược lại, nguyên tắc phía máy chủ là bắt đầu lắng nghe bằng ConnectNamedPipe trước khi client khởi động.8

Luồng thử lại kết nối phía clientMở pipe bằng CreateFile; nếu pipe không tồn tại thì chờ ngắn rồi thử lại; nếu mọi thực thể đều đang dùng(ERROR_PIPE_BUSY) thì chờ một thực thể trống bằng WaitNamedPipe rồi thử lại; khi thành công thì vào giao tiếpThành côngPipe không tồn tạiERROR_PIPE_BUSYMở bằng CreateFileKết quả?Bắt đầu giao tiếpChờ ngắn(máy chủ chưa chạy)Chờ thực thể trống bằng WaitNamedPipe

Hình 9: Xử lý kết nối phía client phân biệt hai loại thất bại, “không tồn tại” và “đầy”, rồi đưa cả hai trở lại lần thử lại.

Phát hiện ngắt kết nối. Khi đối phương thoát, Read/Write thất bại với ERROR_BROKEN_PIPE và tương tự. Đó không phải bất thường; đó là giao tiếp hàng ngày. Máy chủ phát hiện ngắt kết nối, DisconnectNamedPipe thực thể, và chuẩn bị cho kết nối tiếp; client kết nối lại — ý tưởng “kết nối lại idempotent” được mô tả trong bài viết về ngủ/thức cũng áp dụng ở đây.

Giả định về kích thước thông điệp. Bên cạnh các lần đọc bị tách của chế độ thông điệp (Chương 3), nếu bạn không quyết định như một phần của giao thức “một thông điệp tối đa bao nhiêu byte”, một đối phương độc hại (hoặc có lỗi) có thể lãng phí bộ nhớ của bạn bằng một thông điệp khổng lồ. Quyết định một giới hạn trên, và ngắt kết nối nếu vượt; đó là cách tiếp cận an toàn.

Hoàn tất ghi và việc đối phương nhận là hai việc khác nhau. Thành công của WriteFile không có nghĩa ứng dụng phía bên kia đã xử lý dữ liệu. Các thao tác cần chắc chắn được bảo đảm bằng những thiết kế như xác nhận bằng thông điệp phản hồi, và đưa sự tương ứng giữa yêu cầu và phản hồi vào giao thức.

7. Tóm tắt

  • Named pipe là ứng viên đầu tiên cho IPC trên cùng một máy. Lý do là sự dễ dùng giống I/O tệp, và sự gắn kết với mô hình bảo mật Windows gồm ACL và mạo danh.
  • Chọn chế độ là “chế độ thông điệp cho yêu cầu và phản hồi, chế độ byte nếu bạn đã có framing riêng”. Ngay cả ở chế độ thông điệp bạn vẫn phải xử lý lần đọc bị tách (ERROR_MORE_DATA).
  • Nhiều client là nhiều thực thể + chồng lấp, hoặc .NET async/await. Với việc mới, hình dạng bất đồng bộ của .NET là cách thẳng thắn.
  • Trên một pipe là ranh giới đặc quyền, hãy lấy từ chối từ xa, ACL rõ ràng, FIRST_PIPE_INSTANCE (chỉ thực thể đầu), và giảm mức mạo danh phía client như một bộ.
  • Với ImpersonateNamedPipeClient, kiểm tra giá trị trả về và RevertToSelf là dây cứu sinh.
  • Đan “ngày thường của giao tiếp” — thứ tự khởi động, ngắt kết nối, giới hạn trên của thông điệp, xác nhận phản hồi — vào thiết kế giao thức.

Named pipe là API cũ, nhưng với mục đích “để các tiến trình nói chuyện với nhau trên cùng một máy trong khi tôn trọng ranh giới tài khoản Windows”, chúng vẫn là công cụ tự nhiên nhất cho công việc. Các điểm phán đoán thiết kế gần như đã hết trong phạm vi bài này. Sau đó, hãy viết giao thức của riêng bạn ra một tờ giấy trước khi bắt đầu triển khai.

Bài viết liên quan

Lĩnh vực tư vấn liên quan

KomuraSoft LLC đảm nhận thiết kế và triển khai liên quan giao tiếp giữa các tiến trình — tách dịch vụ khỏi ứng dụng UI, cô lập đặc quyền quản trị, và tương tự — thay thế IPC hiện có (bộ nhớ dùng chung, socket tự viết, COM, v.v.) bằng named pipe, và rà soát bảo mật giao tiếp pipe của dịch vụ đặc quyền. Tư vấn bắt đầu từ việc cùng soi thiết kế giao thức đều được chào đón.

Liên kết tham khảo

  1. Microsoft Learn, Named Pipes. Về việc named pipe là kênh một chiều hoặc hai chiều giữa máy chủ pipe và một hoặc nhiều client pipe; về việc mọi thực thể chia sẻ cùng tên trong khi có bộ đệm và handle độc lập; và về việc dùng được từ tiến trình cục bộ lẫn từ xa.  2

  2. Microsoft Learn, Impersonating a Named Pipe Client. Về việc mạo danh để luồng máy chủ vận hành trong đặc quyền của client; về mức mạo danh mặc định là SecurityImpersonation; và về việc client có thể kiểm soát mức mạo danh bằng cờ SECURITY_SQOS_PRESENT tại thời điểm CreateFile (SECURITY_IDENTIFICATION chỉ cho phép nhận diện).  2

  3. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). Về hướng pipe (inbound, outbound, hai chiều), kiểu byte và kiểu thông điệp (PIPE_TYPE_BYTE / PIPE_TYPE_MESSAGE) cùng chế độ đọc (PIPE_READMODE_MESSAGE), số thực thể tối đa (PIPE_UNLIMITED_INSTANCES), chế độ bất đồng bộ qua FILE_FLAG_OVERLAPPED, bảo đảm thực thể đầu qua FILE_FLAG_FIRST_PIPE_INSTANCE, và thời gian chờ mặc định cho WaitNamedPipe.  2 3 4

  4. Microsoft Learn, Named Pipe Server Using Overlapped I/O. Về mẫu chính thức của máy chủ một luồng xử lý kết nối đồng thời với nhiều client qua thao tác chồng lấp. Về hình dạng chờ trên cấu trúc OVERLAPPED và sự kiện của từng thực thể bằng WaitForMultipleObjects rồi đẩy máy trạng thái của thực thể đã hoàn tất, và về xác nhận hoàn tất I/O đang chờ bằng GetOverlappedResult.  2

  5. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). Về hai chế độ client từ xa, PIPE_ACCEPT_REMOTE_CLIENTS (chấp nhận kết nối từ xa và kiểm tra chúng đối với security descriptor) và PIPE_REJECT_REMOTE_CLIENTS (tự động từ chối kết nối client từ xa).  2

  6. Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). Về việc một luồng phía máy chủ bắt đầu mạo danh trong ngữ cảnh bảo mật của client của thông điệp cuối cùng đọc từ pipe; về việc trở về bằng RevertToSelf sau khi hoàn tất; và về việc tiếp tục sau khi mạo danh thất bại khiến thực thi trong ngữ cảnh (đặc quyền) của chính tiến trình máy chủ, nên luôn phải kiểm tra giá trị trả về và khi thất bại không được thực thi yêu cầu của client.  2 3

  7. Microsoft Learn, Named Pipe Client. Về việc client mở pipe bằng CreateFile; về ERROR_PIPE_BUSY khi mọi thực thể đang dùng, chờ một thực thể trống bằng WaitNamedPipe; và về handle đã mở mặc định là đọc byte, chặn, và không chồng lấp, với SetNamedPipeHandleState có thể đổi sang chế độ đọc thông điệp.  2

  8. Microsoft Learn, Named Pipe Operations. Về thao tác chồng lấp qua ReadFileEx / WriteFileEx, lần đọc không tiêu thụ qua PeekNamedPipe, TransactNamedPipe thực hiện gửi yêu cầu và nhận phản hồi trong một lần gọi trên pipe hai chiều kiểu thông điệp, và lần đọc chặn trước khi client khởi động có thể gây cuộc đua.  2

  9. Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). Về kết nối và đọc/ghi bằng NamedPipeServerStream / NamedPipeClientStream, truyền theo đơn vị thông điệp qua PipeTransmissionMode.Message, và xử lý nhiều client bằng phương thức bất đồng bộ.  2

  10. Microsoft Learn, PipeOptions Enum (System.IO.Pipes). Về việc bật I/O bất đồng bộ bằng Asynchronous, và về CurrentUserOnly có thể chỉ cho phép kết nối với các tiến trình của cùng người dùng (và cùng mức nâng quyền). 

  11. Microsoft Learn, Named Pipe Security and Access Rights. Về thành phần các quyền truy cập named pipe; về việc GENERIC_WRITE bao gồm FILE_CREATE_PIPE_INSTANCE, nên cấp generic write cho client cũng cho phép tạo thực thể máy chủ; và về việc đọc và ghi dữ liệu được cấp như các quyền truy cập riêng. 

Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.

Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.

Bài viết liên quan trực tiếp đến các dịch vụ sau.

Câu hỏi thường gặp

Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.

Nên chọn named pipe hay TCP (socket localhost) như thế nào?
Với giao tiếp giữa các tiến trình trên cùng một máy, named pipe là ứng viên đầu tiên. Lý do nằm ở mô hình bảo mật. Một pipe có thể kiểm soát "ai được kết nối" ở cấp hệ điều hành bằng security descriptor của Windows (ACL), và máy chủ có thể xem xét rồi mượn tài khoản Windows của bên kết nối bằng ImpersonateNamedPipeClient. Điều đó khác với cổng TCP localhost, nơi bất kỳ ai cũng có thể kết nối nên bạn phải tự xác thực đối phương. Mặt khác, các lựa chọn dựa trên TCP có lợi khi khả năng giao tiếp từ xa sẽ xuất hiện sau này, khi bạn cũng nói chuyện với tiến trình trên hệ điều hành khác, hoặc khi muốn tái sử dụng một giao thức sẵn có như gRPC. Cách đánh giá này cũng được trình bày trong bài viết về việc chọn cơ chế giao tiếp giữa các tiến trình trên Windows.
Nên dùng chế độ byte hay chế độ thông điệp?
Nếu muốn coi "một lần ghi = một đơn vị ý nghĩa", chế độ thông điệp (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE) rất tiện. Bên nhận đọc theo đúng đơn vị mà bên gửi đã ghi, nên bạn không phải tự quản lý ranh giới. Chế độ byte là một "luồng byte không ngắt" giống TCP, và bạn phải tự thiết kế khung (framing) — ví dụ tiền tố độ dài. Nếu bạn đang mang một giao thức đã có framing (ví dụ dạng tuần tự hóa có tiền tố độ dài), chế độ byte là ổn. Một lưu ý: ngay cả trong chế độ thông điệp, nếu bộ đệm nhận nhỏ hơn thông điệp thì bạn nhận được lần đọc bị tách (ERROR_MORE_DATA), nên vẫn phải xử lý. Ngoài ra, chế độ đọc là thiết lập theo từng handle, và CreateNamedPipe chỉ đặt nó ở phía máy chủ. Client phải chỉ định PIPE_READMODE_MESSAGE bằng SetNamedPipeHandleState sau CreateFile. Trong .NET, máy chủ chỉ định PipeTransmissionMode.Message, và client gán NamedPipeClientStream.ReadMode thành Message sau khi kết nối.
Làm sao xây máy chủ nói chuyện với nhiều client cùng lúc?
Một named pipe có thể tạo nhiều thực thể dưới cùng một tên, và một thực thể phục vụ một client. Có hai hình dạng. Một là thiết kế đồng bộ, gán một luồng cho mỗi client; triển khai thẳng thắn, nhưng tiêu thụ một luồng mỗi client. Cách kia là dùng I/O bất đồng bộ với FILE_FLAG_OVERLAPPED và để một số ít luồng xử lý ConnectNamedPipe, ReadFile và WriteFile cho mọi thực thể; mẫu chính thức của Microsoft cũng cho thấy một triển khai xử lý nhiều thực thể trên một luồng duy nhất. Trong .NET bạn có thể viết hình dạng bất đồng bộ gần như thẳng thắn như hình dạng đồng bộ, bằng NamedPipeServerStream.WaitForConnectionAsync và async/await. Trừ khi có lý do đặc biệt, hình dạng bất đồng bộ của .NET là thứ tôi khuyến nghị cho triển khai mới.
Mức tối thiểu tôi nên làm cho bảo mật named pipe là gì?
Bốn điểm. Thứ nhất, nếu không cần kết nối từ xa, chỉ định PIPE_REJECT_REMOTE_CLIENTS và từ chối rõ ràng kết nối qua mạng. Thứ hai, đặt ACL thích hợp bằng SECURITY_ATTRIBUTES và thu hẹp người dùng cùng nhóm được phép kết nối (ACL mặc định quá lỏng với một số mục đích). Thứ ba, chỉ định FILE_FLAG_FIRST_PIPE_INSTANCE khi tạo thực thể đầu tiên, để phát hiện "chiếm tên" — một pipe cùng tên được tạo trước (đừng gắn cờ này vào thực thể thứ hai trở đi). Thứ tư, client chỉ muốn máy chủ nhận diện mình nên chỉ định SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION trên CreateFile, để một máy chủ giả không thể mượn (mạo danh) đặc quyền của nó. Trong thiết kế broker, nơi máy chủ thực hiện truy cập thật dưới đặc quyền của client, việc mạo danh phải được phép, nên bạn dùng hoặc không dùng hạn chế này tùy thiết kế có để máy chủ mượn đặc quyền hay không.
Có lưu ý nào khi dùng ImpersonateNamedPipeClient?
Quan trọng nhất là kiểm tra giá trị trả về. Nếu bạn tiếp tục sau khi mạo danh thất bại, các thao tác sau chạy với đặc quyền riêng của tiến trình máy chủ (thường là cao), và những thao tác lẽ ra không được phép với client vẫn được thông qua. Tài liệu chính thức cũng nêu rõ rằng khi thất bại bạn không được thực thi yêu cầu của client. Bạn cũng cần chỉ gọi nó sau khi đã đọc gì đó — việc mạo danh được thực hiện trong ngữ cảnh "thông điệp cuối cùng đọc từ pipe" — và trở về đáng tin cậy ngữ cảnh gốc bằng RevertToSelf khi xong việc. Cơ chế quanh mạo danh (token, mức mạo danh, SeImpersonatePrivilege) được trình bày chi tiết trong bài viết về token mạo danh.

Hồ sơ tác giả

Trang giới thiệu tác giả bài viết.

Go Komura

Đại diện của KomuraSoft LLC

Chuyên về phát triển phần mềm Windows, tư vấn kỹ thuật và điều tra lỗi, đặc biệt trong các dự án có hệ thống hiện hữu và lỗi khó tái hiện.

Liên kết công khai

Quay lại blog