Named Pipe ในทางปฏิบัติ — IPC มาตรฐานของ Windows ตั้งแต่การออกแบบถึงความปลอดภัย

· · Windows, IPC, การพัฒนา Windows, C#, C++, ความปลอดภัย, Win32 API

“อยากให้บริการที่รันอยู่กับ UI ตั้งค่าแลกคำสั่งกัน” “อยากแยกเฉพาะงานที่ต้องใช้สิทธิ์ผู้ดูแลระบบไปอีกโปรเซส” “อยากให้เครื่องมือบนเครื่องเดียวกันส่งข้อมูลหากัน” — เมื่อการสื่อสารระหว่างโปรเซส (IPC) แบบนี้จำเป็นบน Windows สิ่งมาตรฐานที่ควรพิจารณาเป็นอันดับแรกคือ named pipe

บทความเรื่องการเลือกการสื่อสารระหว่างโปรเซสบน Windows จัดตำแหน่ง named pipe ว่าเป็น “ตัวเลือกแรกสำหรับ IPC บนเครื่องเดียวกัน” บทความนี้คือการลงรายละเอียด ทำไมจึงเป็นตัวเลือกแรก จะเลือกโหมดและรูปเซิร์ฟเวอร์อย่างไร และต้องปกป้องอะไรเมื่อบริการที่มีสิทธิ์สูงใช้งาน — มุ่งไปที่นักพัฒนาที่เขียนแอปธุรกิจและบริการบน Windows โดยจัดระเบียบวัสดุสำหรับการตัดสินใจออกแบบจากแหล่งปฐมภูมิ

1. สรุปก่อนเลย

  • named pipe คือช่องสื่อสารสองทิศทางระหว่างโปรเซสที่มีเนมสเปซรูป \\.\pipe\name คุณสร้างหลายอินสแตนซ์ภายใต้ชื่อเดียวกันและรับหลายไคลเอนต์พร้อมกันได้1
  • เหตุที่มันเป็นตัวเลือกแรกของ IPC บนเครื่องเดียวกันคือโมเดลความปลอดภัย คุณควบคุมผู้ที่เชื่อมต่อด้วย ACL และเซิร์ฟเวอร์สามารถตรวจและยืม (impersonate) บัญชี Windows ของไคลเอนต์ได้ localhost TCP ไม่มีทั้งสองอย่าง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. named pipe คืออะไร — เนมสเปซ อินสแตนซ์ และวิธีเชื่อมต่อ

named pipe คือช่องที่ระบุด้วยชื่อ เช่น \\.\pipe\MyCompany.MyApp.Control เซิร์ฟเวอร์สร้างด้วย CreateNamedPipe และไคลเอนต์เปิดชื่อเดียวกันด้วย CreateFile เมื่อเปิดแล้วทั้งสองฝั่งอ่านเขียนด้วย ReadFile / WriteFile — จุดเด่นคือใช้ได้ในรูปเดียวกับไฟล์ I/O1

แนวคิดสำคัญคือ อินสแตนซ์ คุณสร้างหลายอินสแตนซ์ของท่อชื่อเดียวกันได้ และหนึ่งอินสแตนซ์คือหนึ่งช่องกับหนึ่งไคลเอนต์ การเรียก CreateNamedPipe ครั้งแรกกำหนดจำนวนอินสแตนซ์สูงสุด (หรือไม่จำกัด)3

การเชื่อมต่อฝั่งไคลเอนต์มีสูตรมาตรฐาน เมื่อทุกอินสแตนซ์ถูกใช้ CreateFile ล้มเหลวด้วย ERROR_PIPE_BUSY จึงรอช่องว่างด้วย WaitNamedPipe แล้วลองใหม่ นอกจากนี้สิทธิ์ที่ระบุตอนเปิดต้องตรงกับทิศทางที่เซิร์ฟเวอร์สร้าง — ท่อสองทิศทางเปิดด้วยอ่านหรือเขียนก็ได้ แต่ท่อขาออกที่เซิร์ฟเวอร์เขียนอย่างเดียวต้องเปิดแบบอ่านอย่างเดียว และท่อขาเข้าที่เซิร์ฟเวอร์อ่านอย่างเดียวต้องเปิดแบบเขียนอย่างเดียว ไม่เช่นนั้น CreateFile จะล้มเหลว7

