ผมเริ่มจากภายนอกเหมือนที่ผู้ใช้จะทำ พิมพ์ชื่อลงในเบราว์เซอร์ของโทรศัพท์บนข้อมูลมือถือ แล้วเห็นการเชื่อมต่อค้างจนหมดเวลา ชื่อถูกแปลงเป็นที่อยู่ได้ แสดงว่า DNS ปกติดี หน้าส่งต่อพอร์ตของเราเตอร์ที่บ้านก็เป็นทางตันเช่นกัน เพราะที่อยู่ WAN ของมันขึ้นว่า 100.64.31.42 ซึ่งไม่ใช่ที่อยู่ที่ผู้ให้บริการจะส่งต่อมาถึงผมจากอินเทอร์เน็ต ตัวเลขนี้บอกความล้มเหลวทั้งหมด สายอยู่หลัง carrier-grade NAT ดังนั้นที่อยู่สาธารณะทางฝั่งของผมจึงเป็นของผู้ให้บริการ ต่อไปนี้คือเส้นทางที่ผมเดินจากแท็บที่ค้างอยู่นั้นไปจนถึงบริการที่ตอบสนองได้

เริ่มจากไคลเอนต์แล้วไล่เข้าข้างใน

ก่อนจะแก้กฎใด ๆ ให้พิสูจน์ก่อนว่าลูกโซ่หยุดตรงไหน บนแล็ปท็อปผมรัน dig +short files.example.net แล้วได้ที่อยู่ของ VPS กลับมา ซึ่งตัดปัญหา DNS ค้างเก่าบนเครื่องของผมออกไป จากนั้น curl -v --connect-timeout 5 files.example.net:5022 กลับหมดเวลาแทนที่จะถูกปฏิเสธ ความต่างนี้สำคัญ เพราะการหมดเวลามักหมายถึงแพ็กเก็ตถูกทิ้งระหว่างทาง ขณะที่การปฏิเสธหมายถึงมีอะไรบางอย่างตอบกลับมาว่าไม่

ก้าวถัดไปเข้าข้างในคือตัว VPS เอง ss -ltnp ไม่แสดงอะไรที่ฟังอยู่บน 5022 ซึ่งเป็นสิ่งที่คาดไว้ก่อนมีกฎ DNAT บนเครื่องเดียวกัน wg show (หรือ ip -br a show tun0 สำหรับ OpenVPN) ยืนยันว่า peer ที่บ้านทำงานอยู่และมี handshake ใหม่ ถ้า peer ล่ม กฎ NAT ใด ๆ ก็ช่วยไม่ได้ และไฟสีเขียวบนหน้า VPN ของเราเตอร์ที่บ้านก็ไม่มีความหมาย

ยืนยันว่าสายที่บ้านอยู่หลัง CGNAT จริง

เทียบที่อยู่ที่เราเตอร์รายงานบนอินเทอร์เฟซ WAN กับสิ่งที่บริการภายนอกเห็น ทุกอย่างในช่วง 100.64.0.0/10 คือตั้งแต่ 100.64.x.x ถึง 100.127.x.x เป็นช่วงที่อยู่ร่วมกันตาม RFC 6598 และเข้าถึงจากอินเทอร์เน็ตไม่ได้ คุณจะได้ข้อสรุปเดียวกันเมื่อเราเตอร์อ้างว่ามีที่อยู่ส่วนตัวธรรมดา แต่การค้นหาสาธารณะกลับให้สิ่งที่เกี่ยวข้องกันไม่ได้ สาย CGNAT อาจมาพร้อมที่อยู่สาธารณะที่อยู่ได้เพียงถึง lease ถัดไป การส่งต่อพอร์ตจึงดูเหมือนทำงานหนึ่งชั่วโมงแล้วก็ตายเงียบ ๆ

ผู้ให้บริการบางรายให้ที่อยู่สาธารณะจริง แต่บล็อกพอร์ตขาเข้า 80 และ 443 ในบัญชีที่พักอาศัย ถ้าบริการของคุณเป็นเว็บอย่างเดียวและที่อยู่ WAN เป็นสาธารณะจริง ให้ถามฝ่ายสนับสนุนก่อนสร้าง relay ถ้าไม่ใช่ การออกแบบด้านล่างใช้ได้เท่ากันกับทุกพอร์ต และควรหยุดการทดลองฝั่งเราเตอร์ เพราะตัวแปลงในเครือข่ายของผู้ให้บริการจะตัดการเชื่อมต่อก่อนที่มันจะไปถึงอินเทอร์เฟซ WAN ของคุณ

ชี้ชื่อไปที่ที่อยู่ของ VPS

DNS ไม่ได้ส่งต่อทราฟฟิก ดังนั้นระเบียน A ของบริการต้องมีที่อยู่สาธารณะของ VPS ระเบียน A ที่ชี้ไป 192.168.1.20 จะแปลงได้อย่างมีประโยชน์เฉพาะไคลเอนต์ที่อยู่ใน VPN แล้วเท่านั้น จากอินเทอร์เน็ตมันจะแปลงเป็นที่อยู่ที่ไม่มีใครกำหนดเส้นทางได้ ผมเก็บชื่อแยกต่างหากสำหรับการเข้าถึงของผู้ดูแล VPS เอง เพื่อไม่ให้สับสนว่าระเบียนหนึ่งอ้างถึงโฮสต์ใด

