diff options
author | euxane | 2025-02-08 22:41:27 +0100 |
---|---|---|
committer | euxane | 2025-02-08 23:34:38 +0100 |
commit | bda07906d1851f95bfc25f0bfc43a42ced8d48b2 (patch) | |
tree | a97f4a5ff7570a1f6d5da3fa8cbd0a215d3b9064 /ping.nim | |
parent | 93d00382d3a8f4a89674c60207d6b797bf0d0b99 (diff) | |
download | tickwatch-bda07906d1851f95bfc25f0bfc43a42ced8d48b2.tar.gz |
repo: move nim sources into subdir
Diffstat (limited to 'ping.nim')
-rw-r--r-- | ping.nim | 320 |
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 | |||
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 | func 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 | |||
187 | func 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 | |||
194 | proc 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 | |||
204 | type PingMonitor = ref object | ||
205 | socket: Socket | ||
206 | target: IpAddress | ||
207 | source: IpAddress | ||
208 | ident: uint16 | ||
209 | payload: string | ||
210 | seqNum: uint16 | ||
211 | |||
212 | proc procIdent*(): uint16 = | ||
213 | uint16(getCurrentProcessId() and 0xFFFF) | ||
214 | |||
215 | proc 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 | |||
223 | proc buildEchoRequest(mon: PingMonitor): ICMPPacket = | ||
224 | mon.source = getPrimaryIPAddr(dest=mon.target) | ||
225 | buildEchoRequest(mon.source, mon.target, mon.ident, mon.seqNum, mon.payload) | ||
226 | |||
227 | proc 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 | |||
235 | func 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 | |||
242 | proc 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 | |||
251 | proc genRandomPayload(length = RANDOM_PAYLOAD_LENGTH): string = | ||
252 | newSeqWith(length, char.rand).join | ||
253 | |||
254 | proc 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 |