ทิศทางของท่อกับสิทธิ์ที่ไคลเอนต์ระบุไคลเอนต์เปิดท่อสองทิศทางด้วยอ่านหรือเขียนก็ได้ แต่ต้องเปิดท่อขาออกที่เซิร์ฟเวอร์เขียนอย่างเดียวแบบอ่านอย่างเดียว และเปิดท่อขาเข้าที่เซิร์ฟเวอร์อ่านอย่างเดียวแบบเขียนอย่างเดียวสองทิศทางขาออกขาเข้าทิศทางที่เซิร์ฟเวอร์สร้าง?อ่านหรือเขียนก็ได้เปิดแบบอ่านอย่างเดียวเปิดแบบเขียนอย่างเดียว

ภาพ 1: หากทิศทางกับสิทธิ์การเข้าถึงไม่ตรงกัน CreateFile จะล้มเหลว เมื่อไล่ข้อผิดพลาดการเชื่อมต่อ ให้ตรวจจุดนี้ก่อน

โครงสร้างพื้นฐานของ named pipeเซิร์ฟเวอร์สร้างหลายอินสแตนซ์ของท่อชื่อเดียวกันและรอการเชื่อมต่อด้วย ConnectNamedPipe แต่ละไคลเอนต์เปิดชื่อด้วย CreateFile และมีช่องสองทิศทางแบบหนึ่งต่อหนึ่งกับหนึ่งอินสแตนซ์เซิร์ฟเวอร์อินสแตนซ์ 1อินสแตนซ์ 2อินสแตนซ์ 3ไคลเอนต์ Aไคลเอนต์ Bไคลเอนต์ C

ภาพ 2: การถือหลายอินสแตนซ์ชื่อเดียวกัน ทำให้เซิร์ฟเวอร์หนึ่งคุยแบบหนึ่งต่อหนึ่งกับหลายไคลเอนต์พร้อมกันได้

named pipe ยังเปิดระยะไกลผ่าน 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 → อ่านเขียน → ตัดการเชื่อมต่อแล้วไปไคลเอนต์ถัดไป” มีสองรูปแบบสำหรับการคุยกับหลายไคลเอนต์พร้อมกัน

แบบซิงโครนัส หนึ่งเธรดต่ออินสแตนซ์ คุณจัดสรรเธรดหนึ่งเส้นให้อินสแตนซ์แต่ละตัว และแต่ละเส้นคุยกับไคลเอนต์ของตนด้วย I/O แบบซิงโครนัส โค้ดตรงไปตรงมา แต่กินเธรดหนึ่งเส้นต่อไคลเอนต์ และยังต้องมีวิธีออกจาก I/O ที่บล็อกเมื่อปิดระบบทั้งก้อน

overlapped (อะซิงโครนัส) คุณสร้างอินสแตนซ์ด้วย FILE_FLAG_OVERLAPPED ออก ConnectNamedPipe / ReadFile / WriteFile แบบอะซิงโครนัส และให้เธรดจำนวนน้อยจัดการความเสร็จของทุกอินสแตนซ์ ตัวอย่างทางการของ Microsoft แสดงเซิร์ฟเวอร์ที่รออาร์เรย์ของอีเวนต์ด้วย WaitForMultipleObjects และประมวลผลหลายอินสแตนซ์บนเธรดเดียว4 คำอธิบายทั่วไปของ I/O แบบอะซิงโครนัสอยู่ในบทความซีรีส์ I/O และที่ขนาดใหญ่ขึ้นคุณยังแนบ IOCP หรือ I/O ของ thread pool ได้

โครงสร้างของเซิร์ฟเวอร์แบบ 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. ความปลอดภัย — สี่สิ่งที่ต้องทำเมื่อบริการที่มีสิทธิ์สูงใช้ท่อ

เหตุผลใหญ่ที่สุดที่ named pipe เป็นตัวเลือกแรกของ IPC บนเครื่องเดียวกันคือโมเดลความปลอดภัย แต่เป็นเช่นนั้นก็ต่อเมื่อคุณตั้งค่าถูกต้อง โดยเฉพาะในการออกแบบแบบโบรกเกอร์ แบบ “บริการสิทธิ์ผู้ดูแลระบบ + แอป UI สิทธิ์ต่ำ” ท่อคือขอบเขตสิทธิ์เอง มีสี่จุดที่ต้องตรึงไว้

