aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreuxane2024-11-24 17:17:02 +0100
committereuxane2024-11-24 17:17:02 +0100
commit5c49ffa36145b63bd6415af27ac6fee6b8dd5fe3 (patch)
tree9a999035889359976efce38b1c168fd455a220b4
parenta348e8732aa03e75b632983949469e2fcbc5a849 (diff)
downloadtickwatch-5c49ffa36145b63bd6415af27ac6fee6b8dd5fe3.tar.gz
ping: add module
-rw-r--r--ping.nim286
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
5import std/sequtils
6import std/strutils
7import std/random
8import std/os
9import std/net
10import std/nativesockets
11import std/posix
12import std/times
13import std/monotimes
14
15
16type
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
35const
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
46func pack16(n: uint16): array[2, uint8] =
47 cast[array[2, uint8]](nativesockets.htons(n))
48
49func pack32(n: uint32): array[4, uint8] =
50 cast[array[4, uint8]](nativesockets.htonl(n))
51
52func unpack16(a: array[2, uint8]): uint16 =
53 nativesockets.ntohs(cast[uint16](a))
54
55func unpack32(a: array[4, uint8]): uint32 =
56 nativesockets.ntohl(cast[uint32](a))
57
58func 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
67func 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
75func 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
83func 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
96func 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
104func 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
120func 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
133func domainProtocol(family: IpAddressFamily): (Domain, Protocol) =
134 case family:
135 of IPv6: (AF_INET6, IPPROTO_ICMPv6)
136 of IPv4: (AF_INET, IPPROTO_ICMP)
137
138func icmpReplyType(family: IpAddressFamily): uint8 =
139 case family:
140 of IPv6: ICMPv6_ECHO_REPLY
141 of IPv4: ICMP_ECHO_REPLY
142
143func 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
148func 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
155proc 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
161proc 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
170type PingMonitor = ref object
171 socket: Socket
172 target: IpAddress
173 source: IpAddress
174 ident: uint16
175 payload: string
176 seqNum: uint16
177
178proc procIdent*(): uint16 =
179 uint16(getCurrentProcessId() and 0xFFFF)
180
181proc 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
190func buildEchoRequest(mon: PingMonitor): ICMPPacket =
191 buildEchoRequest(mon.source, mon.target, mon.ident, mon.seqNum, mon.payload)
192
193proc 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
201func 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
208proc 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
217proc genRandomPayload(length = RANDOM_PAYLOAD_LENGTH): string =
218 newSeqWith(length, char.rand).join
219
220proc 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
238when 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 )