@juit/lib-ping
Version:
ICMPv4/ICMPv6 Ping library for NodeJS
185 lines (159 loc) • 9.79 kB
text/typescript
/* ========================================================================== *
* ICMP ECHO REQUEST / RESPONSE MESSAGES *
* ========================================================================== *
* *
* PACKET STRUCTURE *
* *
* 0 1 2 3 4 *
* 0 +------+------+------+------+ *
* | TYPE | CODE | CHECKSUM | *
* 4 +------+------+------+------+ *
* | IDENTIFIER | SEQUENCE | *
* 8 +-------------+-------------+ *
* | PAYLOAD (timestamp) | *
* | (8 bytes) | *
* 16 +---------------------------+ *
* | PAYLOAD (full sequence) | *
* | (4 bytes) | *
* 20 +---------------------------+ *
* | PAYLOAD (correl. data) | *
* | (48 bytes) | *
* 64 +---------------------------+ *
* *
* TYPES AND CODES *
* *
* Type | ECHO Request | Echo Response | *
* -------+--------------+---------------+ *
* ICMPv4 | 0x08 | 0x00 | *
* ICMPv6 | 0x80 | 0x81 | *
* -------+--------------+---------------+ *
* *
* Description: *
* *
* - Type: kind of packet (ECHO Request/Response, ICMPv4/ICMPv6) *
* - Code: always 0x00 *
* - Identifier: identifies the process ID sending the ECHO Request *
* - Sequence: sequential number for correlating messages *
* - Checksum: calculated over the whole packet with checksum as zero *
* - Payload: 8 bytes timestamp in nanos, full sequence, correlation data *
* *
* We kind of like align with the normal "ping" utility that sends by default *
* 64 bytes of data (including the 8 bytes ICMP header) and we fill the whole *
* 56 bytes of payload with 8 bytes of timestamps in nanosecods, followed by *
* 54 bytes of "correlation data" (a random number). *
* *
* ========================================================================== */
import { randomBytes } from 'node:crypto'
export const ERR_WRONG_LENGTH = -1n
export const ERR_WRONG_CORRELATION = -2n
export const ERR_WRONG_ICMP_TYPE = -3n
export const ERR_WRONG_ICMP_CODE = -4n
export const ERR_WRONG_SEQUENCE = -5n
export const ERR_SEQUENCE_TOO_BIG = -6n
export const ERR_SEQUENCE_TOO_SMALL = -7n
export const ERR_LATENCY_NEGATIVE = -8n
export function getWarning(num: bigint): { code: string, message: string } {
if (num >= 0) return { code: 'OK', message: `Latency is ${Number(num) / 1000000} ms` }
switch (num) {
case ERR_WRONG_LENGTH: return { code: 'ERR_WRONG_LENGTH', message: 'Received packet with invalid length' }
case ERR_WRONG_CORRELATION: return { code: 'ERR_WRONG_CORRELATION', message: 'Received packet with invalid correlation data' }
case ERR_WRONG_ICMP_TYPE: return { code: 'ERR_WRONG_ICMP_TYPE', message: 'Received packet with invalid ICMP type' }
case ERR_WRONG_ICMP_CODE: return { code: 'ERR_WRONG_ICMP_CODE', message: 'Received packet with invalid ICMP code' }
case ERR_WRONG_SEQUENCE: return { code: 'ERR_WRONG_SEQUENCE', message: 'Received packet with mismatched sequence in header/payload' }
case ERR_SEQUENCE_TOO_BIG: return { code: 'ERR_SEQUENCE_TOO_BIG', message: 'Received packet with sequence in the future' }
case ERR_SEQUENCE_TOO_SMALL: return { code: 'ERR_SEQUENCE_TOO_SMALL', message: 'Received packet with sequence in the past (duplicate packet?)' }
case ERR_LATENCY_NEGATIVE: return { code: 'ERR_LATENCY_NEGATIVE', message: 'Received packet with negative latence (time travel is possible!)' }
default: return { code: 'ERR_UNKNOWN', message: `Unknown error code (code=${num})` }
}
}
export class ProtocolHandler {
private readonly __packet: Buffer = randomBytes(64)
private readonly __type: number
private __seq_out: number = 0
private __seq_in: number = 0
constructor(v6: boolean) {
this.__type = (v6 ? 0x81 : 0x00)
// type (0x80 for IPv6, 0x08 for IPv4), code (0x00), checksum (0x0000)
this.__packet.writeUInt32BE(v6 ? 0x80000000 : 0x08000000, 0)
// itentifier (process pid)
this.__packet.writeUInt16BE(process.pid % 0x0ffff, 4)
// sequence (for now set to 0)
this.__packet.writeUInt16BE(this.__seq_out, 6)
// timestamp (set to zero as well)
this.__packet.writeBigUInt64BE(0n, 8)
}
outgoing(): Buffer {
const buffer = Buffer.from(this.__packet)
// Prep the sequence (full sequence and lower 16 bits)
buffer.writeUInt32BE(++ this.__seq_out, 16)
buffer.writeUInt16BE(this.__seq_out & 0x0ff, 6)
// Prep the timestamp
buffer.writeBigUInt64BE(process.hrtime.bigint(), 8)
// Calculate the checksum
buffer.writeUInt16BE(rfc1071crc(buffer), 2)
// All done
return buffer
}
incoming(buffer: Buffer, now: bigint = process.hrtime.bigint()): bigint {
// if the buffer is _bigger_ then our fixed 64 bytes packet size, it might
// be prepended by the IPv4 or IPv6 header (this happens on Macs)
if (buffer.length > 64) {
const first = buffer.readUInt8(0)
const version = first >> 4
if (version === 6) {
// IPv6 is easy and has a fixed header length of 40 bytes, soo....
if (buffer.length === 104) buffer = buffer.subarray(40)
} else if (version === 4) {
// IPv4 has a variable header length, the lower 4 bits of the first byte
// indicate the length of the header in 32-bit (4-byte) words...
const length = (first & 0xF) * 4
if (buffer.length === (length + 64)) buffer = buffer.subarray(length)
}
}
// If the buffer length is not 64 bytes after trimming above, we can
// safely assume this is not an ECHO reply to one of our packets
if (buffer.length !== 64) return ERR_WRONG_LENGTH
// Compare the _correlation data_ part of the buffer to determine whether
// this was a packet sent for us or not.... If not, ignore
if (buffer.compare(this.__packet, 20, 64, 20, 64) !== 0) return ERR_WRONG_CORRELATION
// Check the type, it _MUST_ be an echo reply (IPv4 or IPv6)
const type = buffer.readUInt8(0)
if (type !== this.__type) return ERR_WRONG_ICMP_TYPE
// Check the code, it _MUST_ be 0x00
const code = buffer.readUInt8(1)
if (code !== 0x00) return ERR_WRONG_ICMP_CODE
// Checksums on IPv6 require a "pseudo header" to be prepended so for now
// we skip the whole shabang and ignore it... I assume the kernel checks...
// const checksum = buffer.readUInt16BE(2)
// const computedChecksum = rfc1071crc(buffer)
// if (checksum != computedChecksum) return
// Identifier seems to get messed up on Linux IPv6 when pinging localhost,
// so we ignore this too, and simply rely on out _correlation data_ above...
// const identifier = buffer.readUInt16BE(4)
// if (identifier != this._id) return
// Check the sequence, as it's monotonic we can discard values greater than
// the last we sent out, or lower-or-equal than the last one we received...
const sequence = buffer.readUInt32BE(16) // this is in our "payload"
// Sequence in the ICMP header is 16 bits, but as some kernels only return
// the lower 8 bits on ECHO Reply packets, so only match the lower 8 bits...
if ((buffer.readUInt16BE(6) & 0xFF) != (sequence & 0xFF)) return ERR_WRONG_SEQUENCE
// If the full sequence is greater than whatever we sent out, we ignore
if (sequence > this.__seq_out) return ERR_SEQUENCE_TOO_BIG
// If the full sequence is lower (or equal) to the last one received this
// means we received either a duplicate packet, or an out of order one
if (sequence <= this.__seq_in) return ERR_SEQUENCE_TOO_SMALL
// Calculate the delta-time in nanoseconds, if negative obviously ignore
const latency = now - buffer.readBigInt64BE(8)
if (latency < 0n) return ERR_LATENCY_NEGATIVE
// Store the last sequence number and return our latency
this.__seq_in = sequence
return latency
}
}
export function rfc1071crc(buffer: Buffer): number {
let sum = 0
for (let i = 0; i < buffer.length; i += 2) {
sum = (sum + buffer.readUInt16BE(i)) % 0xFFFF
}
return (~sum) & 0xFFFF
}