(1) ปฏิเสธระยะไกล ท่อที่ตั้งใจเป็น IPC ในเครื่องแต่เปิดจากเครือข่ายได้ คือพื้นผิวโจมตีในตัว ระบุ PIPE_REJECT_REMOTE_CLIENTS บน CreateNamedPipe แล้วการเชื่อมต่อของไคลเอนต์ระยะไกลจะถูกปฏิเสธอัตโนมัติ5

(2) ทำให้ ACL ชัดเจน ส่ง security descriptor ใน SECURITY_ATTRIBUTES และจำกัดผู้ใช้กับกลุ่มที่อนุญาตให้เชื่อมต่อ อย่าให้ไคลเอนต์ GENERIC_WRITE — สิทธิ์ FILE_CREATE_PIPE_INSTANCE ที่รวมอยู่ในนั้นจะให้ไคลเอนต์ที่ได้รับอนุญาตสร้างอินสแตนซ์เซิร์ฟเวอร์ชื่อเดียวกันเองและขโมยการเชื่อมต่อถัดไป ให้สิทธิ์อ่านและเขียนเป็นสิทธิ์เดี่ยว และอย่าส่งสิทธิ์สร้างอินสแตนซ์11

(3) ป้องกันการแย่งชื่อ ชื่อท่อเป็นแบบมาก่อนได้ก่อน หากโปรเซสประสงค์ร้ายสร้างท่อชื่อเดียวกันก่อนแล้วรอ ไคลเอนต์จะเชื่อมต่อกับเซิร์ฟเวอร์ปลอม เซิร์ฟเวอร์ระบุ FILE_FLAG_FIRST_PIPE_INSTANCE เมื่อสร้างอินสแตนซ์แรก เพื่อรับประกันว่า “ฉันมาก่อน” และหากล้มเหลวให้สงสัยการแย่งแล้วหยุด แฟล็กนี้มีไว้เฉพาะอินสแตนซ์แรกที่อ้างชื่อ การใส่กับอินสแตนซ์ที่สองเป็นต้นไปจะทำให้การสร้างล้มเหลว3

(4) ไคลเอนต์ลดระดับการปลอมตัวให้เหลือเท่าที่จำเป็น นี่คือการเตรียมกรณีที่คู่สื่อสารเป็นเซิร์ฟเวอร์ปลอม หากไคลเอนต์ระบุ **SECURITY_SQOS_PRESENT SECURITY_IDENTIFICATION** บน CreateFile เซิร์ฟเวอร์ระบุตัวไคลเอนต์ได้แต่ยืมสิทธิ์นั้นไปกระทำการไม่ได้2 นี่คือการแลกกับเวิร์กโฟลว์การปลอมตัว — ในการออกแบบแบบโบรกเกอร์ที่เซิร์ฟเวอร์เข้าถึงจริงด้วยสิทธิ์ของไคลเอนต์ ระดับ identification ไม่พอให้การปลอมตัวสำเร็จ และคุณต้องอนุญาต SECURITY_IMPERSONATION การอนุญาตนั้นมีเงื่อนไขว่าแน่ใจว่าเชื่อมต่อกับเซิร์ฟเวอร์แท้ การป้องกันการแย่งฝั่งเซิร์ฟเวอร์เป็นเพียงกลไกที่สังเกตผ่านการเริ่มต้นล้มเหลว หากบริการแท้ไม่อยู่และผู้โจมตีสร้างท่อชื่อเดียวกันก่อน ไคลเอนต์ยังเชื่อมต่อกับเซิร์ฟเวอร์ปลอมได้ อนุญาตเฉพาะเมื่อยืนยันคู่สื่อสารได้ผ่านการเริ่มบริการที่รับประกันได้ หรือการรับรองร่วมหลังเชื่อมต่อ

การตรวจตัวตนและการยืมสิทธิ์ฝั่งเซิร์ฟเวอร์คือ ImpersonateNamedPipeClient เรียกสิ่งนี้หลังอ่านคำขอจากท่อ แล้วเธรดที่เรียกจะเริ่มรันในบริบทความปลอดภัยของผู้ส่งข้อความล่าสุดที่อ่าน เปิดไฟล์ด้วยสิทธิ์ของไคลเอนต์แล้วการตรวจสิทธิ์ทำกับไคลเอนต์ — นี่คือกลไกที่บริการที่มีสิทธิ์สูงดำเนินการ “งานที่ถูกขอ ด้วยสิทธิ์ของผู้ขอ”6 เงื่อนไขเด็ดขาดในการใช้คือการตรวจค่าที่คืนมา หากเดินต่อหลังการปลอมตัวล้มเหลว งานถัดไปจะรันด้วยสิทธิ์สูงของเซิร์ฟเวอร์เอง เอกสารทางการระบุชัดว่า “เมื่อล้มเหลวห้ามดำเนินการตามคำขอของไคลเอนต์” คู่กับ RevertToSelf หลังงานเสร็จ แนวปฏิบัติในบทความเรื่อง impersonation token ใช้ได้ตามเดิม

