บทความ ไฟร์วอลล์ แนะนำไว้แล้วว่าตัวกรองมีสองแบบ — แบบที่ดูแพ็กเก็ตทีละใบ กับแบบที่จำการเชื่อมต่อไว้ และแสดงว่ากฎไร้สถานะที่ดู แค่บิต ACK ถูกหลอกได้ง่ายแค่ไหน
แต่ยังไม่ได้ตอบสองคำถามที่ตามมา — มันจำอะไรไว้บ้าง และ การจำนั้นมีราคา เท่าไหร่
คำตอบข้อแรกน่าสนใจกว่าที่คิด เพราะไฟร์วอลล์ไม่ได้จำแค่ว่า "มีการเชื่อมต่อนี้ อยู่" มันรันเครื่องสถานะของตัวเองขนานไปกับปลายทางทั้งสองฝั่ง และสำหรับ โพรโทคอลที่ไม่มีการเชื่อมต่ออย่าง UDP มันประดิษฐ์การเชื่อมต่อขึ้นมาเอง
ส่วนคำตอบข้อที่สองมีสามข้อ และข้อที่สองอธิบายอาการล่มที่หายากที่สุดข้อหนึ่ง ในงานจริง
ถ้ายังไม่เคยดู เริ่มตรงนี้
หลักฐานทั้งหมดอยู่ในหน้าคู่มือบนเครื่องนี้เอง
$ man pf.conf | grep -A2 'tcp.established'
สิ่งที่บอกความจริงมากที่สุดคือรายชื่อค่า timeout ที่ตั้งได้ เพราะจำนวนและ ชื่อของมันเปิดเผยว่าไฟร์วอลล์คิดว่าการเชื่อมต่อมีกี่สถานะ
ไฟร์วอลล์รันเครื่องสถานะของตัวเอง
สำหรับ TCP หน้าคู่มือระบุหกสถานะ พร้อมคำอธิบายของแต่ละอัน
tcp.first หลังแพ็กเก็ตแรก
tcp.opening ก่อนที่ปลายทางจะเคยส่งอะไรกลับมา
tcp.established สถานะที่ตั้งขึ้นเรียบร้อยแล้ว
tcp.closing หลังส่ง FIN ใบแรก
tcp.finwait หลังแลก FIN กันครบสองใบ
tcp.closed หลังฝ่ายใดฝ่ายหนึ่งส่ง RST
สังเกตว่านี่คือเครื่องสถานะของ TCP เกือบทั้งชุด แต่ ไฟร์วอลล์ไม่ใช่ปลายทาง มันแค่ดูแพ็กเก็ตที่วิ่งผ่านแล้วเดาว่าตอนนี้การเชื่อมต่ออยู่สถานะไหน
และเพราะมันเดา มันก็เดาผิดได้ หน้าคู่มือยกกรณีจริงไว้ที่ tcp.finwait
"Some hosts (notably web servers on Solaris) send TCP packets even after closing the connection. Increasing tcp.finwait (and possibly tcp.closing) can prevent blocking of such packets."
เครื่องปลายทางบอกว่าปิดแล้ว แต่ยังส่งต่อ ส่วนไฟร์วอลล์เชื่อว่าปิดแล้วจริง จึงบล็อกทิ้ง นี่คือกรณีที่ความเห็นของไฟร์วอลล์กับความจริงของปลายทางไม่ตรงกัน และผลคือแพ็กเก็ตที่ถูกต้องถูกทิ้ง
UDP ไม่มีการเชื่อมต่อ ไฟร์วอลล์จึงประดิษฐ์ขึ้นมา
นี่คือส่วนที่ผมคิดว่าน่าสนใจที่สุดในหน้าคู่มือทั้งหน้า
UDP ตามที่บทความ TCP และ UDP อธิบาย ไม่มีการจับมือ ไม่มีการปิด ไม่มีสถานะอะไรทั้งสิ้น แต่หน้าคู่มือกลับมีสามสถานะให้ตั้ง
udp.first หลังแพ็กเก็ตแรก
udp.single ต้นทางส่งมากกว่าหนึ่งใบ แต่ปลายทางไม่เคยส่งกลับเลย
udp.multiple ทั้งสองฝั่งเคยส่งแล้ว
สามสถานะนี้ไม่มีอยู่ในโพรโทคอล UDP ไม่มีบิตไหนในหัว UDP บอกเรื่องนี้ ไฟร์วอลล์สร้างมันขึ้นมาจากสิ่งเดียวที่มันสังเกตได้ — ทิศทางของทราฟฟิก
udp.single แปลว่า "ฉันเห็นแต่ฝั่งเดียวพูด" ซึ่งน่าสงสัยกว่า udp.multiple ที่แปลว่า "ทั้งสองฝั่งคุยกัน" ไฟร์วอลล์จึงให้เวลาต่างกัน — การคุยที่มีสองฝั่ง ตอบกันย่อมน่าจะเป็นการเชื่อมต่อจริง
หน้าคู่มือสรุปแนวคิดนี้ไว้ว่า "ICMP and UDP are handled in a fashion similar to TCP" — คือปฏิบัติต่อของที่ไม่มีสถานะ เหมือนของที่มีสถานะ
ICMP ก็เช่นกัน มี icmp.first กับ icmp.error และหน้าคู่มือแสดงว่ากฎแบบมี สถานะจับคู่คำตอบได้จริง
"allows echo requests (such as those created by ping(8)) out statefully, and matches incoming echo replies correctly to states"
นั่นคือเหตุผลที่ ping ทำงานได้โดยไม่ต้องเขียนกฎเปิดขากลับ ทั้งที่ ICMP ไม่มีพอร์ตให้จับคู่เลย
ไม่ได้ดูแค่สี่ค่า — มันตรวจลำดับด้วย
บทความไฟร์วอลล์บอกว่าตัวกรองมีสถานะดูสี่ค่า คือที่อยู่และพอร์ตทั้งสองฝั่ง นั่นจริงแต่ไม่ครบ หน้าคู่มืออธิบายว่ามันทำมากกว่านั้น
"For TCP connections, comparing a packet to a state involves checking its sequence numbers, as well as TCP timestamps if a scrub reassemble tcp rule applies to the connection. If these values are outside the narrow windows of expected values, the packet is dropped."
และบอกเหตุผลตรง ๆ ว่ากันอะไร
"This prevents spoofing attacks, such as when an attacker sends packets with a fake source address/port but does not know the connection's sequence numbers."
ผมคำนวณให้เห็นเป็นตัวเลข สมมติผู้บุกรุกรู้ที่อยู่ทั้งสองฝั่งแต่มองทราฟฟิก ไม่เห็น
filter must guess odds, 1 in
stateless, ACK bit only source port 65,535
stateful, 4-tuple + seq port + sequence 4,294,967,296
... plus TCP timestamps +10 to 18 bits 1,125,899,906,842,624
การตรวจลำดับเพิ่มความยากอีก 65,537 เท่า และหน้าคู่มือระบุว่า timestamp เพิ่มให้อีก "by 10 to 18 bits" ซึ่งที่ 18 บิตคือ 262,144 เท่า
นี่คือคำตอบที่แท้จริงของกับดักบิต ACK ที่บทก่อนหน้าแสดงไว้ — ไม่ใช่แค่ว่า ตัวกรองมีสถานะ "จำได้" แต่มันมีข้อมูลที่ผู้บุกรุกไม่มีทางรู้ถ้าไม่ได้เห็น ทราฟฟิกจริง
ราคาที่หนึ่ง — หน่วยความจำ และ timeout ที่หดตัวเองได้
การจำทุกการเชื่อมต่อแปลว่าต้องมีที่เก็บ และเมื่อที่เก็บใกล้เต็ม pf มีกลไก ที่ตอบสนองแบบที่คนไม่ค่อยรู้
"Timeout values can be reduced adaptively as the number of state table entries grows."
มันทำงานเป็นเส้นตรงระหว่างสองค่า
adaptive.start จำนวนรายการที่เริ่มลด timeout
adaptive.end จำนวนรายการที่ timeout กลายเป็นศูนย์
หน้าคู่มืออธิบายจุดปลายไว้ตรง ๆ
"When reaching this number of state entries, all timeout values become zero, effectively purging all state entries immediately."
ล้างทั้งตารางทันที ซึ่งแปลว่าการเชื่อมต่อที่กำลังทำงานอยู่ทั้งหมดขาดพร้อมกัน เอกสารเองก็เตือนว่า "it should not actually be reached"
พฤติกรรมนี้เป็นคำอธิบายที่ดีของอาการที่ดูเหมือนอุปกรณ์รีบูต ทั้งที่ไม่ได้รีบูต — ทุกอย่างขาดพร้อมกันแล้วกลับมาใหม่ได้เอง
ราคาที่สอง — เส้นทางไม่สมมาตรทำให้พังทั้งที่ทุกอย่างถูก
นี่คือราคาที่หาสาเหตุยากที่สุด และมันตามมาจากบทความ การเลือกเส้นทาง โดยตรง
บทความนั้นชี้ว่า เส้นทางเป็นเรื่องทิศทางเดียว เส้นทางขาไปกับขากลับไม่ จำเป็นต้องเป็นเส้นเดียวกัน ซึ่งบนอินเทอร์เน็ตเป็นเรื่องปกติมาก
ตัวกรองไร้สถานะไม่สนใจเรื่องนี้เลย เพราะมันดูทีละใบอยู่แล้ว แต่ตัวกรองมีสถานะ จะพัง เพราะ
outbound firewall A สร้างสถานะของการเชื่อมต่อนี้ไว้
return firewall B ไม่เคยเห็นขาไป จึงไม่มีสถานะให้เข้าคู่
result firewall B ทิ้งแพ็กเก็ตที่ถูกต้องสมบูรณ์
ทุกอย่างตั้งค่าถูก เส้นทางถูก ไฟร์วอลล์ทำงานถูกตามที่ออกแบบ แต่ใช้ไม่ได้ และอาการจะเป็นแบบสุ่ม เพราะเส้นทางอาจสลับไปมาตามภาระของแต่ละเส้น
นี่คือเหตุผลที่การมีไฟร์วอลล์สองตัวคู่กันต้องแลกสถานะกัน หรือไม่ก็ต้องบังคับ ให้ทราฟฟิกของการเชื่อมต่อเดียวกันผ่านตัวเดียวกันเสมอ — ซึ่งทั้งสองทางเพิ่ม ความซับซ้อนที่ตัวกรองไร้สถานะไม่ต้องมี
ราคาที่สาม — ขยายแนวนอนไม่ได้ฟรี
ตัวกรองไร้สถานะขยายได้ง่ายมาก เพราะทุกตัวตัดสินใจเหมือนกันโดยไม่ต้องรู้อะไร จากตัวอื่น เพิ่มเครื่องแล้วกระจายทราฟฟิกไปได้เลย
ตัวกรองมีสถานะทำแบบนั้นไม่ได้ เพราะการตัดสินใจขึ้นกับสิ่งที่เคยเห็นมาก่อน เครื่องที่เพิ่งเพิ่มเข้ามาไม่มีประวัติของการเชื่อมต่อที่กำลังทำงานอยู่ จึงจะ ทิ้งทราฟฟิกเหล่านั้นทั้งหมด
หน้าคู่มือยังชี้ข้อได้เปรียบด้านความเร็วไว้ด้วย
"Also, looking up states is usually faster than evaluating rules."
ซึ่งจริง — เมื่อสถานะมีแล้ว การจับคู่เร็วกว่าการไล่กฎทั้งชุด แต่ข้อได้เปรียบ นั้นแลกมากับข้อจำกัดสามข้อข้างบน
เมื่อมันโกหก
"มีสถานะดีกว่าเสมอ" ดีกว่าในแง่ความปลอดภัยชัดเจน ตามตัวเลขข้างบน แต่แลกมา กับหน่วยความจำ ความเปราะบางต่อเส้นทางไม่สมมาตร และการขยายที่ยากกว่า
"ไฟร์วอลล์รู้ว่าการเชื่อมต่อปิดแล้ว" มันเดาจากแพ็กเก็ตที่เห็น และเดาผิดได้ กรณี Solaris ในหน้าคู่มือคือตัวอย่างที่มีเอกสารรองรับ
"UDP ไม่มีสถานะ ไฟร์วอลล์จึงกรองไม่ได้" กรองได้ แต่ด้วยสถานะที่มันประดิษฐ์ ขึ้นเองจากทิศทางทราฟฟิก ซึ่งแปลว่ามันเชื่อถือได้น้อยกว่าของ TCP โดยธรรมชาติ
"ตารางสถานะเต็มแล้วแค่ปฏิเสธของใหม่" ไม่เสมอไป ถ้าตั้ง adaptive ไว้ ค่า timeout จะหดลงเรื่อย ๆ และที่จุดปลาย เอกสารบอกว่ารายการทั้งหมดถูกล้างทันที ซึ่งกระทบของเก่าด้วย
"ทราฟฟิกไปกลับใช้เส้นทางเดียวกัน" ไม่จำเป็น และสมมติฐานนี้คือสิ่งที่ทำให้ ตัวกรองมีสถานะพังในเครือข่ายที่มีหลายเส้นทาง
ตัวอย่างจริงจากงานจริง
กรณีที่ 1 — การเชื่อมต่อขาดหลังเงียบไปพักหนึ่ง
สถานการณ์ เปิดหน้าต่าง ssh ทิ้งไว้ กลับมาพิมพ์แล้วค้าง ทั้งที่เครือข่ายปกติ
อ่านอย่างไร ดูค่า tcp.established ของอุปกรณ์ระหว่างทาง ถ้าการเชื่อมต่อ เงียบนานกว่าค่านั้น สถานะจะถูกลบ และแพ็กเก็ตถัดไปจะไม่เข้าคู่กับอะไรเลย ปลายทางทั้งสองยังคิดว่าการเชื่อมต่อเปิดอยู่ ส่วนไฟร์วอลล์ลืมไปแล้ว วิธีแก้คือ เปิด keepalive ที่ปลายทาง ไม่ใช่แก้ที่ไฟร์วอลล์ เพราะการเพิ่ม timeout ทำให้ ตารางโตขึ้นตามสัดส่วนตรง ๆ
สิ่งที่ยังไม่ได้พิสูจน์ อาการค้างแบบนี้เกิดจาก NAT ที่ลืมการแมปได้ด้วย ซึ่งเป็นคนละตารางกัน ต้องดูว่าอุปกรณ์ตัวไหนทำอะไรอยู่ก่อนจะสรุป
กรณีที่ 2 — บางการเชื่อมต่อล่มแบบสุ่มหลังเพิ่มเส้นทาง
สถานการณ์ เพิ่มวงจรที่สองเพื่อความทนทาน แล้วบางการเชื่อมต่อเริ่มล่ม แบบไม่มีรูปแบบ
อ่านอย่างไร ตรวจว่าทราฟฟิกขาไปกับขากลับผ่านอุปกรณ์ตัวเดียวกันไหม ถ้าไม่ นี่คือกรณีเส้นทางไม่สมมาตรที่อธิบายไว้ข้างบน อาการเป็นแบบสุ่มเพราะการเลือก เส้นทางเปลี่ยนไปตามภาระ ไม่ใช่เพราะอุปกรณ์เสีย
สิ่งที่ยังไม่ได้พิสูจน์ การเห็นว่าเส้นทางไม่สมมาตรอธิบายอาการได้ แต่ยังไม่ ได้พิสูจน์ว่าไฟร์วอลล์เป็นตัวทิ้งจริง ต้องดูตัวนับของอุปกรณ์ทั้งสองฝั่งว่ามี แพ็กเก็ตที่ถูกทิ้งเพราะไม่เข้าคู่กับสถานะหรือเปล่า
กรณีที่ 3 — เลือกว่าจะใช้แบบไหนกับงานของตัวเอง
สถานการณ์ ต้องออกแบบการกรองสำหรับระบบที่มีทราฟฟิกสูงมาก
อ่านอย่างไร ถามสามข้อ
1 ทราฟฟิกไปกลับผ่านอุปกรณ์ตัวเดียวกันแน่นอนไหม
2 จำนวนการเชื่อมต่อพร้อมกันสูงสุดเท่าไหร่ และหน่วยความจำพอไหม
3 ต้องเพิ่มเครื่องกลางคันเพื่อรับภาระไหม
ถ้าข้อแรกตอบว่าไม่แน่นอน หรือข้อสามตอบว่าใช่ ตัวกรองไร้สถานะที่ตำแหน่งนอกสุด บวกกับตัวกรองมีสถานะที่ตำแหน่งในสุด มักเป็นการแบ่งงานที่ทำงานได้จริงมากกว่า การใช้แบบเดียวทั้งหมด
สิ่งที่ยังไม่ได้พิสูจน์ สามข้อนี้ครอบคลุมข้อจำกัดเชิงสถาปัตยกรรม ไม่ได้ ครอบคลุมเรื่องนโยบายว่าอะไรควรอนุญาต ซึ่งเป็นคำถามคนละชุดที่บทความ ไฟร์วอลล์ พูดถึง
สิ่งที่การจำซื้อมา และสิ่งที่มันขายไป
การจำการเชื่อมต่อซื้อความปลอดภัยมาได้จริง และซื้อมาเยอะมาก — จาก 1 ใน 65,535 เป็น 1 ใน หนึ่งพันล้านล้าน ตามตัวเลขที่คำนวณไว้
สิ่งที่มันขายไปคือความเรียบง่าย ตัวกรองไร้สถานะเป็นฟังก์ชันบริสุทธิ์ที่ให้ คำตอบเดิมทุกครั้งจากอินพุตเดิม ส่วนตัวกรองมีสถานะขึ้นกับอดีต ซึ่งแปลว่ามัน มีหน่วยความจำที่เต็มได้ มีสมมติฐานเรื่องเส้นทางที่ผิดได้ และมีความเห็นเกี่ยวกับ การเชื่อมต่อที่ไม่ตรงกับความจริงได้
ทุกอาการล่มที่แปลกที่สุดของไฟร์วอลล์ มาจากคำว่า "ขึ้นกับอดีต" คำเดียวนี้
อ้างอิง
จากเครื่องที่ใช้เขียน
man pf.conf— รายชื่อสถานะทั้งของ TCP, UDP, ICMP ค่า adaptive timeout การตรวจลำดับและ timestamp และประโยคทั้งหมดที่ยกมาอ้างในบทความนี้- บทความ ไฟร์วอลล์ บนเว็บนี้ อ้างหน้าคู่มือเดียวกัน ในส่วนของลำดับกฎและค่าตั้งต้น
คำนวณเอง
- ตารางโอกาสในการเดาคำนวณจากพื้นที่หมายเลขลำดับของ TCP ที่กว้าง 2^32 หน้าต่าง 64 กิโลไบต์ พอร์ตต้นทาง 16 บิต และค่า 10 ถึง 18 บิตของ timestamp ตามที่หน้าคู่มือระบุ