Tutorial: Create NTP dissector package

Note: This is a simplified example for an explanation and does not cover the whole NTP specification.

Please refer to NTP (Network Time Protocol) RFC5905 for the protocol details.

1. Create package files with Dripcap CLI

Make sure that we use --tool option to add driptool package as a dependency.

$ dripcap create-package --tool
create-package: Name: dripcap-ntp
create-package: Description: NTP dissector
create-package: Version: (1.0.0)
create-package: License: (MIT)
✔ Package created at ~/.dripcap/packages/dripcap-ntp

2. Create ntp.es

$ cd ~/.dripcap/packages/dripcap-ntp
$ touch ntp.es
$ ls
main.es        node_modules    ntp.es        package.json

# edit ntp.es with your favorite editor :)

3. Capture UDP packets

For simplicity, we will deal with only NTP over UDP/IPv4.

export default class NTPDissector {
  static get namespaces() {
    return [
      '::Ethernet::IPv4::UDP'
    ]
  }
}

4. Import dependencies

Put the following import statements at the top of utp.es.

Note: dripcap package provides built-in dissector classes.

import { Layer, Item, Value } from 'dripcap';
import Enum from 'driptool/enum';
import { IPv4Address } from 'driptool/ipv4';

4. analyze() method

Add analyze method. This is an entry point of the dissection process.

Now we created the minimal structure for a dissector class.

import { Layer, Item, Value } from 'dripcap';
import Enum from 'driptool/enum';
import { IPv4Address } from 'driptool/ipv4';

export default class NTPDissector {
  static get namespaces() {
    return [
      '::Ethernet::IPv4::UDP'
    ];
  }

  analyze(packet, parentLayer) {

  }
}

5. Filter by the port number

Strictly, we could not directly determine which UDP packet contains NTP because UDP header does not indicate the protocol type of the payload.

One of the easiest method is checking if the UDP port number is 123.

analyze(packet, parentLayer) {
  // parentLayer points to the UDP header

  if (parentLayer.getValue('srcPort').data !== 123 ||
      parentLayer.getValue('dstPort').data !== 123) {
    return;
  }

  // The packet is certainly NTP.
}

If analyze functions returns nothing (undefined), the dissector just discards captured UDP packets.

6. Define a layer object

    let layer = {
      namespace: parentLayer.namespace + '::NTP',
      name: 'NTP',
      id: 'ntp',
      items: []
    };

7. Read Leap Indicator

Value Meaning
0 no warning
1 last minute of the day has 61 seconds
2 last minute of the day has 59 seconds
3 unknown (clock unsynchronized)

From RFC 5905 §7.3

    // parentLayer.payload points a Buffer of the payload
    let head = parentLayer.payload.readUInt8(0);

    let liTable = {
      0: 'no warning',
      1: 'last minute of the day has 61 seconds',
      2: 'last minute of the day has 59 seconds',
      3: 'unknown (clock unsynchronized)',
    };

    let leapIndicator = parentLayer.payload.readUInt8(0) >> 6;
    let li = new Enum(liTable, leapIndicator);
    layer.items.push({
      name: 'Leap Indicator',
      id: 'li',
      range: '0:1',
      value: leapIndicator,
      summary: li.toString()
    });

8. Read Version, Mode, Stratum, Poll Interval and Precision

Value Meaning
0 reserved
1 symmetric active
2 symmetric passive
3 client
4 server
5 broadcast
6 NTP control message
7 reserved for private use

From RFC 5905 §7.3

    let version = (head >> 3) & 0b111;
    layer.items.push({
      name: 'Version',
      id: 'version',
      range: '0:1',
      value: version
    });

    let modeTable = {
      0: 'reserved',
      1: 'symmetric active',
      2: 'symmetric passive',
      3: 'client',
      4: 'server',
      5: 'broadcast',
      6: 'NTP control message',
      7: 'reserved for private use',
    };

    let modeNumber = head & 0b111;
    let mode = new Enum(modeTable, modeNumber);
    layer.items.push({
      name: 'Mode',
      id: 'mode',
      range: '0:1',
      value: modeNumber,
      summary: mode.toString()
    });

    let stratum = parentLayer.payload.readUInt8(1);
    layer.items.push({
      name: 'Stratum',
      id: 'stratum',
      range: '1:2',
      value: stratum
    });

    let pollInterval = parentLayer.payload.readInt8(2);
    layer.items.push({
      name: 'Poll interval',
      id: 'pollInterval',
      range: '2:3',
      value: pollInterval
    });

    let precision = parentLayer.payload.readInt8(3);
    layer.items.push({
      name: 'Precision',
      id: 'precision',
      range: '3:4',
      value: precision
    });