ลำดับการจัดการคำขอที่ใช้การปลอมตัวเซิร์ฟเวอร์อ่านคำขอจากท่อ ยืนยันว่า ImpersonateNamedPipeClient สำเร็จ แล้วทำงานด้วยสิทธิ์ของไคลเอนต์และกลับสู่บริบทของตนด้วย RevertToSelf หากการปลอมตัวล้มเหลวจะปฏิเสธคำขอโดยไม่ดำเนินการServerClientServerClientOn failure, refuse without executing the requestSend a requestRead the requestImpersonateNamedPipeClientPerform the operation with the client's privilegesRevertToSelf to restore the original contextReply with the result

ภาพ 7: การยืนยันว่าการปลอมตัวสำเร็จกับ RevertToSelf ที่แน่นอนมาเป็นชุด หากเดินต่อเมื่อล้มเหลว จะรันด้วยสิทธิ์ของเซิร์ฟเวอร์

สี่จุดที่ปกป้องท่อของบริการที่มีสิทธิ์สูงฝั่งเซิร์ฟเวอร์เสริมทางเข้าด้วยการปฏิเสธระยะไกล ACL ที่ชัดเจน และการรับประกันอินสแตนซ์แรก ฝั่งไคลเอนต์ระบุระดับการปลอมตัวขั้นต่ำที่จำเป็นเพื่อไม่ให้เซิร์ฟเวอร์ปลอมยืมสิทธิ์ (จำกัดที่ระดับ identification หากการออกแบบไม่ให้เซิร์ฟเวอร์ยืมสิทธิ์)ฝั่งไคลเอนต์ระบุระดับปลอมตัวขั้นต่ำฝั่งเซิร์ฟเวอร์PIPE_REJECT_REMOTE_CLIENTSจำกัดผู้เชื่อมต่อด้วย ACLFIRST_PIPE_INSTANCE (เฉพาะอินสแตนซ์แรก)ท่อในฐานะขอบเขตสิทธิ์

ภาพ 8: ในการออกแบบที่ท่อคือขอบเขตสิทธิ์ ให้ทำสามข้อฝั่งเซิร์ฟเวอร์บวกหนึ่งข้อฝั่งไคลเอนต์เป็นชุด

6. กับดักในทางปฏิบัติ

การแข่งลำดับการเริ่มต้น หากไคลเอนต์มาเชื่อมต่อก่อนที่เซิร์ฟเวอร์จะสร้างท่อ คุณจะได้ข้อผิดพลาด “ท่อไม่มีอยู่” ฝั่งไคลเอนต์สร้าง “ไม่มีอยู่ → รอสักครู่แล้วลองใหม่” ในทางกลับกัน หลักฝั่งเซิร์ฟเวอร์คือเริ่มฟังด้วย ConnectNamedPipe ก่อนที่ไคลเอนต์จะเริ่ม8

ลำดับการลองเชื่อมต่อใหม่ของไคลเอนต์เปิดท่อด้วย CreateFile หากท่อไม่มีอยู่ให้รอสั้น ๆ แล้วลองใหม่ หากทุกอินสแตนซ์ถูกใช้ (ERROR_PIPE_BUSY) ให้รอช่องว่างด้วย WaitNamedPipe แล้วลองใหม่ เมื่อสำเร็จเข้าสู่การสื่อสารสำเร็จท่อไม่มีอยู่ERROR_PIPE_BUSYเปิดด้วย CreateFileผลลัพธ์?เริ่มสื่อสารรอสั้น ๆ (เซิร์ฟเวอร์ยังไม่เริ่ม)รออินสแตนซ์ว่างด้วย WaitNamedPipe

ภาพ 9: การจัดการเชื่อมต่อฝั่งไคลเอนต์แยกความล้มเหลวสองชนิดคือ “ไม่มีอยู่” กับ “เต็ม” แล้วรวมทั้งคู่กลับเข้าสู่การลองใหม่

