IPv4 주소 체계
32비트 IPv4 주소를 점 십진 표기와 이진수로 읽고, 프리픽스 길이로 네트워크부와 호스트부를 나누는 계산을 ipaddress 모듈로 확인합니다. 옛 클래스 체계가 사라진 이유, 사설 주소와 공인 주소와 공유 주소, 루프백과 링크 로컬 같은 특수 주소, IP 헤더의 TTL을 정리합니다.
앞 장의 MAC 주소는 같은 링크 안의 장치를 가리킬 뿐이어서, 서울의 컴퓨터가 미국의 서버에 데이터를 보내려면 다른 주소가 필요합니다. 전 세계의 네트워크를 건너 목적지를 찾아가는 논리적인 주소가 IP(internet protocol) 주소입니다. IPv4는 출발지와 목적지 호스트를 32비트 주소로 가리키고, 라우터들이 그 주소를 보고 패킷을 목적지 쪽으로 넘겨 가게 합니다. 패킷이 빠짐없이 순서대로 도착하게 하는 일은 IP가 아니라 TCP 같은 위 계층이 맡습니다.
32비트 주소 읽기
IPv4 주소는 32비트로, 8비트씩 네 덩어리로 나누어 각각을 십진수로 쓰고 점으로 잇는 점 십진 표기(dotted decimal notation)로 나타냅니다. 예를 들어 192.168.1.100을 이진수로 쓰면 11000000.10101000.00000001.01100100입니다. 덩어리 하나는 0부터 255까지의 값이고, 주소는 모두 2³², 곧 약 43억 개입니다.
더 깊이: 점 십진 표기가 아닌데도 읽히는 주소
점 십진 표기는 사람이 읽기 위한 관례일 뿐, 모든 프로그램이 이 형식만 받아들이는 것은 아닙니다. C 라이브러리의 오래된 함수 inet_aton()은 man 페이지에 적힌 대로 마지막 부분이 남은 바이트를 모두 채우는 a.b.c, a.b, a 형식도 받고, 각 부분이 0으로 시작하면 8진수로, 0x로 시작하면 16진수로 읽습니다. 그래서 127.1, 2130706433, 0x7f.0.0.1은 모두 127.0.0.1과 같은 주소가 되고, 010.0.0.1은 10.0.0.1이 아니라 8.0.0.1이 됩니다.
이 차이는 보안 문제로 이어질 수 있습니다. 주소를 검사하는 코드와 실제로 연결하는 코드가 같은 문자열을 서로 다른 주소로 읽으면, 내부 주소로 가는 요청을 막으려던 검사를 지나칠 수 있기 때문입니다. 그래서 새 코드는 네 부분의 십진수만 받는 inet_pton()을 씁니다. Python의 ipaddress 모듈도 3.9.5부터는 8진수와 헷갈릴 수 있는 앞자리 0을 오류로 처리해, inet_pton()만큼 엄격하게 읽습니다.
네트워크부와 호스트부
IP 주소는 두 부분으로 나뉩니다. 앞의 네트워크부(network part)는 장치가 속한 네트워크를, 뒤의 호스트부(host part)는 그 네트워크 안의 장치를 가리킵니다. 어디까지가 네트워크부인지는 주소만 보고는 알 수 없고, 앞에서부터 몇 비트가 네트워크부인지 알려 주는 프리픽스 길이(prefix length)가 함께 있어야 합니다. 192.168.1.100/26이라면 앞의 26비트가 네트워크부입니다. 같은 정보를 네트워크부의 비트를 모두 1로 채운 서브넷 마스크(subnet mask) 255.255.255.192로 나타내기도 합니다.
IPv4 PREFIX · SAME ADDRESS / DIFFERENT BOUNDARY
주소가 같아도 프리픽스가 다르면 네트워크 경계가 달라진다
192.168.1.100의 마지막 옥텟은 01100100입니다. /24는 마지막 8비트를 호스트부로 두고, /26은 그중 앞 2비트를 네트워크부로 가져갑니다.
/24 마스크는 255.255.255.0, /26은 255.255.255.192입니다. /26에서 01 뒤를 0으로 채운 01000000은 64이며, 호스트부 100100은 그 안의 오프셋 36입니다. 클래스나 첫 옥텟만으로 마스크를 추론하지 않습니다.
주소와 마스크를 비트마다 AND하면 호스트부가 0이 되어 네트워크 주소가 남습니다. 파이썬의 ipaddress 모듈로 이 계산을 해 봅니다.
import ipaddress
iface = ipaddress.ip_interface("192.168.1.100/26")
net = iface.network
print(f"마지막 바이트: {100:08b} AND {192:08b} = {100 & 192:08b}")
print(f"네트워크: {net}, 마스크: {net.netmask}, 브로드캐스트: {net.broadcast_address}")
print(f"호스트 범위: {net[1]}부터 {net[-2]}까지 {net.num_addresses - 2}개")
print(f"호스트부의 값: {int(iface.ip) - int(net.network_address)}")마지막 바이트: 01100100 AND 11000000 = 01000000
네트워크: 192.168.1.64/26, 마스크: 255.255.255.192, 브로드캐스트: 192.168.1.127
호스트 범위: 192.168.1.65부터 192.168.1.126까지 62개
호스트부의 값: 36마지막 바이트 100(01100100)과 마스크 192(11000000)를 AND하면 64가 남으므로, 이 주소의 네트워크는 192.168.1.64/26입니다. 호스트부는 32 - 26 = 6비트이므로 이 네트워크에는 주소가 2⁶ = 64개 있고, 호스트부가 모두 0인 첫 주소는 네트워크 자체를, 모두 1인 마지막 주소 .127은 그 네트워크 전체에 보내는 브로드캐스트를 가리키므로 장치에 줄 수 있는 주소는 62개입니다. 주소 .100은 네트워크의 시작 .64에서 36만큼 떨어진 호스트입니다.
사라진 클래스 체계
초기의 인터넷은 주소의 첫 비트들만 보고 네트워크부의 길이를 정하는 클래스(class) 체계를 썼습니다. 아래 표에서 각 줄의 0, 10, 110처럼 앞에서부터 처음 0이 나오는 자리가 클래스를 정하고, 클래스가 정해지면 네트워크부의 길이도 8, 16, 24비트로 함께 정해졌습니다. A 클래스의 범위가 0이 아니라 1에서 시작해 126에서 끝나는 것은, 0과 127로 시작하는 블록을 특별한 용도로 따로 남겨 두었기 때문입니다. 127은 뒤에서 볼 루프백 주소입니다.
A 클래스: 0xxxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx 네트워크 8비트, 호스트 24비트 1.0.0.0부터 126.255.255.255까지
B 클래스: 10xxxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx 네트워크 16비트, 호스트 16비트 128.0.0.0부터 191.255.255.255까지
C 클래스: 110xxxxx.xxxxxxxx.xxxxxxxx.xxxxxxxx 네트워크 24비트, 호스트 8비트 192.0.0.0부터 223.255.255.255까지
D 클래스: 1110xxxx... 멀티캐스트 224부터 239까지
E 클래스: 1111xxxx... 실험용 240부터 255까지이 체계에서는 기관에 A 클래스 하나를 주면 호스트 주소 1,600만여 개가, B 클래스 하나를 주면 6만 5천여 개가 한꺼번에 돌아갔습니다. 주소가 300개 필요한 회사는 C 클래스(254개)로는 모자라 B 클래스를 받아야 했고, 나머지 6만여 개는 쓰지도 못한 채 묶여 주소가 빠르게 바닥났습니다. 그래서 1993년부터 프리픽스 길이를 주소와 함께 적어 필요한 만큼만 나누어 주는 CIDR 방식으로 바뀌었고, 지금은 주소의 첫 비트로 네트워크부의 길이를 추측하지 않습니다. CIDR은 다음 글에서 다룹니다.
사설 주소와 공인 주소
공인 IP 주소(public IP address)는 전 세계에서 하나뿐이도록 관리되는 주소로, 인터넷에서 그 주소로 패킷을 보낼 수 있습니다. 사설 IP 주소(private IP address)는 RFC 1918이 조직 안에서 마음대로 쓰도록 정해 둔 주소로, 여러 조직이 같은 주소를 겹쳐 써도 되는 대신 인터넷으로는 직접 내보내지 않습니다.
| 사설 주소 블록 | 범위 | 주소 수 |
|---|---|---|
10.0.0.0/8 | 10.0.0.0부터 10.255.255.255까지 | 16,777,216 |
172.16.0.0/12 | 172.16.0.0부터 172.31.255.255까지 | 1,048,576 |
192.168.0.0/16 | 192.168.0.0부터 192.168.255.255까지 | 65,536 |
집의 공유기가 나누어 주는 192.168.0.x 같은 주소가 사설 주소이며, 사설 주소를 쓰는 장치가 인터넷에 나갈 때는 공유기가 4장 3편의 NAT로 출발지 주소를 공인 주소로 바꾸어 줍니다. 통신사가 가입자 여럿에게 공인 주소 하나를 나누어 쓰게 하는 CGNAT를 위해 RFC 6598이 따로 정한 100.64.0.0/10이라는 공유 주소 공간도 있습니다. ipaddress 모듈은 IANA의 특수 용도 주소 목록에 따라 주소가 사설인지, 전역에서 도달 가능한지 알려 줍니다.
import ipaddress
for text in ("192.168.1.100", "8.8.8.8", "100.64.0.1", "127.0.0.1", "169.254.1.1"):
ip = ipaddress.ip_address(text)
extra = ", ".join(n for n in ("loopback", "link_local") if getattr(ip, f"is_{n}"))
line = f"{text:<14} 사설 {ip.is_private}, 전역 {ip.is_global}"
print(line + (f" ({extra})" if extra else ""))192.168.1.100 사설 True, 전역 False
8.8.8.8 사설 False, 전역 True
100.64.0.1 사설 False, 전역 False
127.0.0.1 사설 True, 전역 False (loopback)
169.254.1.1 사설 True, 전역 False (link_local)is_private는 RFC 1918의 주소인지만 보는 것이 아니라, 전역에서 도달할 수 없는 주소이면 참이므로 루프백과 링크 로컬 주소도 참입니다. 100.64.0.0/10은 사설도 전역도 아닌 예외로, 모듈의 문서에도 그렇게 적혀 있습니다. 그러니 이 속성의 이름만 보고 RFC 1918의 사설 주소라고 단정하면 안 됩니다.
특수한 주소들
| 주소 | 뜻 |
|---|---|
0.0.0.0/0 | 모든 IPv4 주소와 일치하는 기본 경로. 더 구체적인 경로가 없을 때 쓴다 |
0.0.0.0 | 아직 주소가 정해지지 않았다는 표시. 서버가 이 주소로 바인딩하면 모든 인터페이스에서 받는다 |
127.0.0.0/8 | 루프백. 보낸 패킷이 밖으로 나가지 않고 자기 자신에게 돌아온다 |
169.254.0.0/16 | 링크 로컬. DHCP로 주소를 받지 못했을 때 스스로 정하는 주소 |
255.255.255.255 | 같은 링크의 모두에게 보내는 제한 브로드캐스트 |
192.0.2.0/24 등 | 문서와 예제에 쓰도록 정해 둔 주소(RFC 5737) |
컴퓨터의 주소가 169.254로 시작한다면, DHCP 서버에게서 주소를 받지 못해 스스로 정한 것이므로 DHCP 서버에 닿는지부터 확인해야 합니다. 문서용 주소 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24는 인터넷에서 쓰이지 않으므로, 예제를 실수로 실행해도 남의 서버로 패킷이 가지 않습니다.
ip addr show # Linux: 인터페이스의 IP 주소와 프리픽스 길이
ip route | grep default # Linux: 기본 게이트웨이
ipconfig # Windows: IP 주소, 서브넷 마스크, 기본 게이트웨이
curl -s https://ifconfig.me # 바깥에서 본 내 공인 주소(외부 서비스에 묻는다)ip addr show는 주소를 192.168.0.10/24처럼 프리픽스 길이와 함께 보여 주고, Windows의 ipconfig는 같은 정보를 255.255.255.0 같은 서브넷 마스크로 따로 보여 줍니다. 집에서 실행하면 대개 ip addr show나 ipconfig에는 사설 주소가 나오지만, 마지막 명령은 그와 다른 공인 주소를 돌려줍니다. 공유기가 NAT로 출발지 주소를 공인 주소로 바꾸어 내보내므로, 바깥의 서비스는 그 공인 주소만 보기 때문입니다.
IP 헤더와 TTL
IPv4 헤더의 필드는 2장 4편에서 보았습니다. 그 가운데 TTL(time to live)은 패킷이 네트워크를 끝없이 떠도는 것을 막는 칸입니다. 라우터는 패킷을 넘길 때마다 TTL을 1 줄이고, 0이 되면 패킷을 버린 뒤 보낸 쪽에 ICMP Time Exceeded 메시지를 돌려줍니다. 처음 값은 운영체제마다 달라 Linux는 보통 64, Windows는 128을 씁니다.
traceroute는 이 성질을 이용합니다. TTL을 1로 정한 패킷을 보내면 첫 라우터가 버리면서 Time Exceeded를 돌려주므로 첫 라우터의 주소를 알 수 있고, TTL을 2, 3으로 늘려 가며 보내면 경로의 라우터들을 차례로 알아낼 수 있습니다.
정리
- IPv4 주소는 32비트이며 점 십진 표기로 나타내고, 프리픽스 길이나 서브넷 마스크가 있어야 네트워크부와 호스트부를 나눌 수 있습니다.
- 주소와 마스크를 AND하면 네트워크 주소가 나오고, 호스트부가 모두 0인 주소와 모두 1인 주소는 장치에 주지 않습니다.
- 주소의 첫 비트로 크기를 정하던 클래스 체계는 주소를 낭비해, 프리픽스 길이를 함께 적는 CIDR로 바뀌었습니다.
- 사설 주소는 조직 안에서 겹쳐 써도 되는 주소이고, CGNAT용 공유 주소와 루프백, 링크 로컬, 문서용 주소 같은 특수 주소도 있습니다.
- 라우터는 TTL을 1씩 줄이고 0이 되면 패킷을 버리며,
traceroute는 이를 이용해 경로를 알아냅니다.
연습 문제
1. 같은 /26에 속할까
192.168.1.200/26의 네트워크 주소, 호스트부의 값, 전체 주소 범위를 구하세요. 이 주소와 192.168.1.100/26은 같은 네트워크에 속하나요? 두 주소의 프리픽스가 /24라면 어떤가요?
해설 보기
200은 이진수로 11001000이고, 마스크의 마지막 바이트 11000000과 AND하면 11000000, 곧 192가 남으므로 네트워크는 192.168.1.192/26입니다. 호스트부는 001000, 곧 8이고, 전체 범위는 .192부터 .255까지이며 장치에 줄 수 있는 주소는 .193부터 .254까지입니다. 192.168.1.100은 .64부터 .127까지의 블록에 있으므로 둘은 서로 다른 /26에 속합니다. 프리픽스가 /24라면 둘 다 192.168.1.0/24에 속하므로, 같은 네트워크인지는 주소만이 아니라 프리픽스 길이까지 알아야 판단할 수 있습니다.
2. 300개가 필요한 회사
주소가 300개 필요한 회사에 클래스 체계로 주소를 준다면 어느 클래스를 받아야 하고, 쓰지 못하는 주소는 몇 개인가요? 프리픽스 길이를 자유롭게 정할 수 있다면 어떤 크기의 블록 하나를 주면 되나요?
해설 보기
C 클래스는 장치에 줄 수 있는 주소가 254개뿐이라 모자라므로 B 클래스를 받아야 합니다. B 클래스의 호스트 주소는 65,534개이므로 300개를 쓰고 65,234개가 놀게 됩니다. 프리픽스 길이를 자유롭게 정할 수 있다면, 호스트부가 9비트인 /23 블록 하나가 주소 512개, 장치에 줄 수 있는 주소 510개를 담아 300개를 넉넉히 감당합니다. 낭비가 210개로 줄어들며, 이것이 CIDR이 주소를 아끼는 방법입니다.
3. 사설이 아니라고 했는데
ipaddress.ip_address("100.64.0.1").is_private가 False를 돌려주었습니다. 그렇다면 이 주소는 인터넷에서 누구나 닿을 수 있는 공인 주소인가요? 이 주소는 어디에서 쓰나요?
해설 보기
아닙니다. 100.64.0.0/10은 통신사가 CGNAT를 위해 가입자 쪽에서 쓰도록 RFC 6598이 정한 공유 주소 공간으로, 인터넷에서 전역으로 닿을 수 있는 주소가 아닙니다. 모듈은 이 블록을 사설과 전역의 두 분류 모두에서 뺀 예외로 다루므로 is_global도 False를 돌려줍니다. 스마트폰이나 일부 가정용 회선이 이런 주소를 받으며, 이때 가입자는 통신사의 NAT 뒤에 있으므로 바깥에서 그 장치로 직접 연결을 맺기 어렵습니다. is_private 하나만 보고 공인과 사설을 나누면 이런 주소를 잘못 분류하게 됩니다.
다음 글 서브넷과 CIDR에서는 큰 주소 블록을 필요한 크기로 나누는 서브넷팅과, 여러 블록을 하나로 묶어 경로를 줄이는 방법을 살펴봅니다.