diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index bebdd6a5..c76e50ef 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -230,6 +230,7 @@ "DNS over HTTPS", "Strip HTTP headers", "Dechunk HTTP response", + "Parse Ethernet frame", "Parse User Agent", "Parse IP range", "Parse IPv6 address", diff --git a/src/core/operations/ParseEthernetFrame.mjs b/src/core/operations/ParseEthernetFrame.mjs new file mode 100644 index 00000000..9dac5d57 --- /dev/null +++ b/src/core/operations/ParseEthernetFrame.mjs @@ -0,0 +1,115 @@ +/** + * @author tedk [tedk@ted.do] + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Utils from "../Utils.mjs"; +import {fromHex, toHex} from "../lib/Hex.mjs"; + +/** + * Parse Ethernet frame operation + */ +class ParseEthernetFrame extends Operation { + + /** + * ParseEthernetFrame constructor + */ + constructor() { + super(); + + this.name = "Parse Ethernet frame"; + this.module = "Default"; + this.description = "Parses an Ethernet frame and either shows the deduced values (Source and destination MAC, VLANs) or returns the packet data.

Good for use in conjunction with the Parse IPv4, and Parse TCP/UDP recipes."; + this.infoURL = "https://en.wikipedia.org/wiki/Ethernet_frame#Frame_%E2%80%93_data_link_layer"; + this.inputType = "string"; + this.outputType = "html"; + this.args = [ + { + name: "Input type", + type: "option", + value: [ + "Raw", "Hex" + ], + defaultIndex: 0, + }, + { + name: "Return type", + type: "option", + value: [ + "Text output", "Packet data", "Packet data (hex)", + ], + defaultIndex: 0, + } + ]; + } + + + /** + * @param {string} input + * @param {Object[]} args + * @returns {html} + */ + run(input, args) { + const format = args[0]; + const outputFormat = args[1]; + + if (format === "Hex") { + input = fromHex(input); + } else if (format === "Raw") { + input = new Uint8Array(Utils.strToArrayBuffer(input)); + } else { + throw new OperationError("Invalid input format selected."); + } + + const destinationMac = input.slice(0, 6); + const sourceMac = input.slice(6, 12); + + let offset = 12; + const vlans = []; + + while (offset < input.length) { + const ethType = Utils.byteArrayToChars(input.slice(offset, offset+2)); + offset += 2; + + + if (ethType === "\x08\x00") { + break; + } else if (ethType === "\x81\x00" || ethType === "\x88\xA8") { + // Parse the VLAN tag: + // [0000] 0000 0000 0000 + // ^^^ PRIO - Ignored + // ^ DEI - Ignored + // ^^^^ ^^^^ ^^^^ VLAN ID + const vlanTag = input.slice(offset+2, offset+4); + vlans.push((vlanTag[0] & 0b00001111) << 4 | vlanTag[1]); + + offset += 2; + } else { + break; + } + } + + const packetData = input.slice(offset); + + if (outputFormat === "Packet data") { + return Utils.byteArrayToChars(packetData); + } else if (outputFormat === "Packet data (hex)") { + return toHex(packetData); + } else if (outputFormat === "Text output") { + let retval = `Source MAC: ${toHex(sourceMac, ":")}\nDestination MAC: ${toHex(destinationMac, ":")}\n`; + if (vlans.length > 0) { + retval += `VLAN: ${vlans.join(", ")}\n`; + } + retval += `Data:\n${toHex(packetData)}`; + return retval; + } + + } + + +} + +export default ParseEthernetFrame; diff --git a/src/core/operations/ParseIPv4Header.mjs b/src/core/operations/ParseIPv4Header.mjs index 84351cdc..4eed5d46 100644 --- a/src/core/operations/ParseIPv4Header.mjs +++ b/src/core/operations/ParseIPv4Header.mjs @@ -33,6 +33,12 @@ class ParseIPv4Header extends Operation { "name": "Input format", "type": "option", "value": ["Hex", "Raw"] + }, + { + "name": "Output format", + "type": "option", + "value": ["Table", "Data (hex)", "Data (raw)"], + defaultIndex: 0, } ]; } @@ -44,6 +50,8 @@ class ParseIPv4Header extends Operation { */ run(input, args) { const format = args[0]; + const outputFormat = args[1]; + let output; if (format === "Hex") { @@ -98,7 +106,10 @@ class ParseIPv4Header extends Operation { checksumResult = givenChecksum + " (incorrect, should be " + correctChecksum + ")"; } - output = ` + const data = input.slice(ihl * 4); + + if (outputFormat === "Table") { + output = `
FieldValue
@@ -116,13 +127,19 @@ class ParseIPv4Header extends Operation { -`; + +`; - if (ihl > 5) { - output += ``; + if (ihl > 5) { + output += ``; + } + + return output + "
FieldValue
Version${version}
Internet Header Length (IHL)${ihl} (${ihl * 4} bytes)
Differentiated Services Code Point (DSCP)${dscp}
Protocol${protocol}, ${protocolInfo.protocol} (${protocolInfo.keyword})
Header checksum${checksumResult}
Source IP address${ipv4ToStr(srcIP)}
Destination IP address${ipv4ToStr(dstIP)}
Destination IP address${ipv4ToStr(dstIP)}
Data (hex)${toHex(data)}
Options${toHex(options)}
Options${toHex(options)}
"; + } else if (outputFormat === "Data (hex)") { + return toHex(data); + } else if (outputFormat === "Data (raw)") { + return Utils.byteArrayToChars(data); } - - return output + ""; } } diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index 200c0d3f..782600f2 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -249,7 +249,7 @@ module.exports = { testOpHtml(browser, "Parse colour code", "#000", ".colorpicker-preview", "rgb(0, 0, 0)"); testOpHtml(browser, "Parse DateTime", "01/12/2000 13:00:00", "", /Date: Friday 1st December 2000/); // testOp(browser, "Parse IP range", "test input", "test_output"); - testOpHtml(browser, "Parse IPv4 header", "45 c0 00 c4 02 89 00 00 ff 11 1e 8c c0 a8 0c 01 c0 a8 0c 02", "tr:last-child td:last-child", "192.168.12.2"); + testOpHtml(browser, "Parse IPv4 header", "45 c0 00 c4 02 89 00 00 ff 11 1e 8c c0 a8 0c 01 c0 a8 0c 02", "tr:nth-last-child(2) td:last-child", "192.168.12.2"); // testOp(browser, "Parse IPv6 address", "test input", "test_output"); // testOp(browser, "Parse ObjectID timestamp", "test input", "test_output"); // testOp(browser, "Parse QR Code", "test input", "test_output"); diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 40ce7a2e..1b9c8dbe 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -110,6 +110,7 @@ import "./tests/NetBIOS.mjs"; import "./tests/NormaliseUnicode.mjs"; import "./tests/NTLM.mjs"; import "./tests/OTP.mjs"; +import "./tests/ParseEthernetFrame.mjs"; import "./tests/ParseIPRange.mjs"; import "./tests/ParseObjectIDTimestamp.mjs"; import "./tests/ParseQRCode.mjs"; diff --git a/tests/operations/tests/ParseEthernetFrame.mjs b/tests/operations/tests/ParseEthernetFrame.mjs new file mode 100644 index 00000000..c849e207 --- /dev/null +++ b/tests/operations/tests/ParseEthernetFrame.mjs @@ -0,0 +1,45 @@ +/** + * Parse Ethernet frame tests. + * + * @author tedk [tedk@ted.do] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Parse plain Ethernet frame", + input: "000000000000ffffffffffff08004500", + expectedOutput: "Source MAC: ff:ff:ff:ff:ff:ff\nDestination MAC: 00:00:00:00:00:00\nData:\n45 00", + recipeConfig: [ + { + "op": "Parse Ethernet frame", + "args": ["Hex", "Text output"] + } + ] + }, + // Example PCAP data from: https://packetlife.net/captures/protocol/vlan/ + { + name: "Parse Ethernet frame with one VLAN tag (802.1q)", + input: "01000ccdcdd00013c3dfae188100a0760165aaaa", + expectedOutput: "Source MAC: 00:13:c3:df:ae:18\nDestination MAC: 01:00:0c:cd:cd:d0\nVLAN: 117\nData:\naa aa", + recipeConfig: [ + { + "op": "Parse Ethernet frame", + "args": ["Hex", "Text output"] + } + ] + }, + { + name: "Parse Ethernet frame with two VLAN tags (802.1ad)", + input: "0019aa7de688002155c8f13c810000d18100001408004500", + expectedOutput: "Source MAC: 00:21:55:c8:f1:3c\nDestination MAC: 00:19:aa:7d:e6:88\nVLAN: 16, 128\nData:\n45 00", + recipeConfig: [ + { + "op": "Parse Ethernet frame", + "args": ["Hex", "Text output"] + } + ] + } +]);