9. Read RootDelay, Root Dispersion and Identifier

       0                   1                   2                   3
       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |          Seconds              |           Fraction            |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                               NTP Short Format

From RFC 5905 §6

    let rootDelay = parentLayer.payload.readInt16BE(4) +
                    parentLayer.payload.readUInt16BE(6) / 65536;

    layer.items.push({
      name: 'Root delay',
      id: 'rootDelay',
      range: '4:8',
      value: rootDelay
    });

    let rootDispersion = parentLayer.payload.readInt16BE(8) +
                         parentLayer.payload.readUInt16BE(10) / 65536;

    layer.items.push({
      name: 'Root dispersion',
      id: 'rootDispersion',
      range: '8:12',
      value: rootDispersion
    });

    let identifier = parentLayer.payload.slice(12, 16);

    layer.items.push({
      name: 'Reference clock identifier',
      id: 'identifier',
      range: '12:16',
      value: stratum >= 2 ? IPv4Address(identifier) : identifier
    });

10. Read Reference Timestamp, Originate Timestamp, Receive Timestamp and Transmit Timestamp

       0                   1                   2                   3
       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                            Seconds                            |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                            Fraction                           |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                             NTP Timestamp Format

From RFC 5905 §6

    const fraction = 4294967296;
    const unixOffset = 2208988800;

    let referenceTs = parentLayer.payload.readUInt32BE(16) +
                      parentLayer.payload.readUInt32BE(20) / fraction;

    layer.items.push({
      name: 'Reference timestamp',
      id: 'referenceTs',
      range: '16:24',
      value: new Date((referenceTs - unixOffset) * 1000)
    });

    let originateTs = parentLayer.payload.readUInt32BE(24) +
                      parentLayer.payload.readUInt32BE(28) / fraction;

    layer.items.push({
      name: 'Originate timestamp',
      id: 'originateTs',
      range: '24:32',
      value: new Date((originateTs - unixOffset) * 1000)
    });

    let receiveTs = parentLayer.payload.readUInt32BE(32) +
                    parentLayer.payload.readUInt32BE(36) / fraction;

    layer.items.push({
      name: 'Receive timestamp',
      id: 'receiveTs',
      range: '32:40',
      value: new Date((receiveTs - unixOffset) * 1000)
    });

    let transmitTs = parentLayer.payload.readUInt32BE(40) +
                     parentLayer.payload.readUInt32BE(48) / fraction;

    layer.items.push({
      name: 'Transmit timestamp',
      id: 'transmitTs',
      range: '40:48',
      value: new Date((transmitTs - unixOffset) * 1000)
    });

11. Returns the Layer instance

Return the Layer instance to notify that the packet had been succesfully decoded as NTP.

    return new Layer(layer);

We can also return multiple layers at once with an array (rarely used).

    return [new Layer(layer)];

12. Complated ntp.es

import {Layer, Item, Value} from 'dripcap';
import Enum from 'driptool/enum';
import {IPv4Address} from 'driptool/ipv4';

export default class NTPDissector {
  static get namespaces() {
    return [
    '::Ethernet::IPv4::UDP',
    '::Ethernet::IPv6::UDP'
    ];
  }

