layers_NTP.js

const { OsiModelLayers } = require('./osi');
const { makeLayer, attach } = require('./define');

// Number of seconds between the NTP epoch (1 Jan 1900) and the Unix epoch
// (1 Jan 1970). Used to convert NTP 64-bit timestamps to JS Date.
const NTP_EPOCH_OFFSET = 2208988800;

const tsFields = ['referenceTimestamp', 'originTimestamp', 'receiveTimestamp', 'transmitTimestamp'];
const tsOffsets = {
  referenceTimestamp: 16,
  originTimestamp: 24,
  receiveTimestamp: 32,
  transmitTimestamp: 40,
};

/**
 * Convert a JS Date to an NTP 64-bit timestamp written into `buf` at `offset`.
 */
function writeNtpTimestamp(buf, offset, dateOrMs) {
  if (dateOrMs == null || dateOrMs === 0) {
    buf.writeUInt32BE(0, offset);
    buf.writeUInt32BE(0, offset + 4);
    return;
  }
  const ms = dateOrMs instanceof Date ? dateOrMs.getTime() : Number(dateOrMs);
  const totalSeconds = ms / 1000 + NTP_EPOCH_OFFSET;
  const sec = Math.floor(totalSeconds);
  const frac = Math.floor((totalSeconds - sec) * 0x100000000);
  buf.writeUInt32BE(sec >>> 0, offset);
  buf.writeUInt32BE(frac >>> 0, offset + 4);
}

/**
 * Read an NTP 64-bit timestamp from `buf` at `offset` and return a JS Date.
 * Returns `null` for the all-zero "unspecified" timestamp.
 */
function readNtpTimestamp(buf, offset) {
  const sec = buf.readUInt32BE(offset);
  const frac = buf.readUInt32BE(offset + 4);
  if (sec === 0 && frac === 0) return null;
  const ms = (sec - NTP_EPOCH_OFFSET) * 1000 + Math.round(frac / 0x100000000 * 1000);
  return new Date(ms);
}

const ntpTimestamp = (offset) => ({
  get() { return readNtpTimestamp(this._buf, offset); },
  set(v) { writeNtpTimestamp(this._buf, offset, v); },
});

/**
 * NTP (Network Time Protocol) layer
 * @class
 * @implements {Layer}
 * @property {number} li - Leap indicator (0..3).
 * @property {number} vn - Version number (typically 3 or 4).
 * @property {number} mode - Mode (1=symmetric active, 3=client, 4=server, 5=broadcast).
 * @property {number} stratum - Stratum level (0=unspecified, 1=primary, 2-15=secondary).
 * @property {number} poll - Poll interval as signed log2 seconds.
 * @property {number} precision - Precision as signed log2 seconds.
 * @property {number} rootDelay - 32-bit fixed-point root delay.
 * @property {number} rootDispersion - 32-bit fixed-point root dispersion.
 * @property {number} referenceId - Reference identifier (4 bytes; IPv4 or 4-char ASCII).
 * @property {Date|null} referenceTimestamp - Time at which the local clock was last set.
 * @property {Date|null} originTimestamp - Time at the client when the request was sent.
 * @property {Date|null} receiveTimestamp - Time at the server when the request was received.
 * @property {Date|null} transmitTimestamp - Time at the server when the response was sent.
 */
const NTP = (() => {
  // Byte 0 of an NTP packet packs LI(2) | VN(3) | Mode(3) MSB->LSB.
  // struct-compile places the first declared field in the lowest bits, so
  // the order is reversed to keep the on-wire MSB ordering correct.
  const { Layer, proto } = makeLayer('NTP', `
    //@NE
    struct NTPHeader {
      uint8_t mode:3, vn:3, li:2;
      uint8_t stratum;
      int8_t  poll;
      int8_t  precision;
      uint32_t rootDelay;
      uint32_t rootDispersion;
      uint32_t referenceId;
      uint8_t  referenceTimestamp[8];
      uint8_t  originTimestamp[8];
      uint8_t  receiveTimestamp[8];
      uint8_t  transmitTimestamp[8];
    } __attribute__(packed);
  `, {
    osi: OsiModelLayers.Application,
  });

  for (const f of tsFields) {
    attach.virtualField(proto, f, ntpTimestamp(tsOffsets[f]));
  }

  // struct-compile coerces Date instances to strings when writing the raw
  // 8-byte timestamp arrays, which would corrupt the buffer. Strip
  // timestamp fields out of `data` before calling super.merge and route
  // them through the virtual setters which encode NTP 64-bit format.
  const baseMerge = proto.merge;
  proto.merge = function(data) {
    if (data == null) return;
    if (Buffer.isBuffer(data)) {
      baseMerge.call(this, data);
      return;
    }
    const stripped = { ...data };
    for (const f of tsFields) delete stripped[f];
    baseMerge.call(this, stripped);
    for (const f of tsFields) {
      if (data[f] !== undefined) this[f] = data[f];
    }
  };

  // Build the object manually instead of calling super.toObject(): the
  // latter would walk the struct field list and read the timestamp byte
  // arrays as raw bytes (or invoke our Date-returning getters which
  // struct-compile then tries to treat as byte arrays).
  proto.toObject = function() {
    return {
      li: this.li,
      vn: this.vn,
      mode: this.mode,
      stratum: this.stratum,
      poll: this.poll,
      precision: this.precision,
      rootDelay: this.rootDelay,
      rootDispersion: this.rootDispersion,
      referenceId: this.referenceId,
      referenceTimestamp: this.referenceTimestamp,
      originTimestamp: this.originTimestamp,
      receiveTimestamp: this.receiveTimestamp,
      transmitTimestamp: this.transmitTimestamp,
    };
  };

  attach.defaults(proto, {
    li: 0,
    vn: 4,
    mode: 3,
    stratum: 0,
  });

  return Layer;
})();

module.exports = { NTP };