การตรวจจับการตัดการเชื่อมต่อ เมื่อคู่สื่อสารออก Read/Write ล้มเหลวด้วย ERROR_BROKEN_PIPE เป็นต้น นั่นไม่ใช่ความผิดปกติ แต่เป็นการสื่อสารประจำวัน เซิร์ฟเวอร์ตรวจการตัด ทำ DisconnectNamedPipe กับอินสแตนซ์ แล้วเตรียมรับการเชื่อมต่อถัดไป ไคลเอนต์เชื่อมต่อใหม่ — แนวคิด “การเชื่อมต่อใหม่ที่ทำซ้ำได้” ที่อธิบายในบทความเรื่อง sleep/resume ใช้ที่นี่ด้วย

สมมติฐานเรื่องขนาดข้อความ นอกจากการอ่านแบบแยกส่วนของโหมดข้อความ (บทที่ 3) หากคุณไม่ตัดสินในโปรโตคอลว่า “ข้อความหนึ่งมีได้สูงสุดกี่ไบต์” คู่สื่อสารที่ประสงค์ร้าย (หรือมีบั๊ก) สามารถสิ้นเปลืองหน่วยความจำด้วยข้อความยักษ์ กำหนดเพดาน และตัดการเชื่อมต่อหากเกิน นั่นคือแนวทางที่ปลอดภัย

ความเสร็จของการเขียนกับการที่คู่สื่อสารรับเป็นคนละเรื่อง ความสำเร็จของ WriteFile ไม่ได้หมายความว่าแอปของคู่สื่อสารประมวลผลข้อมูลแล้ว งานที่ต้องการความแน่นอนต้องมีหลักประกันด้วยการออกแบบ เช่น ยืนยันด้วยข้อความตอบกลับ และใส่ความสอดคล้องของคำขอกับคำตอบในโปรโตคอล

7. สรุป

  • named pipe คือตัวเลือกแรกของ IPC บนเครื่องเดียวกัน เหตุผลคือความใช้ง่ายแบบเดียวกับไฟล์ I/O และการรวมกับโมเดลความปลอดภัยของ Windows ทั้ง ACL และการปลอมตัว
  • การเลือกโหมดคือ “โหมดข้อความสำหรับคำขอและคำตอบ โหมดไบต์หากมีเฟรมของตนเองอยู่แล้ว” แม้ในโหมดข้อความ คุณยังต้องจัดการการอ่านแบบแยกส่วน (ERROR_MORE_DATA)
  • หลายไคลเอนต์คือหลายอินสแตนซ์ + overlapped หรือ .NET async/await สำหรับงานใหม่ รูปแบบอะซิงโครนัสของ .NET คือแบบตรงไปตรงมา
  • บนท่อที่เป็นขอบเขตสิทธิ์ ให้ทำชุดการปฏิเสธระยะไกล ACL ที่ชัดเจน FIRST_PIPE_INSTANCE (เฉพาะอินสแตนซ์แรก) และการลดระดับการปลอมตัวฝั่งไคลเอนต์
  • สำหรับ ImpersonateNamedPipeClient การตรวจค่าที่คืนมากับ RevertToSelf คือเส้นชีวิต
  • ถัก “ชีวิตประจำวันของการสื่อสาร” — ลำดับการเริ่มต้น การตัดการเชื่อมต่อ เพดานข้อความ การยืนยันคำตอบ — เข้าไปในการออกแบบโปรโตคอล

named pipe เป็น API เก่า แต่สำหรับงาน “ให้โปรเซสคุยกันบนเครื่องเดียวกันโดยเคารพขอบเขตบัญชี Windows” มันยังเป็นเครื่องมือที่เป็นธรรมชาติที่สุด จุดตัดสินใจออกแบบเกือบหมดภายในขอบเขตของบทความนี้ หลังจากนั้น ให้เขียนโปรโตคอลของตนเองลงบนกระดาษหนึ่งแผ่นก่อนเริ่มอิมพลีเมนต์

บทความที่เกี่ยวข้อง

ด้านให้คำปรึกษาที่เกี่ยวข้อง

KomuraSoft LLC รับออกแบบและอิมพลีเมนต์ที่เกี่ยวกับการสื่อสารระหว่างโปรเซส — การแยกบริการจากแอป UI การแยกสิทธิ์ผู้ดูแลระบบ เป็นต้น — การแทนที่ IPC เดิม (shared memory ซ็อกเก็ตที่สร้างเอง COM เป็นต้น) ด้วย named pipe และการตรวจทานความปลอดภัยของการสื่อสารผ่านท่อของบริการที่มีสิทธิ์สูง ยินดีให้คำปรึกษาตั้งแต่ขั้นช่วยดูการออกแบบโปรโตคอล

ลิงก์อ้างอิง

  1. Microsoft Learn, Named Pipes. ว่าด้วย named pipe เป็นช่องทางเดียวหรือสองทิศทางระหว่างเซิร์ฟเวอร์ท่อกับไคลเอนต์ท่อหนึ่งตัวหรือมากกว่า ว่าทุกอินสแตนซ์ใช้ชื่อเดียวกันแต่มีบัฟเฟอร์และแฮนเดิลอิสระ และใช้ได้จากโปรเซสในเครื่องและระยะไกล  2

  2. Microsoft Learn, Impersonating a Named Pipe Client. ว่าด้วยการปลอมตัวให้เธรดเซิร์ฟเวอร์ทำงานภายในสิทธิ์ของไคลเอนต์ ระดับการปลอมตัวเริ่มต้นคือ SecurityImpersonation และไคลเอนต์ควบคุมระดับการปลอมตัวด้วยแฟล็ก SECURITY_SQOS_PRESENT ตอน CreateFile (SECURITY_IDENTIFICATION อนุญาตเฉพาะการระบุตัวตน)  2

  3. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). ว่าด้วยทิศทางท่อ (inbound, outbound, bidirectional) ประเภทไบต์และประเภทข้อความ (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 แล้วเดินเครื่องสถานะของอินสแตนซ์ที่เสร็จ และการยืนยันความเสร็จของ I/O ที่ค้างด้วย GetOverlappedResult  2

  5. Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). ว่าด้วยโหมดไคลเอนต์ระยะไกลสองแบบ PIPE_ACCEPT_REMOTE_CLIENTS (รับการเชื่อมต่อระยะไกลแล้วตรวจกับ security descriptor) และ 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 และแฮนเดิลที่เปิดมีค่าเริ่มต้นเป็นการอ่านไบต์ บล็อก และ non-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). ว่าด้วยการเปิด I/O แบบอะซิงโครนัสด้วย Asynchronous และ CurrentUserOnly สามารถอนุญาตการเชื่อมต่อเฉพาะกับโปรเซสของผู้ใช้คนเดียวกัน (และระดับการยกระดับเดียวกัน) 

  11. Microsoft Learn, Named Pipe Security and Access Rights. ว่าด้วยองค์ประกอบของสิทธิ์เข้าถึง named pipe ว่า GENERIC_WRITE รวม FILE_CREATE_PIPE_INSTANCE ดังนั้นการให้ generic write แก่ไคลเอนต์ยังอนุญาตให้สร้างอินสแตนซ์เซิร์ฟเวอร์ และการอ่านเขียนข้อมูลถูกให้เป็นสิทธิ์เข้าถึงเดี่ยว 

บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง

Win32 Thread Pool API — คอนเคอร์เรนซีโดยไม่สร้างเธรดเอง ด้วย CreateThreadpoolWork

โค้ดเนทีฟของคุณกระจายการเรียก CreateThread ไปทั่วหรือไม่ บทความนี้อธิบาย Win32 thread pool API ที่ออกแบบใหม่ใน Vista — ออบเจ็กต์สี่ชนิด w...

เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 2) — หน่วยความจำที่แม้เคอร์เนลก็มองไม่เห็น: กลไกของ VBS, HVCI และ Credential Guard

เมื่อติดตั้งใหม่บนฮาร์ดแวร์ที่รองรับ VBS จะเปิดตามค่าเริ่มต้น และใช้ hypervisor กับ SLAT สร้างการแยกที่แข็งกว่าเคอร์เนล บทความนี้อธิบายโค...

เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 3) — เครื่องเสมือนที่บูตในไม่กี่วินาที: ทำไม WSL2, Windows Sandbox และคอนเทนเนอร์จึงเบา

ทำไม WSL2 และ Windows Sandbox จึงเริ่มในไม่กี่วินาทีและรู้สึกเบา? บทความนี้อธิบายกลไก ตั้งแต่ dynamic base image และ direct map ไปจนถึงกา...

เชิงลึกของการจำลองเสมือนบน Windows (ตอนที่ 1) — Windows ของคุณรันอยู่ที่ไหนจริง ๆ? Hypervisor และพาร์ติชัน

