Skip to content

Commit 7040a3c

Browse files
committed
Working on implementing ping example.
1 parent 3771f5f commit 7040a3c

File tree

2 files changed

+397
-77
lines changed

2 files changed

+397
-77
lines changed

examples/icmp_echo_client.py

Lines changed: 125 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525

2626

2727
"""
28-
The example 'user space' client for ICMPv4/v6 Echo.
28+
The example 'user space' client for ICMP echo. It actively sends messages
29+
to the ICMP Echo service.
2930
3031
examples/icmp_echo_client.py
3132
@@ -35,14 +36,14 @@
3536

3637
from __future__ import annotations
3738

38-
import random
39-
import threading
39+
import os
40+
import socket as python_socket
41+
import struct
4042
import time
41-
from datetime import datetime
42-
from typing import cast
4343

4444
import click
4545

46+
from examples.lib.client import Client
4647
from net_addr import (
4748
ClickTypeIp4Address,
4849
ClickTypeIp4Host,
@@ -57,27 +58,31 @@
5758
IpAddress,
5859
MacAddress,
5960
)
60-
from pytcp import stack
61-
from pytcp.protocols.icmp4.message.icmp4_message__echo_request import (
62-
Icmp4EchoRequestMessage,
63-
)
64-
from pytcp.protocols.icmp6.message.icmp6_message__echo_request import (
65-
Icmp6EchoRequestMessage,
66-
)
67-
from pytcp.stack import GITHUB_REPO, PYTCP_VERSION
61+
from pytcp import socket, stack
62+
63+
ICMP4__ECHO_REQUEST__TYPE = 8
64+
ICMP4__ECHO_REQUEST__CODE = 0
6865

66+
ICMP6_ECHO_REQUEST_TYPE = 128
67+
ICMP6_ECHO_REQUEST_CODE = 0
6968

70-
class IcmpEchoClient:
69+
70+
class IcmpEchoClient(Client):
7171
"""
72-
ICMPv4/v6 Echo client support class.
72+
ICMP Echo client support class.
7373
"""
7474

75+
_protocol_name = "ICMP"
76+
_client_name = "Echo"
77+
7578
def __init__(
7679
self,
7780
*,
7881
local_ip_address: IpAddress,
7982
remote_ip_address: IpAddress,
8083
message_count: int = -1,
84+
message_delay: int = 1,
85+
message_size: int = 5,
8186
) -> None:
8287
"""
8388
Class constructor.
@@ -86,84 +91,127 @@ def __init__(
8691
self._local_ip_address = local_ip_address
8792
self._remote_ip_address = remote_ip_address
8893
self._message_count = message_count
94+
self._message_delay = message_delay
95+
self._message_size = message_size
8996
self._run_thread = False
9097

91-
def start(self) -> None:
98+
@staticmethod
99+
def _checksum(data: bytes) -> int:
92100
"""
93-
Start the service thread.
101+
Compute the Internet Checksum of the supplied data.
94102
"""
95103

96-
click.echo("Starting the ICMP Echo client.")
97-
self._run_thread = True
98-
threading.Thread(target=self._thread__client).start()
99-
time.sleep(0.1)
104+
if len(data) % 2:
105+
data += b"\x00"
106+
res = sum(struct.unpack(f"!{len(data) // 2}H", data))
107+
res = (res >> 16) + (res & 0xFFFF)
108+
res += res >> 16
109+
return int(~res & 0xFFFF)
100110

101-
def stop(self) -> None:
111+
@classmethod
112+
def _create_icmp4_message(cls, identifier: int, sequence: int) -> bytes:
102113
"""
103-
Stop the service thread.
114+
Create ICMPv4 Echo Request packet.
104115
"""
105116

106-
click.echo("Stopinging the ICMP Echo client.")
107-
self._run_thread = False
108-
time.sleep(0.1)
117+
def header(cksum: int = 0) -> bytes:
118+
return struct.pack(
119+
"!BBHHH",
120+
ICMP4__ECHO_REQUEST__TYPE,
121+
ICMP4__ECHO_REQUEST__CODE,
122+
cksum,
123+
identifier,
124+
sequence,
125+
)
109126

110-
def _thread__client(self) -> None:
111-
assert self._local_ip_address is not None
127+
payload = struct.pack("!d", time.time()) # 8-byte timestamp
128+
cksum = cls._checksum(header() + payload)
112129

113-
flow_id = random.randint(0, 65535)
130+
return header(cksum) + payload
114131

115-
message_count = self._message_count
132+
@classmethod
133+
def _create_icmp6_message(
134+
cls, src_ip: str, dst_ip: str, identifier: int, sequence: int
135+
) -> bytes:
136+
"""
137+
Create ICMPv6 Echo Request packet with manually computed checksum.
138+
"""
116139

117-
message_seq = 0
118-
while self._run_thread and message_count:
119-
message = bytes(
120-
f"PyTCP {PYTCP_VERSION}, {GITHUB_REPO} - {str(datetime.now())}",
121-
"utf-8",
140+
def header(cksum: int = 0) -> bytes:
141+
return struct.pack(
142+
"!BBHHH",
143+
ICMP6_ECHO_REQUEST_TYPE,
144+
ICMP6_ECHO_REQUEST_CODE,
145+
cksum,
146+
identifier,
147+
sequence,
122148
)
123149

124-
match self._local_ip_address.version, self._remote_ip_address.version:
125-
case 4, 4:
126-
stack.packet_handler.send_icmp4_packet(
127-
ip4__local_address=cast(
128-
Ip4Address, self._local_ip_address
129-
),
130-
ip4__remote_address=cast(
131-
Ip4Address, self._remote_ip_address
132-
),
133-
icmp4__message=Icmp4EchoRequestMessage(
134-
id=flow_id,
135-
seq=message_seq,
136-
data=message,
137-
),
138-
)
139-
case 6, 6:
140-
stack.packet_handler.send_icmp6_packet(
141-
ip6__local_address=cast(
142-
Ip6Address, self._local_ip_address
143-
),
144-
ip6__remote_address=cast(
145-
Ip6Address, self._remote_ip_address
146-
),
147-
icmp6__message=Icmp6EchoRequestMessage(
148-
id=flow_id,
149-
seq=message_seq,
150-
data=message,
151-
),
152-
)
153-
case _:
154-
raise ValueError(
155-
"Unsupported IP version combination: "
156-
f"{self._local_ip_address.version=}, "
157-
f"{self._remote_ip_address.version=}"
158-
)
150+
payload = struct.pack("!d", time.time()) # 8-byte timestamp
151+
icmp_packet_wo_cksum = header(0) + payload
152+
153+
# Build pseudo-header for checksum
154+
def inet6_aton(addr: str) -> bytes:
155+
return bytes(python_socket.inet_pton(python_socket.AF_INET6, addr))
156+
157+
src = inet6_aton(src_ip)
158+
dst = inet6_aton(dst_ip)
159+
upper_layer_len = struct.pack("!I", len(icmp_packet_wo_cksum))
160+
next_header = b"\x00" * 3 + struct.pack("!B", int(socket.IPPROTO_ICMP6))
161+
162+
pseudo_header = src + dst + upper_layer_len + next_header
163+
checksum_input = pseudo_header + icmp_packet_wo_cksum
164+
165+
cksum = cls._checksum(checksum_input)
166+
167+
return header(cksum) + payload
168+
169+
def _thread__client(self) -> None:
170+
"""
171+
Client thread.
172+
"""
159173

174+
if client_socket := self._get_client_socket():
175+
message_count = self._message_count
176+
177+
while self._run_thread and message_count:
178+
match (
179+
self._local_ip_address.version,
180+
self._remote_ip_address.version,
181+
):
182+
case 6, 6:
183+
icmp_message = self._create_icmp6_message(
184+
src_ip=str(self._local_ip_address),
185+
dst_ip=str(self._remote_ip_address),
186+
identifier=os.getpid() & 0xFFFF,
187+
sequence=self._message_count - message_count + 1,
188+
)
189+
case 4, 4:
190+
icmp_message = self._create_icmp4_message(
191+
identifier=os.getpid() & 0xFFFF,
192+
sequence=self._message_count - message_count + 1,
193+
)
194+
case _:
195+
raise ValueError("Invalid IP address versions.")
196+
197+
try:
198+
client_socket.send(icmp_message)
199+
except OSError as error:
200+
click.echo(f"Client ICMP Echo: send() error - {error!r}.")
201+
break
202+
203+
click.echo(
204+
f"Client ICMP Echo: Sent {len(icmp_message)} bytes of data to "
205+
f"{self._remote_ip_address}."
206+
)
207+
time.sleep(self._message_delay)
208+
message_count = min(message_count, message_count - 1)
209+
210+
client_socket.close()
160211
click.echo(
161-
f"Client ICMP Echo: Sent ICMP Echo ({flow_id}/{message_seq}) "
162-
f"to {self._remote_ip_address} - {str(message)}."
212+
"Client ICMP Echo: Closed connection to "
213+
f"{self._remote_ip_address}.",
163214
)
164-
time.sleep(1)
165-
message_seq += 1
166-
message_count = min(message_count, message_count - 1)
167215

168216

169217
@click.command()
@@ -221,7 +269,7 @@ def cli(
221269
) -> None:
222270
"""
223271
Start PyTCP stack and stop it when user presses Ctrl-C.
224-
Start Icmp Echo client.
272+
Start ICMP Echo client.
225273
"""
226274

227275
if ip6_host:
@@ -250,7 +298,7 @@ def cli(
250298
)
251299
case _:
252300
raise ValueError(
253-
f"Unsupported IP version: {remote_ip_address.version}"
301+
f"Invalid remote IP address version: {remote_ip_address.version}"
254302
)
255303

256304
try:

0 commit comments

Comments
 (0)