ipaddress 모듈 입문
ipaddress 모듈 입문 (An Introduction to the ipaddress Module)
IP 주소와 네트워크를 다루는 ipaddress 모듈을 아주 부드럽게 소개하는 문서예요. 주된 대상은 IP 네트워킹 용어에 아직 익숙하지 않은 분들이지만, ipaddress가 IP 네트워킹 개념을 어떻게 표현하는지 한눈에 살펴보고 싶은 네트워크 엔지니어에게도 유용합니다.
출처: Python 공식 문서
Address/Network/Interface 객체 만들기
ipaddress는 IP 주소를 들여다보고 조작하기 위한 모듈이니, 가장 먼저 하고 싶은 일은 뭔가 객체를 만드는 거겠죠. 문자열이나 정수에서 객체를 만들 수 있어요.
IP 버전에 대한 한마디
IP 주소 지정에 특히 익숙하지 않은 독자라면 알아둬야 할 게 있어요. 인터넷 프로토콜(IP)은 지금 프로토콜 4번(IPv4)에서 6번(IPv6)으로 옮겨가는 과정 중이에요. 이 전환이 일어나는 큰 이유는, 4번 프로토콜이 인터넷에 직접 연결되는 기기가 점점 늘어나는 온 세상의 수요를 감당할 만큼 주소를 충분히 제공하지 못하기 때문입니다. 두 버전의 차이를 상세히 설명하는 건 이 입문 문서의 범위를 벗어나지만, 이 두 버전이 존재한다는 것과, 때로는 둘 중 하나를 강제로 써야 한다는 것만은 알아두셔야 해요.
IP 호스트 주소
IP 주소 지정을 다룰 때 가장 기본적인 단위는 주소인데, 흔히 '호스트 주소(host address)'라고 부르죠. 주소를 만드는 가장 간단한 방법은 ipaddress.ip_address() 팩토리 함수를 쓰는 건데, 이 함수는 넘겨받은 값에 따라 IPv4 주소를 만들지 IPv6 주소를 만들지 자동으로 결정합니다:
>>> ipaddress.ip_address('192.0.2.1')
IPv4Address('192.0.2.1')
>>> ipaddress.ip_address('2001:DB8::1')
IPv6Address('2001:db8::1')
주소는 정수에서 직접 만들 수도 있어요. 32비트 안에 들어가는 값은 IPv4 주소로 간주됩니다:
>>> ipaddress.ip_address(3221225985)
IPv4Address('192.0.2.1')
>>> ipaddress.ip_address(42540766411282592856903984951653826561)
IPv6Address('2001:db8::1')
IPv4 또는 IPv6를 강제로 쓰려면 해당 클래스를 직접 호출하면 돼요. 특히 작은 정수로 IPv6 주소를 강제로 만들 때 유용합니다:
>>> ipaddress.ip_address(1)
IPv4Address('0.0.0.1')
>>> ipaddress.IPv4Address(1)
IPv4Address('0.0.0.1')
>>> ipaddress.IPv6Address(1)
IPv6Address('::1')
네트워크 정의하기
호스트 주소는 보통 IP 네트워크로 묶여 다니기 때문에, ipaddress는 네트워크 정의를 만들고, 들여다보고, 조작하는 방법도 제공합니다. IP 네트워크 객체는 그 네트워크에 속하는 호스트 주소의 범위를 정의하는 문자열로부터 만들어져요. 정보를 표현하는 가장 단순한 형태는 "네트워크 주소/네트워크 프리픽스" 쌍입니다. 여기서 프리픽스는 주소가 네트워크에 속하는지 판단하기 위해 비교할 앞쪽 비트의 개수, 네트워크 주소는 그 비트들이 가져야 할 예상 값을 정의해요.
주소 때와 마찬가지로 올바른 IP 버전을 자동으로 정해주는 팩토리 함수가 제공됩니다:
>>> ipaddress.ip_network('192.0.2.0/24')
IPv4Network('192.0.2.0/24')
>>> ipaddress.ip_network('2001:db8::0/96')
IPv6Network('2001:db8::/96')
네트워크 객체에는 어떤 호스트 비트도 설정될 수 없어요. 따라서 실제로는 192.0.2.1/24 같은 건 네트워크를 나타내지 못합니다. 이런 정의는 'ip-on-a-network' 표기법이 주어진 네트워크 위의 컴퓨터 네트워크 인터페이스를 기술할 때 흔히 쓰이기 때문에 **인터페이스 객체(interface object)**라고 부르는데, 다음 절에서 더 자세히 다룰게요.
기본적으로 호스트 비트가 설정된 상태에서 네트워크 객체를 만들려 하면 ValueError가 발생합니다. 대신 그 추가 비트들을 0으로 강제하고 싶다면 생성자에 strict=False 플래그를 넘기면 돼요:
>>> ipaddress.ip_network('192.0.2.1/24')
Traceback (most recent call last):
...
ValueError: 192.0.2.1/24 has host bits set
>>> ipaddress.ip_network('192.0.2.1/24', strict=False)
IPv4Network('192.0.2.0/24')
문자열 형태가 훨씬 유연하지만, 호스트 주소처럼 네트워크도 정수로 정의할 수 있어요. 이 경우 네트워크는 그 정수가 나타내는 단 하나의 주소만 포함하는 것으로 간주되므로, 네트워크 프리픽스는 네트워크 주소 전체를 포함합니다:
>>> ipaddress.ip_network(3221225984)
IPv4Network('192.0.2.0/32')
>>> ipaddress.ip_network(42540766411282592856903984951653826560)
IPv6Network('2001:db8::/128')
주소 때와 마찬가지로, 팩토리 함수 대신 특정 종류의 네트워크 클래스 생성자를 직접 호출하면 그 종류를 강제할 수 있어요.
호스트 인터페이스
바로 위에서 말했듯, 특정 네트워크 위의 주소 하나를 기술하려면 주소 클래스만으로도, 네트워크 클래스만으로도 충분하지 않아요. 192.0.2.1/24 같은 표기법은 네트워크 엔지니어나 방화벽·라우터용 도구를 만드는 사람들이 "네트워크 192.0.2.0/24 위의 호스트 192.0.2.1"을 줄여 말할 때 흔히 씁니다. 그래서 ipaddress는 주소를 특정 네트워크와 연결해 주는 하이브리드 클래스 집합을 제공해요. 생성 인터페이스는 네트워크 객체를 정의할 때와 동일한데, 단지 주소 부분이 네트워크 주소일 필요가 없다는 점만 다릅니다.
>>> ipaddress.ip_interface('192.0.2.1/24')
IPv4Interface('192.0.2.1/24')
>>> ipaddress.ip_interface('2001:db8::1/96')
IPv6Interface('2001:db8::1/96')
(네트워크 때처럼) 정수 입력도 받아들이고, 관련 생성자를 직접 호출하면 특정 IP 버전을 강제할 수도 있어요.
Address/Network/Interface 객체 들여다보기
애써 IPv(4|6)(Address|Network|Interface) 객체를 만들었으니, 이제 그 정보를 얻고 싶겠죠. ipaddress는 이걸 쉽고 직관적으로 만들려고 해요.
IP 버전 추출:
>>> addr4 = ipaddress.ip_address('192.0.2.1')
>>> addr6 = ipaddress.ip_address('2001:db8::1')
>>> addr6.version
6
>>> addr4.version
4
인터페이스에서 네트워크 얻기:
>>> host4 = ipaddress.ip_interface('192.0.2.1/24')
>>> host4.network
IPv4Network('192.0.2.0/24')
>>> host6 = ipaddress.ip_interface('2001:db8::1/96')
>>> host6.network
IPv6Network('2001:db8::/96')
네트워크에 개별 주소가 몇 개나 들어 있는지 알아내기:
>>> net4 = ipaddress.ip_network('192.0.2.0/24')
>>> net4.num_addresses
256
>>> net6 = ipaddress.ip_network('2001:db8::0/96')
>>> net6.num_addresses
4294967296
네트워크의 "사용 가능한" 주소들을 순회하기:
>>> net4 = ipaddress.ip_network('192.0.2.0/24')
>>> for x in net4.hosts():
... print(x)
192.0.2.1
192.0.2.2
192.0.2.3
192.0.2.4
...
192.0.2.252
192.0.2.253
192.0.2.254
넷마스크(네트워크 프리픽스에 해당하는 설정 비트)나 호스트마스크(넷마스크에 속하지 않는 모든 비트) 얻기:
>>> net4 = ipaddress.ip_network('192.0.2.0/24')
>>> net4.netmask
IPv4Address('255.255.255.0')
>>> net4.hostmask
IPv4Address('0.0.0.255')
>>> net6 = ipaddress.ip_network('2001:db8::0/96')
>>> net6.netmask
IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff::')
>>> net6.hostmask
IPv6Address('::ffff:ffff')
주소를 '펼치기(explode)' 또는 '압축하기(compress)':
>>> addr6.exploded
'2001:0db8:0000:0000:0000:0000:0000:0001'
>>> addr6.compressed
'2001:db8::1'
>>> net6.exploded
'2001:0db8:0000:0000:0000:0000:0000:0000/96'
>>> net6.compressed
'2001:db8::/96'
IPv4는 펼치기/압축을 지원하지 않지만, 연관된 객체들은 여전히 관련 속성을 제공합니다. 덕분에 버전에 무관한 코드가 IPv4를 올바르게 처리하면서도 IPv6 주소에는 가장 간결하거나 가장 장황한 형태를 쉽게 쓸 수 있어요.
네트워크를 주소의 리스트처럼
네트워크를 리스트처럼 다루면 유용할 때가 있어요. 그렇게 하면 인덱싱이 가능해집니다:
>>> net4[1]
IPv4Address('192.0.2.1')
>>> net4[-1]
IPv4Address('192.0.2.255')
>>> net6[1]
IPv6Address('2001:db8::1')
>>> net6[-1]
IPv6Address('2001:db8::ffff:ffff')
또한 네트워크 객체는 이런 리스트 멤버십 테스트 문법을 쓰기에 알맞아요:
if address in network:
# do something
포함 여부 검사는 네트워크 프리픽스를 기준으로 효율적으로 수행됩니다:
>>> addr4 = ipaddress.ip_address('192.0.2.1')
>>> addr4 in ipaddress.ip_network('192.0.2.0/24')
True
>>> addr4 in ipaddress.ip_network('192.0.3.0/24')
False
비교 (Comparisons)
ipaddress는 말이 되는 범위에서 객체들을 비교하는 간단하고 직관적인 방법을 제공합니다:
>>> ipaddress.ip_address('192.0.2.1') < ipaddress.ip_address('192.0.2.2')
True
서로 다른 버전이나 서로 다른 타입의 객체를 비교하려 하면 TypeError 예외가 발생해요.
다른 모듈과 함께 IP 주소 사용하기
IP 주소를 쓰는 다른 모듈(socket 같은)은 보통 이 모듈의 객체를 직접 받아들이지 않습니다. 대신 그 모듈이 받아들일 정수나 문자열로 강제 변환(코어스)해서 넘겨야 해요:
>>> addr4 = ipaddress.ip_address('192.0.2.1')
>>> str(addr4)
'192.0.2.1'
>>> int(addr4)
3221225985
인스턴스 생성 실패 시 더 자세한 정보 얻기
버전에 무관한 팩토리 함수를 이용해 주소/네트워크/인터페이스 객체를 만들 때, 오류는 전부 ValueError로 보고되는데 메시지도 "넘겨진 값이 해당 타입의 객체로 인식되지 않았다"는 식의 일반적인 내용뿐이에요. 구체적인 오류가 없는 이유는, 값이 왜 거부됐는지 자세히 설명하려면 그 값이 IPv4여야 하는지 IPv6여야 하는지를 알아야 하기 때문입니다.
이런 추가 세부 정보가 필요한 사용 사례를 지원하기 위해, 개별 클래스 생성자는 실제로 ValueError의 서브클래스인 ipaddress.AddressValueError와 ipaddress.NetmaskValueError를 발생시켜 정의의 어느 부분이 정확히 파싱에 실패했는지 알려줍니다.
클래스 생성자를 직접 쓰면 오류 메시지도 훨씬 상세해져요. 예:
>>> ipaddress.ip_address("192.168.0.256")
Traceback (most recent call last):
...
ValueError: '192.168.0.256' does not appear to be an IPv4 or IPv6 address
>>> ipaddress.IPv4Address("192.168.0.256")
Traceback (most recent call last):
...
ipaddress.AddressValueError: Octet 256 (> 255) not permitted in '192.168.0.256'
>>> ipaddress.ip_network("192.168.0.1/64")
Traceback (most recent call last):
...
ValueError: '192.168.0.1/64' does not appear to be an IPv4 or IPv6 network
>>> ipaddress.IPv4Network("192.168.0.1/64")
Traceback (most recent call last):
...
ipaddress.NetmaskValueError: '64' is not a valid netmask
다만 이 모듈 전용 예외 둘 다 부모 클래스가 ValueError이기 때문에, 특정 오류 타입에 신경 쓰지 않는다면 다음과 같은 코드를 그대로 쓸 수 있어요:
try:
network = ipaddress.IPv4Network(address)
except ValueError:
print('address/netmask is invalid for IPv4:', address)
더 알아보기 (Learn more)
ipaddress라이브러리 문서 — 클래스와 메서드 전체 레퍼런스- Python 공식 문서: ipaddress 모듈 입문