วิธีกำหนดเส้นทางให้แอปเดียวผ่าน VPN โดยไม่ต้องทั้งเครื่อง
ไม่มีเดสก์ท็อปที่ไม่ได้จัดการรายใดมีสวิตช์ VPN ต่อโปรเซส เลือกชั้นที่ใช้เลือกทราฟฟิก: ซ็อกเก็ตที่ผูกไว้ กฎ UID โปรไฟล์ไฟร์วอลล์ หรือฮ็อป SOCKS
ไคลเอนต์ VPN เชื่อมต่อได้ เบราว์เซอร์ทำงาน และคำสั่งเดียวที่คุณสนใจจริง ๆ ก็ยังออกผ่านการเชื่อมต่อที่บ้านของคุณ หรือตรงกันข้าม อุโมงค์คว้าเส้นทางเริ่มต้นไป และเซิร์ฟเวอร์ไฟล์ในเครื่องก็หายไปจากเดสก์ท็อป ผมเจอเรื่องนี้กับเอเจนต์บิลด์ที่ต้องเข้าถึงกระจกอาร์ติแฟกต์ภายใน ขณะที่โฮสต์ส่วนที่เหลือยังอยู่ใน LAN ของออฟฟิศ และไคลเอนต์เสนอทางเลือกเพียงสองอย่าง คือทราฟฟิกทั้งหมดหรือไม่เลย คำขอนี้เหมือนกันทั้งสองทิศทาง คือแอปนี้และไม่ใช่ทั้งเครื่อง และไคลเอนต์ส่วนใหญ่ไม่มีช่องทำเครื่องหมายสำหรับมัน เพราะตารางเส้นทางไม่เคยรู้ว่าโปรแกรมคืออะไร
ทำไมตารางเส้นทางจึงมองไม่เห็นโปรแกรมของคุณ
เมื่อโปรเซสส่งแพ็กเก็ต สแต็กจะค้นหาที่อยู่ปลายทางในตารางเส้นทาง ทั้งชื่อไฟล์ปฏิบัติการและพอร์ตไม่เข้าไปในการค้นหานั้น ชื่อโดเมนล้มเหลวด้วยเหตุผลเดียวกัน อุโมงค์พาทราฟฟิกไปยังที่อยู่ และเมื่อชื่อถูกแปลงเป็นที่อยู่ การกำหนดเส้นทางคือคันโยกเดียวที่เคอร์เนลยังมี นั่นคือเหตุผลที่ "VPN เฉพาะไซต์นี้" สุดท้ายหมายถึงพร็อกซี และ "VPN เฉพาะโปรแกรมนี้" หมายถึงหนึ่งในกลไกไม่กี่อย่าง
| กลไก | สิ่งที่เลือก | ครอบคลุม | เมื่ออุโมงค์ขาด |
|---|---|---|---|
| การผูกซ็อกเก็ต | ที่อยู่ต้นทาง | ไคลเอนต์ที่คุณควบคุม | การเชื่อมต่อล้มเหลว |
| กฎ UID (Linux) | ผู้ใช้เจ้าของ | ทุกโปรเซสของบัญชีเดียว | การเชื่อมต่อล้มเหลว |
| โปรไฟล์ไฟร์วอลล์ (Windows) | โปรไฟล์อะแดปเตอร์ | โปรแกรมเดียว | ล้มเหลวแบบปิด |
| เนมสเปซเครือข่าย | การเป็นสมาชิกเนมสเปซ | ทุกอย่างที่เริ่มภายใน | การเชื่อมต่อล้มเหลว |
| ฮ็อป SOCKS | การตั้งค่าพร็อกซีของแอป | แอปเดียวหรือโปรไฟล์เบราว์เซอร์เดียว | การเชื่อมต่อล้มเหลว |
ไม่มีสิ่งใดเป็นเส้นทางต่อโปรเซส แต่ละอย่างย้ายการตัดสินใจไปยังชั้นที่มองเห็นโปรแกรม ผู้ใช้ หรือการกำหนดค่าของแอปเอง
ปลายทางเดียวก็ยังเป็นเส้นทาง ไม่ใช่กฎของโปรแกรม บน macOS สูตรปกติคือยกเลิกเครื่องหมายที่ "ส่งทราฟฟิกทั้งหมดผ่าน VPN" แล้วเพิ่มเส้นทางโฮสต์: sudo route add -host internal-tuner.example -interface ppp0 ซึ่งลบออกอีกครั้งได้ด้วย sudo route delete -host internal-tuner.example -interface ppp0 อ่านชื่ออินเทอร์เฟซจาก ifconfig ก่อน เพราะ ppp0 เป็นเพียงตัวอย่าง อักขระไวลด์การ์ดใช้ไม่ได้ในเส้นทางโฮสต์ บริการที่คุยกับหลายชื่อจึงต้องมีรายการหนึ่งต่อหนึ่งชื่อ และกฎนี้ครอบคลุมทุกโปรเซสในเครื่อง ไม่ใช่เฉพาะตัวที่คุณคิดไว้
ผูกซ็อกเก็ตกับที่อยู่ของอุโมงค์
เมื่อคุณควบคุมไคลเอนต์ การผูกคือตัวเลือกที่ถูกที่สุด ซ็อกเก็ตที่ผูกกับที่อยู่ภายในของอุโมงค์จะออกผ่านอินเทอร์เฟซนั้นตราบใดที่มีเส้นทางไปยังปลายทางที่นั่น และทุกอย่างอื่นในเครื่องยังคงเส้นทางปกติ ความละเอียดอ่อนคือการผูกเลือกที่อยู่ต้นทาง ขณะที่ตารางเส้นทางยังเลือกทางออก การผูกอย่างเดียวไม่ย้ายทราฟฟิกที่ไม่มีเส้นทางเข้าไปในอุโมงค์ และนั่นคือความผิดพลาดเบื้องหลังรายงาน "ผูกแล้วแต่ยังออกผิดทาง" ส่วนใหญ่
import socket
s = socket.socket()
s.bind(("10.8.0.2", 0))
s.connect(("build.internal.example", 443))เครื่องมือระดับสูงกว่าแสดงแนวคิดเดียวกัน: curl --interface 10.8.0.2, CURLOPT_INTERFACE ใน PHP และอาร์กิวเมนต์ source_address ในไคลเอนต์ HTTP ของ Python ที่อยู่ต้องเป็นของอุโมงค์ และการพิมพ์ผิดตรงนี้จะล้มเหลวเป็นการเชื่อมต่อผิดพลาดธรรมดา ไม่ใช่การบ่นเรื่องเส้นทาง การผูกกับชื่ออุปกรณ์แทนที่อยู่เป็นสิทธิพิเศษของ Linux และไม่ส่งต่อไปยัง Windows หรือ macOS นั่นคือเหตุผลที่สคริปต์ที่พกพาได้ส่งที่อยู่
Linux: จับคู่เจ้าของ ไม่ใช่ชื่อโปรแกรม
เคอร์เนลจับคู่ผู้ใช้เจ้าของซ็อกเก็ตได้ ดังนั้น บัญชีเฉพาะจึงกลายเป็นตัวเลือก วางอุโมงค์ไว้ในตารางเส้นทางของตัวเองและส่งเฉพาะทราฟฟิกของบัญชีนั้นไปที่นั่น:
ip rule add uidrange 1001-1001 lookup 100
ip route add default dev tun0 table 100วิธีนี้ใช้ได้เฉพาะเมื่ออุโมงค์ยังไม่ใช่เส้นทางเริ่มต้น เริ่มไคลเอนต์ด้วย --route-nopull หรือกรองการเปลี่ยนเส้นทางที่ถูกส่งมาด้วย --pull-filter ignore "redirect-gateway" บน OpenVPN 2.4 และใหม่กว่า แล้วเพิ่มรายการในตารางเอง มิฉะนั้นจะมีเส้นทางเริ่มต้นทั้งสองและตัวที่ลำดับความสำคัญต่ำกว่าจะชนะ เวอร์ชันที่ตั้งใจคือชี้เส้นทางเริ่มต้นไปที่เกตเวย์ของอุโมงค์ หรือทำจากสคริปต์ตัดการเชื่อมต่อ มันกันทราฟฟิกออกจากอินเทอร์เน็ตเมื่ออุโมงค์ขาด แต่นั่นคือสวิตช์ฆ่า ไม่ใช่ตัวเลือก และมันอยู่จนกว่าจะมีคนเอาออก
เวอร์ชันที่แข็งแกร่งกว่าคือเนมสเปซเครือข่าย สร้างมัน รันไคลเอนต์ข้างใน และเริ่มแอปด้วย ip netns exec ไม่มีอะไรนอกเนมสเปซเห็นอินเทอร์เฟซอุโมงค์ แอปจึงกลับไปใช้เส้นทางของโฮสต์โดยอุบัติเหตุไม่ได้ ต้นทุนเป็นของจริง เนมสเปซต้องมีคู่ veth เพื่อเข้าถึงเครือข่ายกายภาพและไฟล์รีโซลเวอร์ของตัวเอง เพราะเนมสเปซการเมานต์ไม่ได้ใช้การตั้งค่าการแปลงชื่อของโฮสต์ร่วมกัน
โหมดล้มเหลวสองอย่างอธิบายการรั่วไหลส่วนใหญ่ การแปลงชื่อยังเกิดในรีโซลเวอร์ของโฮสต์ คำถามจึงออกนอกอุโมงค์แม้การเชื่อมต่อ TCP จะเข้าไปข้างใน และ IPv6 ต้องมี ip -6 rule ของตัวเอง เพราะกฎ v4 ไม่ครอบคลุม
Windows และ macOS: จำกัดไฟร์วอลล์หรือมอบพร็อกซี
Windows ก็ไม่มีตัวเลือกเส้นทางต่อแอปเช่นกัน แต่กฎไฟร์วอลล์จำกัดให้เฉพาะโปรไฟล์ได้ และอะแดปเตอร์ VPN จัดเป็นสาธารณะได้ขณะที่ LAN ที่บ้านยังเป็นส่วนตัว สร้างกฎบล็อกขาออกสำหรับโปรแกรมบนโปรไฟล์ส่วนตัวและโดเมน เปิดสาธารณะไว้ และแอปจะเข้าถึงเครือข่ายได้ทางอุโมงค์เท่านั้น ตั้งหมวดหมู่เครือข่ายของการเชื่อมต่อก่อน แล้วยืนยันในแผงตรวจสอบของไฟร์วอลล์ว่ากฎตรงกัน มันคือการบล็อก ไม่ใช่เส้นทาง เมื่ออุโมงค์ขาดมันล้มเหลวแบบปิด และหยุดแอปที่เพิกเฉยต่อไฟร์วอลล์ไม่ได้
macOS ใช้ตรรกะเดียวกันด้วยเครื่องมือต่างกัน ตัวรับฟัง SOCKS ที่เข้าถึงผ่าน SSH เป็นทางเลือกที่รุกรานน้อยที่สุด เพราะ ssh -D 1080 user@host สร้างพอร์ตภายในหนึ่งพอร์ต และเฉพาะแอปที่กำหนดให้ใช้จึงไปทางนั้น กฎ pf บล็อกโปรแกรมตามผู้ใช้หรือกลุ่มได้ แต่ยังเลือกอุโมงค์ให้เฉพาะโปรแกรมนั้นไม่ได้ Tunnelblick กรองตามโดเมนไม่ได้เลย เพราะเส้นทางทำงานกับที่อยู่ ผู้ใช้ OpenConnect หันไปใช้ vpn-slice เมื่อเซิร์ฟเวอร์ส่งรายการคำนำหน้าที่กว้างเกินไป
ทางแก้สองอย่างครอบคลุมสิ่งที่เครื่องมือของแพลตฟอร์มทำไม่ได้ ส่วนขยายเบราว์เซอร์ที่สลับพร็อกซีตามโดเมนเป็นคำตอบที่ใช้ได้จริงสำหรับ "เฉพาะไซต์นี้" เพราะทำงานที่ชั้นแอปซึ่งชื่อยังมีอยู่ มันครอบคลุมเบราว์เซอร์เดียวและอย่างอื่นไม่ได้ วิธีหยาบคือรันไคลเอนต์ของผู้ขายในเครื่องเสมือน ให้เกสต์เป็นเจ้าของการเปลี่ยนเส้นทางขณะที่โฮสต์ยังคงเส้นทางของตน ด้วยต้นทุนสแต็กเครือข่ายสองชุดที่ต้องดูแล
ฮ็อป SOCKS สำหรับเครื่องมือที่ไม่มีตัวเลือกผูก
แอปส่วนใหญ่ชี้ไปที่พร็อกซี HTTP หรือ SOCKS ได้แม้ไม่รู้อะไรเกี่ยวกับอินเทอร์เฟซ ไคลเอนต์ SSH ให้มาโดยไม่ต้องเปลี่ยนฝั่งเซิร์ฟเวอร์:
ssh -N -D 127.0.0.1:9999 [email protected]อะไรก็ตามที่ตั้งให้ใช้พอร์ตนั้นจะออกที่ป้อมปราการ ขณะที่เดสก์ท็อปส่วนที่เหลือยังคงการเชื่อมต่อปกติ อินสแตนซ์ 3proxy ภายในที่ผูกกับที่อยู่ของอุโมงค์ทำแบบเดียวกันสำหรับพอร์ตเดียวโดยไม่ต้องใช้ SSH และ stunnel ครอบคลุมพอร์ตเดียวที่ห่อด้วย TLS ข้อจำกัดเป็นเรื่องปฏิบัติ พร็อกซีพาได้เฉพาะโปรโตคอลที่รู้วิธีใช้ UDP ต้องมีการเชื่อมโยง UDP ของ SOCKS5 ซึ่งการตั้งค่า "SOCKS5" ของไคลเอนต์หลายตัวไม่ได้เปิด และ DNS ต้องให้พร็อกซีแปลง (socks5h ใน curl) ไม่เช่นนั้นชื่อจะรั่วไปยังรีโซลเวอร์ในเครื่อง
เก็บตัวรับฟังไว้ที่ลูปแบ็ก เว้นแต่เครื่องอื่นต้องการ
ตรวจสอบเส้นทางด้วยการดักจับ ไม่ใช่ข้อความสถานะ
หน้าต่างสถานะของแอปอธิบายการตั้งค่า ไม่ใช่แพ็กเก็ต ดักจับบนอินเทอร์เฟซทั้งสองและเปรียบเทียบ:
tcpdump -i any -n port 443
ss -tnp | grep 443ถ้าการเชื่อมต่อปรากฏบนอินเทอร์เฟซกายภาพโดยไม่มีรายการที่ตรงกันบนอุโมงค์ ตัวเลือกไม่ได้ผล ไม่ว่าไคลเอนต์จะรายงานอะไร ตรวจสอบ IPv6 และรีโซลเวอร์แยกกัน และมองหาเส้นทางที่สอง เช่น โปรเซสช่วยหรือพร็อกซีระบบที่สืบทอดมา
จากนั้นเลือกขอบเขตที่ตรงกับความต้องการ พร็อกซีพอสำหรับหนึ่งไซต์ กฎ UID พอสำหรับหนึ่งบัญชี โปรไฟล์ไฟร์วอลล์พอเมื่อการเชื่อมต่อต้องล้มเหลวแบบปิด และเนมสเปซเป็นทางเลือกเดียวที่ซ่อนอุโมงค์จากทุกอย่างภายนอก