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.
- flux_networking_shared-0.2.2/.gitignore +47 -0
- flux_networking_shared-0.2.2/PKG-INFO +35 -0
- flux_networking_shared-0.2.2/README.md +18 -0
- flux_networking_shared-0.2.2/dummy_packages/probert/probert/__init__.py +3 -0
- flux_networking_shared-0.2.2/dummy_packages/probert/probert/network.py +43 -0
- flux_networking_shared-0.2.2/dummy_packages/probert/setup.py +9 -0
- flux_networking_shared-0.2.2/pyproject.toml +50 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/__init__.py +54 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/async_ping.py +240 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/helpers.py +207 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/log.py +4 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/models/__init__.py +13 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/models/dns.py +108 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/models/network.py +403 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/network_observer.py +214 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/systemd_parser.py +24 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/__init__.py +80 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/css/network_screen.tcss +55 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/messages/__init__.py +42 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/models/__init__.py +31 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/models/interface_config_result.py +39 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/models/network.py +138 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/models/upnp.py +103 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/screens/__init__.py +13 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/screens/confirm_exit.py +67 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/screens/interface_modal_screen.py +417 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/screens/network_screen.py +719 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/validators/__init__.py +13 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/validators/network_validator.py +200 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/__init__.py +44 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/address_builder.py +233 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/connectivity.py +283 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/dns.py +86 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/dns_summary.py +91 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/field_set.py +62 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/focus_label.py +41 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/interface.py +270 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/interface_group.py +293 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/label_switch.py +73 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/labels.py +45 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/loading.py +189 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/name_resolution.py +173 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/network_overview.py +198 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/network_summary.py +94 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/route_summary.py +87 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/routing.py +78 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/upnp_action_bar.py +184 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/upnp_builder.py +691 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/tui/widgets/validate_blur_input.py +27 -0
- flux_networking_shared-0.2.2/src/flux_networking_shared/upnp_querier.py +300 -0
- 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,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,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())
|