├── .gitignore ├── src ├── lib.rs ├── error.rs ├── template.rs ├── dissector.rs ├── keyword.rs ├── schema.rs ├── validator.rs └── generator.rs ├── .github └── workflows │ ├── rust-audit.yml │ ├── rust-coverage.yml │ └── rust-ci.yml ├── Cargo.toml ├── LICENSE ├── example └── example_dissector.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod dissector; 2 | mod keyword; 3 | mod schema; 4 | mod template; 5 | 6 | pub mod error; 7 | pub mod generator; 8 | pub mod validator; 9 | -------------------------------------------------------------------------------- /.github/workflows/rust-audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | on: 3 | schedule: 4 | - cron: '0 0 * * 0' 5 | jobs: 6 | all: 7 | name: All 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions-rs/audit-check@v1 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wirdigen" 3 | version = "0.3.0" 4 | authors = ["Aurélien VERNIZEAU "] 5 | edition = "2021" 6 | description = "LUA Wireshark dissector validator/generator from JSON" 7 | readme = "README.md" 8 | homepage = "https://github.com/Cerclique/wirdigen/" 9 | repository = "https://github.com/Cerclique/wirdigen/" 10 | license-file = "LICENSE" 11 | keywords = ["wireshark", "dissector", "lua"] 12 | 13 | [dependencies] 14 | serde = { version = "1.0.136", features = ["derive"] } 15 | serde_json = "1.0.79" 16 | anyhow = "1.0.56" 17 | thiserror = "1.0.30" 18 | jsonschema = "0.15.1" 19 | chrono = "0.4.19" 20 | regex = "1.5.5" 21 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Enum of possible error returned by modules 2 | 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum WirdigenError { 7 | /// Error inherited from std::io::Error 8 | #[error(transparent)] 9 | IOError(#[from] std::io::Error), 10 | 11 | /// Error inherited from serde_json::Error 12 | #[error(transparent)] 13 | SerdeJsonError(#[from] serde_json::Error), 14 | 15 | /// Error inherited from regex::Error 16 | #[error(transparent)] 17 | RegexError(#[from] regex::Error), 18 | 19 | /// String descripton of error returned by jsonschema::ValidationError 20 | #[error("Failed to compile JSON schema")] 21 | JSONSchemaCompilation(String), 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/rust-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | check: 9 | name: Rust project 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Install stable toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | override: true 20 | 21 | - name: Run cargo-tarpaulin 22 | uses: actions-rs/tarpaulin@v0.1 23 | with: 24 | version: '0.15.0' 25 | args: '-- --test-threads 1' 26 | 27 | - name: Upload to codecov.io 28 | uses: codecov/codecov-action@v1.0.2 29 | with: 30 | token: ${{secrets.CODECOV_TOKEN}} 31 | 32 | - name: Archive code coverage results 33 | uses: actions/upload-artifact@v1 34 | with: 35 | name: code-coverage-report 36 | path: cobertura.xml -------------------------------------------------------------------------------- /.github/workflows/rust-ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | cargo_build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Build 18 | run: cargo build --verbose 19 | cargo_test: 20 | needs: cargo_build 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Build 25 | run: cargo test --verbose 26 | cargo_clippy: 27 | needs: [cargo_build, cargo_test] 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions-rs/toolchain@v1 32 | with: 33 | toolchain: nightly 34 | components: clippy 35 | override: true 36 | - uses: actions-rs/clippy-check@v1 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | args: --all-features 40 | -------------------------------------------------------------------------------- /src/template.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const DISSECTOR_TEMPLATE: &str = r#" 2 | --[[ 3 | Author: %PROJECT_NAME% 4 | Date: %DATE% 5 | Description: Wireshark Dissector for "%DISSECTOR_NAME%" 6 | ]]-- 7 | 8 | local %DISSECTOR_NAME% = Proto("%DISSECTOR_NAME%", "%DISSECTOR_NAME% Protocol") 9 | 10 | -- ValueString Declaration Section 11 | %VALUESTRING_DECLARATION% 12 | 13 | -- Fields Declaration Section 14 | %FIELDS_DECLARATION% 15 | 16 | %DISSECTOR_NAME%.fields = { 17 | %FIELDS_LIST% 18 | } 19 | 20 | -- Dissector Callback Declaration 21 | function %DISSECTOR_NAME%.dissector(buffer, pinfo, tree) 22 | local length = buffer:len() 23 | if length == 0 then return end 24 | 25 | -- Adds dissector name to protocol column 26 | pinfo.cols.protocol = %DISSECTOR_NAME%.name 27 | 28 | -- Creates the subtree 29 | local subtree = tree:add(%DISSECTOR_NAME%, buffer(),"%DISSECTOR_NAME% Protocol Data") 30 | 31 | -- Adds Variables to the subtree 32 | %SUBTREE_POPULATION% 33 | end 34 | 35 | local %PROTOCOL%_port = DissectorTable.get("%PROTOCOL%.port") 36 | %PORTS% 37 | "#; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Aurélien VERNIZEAU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/dissector.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub(crate) struct Connection { 5 | /// Protocol to spy: TCP or UDP 6 | pub protocol: String, 7 | 8 | /// List of port to listen 9 | pub ports: Vec, 10 | } 11 | 12 | #[derive(Serialize, Deserialize)] 13 | pub(crate) struct ValueString { 14 | /// Value to be replaced 15 | pub value: i64, 16 | 17 | /// String description of the value 18 | pub string: String 19 | } 20 | 21 | #[derive(Serialize, Deserialize)] 22 | pub(crate) struct DataChunk { 23 | /// Name of the attribute 24 | pub name: String, 25 | 26 | /// Data type of the attribute 27 | pub format: String, 28 | 29 | /// How the data should be displayed 30 | pub base: String, 31 | 32 | /// Offset from the begining of the packet 33 | pub offset: u32, 34 | 35 | /// Size of the attribute 36 | pub size: u32, 37 | 38 | /// ValueString (Optionnal) 39 | pub valstr: Option> 40 | } 41 | 42 | #[derive(Serialize, Deserialize)] 43 | pub(crate) struct Dissector { 44 | /// Name of the dissector 45 | pub name: String, 46 | 47 | /// Big or little endian 48 | pub endianness: String, 49 | 50 | /// Network information 51 | pub connection: Connection, 52 | 53 | /// Packet description 54 | pub data: Vec, 55 | } 56 | -------------------------------------------------------------------------------- /src/keyword.rs: -------------------------------------------------------------------------------- 1 | pub(crate) enum Keyword { 2 | Date, 3 | DissectorName, 4 | FieldsDeclaration, 5 | FieldsList, 6 | Ports, 7 | ProjectName, 8 | Protocol, 9 | SubtreePopulation, 10 | ValueString, 11 | } 12 | 13 | impl Keyword { 14 | pub(crate) fn as_str(&self) -> &'static str { 15 | match self { 16 | Self::Date => "%DATE%", 17 | Self::DissectorName => "%DISSECTOR_NAME%", 18 | Self::FieldsDeclaration => "%FIELDS_DECLARATION%", 19 | Self::FieldsList => "%FIELDS_LIST%", 20 | Self::Ports => "%PORTS%", 21 | Self::ProjectName => "%PROJECT_NAME%", 22 | Self::Protocol => "%PROTOCOL%", 23 | Self::SubtreePopulation => "%SUBTREE_POPULATION%", 24 | Self::ValueString => "%VALUESTRING_DECLARATION%" 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod unit_test { 31 | use super::Keyword; 32 | 33 | #[test] 34 | fn keyword_as_str() { 35 | assert_eq!(Keyword::Date.as_str(), "%DATE%"); 36 | assert_eq!(Keyword::DissectorName.as_str(), "%DISSECTOR_NAME%"); 37 | assert_eq!(Keyword::FieldsDeclaration.as_str(), "%FIELDS_DECLARATION%"); 38 | assert_eq!(Keyword::FieldsList.as_str(), "%FIELDS_LIST%"); 39 | assert_eq!(Keyword::Ports.as_str(), "%PORTS%"); 40 | assert_eq!(Keyword::ProjectName.as_str(), "%PROJECT_NAME%"); 41 | assert_eq!(Keyword::Protocol.as_str(), "%PROTOCOL%"); 42 | assert_eq!(Keyword::SubtreePopulation.as_str(), "%SUBTREE_POPULATION%"); 43 | assert_eq!(Keyword::ValueString.as_str(), "%VALUESTRING_DECLARATION%") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | pub const JSON_SCHEMA: &str = r#" 2 | { 3 | "id": "schemaTemplate", 4 | "type" : "object", 5 | "properties" : { 6 | "name" : { 7 | "type" : "string", 8 | "minLength" : 1, 9 | "maxLength" : 32 10 | }, 11 | "endianness" : { 12 | "type" : "string", 13 | "enum" : ["little", "big"] 14 | }, 15 | "connection" : { 16 | "type" : "object", 17 | "properties" : { 18 | "protocol" : { 19 | "type" : "string", 20 | "enum" : [ "udp", "tcp" ] 21 | }, 22 | "ports" : { 23 | "type" : "array", 24 | "UniqueItems" : true, 25 | "minItems" : 1, 26 | "items" : { 27 | "type" : "number", 28 | "minimum" : 1, 29 | "maximum" : 65535 30 | } 31 | } 32 | }, 33 | "required" : ["protocol", "ports"] 34 | }, 35 | "data" : { 36 | "type" : "array", 37 | "minItems" : 1, 38 | "items" : { 39 | "type" : "object", 40 | "properties" : { 41 | "name" : { 42 | "type" : "string", 43 | "minLength" : 1, 44 | "maxLength" : 32 45 | }, 46 | "format" : { 47 | "type" : "string", 48 | "enum" : ["bool", "char", "uint8", "uint16", "uint24", "uint32", "uint64", "int8", "int16", "int24", "int32", "int64", "float", "double", "absolute_time", "relative_time", "ether", "bytes", "ipv4", "ipv6", "guid", "oid", "none"] 49 | }, 50 | "base" : { 51 | "type" : "string", 52 | "enum" : ["NONE", "DEC", "HEX", "OCT", "DEC_HEX", "HEX_DEC", "UTC", "LOCAL", "DOY_UTC", "DOT", "DASH", "COLON", "SPACE"] 53 | }, 54 | "offset" : { 55 | "type" : "number" 56 | }, 57 | "size" : { 58 | "type" : "number" 59 | }, 60 | "valstr" : { 61 | "type": "array", 62 | "minItems" : 1, 63 | "items" : { 64 | "type" : "object", 65 | "properties" : { 66 | "value" : { 67 | "type" : "number" 68 | }, 69 | "string" : { 70 | "type" : "string", 71 | "minLength" : 1, 72 | "maxLength" : 32 73 | } 74 | }, 75 | "required" : ["value", "string"] 76 | } 77 | } 78 | }, 79 | "required" : ["name", "format", "base", "offset", "size"] 80 | } 81 | } 82 | }, 83 | "required" : ["name", "endianness", "connection", "data"] 84 | } 85 | "#; 86 | -------------------------------------------------------------------------------- /src/validator.rs: -------------------------------------------------------------------------------- 1 | //! Module to validate a dissector JSON description 2 | 3 | use jsonschema::JSONSchema; 4 | use serde_json::Value; 5 | 6 | use crate::error::WirdigenError; 7 | use crate::schema::JSON_SCHEMA; 8 | 9 | pub struct Validator { 10 | schema_value: JSONSchema, 11 | } 12 | 13 | impl Validator { 14 | /// Create a new validator object. 15 | pub fn new() -> Result { 16 | let json_schema: Value = serde_json::from_str(JSON_SCHEMA)?; 17 | 18 | let data = Self::compile_schema(json_schema)?; 19 | Ok(Validator { schema_value: data }) 20 | } 21 | 22 | /// Validate a dissector in serde_json format against predefined schema. 23 | pub fn validate(self, json_raw: &Value) -> bool { 24 | match self.schema_value.validate(json_raw) { 25 | Err(errors) => { 26 | for err in errors { 27 | eprintln!("{:#?}", err); 28 | } 29 | false 30 | } 31 | Ok(_) => true, 32 | } 33 | } 34 | } 35 | 36 | impl Validator { 37 | fn compile_schema(value: Value) -> Result { 38 | match JSONSchema::compile(&value) { 39 | Err(e) => Err(WirdigenError::JSONSchemaCompilation(e.to_string())), 40 | Ok(data) => Ok(data), 41 | } 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod unit_test { 47 | use super::*; 48 | 49 | use std::fs::File; 50 | use std::io::BufReader; 51 | 52 | #[test] 53 | fn validator_new() -> Result<(), WirdigenError> { 54 | let _ = Validator::new()?; 55 | Ok(()) 56 | } 57 | 58 | #[test] 59 | fn validator_compile_schema_valid() -> Result<(), WirdigenError> { 60 | let valid_schema = r#" 61 | { 62 | "properties" : { 63 | "test": { 64 | "type": "string" 65 | } 66 | } 67 | }"#; 68 | 69 | let value = serde_json::from_str(valid_schema)?; 70 | 71 | if let Err(_) = Validator::compile_schema(value) { 72 | panic!("The schema should have compiled") 73 | } 74 | Ok(()) 75 | } 76 | 77 | #[test] 78 | fn validator_compile_schema_invalid() -> Result<(), WirdigenError> { 79 | // "any" is no longer a valid type keyword for a schema 80 | let invalid_schema = r#" 81 | { 82 | "properties" : { 83 | "test": { 84 | "type": "any" 85 | } 86 | } 87 | }"#; 88 | 89 | let value = serde_json::from_str(invalid_schema)?; 90 | 91 | if let Ok(_) = Validator::compile_schema(value) { 92 | panic!("The schema should not have compiled") 93 | } 94 | Ok(()) 95 | } 96 | 97 | #[test] 98 | fn validator_validate_true() -> Result<(), WirdigenError> { 99 | let file = File::open("./example/example_dissector.json")?; 100 | let rdr = BufReader::new(file); 101 | let value: Value = serde_json::from_reader(rdr)?; 102 | let mgr = Validator::new()?; 103 | 104 | assert_eq!(mgr.validate(&value), true); 105 | Ok(()) 106 | } 107 | 108 | #[test] 109 | fn validator_validate_false() -> Result<(), WirdigenError> { 110 | // Invalid dissector 111 | let json_raw = r#" 112 | { 113 | }"#; 114 | 115 | let value = serde_json::from_str(json_raw)?; 116 | 117 | let mgr = Validator::new()?; 118 | 119 | assert_eq!(mgr.validate(&value), false); 120 | Ok(()) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/generator.rs: -------------------------------------------------------------------------------- 1 | //! Module to generate LUA plugin from JSON of a dissector 2 | 3 | use chrono::offset::Local; 4 | use regex::Regex; 5 | use serde_json::Value; 6 | use std::env; 7 | use std::fmt::Write as StringWrite; 8 | use std::fs::File; 9 | use std::io::Write as FileWrite; 10 | use std::io::{BufWriter, Read}; 11 | 12 | use crate::dissector::Dissector; 13 | use crate::error::WirdigenError; 14 | use crate::keyword::Keyword; 15 | use crate::template::DISSECTOR_TEMPLATE; 16 | 17 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 18 | 19 | pub struct Generator { 20 | output_dir: String, 21 | } 22 | 23 | impl Default for Generator { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl Generator { 30 | /// Create a new generator object 31 | pub fn new() -> Generator { 32 | Generator { 33 | output_dir: env::temp_dir().display().to_string(), 34 | } 35 | } 36 | } 37 | 38 | impl Generator { 39 | /// Try to generate LUA plugin from a reader 40 | pub fn from_reader(&self, rdr: R) -> Result 41 | where 42 | R: Read, 43 | { 44 | let dissector_value: Dissector = serde_json::from_reader(rdr)?; 45 | self.generate_dissector(dissector_value) 46 | } 47 | 48 | /// Try to generate LUA plugin from a serde_json value 49 | pub fn from_value(&self, value: Value) -> Result { 50 | let dissector: Dissector = serde_json::from_value(value)?; 51 | self.generate_dissector(dissector) 52 | } 53 | 54 | /// Set the output directory where plugin are generated. 55 | /// 56 | /// Note: This function does not create non-existent directory 57 | pub fn set_output_directory(&mut self, dir_path: &str) { 58 | self.output_dir = dir_path.to_string(); 59 | } 60 | 61 | /// Get current output directory of generated directory 62 | /// 63 | /// Note: By default, the value is set to the temporary directory of the OS 64 | pub fn get_output_directory(&self) -> &str { 65 | &self.output_dir 66 | } 67 | } 68 | 69 | impl Generator { 70 | fn generate_dissector(&self, dissector: Dissector) -> Result { 71 | // Load template from string constant 72 | let mut output_data: String = String::from(DISSECTOR_TEMPLATE); 73 | 74 | let tool_name = format!("WIRDIGEN {}", VERSION); 75 | // Project name 76 | output_data = 77 | self.find_and_replace_all(&output_data, Keyword::ProjectName.as_str(), &tool_name)?; 78 | 79 | // Dissector name 80 | output_data = self.find_and_replace_all( 81 | &output_data, 82 | Keyword::DissectorName.as_str(), 83 | &dissector.name, 84 | )?; 85 | 86 | // Date 87 | output_data = self.find_and_replace_all( 88 | &output_data, 89 | Keyword::Date.as_str(), 90 | &Local::now().format("%Y-%m-%d %H:%M:%S").to_string(), 91 | )?; 92 | 93 | // Fields list 94 | // Fields declaration 95 | // Subtree population 96 | // ValueString 97 | 98 | let mut fields_list_buffer: String = String::new(); 99 | let mut fields_declaration_buffer: String = String::new(); 100 | let mut subtree_population_buffer: String = String::new(); 101 | let mut valstr_buffer: String = String::new(); 102 | 103 | let data_prefix_name: String = dissector.name.to_lowercase(); 104 | 105 | for data in dissector.data { 106 | let full_filter_name: String = format!("{}.{}", data_prefix_name, data.name); 107 | 108 | // Check if 'valstr' is defined for current data chunk 109 | if let Some(valstr_vec) = data.valstr { 110 | let mut valstr_value_buffer: String = String::new(); 111 | for valstr_item in valstr_vec { 112 | let _ = write!( 113 | valstr_value_buffer, 114 | "[{}] = \"{}\", ", 115 | valstr_item.value, valstr_item.string 116 | ); 117 | } 118 | 119 | // Remove last whitespace and ',' 120 | valstr_value_buffer.truncate(valstr_value_buffer.chars().count() - 2); 121 | 122 | let valstr_name = format!("VALSTR_{}", data.name.to_uppercase()); 123 | let _ = writeln!( 124 | valstr_buffer, 125 | "local {} = {{ {} }}", 126 | valstr_name, valstr_value_buffer 127 | ); 128 | 129 | let _ = writeln!( 130 | fields_declaration_buffer, 131 | "local {} = ProtoField.{}(\"{}\", \"{}\", base.{}, {})", 132 | data.name, data.format, full_filter_name, data.name, data.base, valstr_name 133 | ); 134 | } else { 135 | let _ = writeln!( 136 | fields_declaration_buffer, 137 | "local {} = ProtoField.{}(\"{}\", \"{}\", base.{})", 138 | data.name, data.format, full_filter_name, data.name, data.base 139 | ); 140 | } 141 | 142 | let _ = write!(fields_list_buffer, "{},\n\t", data.name); 143 | 144 | let add_endianness = if dissector.endianness == "little" { 145 | String::from("add_le") 146 | } else { 147 | String::from("add") 148 | }; 149 | 150 | let buffer_declaration: String = format!("buffer({}, {})", data.offset, data.size); 151 | 152 | let _ = write!( 153 | subtree_population_buffer, 154 | "subtree:{}({}, {})\n\t", 155 | add_endianness, data.name, buffer_declaration 156 | ); 157 | } 158 | 159 | fields_declaration_buffer.truncate(fields_declaration_buffer.chars().count() - 1); 160 | fields_list_buffer.truncate(fields_list_buffer.chars().count() - 2); 161 | 162 | output_data = self.find_and_replace_all( 163 | &output_data, 164 | Keyword::FieldsList.as_str(), 165 | &fields_list_buffer, 166 | )?; 167 | 168 | output_data = 169 | self.find_and_replace_all(&output_data, Keyword::ValueString.as_str(), &valstr_buffer)?; 170 | 171 | output_data = self.find_and_replace_all( 172 | &output_data, 173 | Keyword::FieldsDeclaration.as_str(), 174 | &fields_declaration_buffer, 175 | )?; 176 | 177 | output_data = self.find_and_replace_all( 178 | &output_data, 179 | Keyword::SubtreePopulation.as_str(), 180 | &subtree_population_buffer, 181 | )?; 182 | 183 | output_data = self.find_and_replace_all( 184 | &output_data, 185 | Keyword::Protocol.as_str(), 186 | &dissector.connection.protocol, 187 | )?; 188 | 189 | let mut ports_buffer: String = String::new(); 190 | for port in dissector.connection.ports { 191 | let _ = writeln!( 192 | ports_buffer, 193 | "{}_port:add({}, {})", 194 | dissector.connection.protocol, port, dissector.name 195 | ); 196 | } 197 | 198 | output_data = 199 | self.find_and_replace_all(&output_data, Keyword::Ports.as_str(), &ports_buffer)?; 200 | 201 | let slash_platform: String = if cfg!(windows) { 202 | String::from("\\") 203 | } else { 204 | String::from("/") 205 | }; 206 | 207 | let output_filename: String = format!( 208 | "{}{}dissector_{}.lua", 209 | self.output_dir, slash_platform, dissector.name 210 | ); 211 | 212 | let output_file = File::create(&output_filename)?; 213 | let mut f = BufWriter::new(output_file); 214 | f.write_all(output_data.as_bytes())?; 215 | Ok(output_filename) 216 | } 217 | 218 | fn find_and_replace_all( 219 | &self, 220 | buffer: &str, 221 | to_search: &str, 222 | to_replace: &str, 223 | ) -> Result { 224 | let re = Regex::new(to_search)?; 225 | Ok(re.replace_all(buffer, to_replace).to_string()) 226 | } 227 | } 228 | 229 | #[cfg(test)] 230 | mod unit_test { 231 | use super::*; 232 | 233 | use std::io::BufReader; 234 | 235 | #[test] 236 | fn generator_default() { 237 | let _ = Generator::default(); 238 | } 239 | 240 | #[test] 241 | fn generator_find_and_replace_all() -> Result<(), WirdigenError> { 242 | let buffer: &str = "one two three one one"; 243 | let to_search: &str = "one"; 244 | let to_replace: &str = "zero"; 245 | 246 | let expected: &str = "zero two three zero zero"; 247 | 248 | let result = Generator::default().find_and_replace_all(buffer, to_search, to_replace)?; 249 | 250 | assert_eq!(result, expected); 251 | Ok(()) 252 | } 253 | 254 | #[test] 255 | fn generator_from_reader() -> Result<(), WirdigenError> { 256 | let file = File::open("./example/example_dissector.json")?; 257 | let rdr = BufReader::new(file); 258 | 259 | let output_file_path = Generator::default().from_reader(rdr)?; 260 | println!("{}", output_file_path); 261 | 262 | Ok(()) 263 | } 264 | 265 | #[test] 266 | fn generator_from_value() -> Result<(), WirdigenError> { 267 | let file = File::open("./example/example_dissector.json")?; 268 | let rdr = BufReader::new(file); 269 | let value: Value = serde_json::from_reader(rdr)?; 270 | 271 | let output_file_path = Generator::default().from_value(value)?; 272 | println!("{}", output_file_path); 273 | 274 | Ok(()) 275 | } 276 | 277 | #[test] 278 | fn generator_set_output_directory() { 279 | let mut gen = Generator::default(); 280 | 281 | let expected = env::temp_dir().display().to_string(); 282 | assert_eq!(gen.get_output_directory(), expected); 283 | 284 | let new_output_dir = format!("{}/toast", expected); 285 | gen.set_output_directory(&new_output_dir); 286 | 287 | let expected = new_output_dir; 288 | assert_eq!(gen.get_output_directory(), expected); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /example/example_dissector.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "test", 3 | "endianness" : "little", 4 | "connection" : { 5 | "protocol" : "udp", 6 | "ports" : [60001, 60002] 7 | }, 8 | "data" : [ 9 | { 10 | "name" : "bool_NONE", 11 | "format" : "bool", 12 | "base" : "NONE", 13 | "offset" : 0, 14 | "size" : 1 15 | }, 16 | { 17 | "name" : "char_OCT", 18 | "format" : "char", 19 | "base" : "OCT", 20 | "offset" : 1, 21 | "size" : 1 22 | }, 23 | { 24 | "name" : "char_HEX", 25 | "format" : "char", 26 | "base" : "HEX", 27 | "offset" : 2, 28 | "size" : 1 29 | }, 30 | { 31 | "name" : "uint8_DEC", 32 | "format" : "uint8", 33 | "base" : "DEC", 34 | "offset" : 3, 35 | "size" : 1 36 | }, 37 | { 38 | "name" : "uint8_OCT", 39 | "format" : "uint8", 40 | "base" : "OCT", 41 | "offset" : 4, 42 | "size" : 1 43 | }, 44 | { 45 | "name" : "uint8_HEX", 46 | "format" : "uint8", 47 | "base" : "HEX", 48 | "offset" : 5, 49 | "size" : 1 50 | }, 51 | { 52 | "name" : "uint8_DEC_HEX", 53 | "format" : "uint8", 54 | "base" : "DEC_HEX", 55 | "offset" : 6, 56 | "size" : 1 57 | }, 58 | { 59 | "name" : "uint8_HEX_DEC", 60 | "format" : "uint8", 61 | "base" : "HEX_DEC", 62 | "offset" : 7, 63 | "size" : 1 64 | }, 65 | { 66 | "name" : "uint16_DEC", 67 | "format" : "uint16", 68 | "base" : "DEC", 69 | "offset" : 8, 70 | "size" : 2 71 | }, 72 | { 73 | "name" : "uint16_OCT", 74 | "format" : "uint16", 75 | "base" : "OCT", 76 | "offset" : 10, 77 | "size" : 2 78 | }, 79 | { 80 | "name" : "uint16_HEX", 81 | "format" : "uint16", 82 | "base" : "HEX", 83 | "offset" : 12, 84 | "size" : 2 85 | }, 86 | { 87 | "name" : "uint16_DEC_HEX", 88 | "format" : "uint16", 89 | "base" : "DEC_HEX", 90 | "offset" : 14, 91 | "size" : 2 92 | }, 93 | { 94 | "name" : "uint16_HEX_DEC", 95 | "format" : "uint16", 96 | "base" : "HEX_DEC", 97 | "offset" : 16, 98 | "size" : 2 99 | }, 100 | { 101 | "name": "uint24_DEC", 102 | "format": "uint24", 103 | "base": "DEC", 104 | "offset": 18, 105 | "size": 3 106 | }, 107 | { 108 | "name": "uint24_OCT", 109 | "format": "uint24", 110 | "base": "OCT", 111 | "offset": 21, 112 | "size": 3 113 | }, 114 | { 115 | "name": "uint24_HEX", 116 | "format": "uint24", 117 | "base": "HEX", 118 | "offset": 24, 119 | "size": 3 120 | }, 121 | { 122 | "name": "uint24_DEC_HEX", 123 | "format": "uint24", 124 | "base": "DEC_HEX", 125 | "offset": 27, 126 | "size": 3 127 | }, 128 | { 129 | "name": "uint24_HEX_DEC", 130 | "format": "uint24", 131 | "base": "HEX_DEC", 132 | "offset": 30, 133 | "size": 3 134 | }, 135 | { 136 | "name" : "uint32_DEC", 137 | "format" : "uint32", 138 | "base" : "DEC", 139 | "offset" : 33, 140 | "size" : 4 141 | }, 142 | { 143 | "name" : "uint32_OCT", 144 | "format" : "uint32", 145 | "base" : "OCT", 146 | "offset" : 37, 147 | "size" : 4 148 | }, 149 | { 150 | "name" : "uint32_HEX", 151 | "format" : "uint32", 152 | "base" : "HEX", 153 | "offset" : 41, 154 | "size" : 4 155 | }, 156 | { 157 | "name" : "uint32_DEC_HEX", 158 | "format" : "uint32", 159 | "base" : "DEC_HEX", 160 | "offset" : 45, 161 | "size" : 4 162 | }, 163 | { 164 | "name" : "uint32_HEX_DEC", 165 | "format" : "uint32", 166 | "base" : "HEX_DEC", 167 | "offset" : 49, 168 | "size" : 4 169 | }, 170 | { 171 | "name" : "uint64_DEC", 172 | "format" : "uint64", 173 | "base" : "DEC", 174 | "offset" : 53, 175 | "size" : 8 176 | }, 177 | { 178 | "name" : "uint64_OCT", 179 | "format" : "uint64", 180 | "base" : "OCT", 181 | "offset" : 61, 182 | "size" : 8 183 | }, 184 | { 185 | "name" : "uint64_HEX", 186 | "format" : "uint64", 187 | "base" : "HEX", 188 | "offset" : 69, 189 | "size" : 8 190 | }, 191 | { 192 | "name" : "uint64_DEC_HEX", 193 | "format" : "uint64", 194 | "base" : "DEC_HEX", 195 | "offset" : 77, 196 | "size" : 8 197 | }, 198 | { 199 | "name" : "uint64_HEX_DEC", 200 | "format" : "uint64", 201 | "base" : "HEX_DEC", 202 | "offset" : 85, 203 | "size" : 8 204 | }, 205 | { 206 | "name" : "int8_DEC", 207 | "format" : "int8", 208 | "base" : "DEC", 209 | "offset" : 93, 210 | "size" : 1 211 | }, 212 | { 213 | "name" : "int16_DEC", 214 | "format" : "int16", 215 | "base" : "DEC", 216 | "offset" : 94, 217 | "size" : 2 218 | }, 219 | { 220 | "name": "int24_DEC", 221 | "format": "int24", 222 | "base": "DEC", 223 | "offset": 96, 224 | "size": 3 225 | }, 226 | { 227 | "name" : "int32_DEC", 228 | "format" : "int32", 229 | "base" : "DEC", 230 | "offset" : 99, 231 | "size" : 4 232 | }, 233 | { 234 | "name" : "int64_DEC", 235 | "format" : "int64", 236 | "base" : "DEC", 237 | "offset" : 103, 238 | "size" : 8 239 | }, 240 | { 241 | "name" : "float_NONE", 242 | "format" : "float", 243 | "base" : "NONE", 244 | "offset" : 111, 245 | "size" : 4 246 | }, 247 | { 248 | "name" : "double_NONE", 249 | "format" : "double", 250 | "base" : "NONE", 251 | "offset" : 115, 252 | "size" : 8 253 | }, 254 | { 255 | "name" : "absolute_time_LOCAL", 256 | "format" : "absolute_time", 257 | "base" : "LOCAL", 258 | "offset" : 123, 259 | "size" : 8 260 | }, 261 | { 262 | "name" : "absolute_time_UTC", 263 | "format" : "absolute_time", 264 | "base" : "UTC", 265 | "offset" : 131, 266 | "size" : 8 267 | }, 268 | { 269 | "name" : "absolute_time_DOY_UTC", 270 | "format" : "absolute_time", 271 | "base" : "DOY_UTC", 272 | "offset" : 139, 273 | "size" : 8 274 | }, 275 | { 276 | "name" : "relative_time_NONE", 277 | "format" : "relative_time", 278 | "base" : "NONE", 279 | "offset" : 147, 280 | "size" : 8 281 | }, 282 | { 283 | "name" : "bytes_NONE", 284 | "format" : "bytes", 285 | "base" : "NONE", 286 | "offset" : 155, 287 | "size" : 10 288 | }, 289 | { 290 | "name" : "bytes_DOT", 291 | "format" : "bytes", 292 | "base" : "DOT", 293 | "offset" : 165, 294 | "size" : 10 295 | }, 296 | { 297 | "name" : "bytes_DASH", 298 | "format" : "bytes", 299 | "base" : "DASH", 300 | "offset" : 175, 301 | "size" : 10 302 | }, 303 | { 304 | "name" : "bytes_COLON", 305 | "format" : "bytes", 306 | "base" : "COLON", 307 | "offset" : 185, 308 | "size" : 10 309 | }, 310 | { 311 | "name" : "bytes_SPACE", 312 | "format" : "bytes", 313 | "base" : "SPACE", 314 | "offset" : 195, 315 | "size" : 10 316 | }, 317 | { 318 | "name" : "unused", 319 | "format" : "none", 320 | "base" : "NONE", 321 | "offset" : 205, 322 | "size" : 3 323 | }, 324 | { 325 | "name" : "ipv4_NONE", 326 | "format" : "ipv4", 327 | "base" : "NONE", 328 | "offset" : 208, 329 | "size" : 4 330 | }, 331 | { 332 | "name" : "ipv6_NONE", 333 | "format" : "ipv6", 334 | "base" : "NONE", 335 | "offset" : 212, 336 | "size" : 16 337 | }, 338 | { 339 | "name" : "ether_NONE", 340 | "format" : "ether", 341 | "base" : "NONE", 342 | "offset" : 228, 343 | "size" : 6 344 | }, 345 | { 346 | "name" : "guid_NONE", 347 | "format" : "guid", 348 | "base" : "NONE", 349 | "offset" : 234, 350 | "size" : 16 351 | }, 352 | { 353 | "name" : "oid_NONE", 354 | "format" : "oid", 355 | "base" : "NONE", 356 | "offset" : 250, 357 | "size" : 16 358 | }, 359 | { 360 | "name": "answer_to_universe", 361 | "format" : "uint8", 362 | "base" : "DEC", 363 | "offset" : 266, 364 | "size" : 1, 365 | "valstr" : [ 366 | { "value" : 42, "string" : "FORTY-TWO"} 367 | ] 368 | }, 369 | { 370 | "name": "temperature", 371 | "format" : "int32", 372 | "base" : "DEC", 373 | "offset" : 267, 374 | "size" : 4, 375 | "valstr" : [ 376 | { "value" : -273, "string" : "ABSOLUTE ZERO" } 377 | ] 378 | } 379 | ] 380 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![RustCI](https://github.com/cerclique/wirdigen/actions/workflows/rust-ci.yml/badge.svg) 2 | [![Codecov](https://codecov.io/gh/Cerclique/wirdigen/branch/master/graph/badge.svg?token=7TATDXMKQA)](https://codecov.io/gh/Cerclique/wirdigen) 3 | ![RustAudit](https://github.com/cerclique/wirdigen/actions/workflows/rust-audit.yml/badge.svg) 4 | 5 | --- 6 | 7 | 8 | ### Table of Contents 9 | 10 | * [Overview](#overview) 11 | * [How to use](#howtouse) 12 | * [Validator](#howtouse_validator) 13 | * [Generator](#howtouse_generator) 14 | * [Dissector format](#dissector_format) 15 | * [Name](#dissector_name) 16 | * [Endianess](#dissector_endianness) 17 | * [Connection](#dissector_connection) 18 | * [Data](#dissector_data) 19 | * [Compatibility matrices](#compat_matrices) 20 | * [Numeric](#matrix_numeric) 21 | * [Time](#matrix_time) 22 | * [Raw](#matrix_raw) 23 | * [Specific](#matrix_specific) 24 | * [Import dissector into wireshark](#import_dissector) 25 | * [Roadmap](#roadmap) 26 | * [Related tools](#related_tools) 27 | 28 | # **Overview** 29 | 30 | **Wirdigen** (_**Wir**eshark **Di**ssector **Gen**erator_) is a small library that aims to generate LUA dissector for Wireshark based on a JSON description of the packet you want to analyze. 31 | 32 | For more information about packet dissection, please refer to Wireshark [documentation](https://www.wireshark.org/docs/wsdg_html_chunked/ChapterDissection.html) and [wiki](https://wiki.wireshark.org/Lua/Dissectors). 33 | 34 | # **How to use** 35 | 36 | The library is composed of two tools: 37 | 38 | ## **Validator** 39 | 40 | `Validator` compare a JSON packet description with a predefined JSON schema to ensure data integrity for plugin generation. 41 | 42 | If the packet description is invalid, errors are automatically reported to the user through `stderr` with detailled location/description. 43 | 44 | ``` rust 45 | use wirdigen::validator::Validator; 46 | use wirdigen::error::WirdigenError; 47 | 48 | fn foo() -> Result<(), WirdigenError> { 49 | // Load JSON file 50 | let file_path = "./example/example_dissector.json"; 51 | let file = File::open(file_path)?; 52 | let rdr = BufReader::new(file); 53 | 54 | // Create serde JSON value from the file 55 | let value: Value = serde_json::from_reader(rdr)?; 56 | 57 | // Create Validator object 58 | let val = Validator::new()?; 59 | 60 | // Call to validation function 61 | match val.validate(&value) { 62 | true => println!("{}: VALID", file_path), 63 | false => println!("{}: INVALID", file_path) 64 | } 65 | Ok(()) 66 | } 67 | ``` 68 | 69 | ## **Generator** 70 | 71 | `Generator` generate LUA plugin based on JSON input given by the user. 72 | 73 | ```rust 74 | use wirdigen::generator::Generator; 75 | use wirdigen::error::WirdigenError; 76 | 77 | fn foo() -> Result<(), WirdigenError> { 78 | // Load JSON file 79 | let file_path = "./example/example_dissector.json"; 80 | let file = File::open(file_path)?; 81 | let rdr = BufReader::new(file); 82 | 83 | // Create Generator object 84 | let gen = Generator::default(); 85 | // let gen = Generator::new(); <-- Alternative 86 | 87 | // Generate from a reader source 88 | let generated_file_path: String = gen.from_reader(rdr)?; 89 | println!("Generated: {}", generated_file_path); 90 | 91 | Ok(()) 92 | } 93 | ``` 94 | 95 | **Note:** 96 | The `Generator` does not perform any pre-validation on the user input. This is the role of the `Validator`. In case of invalid file, method from `Generator` will return appropriate `Err(WirdigenError)`. To avoid these kinds of problem, it's best to first perform a validation and, then, generate the LUA dissector. 97 | 98 | `Generator` object also have a `from_value` method in order to reuse the serde-json `Value` from the validation task for the generation. 99 | 100 | ```rust 101 | fn foo() -> Result<(), WirdigenError> { 102 | // Open the JSON file 103 | let file_path = "./example/example_dissector.json"; 104 | let file = File::open(file_path)?; 105 | let rdr = BufReader::new(file); 106 | 107 | // Create serde JSON value from the file 108 | let value: Value = serde_json::from_reader(rdr)?; 109 | 110 | // Create Validator object 111 | let val = Validator::new()?; 112 | 113 | // Call to validation method 114 | if val.validate(&value) { 115 | // Create Generator object 116 | let gen = Generator::default(); 117 | 118 | // Generate plugin from validated data 119 | let generated_file_path: String = gen.from_value(value)?; 120 | println!("{}", generated_file_path); 121 | } 122 | else { 123 | println!("Invalid user input: {}", file_path); 124 | } 125 | 126 | Ok(()) 127 | } 128 | ``` 129 | 130 | By default, the plugin is generated in the temporary folder defined in environment variable. The user can modify the output directory through `set_output_directory()` method and retrieve the current one through `get_output_directory()`. 131 | 132 | ```rust 133 | fn foo { 134 | let mut gen = Generator::default(); 135 | 136 | println!("{}", gen.get_output_directory()); 137 | 138 | let new_output = "/Documents/MyDissectors"; 139 | gen.set_output_directory(new_output); 140 | 141 | println!("{}", gen.get_output_directory()); 142 | } 143 | ``` 144 | 145 | **Note:** 146 | The method `set_output_directory` does not create non-existant directory from user input. 147 | If the output directory is not reachable, the error will be propagated from the generation method when trying to create the final LUA file. 148 | 149 | # **Dissector format** 150 | 151 | A JSON dissector description is composed of 4 elements: 152 | - `name` 153 | - `endianness` 154 | - `connection` 155 | - `data` 156 | 157 | ## **Name** 158 | 159 | `name` element is a string (max size: 32) representing the name of the protocol to dissect that will be used inside Wireshark ("Protocol" column) to identify your packet. 160 | 161 | **Note:** This name is also used for the generated LUA file. For example, if the attribute is `MY_PROTO`, the generated plugin will be called `dissector_MY_PROTO.lua`. 162 | 163 | ## **Endianness** 164 | 165 | String defining which endianness is used by the protocol. 166 | Possible values are `little` and `big`. 167 | 168 | ## **Connection** 169 | 170 | The `connection` object contains 2 fields : 171 | - `protocol`: String. Either `udp` or `tcp`. 172 | - `ports`: Array of port the dissector need to spy (between 1 and 65535). 173 | 174 | ## **Data** 175 | 176 | `data` is an array of object describing the packet. Each object define a chunk of the packet we want to identify. 177 | 178 | Each chunk contains the following attributes: 179 | - `name`: String (max size: 32). 180 | - `format`: String representing the data type of the chunk. Refer to format/base matrices below for available values. 181 | - `base`: String representing how the value should be displayed. Refer to format/base matrices below for available values. 182 | - `offset`: Position offset, in byte, from the begining of the packet. 183 | - `size`: Size, in byte, of the chunk inside the packet. 184 | - `valstr` (Optional) : Array of value/string object to identify specific values in the payload. See below for more information. 185 | 186 | **About `strval`:** 187 | 188 | This attribute can be used to identify and replace specific value by its string representation (max size: 32). For instance, when dealing with webpage status code, it can be usefull to view status code `404` as `"NOT FOUND"` or `200` as `"OK"` inside wireshark capture view. 189 | This example can be described as follow inside the dissector JSON file : 190 | 191 | ```json 192 | { 193 | ..., // First part of data chunk description 194 | "valstr" : [ 195 | { "value": 404, "string": "NOT FOUND" }, 196 | { "value": 200, "string": "OK"} 197 | ] 198 | } 199 | ``` 200 | # **Format/Base compatibility matrices** 201 | 202 | These matrices show which format/base combination are supported by Wirdigen. 203 | 204 | ## **Numeric** 205 | 206 | | Format \ Base | NONE | DEC | OCT | HEX | DEC_HEX | HEX_DEC | 207 | |:-------------:|:----:|:---:|:---:|:---:|:-------:|:-------:| 208 | | bool | X | | | | | | 209 | | char | | | X | X | | | 210 | | uint8 | | X | X | X | X | X | 211 | | uint16 | | X | X | X | X | X | 212 | | uint24 | | X | X | X | X | X | 213 | | uint32 | | X | X | X | X | X | 214 | | uint64 | | X | X | X | X | X | 215 | | int8 | | X | | | | | 216 | | int16 | | X | | | | | 217 | | int24 | | X | | | | | 218 | | int32 | | X | | | | | 219 | | int64 | | X | | | | | 220 | | float(*) | X | X | X | X | X | X | 221 | | double(*) | X | X | X | X | X | X | 222 | 223 | _(*) = For the specified `format`, the `base` is ignored by Wireshark._ 224 | 225 | ## **Time** 226 | 227 | | Format \ Base | LOCAL | UTC | DOY_UTC | 228 | |:----------------:|:-----:|:---:|:-------:| 229 | | absolute_time | X | X | X | 230 | | relative_time(*) | X | X | X | 231 | 232 | _(*) = For the specified `format`, the `base` is ignored by Wireshark._ 233 | 234 | ## **Raw** 235 | 236 | | Format \ Base | NONE | DOT | DASH | COLON | SPACE | 237 | |:-------------:|:----:|:---:|:----:|:-----:|:-----:| 238 | | byte | X | X | X | X | X | 239 | 240 | ## **Specific** 241 | 242 | For these specific type of data, display is automatically handled by Wirehsark. Hense, `base` is ignored. I would recommend using `NONE` in these case. 243 | 244 | - none 245 | - ipv4 246 | - ipv6 247 | - ether 248 | - guid 249 | - oid 250 | 251 | # **Import dissector into Wireshark** 252 | 253 | First, you need to check in which directory Wireshark is looking for LUA plugin. 254 | 255 | To do this, open Wireshark and go to `help -> About Wireshark -> Folder`. 256 | 257 | Find the path associated to `"Personal Lua plugins"`. This is where you need to copy your dissector. If the path does not exist on your machine, you can manually create missing directories. 258 | 259 | The dissector script will be active after Wireshark is refreshed. You can either restart Wireshark or press **Ctrl + Shift + L** to reload all Lua scripts. 260 | 261 | **Note:** You need to reload/restart everytime you make a change in a dissector. 262 | 263 | # **Roadmap** 264 | 265 | - Missing data type: 266 | - framenum 267 | - string 268 | - stringz 269 | - ubytes 270 | - protocol 271 | - rel_oid 272 | - systemid 273 | - eui64 274 | 275 | - Support for child subtree to clearly describe more complex packet. 276 | 277 | # Related tools 278 | 279 | - [rust_dissector_generator](https://github.com/Cerclique/rust_dissector_generator): Simple executable using Wirdigen library 280 | - [rust_dissector_udp](https://github.com/Cerclique/rust_dissector_udp): Send custom packet over UDP to test generated plugin by the library inside wireshark. 281 | --------------------------------------------------------------------------------