นากกายถือป้ายแยกซองจดหมายในบ้านกับซองที่ส่งออกถนนใหญ่

Python ipaddress: แยก IP ส่วนตัวกับสาธารณะก่อนเปิดพอร์ต

น้องกายEngineering

เวลาสคริปต์รับ IP จากผู้ใช้หรือจาก webhook แล้วเอาไปเปิด socket / ยิงต่อทันที ผมพบว่าพลาดง่ายมากถ้าไม่แยกก่อนว่าเป็นที่อยู่ส่วนตัว (private) หรือสาธารณะ (global) โมดูล ipaddress ให้ทรัพย์สิน is_private และ is_global ตรง ๆ ครับ ไม่ต้องจำช่วง 10.x / 192.168.x เอง

WHY: กรองที่อยู่ก่อนแตะเครือข่าย

IP ส่วนตัวเหมือนบ้านเลขที่ในซอย — ใช้คุยในบ้านได้ แต่ไม่ควรเป็นปลายทางของปุ่ม “ส่งออกอินเทอร์เน็ต” โดยไม่รู้ตัว ส่วน IP สาธารณะเหมือนเลขที่ติดถนนใหญ่ ถ้าสคริปต์รับค่าจากฟอร์มแล้วยิงต่อได้ทุกที่ อาจเผลอสแกนเครื่องในออฟฟิศหรือยิงเข้า printer ใน LAN

import ipaddress
import unittest


def classify(raw: str) -> str:
    addr = ipaddress.ip_address(raw.strip())
    if addr.is_loopback:
        return "loopback"
    if addr.is_private:
        return "private"
    if addr.is_global:
        return "global"
    return "other"


class IpClassifyTests(unittest.TestCase):
    def test_private_and_global(self) -> None:
        self.assertEqual(classify("192.168.1.10"), "private")
        self.assertEqual(classify("10.0.0.5"), "private")
        self.assertEqual(classify("8.8.8.8"), "global")

    def test_loopback(self) -> None:
        self.assertEqual(classify("127.0.0.1"), "loopback")

    def test_bad_input(self) -> None:
        with self.assertRaises(ValueError):
            classify("not-an-ip")


if __name__ == "__main__":
    unittest.main()

HOW: ใช้ทรัพย์สินของออบเจ็กต์ ไม่ฮาร์ดโค้ดช่วง

ip_address รับทั้ง IPv4 และ IPv6 แล้วคืนออบเจ็กต์ที่มีธงพร้อมใช้ is_private ตาม RFC สำหรับช่วง private / unique-local ส่วน is_global บอกว่าใช้บนอินเทอร์เน็ตสาธารณะได้ อย่าเขียน startswith("192.168.") เอง เพราะพลาด IPv6 และช่วงอื่น

ตัวอย่างการใช้จริง: ก่อนเปิดพอร์ต listen ที่ผูกกับอินเทอร์เฟซภายนอก หรือก่อนที่ระบบจะ “เรียกกลับ” URL ที่ผู้ใช้ใส่ ให้ classify ก่อน แล้วปฏิเสธ private/loopback ถ้าจานงานคือบริการสาธารณะ

ที่อยู่บางชนิดไม่ private แต่ก็ไม่ global (เช่น link-local) ฟังก์ชันตัวอย่างจึงมีถัง other ถ้าต้องการอนุญาตเฉพาะ public egress ให้ผ่านเฉพาะ is_global และทดสอบ IPv6 ด้วย

Recap

  • ใช้ ipaddress.ip_address แทนการจำเลขช่วงเอง
  • แยก loopback / private / global ก่อน bind หรือยิงต่อ
  • อย่าลืม IPv6 และค่าที่ parse ไม่ได้ต้อง raise ชัด ๆ