เมื่อเปิด Hyper-V แล้ว Windows ฝั่งโฮสต์เองก็รันบน hypervisor ในฐานะ root partition บทความนี้อธิบายรากฐานของการจำลองเสมือนผ่านบทบาทของ VT...

ทำให้เว็บไซต์ของคุณปรากฏในการค้นหาด้วยชื่อพื้นที่ — คู่มือ Local SEO ภาคปฏิบัติสำหรับ SME (หน้าเพจพื้นที่และ Google Business Profile)

สำหรับ SME ที่เว็บไซต์ไม่ปรากฏเมื่อมีคนค้นหา "ชื่อพื้นที่ + ประเภทธุรกิจ" บทความนี้เรียบเรียงลำดับขั้นตอนการปรับปรุง Local SEO ได้แก่ การ...

หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ

บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้

คำถามที่พบบ่อย

คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้

ควรเลือก named pipe หรือ TCP (ซ็อกเก็ต localhost) อย่างไร
สำหรับการสื่อสารระหว่างโปรเซสบนเครื่องเดียวกัน named pipe คือตัวเลือกแรก เหตุผลอยู่ที่โมเดลความปลอดภัย ท่อสามารถควบคุม "ใครเชื่อมต่อได้" ในระดับ OS ด้วย Windows security descriptor (ACL) และเซิร์ฟเวอร์สามารถตรวจและยืมบัญชี Windows ของคู่ที่เชื่อมต่อด้วย ImpersonateNamedPipeClient ซึ่งต่างจากพอร์ต TCP บน localhost ที่ใครก็เชื่อมต่อได้ จึงต้องยืนยันตัวตนของคู่สื่อสารด้วยการรับรองของตนเอง ในทางกลับกัน ตัวเลือกบน TCP ได้เปรียบเมื่อมีแนวโน้มว่าจะสื่อสารระยะไกลในภายหลัง เมื่อต้องคุยกับโปรเซสบน OS อื่น หรือเมื่ออยากใช้สินทรัพย์โปรโตคอลที่มีอยู่แล้ว เช่น gRPC การตัดสินใจนี้ยังถูกจัดระเบียบไว้ในบทความเรื่องการเลือกการสื่อสารระหว่างโปรเซสบน Windows
ควรใช้โหมดไบต์หรือโหมดข้อความ
หากอยากจัดการแบบ "หนึ่งครั้งที่เขียน = หนึ่งหน่วยความหมาย" โหมดข้อความ (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE) สะดวกกว่า ฝั่งรับอ่านได้ตามหน่วยที่ฝั่งส่งเขียน จึงไม่ต้องจัดการขอบเขตเอง โหมดไบต์เป็น "สายไบต์ต่อเนื่อง" เหมือน TCP และคุณต้องออกแบบเฟรมเอง เช่น เติมความยาวนำหน้า หากคุณกำลังส่งโปรโตคอลที่มีเฟรมอยู่แล้ว (เช่น รูปแบบซีเรียลไลซ์ที่เติมความยาว) โหมดไบต์ก็ใช้ได้ ข้อควรระวัง: แม้ในโหมดข้อความ หากบัฟเฟอร์รับเล็กกว่าข้อความ คุณจะได้อ่านแบบแยกส่วน (ERROR_MORE_DATA) จึงยังต้องจัดการกรณีนั้น นอกจากนี้โหมดอ่านเป็นการตั้งค่ารายแฮนเดิล และ CreateNamedPipe กำหนดเฉพาะฝั่งเซิร์ฟเวอร์ ไคลเอนต์ต้องระบุ PIPE_READMODE_MESSAGE ด้วย SetNamedPipeHandleState หลัง CreateFile ใน .NET เซิร์ฟเวอร์ระบุ PipeTransmissionMode.Message และไคลเอนต์ตั้ง NamedPipeClientStream.ReadMode เป็น Message หลังเชื่อมต่อ
จะสร้างเซิร์ฟเวอร์ที่คุยกับหลายไคลเอนต์พร้อมกันได้อย่างไร
named pipe สามารถสร้างหลายอินสแตนซ์ภายใต้ชื่อเดียวกัน และหนึ่งอินสแตนซ์รับผิดชอบหนึ่งไคลเอนต์ มีสองรูปแบบ หนึ่งคือการออกแบบแบบซิงโครนัสที่จัดสรรเธรดต่อไคลเอนต์ การอิมพลีเมนต์ตรงไปตรงมา แต่กินเธรดหนึ่งเส้นต่อไคลเอนต์ อีกแบบคือใช้ I/O แบบอะซิงโครนัสด้วย FILE_FLAG_OVERLAPPED แล้วให้เธรดจำนวนน้อยจัดการ ConnectNamedPipe, ReadFile และ WriteFile ของทุกอินสแตนซ์ ตัวอย่างทางการของ Microsoft ก็แสดงการประมวลผลหลายอินสแตนซ์บนเธรดเดียว ใน .NET คุณเขียนรูปแบบอะซิงโครนัสได้เกือบตรงไปตรงมาเท่าแบบซิงโครนัส ด้วย NamedPipeServerStream.WaitForConnectionAsync และ async/await หากไม่มีเหตุผลพิเศษ รูปแบบอะซิงโครนัสของ .NET คือสิ่งที่แนะนำสำหรับการอิมพลีเมนต์ใหม่
อย่างน้อยที่สุดควรทำอะไรเพื่อความปลอดภัยของ named pipe
สี่ข้อ ประการแรก หากไม่ต้องการการเชื่อมต่อระยะไกล ให้ระบุ PIPE_REJECT_REMOTE_CLIENTS และปฏิเสธการเชื่อมต่อผ่านเครือข่ายอย่างชัดเจน ประการที่สอง ตั้ง ACL ที่เหมาะสมด้วย SECURITY_ATTRIBUTES และจำกัดผู้ใช้กับกลุ่มที่เชื่อมต่อได้ (ACL เริ่มต้นหลวมเกินไปสำหรับบางงาน) ประการที่สาม ระบุ FILE_FLAG_FIRST_PIPE_INSTANCE เมื่อสร้างอินสแตนซ์แรก เพื่อตรวจจับ "การแย่งชื่อ" ที่ท่อชื่อเดียวกันถูกสร้างก่อน (อย่าใส่แฟล็กนี้กับอินสแตนซ์ที่สองเป็นต้นไป) ประการที่สี่ ไคลเอนต์ที่อยากให้เซิร์ฟเวอร์ระบุตัวตนเท่านั้น ควรระบุ SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION บน CreateFile เพื่อไม่ให้เซิร์ฟเวอร์ปลอมยืม (impersonate) สิทธิ์ของตน ในการออกแบบแบบโบรกเกอร์ที่เซิร์ฟเวอร์เข้าถึงจริงด้วยสิทธิ์ของไคลเอนต์ ต้องอนุญาตการปลอมตัว จึงใช้ข้อจำกัดนี้หรือไม่ขึ้นกับว่าการออกแบบให้เซิร์ฟเวอร์ยืมสิทธิ์หรือไม่
มีข้อควรระวังเมื่อใช้ ImpersonateNamedPipeClient หรือไม่
สิ่งสำคัญที่สุดคือการตรวจค่าที่คืนมา หากคุณเดินหน้าต่อหลังการปลอมตัวล้มเหลว งานถัดไปจะรันด้วยสิทธิ์ของโปรเซสเซิร์ฟเวอร์เอง (มักสูง) และงานที่ไม่ควรอนุญาตให้ไคลเอนต์จะผ่าน เอกสารทางการยังระบุชัดว่าเมื่อล้มเหลวห้ามดำเนินการตามคำขอของไคลเอนต์ คุณยังต้องเรียกหลังจากอ่านบางอย่างแล้วเท่านั้น — การปลอมตัวทำในบริบทของ "ข้อความล่าสุดที่อ่านจากท่อ" — และต้องกลับสู่บริบทเดิมอย่างแน่นอนด้วย RevertToSelf เมื่องานเสร็จ กลไกเรื่องการปลอมตัว (โทเค็น ระดับการปลอมตัว SeImpersonatePrivilege) มีรายละเอียดในบทความเรื่อง impersonation token

โปรไฟล์ผู้เขียน

หน้าแนะนำผู้เขียนบทความ

Go Komura

ผู้แทนของ KomuraSoft LLC

เชี่ยวชาญด้านการพัฒนาซอฟต์แวร์ Windows ที่ปรึกษาเทคนิค และการตรวจสอบบั๊ก โดยเฉพาะโปรเจกต์ที่มีระบบเดิมและบั๊กที่ทำซ้ำได้ยาก

ลิงก์สาธารณะ

กลับไปยังบล็อก