Skip to content

Instantly share code, notes, and snippets.

@bandrel
Created February 23, 2026 14:43
Show Gist options
  • Select an option

  • Save bandrel/ba8251f1956f332b9356c218122845ba to your computer and use it in GitHub Desktop.

Select an option

Save bandrel/ba8251f1956f332b9356c218122845ba to your computer and use it in GitHub Desktop.
ICMP Timestamp Request Check
#!/usr/bin/env python3
"""
ICMP Timestamp Request/Reply Checker
Sends ICMP Timestamp Request (Type 13) packets to target hosts and checks
for Timestamp Reply (Type 14) responses. Useful for reconnaissance and
verifying host ICMP timestamp behavior.
Uses raw sockets — requires root/sudo privileges.
Zero external dependencies.
"""
import socket
import struct
import time
from dataclasses import dataclass
# ICMP Type constants
ICMP_TIMESTAMP_REQUEST = 13
ICMP_TIMESTAMP_REPLY = 14
ICMP_DEST_UNREACHABLE = 3
@dataclass
class TimestampResult:
"""Holds the result of an ICMP Timestamp probe."""
target: str
responded: bool
originate_ts: int | None = None
receive_ts: int | None = None
transmit_ts: int | None = None
rtt_ms: float | None = None
error: str | None = None
@property
def clock_delta_ms(self) -> int | None:
"""Estimate clock difference between local and remote host (ms since midnight UTC)."""
if self.originate_ts is not None and self.receive_ts is not None:
return self.receive_ts - self.originate_ts
return None
def __str__(self) -> str:
if not self.responded:
reason = f" ({self.error})" if self.error else ""
return f"{self.target}: No ICMP Timestamp Reply{reason}"
lines = [
f"{self.target}: ICMP Timestamp Reply received",
f" Originate : {self.originate_ts} ms since midnight UTC",
f" Receive : {self.receive_ts} ms since midnight UTC",
f" Transmit : {self.transmit_ts} ms since midnight UTC",
f" RTT : {self.rtt_ms:.2f} ms",
]
if self.clock_delta_ms is not None:
lines.append(f" Clock Δ : {self.clock_delta_ms} ms (approx)")
return "\n".join(lines)
def get_ms_since_midnight_utc() -> int:
"""Return the number of milliseconds since midnight UTC."""
now = time.gmtime()
ms = (now.tm_hour * 3600 + now.tm_min * 60 + now.tm_sec) * 1000
ms += int((time.time() % 1) * 1000)
return ms
def checksum(data: bytes) -> int:
"""Compute the ICMP checksum (RFC 1071 one's complement sum)."""
if len(data) % 2:
data += b"\x00"
s = 0
for i in range(0, len(data), 2):
word = (data[i] << 8) + data[i + 1]
s += word
s = (s >> 16) + (s & 0xFFFF)
s += s >> 16
return ~s & 0xFFFF
def build_timestamp_request(
originate_ts: int | None = None,
icmp_id: int = 0x1337,
icmp_seq: int = 1,
) -> tuple[bytes, int]:
"""
Build an ICMP Timestamp Request packet (Type 13).
Layout (20 bytes total):
Type (1) | Code (1) | Checksum (2) | ID (2) | Seq (2)
Originate Timestamp (4) | Receive Timestamp (4) | Transmit Timestamp (4)
Returns:
Tuple of (packet_bytes, originate_timestamp_used)
"""
if originate_ts is None:
originate_ts = get_ms_since_midnight_utc()
# Build with zero checksum first
header = struct.pack(
"!BBHHH", ICMP_TIMESTAMP_REQUEST, 0, 0, icmp_id, icmp_seq
)
payload = struct.pack("!III", originate_ts, 0, 0)
raw = header + payload
# Compute and insert checksum
chk = checksum(raw)
packet = struct.pack(
"!BBHHH", ICMP_TIMESTAMP_REQUEST, 0, chk, icmp_id, icmp_seq
) + payload
return packet, originate_ts
def parse_timestamp_reply(data: bytes) -> tuple[int, int, int, int, int] | None:
"""
Parse an ICMP Timestamp Reply from raw IP+ICMP bytes.
Returns:
Tuple of (icmp_type, icmp_code, originate, receive, transmit)
or None if the data is too short.
"""
if data is None or len(data) < 40: # 20 IP + 8 ICMP hdr + 12 timestamps
return None
ip_ihl = (data[0] & 0x0F) * 4
icmp_data = data[ip_ihl:]
if len(icmp_data) < 20:
return None
icmp_type, icmp_code = struct.unpack("!BB", icmp_data[:2])
originate, receive, transmit = struct.unpack("!III", icmp_data[8:20])
return icmp_type, icmp_code, originate, receive, transmit
def check_icmp_timestamp(
target: str,
timeout: float = 3.0,
icmp_id: int = 0x1337,
icmp_seq: int = 1,
) -> TimestampResult:
"""
Send an ICMP Timestamp Request to a target and analyze the reply.
Args:
target: IP address or hostname to probe.
timeout: Seconds to wait for a reply.
icmp_id: ICMP identifier field.
icmp_seq: ICMP sequence number.
Returns:
TimestampResult with probe details.
"""
try:
dest_ip = socket.gethostbyname(target)
except socket.gaierror as e:
return TimestampResult(
target=target, responded=False, error=f"DNS resolution failed: {e}"
)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
except PermissionError:
return TimestampResult(
target=target, responded=False, error="Permission denied - run with sudo/root"
)
except OSError as e:
return TimestampResult(target=target, responded=False, error=str(e))
try:
sock.settimeout(timeout)
packet, originate_ts = build_timestamp_request(icmp_id=icmp_id, icmp_seq=icmp_seq)
start = time.monotonic()
sock.sendto(packet, (dest_ip, 0))
while True:
elapsed = time.monotonic() - start
if elapsed >= timeout:
return TimestampResult(
target=target, responded=False, error="Timeout - no response received"
)
sock.settimeout(timeout - elapsed)
try:
data, addr = sock.recvfrom(1024)
except socket.timeout:
return TimestampResult(
target=target, responded=False, error="Timeout - no response received"
)
rtt = (time.monotonic() - start) * 1000
parsed = parse_timestamp_reply(data)
if parsed is None:
continue
icmp_type, icmp_code, orig, recv, xmit = parsed
if icmp_type == ICMP_DEST_UNREACHABLE:
return TimestampResult(
target=target,
responded=False,
error=f"ICMP Destination Unreachable (code {icmp_code})",
)
if icmp_type == ICMP_TIMESTAMP_REPLY:
return TimestampResult(
target=target,
responded=True,
originate_ts=orig,
receive_ts=recv,
transmit_ts=xmit,
rtt_ms=rtt,
)
# Ignore unrelated ICMP packets
continue
except Exception as e:
return TimestampResult(target=target, responded=False, error=str(e))
finally:
sock.close()
def scan_targets(targets: list[str], timeout: float = 3.0) -> list[TimestampResult]:
"""Probe multiple targets for ICMP Timestamp responses."""
results = []
for target in targets:
result = check_icmp_timestamp(target, timeout=timeout)
results.append(result)
return results
def main():
import argparse
parser = argparse.ArgumentParser(
description="Check if hosts respond to ICMP Timestamp Requests (Type 13)"
)
parser.add_argument("targets", nargs="+", help="Target IP addresses or hostnames")
parser.add_argument(
"-t", "--timeout", type=float, default=3.0, help="Timeout per probe (seconds)"
)
args = parser.parse_args()
print(f"ICMP Timestamp Probe - checking {len(args.targets)} target(s)\n")
print("=" * 60)
results = scan_targets(args.targets, timeout=args.timeout)
responding = 0
for result in results:
print(result)
print("-" * 60)
if result.responded:
responding += 1
print(f"\nSummary: {responding}/{len(results)} hosts responded with Timestamp Reply")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment