“อยากให้บริการที่รันอยู่กับ 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
flowchart TB
accTitle: ทิศทางของท่อกับสิทธิ์ที่ไคลเอนต์ระบุ
accDescr: ไคลเอนต์เปิดท่อสองทิศทางด้วยอ่านหรือเขียนก็ได้ แต่ต้องเปิดท่อขาออกที่เซิร์ฟเวอร์เขียนอย่างเดียวแบบอ่านอย่างเดียว และเปิดท่อขาเข้าที่เซิร์ฟเวอร์อ่านอย่างเดียวแบบเขียนอย่างเดียว
q{"ทิศทางที่เซิร์ฟเวอร์สร้าง?"} -->|"สองทิศทาง"| dc["อ่านหรือเขียนก็ได้"]
q -->|"ขาออก"| oc["เปิดแบบอ่านอย่างเดียว"]
q -->|"ขาเข้า"| ic["เปิดแบบเขียนอย่างเดียว"]
ภาพ 1: หากทิศทางกับสิทธิ์การเข้าถึงไม่ตรงกัน CreateFile จะล้มเหลว เมื่อไล่ข้อผิดพลาดการเชื่อมต่อ ให้ตรวจจุดนี้ก่อน
flowchart TB
accTitle: โครงสร้างพื้นฐานของ named pipe
accDescr: เซิร์ฟเวอร์สร้างหลายอินสแตนซ์ของท่อชื่อเดียวกันและรอการเชื่อมต่อด้วย ConnectNamedPipe แต่ละไคลเอนต์เปิดชื่อด้วย CreateFile และมีช่องสองทิศทางแบบหนึ่งต่อหนึ่งกับหนึ่งอินสแตนซ์
s["เซิร์ฟเวอร์"] --> i1["อินสแตนซ์ 1"]
s --> i2["อินสแตนซ์ 2"]
s --> i3["อินสแตนซ์ 3"]
c1["ไคลเอนต์ A"] <--> i1
c2["ไคลเอนต์ B"] <--> i2
c3["ไคลเอนต์ C"] <--> i3
ภาพ 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
flowchart TB
accTitle: ลูปอ่านแบบแยกส่วนในโหมดข้อความ
accDescr: หาก ReadFile สำเร็จข้อความครบ หากคืน ERROR_MORE_DATA ให้อ่านส่วนที่เหลือซึ่งไม่พอในบัฟเฟอร์แล้วต่อกัน ข้อผิดพลาดอื่นถือเป็นการตัดการเชื่อมต่อ
read["อ่านด้วย ReadFile"] --> r{"ผลลัพธ์?"}
r -->|"สำเร็จ"| done["ข้อความครบ"]
r -->|"ERROR_MORE_DATA"| more["อ่านส่วนที่เหลือแล้วต่อกัน"]
more --> read
r -->|"ข้อผิดพลาดอื่น"| dis["ถือว่าตัดการเชื่อมต่อ"]
ภาพ 3: แม้ในโหมดข้อความ คุณยังต้องมี “ลูปอ่านส่วนที่เหลือ” หากไม่มี มีเพียงข้อความใหญ่ที่พัง
flowchart TB
accTitle: ความต่างระหว่างโหมดไบต์กับโหมดข้อความ
accDescr: ในโหมดไบต์การเขียนสามครั้งกลายเป็นสายไบต์ต่อเนื่องและฝั่งรับต้องแยกเอง ในโหมดข้อความหน่วยของการเขียนแต่ละครั้งถูกเก็บไว้และถึงฝั่งรับตามเดิม
bw["โหมดไบต์: เขียน AAA, BB, CCCC"] --> br["รับเป็นสายไบต์ AAABBCCCC"]
br --> bf["คุณออกแบบเฟรมเอง"]
mw["โหมดข้อความ: เขียนสามครั้งเดียวกัน"] --> mr["รับเป็นสามข้อความ: AAA, BB, CCCC"]
mr --> mf["หน่วยการเขียนถูกเก็บไว้"]
ภาพ 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 ได้
flowchart TB
accTitle: โครงสร้างของเซิร์ฟเวอร์แบบ overlapped
accDescr: ความเสร็จของการทำงานอะซิงโครนัสแต่ละอินสแตนซ์ถูกรับบนอาร์เรย์ของอีเวนต์ และเธรดจำนวนน้อยรอด้วย WaitForMultipleObjects แล้วเดินหน้าอินสแตนซ์ที่เสร็จ ทำให้จำนวนเธรดไม่ผูกกับจำนวนไคลเอนต์
i1["งานอะซิงค์อินสแตนซ์ 1"] --> ev["อาร์เรย์ของอีเวนต์"]
i2["งานอะซิงค์อินสแตนซ์ 2"] --> ev
i3["งานอะซิงค์อินสแตนซ์ 3"] --> ev
ev --> wait["รอความเสร็จด้วย WaitForMultipleObjects"]
wait --> proc["เดินหน้าอินสแตนซ์ที่เสร็จ"]
proc --> wait
ภาพ 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 ในบทถัดไป
flowchart TB
accTitle: ลูปรับของเซิร์ฟเวอร์อะซิงโครนัสใน .NET
accDescr: ลูปรับสร้าง NamedPipeServerStream รอการเชื่อมต่อด้วย WaitForConnectionAsync และเมื่อมาถึงจะแยกการจัดการไคลเอนต์แบบอะซิงโครนัสแล้วกลับไปรับถัดไปทันที จึงรับการเชื่อมต่อพร้อมกันในโค้ดตรงไปตรงมา
mk["สร้างสตรีมเซิร์ฟเวอร์"] --> wc["รอด้วย WaitForConnectionAsync"]
wc --> got["การเชื่อมต่อมาถึง"]
got --> hd["แยกจัดการไคลเอนต์แบบอะซิงค์"]
hd --> mk
ภาพ 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 ใช้ได้ตามเดิม
sequenceDiagram
accTitle: ลำดับการจัดการคำขอที่ใช้การปลอมตัว
accDescr: เซิร์ฟเวอร์อ่านคำขอจากท่อ ยืนยันว่า ImpersonateNamedPipeClient สำเร็จ แล้วทำงานด้วยสิทธิ์ของไคลเอนต์และกลับสู่บริบทของตนด้วย RevertToSelf หากการปลอมตัวล้มเหลวจะปฏิเสธคำขอโดยไม่ดำเนินการ
participant C as Client
participant S as Server
C->>S: Send a request
S->>S: Read the request
S->>S: ImpersonateNamedPipeClient
Note over S: On failure, refuse without executing the request
S->>S: Perform the operation with the client's privileges
S->>S: RevertToSelf to restore the original context
S->>C: Reply with the result
ภาพ 7: การยืนยันว่าการปลอมตัวสำเร็จกับ RevertToSelf ที่แน่นอนมาเป็นชุด หากเดินต่อเมื่อล้มเหลว จะรันด้วยสิทธิ์ของเซิร์ฟเวอร์
flowchart TB
accTitle: สี่จุดที่ปกป้องท่อของบริการที่มีสิทธิ์สูง
accDescr: ฝั่งเซิร์ฟเวอร์เสริมทางเข้าด้วยการปฏิเสธระยะไกล ACL ที่ชัดเจน และการรับประกันอินสแตนซ์แรก ฝั่งไคลเอนต์ระบุระดับการปลอมตัวขั้นต่ำที่จำเป็นเพื่อไม่ให้เซิร์ฟเวอร์ปลอมยืมสิทธิ์ (จำกัดที่ระดับ identification หากการออกแบบไม่ให้เซิร์ฟเวอร์ยืมสิทธิ์)
subgraph sv["ฝั่งเซิร์ฟเวอร์"]
r1["PIPE_REJECT_REMOTE_CLIENTS"]
r2["จำกัดผู้เชื่อมต่อด้วย ACL"]
r3["FIRST_PIPE_INSTANCE (เฉพาะอินสแตนซ์แรก)"]
end
subgraph cl["ฝั่งไคลเอนต์"]
r4["ระบุระดับปลอมตัวขั้นต่ำ"]
end
sv --> safe["ท่อในฐานะขอบเขตสิทธิ์"]
cl --> safe
ภาพ 8: ในการออกแบบที่ท่อคือขอบเขตสิทธิ์ ให้ทำสามข้อฝั่งเซิร์ฟเวอร์บวกหนึ่งข้อฝั่งไคลเอนต์เป็นชุด
6. กับดักในทางปฏิบัติ
การแข่งลำดับการเริ่มต้น หากไคลเอนต์มาเชื่อมต่อก่อนที่เซิร์ฟเวอร์จะสร้างท่อ คุณจะได้ข้อผิดพลาด “ท่อไม่มีอยู่” ฝั่งไคลเอนต์สร้าง “ไม่มีอยู่ → รอสักครู่แล้วลองใหม่” ในทางกลับกัน หลักฝั่งเซิร์ฟเวอร์คือเริ่มฟังด้วย ConnectNamedPipe ก่อนที่ไคลเอนต์จะเริ่ม8
flowchart TB
accTitle: ลำดับการลองเชื่อมต่อใหม่ของไคลเอนต์
accDescr: เปิดท่อด้วย CreateFile หากท่อไม่มีอยู่ให้รอสั้น ๆ แล้วลองใหม่ หากทุกอินสแตนซ์ถูกใช้ (ERROR_PIPE_BUSY) ให้รอช่องว่างด้วย WaitNamedPipe แล้วลองใหม่ เมื่อสำเร็จเข้าสู่การสื่อสาร
cf["เปิดด้วย CreateFile"] --> ok{"ผลลัพธ์?"}
ok -->|"สำเร็จ"| go["เริ่มสื่อสาร"]
ok -->|"ท่อไม่มีอยู่"| wait1["รอสั้น ๆ (เซิร์ฟเวอร์ยังไม่เริ่ม)"]
ok -->|"ERROR_PIPE_BUSY"| wnp["รออินสแตนซ์ว่างด้วย WaitNamedPipe"]
wait1 --> cf
wnp --> cf
ภาพ 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” มันยังเป็นเครื่องมือที่เป็นธรรมชาติที่สุด จุดตัดสินใจออกแบบเกือบหมดภายในขอบเขตของบทความนี้ หลังจากนั้น ให้เขียนโปรโตคอลของตนเองลงบนกระดาษหนึ่งแผ่นก่อนเริ่มอิมพลีเมนต์
บทความที่เกี่ยวข้อง
- เลือกการสื่อสารระหว่างโปรเซสบน Windows ── ตารางตัดสินใจสำหรับ Named Pipes / TCP / gRPC / Shared Memory / COM
- วิธีแยก “เฉพาะงานที่ต้องใช้สิทธิ์ผู้ดูแลระบบ” ในแอป Windows อย่างเป็นรูปธรรม
- จัดการ impersonation token ของ Windows ให้ถูกต้อง — ยืมสิทธิ์รายเธรดแล้วคืนอย่างปลอดภัย
- เชิงลึกของ Windows I/O (ตอนที่ 2) — I/O แบบซิงโครนัสและอะซิงโครนัส: ความหมายที่แท้จริงของ OVERLAPPED
- กับดักของ Shared Memory และแนวปฏิบัติจริง
ด้านให้คำปรึกษาที่เกี่ยวข้อง
KomuraSoft LLC รับออกแบบและอิมพลีเมนต์ที่เกี่ยวกับการสื่อสารระหว่างโปรเซส — การแยกบริการจากแอป UI การแยกสิทธิ์ผู้ดูแลระบบ เป็นต้น — การแทนที่ IPC เดิม (shared memory ซ็อกเก็ตที่สร้างเอง COM เป็นต้น) ด้วย named pipe และการตรวจทานความปลอดภัยของการสื่อสารผ่านท่อของบริการที่มีสิทธิ์สูง ยินดีให้คำปรึกษาตั้งแต่ขั้นช่วยดูการออกแบบโปรโตคอล
- การพัฒนาแอปพลิเคชัน Windows
- ที่ปรึกษาด้านเทคนิคและการตรวจทานการออกแบบ
- การตรวจสอบบั๊กและวิเคราะห์หาสาเหตุที่แท้จริง
- ติดต่อเรา
ลิงก์อ้างอิง
-
Microsoft Learn, Named Pipes. ว่าด้วย named pipe เป็นช่องทางเดียวหรือสองทิศทางระหว่างเซิร์ฟเวอร์ท่อกับไคลเอนต์ท่อหนึ่งตัวหรือมากกว่า ว่าทุกอินสแตนซ์ใช้ชื่อเดียวกันแต่มีบัฟเฟอร์และแฮนเดิลอิสระ และใช้ได้จากโปรเซสในเครื่องและระยะไกล ↩ ↩2
-
Microsoft Learn, Impersonating a Named Pipe Client. ว่าด้วยการปลอมตัวให้เธรดเซิร์ฟเวอร์ทำงานภายในสิทธิ์ของไคลเอนต์ ระดับการปลอมตัวเริ่มต้นคือ SecurityImpersonation และไคลเอนต์ควบคุมระดับการปลอมตัวด้วยแฟล็ก SECURITY_SQOS_PRESENT ตอน CreateFile (SECURITY_IDENTIFICATION อนุญาตเฉพาะการระบุตัวตน) ↩ ↩2
-
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
-
Microsoft Learn, Named Pipe Server Using Overlapped I/O. ว่าด้วยตัวอย่างทางการของเซิร์ฟเวอร์เธรดเดียวที่ประมวลผลการเชื่อมต่อพร้อมกันกับหลายไคลเอนต์ผ่านงาน overlapped รูปแบบที่รอโครงสร้าง OVERLAPPED และอีเวนต์ของแต่ละอินสแตนซ์ด้วย WaitForMultipleObjects แล้วเดินเครื่องสถานะของอินสแตนซ์ที่เสร็จ และการยืนยันความเสร็จของ I/O ที่ค้างด้วย GetOverlappedResult ↩ ↩2
-
Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). ว่าด้วยโหมดไคลเอนต์ระยะไกลสองแบบ PIPE_ACCEPT_REMOTE_CLIENTS (รับการเชื่อมต่อระยะไกลแล้วตรวจกับ security descriptor) และ PIPE_REJECT_REMOTE_CLIENTS (ปฏิเสธการเชื่อมต่อไคลเอนต์ระยะไกลอัตโนมัติ) ↩ ↩2
-
Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). ว่าด้วยเธรดฝั่งเซิร์ฟเวอร์เริ่มการปลอมตัวในบริบทความปลอดภัยของไคลเอนต์ของข้อความล่าสุดที่อ่านจากท่อ การกลับด้วย RevertToSelf หลังเสร็จ และการเดินต่อหลังการปลอมตัวล้มเหลวทำให้รันในบริบท (ที่มีสิทธิ์) ของโปรเซสเซิร์ฟเวอร์เอง จึงต้องตรวจค่าที่คืนมาเสมอ และเมื่อล้มเหลวห้ามดำเนินการตามคำขอของไคลเอนต์ ↩ ↩2 ↩3
-
Microsoft Learn, Named Pipe Client. ว่าด้วยไคลเอนต์เปิดท่อด้วย CreateFile เมื่อทุกอินสแตนซ์ถูกใช้จะได้ ERROR_PIPE_BUSY จึงรอช่องว่างด้วย WaitNamedPipe และแฮนเดิลที่เปิดมีค่าเริ่มต้นเป็นการอ่านไบต์ บล็อก และ non-overlapped โดย SetNamedPipeHandleState สามารถเปลี่ยนเป็นโหมดอ่านข้อความได้ ↩ ↩2
-
Microsoft Learn, Named Pipe Operations. ว่าด้วยงาน overlapped ผ่าน ReadFileEx / WriteFileEx การอ่านโดยไม่บริโภคผ่าน PeekNamedPipe TransactNamedPipe ที่ส่งคำขอและรับคำตอบในครั้งเดียวบนท่อสองทิศทางชนิดข้อความ และการอ่านแบบบล็อกก่อนที่ไคลเอนต์จะเริ่มซึ่งอาจก่อการแข่ง ↩ ↩2
-
Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). ว่าด้วยการเชื่อมต่อและอ่านเขียนด้วย NamedPipeServerStream / NamedPipeClientStream การส่งตามหน่วยข้อความผ่าน PipeTransmissionMode.Message และการจัดการหลายไคลเอนต์ด้วยเมธอดอะซิงโครนัส ↩ ↩2
-
Microsoft Learn, PipeOptions Enum (System.IO.Pipes). ว่าด้วยการเปิด I/O แบบอะซิงโครนัสด้วย Asynchronous และ CurrentUserOnly สามารถอนุญาตการเชื่อมต่อเฉพาะกับโปรเซสของผู้ใช้คนเดียวกัน (และระดับการยกระดับเดียวกัน) ↩
-
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 ได้แก่ การ...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
บริการที่เกี่ยวข้องกับหัวข้อนี้
บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้
พัฒนาแอป Windows
แอปธุรกิจ การเชื่อมต่ออุปกรณ์ และเครื่องมือสื่อสาร ตั้งแต่ความต้องการจนถึงการพัฒนา
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- ควรเลือก 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