Một địa chỉ IP như 10.10.0.1 tự nó không nói cho máy biết ai là "hàng xóm cùng mạng" và ai phải đi qua router. Thứ quyết định điều đó là subnet mask, hay cách viết gọn hơn là prefix — con số sau dấu gạch chéo trong 10.10.0.1/24. Bài này dựng một đường mạng thật trong container, rồi cho chính nhân Linux nói ra: cùng một địa chỉ đích, prefix đổi thì kết luận "cùng mạng con hay không" cũng đổi theo.
Prefix cắt 32 bit thành hai phần
Một địa chỉ IPv4 là 32 bit. Subnet mask cũng là 32 bit, gồm một dãy bit 1 liền nhau rồi tới các bit 0. Số bit 1 chính là prefix. Phần bit 1 khoanh vùng phần mạng; phần bit 0 còn lại là phần host. /24 nghĩa là 24 bit đầu là mạng, 8 bit cuối là host — tức mặt nạ 255.255.255.0.
Từ prefix, mọi con số khác suy ra bằng phép tính bit, không cần tra bảng. Tôi để ipaddress trong Python tính trực tiếp:
| prefix | netmask | tổng địa chỉ | host dùng |
|---|---|---|---|
| /24 | 255.255.255.0 | 256 | 254 |
| /25 | 255.255.255.128 | 128 | 126 |
| /26 | 255.255.255.192 | 64 | 62 |
| /28 | 255.255.255.240 | 16 | 14 |
| /30 | 255.255.255.252 | 4 | 2 |
| /31 | 255.255.255.254 | 2 | 2 |
Tổng địa chỉ là 2 mũ (số bit host). Với /24 là 2^8 = 256, /30 là 2^2 = 4. "Host dùng" thường bằng tổng trừ 2 — trừ đi địa chỉ mạng (bit host toàn 0) và địa chỉ quảng bá (bit host toàn 1). Trừ khi là /31, nhưng ta sẽ nói tới nó sau.
Một mẹo tính nhẩm biên mạng con mà không cần đổi ra nhị phân: cỡ khối bằng 256 trừ đi octet cuối của mặt nạ. Với /26, mặt nạ là 255.255.255.192, cỡ khối là 256 − 192 = 64. Nghĩa là các mạng con /26 bắt đầu ở bội số của 64: .0, .64, .128, .192. Một địa chỉ như 10.10.0.70/26 do đó nằm trong khối .64 – .127, với .64 là địa chỉ mạng và .127 là quảng bá. Cùng cách đó, /28 có cỡ khối 16 nên biên rơi vào .0, .16, .32, …; còn /25 cắt một /24 thành đúng hai nửa .0 – .127 và .128 – .255. Nhớ cỡ khối là đủ để trả lời "hai địa chỉ này có cùng mạng con không" trong đầu, không cần máy tính.
Cùng một địa chỉ, hai kết luận khác nhau
Đây là phần thú vị nhất, và cũng là thứ chỉ đo được khi chạy thật. Tôi tạo một cặp veth, gán cho veth0 một địa chỉ với prefix chọn trước, rồi hỏi nhân bằng ip route get xem nó xử lý một đích thế nào:
ip link add veth0 type veth peer name veth1
ip link set veth0 up
ip addr add 10.10.0.1/24 dev veth0
ip route get 10.10.0.20 # cùng /24?
ip route get 10.10.1.20 # ngoài /24
Với veth0 = 10.10.0.1/24 (mạng con trải từ .0 tới .255):
10.10.0.20→ trên-link: nhân trả vềdev veth0, tức nó sẽ ARP thẳng để tìm địa chỉ MAC.10.10.1.20→ ngoài-link: nằm ngoài/24, cần một router; không có route thì "unreachable".
Bây giờ đổi mỗi prefix, giữ nguyên địa chỉ của chính mình:
ip addr flush dev veth0
ip addr add 10.10.0.1/30 dev veth0 # mạng con chỉ .0 - .3
ip route get 10.10.0.2 # trong /30
ip route get 10.10.0.20 # ngoài /30
Với /30 (mạng con chỉ gồm .0 tới .3):
10.10.0.2→ trên-link.10.10.0.20→ ngoài-link.
Chú ý địa chỉ 10.10.0.20: dưới /24 nó là hàng xóm, dưới /30 nó thành người xa lạ phải đi qua router. Cùng một IP, verdict lật ngược, chỉ vì prefix đổi. Đó là bằng chứng trực tiếp rằng biên mạng con do prefix vẽ ra, không phải do bản thân địa chỉ. Tôi chạy phép đo này ba lần, kết quả giống hệt tới từng dòng — đúng như mong đợi với một quyết định thuần bit.
Để chắc chắn "trên-link" không chỉ là chữ trong bảng định tuyến, tôi ping thật giữa 10.10.0.1/24 và một đầu veth khác ở 10.10.0.2/24: gói đi về trong 0,01 mili giây và bảng ARP hiện 10.10.0.2 lladdr ... REACHABLE. Hàng xóm thật, ARP thật.
/31: bỏ hẳn quy ước hai địa chỉ dư
/30 cho 4 địa chỉ nhưng chỉ 2 dùng được — một nửa số địa chỉ bị "đốt" cho mạng và quảng bá. Với các liên kết điểm-điểm giữa hai router, đó là lãng phí khó chịu. RFC 3021 cho phép dùng /31: chỉ 2 địa chỉ, và cả hai đều là host thật, không dành riêng cái nào cho mạng hay quảng bá.
Tôi không muốn tin lời, nên đo:
ip addr add 10.10.0.0/31 dev veth0 # .0 làm HOST, không phải "network"
ip netns exec s2 ip addr add 10.10.0.1/31 dev veth1
ping -c1 10.10.0.1
64 bytes from 10.10.0.1: ... time=0.005 ms. Địa chỉ 10.10.0.0 — cái mà mọi bảng trong sách gọi là "địa chỉ mạng, không gán cho host" — ở đây là một host bình thường, ping xuyên qua chạy ngon. /31 biến 2 địa chỉ thành 2 host, không dư byte nào.
Một lần tôi tưởng sai về nhân
Trong lúc đo, tôi định chứng minh rằng địa chỉ .0 và .255 là "không dùng được" bằng cách thử gán chúng và chờ nhân báo lỗi. Kết quả trái với dự đoán của tôi:
ip addr add 10.10.0.0/24 dev d0 # -> CHẤP NHẬN, không lỗi
ip addr add 10.10.0.255/24 dev d0 # -> CHẤP NHẬN, không lỗi
Nhân Linux nhận cả hai không một tiếng phàn nàn, và ip addr show liệt kê chúng như địa chỉ hợp lệ. Vậy con số "254 host dùng được" (chứ không phải 256) không phải do nhân cấm — nó là quy ước: .0 mang nghĩa "mạng con này" và .255 là địa chỉ quảng bá theo cách các host và giao thức khác diễn giải. Bạn có thể gán chúng cho một giao diện; chỉ là các máy khác sẽ hiểu nhầm, và quảng bá tới .255 sẽ không tới riêng máy đó. Chính vì .255 là quảng bá theo quy ước nên /31 — vốn không có địa chỉ quảng bá — mới an toàn dùng cả hai địa chỉ.
Bài học đo lường: đừng suy ra "nhân cấm" từ "sách nói không dùng được". Hai điều đó khác nhau, và chỉ cần một lệnh ip addr add là phân biệt được.
Vì sao điều này quan trọng khi lập trình
Khi dịch vụ của bạn mở kết nối tới một IP, quyết định đầu tiên của máy là: đích này trên-link hay ngoài-link? Nếu trên-link, nó ARP thẳng; nếu ngoài-link, nó gửi cho gateway. Prefix cấu hình sai — ví dụ đặt /24 trong khi hạ tầng thật là /25 — khiến máy tưởng nửa số host là hàng xóm trong khi thực ra chúng ở mạng con khác, và gói đi thẳng vào hư không thay vì qua router. Triệu chứng là "kết nối tới một số IP thì được, một số thì treo", một lỗi rất khó lần nếu không nhớ rằng prefix mới là thứ vẽ biên.
Chọn prefix cũng là chọn ngân sách địa chỉ: một /24 chứa 254 host, một /28 chỉ 14. Và cho liên kết điểm-điểm, /31 gấp đôi hiệu quả so với /30.
Thử ba mươi giây
Chạy ip route get <một IP bất kỳ> trên máy của bạn. Nếu dòng trả về có dev <giao diện> mà không có via, đích đó đang được coi là trên-link — máy sẽ ARP thẳng. Nếu có via <gateway>, nó là ngoài-link. Đổi prefix của giao diện rồi hỏi lại cùng một IP: bạn sẽ thấy verdict lật, và tận mắt thấy subnet mask đang làm gì.