Skip to content

Commit 1f4e711

Browse files
authored
Add ipinterface type to fieldtypes.net (#160)
1 parent 380ff1c commit 1f4e711

File tree

5 files changed

+147
-8
lines changed

5 files changed

+147
-8
lines changed

flow/record/fieldtypes/net/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
from __future__ import annotations
22

33
from flow.record.fieldtypes import string
4-
from flow.record.fieldtypes.net.ip import IPAddress, IPNetwork, ipaddress, ipnetwork
4+
from flow.record.fieldtypes.net.ip import (
5+
IPAddress,
6+
IPNetwork,
7+
ipaddress,
8+
ipinterface,
9+
ipnetwork,
10+
)
511

612
__all__ = [
713
"IPAddress",
814
"IPNetwork",
915
"ipaddress",
16+
"ipinterface",
1017
"ipnetwork",
1118
]
1219

flow/record/fieldtypes/net/ip.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
from ipaddress import (
44
IPv4Address,
5+
IPv4Interface,
56
IPv4Network,
67
IPv6Address,
8+
IPv6Interface,
79
IPv6Network,
810
ip_address,
11+
ip_interface,
912
ip_network,
1013
)
1114
from typing import Union
@@ -15,16 +18,19 @@
1518

1619
_IPNetwork = Union[IPv4Network, IPv6Network]
1720
_IPAddress = Union[IPv4Address, IPv6Address]
21+
_IPInterface = Union[IPv4Interface, IPv6Interface]
22+
_ConversionTypes = Union[str, int, bytes]
23+
_IPTypes = Union[_IPNetwork, _IPAddress, _IPInterface]
1824

1925

2026
class ipaddress(FieldType):
21-
val = None
27+
val: _IPAddress = None
2228
_type = "net.ipaddress"
2329

24-
def __init__(self, addr: str | int | bytes):
30+
def __init__(self, addr: _ConversionTypes | _IPAddress):
2531
self.val = ip_address(addr)
2632

27-
def __eq__(self, b: str | int | bytes | _IPAddress) -> bool:
33+
def __eq__(self, b: _ConversionTypes | _IPAddress) -> bool:
2834
try:
2935
return self.val == ip_address(b)
3036
except ValueError:
@@ -53,13 +59,13 @@ def _unpack(data: int) -> ipaddress:
5359

5460

5561
class ipnetwork(FieldType):
56-
val = None
62+
val: _IPNetwork = None
5763
_type = "net.ipnetwork"
5864

59-
def __init__(self, addr: str | int | bytes):
65+
def __init__(self, addr: _ConversionTypes | _IPNetwork):
6066
self.val = ip_network(addr)
6167

62-
def __eq__(self, b: str | int | bytes | _IPNetwork) -> bool:
68+
def __eq__(self, b: _ConversionTypes | _IPNetwork) -> bool:
6369
try:
6470
return self.val == ip_network(b)
6571
except ValueError:
@@ -98,6 +104,52 @@ def _pack(self) -> str:
98104
def _unpack(data: str) -> ipnetwork:
99105
return ipnetwork(data)
100106

107+
@property
108+
def netmask(self) -> ipaddress:
109+
return ipaddress(self.val.netmask)
110+
111+
112+
class ipinterface(FieldType):
113+
val: _IPInterface = None
114+
_type = "net.ipinterface"
115+
116+
def __init__(self, addr: _ConversionTypes | _IPTypes) -> None:
117+
self.val = ip_interface(addr)
118+
119+
def __eq__(self, b: _ConversionTypes | _IPTypes) -> bool:
120+
try:
121+
return self.val == ip_interface(b)
122+
except ValueError:
123+
return False
124+
125+
def __hash__(self) -> int:
126+
return hash(self.val)
127+
128+
def __str__(self) -> str:
129+
return str(self.val)
130+
131+
def __repr__(self) -> str:
132+
return f"{self._type}({str(self)!r})"
133+
134+
@property
135+
def ip(self) -> ipaddress:
136+
return ipaddress(self.val.ip)
137+
138+
@property
139+
def network(self) -> ipnetwork:
140+
return ipnetwork(self.val.network)
141+
142+
@property
143+
def netmask(self) -> ipaddress:
144+
return ipaddress(self.val.netmask)
145+
146+
def _pack(self) -> str:
147+
return self.val.compressed
148+
149+
@staticmethod
150+
def _unpack(data: str) -> ipinterface:
151+
return ipinterface(data)
152+
101153

102154
# alias: net.IPAddress -> net.ipaddress
103155
# alias: net.IPNetwork -> net.ipnetwork

flow/record/jsonpacker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def pack_obj(self, obj: Any) -> dict | str:
6868
"sha1": obj.sha1,
6969
"sha256": obj.sha256,
7070
}
71-
if isinstance(obj, (fieldtypes.net.ipaddress, fieldtypes.net.ipnetwork)):
71+
if isinstance(obj, (fieldtypes.net.ipaddress, fieldtypes.net.ipnetwork, fieldtypes.net.ipinterface)):
7272
return str(obj)
7373
if isinstance(obj, bytes):
7474
return base64.b64encode(obj).decode()

