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
3031examples/icmp_echo_client.py
3132
3536
3637from __future__ import annotations
3738
38- import random
39- import threading
39+ import os
40+ import socket as python_socket
41+ import struct
4042import time
41- from datetime import datetime
42- from typing import cast
4343
4444import click
4545
46+ from examples .lib .client import Client
4647from net_addr import (
4748 ClickTypeIp4Address ,
4849 ClickTypeIp4Host ,
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