ถ้าทั้ง VPS และเครื่องที่บ้านให้บริการ HTTP ให้แต่ละตัวมีชื่อของตัวเอง และให้ VPS ตัดสินใจว่าใครจะตอบ การรวมทั้งสองไว้ในชื่อเดียวแล้วหวังว่า proxy จะเลือก backend ถูกตัว คือวิธีที่ทำให้คุณเสียเวลาครึ่งวันไล่บั๊กผิดโฮสต์

DNAT หนึ่งพอร์ต ปล่อย SSH ไว้ที่ 22

ทางเลือกตรงไปตรงมาคือ NAT ปลายทางบน VPS เปิด forwarding สำหรับเคอร์เนลที่กำลังทำงาน และเก็บที่อยู่ tunnel ของเซิร์ฟเวอร์ที่บ้านไว้ในกฎ ไม่ใช่ที่อยู่ LAN ของมัน

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A PREROUTING -p tcp -d 198.51.100.10 --dport 5022 -j DNAT --to-destination 10.8.0.2

ปล่อย SSH ไปยัง VPS เองไว้ที่ 22 แล้วส่งต่อพอร์ตอื่น เช่น 5022 ไปยังเครื่องที่บ้าน ถ้าส่งต่อ 22 ไปยังเครื่องที่ปิดอยู่ คุณจะเสียเส้นทางเดียวที่แก้ไขมันได้ ตรรกะเดียวกันใช้กับ proxy คือเก็บพอร์ตจัดการของ VPS ไว้นอกกฎ relay

ทำให้กฎคงอยู่เมื่อมันทำงานได้ บน Debian หรือ Ubuntu apt install iptables-persistent แล้วตามด้วย netfilter-persistent save จะเขียนกฎลงใน /etc/iptables/rules.v4 บนโฮสต์ที่ใช้ firewalld สิ่งที่เทียบเท่าคือการส่งต่อพอร์ตถาวรบนโซน ซึ่งต้องรัน firewall-cmd --reload ก่อนจึงจะมีผล กฎที่อยู่ในหน่วยความจำเท่านั้นจะหายไปเมื่อผู้ให้บริการรีบูตบำรุงรักษาครั้งแรก ซึ่งมักเป็นเวลาที่ไม่ดี

แก้เส้นทางขากลับ หรือเลี่ยงไม่ให้ต้องใช้

DNAT เขียนใหม่เฉพาะปลายทาง เซิร์ฟเวอร์ที่บ้านเห็นการเชื่อมต่อจากที่อยู่ไคลเอนต์เดิม และถ้าเส้นทางเริ่มต้นของมันชี้ไปที่เราเตอร์ที่บ้าน คำตอบจะออกทาง WAN ของบ้าน ซึ่งตัวแปลงของผู้ให้บริการจะทิ้งมันไป ความไม่สมมาตรนี้คือความล้มเหลวที่สองที่ผมเจอ หลังจากกฎ DNAT ดูถูกต้องสมบูรณ์แล้ว

มีสองวิธีที่ได้ผล ให้เครื่องที่บ้านมีเส้นทางตามนโยบาย เพื่อให้ทราฟฟิกจาก tunnel กลับออกไปทาง tunnel

ip rule add from 10.8.0.2 table 100
ip route add default dev wg0 table 100

หรือให้ VPS เขียนที่อยู่ต้นทางใหม่ เพื่อให้คำตอบมีที่อยู่ที่มันไปถึงได้ และให้ conntrack พลิกกลับตอนขาออก

iptables -t nat -A POSTROUTING -d 10.8.0.2 -p tcp --dport 5022 -j SNAT --to-source 10.8.0.1

เวอร์ชัน SNAT ซ่อนที่อยู่ไคลเอนต์จริงจากเซิร์ฟเวอร์ที่บ้าน บันทึกของแอปพลิเคชันจึงแสดง 10.8.0.1 เป็น peer ส่งที่อยู่เดิมต่อในชั้นแอปพลิเคชัน หรือยอมรับว่าการบันทึกต่อไคลเอนต์จะหายไป แต่ตัดสินใจอย่างรู้ตัวแทนที่จะไปค้นพบระหว่างเกิดเหตุ

ตรวจทั้งเส้นทางจากภายนอก

ตรวจจากเครือข่ายที่ไม่ใช่ทั้ง VPS และไม่ใช่บ้าน เช่น โทรศัพท์บนข้อมูลมือถือ หรือ shell ราคาถูกที่อื่น nc -vz files.example.net 5022 ควรแจ้งว่าการเชื่อมต่อสำเร็จ และ openssl s_client -connect files.example.net:443 -servername files.example.net ควรแสดงใบรับรองของชื่อสาธารณะ จากนั้นตรวจ security group หรือไฟร์วอลล์คลาวด์ของผู้ให้บริการ กฎ DNAT ที่ไม่เคยเห็นแพ็กเก็ตมักถูกบล็อกที่ชั้นเหนือโฮสต์ขึ้นไปหนึ่งชั้น

สุดท้าย ตัดสินใจว่าเกิดอะไรขึ้นเมื่อ tunnel ล่ม กฎ DNAT ยังคงรับการเชื่อมต่อและป้อนมันเข้า tunnel ที่ตายแล้วอย่างเงียบ ๆ ซึ่งดูเหมือนบริการที่พังพอดี การตรวจสุขภาพบน VPS ที่ปฏิเสธพอร์ตตราบใดที่ peer ไม่ทำงาน ใช้โค้ดไม่กี่บรรทัดและประหยัดเวลาการเดาไปหนึ่งชั่วโมง หลังจากนั้นหยุดแก้กฎและเฝ้าดูหนึ่งวันก่อนจะเชื่อถือมัน

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