  analyze(packet, parentLayer) {
    if (parentLayer.getValue('srcPort').data !== 123 ||
        parentLayer.getValue('dstPort').data !== 123) {
      return;
    }

    let layer = {
      namespace: '::Ethernet::IPv4::UDP::NTP',
      name: 'NTP',
      id: 'ntp',
      items: []
    };

    let head = parentLayer.payload.readUInt8(0);

    let liTable = {
      0: 'no warning',
      1: 'last minute of the day has 61 seconds',
      2: 'last minute of the day has 59 seconds',
      3: 'unknown (clock unsynchronized)',
    };

    let leapIndicator = parentLayer.payload.readUInt8(0) >> 6;
    let li = new Enum(liTable, leapIndicator);
    layer.items.push({
      name: 'Leap Indicator',
      id: 'li',
      range: '0:1',
      value: leapIndicator,
      summary: li.toString()
    });

    let version = (head >> 3) & 0b111;
    layer.items.push({
      name: 'Version',
      id: 'version',
      range: '0:1',
      value: version
    });

    let modeTable = {
      0: 'reserved',
      1: 'symmetric active',
      2: 'symmetric passive',
      3: 'client',
      4: 'server',
      5: 'broadcast',
      6: 'NTP control message',
      7: 'reserved for private use',
    };

    let modeNumber = head & 0b111;
    let mode = new Enum(modeTable, modeNumber);
    layer.items.push({
      name: 'Mode',
      id: 'mode',
      range: '0:1',
      value: modeNumber,
      summary: mode.toString()
    });

    let stratum = parentLayer.payload.readUInt8(1);
    layer.items.push({
      name: 'Stratum',
      id: 'stratum',
      range: '1:2',
      value: stratum
    });

    let pollInterval = parentLayer.payload.readInt8(2);
    layer.items.push({
      name: 'Poll interval',
      id: 'pollInterval',
      range: '2:3',
      value: pollInterval
    });

    let precision = parentLayer.payload.readInt8(3);
    layer.items.push({
      name: 'Precision',
      id: 'precision',
      range: '3:4',
      value: precision
    });

    let rootDelay = parentLayer.payload.readInt16BE(4) +
                    parentLayer.payload.readUInt16BE(6) / 65536;

    layer.items.push({
      name: 'Root delay',
      id: 'rootDelay',
      range: '4:8',
      value: rootDelay
    });

    let rootDispersion = parentLayer.payload.readInt16BE(8) +
                         parentLayer.payload.readUInt16BE(10) / 65536;

    layer.items.push({
      name: 'Root dispersion',
      id: 'rootDispersion',
      range: '8:12',
      value: rootDispersion
    });

    let identifier = parentLayer.payload.slice(12, 16);

    layer.items.push({
      name: 'Reference clock identifier',
      id: 'identifier',
      range: '12:16',
      value: stratum >= 2 ? IPv4Address(identifier) : identifier
    });

    const fraction = 4294967296;
    const unixOffset = 2208988800;

    let referenceTs = parentLayer.payload.readUInt32BE(16) +
                      parentLayer.payload.readUInt32BE(20) / fraction;

    layer.items.push({
      name: 'Reference timestamp',
      id: 'referenceTs',
      range: '16:24',
      value: new Date((referenceTs - unixOffset) * 1000)
    });

    let originateTs = parentLayer.payload.readUInt32BE(24) +
                      parentLayer.payload.readUInt32BE(28) / fraction;

    layer.items.push({
      name: 'Originate timestamp',
      id: 'originateTs',
      range: '24:32',
      value: new Date((originateTs - unixOffset) * 1000)
    });

    let receiveTs = parentLayer.payload.readUInt32BE(32) +
                    parentLayer.payload.readUInt32BE(36) / fraction;

    layer.items.push({
      name: 'Receive timestamp',
      id: 'receiveTs',
      range: '32:40',
      value: new Date((receiveTs - unixOffset) * 1000)
    });

    let transmitTs = parentLayer.payload.readUInt32BE(40) +
                     parentLayer.payload.readUInt32BE(48) / fraction;

    layer.items.push({
      name: 'Transmit timestamp',
      id: 'transmitTs',
      range: '40:48',
      value: new Date((transmitTs - unixOffset) * 1000)
    });

    return new Layer(layer);
  }
}

13. Register the ntp dissector in main.es

import { Session } from 'dripcap';

export default class Ntp {
  async activate() {
    Session.registerDissector(`${__dirname}/ntp.es`);
  }

  async deactivate() {
    Session.unregisterDissector(`${__dirname}/ntp.es`);
  }
}

results matching ""

    No results matching ""