layers_ICMPv6.js

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

/**
 * ICMPv6 protocol layer
 * @class
 * @implements {Layer}
 * @property {number} type - ICMPv6 message type (see ICMPv6Types).
 * @property {number} code - ICMPv6 message code.
 * @property {number} checksum - ICMPv6 checksum.
 */
const ICMPv6 = (() => {
  const { ICMPv6EchoHeader } = compile(`
    //@NE
    struct ICMPv6EchoHeader {
      uint16_t id;
      uint16_t sequence;
    } __attribute__(packed);
  `);
  const { ICMPv6DestUnreachableHeader } = compile(`
    //@NE
    struct ICMPv6DestUnreachableHeader {
      uint32_t unused;
    } __attribute__(packed);
  `);
  const { ICMPv6TimeExceededHeader } = compile(`
    //@NE
    struct ICMPv6TimeExceededHeader {
      uint32_t unused;
    } __attribute__(packed);
  `);
  const { ICMPv6ParamProblemHeader } = compile(`
    //@NE
    struct ICMPv6ParamProblemHeader {
      uint32_t pointer;
    } __attribute__(packed);
  `);

  const tailLengthFor = (type) => {
    switch (type) {
      case ICMPv6Types.EchoRequest:
      case ICMPv6Types.EchoReply:
        return ICMPv6EchoHeader.prototype.config.length;
      case ICMPv6Types.DestinationUnreachable:
        return ICMPv6DestUnreachableHeader.prototype.config.length;
      case ICMPv6Types.TimeExceeded:
        return ICMPv6TimeExceededHeader.prototype.config.length;
      case ICMPv6Types.ParameterProblem:
        return ICMPv6ParamProblemHeader.prototype.config.length;
      default:
        return 0;
    }
  };

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

  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(ICMPv6Types).find(([, v]) => v === this.type)?.[0] || 'Unknown'; },
  });
  attach.virtualField(proto, 'isEchoRequest', { get() { return this.type === ICMPv6Types.EchoRequest; } });
  attach.virtualField(proto, 'isEchoReply', { get() { return this.type === ICMPv6Types.EchoReply; } });
  attach.virtualField(proto, 'isDestinationUnreachable', { get() { return this.type === ICMPv6Types.DestinationUnreachable; } });
  attach.virtualField(proto, 'isTimeExceeded', { get() { return this.type === ICMPv6Types.TimeExceeded; } });
  attach.virtualField(proto, 'isParameterProblem', { get() { return this.type === ICMPv6Types.ParameterProblem; } });

  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 ICMPv6EchoHeader(tail).merge(data);
    else if (this.isDestinationUnreachable) new ICMPv6DestUnreachableHeader(tail).merge(data);
    else if (this.isTimeExceeded) new ICMPv6TimeExceededHeader(tail).merge(data);
    else if (this.isParameterProblem) new ICMPv6ParamProblemHeader(tail).merge(data);
  };

  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,
    };
    const tail = this._buf.subarray(baseLength);
    if (this.isEchoRequest || this.isEchoReply) {
      const h = new ICMPv6EchoHeader(tail);
      return { ...base, id: h.id, sequence: h.sequence };
    }
    if (this.isDestinationUnreachable) {
      const h = new ICMPv6DestUnreachableHeader(tail);
      return { ...base, unused: h.unused };
    }
    if (this.isTimeExceeded) {
      const h = new ICMPv6TimeExceededHeader(tail);
      return { ...base, unused: h.unused };
    }
    if (this.isParameterProblem) {
      const h = new ICMPv6ParamProblemHeader(tail);
      return { ...base, pointer: h.pointer };
    }
    return base;
  };

  // Empty table -> always Payload via byField fallback. ICMPv6 checksum is
  // an IPv6 pseudo-header concern handled (or not) by the IPv6 layer.
  attach.dispatch(proto, byField('type', {}));

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

  return Layer;
})();

module.exports = { ICMPv6 };