diff options
author | euxane | 2024-11-24 17:17:02 +0100 |
---|---|---|
committer | euxane | 2024-11-24 17:17:02 +0100 |
commit | 5c49ffa36145b63bd6415af27ac6fee6b8dd5fe3 (patch) | |
tree | 9a999035889359976efce38b1c168fd455a220b4 | |
parent | a348e8732aa03e75b632983949469e2fcbc5a849 (diff) | |
download | tickwatch-5c49ffa36145b63bd6415af27ac6fee6b8dd5fe3.tar.gz |
ping: add module
-rw-r--r-- | ping.nim | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/ping.nim b/ping.nim new file mode 100644 index 0000000..c99da96 --- /dev/null +++ b/ping.nim | |||
@@ -0,0 +1,286 @@ | |||
1 | # tickwatch | ||
2 | # Author: Euxane TRAN-GIRARD | ||
3 | # Licence: EUPL-1.2 | ||
4 | |||
5 | import std/sequtils | ||
6 | import std/strutils | ||
7 | import std/random | ||
8 | import std/os | ||
9 | import std/net | ||
10 | import std/nativesockets | ||
11 | import std/posix | ||
12 | import std/times | ||
13 | import std/monotimes | ||
14 | |||
15 | |||
16 | type | ||
17 | ICMPPacket = object | ||
18 | ## https://datatracker.ietf.org/doc/html/rfc792 | ||
19 | ## https://datatracker.ietf.org/doc/html/rfc4443#section-4.1 | ||
20 | typ: uint8 | ||
21 | code: uint8 | ||
22 | checksum: uint16 | ||
23 | ident: uint16 | ||
24 | seqNum: uint16 | ||
25 | payload: string | ||
26 | |||
27 | IPv6PseudoHeader = object | ||
28 | ## https://datatracker.ietf.org/doc/html/rfc2460#section-8.1 | ||
29 | source: IpAddress | ||
30 | dest: IpAddress | ||
31 | length: uint32 | ||
32 | next: uint8 | ||
33 | |||
34 | |||
35 | const | ||
36 | ICMP_ECHO_REQUEST = 8u8 | ||
37 | ICMP_ECHO_REPLY = 0u8 | ||
38 | ICMPv6_ECHO_REQUEST = 128u8 | ||
39 | ICMPv6_ECHO_REPLY = 129u8 | ||
40 | ICMP_IPv4_HEADER_LENGTH = 20 | ||
41 | RANDOM_PAYLOAD_LENGTH = 32 | ||
42 | ICMP_PACKET_BASE_LENGTH = 8 | ||
43 | DEFAULT_TIMEOUT = initDuration(seconds = 1) | ||
44 | |||
45 | |||
46 | func pack16(n: uint16): array[2, uint8] = | ||
47 | cast[array[2, uint8]](nativesockets.htons(n)) | ||
48 | |||
49 | func pack32(n: uint32): array[4, uint8] = | ||
50 | cast[array[4, uint8]](nativesockets.htonl(n)) | ||
51 | |||
52 | func unpack16(a: array[2, uint8]): uint16 = | ||
53 | nativesockets.ntohs(cast[uint16](a)) | ||
54 | |||
55 | func unpack32(a: array[4, uint8]): uint32 = | ||
56 | nativesockets.ntohl(cast[uint32](a)) | ||
57 | |||
58 | func pack(p: ICMPPacket): seq[uint8] = | ||
59 | result = newSeqOfCap[uint8](ICMP_PACKET_BASE_LENGTH + p.payload.len) | ||
60 | result.add p.typ | ||
61 | result.add p.code | ||
62 | result.add p.checksum.pack16 | ||
63 | result.add p.ident.pack16 | ||
64 | result.add p.seqNum.pack16 | ||
65 | result.add cast[seq[uint8]](p.payload) | ||
66 | |||
67 | func unpackICMPPacket(a: openArray[uint8]): ICMPPacket = | ||
68 | result.typ = a[0] | ||
69 | result.code = a[1] | ||
70 | result.checksum = [a[2], a[3]].unpack16 | ||
71 | result.ident = [a[4], a[5]].unpack16 | ||
72 | result.seqNum = [a[6], a[7]].unpack16 | ||
73 | result.payload = cast[string](a[8..^1]) | ||
74 | |||
75 | func pack(h: IPv6PseudoHeader): seq[uint8] = | ||
76 | result = newSeqOfCap[uint8](40) | ||
77 | result.add h.source.address_v6 | ||
78 | result.add h.dest.address_v6 | ||
79 | result.add h.length.pack32 | ||
80 | result.add [0u8, 0, 0] | ||
81 | result.add h.next | ||
82 | |||
83 | func checksum(a: openArray[uint8]): uint16 = | ||
84 | ## https://datatracker.ietf.org/doc/html/rfc1071 | ||
85 | var sum = 0u32 | ||
86 | for i in countup(0, a.len - 1, 2): | ||
87 | sum += uint32(a[i]) shl 8 | ||
88 | if i + 1 < a.len: | ||
89 | sum += uint32(a[i + 1]) | ||
90 | |||
91 | while (sum shr 16) != 0: | ||
92 | sum = (sum and 0xFFFF) + (sum shr 16) | ||
93 | |||
94 | not uint16(sum) | ||
95 | |||
96 | func icmpV6Header(source, dest: IpAddress, length: int): IPv6PseudoHeader = | ||
97 | IPv6PseudoHeader( | ||
98 | source: source, | ||
99 | dest: dest, | ||
100 | length: length.uint32, | ||
101 | next: posix.IPPROTO_ICMPv6.uint8, | ||
102 | ) | ||
103 | |||
104 | func buildEchoRequest( | ||
105 | source, dest: IpAddress, | ||
106 | ident, seqNum: uint16, | ||
107 | payload: string = "", | ||
108 | ): ICMPPacket = | ||
109 | result = ICMPPacket(ident: ident, seqNum: seqNum, payload: payload) | ||
110 | case dest.family: | ||
111 | of IPv6: | ||
112 | result.typ = ICMPv6_ECHO_REQUEST | ||
113 | let packedICMP = result.pack | ||
114 | let pseudoHeader = icmpV6Header(source, dest, packedICMP.len).pack | ||
115 | result.checksum = checksum(pseudoHeader & packedICMP) | ||
116 | of IPv4: | ||
117 | result.typ = ICMP_ECHO_REQUEST | ||
118 | result.checksum = checksum(result.pack) | ||
119 | |||
120 | func validateChecksum(packet: ICMPPacket, source, dest: IpAddress): bool = | ||
121 | var sourcePacket = packet | ||
122 | sourcePacket.checksum = 0 | ||
123 | case packet.typ: | ||
124 | of ICMPv6_ECHO_REPLY: | ||
125 | let packedICMP = sourcePacket.pack | ||
126 | let pseudoHeader = icmpV6Header(source, dest, packedICMP.len).pack | ||
127 | packet.checksum == checksum(pseudoHeader & packedICMP) | ||
128 | of ICMP_ECHO_REPLY: | ||
129 | packet.checksum == checksum(sourcePacket.pack) | ||
130 | else: | ||
131 | false | ||
132 | |||
133 | func domainProtocol(family: IpAddressFamily): (Domain, Protocol) = | ||
134 | case family: | ||
135 | of IPv6: (AF_INET6, IPPROTO_ICMPv6) | ||
136 | of IPv4: (AF_INET, IPPROTO_ICMP) | ||
137 | |||
138 | func icmpReplyType(family: IpAddressFamily): uint8 = | ||
139 | case family: | ||
140 | of IPv6: ICMPv6_ECHO_REPLY | ||
141 | of IPv4: ICMP_ECHO_REPLY | ||
142 | |||
143 | func stripIPHeader(family: IpAddressFamily, buffer: seq[uint8]): seq[uint8] = | ||
144 | case family: | ||
145 | of IPv6: buffer | ||
146 | of IPv4: buffer[min(ICMP_IPv4_HEADER_LENGTH, buffer.len)..^1] | ||
147 | |||
148 | func toTimeval(d: Duration): Timeval = | ||
149 | let uSecs = d.inMicroseconds | ||
150 | Timeval( | ||
151 | tv_sec: posix.Time(uSecs div (1000 * 1000)), | ||
152 | tv_usec: posix.Suseconds(uSecs mod (1000 * 1000)), | ||
153 | ) | ||
154 | |||
155 | proc setTimeout(sock: Socket, opt: cint, timeout: Duration) = | ||
156 | let timeVal = timeout.toTimeval | ||
157 | let timeValLen = sizeof(timeVal).SockLen | ||
158 | if sock.getFd().setSockOpt(SOL_SOCKET, opt, timeVal.addr, timeValLen) < 0: | ||
159 | raiseOsError osLastError() | ||
160 | |||
161 | proc receiveReply(sock: Socket, timeout: Duration): seq[uint8] = | ||
162 | var buffer: string | ||
163 | var ipAddr: IpAddress | ||
164 | var port: Port | ||
165 | sock.setTimeout(SO_RCVTIMEO, timeout) | ||
166 | discard sock.recvFrom(buffer, 1024, ipAddr, port) | ||
167 | stripIPHeader(ipAddr.family, cast[seq[uint8]](buffer)) | ||
168 | |||
169 | |||
170 | type PingMonitor = ref object | ||
171 | socket: Socket | ||
172 | target: IpAddress | ||
173 | source: IpAddress | ||
174 | ident: uint16 | ||
175 | payload: string | ||
176 | seqNum: uint16 | ||
177 | |||
178 | proc procIdent*(): uint16 = | ||
179 | uint16(getCurrentProcessId() and 0xFFFF) | ||
180 | |||
181 | proc initPingMonitor*(target: IpAddress, ident: uint16): PingMonitor = | ||
182 | let (domain, proto) = target.family.domainProtocol | ||
183 | PingMonitor( | ||
184 | socket: newSocket(domain, SOCK_RAW, proto), | ||
185 | target: target, | ||
186 | source: getPrimaryIPAddr(target), | ||
187 | ident: ident, | ||
188 | ) | ||
189 | |||
190 | func buildEchoRequest(mon: PingMonitor): ICMPPacket = | ||
191 | buildEchoRequest(mon.source, mon.target, mon.ident, mon.seqNum, mon.payload) | ||
192 | |||
193 | proc sendEchoRequest(mon: PingMonitor, timeout: Duration) = | ||
194 | try: | ||
195 | let echoRequest = mon.buildEchoRequest | ||
196 | mon.socket.setTimeout(SO_SNDTIMEO, timeout) | ||
197 | mon.socket.sendTo(mon.target, Port 0, cast[string](echoRequest.pack)) | ||
198 | except CatchableError: | ||
199 | discard | ||
200 | |||
201 | func validateICMPReply(mon: PingMonitor, packet: ICMPPacket): bool = | ||
202 | packet.typ == icmpReplyType(mon.target.family) and | ||
203 | packet.ident == mon.ident and | ||
204 | packet.seqNum == mon.seqNum and | ||
205 | packet.payload == mon.payload and | ||
206 | packet.validateChecksum(mon.target, mon.source) | ||
207 | |||
208 | proc tryReceiveReply(mon: PingMonitor, timeout: Duration): bool = | ||
209 | try: | ||
210 | let data = mon.socket.receiveReply(timeout) | ||
211 | if data.len < ICMP_PACKET_BASE_LENGTH: return false | ||
212 | let unpacked = unpackICMPPacket(data) | ||
213 | mon.validateICMPReply(unpacked) | ||
214 | except CatchableError: | ||
215 | false | ||
216 | |||
217 | proc genRandomPayload(length = RANDOM_PAYLOAD_LENGTH): string = | ||
218 | newSeqWith(length, char.rand).join | ||
219 | |||
220 | proc ping*(mon: PingMonitor, timeout = DEFAULT_TIMEOUT): Duration = | ||
221 | let startTime = getMonoTime() | ||
222 | let replyDeadline = startTime + timeout | ||
223 | |||
224 | inc mon.seqNum | ||
225 | mon.payload = genRandomPayload() | ||
226 | mon.sendEchoRequest(timeout) | ||
227 | |||
228 | while true: | ||
229 | let receiveTimeout = replyDeadline - getMonoTime() | ||
230 | if receiveTimeout <= DurationZero: | ||
231 | break | ||
232 | if mon.tryReceiveReply(receiveTimeout): | ||
233 | return getMonoTime() - startTime | ||
234 | |||
235 | raise newException(TimeoutError, "Ping timeout") | ||
236 | |||
237 | |||
238 | when defined(test): | ||
239 | import std/unittest | ||
240 | suite "ping": | ||
241 | test "pack/unpack endianness": | ||
242 | check pack16(256) == [1u8, 0] | ||
243 | check unpack16([1u8, 0]) == 256 | ||
244 | check pack32(256) == [0u8, 0, 1, 0] | ||
245 | check unpack32([0u8, 0, 1, 0]) == 256 | ||
246 | |||
247 | test "pack/unpack ICMP packet": | ||
248 | let packet = ICMPPacket( | ||
249 | typ: 1, | ||
250 | code: 2, | ||
251 | checksum: 3, | ||
252 | ident: 4, | ||
253 | seqNum: 5, | ||
254 | payload: cast[string](@[6u8, 7]), | ||
255 | ) | ||
256 | let packed = pack(packet) | ||
257 | check packed == [1u8, 2, 0, 3, 0, 4, 0, 5, 6, 7] | ||
258 | check unpackICMPPacket(packed) == packet | ||
259 | |||
260 | test "pack IPv6 pseudo-header": | ||
261 | let pseudoHeader = IPv6PseudoHeader( | ||
262 | source: parseIpAddress("::1"), | ||
263 | dest: parseIpAddress("::2"), | ||
264 | length: 1234, | ||
265 | next: 123, | ||
266 | ) | ||