flux-networking-shared 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. flux_networking_shared-0.2.2/.gitignore +47 -0
  2. flux_networking_shared-0.2.2/PKG-INFO +35 -0
  3. flux_networking_shared-0.2.2/README.md +18 -0
  4. flux_networking_shared-0.2.2/dummy_packages/probert/probert/__init__.py +3 -0
  5. flux_networking_shared-0.2.2/dummy_packages/probert/probert/network.py +43 -0
  6. flux_networking_shared-0.2.2/dummy_packages/probert/setup.py +9 -0
  7. flux_networking_shared-0.2.2/pyproject.toml +50 -0
  8. flux_networking_shared-0.2.2/src/flux_networking_shared/__init__.py +54 -0
  9. flux_networking_shared-0.2.2/src/flux_networking_shared/async_ping.py +240 -0
  10. flux_networking_shared-0.2.2/src/flux_networking_shared/helpers.py +207 -0
  11. flux_networking_shared-0.2.2/src/flux_networking_shared/log.py +4 -0
  12. flux_networking_shared-0.2.2/src/flux_networking_shared/models/__init__.py +13 -0
  13. flux_networking_shared-0.2.2/src/flux_networking_shared/models/dns.py +108 -0
  14. flux_networking_shared-0.2.2/src/flux_networking_shared/models/network.py +403 -0
  15. flux_networking_shared-0.2.2/src/flux_networking_shared/network_observer.py +214 -0
  16. flux_networking_shared-0.2.2/src/flux_networking_shared/systemd_parser.py +24 -0
  17. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/__init__.py +80 -0
  18. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/css/network_screen.tcss +55 -0
  19. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/messages/__init__.py +42 -0
  20. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/models/__init__.py +31 -0
  21. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/models/interface_config_result.py +39 -0
  22. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/models/network.py +138 -0
  23. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/models/upnp.py +103 -0
  24. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/screens/__init__.py +13 -0
  25. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/screens/confirm_exit.py +67 -0
  26. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/screens/interface_modal_screen.py +417 -0
  27. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/screens/network_screen.py +719 -0
  28. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/validators/__init__.py +13 -0
  29. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/validators/network_validator.py +200 -0
  30. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/__init__.py +44 -0
  31. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/address_builder.py +233 -0
  32. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/connectivity.py +283 -0
  33. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/dns.py +86 -0
  34. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/dns_summary.py +91 -0
  35. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/field_set.py +62 -0
  36. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/focus_label.py +41 -0
  37. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/interface.py +270 -0
  38. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/interface_group.py +293 -0
  39. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/label_switch.py +73 -0
  40. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/labels.py +45 -0
  41. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/loading.py +189 -0
  42. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/name_resolution.py +173 -0
  43. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/network_overview.py +198 -0
  44. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/network_summary.py +94 -0
  45. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/route_summary.py +87 -0
  46. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/routing.py +78 -0
  47. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/upnp_action_bar.py +184 -0
  48. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/upnp_builder.py +691 -0
  49. flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/validate_blur_input.py +27 -0
  50. flux_networking_shared-0.2.2/src/flux_networking_shared/upnp_querier.py +300 -0
  51. flux_networking_shared-0.2.2/uv.lock +724 -0
