layers_ICMP.js

const { compile } = require('struct-compile');
const { OsiModelLayers } = require('./osi');
const { ICMPTypes } = require('./enums');
const { makeLayer, attach, byField } = require('./define');

/**
 * ICMP protocol layer
 * @class
 * @implements {Layer}
 * @property {number} type - ICMP message type (see ICMPTypes)
 * @property {number} code - ICMP message code
 * @property {number} checksum - ICMP checksum
 * @property {string} typeName - Human-readable name of the ICMP message type
 * @property {boolean} isEchoRequest - True for type=8 (EchoRequest).
 * @property {boolean} isEchoReply - True for type=0 (EchoReply).
 * @property {boolean} isDestinationUnreachable - True for type=3.
 * @property {boolean} isTimeExceeded - True for type=11.
 * @property {boolean} isParameterProblem - True for type=12.
 * @property {boolean} isTimestampRequest - True for type=13.
 * @property {boolean} isTimestampReply - True for type=14.
 */
const ICMP = (() => {
  // Sub-headers attached to the base 4-byte ICMP header by message type.
  // Compiled inside the IIFE so they don't leak into the module export.
  const { ICMPEchoHeader } = compile(`
    //@NE
    struct ICMPEchoHeader {
      uint16_t id;
      uint16_t sequence;
      uint64_t timestamp;
    } __attribute__(packed);
  `);
  const { ICMPTimestampHeader } = compile(`
    //@NE
    struct ICMPTimestampHeader {
      uint16_t id;
      uint16_t sequence;
      uint32_t originateTimestamp;
      uint32_t receiveTimestamp;
      uint32_t transmitTimestamp;
    } __attribute__(packed);
  `);
  const { ICMPDestUnreachableHeader } = compile(`
    //@NE
    struct ICMPDestUnreachableHeader {
      uint16_t unused;
      uint16_t nextHopMTU;
    } __attribute__(packed);
  `);
  const { ICMPTimeExceededHeader } = compile(`
    //@NE
    struct ICMPTimeExceededHeader {
      uint32_t unused;
    } __attribute__(packed);
  `);
  const { ICMPParamProblemHeader } = compile(`
    //@NE
    struct ICMPParamProblemHeader {
      uint8_t errorOctetPointer;
      uint8_t unused[3];
    } __attribute__(packed);
  `);

  // Total header size as a function of `type`. Defaults to base 4 bytes
  // (just type/code/checksum) when the message type doesn't carry a
  // structured tail.
  const tailLengthFor = (type) => {
    switch (type) {
      case ICMPTypes.EchoRequest:
      case ICMPTypes.EchoReply:
        return ICMPEchoHeader.prototype.config.length;
      case ICMPTypes.TimestampRequest:
      case ICMPTypes.TimestampReply:
        return ICMPTimestampHeader.prototype.config.length;
      case ICMPTypes.DestinationUnreachable:
        return ICMPDestUnreachableHeader.prototype.config.length;
      case ICMPTypes.TimeExceeded:
        return ICMPTimeExceededHeader.prototype.config.length;
      case ICMPTypes.ParameterProblem:
        return ICMPParamProblemHeader.prototype.config.length;
      default:
        return 0;
    }
  };

  const { Layer, proto, baseLength } = makeLayer('ICMP', `
    //@NE
    struct ICMPHeader {
      uint8_t type;
      uint8_t code;
      uint16_t checksum;
    } __attribute__(packed);
  `, {
    osi: OsiModelLayers.Network,
    length: false,
    toAlloc: (data) => baseLengthFor(data),
  });

  // Bootstrap: makeLayer reads `toAlloc` once and turns it into the static
  // method, so the closure must already be defined when the IIFE runs.
  // We capture baseLength via the destructure above and route through this
  // tiny helper to keep the call site declarative.
  function baseLengthFor(data) {
    return baseLength + tailLengthFor(data && data.type);
  }
  Layer.toAlloc = (data) => baseLengthFor(data);

  // Type-driven length getter on the prototype. Recomputed on every access
  // so changing `type` updates `length` (matches the legacy class).
  Object.defineProperty(proto, 'length', {
    get() {
      try { return baseLength + tailLengthFor(this.type); }
      catch (_e) { return baseLength; }
    },
    configurable: true,
  });

  attach.virtualField(proto, 'typeName', {
    get() { return Object.entries(ICMPTypes).find(([, v]) => v === this.type)?.[0] || 'Unknown'; },
  });
  attach.virtualField(proto, 'isEchoRequest', { get() { return this.type === ICMPTypes.EchoRequest; } });
  attach.virtualField(proto, 'isEchoReply', { get() { return this.type === ICMPTypes.EchoReply; } });
  attach.virtualField(proto, 'isDestinationUnreachable', { get() { return this.type === ICMPTypes.DestinationUnreachable; } });
  attach.virtualField(proto, 'isTimeExceeded', { get() { return this.type === ICMPTypes.TimeExceeded; } });
  attach.virtualField(proto, 'isParameterProblem', { get() { return this.type === ICMPTypes.ParameterProblem; } });
  attach.virtualField(proto, 'isTimestampRequest', { get() { return this.type === ICMPTypes.TimestampRequest; } });
  attach.virtualField(proto, 'isTimestampReply', { get() { return this.type === ICMPTypes.TimestampReply; } });

  // After struct-compile sets the base type/code/checksum, dispatch to the
  // matching sub-header to populate the type-specific tail.
  const baseMerge = proto.merge;
  proto.merge = function(data) {
    baseMerge.call(this, data);
    if (Buffer.isBuffer(data)) return;
    const tail = this._buf.subarray(baseLength);
    if (this.isEchoRequest || this.isEchoReply) new ICMPEchoHeader(tail).merge(data);
    else if (this.isTimestampRequest || this.isTimestampReply) new ICMPTimestampHeader(tail).merge(data);
    else if (this.isDestinationUnreachable) new ICMPDestUnreachableHeader(tail).merge(data);
    else if (this.isTimeExceeded) new ICMPTimeExceededHeader(tail).merge(data);
    else if (this.isParameterProblem) new ICMPParamProblemHeader(tail).merge(data);
  };

  // Custom toObject mirrors merge: base header + type marker + the
  // type-specific tail header's fields.
  const baseToObject = proto.toObject;
  proto.toObject = function() {
    const base = {
      ...baseToObject.call(this),
      typeName: this.typeName,
      isEchoRequest: this.isEchoRequest,
      isEchoReply: this.isEchoReply,
      isDestinationUnreachable: this.isDestinationUnreachable,
      isTimeExceeded: this.isTimeExceeded,
      isParameterProblem: this.isParameterProblem,
      isTimestampRequest: this.isTimestampRequest,
      isTimestampReply: this.isTimestampReply,
    };
    const tail = this._buf.subarray(baseLength);
    if (this.isEchoRequest || this.isEchoReply) {
      const h = new ICMPEchoHeader(tail);
      return { ...base, id: h.id, sequence: h.sequence, timestamp: h.timestamp };
    }
    if (this.isTimestampRequest || this.isTimestampReply) {
      const h = new ICMPTimestampHeader(tail);
      return {
        ...base,
        id: h.id,
        sequence: h.sequence,
        originateTimestamp: h.originateTimestamp,
        receiveTimestamp: h.receiveTimestamp,
        transmitTimestamp: h.transmitTimestamp,
      };
    }
    if (this.isDestinationUnreachable) {
      const h = new ICMPDestUnreachableHeader(tail);
      return { ...base, unused: h.unused, nextHopMTU: h.nextHopMTU };
    }
    if (this.isTimeExceeded) {
      const h = new ICMPTimeExceededHeader(tail);
      return { ...base, unused: h.unused };
    }
    if (this.isParameterProblem) {
      const h = new ICMPParamProblemHeader(tail);
      return { ...base, errorOctetPointer: h.errorOctetPointer, unused: h.unused };
    }
    return base;
  };

  attach.checksum.ip(proto);

  // No structured child protocols: empty table -> always Payload via
  // byField's fallback.
  attach.dispatch(proto, byField('type', {}));

  attach.defaults(proto, {
    type: ICMPTypes.EchoRequest,
    code: 0,
  });

  return Layer;
})();

module.exports = { ICMP };