flow/record/whitelist.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"bytes",
2525
"record",
2626
"net.ipaddress",
27+
"net.ipinterface",
2728
"net.ipnetwork",
2829
"net.IPAddress",
2930
"net.IPNetwork",

tests/test_fieldtype_ip.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,85 @@ def test_record_ipnetwork() -> None:
128128
assert "::1" not in data
129129

130130

131+
def test_record_ipinterface() -> None:
132+
TestRecord = RecordDescriptor(
133+
"test/ipinterface",
134+
[
135+
("net.ipinterface", "interface"),
136+
],
137+
)
138+
139+
# ipv4
140+
r = TestRecord("192.168.0.0/24")
141+
assert r.interface == "192.168.0.0/24"
142+
assert "bad.ip" not in r.interface.network
143+
assert "192.168.0.1" in r.interface.network
144+
assert isinstance(r.interface, net.ipinterface)
145+
assert repr(r.interface) == "net.ipinterface('192.168.0.0/24')"
146+
assert hash(r.interface) == hash(net.ipinterface("192.168.0.0/24"))
147+
148+
r = TestRecord("192.168.1.1")
149+
assert r.interface.ip == "192.168.1.1"
150+
assert r.interface.network == "192.168.1.1/32"
151+
assert r.interface == "192.168.1.1/32"
152+
assert r.interface.netmask == "255.255.255.255"
153+
154+
r = TestRecord("192.168.1.24/255.255.255.0")
155+
assert r.interface == "192.168.1.24/24"
156+
assert r.interface.ip == "192.168.1.24"
157+
assert r.interface.network == "192.168.1.0/24"
158+
assert r.interface.netmask == "255.255.255.0"
159+
160+
# ipv6 - https://en.wikipedia.org/wiki/IPv6_address
161+
r = TestRecord("::1")
162+
assert r.interface == "::1"
163+
assert r.interface == "::1/128"
164+
165+
r = TestRecord("64:ff9b::2/96")
166+
assert r.interface == "64:ff9b::2/96"
167+
assert r.interface.ip == "64:ff9b::2"
168+
assert r.interface.network == "64:ff9b::/96"
169+
assert r.interface.netmask == "ffff:ffff:ffff:ffff:ffff:ffff::"
170+
171+
# instantiate from different types
172+
assert TestRecord(1).interface == "0.0.0.1/32"
173+
assert TestRecord(0x7F0000FF).interface == "127.0.0.255/32"
174+
assert TestRecord(b"\x7f\xff\xff\xff").interface == "127.255.255.255/32"
175+
176+
# Test whether it functions in a set
177+
data = {TestRecord(x).interface for x in ["192.168.0.0/24", "192.168.0.0/24", "::1", "::1"]}
178+
assert len(data) == 2
179+
assert net.ipinterface("::1") in data
180+
assert net.ipinterface("192.168.0.0/24") in data
181+
assert "::1" not in data
182+
183+
184+
def test_record_ipinterface_types() -> None:
185+
TestRecord = RecordDescriptor(
186+
"test/ipinterface",
187+
[
188+
(
189+
"net.ipinterface",
190+
"interface",
191+
)
192+
],
193+
)
194+
195+
r = TestRecord("192.168.0.255/24")
196+
_if = r.interface
197+
assert isinstance(_if, net.ipinterface)
198+
assert isinstance(_if.ip, net.ipaddress)
199+
assert isinstance(_if.network, net.ipnetwork)
200+
assert isinstance(_if.netmask, net.ipaddress)
201+
202+
r = TestRecord("64:ff9b::/96")
203+
_if = r.interface
204+
assert isinstance(_if, net.ipinterface)
205+
assert isinstance(_if.ip, net.ipaddress)
206+
assert isinstance(_if.network, net.ipnetwork)
207+
assert isinstance(_if.netmask, net.ipaddress)
208+
209+
131210
@pytest.mark.parametrize("PSelector", [Selector, CompiledSelector])
132211
def test_selector_ipaddress(PSelector: type[Selector]) -> None:
133212
TestRecord = RecordDescriptor(

0 commit comments

Comments
 (0)