layers_ICMP.js

const { compile } = require('struct-compile');

const { checksums } = require('#lib/bindings');

const { OsiModelLayers } = require('./osi');
const { ICMPTypes } = require('./enums');
const child = require('./child');
const mixins = require('./mixins');

const { ICMPHeader } = compile(`
  //@NE
  struct ICMPHeader {
    uint8_t type;
    uint8_t code;
    uint16_t checksum;
  } __attribute__(packed);
`);

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);
`);

const childProto = {};
const lookupChild = child.lookupChild(childProto);
const lookupKey = child.lookupKey(childProto);

/**
 * 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
 */
class ICMP extends ICMPHeader {
  name = 'ICMP';
  static osi = OsiModelLayers.Network;
  osi = OsiModelLayers.Network;

  /**
   * @param {Buffer|Object} data - Input buffer or object with protocol fields
   * @param {Object} opts - Options for the layer
   */
  constructor(data = {}, opts = {}) {
    const isObj = typeof data == 'object' && !Buffer.isBuffer(data);
    if (isObj) {
      super(data, { toAlloc: ICMP.toAlloc(data) });
    }
    else {
      super(data);
    }

    mixins.ctor(this, data, opts);

    if (isObj) {
      this.merge(data);
    }
  }

  _customPayload() {
    return this._buf.subarray(ICMPHeader.prototype.config.length);
  }

  merge(data) {
    super.merge(data);

    const tailBuffer = this._customPayload();

    if (this.isEchoRequest || this.isEchoReply) {
      const echo = new ICMPEchoHeader(tailBuffer);
      echo.merge(data);
    } else if (this.isTimestampRequest || this.isTimestampReply) {
      const timestamp = new ICMPTimestampHeader(tailBuffer);
      timestamp.merge(data);
    } else if (this.isDestinationUnreachable) {
      const unreachable = new ICMPDestUnreachableHeader(tailBuffer);
      unreachable.merge(data);
    } else if (this.isTimeExceeded) {
      const timeExceeded = new ICMPTimeExceededHeader(tailBuffer);
      timeExceeded.merge(data);
    } else if (this.isParameterProblem) {
      const paramProblem = new ICMPParamProblemHeader(tailBuffer);
      paramProblem.merge(data);
    }
  }

  static _toAllocForType(type) {
    let customPayloadLength = 0;
    switch (type) {
      case ICMPTypes.EchoRequest:
      case ICMPTypes.EchoReply:
        customPayloadLength = ICMPEchoHeader.prototype.config.length;
        break;
      case ICMPTypes.TimestampRequest:
      case ICMPTypes.TimestampReply:
        customPayloadLength = ICMPTimestampHeader.prototype.config.length;
        break;
      case ICMPTypes.DestinationUnreachable:
        customPayloadLength = ICMPDestUnreachableHeader.prototype.config.length;
        break;
      case ICMPTypes.TimeExceeded:
        customPayloadLength = ICMPTimeExceededHeader.prototype.config.length;
        break;
      case ICMPTypes.ParameterProblem:
        customPayloadLength = ICMPParamProblemHeader.prototype.config.length;
        break;
    }

    return customPayloadLength + ICMPHeader.prototype.config.length;
  }

  /**
   * Get the required allocation size based on ICMP message type
   * @param {Object} data - The data object containing type and other fields
   * @returns {number} Required allocation size
   */
  static toAlloc(data) {
    return ICMP._toAllocForType(data.type);
  }

  get length() {
    try {
      return ICMP._toAllocForType(this.type);
    } catch(err) {
      return super.length;
    }
  }

  /**
   * Get the name of the ICMP message type
   * @returns {string} Name of the ICMP message type
   */
  get typeName() {
    return Object.entries(ICMPTypes).find(([_, value]) => value === this.type)?.[0] || 'Unknown';
  }

  /**
   * Check if this is an Echo Request message
   * @returns {boolean}
   */
  get isEchoRequest() {
    return this.type === ICMPTypes.EchoRequest;
  }

  /**
   * Check if this is an Echo Reply message
   * @returns {boolean}
   */
  get isEchoReply() {
    return this.type === ICMPTypes.EchoReply;
  }

  /**
   * Check if this is a Destination Unreachable message
   * @returns {boolean}
   */
  get isDestinationUnreachable() {
    return this.type === ICMPTypes.DestinationUnreachable;
  }

  /**
   * Check if this is a Time Exceeded message
   * @returns {boolean}
   */
  get isTimeExceeded() {
    return this.type === ICMPTypes.TimeExceeded;
  }

  /**
   * Check if this is a Parameter Problem message
   * @returns {boolean}
   */
  get isParameterProblem() {
    return this.type === ICMPTypes.ParameterProblem;
  }

  /**
   * Check if this is a Timestamp Request message
   * @returns {boolean}
   */
  get isTimestampRequest() {
    return this.type === ICMPTypes.TimestampRequest;
  }

  /**
   * Check if this is a Timestamp Reply message
   * @returns {boolean}
   */
  get isTimestampReply() {
    return this.type === ICMPTypes.TimestampReply;
  }

  toObject() {
    const base = {
      ...super.toObject(),
      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 tailBuffer = this._customPayload();

    if (this.isEchoRequest || this.isEchoReply) {
      const echo = new ICMPEchoHeader(tailBuffer);
      return {
        ...base,
        id: echo.id,
        sequence: echo.sequence,
        timestamp: echo.timestamp
      };
    }

    if (this.isTimestampRequest || this.isTimestampReply) {
      const timestamp = new ICMPTimestampHeader(tailBuffer);
      return {
        ...base,
        id: timestamp.id,
        sequence: timestamp.sequence,
        originateTimestamp: timestamp.originateTimestamp,
        receiveTimestamp: timestamp.receiveTimestamp,
        transmitTimestamp: timestamp.transmitTimestamp
      };
    }

    if (this.isDestinationUnreachable) {
      const unreachable = new ICMPDestUnreachableHeader(tailBuffer);
      return {
        ...base,
        unused: unreachable.unused,
        nextHopMTU: unreachable.nextHopMTU
      };
    }

    if (this.isTimeExceeded) {
      const timeExceeded = new ICMPTimeExceededHeader(tailBuffer);
      return {
        ...base,
        unused: timeExceeded.unused
      };
    }

    if (this.isParameterProblem) {
      const paramProblem = new ICMPParamProblemHeader(tailBuffer);
      return {
        ...base,
        errorOctetPointer: paramProblem.errorOctetPointer,
        unused: paramProblem.unused
      };
    }

    return base;
  }

  /**
   * Calculates and updates the checksum for the IPv4 layer.
   * This method mutates the object by setting the `checksum` property
   * based on the current state of the `buffer`.
   */
  calculateChecksum() {
    this.checksum = checksums.ip(this.buffer.subarray(0, this.length));
  }

  checksums(obj) {
    if (!obj.checksum) {
      this.calculateChecksum();
    }
  }

  defaults(obj, layers) {
    if (!obj.type) {
      this.type = ICMPTypes.EchoRequest;
    }
    if (!obj.code) {
      this.code = 0;
    }
  }

  nextProto(layers) {
    return lookupChild(layers, this.type, this);
  }
}

module.exports = { ICMP };