aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 )
267 check pseudoHeader.pack == [
268 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
269 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
270 0, 0, 4, 210, 0, 0, 0, 123,