@@ -0,0 +1,47 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # Virtual environments
28
+ .venv/
29
+ venv/
30
+ ENV/
31
+
32
+ # IDE
33
+ .idea/
34
+ .vscode/
35
+ *.swp
36
+ *.swo
37
+
38
+ # Ruff
39
+ .ruff_cache/
40
+
41
+ # Testing
42
+ .coverage
43
+ .pytest_cache/
44
+ htmlcov/
45
+
46
+ # OS
47
+ .DS_Store
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: flux-networking-shared
3
+ Version: 0.2.2
4
+ Summary: Shared networking utilities for Flux daemon and TUI
5
+ Author-email: David White <david@runonflux.io>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: aiofiles<26,>=25.1.0
8
+ Requires-Dist: pyyaml<7,>=6.0.3
9
+ Requires-Dist: textual<7,>=6.11.0
10
+ Requires-Dist: yarl<2,>=1.22.0
11
+ Provides-Extra: backend
12
+ Requires-Dist: aiohttp<4,>=3.13.2; extra == 'backend'
13
+ Requires-Dist: miniupnpc<3,>=2.3.3; extra == 'backend'
14
+ Requires-Dist: probert>=0.0.18; extra == 'backend'
15
+ Requires-Dist: pyroute2<1,>=0.9.2; extra == 'backend'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # flux-networking-shared
19
+
20
+ Shared networking utilities for Flux daemon and TUI.
21
+
22
+ This package contains platform-independent networking code that can be used by both the flux-configd daemon and the flux_iso_networking TUI package.
23
+
24
+ ## Components
25
+
26
+ - `UpnpQuerier` - UPnP port mapping discovery
27
+ - `NetworkObserver` - Network interface change observation (via probert)
28
+ - `IcmpPacketSender` - Raw ICMP ping utilities
29
+ - `SystemdConfigParser` - Parse systemd network configurations
30
+ - Network models (Route, NetworkInterface, FluxShapingPolicy, etc.)
31
+
32
+ ## Platform Support
33
+
34
+ - **Linux**: Full functionality with real probert dependency
35
+ - **macOS**: Development support with stub probert implementation
@@ -0,0 +1,18 @@
1
+ # flux-networking-shared
2
+
3
+ Shared networking utilities for Flux daemon and TUI.
4
+
5
+ This package contains platform-independent networking code that can be used by both the flux-configd daemon and the flux_iso_networking TUI package.
6
+
7
+ ## Components
8
+
9
+ - `UpnpQuerier` - UPnP port mapping discovery
10
+ - `NetworkObserver` - Network interface change observation (via probert)
11
+ - `IcmpPacketSender` - Raw ICMP ping utilities
12
+ - `SystemdConfigParser` - Parse systemd network configurations
13
+ - Network models (Route, NetworkInterface, FluxShapingPolicy, etc.)
14
+
15
+ ## Platform Support
16
+
17
+ - **Linux**: Full functionality with real probert dependency
18
+ - **macOS**: Development support with stub probert implementation
@@ -0,0 +1,3 @@
1
+ # Dummy probert module for macOS development
2
+ # This allows flux_networking_shared to be installed on macOS
3
+ # without the actual probert dependency which is Linux-only
@@ -0,0 +1,43 @@
1
+ # Dummy probert.network module for macOS development
2
+ # Provides stub classes that network_observer.py imports
3
+
4
+
5
+ class Link:
6
+ """Stub for probert Link class."""
7
+
8
+ def __init__(self):
9
+ self.type = ""
10
+ self.name = ""
11
+ self.flags = 0
12
+
13
+ def serialize(self) -> dict:
14
+ return {}
15
+
16
+
17
+ class NetworkEventReceiver:
18
+ """Stub for probert NetworkEventReceiver class."""
19
+
20
+ def new_link(self, ifindex: int, link: Link):
21
+ pass
22
+
23
+ def update_link(self, ifindex: int):
24
+ pass
25
+
26
+ def del_link(self, ifindex: int):
27
+ pass
28
+
29
+ def route_change(self, action: str, data):
30
+ pass
31
+
32
+
33
+ class UdevObserver:
34
+ """Stub for probert UdevObserver class."""
35
+
36
+ def __init__(self, receiver: NetworkEventReceiver):
37
+ self.receiver = receiver
38
+
39
+ def start(self) -> list[int]:
40
+ return []
41
+
42
+ def data_ready(self, fd: int):
43
+ pass
@@ -0,0 +1,9 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="probert",
5
+ version="0.0.18",
6
+ description="Dummy probert package for macOS development",
7
+ packages=find_packages(),
8
+ py_modules=["probert"],
9
+ )
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "flux-networking-shared"
3
+ version = "0.2.2"
4
+ description = "Shared networking utilities for Flux daemon and TUI"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "David White", email = "david@runonflux.io" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+
11
+ dependencies = [
12
+ "aiofiles>=25.1.0,<26",
13
+ "pyyaml>=6.0.3,<7",
14
+ "textual>=6.11.0,<7",
15
+ "yarl>=1.22.0,<2",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ backend = [
20
+ "aiohttp>=3.13.2,<4",
21
+ "miniupnpc>=2.3.3,<3",
22
+ "probert>=0.0.18",
23
+ "pyroute2>=0.9.2,<1",
24
+ ]
25
+
26
+ [tool.uv]
27
+ # This is to override probert. python 3.12 moved the Container endpoint
28
+ override-dependencies = ["pyudev==0.24.3"]
29
+
30
+ # Limit resolution to supported platforms to avoid probert resolution failures
31
+ environments = [
32
+ "sys_platform == 'darwin'",
33
+ "sys_platform == 'linux'"
34
+ ]
35
+
36
+ [tool.uv.sources]
37
+ # Platform-specific probert: dummy on macOS, real on Linux
38
+ probert = [
39
+ { path = "dummy_packages/probert", marker = "sys_platform == 'darwin'" },
40
+ { git = "https://github.com/canonical/probert.git", branch = "server/jammy", marker = "sys_platform == 'linux'" }
41
+ ]
42
+
43
+ [build-system]
44
+ requires = ["hatchling"]
45
+ build-backend = "hatchling.build"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "ruff>=0.11.6",
50
+ ]
@@ -0,0 +1,54 @@
1
+ """Shared networking utilities for Flux daemon and TUI."""
2
+
3
+ import asyncio
4
+
5
+ from flux_networking_shared.upnp_querier import UpnpQuerier, UpnpLease, FluxPortMap
6
+ from flux_networking_shared.helpers import do_http, exec_binary, ExecBinaryError
7
+ from flux_networking_shared.async_ping import IcmpPacketSender, IcmpPingResponse
8
+ from flux_networking_shared.systemd_parser import SystemdConfigParser
9
+ from flux_networking_shared.models import (
10
+ Route,
11
+ SourceAddress,
12
+ NetworkInterface,
13
+ NetworkInterfaceGroup,
14
+ InterfaceState,
15
+ InterfaceShapingPolicy,
16
+ FluxShapingPolicy,
17
+ ResolvedDnsServer,
18
+ ResolvedHostname,
19
+ )
20
+
21
+
22
+ def clean_upnp() -> list[int]:
23
+ """Remove all UPnP port mappings created by this machine.
24
+
25
+ Returns:
26
+ List of removed port numbers
27
+ """
28
+ async def main() -> list[int]:
29
+ querier = UpnpQuerier()
30
+ return await querier.remove_all_mappings()
31
+
32
+ return asyncio.run(main())
33
+
34
+ __all__ = [
35
+ "UpnpQuerier",
36
+ "UpnpLease",
37
+ "FluxPortMap",
38
+ "do_http",
39
+ "exec_binary",
40
+ "ExecBinaryError",
41
+ "IcmpPacketSender",
42
+ "IcmpPingResponse",
43
+ "SystemdConfigParser",
44
+ "Route",
45
+ "SourceAddress",
46
+ "NetworkInterface",
47
+ "NetworkInterfaceGroup",
48
+ "InterfaceState",
49
+ "InterfaceShapingPolicy",
50
+ "FluxShapingPolicy",
51
+ "ResolvedDnsServer",
52
+ "ResolvedHostname",
53
+ "clean_upnp",
54
+ ]
@@ -0,0 +1,240 @@
1
+ import asyncio
2
+ from socket import (
3
+ socket,
4
+ IPPROTO_ICMP,
5
+ IPPROTO_IP,
6
+ SOCK_RAW,
7
+ AF_INET,
8
+ IP_TTL,
9
+ SOL_SOCKET,
10
+ )
11
+ import struct
12
+ from functools import partial
13
+ from random import randint
14
+ from time import perf_counter
15
+ from dataclasses import dataclass
16
+
17
+ SO_BINDTODEVICE = 25
18
+
19
+ # https://datatracker.ietf.org/doc/html/rfc792
20
+ ICMP_ECHO_REPLY = 0
21
+ ICMP_DEST_UNREACHABLE = 3
22
+ ICMP_ECHO_REQUEST = 8
23
+ ICMP_TTL_EXCEEDED = 11
24
+
25
+ ICMP_RESPONSES = [ICMP_ECHO_REPLY, ICMP_DEST_UNREACHABLE, ICMP_TTL_EXCEEDED]
26
+
27
+
28
+ @dataclass
29
+ class IcmpPingResponse:
30
+ rtt: float | None = None
31
+ message: str | None = None
32
+
33
+ @property
34
+ def rtt_ms(self) -> str:
35
+ return f"{round(self.rtt, 3):.3f} ms" if self.rtt else ""
36
+
37
+ def __bool__(self) -> bool:
38
+ return bool(self.rtt and not self.message)
39
+
40
+
41
+ class IcmpPacketSender:
42
+ def __init__(self) -> None:
43
+ self.loop = asyncio.get_running_loop()
44
+
45
+ @staticmethod
46
+ def calculate_checksum(data: bytes) -> int:
47
+ checksum = 0
48
+
49
+ if len(data) % 2:
50
+ data += b"\x00"
51
+
52
+ for i in range(0, len(data), 2):
53
+ checksum += (data[i] << 8) + data[i + 1]
54
+
55
+ checksum = (checksum >> 16) + (checksum & 0xFFFF)
56
+ checksum += checksum >> 16
57
+
58
+ return (~checksum) & 0xFFFF
59
+
60
+ @staticmethod
61
+ def generate_header(id: int, *, checksum: int | None = None) -> bytes:
62
+ icmp_type = ICMP_ECHO_REQUEST
63
+ icmp_code = 0
64
+ icmp_checksum = checksum or 0
65
+ icmp_sequence = 1
66
+
67
+ icmp_header = struct.pack(
68
+ "!BBHHH",
69
+ icmp_type,
70
+ icmp_code,
71
+ icmp_checksum,
72
+ id,
73
+ icmp_sequence,
74
+ )
75
+
76
+ return icmp_header
77
+
78
+ async def receive_ping(
79
+ self, raw_socket: socket, id: int, timeout: float
80
+ ) -> IcmpPingResponse:
81
+ try:
82
+ while True:
83
+ received = await asyncio.wait_for(
84
+ self.loop.sock_recv(raw_socket, 1500), timeout
85
+ )
86
+
87
+ time_received = perf_counter()
88
+
89
+ offset = 20
90
+ icmp_header_size = 8
91
+
92
+ icmp_header = received[offset : offset + icmp_header_size]
93
+
94
+ # On ttl and dest unreachable, only the type, code, and checksum
95
+ # will be present. The packet_id and sequence are zero. Then
96
+ # follows the ip header, then the original header + data
97
+
98
+ try:
99
+ type, code, checksum, packet_id, sequence = struct.unpack(
100
+ "!BBHHH", icmp_header
101
+ )
102
+ except (struct.error, TypeError):
103
+ continue
104
+
105
+ msg = None
106
+ rtt = None
107
+
108
+ if type == ICMP_ECHO_REPLY:
109
+ if packet_id != id:
110
+ continue
111
+
112
+ data = received[
113
+ offset + 8 : offset + 8 + struct.calcsize("d")
114
+ ]
115
+ time_sent = struct.unpack("d", data)[0]
116
+
117
+ rtt = (time_received - time_sent) * 1000
118
+ elif type == ICMP_TTL_EXCEEDED:
119
+ msg = "TTL Exceeded"
120
+ elif type == ICMP_DEST_UNREACHABLE:
121
+ msg = "Dest Unreachable"
122
+
123
+ return IcmpPingResponse(rtt, msg)
124
+
125
+ except asyncio.TimeoutError:
126
+ self.loop.remove_writer(raw_socket)
127
+ self.loop.remove_reader(raw_socket)
128
+ raw_socket.close()
129
+
130
+ raise TimeoutError("Ping timeout")
131
+
132
+ def send_packet(
133
+ self,
134
+ packet: bytes,
135
+ socket: socket,
136
+ future: asyncio.Future,
137
+ dest: tuple[str, int],
138
+ ) -> None:
139
+ try:
140
+ socket.sendto(packet, dest)
141
+ except (BlockingIOError, InterruptedError):
142
+ return # The callback will be retried
143
+ except Exception as e:
144
+ self.loop.remove_writer(socket)
145
+ future.set_exception(e)
146
+ else:
147
+ self.loop.remove_writer(socket)
148
+ future.set_result(None)
149
+
150
+ async def send_ping(
151
+ self, raw_socket: socket, dest_addr: tuple[str, int], id: int
152
+ ) -> bool:
153
+ dummy_header = self.generate_header(id)
154
+
155
+ fmt = "d"
156
+ rtt_size = struct.calcsize(fmt)
157
+ data = (192 - rtt_size) * "0"
158
+
159
+ payload = struct.pack(fmt, perf_counter()) + data.encode("utf-8")
160
+ checksum = self.calculate_checksum(dummy_header + payload)
161
+
162
+ header = self.generate_header(id, checksum=checksum)
163
+
164
+ future: asyncio.Future[float] = asyncio.Future()
165
+
166
+ callback = partial(
167
+ self.send_packet,
168
+ packet=header + payload,
169
+ socket=raw_socket,
170
+ dest=dest_addr,
171
+ future=future,
172
+ )
173
+ self.loop.add_writer(raw_socket, callback)
174
+
175
+ try:
176
+ await future
177
+ except OSError:
178
+ return False
179
+
180
+ return True
181
+
182
+ async def ping(
183
+ self,
184
+ dest_addr: str,
185
+ *,
186
+ timeout: float = 5.0,
187
+ ttl: int = 64,
188
+ interface: str | None = None,
189
+ ) -> IcmpPingResponse:
190
+ addr = (dest_addr, 0)
191
+
192
+ # info = await self.loop.getaddrinfo(addr)
193
+
194
+ # let this raise
195
+ sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
196
+
197
+ # if interface:
198
+ # sock.bind((interface_ip, 0))
199
+
200
+ if interface:
201
+ # linux only
202
+ encoded = str(interface + "\0").encode("utf-8")
203
+ sock.setsockopt(SOL_SOCKET, SO_BINDTODEVICE, encoded)
204
+
205
+ sock.setblocking(False)
206
+ sock.setsockopt(IPPROTO_IP, IP_TTL, struct.pack("I", ttl))
207
+
208
+ icmp_id = randint(1, 65535)
209
+
210
+ ok = await self.send_ping(sock, addr, icmp_id)
211
+
212
+ if not ok:
213
+ return IcmpPingResponse(message="OSError")
214
+
215
+ try:
216
+ response = await self.receive_ping(sock, icmp_id, timeout)
217
+ except asyncio.TimeoutError:
218
+ response = IcmpPingResponse(message="Timeout exceeded")
219
+ finally:
220
+ sock.close()
221
+
222
+ return response
223
+
224
+
225
+ if __name__ == "__main__":
226
+
227
+ async def main():
228
+ icmp = IcmpPacketSender()
229
+
230
+ while True:
231
+ res = await icmp.ping("8.8.8.8", ttl=14)
232
+
233
+ if res:
234
+ print(res.rtt_ms)
235
+ else:
236
+ print(res.message)
237
+
238
+ await asyncio.sleep(1)
239
+
240
+ asyncio.run(main())