aboutsummaryrefslogtreecommitdiff
path: root/ping.nim
diff options
context:
space:
mode:
authoreuxane2025-02-08 22:41:27 +0100
committereuxane2025-02-08 23:34:38 +0100
commitbda07906d1851f95bfc25f0bfc43a42ced8d48b2 (patch)
treea97f4a5ff7570a1f6d5da3fa8cbd0a215d3b9064 /ping.nim
parent93d00382d3a8f4a89674c60207d6b797bf0d0b99 (diff)
downloadtickwatch-bda07906d1851f95bfc25f0bfc43a42ced8d48b2.tar.gz
repo: move nim sources into subdir
Diffstat (limited to 'ping.nim')
-rw-r--r--ping.nim320
1 files changed, 0 insertions, 320 deletions
diff --git a/ping.nim b/ping.nim
deleted file mode 100644
index dd41882..0000000
--- a/ping.nim
+++ /dev/null
@@ -1,320 +0,0 @@
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
170func toIpAddress(info: ptr AddrInfo): IpAddress =
171 case info.ai_family:
172 of posix.AF_INET6:
173 let sockAddr = cast[ptr posix.SockAddr_in6](info.ai_addr)[]
174 IpAddress(
175 family: IPv6,
176 address_v6: cast[array[0..15, uint8]](sockAddr.sin6_addr.s6_addr),
177 )
178 of posix.AF_INET:
179 let sockAddr = cast[ptr posix.SockAddr_in](info.ai_addr)[]
180 IpAddress(
181 family: IPv4,
182 address_v4: cast[array[0..3, uint8]](sockAddr.sin_addr.s_addr),
183 )
184 else:
185 raise newException(ValueError, "Invalid address info")
186
187func splitDomainHostname(target: string): (Domain, string) =
188 let parts = target.split('/', 1)
189 case parts[0]:
190 of "6": (AF_INET6, parts[1])
191 of "4": (AF_INET, parts[1])
192 else: (AF_UNSPEC, target)
193
194proc resolve*(target: string): IpAddress =
195 try:
196 parseIpAddress(target)
197 except ValueError:
198 let (domain, hostname) = splitDomainHostname(target)
199 let addrInfo = getAddrInfo(hostname, Port 0, domain)
200 defer: freeAddrInfo(addrInfo)
201 addrInfo.toIpAddress
202
203
204type PingMonitor = ref object
205 socket: Socket
206 target: IpAddress
207 source: IpAddress
208 ident: uint16
209 payload: string
210 seqNum: uint16
211
212proc procIdent*(): uint16 =
213 uint16(getCurrentProcessId() and 0xFFFF)
214
215proc initPingMonitor*(target: IpAddress, ident: uint16): PingMonitor =
216 let (domain, proto) = target.family.domainProtocol
217 PingMonitor(
218 socket: newSocket(domain, SOCK_RAW, proto),
219 target: target,
220 ident: ident,
221 )
222
223proc buildEchoRequest(mon: PingMonitor): ICMPPacket =
224 mon.source = getPrimaryIPAddr(dest=mon.target)
225 buildEchoRequest(mon.source, mon.target, mon.ident, mon.seqNum, mon.payload)
226
227proc sendEchoRequest(mon: PingMonitor, timeout: Duration) =
228 try:
229 let echoRequest = mon.buildEchoRequest
230 mon.socket.setTimeout(SO_SNDTIMEO, timeout)
231 mon.socket.sendTo(mon.target, Port 0, cast[string](echoRequest.pack))
232 except CatchableError:
233 discard
234
235func validateICMPReply(mon: PingMonitor, packet: ICMPPacket): bool =
236 packet.typ == icmpReplyType(mon.target.family) and
237 packet.ident == mon.ident and
238 packet.seqNum == mon.seqNum and
239 packet.payload == mon.payload and
240 packet.validateChecksum(mon.target, mon.source)
241
242proc tryReceiveReply(mon: PingMonitor, timeout: Duration): bool =
243 try:
244 let data = mon.socket.receiveReply(timeout)
245 if data.len < ICMP_PACKET_BASE_LENGTH: return false
246 let unpacked = unpackICMPPacket(data)
247 mon.validateICMPReply(unpacked)
248 except CatchableError:
249 false
250
251proc genRandomPayload(length = RANDOM_PAYLOAD_LENGTH): string =
252 newSeqWith(length, char.rand).join
253
254proc ping*(mon: PingMonitor, timeout = DEFAULT_TIMEOUT): Duration =
255 let startTime = getMonoTime()
256 let replyDeadline = startTime + timeout
257
258 inc mon.seqNum
259 mon.payload = genRandomPayload()
260 mon.sendEchoRequest(timeout)
261
262 while true:
263 let receiveTimeout = replyDeadline - getMonoTime()
264 if receiveTimeout <= DurationZero:
265 break