├── .clippy.toml ├── .contributors ├── .contributors.sh ├── .gitignore ├── .rpm └── sawp.spec ├── CHANGELOG.md ├── CONTRIBUTING.fr.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.fr.md ├── README.md ├── azure ├── deploy-rpm-steps.yml ├── fuzz-steps.yml ├── install-rust.yml ├── package-steps.yml ├── qa-pipeline.yml ├── qa-steps.yml └── release-pipeline.yml ├── benches └── modbus.rs ├── cbindgen.toml ├── docker └── Dockerfile ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── fuzz_diameter.rs │ └── fuzz_modbus.rs ├── release.toml ├── sawp-diameter ├── Cargo.toml └── src │ └── lib.rs ├── sawp-dns ├── Cargo.toml ├── cbindgen.toml └── src │ ├── answer.rs │ ├── edns.rs │ ├── enums.rs │ ├── ffi.rs │ ├── header.rs │ ├── lib.rs │ ├── name.rs │ ├── question.rs │ └── rdata.rs ├── sawp-ffi-derive ├── Cargo.toml └── src │ ├── attrs.rs │ └── lib.rs ├── sawp-ffi ├── Cargo.toml └── src │ └── lib.rs ├── sawp-file ├── Cargo.toml └── src │ ├── error.rs │ ├── format.rs │ └── lib.rs ├── sawp-flags-derive ├── Cargo.toml └── src │ └── lib.rs ├── sawp-flags ├── Cargo.toml └── src │ └── lib.rs ├── sawp-gre ├── Cargo.toml └── src │ └── lib.rs ├── sawp-ike ├── Cargo.toml ├── cbindgen.toml ├── src │ ├── ffi.rs │ ├── header.rs │ ├── lib.rs │ └── payloads.rs └── tests │ ├── general.rs │ ├── ikev1_parse.rs │ └── ikev2_parse.rs ├── sawp-json ├── Cargo.toml ├── benches │ └── json.rs └── src │ └── lib.rs ├── sawp-modbus ├── Cargo.toml ├── cbindgen.toml └── src │ ├── ffi.rs │ └── lib.rs ├── sawp-pop3 ├── Cargo.toml ├── cbindgen.toml └── src │ ├── ffi.rs │ └── lib.rs ├── sawp-resp ├── Cargo.toml ├── cbindgen.toml └── src │ ├── ffi.rs │ └── lib.rs ├── sawp-tftp ├── Cargo.toml ├── cbindgen.toml └── src │ ├── ffi.rs │ └── lib.rs └── src ├── error.rs ├── ffi.rs ├── lib.rs ├── parser.rs ├── probe.rs └── protocol.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | cognitive-complexity-threshold = 30 2 | -------------------------------------------------------------------------------- /.contributors: -------------------------------------------------------------------------------- 1 | Alexander Bumstead 2 | Alexander Savage 3 | Bryan Benson 4 | Jacinta Ferrant 5 | Emmanuel Thompson 6 | James Dutrisac 7 | Philippe Antoine 8 | Roland Fischer 9 | Simon Dugas 10 | Todd Mortimer 11 | William Correia 12 | sa-sawp 13 | -------------------------------------------------------------------------------- /.contributors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | dryrun="--dry-run" 6 | name="sa-sawp" 7 | email="sa-sawp@cyber.gc.ca" 8 | mailmap= 9 | mapfile= 10 | ref="main" 11 | 12 | function help() { 13 | read -r -d '' HELP << EOM 14 | Usage: 15 | $0 [options] [--] 16 | 17 | Validate and rewrite author history. 18 | 19 | Options: 20 | -m use mailmap file for replacements 21 | -n default name to use in replacements 22 | -e default email to use in replacements 23 | -r git ref 24 | --replace rewrite git history for the specified ref 25 | -h help 26 | EOM 27 | echo "$HELP" 28 | } 29 | 30 | options=$(getopt -o hn:e:m:r: --long replace -- "$@") 31 | [ $? -eq 0 ] || { 32 | echo "Invalid arguments" 33 | help 34 | exit 1 35 | } 36 | eval set -- "$options" 37 | while true; do 38 | case "$1" in 39 | -h) 40 | help 41 | exit 0 42 | ;; 43 | -n) 44 | shift; 45 | name=$1 46 | ;; 47 | -e) 48 | shift; 49 | email=$1 50 | ;; 51 | -m) 52 | shift; 53 | mapfile=$1 54 | ;; 55 | -r) 56 | shift; 57 | ref=$1 58 | ;; 59 | --replace) 60 | dryrun= 61 | ;; 62 | --) 63 | shift 64 | # process and trim comma seperated values 65 | mailmap=$(echo "$@" | tr ',' '\n' | awk '{$1=$1};1') 66 | break 67 | ;; 68 | esac 69 | shift 70 | done 71 | 72 | # show all contributors 73 | function contributors() { 74 | git log --pretty="%an <%ae>%n%cn <%ce>" $ref | sort | uniq 75 | } 76 | 77 | # show contributors not in .contributors 78 | function diff_contributors() { 79 | # sorted order is different on centos and ubuntu 80 | tmp="/tmp/.contributors.tmp" 81 | cat .contributors | sort > $tmp 82 | contributors | comm -23 - $tmp 83 | rm $tmp 84 | } 85 | 86 | # fails if a contributor is not in the approved list 87 | function validate_contributors() { 88 | # return status 89 | local ret=1 90 | 91 | # show lines unique to the git log (not in .contributors) 92 | local result=$(diff_contributors) 93 | 94 | if test -z "$result"; then 95 | echo "Contributors on $ref" 96 | echo "==========================================" 97 | contributors 98 | echo 99 | ret=0 100 | else 101 | echo "Not in contributors list on $ref" 102 | echo "==========================================" 103 | echo -e "$result" 104 | echo 105 | fi 106 | 107 | return $ret 108 | } 109 | 110 | # uses default author for unapproved contributors 111 | function mailmap() { 112 | if [ -n "$mapfile" ]; then 113 | cat "$mapfile" 114 | fi 115 | if [ -n "$mailmap" ]; then 116 | echo -e "$mailmap" 117 | fi 118 | if [[ -z "$mapfile" && -z "$mailmap" ]]; then 119 | diff_contributors | xargs -L1 echo "$name <$email>" 120 | fi 121 | } 122 | 123 | # exit with the return value of this command 124 | validate_contributors || : 125 | 126 | echo "Changes" 127 | echo "=======" 128 | mailmap $name $email > .mailmap 129 | cat .mailmap 130 | echo 131 | 132 | echo "Git filter-repo" 133 | echo "===============" 134 | git filter-repo $dryrun --force --use-mailmap --refs $ref 135 | rm .mailmap 136 | echo 137 | 138 | # show the results of running the filter-repo command 139 | validate_contributors 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | cobertura.xml 3 | -------------------------------------------------------------------------------- /.rpm/sawp.spec: -------------------------------------------------------------------------------- 1 | Name: sawp 2 | Summary: Security Aware Wire Protocol parsing library (SAWP) FFI package 3 | Version: %{version} 4 | Release: 1 5 | License: Copyright 2020 Crown Copyright, Government of Canada (Canadian Centre for Cyber Security / Communications Security Establishment) 6 | Source0: %{name}-%{version}.tar.gz 7 | 8 | %description 9 | %{summary} 10 | 11 | %package devel 12 | Summary: Security Aware Wire Protocol parsing library (SAWP) FFI package headers and libraries 13 | Requires: %{name} = %{version}-%{release} 14 | Requires: pkgconfig 15 | 16 | %description devel 17 | %{summary} 18 | 19 | %prep 20 | %setup -q 21 | 22 | %build 23 | make %{?_smp_mflags} 24 | 25 | %install 26 | %make_install 27 | 28 | %post -p /sbin/ldconfig 29 | %postun -p /sbin/ldconfig 30 | 31 | %clean 32 | rm -rf %{buildroot} 33 | 34 | %files 35 | %defattr(-,root,root,-) 36 | %{_libdir}/libsawp*.so.* 37 | 38 | %files devel 39 | %{_libdir}/libsawp*.so 40 | %{_includedir}/sawp/*.h -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | 9 | ## [Unreleased] - ReleaseDate 10 | 11 | ## [0.13.1] - 2024-10-02 12 | ### Changed 13 | - sawp-modbus: Make length field of Message public 14 | 15 | ### Fixed 16 | - sawp-resp: Fix crash with negative length arrays triggering a massive memory allocation 17 | - sawp-resp: Override Probe to reject when Invalid is the only message. The default implementation caused all data to always be interpreted as RESP. 18 | - sawp-error: Fix cfg lines to correctly use feature name 19 | 20 | ## [0.13.0] - 2024-07-08 21 | ### Changed 22 | - sawp: increased MSRV to 1.63.0 23 | - sawp: Remove Cargo.toml deps which served to pin MSRV compatible transitive dependencies 24 | - sawp: derive Eq where PartialEq is already derived and Eq could be derived (applied from clippy lint) 25 | - sawp: Include Cargo.lock to help deliver on our MSRV promise 26 | - sawp-file: Update to rmp-serde 1.1.1 27 | - sawp: Applied clippy lints 28 | - sawp-diameter: error flags now use sawp\_flags, changing from a struct to an enum. 29 | 30 | ### Fixed 31 | - sawp: Fix release pipeline to verify MSRV, not stable 32 | 33 | ## [0.12.1] - 2023-04-12 34 | ### Fixed 35 | - sawp-ike: Restricted lengths for attribute parsing to prevent buffer over-reads 36 | - sawp: Pin criterion dependencies to maintain our MSRV promise 37 | - sawp: Remove unused key from release.toml which caused build failures 38 | 39 | 40 | ## [0.12.0] - 2023-02-13 41 | ### Added 42 | - sawp-ike: initial release of protocol parser 43 | - sawp: added ip types to the C/C++ FFI 44 | 45 | ### Changed 46 | - make: compose directories from $DESTDIR at usage time instead of preformatting LIBDIR and INCLUDEDIR with it 47 | - sawp: apply clippy lints 48 | - sawp: increase MSRV to 1.58.1 49 | - sawp: change to 2021 edition 50 | - sawp: update to nom 7.1 51 | - sawp: unpin half version 52 | - sawp-file: unpin rmp version 53 | 54 | ### Fixed 55 | - make: pkgid cut updated for latest version 56 | - make: link to correct artifact 57 | - sawp: impl Display for Error (was todo!) 58 | - sawp-dns: use binary strings instead of taking as\_bytes() of a string 59 | - sawp-dns: parse zero-label names as empty string instead of failing 60 | - sawp-flags: derive Eq on enums when PartialEq is derived 61 | - sawp-modbus: breaking API change - get\_write\_value\_at\_address now takes address by value. 62 | - sawp-pop3: limit keyword count which prevented publishing 63 | - sawp-pop3: more restrictive keyword and status matching 64 | 65 | ## [0.11.1] - 2022-06-21 66 | ### Fixed / Changed 67 | - modbus: fix integer overflow in address and quantity 68 | 69 | ## [0.11.0] - 2022-05-27 70 | ### Fixed / Changed 71 | - modbus: make parser fields public 72 | 73 | ## [0.10.0] - 2022-05-20 74 | ### Fixed / Changed 75 | - modbus: add option for strict probing 76 | - derive: pin proc-macro-crate to v1.1.0 77 | - file: pin rmp to 0.8.10 78 | - azure: fix use of cargo release 79 | 80 | ## [0.9.0] - 2022-02-07 81 | ### Added 82 | - sawp-pop3: initial release of protocol parser. 83 | - sawp-ffi: add helper function for nested containers. 84 | - make: add version target. 85 | - doc: add french translations. 86 | - docker: add Dockerfile. 87 | 88 | ### Fixed / Changed 89 | - azure: pipeline improvements. 90 | - sawp: clippy updates. 91 | - make: fix symlinking issue in install target. 92 | 93 | ## [0.8.0] - 2021-11-10 94 | ### Added 95 | - makefile: install target 96 | - sawp-tftp: option extension parsing 97 | 98 | ### Fixed / Changed 99 | - cargo: force dependency to half 1.7 100 | 101 | ## [0.7.0] - 2021-09-24 102 | ### Added 103 | - sawp-resp: initial release of protocol parser. 104 | 105 | ### Fixed / Changed 106 | - sawp: updated style guide in CONTRIBUTING.md. 107 | - sawp: specified compatible criterion version. 108 | - sawp-diameter: specified compatible bitflags version. 109 | 110 | ## [0.6.0] - 2021-06-15 111 | ### Added 112 | - sawp-gre: initial release of protocol parser. 113 | - sawp-dns: initial release of protocol parser. 114 | - sawp-tftp: add ffi support. 115 | 116 | ### Fixed / Changed 117 | - sawp-ffi: use size_t instead of uintptr_t for size types across all ffi modules. 118 | - sawp-ffi: set soname in ffi shared object libraries. 119 | 120 | ## [0.5.0] - 2021-05-07 121 | ### Added 122 | - sawp-ffi: added getter by index for vecs. 123 | 124 | ### Fixed 125 | - sawp-modbus: `WriteMultipleCoils` parsing divided the request's count by 8 instead of the quantity. 126 | 127 | ## [0.4.0] - 2021-04-12 128 | ### Added 129 | - sawp-modbus: re-export `sawp_flags::Flags` so sawp-flags doesn't need 130 | to be added to Cargo.toml. 131 | - sawp-flags: add `is_empty()` and `is_all()` helper functions. 132 | 133 | ## [0.3.0] - 2021-04-09 134 | ### Fixed 135 | - various pipeline improvements for rpm deployment, publishing packages, 136 | doc tests, memory checks, build and clippy warnings. 137 | 138 | ### Added 139 | - sawp-ffi: added support for option, string, vec and flags. 140 | - sawp-flags-derive: initial release of bitflags handling and storage 141 | derive macros. 142 | - sawp-flags: initial release of bitflags handling and storage crate. 143 | - sawp-modbus: added ffi support for error flags. 144 | - sawp-modbus: use new sawp-flags crate. 145 | - sawp-diameter: added parsing of avp data types. 146 | - sawp-tftp: initial release of protocol parser. 147 | 148 | ## [0.2.0] - 2021-02-22 149 | ### Fixed 150 | - sawp-ffi: missing version number was preventing cargo publish. 151 | - sawp: verbose feature was not being used by protocol parsers. 152 | - sawp-modbus: added error flag for handling invalid protocol instead of failing 153 | to parse the message. 154 | - sawp-modbus: made probing function more strict by failing if any validation 155 | flags are set. 156 | - sawp-modbus: added min and max length checks for better recovery when invalid 157 | lengths are provided. 158 | 159 | ### Added 160 | - sawp: support for building an rpm with all FFI libraries and headers. 161 | 162 | ## [0.1.1] - 2021-02-12 163 | ### Added 164 | - sawp: initial release containing common traits and types used by protocol parsers. 165 | - sawp-modbus: initial release of our first complete protocol parser. Integration 166 | was tested with suricata. 167 | - sawp-diameter: initial release of a protocol parser (todo: add missing AVPs for mobility). 168 | - sawp-json: initial release of a protocol parser (todo: use for 5G protocols). 169 | - sawp-file: initial release for logging and debugging SAWP API calls (todo: not in use yet). 170 | - sawp-ffi: initial release of FFI helper macros and traits. 171 | - sawp-ffi-derive: initial release for generating FFI accessor functions. 172 | - sawp-modbus: FFI support. 173 | 174 | 175 | [Unreleased]: https://github.com/CybercentreCanada/sawp/compare/sawp-0.13.1...HEAD 176 | [0.13.1]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.13.1 177 | [0.13.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.13.0 178 | [0.12.1]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.12.1 179 | [0.12.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.12.0 180 | [0.11.1]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.11.1 181 | [0.11.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.11.0 182 | [0.10.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.10.0 183 | [0.9.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.9.0 184 | [0.8.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.8.0 185 | [0.7.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.7.0 186 | [0.6.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.6.0 187 | [0.5.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.5.0 188 | [0.4.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.4.0 189 | [0.3.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.3.0 190 | [0.2.0]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.2.0 191 | [0.1.1]: https://github.com/CybercentreCanada/sawp/releases/tag/sawp-0.1.1 192 | -------------------------------------------------------------------------------- /CONTRIBUTING.fr.md: -------------------------------------------------------------------------------- 1 | # [ENGLISH](CONTRIBUTING.md) 2 | 3 | # Contribution 4 | 5 | Ce projet est maintenu activement et accepte les contributions de source ouverte. 6 | 7 | Tout changement prévu doit faire l’objet d’une discussion. Pour ce faire, créez un problème ou 8 | communiquez directement avec nous pour confirmer que personne ne travaille sur cette même fonctionnalité 9 | et que le changement proposé correspond à la vision que nous avons adoptée pour la bibliothèque. 10 | 11 | Tous les contributeurs doivent se conformer à un certain code de conduite. Prière de lire le [Code 12 | de conduite de Rust](https://www.rust-lang.org/fr/policies/code-of-conduct) à titre d’exemple. 13 | 14 | En contribuant à ce projet, vous reconnaissez que toutes les contributions seront effectuées en 15 | vertu du contrat de licence inclus dans le fichier LICENSE. 16 | 17 | ## Consignes 18 | 19 | La liste de vérification [Rust API Guidelines 20 | Checklist](https://rust-lang.github.io/api-guidelines/checklist.html) (en anglais seulement) propose 21 | un survol des pratiques exemplaires et des conventions d’affectation des noms à adopter. 22 | 23 | ### Messages liés aux commits 24 | 25 | - Les commits devraient se limiter à une fonction logique ou à une résolution de bogue. 26 | - Le code ne devrait pas être déplacé et modifié dans le même commit. 27 | - Veuillez conserver un historique git clair, descriptif et bref. Effectuez un squash au besoin. 28 | - Incluez les erreurs de compilation ou les étapes nécessaires pour reproduire l’erreur le cas 29 | échéant. 30 | - Le titre des commits ne devrait pas dépasser 50 caractères et inclure le module/la zone et une 31 | brève description. Le commit peut alors être décrit en détail dans le corps. 32 | 33 | ``` 34 | module: brève description 35 | 36 | Ajouter une description du commit. 37 | ``` 38 | 39 | ### Pull Requests 40 | 41 | - Dans la plupart des cas, une demande de tirage (pull request) devrait se limiter à une fonction 42 | logique ou à une résolution de bogue. 43 | - Veuillez utiliser le modèle [dupliquer et 44 | tirer](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-collaborative-development-models) 45 | (en anglais seulement) pour ouvrir les demandes de tirage dans github. 46 | - Assurez-vous que votre branche est à jour avec la branche `main` en utilisant la fonction `git 47 | rebase -i main` afin d'évitez une fusion (git merge). Vous pouvez forcer l’envoi (push) de ces 48 | changements vers la branche de votre fourche (fork). 49 | - Il est possible d’effectuer des changements après l'ouverture d'une demande de tirage. 50 | - Nous effectuerons soit une « fusion squash » ou une « fusion de rebase » avec votre demande de 51 | tirage une fois qu’elle aura été acceptée. 52 | 53 | Il sera plus facile de passer en revue une demande de tirage si elle est bien documentée : 54 | 55 | - Décrivez entièrement la fonction ou la résolution de bogue; 56 | - Ajoutez un exemple d’utilisation, d’entrée ou de sortie, le cas échéant; 57 | - Ajoutez des liens vers les problèmes, les demandes de tirage et les documents externes pertinents 58 | comme les spécifications du protocole citées en référence. 59 | 60 | ### Style de code 61 | 62 | Utiliser `cargo fmt --all` pour le formatage du code. 63 | 64 | À moins qu’il n’y ait une bonne raison de faire autrement, il convient de respecter les directives 65 | générales en matière de style : 66 | 67 | - La longueur des lignes ne devrait pas dépasser 100 caractères (en tenant compte des commentaires). 68 | - Les noms de variables devraient être descriptifs. 69 | 70 | Il est préférable d’ajouter des commentaires sur la ligne précédente que des commentaires de fin : 71 | 72 | _Préférable_ 73 | ```rust 74 | // Commentaire sur la valeur utilisée. 75 | let value = 10; 76 | ``` 77 | 78 | _Éviter_ 79 | ```rust 80 | let value = 10; // Commentaire sur la valeur utilisée. 81 | ``` 82 | 83 | ### Tests et assurance de qualité 84 | 85 | Nous nous engageons à maintenir un certain niveau de qualité en ce qui a trait au code. Veuillez 86 | inclure les tests unitaires de manière à fournir le plus de détails possible concernant la fonction 87 | ou la résolution de bogue. 88 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # [Français](CONTRIBUTING.fr.md) 2 | 3 | # Contributing 4 | 5 | This project is actively maintained and accepting open source contributions. 6 | 7 | Please discuss any planned changes by creating an issue or contacting us 8 | directly to make sure no one else is working on the same feature and to see 9 | if the proposed change aligns with our vision for the library. 10 | 11 | A certain code of conduct is expected of all contributors and please refer to 12 | the [Rust code of conduct](https://www.rust-lang.org/policies/code-of-conduct) 13 | as an example. 14 | 15 | By contributing to this project you acknowledge that all contributions will be 16 | made under the licensing agreement located in the LICENSE file. 17 | 18 | ## Guidelines 19 | 20 | The [Rust API Guidelines Checklist](https://rust-lang.github.io/api-guidelines/checklist.html) 21 | provides a good overview of best practices and naming conventions. 22 | 23 | ### Commit Messages 24 | 25 | - Commits should be limited to one logical feature or bugfix. 26 | - Code should not be moved and changed in the same commit. 27 | - Please keep a clean, descriptive and concise git history. Squash when needed. 28 | - Include compile errors or steps to reproduce when applicable. 29 | - Commits should have a 50 character max title line with the module/area and a 30 | short description. The body can then describe the commit in detail. 31 | 32 | ``` 33 | module: short description 34 | 35 | Describe the commit here. 36 | ``` 37 | 38 | ### Pull Requests 39 | 40 | - Each pull request should be limited to one logical feature or bugfix in most cases. 41 | - Please use the [fork and pull](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-collaborative-development-models) 42 | model to open pull requests on github. 43 | - Keep your branch up to date with main by using `git rebase -i main` and to avoid 44 | merge commits. Force pushing these changes to your fork's branch is fine. 45 | - A pull request may be updated with changes once open. 46 | - We will either "rebase and merge" or "squash and merge" your pull request once accepted. 47 | 48 | A descriptive pull request will make it easier to review: 49 | 50 | - fully describe the feature or bugfix 51 | - include sample usage, input or output when applicable 52 | - include links to relevant issues, pull requests and external documentation such as 53 | the protocol specifications referenced 54 | 55 | ### Coding Style 56 | 57 | Use `cargo fmt --all` for code formatting. 58 | 59 | General style guidelines that should be followed unless there is reason to do otherwise: 60 | 61 | - Lines should not exceed a width of 100 characters, including comments. 62 | - Variable names should be descriptive. 63 | 64 | Comments on the previous line are preferred over trailing comments: 65 | 66 | _Preferred_ 67 | ```rust 68 | // Comment for value. 69 | let value = 10; 70 | ``` 71 | 72 | _Avoid_ 73 | ```rust 74 | let value = 10; // Comment for value. 75 | ``` 76 | 77 | ### Testing & QA 78 | 79 | We are committed to upholding a certain level of code quality. Please include unit tests to 80 | cover as much of the feature or bugfix as possible. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "Security Aware Wire Protocol parsing library" 6 | readme = "README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["parser", "streaming", "protocols", "network", "api"] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "LICENSE", 16 | "README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | # Minimum supported rust version 21 | rust-version = "1.63.0" 22 | 23 | [workspace] 24 | members = [ 25 | "sawp-dns", 26 | "sawp-ffi", 27 | "sawp-ffi-derive", 28 | "sawp-modbus", 29 | "sawp-resp", 30 | "sawp-tftp", 31 | "sawp-file", 32 | "sawp-json", 33 | "sawp-diameter", 34 | "sawp-flags", 35 | "sawp-flags-derive", 36 | "sawp-gre", 37 | "sawp-pop3", 38 | "sawp-ike", 39 | ] 40 | 41 | [features] 42 | ffi = ["cbindgen", "sawp-ffi"] 43 | # Makes error messages more descriptive and verbose at the cost of allocating 44 | # more strings 45 | verbose = [] 46 | 47 | [lib] 48 | crate-type = ["staticlib", "rlib", "cdylib"] 49 | bench = false 50 | 51 | [build-dependencies] 52 | cbindgen = {version = "0.15", optional = true} 53 | 54 | [dev-dependencies] 55 | criterion = "=0.3.4" 56 | 57 | [dependencies] 58 | sawp-ffi = { path = "sawp-ffi", version = "^0.13.1", optional = true} 59 | nom = "7.1.1" 60 | 61 | [[bench]] 62 | name = "modbus" 63 | path = "benches/modbus.rs" 64 | harness = false 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Crown Copyright, Government of Canada (Canadian Centre for Cyber Security / Communications Security Establishment) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 4 | (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 5 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 11 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 12 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Build rpm for ffi releases 2 | # ========================== 3 | # 4 | # Sawp packages can be built using the normal cargo workflow. Only use this 5 | # Makefile to build a single rpm to distribute all packages that support ffi. 6 | # Packages will be added automatically if they contain a cbindgen.toml file. 7 | # 8 | # The rpm repository is located in `target/rpmbuild`. 9 | # Shared objects are located in `target/release` or `target/debug`. 10 | # Headers are located in `target/sawp`. 11 | # 12 | # Example usage: 13 | # ```bash 14 | # # build rpm 15 | # make rpm 16 | # 17 | # # build tarball 18 | # make package 19 | # 20 | # # build ffi headers and shared objects only 21 | # make 22 | # ``` 23 | 24 | CARGO ?= cargo 25 | DESTDIR ?= 26 | PREFIX ?= /usr 27 | LIBDIR ?= $(PREFIX)/lib64 28 | INCLUDEDIR ?= $(PREFIX)/include 29 | 30 | # Use cargo to get the version or fallback to sed 31 | $(eval CRATE_VERSION=$(shell \ 32 | ( \ 33 | (${CARGO} 1> /dev/null 2> /dev/null) \ 34 | && (test -f Cargo.lock || ${CARGO} generate-lockfile) \ 35 | && (${CARGO} pkgid | cut -d\# -f 2 | cut -d@ -f 2 | cut -d: -f 2) \ 36 | ) \ 37 | || (sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml) \ 38 | )) 39 | $(eval CRATE_VERSION_MINOR=$(shell echo ${CRATE_VERSION} | cut -d. -f 1-2)) 40 | $(eval CRATE_VERSION_MAJOR=$(shell echo ${CRATE_VERSION} | cut -d. -f 1)) 41 | 42 | FFI_PACKAGES := $(patsubst sawp-%/cbindgen.toml, %, $(wildcard sawp-*/cbindgen.toml)) 43 | FFI_HEADERS := target/sawp/sawp.h $(patsubst %, target/sawp/%.h, ${FFI_PACKAGES}) 44 | FFI_OBJECTS_RELEASE := target/release/libsawp.so $(patsubst %, target/release/libsawp_%.so, ${FFI_PACKAGES}) 45 | FFI_OBJECTS_DEBUG := target/debug/libsawp.so $(patsubst %, target/debug/libsawp_%.so, ${FFI_PACKAGES}) 46 | 47 | # Source pattern to detect file changes and cache the build. 48 | # Any source file change in the workspace should trigger a rebuild. 49 | SOURCES := $(shell find . -path ./target -prune -false -o -type f \( -name "*.rs" -or -name "cbindgen.toml" -or -name "Cargo.toml" \) ) \ 50 | Makefile 51 | 52 | # Package publication order. 53 | # List of directories that contain a Cargo.toml file to publish. 54 | # This is required because some packages are dependant on others. 55 | PUBLISH := \ 56 | sawp-flags-derive \ 57 | sawp-flags \ 58 | sawp-ffi-derive \ 59 | sawp-ffi \ 60 | . \ 61 | sawp-modbus \ 62 | sawp-diameter \ 63 | sawp-tftp \ 64 | sawp-gre \ 65 | sawp-dns \ 66 | sawp-resp \ 67 | sawp-pop3 \ 68 | sawp-json \ 69 | sawp-file \ 70 | sawp-ike 71 | 72 | .PHONY: env 73 | env: 74 | @echo CARGO: ${CARGO} 75 | @echo CRATE_VERSION: ${CRATE_VERSION} 76 | @echo CRATE_VERSION_MINOR: ${CRATE_VERSION_MINOR} 77 | @echo CRATE_VERSION_MAJOR: ${CRATE_VERSION_MAJOR} 78 | @echo FFI_PACKAGES: ${FFI_PACKAGES} 79 | @echo FFI_HEADERS: ${FFI_HEADERS} 80 | @echo FFI_OBJECTS_RELEASE: ${FFI_OBJECTS_RELEASE} 81 | @echo FFI_OBJECTS_DEBUG: ${FFI_OBJECTS_DEBUG} 82 | @echo SOURCES: ${SOURCES} 83 | @echo DESTDIR: ${DESTDIR} 84 | @echo LIBDIR: $(LIBDIR) 85 | @echo INCLUDEDIR: ${INCLUDEDIR} 86 | 87 | .PHONY: version 88 | version: 89 | @echo ${CRATE_VERSION} 90 | 91 | # prevents intermediate targets from getting removed 92 | .SECONDARY: 93 | 94 | .DEFAULT_GOAL := all 95 | default: all 96 | 97 | .PHONY: all 98 | all: headers shared_objects 99 | 100 | .PHONY: clean 101 | clean: 102 | ${CARGO} clean 103 | 104 | # Headers 105 | # ======= 106 | .PHONY: headers 107 | headers: ${FFI_HEADERS} 108 | 109 | target/sawp/sawp.h: ${SOURCES} 110 | RUSTUP_TOOLCHAIN=nightly cbindgen \ 111 | --config cbindgen.toml \ 112 | --crate sawp \ 113 | --output target/sawp/sawp.h \ 114 | -v \ 115 | --clean 116 | 117 | 118 | # for each cbindgen.toml file, call the corresponding rule to build a header file 119 | target/sawp/%.h: ${SOURCES} 120 | cd sawp-$(*F) && \ 121 | RUSTUP_TOOLCHAIN=nightly cbindgen \ 122 | --config cbindgen.toml \ 123 | --crate sawp-$(*F) \ 124 | --output ../$@ \ 125 | -v \ 126 | --clean 127 | 128 | # Shared Objects 129 | # ============== 130 | .PHONY: shared_objects 131 | shared_objects: debug_objects release_objects 132 | 133 | .PHONY: debug_objects 134 | debug_objects: ${FFI_OBJECTS_DEBUG} 135 | 136 | .PHONY: release_objects 137 | release_objects: ${FFI_OBJECTS_RELEASE} 138 | 139 | target/debug/libsawp_%.so: ${SOURCES} 140 | cd sawp-$(*F) && \ 141 | ${CARGO} build --features ffi --features verbose 142 | 143 | target/release/libsawp_%.so: ${SOURCES} 144 | cd sawp-$(*F) && \ 145 | RUSTFLAGS="-C link-arg=-Wl,-soname,$(@F).${CRATE_VERSION_MAJOR}" ${CARGO} build --features ffi --release 146 | 147 | target/debug/libsawp.so: ${SOURCES} 148 | ${CARGO} build --features ffi --features verbose 149 | 150 | target/release/libsawp.so: ${SOURCES} 151 | RUSTFLAGS="-C link-arg=-Wl,-soname,$(@F).${CRATE_VERSION_MAJOR}" ${CARGO} build --features ffi --release 152 | 153 | # rpm 154 | # === 155 | .PHONY: rpm 156 | rpm: package 157 | rpmbuild -vvv -bb \ 158 | --define "version ${CRATE_VERSION}" \ 159 | --define "_topdir ${PWD}/target/rpmbuild" \ 160 | --define "_prefix $(PREFIX)" \ 161 | .rpm/sawp.spec 162 | 163 | .PHONY: package 164 | package: 165 | rm -rf target/rpmbuild target/_temp 166 | mkdir -p target/rpmbuild/SOURCES target/_temp 167 | cp ${SOURCES} --parents target/_temp 168 | tar -czvf target/rpmbuild/SOURCES/sawp-${CRATE_VERSION}.tar.gz target/_temp --transform 'flags=r;s#^target/_temp#sawp-${CRATE_VERSION}#' 169 | 170 | # note: symlinks must be relative to work with rpmbuild 171 | .PHONY: install 172 | install: 173 | install -d $(DESTDIR)$(LIBDIR) 174 | install -d $(DESTDIR)$(INCLUDEDIR)/sawp 175 | for obj in libsawp.so $(patsubst %, libsawp_%.so, ${FFI_PACKAGES}); do \ 176 | install -m 0755 target/release/$$obj $(DESTDIR)$(LIBDIR)/$$obj.${CRATE_VERSION}; \ 177 | (cd $(DESTDIR)$(LIBDIR) \ 178 | && ln -s ./$$obj.${CRATE_VERSION} ./$$obj \ 179 | && ln -s ./$$obj.${CRATE_VERSION} ./$$obj.${CRATE_VERSION_MAJOR} \ 180 | && ln -s ./$$obj.${CRATE_VERSION} ./$$obj.${CRATE_VERSION_MINOR} \ 181 | ); \ 182 | done 183 | install -m 644 target/sawp/*.h $(DESTDIR)$(INCLUDEDIR)/sawp 184 | 185 | .PHONY: uninstall 186 | uninstall: 187 | rm -f $(DESTDIR)$(LIBDIR)/libsawp*.so* 188 | rm -rf $(DESTDIR)$(INCLUDEDIR)/sawp 189 | 190 | # cargo publish 191 | # ============= 192 | # 193 | # Upload all packages in this workspace to crates.io. Uploads the dependencies 194 | # in the right order. A sleep is used so the newly published crates can be 195 | # fetched from crates.io. 196 | # 197 | # Ideally we could use a command like `cargo workspaces publish --from-git` but 198 | # that doesn't seem to work. 199 | .PHONY: publish 200 | publish: 201 | for pub in $(PUBLISH); do \ 202 | (cd $$pub && ${CARGO} publish && sleep 20); \ 203 | done 204 | 205 | .PHONY: valgrind 206 | valgrind: 207 | ${CARGO} valgrind test --workspace --all-targets 208 | 209 | .PHONY: asan-address 210 | asan-address: export RUSTFLAGS = -Zsanitizer=address 211 | asan-address: export RUSTDOCFLAGS = -Zsanitizer=address 212 | asan-address: 213 | ${CARGO} +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu --workspace --all-targets 214 | 215 | .PHONY: asan-memory 216 | asan-memory: export RUSTFLAGS = -Zsanitizer=memory -Zsanitizer-memory-track-origins 217 | asan-memory: export RUSTDOCFLAGS = -Zsanitizer=memory -Zsanitizer-memory-track-origins 218 | asan-memory: 219 | ${CARGO} +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu --workspace --all-targets 220 | 221 | .PHONY: asan-leak 222 | asan-leak: export RUSTFLAGS = -Zsanitizer=leak 223 | asan-leak: export RUSTDOCFLAGS = -Zsanitizer=leak 224 | asan-leak: 225 | ${CARGO} +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu --workspace --all-targets 226 | 227 | .PHONY: asan 228 | asan: asan-address asan-memory asan-leak 229 | 230 | # asan-address currently fails with `SIGILL` on functions with `extern "C"` 231 | # so it is not included in memcheck until a solution is found 232 | .PHONY: memcheck 233 | memcheck: valgrind 234 | -------------------------------------------------------------------------------- /README.fr.md: -------------------------------------------------------------------------------- 1 | # [English](README.md) 2 | 3 | # Bibliothèque d’analyse des protocoles filaires adaptés à la sécurité. 4 | 5 | Cette bibliothèque contient des analyseurs de protocoles filaires utilisée principalement par les 6 | capteurs de sécurité d’un réseau. 7 | 8 | Chaque analyseur présente une interface commune qui permet au moteur des capteurs d’alimenter 9 | l’analyseur en octets et de renvoyer les métadonnées analysées. Comme on s’attend à ce que les 10 | octets se trouvent sur la couche session, le moteur doit assembler les données sur la couche 11 | transport en une charge utile de session qui sera alors transmise à la bibliothèque. 12 | 13 | Cette bibliothèque vise à assurer la résilience et à analyser le plus grand nombre possible des 14 | messages observés in vivo. Si un message est non valide ou non conforme, il ne devrait pas être 15 | rejeté par l’analyseur. Les analyseurs ajouteront des indicateurs au message advenant l’échec de la 16 | validation plutôt que de générer une erreur. 17 | 18 | L’interface de chaque analyseur est uniforme et simple. Elle se compose de quelques fonctions 19 | permettant de faire ce qui suit : 20 | 21 | - vérifier si ne charge utile correspond ou non au protocole en question (p. ex. s’agit-il du 22 | protocole MODBUS?); 23 | - fournir un plus grand nombre d’octets à l’analyseur; 24 | - définir les rappels à évoquer lors d’événements de métadonnées selon le protocole (tâche); 25 | - indiquer que certains octets ne sont pas accessibles (c.-à-d., notification lors d’une perte de 26 | paquets) (tâche); 27 | - indiquer qu’une session a pris fin (tâche). 28 | 29 | La bibliothèque présente les liaisons Rust et C pour une intégration plus facile aux plateformes de 30 | capteurs de sécurité réseau existantes et à venir. (tâche) 31 | 32 | # Utilisation 33 | Pour commencer à utiliser SAWP, ajoutez un analyseur aux dépendances `Cargo.toml` de 34 | vos projets. La bibliothèque de base sera également nécessaire à l’utilisation de types courants. 35 | 36 | **La version minimale prise en charge de `rustc` est `1.63.0`.** 37 | 38 | ## Exemple 39 | ``` 40 | [dependencies] 41 | sawp-modbus = "0.8.0" 42 | sawp = "0.8.0" 43 | ``` 44 | 45 | ## Prise en charge d’une interface de fonction extérieure (FFI) 46 | Certains analyseurs font appel à une interface de fonction extérieure 47 | (FFI pour Foreign Function Interface) pour les projets C/C++. Il 48 | est possible d’activer la prise en charge d’une FFI au moyen de la fonction `ffi`. 49 | 50 | Un fichier [Makefile](Makefile) est également fourni pour faciliter le processus de compilation. 51 | Veuillez consulter ce fichier pour une documentation plus détaillée. 52 | 53 | ``` 54 | # Installer cbindgen, qui est nécessaire pour générer les en-têtes 55 | cargo install --force 56 | cbindgen 57 | 58 | # Générer les en-têtes et les objets partagés 59 | make 60 | ``` 61 | 62 | # Contribution 63 | 64 | Ce projet est maintenu activement et accepte les contributions de source ouverte. Voir le fichier 65 | [CONTRIBUTION](CONTRIBUTING.fr.md) pour plus de détails. 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Français](README.fr.md) 2 | 3 | # Security Aware Wire Protocol parsing library. 4 | 5 | This library contains parsers for various wire protocols, 6 | and is intended to be used in network security sensors. 7 | 8 | Each parser exposes a common interface that allows the sensor 9 | engine to feed bytes into the parser and receive parsed 10 | metadata back. The bytes are expected to be at the session layer, 11 | so the engine is responsible for assembling transport layer 12 | data into a session payload, which is then fed into this library. 13 | 14 | This library aims to be resilient and parse as many messages as 15 | possible that are seen in the wild. If a message is invalid or 16 | out-of-spec, it should not be discarded by the parser. Parsers 17 | will set flags on the message when it fails validation instead 18 | of returning an error. 19 | 20 | The interface to each parser is uniform and simple, consisting of 21 | only a few functions to: 22 | 23 | - test that a payload is or is not the protocol in question 24 | (eg. is this modbus?) 25 | - provide more bytes to the parser 26 | - set callbacks to invoke on per-protocol metadata events (todo) 27 | - indicate that some bytes are unavailable (ie. notify of packet 28 | loss) (todo) 29 | - indicate a session has ended (todo) 30 | 31 | The library exposes Rust and C bindings for easy integration into 32 | existing and future network security sensor platforms. (todo) 33 | 34 | # Usage 35 | Start using SAWP by including a parser in your project's `Cargo.toml` 36 | dependencies. The base library will also be required for using common 37 | types. 38 | 39 | **The minimum supported version of `rustc` is `1.63.0`.** 40 | 41 | ## Example 42 | ``` 43 | [dependencies] 44 | sawp-modbus = "0.13.1" 45 | sawp = "0.13.1" 46 | ``` 47 | 48 | ## FFI Support 49 | Some parsers have a foreign function interface for use in C/C++ projects. 50 | FFI Support can be enabled by building with the `ffi` feature. 51 | 52 | A [Makefile](Makefile) is also provided to ease the build process. Please refer to this file for more in-depth documentation. 53 | 54 | ``` 55 | # Install cbindgen which is required to generate headers 56 | cargo install --force cbindgen 57 | 58 | # Build headers and shared objects 59 | make 60 | ``` 61 | 62 | # Contributing 63 | 64 | This project is actively maintained and accepting open source 65 | contributions. See [CONTRIBUTING](CONTRIBUTING.md) for more details. 66 | -------------------------------------------------------------------------------- /azure/deploy-rpm-steps.yml: -------------------------------------------------------------------------------- 1 | # deploys the provided rpms to the repository 2 | 3 | parameters: 4 | - name: service_connection 5 | displayName: 'Service Connection' 6 | type: string 7 | - name: source_folder 8 | displayName: 'Source Folder' 9 | type: string 10 | - name: source_contents 11 | displayName: 'Source Contents' 12 | type: string 13 | - name: target_folder 14 | displayName: 'Target Folder' 15 | type: string 16 | - name: deploy 17 | displayName: 'Deploy rpms to repo' 18 | type: boolean 19 | 20 | steps: 21 | 22 | # Publish it within the pipeline 23 | - task: PublishBuildArtifacts@1 24 | inputs: 25 | pathToPublish: ${{ parameters.source_folder }} 26 | artifactName: rpms 27 | 28 | # Deploy rpms to repository 29 | - ${{ if eq(parameters.deploy, true) }}: 30 | 31 | # Copy RPM Files to the repository 32 | - task: CopyFilesOverSSH@0 33 | inputs: 34 | sshEndpoint: ${{ parameters.service_connection }} 35 | sourceFolder: ${{ parameters.source_folder }} 36 | contents: ${{ parameters.source_contents }} 37 | targetFolder: ${{ parameters.target_folder }} 38 | cleanTargetFolder: false 39 | overwrite: false 40 | failOnEmptySource: true 41 | flattenFolders: false 42 | 43 | # Update the RPM repo 44 | - task: SSH@0 45 | inputs: 46 | sshEndpoint: ${{ parameters.service_connection }} 47 | runOptions: 'inline' 48 | inline: sudo createrepo ${{ parameters.target_folder }} 49 | interpreterCommand: /bin/bash 50 | -------------------------------------------------------------------------------- /azure/fuzz-steps.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | # Setup 3 | # ============ 4 | # Configure environment 5 | # 6 | 7 | # Use cargo install path 8 | - script: echo "##vso[task.setvariable variable=PATH;]$PATH:/usr/local/cargo/bin" 9 | displayName: set path 10 | 11 | # Give builder access to cached rust install 12 | - script: sudo chown -R AzDevOps /usr/local/cargo /usr/local/rustup 13 | displayName: chown cargo dir 14 | 15 | # QA Steps 16 | # ======== 17 | # 18 | 19 | # Build fuzz 20 | - script: cargo +nightly fuzz build 21 | displayName: build fuzz 22 | -------------------------------------------------------------------------------- /azure/install-rust.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | name: version 3 | type: string 4 | 5 | steps: 6 | - script: rustup update ${{ parameters.version }} 7 | displayName: "Install Rust version (${{ parameters.version }})" 8 | - script: rustup component add clippy --toolchain ${{ parameters.version }} 9 | displayName: "Install Clippy" 10 | -------------------------------------------------------------------------------- /azure/package-steps.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | # Get the package version 3 | - script: echo "##vso[task.setvariable variable=version]$(make version)" 4 | displayName: package version 5 | 6 | # Make rpm 7 | - script: make rpm 8 | displayName: make rpm 9 | 10 | - task: PublishBuildArtifacts@1 11 | inputs: 12 | pathToPublish: target/rpmbuild/SOURCES/sawp-$(version).tar.gz 13 | artifactName: sawp-$(version).tar.gz 14 | 15 | - task: PublishBuildArtifacts@1 16 | inputs: 17 | pathToPublish: target/rpmbuild/RPMS/x86_64/sawp-$(version)-1.x86_64.rpm 18 | artifactName: sawp-$(version)-1.x86_64.rpm -------------------------------------------------------------------------------- /azure/qa-pipeline.yml: -------------------------------------------------------------------------------- 1 | # Default pipeline to build, check and test SAWP 2 | trigger: 3 | - main 4 | - staging 5 | pr: 6 | - main 7 | - staging 8 | 9 | # Run daily at midnight UTC if there has been a code change since the last scheduled run 10 | schedules: 11 | - cron: "0 0 * * *" 12 | displayName: Daily midnight build 13 | branches: 14 | include: 15 | - main 16 | 17 | resources: 18 | - repo: self 19 | 20 | variables: 21 | - name: rust_msrv 22 | value: 1.63.0 23 | 24 | stages: 25 | - stage: test 26 | pool: $(pool) 27 | dependsOn: [] 28 | jobs: 29 | - job: test 30 | displayName: Test SAWP 31 | steps: 32 | - template: qa-steps.yml 33 | parameters: 34 | rust_msrv: ${{ variables.rust_msrv }} 35 | - template: package-steps.yml 36 | 37 | # Check if cargo release would pass 38 | - script: cargo release --allow-branch HEAD --workspace -- patch 39 | displayName: check release 40 | - stage: fuzz 41 | pool: $(pool) 42 | dependsOn: [] 43 | jobs: 44 | - job: fuzz 45 | displayName: Fuzz SAWP 46 | steps: 47 | - template: fuzz-steps.yml 48 | -------------------------------------------------------------------------------- /azure/qa-steps.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | name: rust_msrv 3 | type: string 4 | 5 | steps: 6 | # Setup 7 | # ============ 8 | # Configure environment 9 | # 10 | 11 | # Use cargo install path 12 | - script: echo "##vso[task.setvariable variable=PATH;]$PATH:/usr/local/cargo/bin" 13 | displayName: set path 14 | 15 | # Give builder access to cached rust install 16 | - script: sudo chown -R AzDevOps /usr/local/cargo /usr/local/rustup 17 | displayName: chown cargo dir 18 | 19 | # Get the package version 20 | - script: echo "##vso[task.setvariable variable=version]$(make version)" 21 | displayName: package version 22 | 23 | - template: install-rust.yml 24 | parameters: 25 | version: ${{ parameters.rust_msrv }} 26 | 27 | # QA Steps 28 | # ======== 29 | # 30 | 31 | # Check code formatting differences 32 | - script: cargo fmt --all -- --check 33 | displayName: check fmt 34 | 35 | # Build project 36 | # First, with the minimum supported rust version 37 | - script: cargo +${{ parameters.rust_msrv }} build --workspace --all-targets --all-features --release --locked 38 | displayName: build (msrv) 39 | 40 | # Then, with stable 41 | - script: cargo build --workspace --all-targets --all-features --release --locked 42 | displayName: build (stable) 43 | 44 | # Check linting warnings 45 | # Run clippy with MSRV so it does not error on lints which are MSRV incompatible such as std API changes 46 | - script: cargo +${{ parameters.rust_msrv }} clippy --workspace --all-targets --all-features --locked -- -D warnings 47 | displayName: check clippy 48 | 49 | # Build documentation 50 | - script: cargo doc --workspace --all-features --no-deps --locked 51 | displayName: cargo doc 52 | 53 | # Publish documentation 54 | - task: ArchiveFiles@2 55 | inputs: 56 | rootFolderOrFile: target/doc 57 | includeRootFolder: false 58 | archiveType: tar 59 | tarCompression: gz 60 | archiveFile: $(Build.ArtifactStagingDirectory)/sawp-doc-$(version).tar.gz 61 | replaceExistingArchive: true 62 | 63 | - task: PublishBuildArtifacts@1 64 | inputs: 65 | pathToPublish: $(Build.ArtifactStagingDirectory)/sawp-doc-$(version).tar.gz 66 | artifactName: sawp-$(version).tar.gz 67 | 68 | # Run the unit tests 69 | - script: cargo test --workspace --all-targets --locked 70 | displayName: run tests 71 | 72 | # Run the memory checks 73 | - script: make memcheck 74 | displayName: memcheck 75 | 76 | # Run the doc tests 77 | # Needed until this issue is fixed: https://github.com/rust-lang/cargo/issues/6669 78 | - script: cargo test --workspace --doc --locked 79 | displayName: run doc tests 80 | 81 | # Check code coverage 82 | - script: | 83 | OUTPUT=$(cargo tarpaulin -v --all --all-features --out Xml) 84 | echo "${OUTPUT}" 85 | 86 | if [[ $(echo "${OUTPUT}" | tail -1 | cut -d ' ' -f 1) < 75 ]] 87 | then 88 | echo "Coverage does not meet required percentage" 89 | exit 1 90 | fi 91 | continueOnError: true 92 | displayName: check code coverage 93 | 94 | # Publish the code coverage reports 95 | - task: PublishCodeCoverageResults@1 96 | inputs: 97 | codeCoverageTool: 'cobertura' 98 | summaryFileLocation: cobertura.xml 99 | 100 | # Check if cargo publish would pass 101 | - script: cargo publish --dry-run --locked 102 | displayName: check publish 103 | -------------------------------------------------------------------------------- /azure/release-pipeline.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: src 3 | displayName: Source Branch 4 | type: string 5 | default: main 6 | - name: dest 7 | displayName: Destination Branch 8 | type: string 9 | default: staging 10 | - name: package 11 | displayName: Use --package or --workspace to release all packages 12 | type: string 13 | default: --workspace 14 | - name: level 15 | displayName: Level (major|minor|patch|rc|alpha|beta|release|) 16 | type: string 17 | default: rc 18 | - name: args 19 | displayName: Cargo Release Extra Args 20 | type: string 21 | default: --no-confirm --no-push --no-publish --execute 22 | - name: name 23 | displayName: Author Name 24 | type: string 25 | default: sa-sawp 26 | - name: email 27 | displayName: Author Email 28 | type: string 29 | default: sa-sawp@cyber.gc.ca 30 | - name: deploy 31 | displayName: 'Deploy rpm to repo?' 32 | type: boolean 33 | default: true 34 | 35 | variables: 36 | - name: rpm_dir 37 | value: target/rpmbuild/RPMS/x86_64/ 38 | - name: rust_msrv 39 | value: 1.63.0 40 | 41 | trigger: none 42 | pr: none 43 | 44 | resources: 45 | - repo: self 46 | 47 | jobs: 48 | - job: release_sawp 49 | displayName: Release SAWP 50 | pool: $(pool) 51 | steps: 52 | # Setup 53 | # ============ 54 | # Configure environment 55 | # 56 | 57 | # git >= 2.0 is required 58 | - script: echo "##vso[task.setvariable variable=PATH;]/opt/rh/sclo-git25/root/usr/bin:$PATH" 59 | displayName: set path 60 | 61 | # SAWP Steps 62 | # ============== 63 | - checkout: self 64 | clean: true 65 | path: sawp 66 | persistCredentials: true 67 | 68 | # Configure author to commit release with 69 | - script: git config user.name ${{ parameters.name }} 70 | - script: git config user.email ${{ parameters.email }} 71 | 72 | # The src branch is the code you want to release -- e.g. main 73 | # The dest branch is where you want the code to go -- e.g. staging 74 | - script: git remote update origin 75 | displayName: update 76 | - script: git checkout ${{ parameters.dest }} && git pull --ff-only origin ${{ parameters.dest }} && git merge --ff-only origin/${{ parameters.src }} || git checkout -b ${{ parameters.dest }} origin/${{ parameters.src }} 77 | displayName: merge 78 | 79 | # Check all contributors are in .contributors list 80 | - script: chmod +x ./.contributors.sh && ./.contributors.sh -r ${{ parameters.dest }} 81 | displayName: contributors 82 | 83 | # QA Checks 84 | # ============== 85 | - template: qa-steps.yml 86 | parameters: 87 | rust_msrv: ${{ variables.rust_msrv }} 88 | 89 | # Release 90 | # ============== 91 | 92 | # Commit and tag the release 93 | - script: cargo release ${{ parameters.package }} ${{ parameters.args }} -- ${{ parameters.level }} 94 | displayName: cargo release 95 | 96 | # Don't push with cargo release because it will fail 97 | - script: git push --follow-tags origin ${{ parameters.dest }} 98 | 99 | # Release Artifacts 100 | # ============== 101 | - template: package-steps.yml 102 | 103 | # Push RPM to repository 104 | - template: deploy-rpm-steps.yml 105 | parameters: 106 | service_connection: $(rpm_service_connection) 107 | source_folder: $(rpm_dir) 108 | source_contents: "*.rpm" 109 | target_folder: $(rpm_repo_dir) 110 | deploy: ${{ parameters.deploy }} 111 | -------------------------------------------------------------------------------- /benches/modbus.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | 3 | // TODO: benchmark modbus protocol parsing 4 | fn modbus(n: u64) -> bool { 5 | n == 20 6 | } 7 | 8 | fn criterion_benchmark(c: &mut Criterion) { 9 | c.bench_function("modbus", |b| b.iter(|| modbus(black_box(20)))); 10 | } 11 | 12 | criterion_group!(benches, criterion_benchmark); 13 | criterion_main!(benches); 14 | -------------------------------------------------------------------------------- /cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C++" 2 | pragma_once = true 3 | 4 | namespace = "sawp" 5 | 6 | autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Do NOT modify manually */" 7 | 8 | after_includes = ''' 9 | 10 | struct Ipv4Addr; 11 | struct Ipv6Addr; 12 | struct IpAddr;''' 13 | 14 | # If this option is true `usize` and `isize` will be converted into `size_t` and `ptrdiff_t` 15 | # instead of `uintptr_t` and `intptr_t` respectively. 16 | usize_is_size_t = true 17 | 18 | [export] 19 | include = ["Direction", "Vec"] 20 | 21 | [parse.expand] 22 | crates = ["sawp"] 23 | all_features = true 24 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # SAWP Dockerfile 2 | # =============== 3 | # 4 | # This file is for testing the `make install` command and shouldn't be 5 | # used in production. 6 | # 7 | # Example build command: 8 | # ``` 9 | # docker build -f docker/Dockerfile -t sawp:latest . 10 | # ``` 11 | FROM centos:7 12 | 13 | # Package dependencies to install rust, cbindgen and to build an rpm 14 | RUN (yum makecache \ 15 | && yum install -y \ 16 | gcc \ 17 | make \ 18 | rpm-build \ 19 | wget \ 20 | && yum clean all) 21 | 22 | # Install Rust Toolchain 23 | # Steps taken from https://github.com/rust-lang/docker-rust/blob/master/Dockerfile-debian.template 24 | ENV RUSTUP_HOME=/usr/local/rustup 25 | ENV CARGO_HOME=/usr/local/cargo 26 | ENV PATH=/usr/local/cargo/bin:$PATH 27 | ENV RUST_VERSION=1.52.1 28 | ENV RUSTUP_VERSION=1.24.2 29 | ENV RUSTUP_ARCH=x86_64-unknown-linux-gnu 30 | ENV RUSTUP_URL="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${RUSTUP_ARCH}/rustup-init" 31 | 32 | RUN (set -ex && wget -qO rustup-init "${RUSTUP_URL}" \ 33 | && chmod +x rustup-init \ 34 | && ./rustup-init -y \ 35 | --no-modify-path \ 36 | --profile minimal \ 37 | --default-toolchain ${RUST_VERSION} \ 38 | --default-host ${RUSTUP_ARCH} \ 39 | && rm rustup-init \ 40 | && chmod -R a+w ${RUSTUP_HOME} ${CARGO_HOME} \ 41 | && rustup --version \ 42 | && cargo --version \ 43 | && rustc --version) 44 | 45 | # Cargo dependencies 46 | RUN cargo install cbindgen 47 | 48 | # Install SAWP 49 | RUN mkdir /scratch 50 | COPY . /scratch 51 | 52 | # Install SAWP 53 | RUN (cd /scratch \ 54 | && make \ 55 | && make install) 56 | 57 | # Alternatively, build and install the rpms 58 | #RUN (cd /scratch \ 59 | # && make \ 60 | # && make rpm \ 61 | # && rpm -ivh /scratch/target/rpmbuild/RPMS/x86_64/*.rpm) 62 | 63 | # Post install 64 | RUN ldconfig 65 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "sawp-fuzz" 4 | version = "0.0.0" 5 | authors = ["Canadian Centre for Cyber Security "] 6 | publish = false 7 | edition = "2021" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.3" 14 | sawp = { path = ".." } 15 | sawp-modbus = { path = "../sawp-modbus" } 16 | sawp-diameter = { path = "../sawp-diameter" } 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "fuzz_modbus" 24 | path = "fuzz_targets/fuzz_modbus.rs" 25 | test = false 26 | doc = false 27 | 28 | [[bin]] 29 | name = "fuzz_diameter" 30 | path = "fuzz_targets/fuzz_diameter.rs" 31 | test = false 32 | doc = false 33 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_diameter.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use sawp_diameter::Diameter; 5 | use sawp::parser::{Parse, Direction}; 6 | 7 | fuzz_target!(|data: &[u8]| { 8 | let parser = Diameter {}; 9 | if let Err(e) = parser.parse(data, Direction::Unknown) { 10 | eprintln!("Diameter: Error parsing {:?}", e); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_modbus.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use sawp_modbus::Modbus; 5 | use sawp::parser::{Parse, Direction}; 6 | 7 | fuzz_target!(|data: &[u8]| { 8 | let parser = Modbus::default(); 9 | if let Err(e) = parser.parse(data, Direction::Unknown) { 10 | eprintln!("Modbus: Error parsing {:?}", e); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | # These settings are shared across all workspace packages 2 | # Use Cargo.toml to customize settings for a particular package. 3 | enable-all-features=true 4 | 5 | # Handled in CI pipeline. 6 | publish=false 7 | push=false 8 | 9 | # Release all workspace packages in a single commit. 10 | consolidate-commits=true 11 | 12 | # There is a missing feature in cargo-release preventing replacements 13 | # like {{version}} from working in "consolidate-commits" mode. 14 | pre-release-commit-message="sawp: release package(s)" 15 | 16 | # Tags have an rpm-like versioning scheme. 17 | # E.g. sawp-0.1.0-rc.1 18 | tag-prefix="{{crate_name}}" 19 | tag-name="{{prefix}}-{{version}}" 20 | 21 | # This will update the version of sawp in workspace packages 22 | # for all releases (major|minor|patch|rc|...). 23 | dependent-version="upgrade" 24 | 25 | # Updates the README.md and CHANGELOG.md so we don't have to 26 | pre-release-replacements = [ 27 | {file="README.md", search="sawp = \"[a-z0-9\\.-]+\"", replace="sawp = \"{{version}}\"", exactly=1}, 28 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}"}, 29 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}"}, 30 | {file="CHANGELOG.md", search="compare/[a-z0-9\\.-]+\\.\\.\\.HEAD", replace="releases/tag/{{tag_name}}", exactly=1}, 31 | {file="CHANGELOG.md", search="", replace="\n\n## [Unreleased] - ReleaseDate", exactly=1}, 32 | {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/CybercentreCanada/sawp/compare/{{tag_name}}...HEAD", exactly=1}, 33 | ] 34 | -------------------------------------------------------------------------------- /sawp-diameter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-diameter" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for Diameter" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["diameter", "parser", "protocol", "mobility", "core-network"] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | verbose = ["sawp/verbose"] 22 | 23 | [dependencies] 24 | sawp = { path = "..", version = "^0.13.1" } 25 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 26 | nom = "7.1.1" 27 | num_enum = "0.5.1" 28 | 29 | [dev-dependencies] 30 | rstest = "0.6.4" 31 | 32 | [lib] 33 | crate-type = ["staticlib", "rlib", "cdylib"] 34 | 35 | # Override default replacements 36 | [package.metadata.release] 37 | pre-release-replacements = [] 38 | -------------------------------------------------------------------------------- /sawp-dns/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-dns" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for DNS" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CyberCentreCanada/sawp" 10 | homepage = "https://github.com/CyberCentreCanada/sawp" 11 | keywords = ["dns", "parser", "protocol",] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | ffi = ["cbindgen", "sawp/ffi", "sawp-ffi"] 22 | verbose = ["sawp/verbose"] 23 | 24 | [build-dependencies] 25 | cbindgen = {version = "0.15", optional = true} 26 | 27 | [dependencies] 28 | sawp-ffi = {path = "../sawp-ffi", version = "^0.13.1", optional = true} 29 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 30 | sawp = {path = "..", version = "^0.13.1" } 31 | nom = "7.1.1" 32 | num_enum = "0.5.1" 33 | byteorder = "1.4.3" 34 | 35 | [lib] 36 | crate-type = ["staticlib", "rlib", "cdylib"] 37 | 38 | [dev-dependencies] 39 | rstest = "0.6.4" 40 | 41 | # Override default replacements 42 | [package.metadata.release] 43 | pre-release-replacements = [] 44 | -------------------------------------------------------------------------------- /sawp-dns/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C++" 2 | pragma_once = true 3 | 4 | includes = ["sawp.h"] 5 | namespaces = ["sawp", "dns"] 6 | 7 | autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Do NOT modify manually */" 8 | 9 | # If this option is true `usize` and `isize` will be converted into `size_t` and `ptrdiff_t` 10 | # instead of `uintptr_t` and `intptr_t` respectively. 11 | usize_is_size_t = true 12 | 13 | [export] 14 | exclude = ["Vec"] 15 | # Need includes for Flags, since they aren't referenced by public FFI functions 16 | include = ["ErrorFlags"] 17 | 18 | [parse.expand] 19 | crates = ["sawp", "sawp-dns"] 20 | all_features = true 21 | -------------------------------------------------------------------------------- /sawp-dns/src/answer.rs: -------------------------------------------------------------------------------- 1 | use nom::bytes::streaming::take; 2 | use nom::number::complete::be_u32; 3 | use nom::number::streaming::be_u16; 4 | 5 | use sawp_flags::{Flag, Flags}; 6 | 7 | use crate::enums::{RecordClass, RecordType}; 8 | use crate::rdata::RDataType; 9 | use crate::{custom_count, ErrorFlags, IResult, Name}; 10 | 11 | #[cfg(feature = "ffi")] 12 | use sawp_ffi::GenerateFFI; 13 | 14 | /// Per RFC1035/RFC4408: max RDATA len = 65535 octets. Since TXT RDATA includes a length byte before 15 | /// each TXT string, min size per TXT is 2 bytes, leaving maximum of 65535/2 parser runs needed. 16 | const MAX_TXT_PARSES: usize = 32767; 17 | /// First three bytes of an OPT AR - determines whether an AR should be parsed with special "OPT logic". 18 | const OPT_RR_START: [u8; 3] = [0, 0, 41]; 19 | 20 | /// A parsed DNS answer 21 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 22 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 23 | #[derive(Debug, PartialEq, Eq)] 24 | pub struct Answer { 25 | pub name: Vec, 26 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 27 | pub rtype: RecordType, 28 | pub rtype_raw: u16, 29 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 30 | pub rclass: RecordClass, 31 | pub rclass_raw: u16, 32 | pub ttl: u32, 33 | pub data: RDataType, 34 | } 35 | 36 | impl Answer { 37 | fn parse<'a>( 38 | input: &'a [u8], 39 | reference_bytes: &'a [u8], 40 | ) -> IResult<'a, (Answer, Flags)> { 41 | let (input, (name, mut error_flags)) = Name::parse(reference_bytes)(input)?; 42 | 43 | let (input, working_rtype) = be_u16(input)?; 44 | let rtype = RecordType::from_raw(working_rtype); 45 | if rtype == RecordType::UNKNOWN { 46 | error_flags |= ErrorFlags::UnknownRtype; 47 | } 48 | 49 | let (input, working_rclass) = be_u16(input)?; 50 | let rclass = RecordClass::from_raw(working_rclass); 51 | if rclass == RecordClass::UNKNOWN { 52 | error_flags |= ErrorFlags::UnknownRclass; 53 | } 54 | 55 | let (input, ttl) = be_u32(input)?; 56 | 57 | let mut answer: Answer = Answer { 58 | name, 59 | rtype, 60 | rtype_raw: working_rtype, 61 | rclass, 62 | rclass_raw: working_rclass, 63 | ttl, 64 | data: RDataType::UNKNOWN(vec![]), 65 | }; 66 | 67 | let (input, data_len) = be_u16(input)?; 68 | let (rem, local_data) = take(data_len)(input)?; 69 | 70 | // always call once 71 | let (mut local_data, (mut rdata, inner_error_flags)) = 72 | RDataType::parse(local_data, reference_bytes, rtype)?; 73 | error_flags |= inner_error_flags; 74 | 75 | // get ref to buffer we will extend first, if TXT 76 | if let RDataType::TXT(ref mut current_rdata) = rdata { 77 | for _ in 0..MAX_TXT_PARSES - 1 { 78 | if local_data.is_empty() { 79 | break; 80 | } 81 | let (new_data, (rdata, inner_error_flags)) = 82 | RDataType::parse(local_data, reference_bytes, rtype)?; 83 | error_flags |= inner_error_flags; 84 | if let RDataType::TXT(new_rdata) = rdata { 85 | current_rdata.extend(new_rdata); 86 | local_data = new_data; 87 | } else { 88 | break; 89 | } 90 | } 91 | } 92 | answer.data = rdata; 93 | Ok((rem, (answer, error_flags))) 94 | } 95 | 96 | fn parse_additional<'a>( 97 | input: &'a [u8], 98 | reference_bytes: &'a [u8], 99 | ) -> IResult<'a, (Answer, Flags, bool)> { 100 | let mut opt_rr_present = false; 101 | if input.len() >= 3 && input[0..3] == OPT_RR_START[0..3] { 102 | let (input, (data, inner_error_flags)) = RDataType::parse_rdata_opt(&input[3..])?; 103 | opt_rr_present = true; 104 | Ok(( 105 | input, 106 | ( 107 | Answer { 108 | name: vec![0], // OPT RRs must be named 0 109 | rtype: RecordType::OPT, 110 | rtype_raw: 41, 111 | rclass: RecordClass::NONE, // OPT RRs have no class 112 | rclass_raw: 254, 113 | ttl: 0, // OPT RRs do not contain a TTL 114 | data, 115 | }, 116 | inner_error_flags, 117 | opt_rr_present, 118 | ), 119 | )) 120 | } else { 121 | let (input, (answer, inner_error_flags)) = Answer::parse(input, reference_bytes)?; 122 | Ok((input, (answer, inner_error_flags, opt_rr_present))) 123 | } 124 | } 125 | 126 | pub fn parse_additionals<'a>( 127 | input: &'a [u8], 128 | reference_bytes: &'a [u8], 129 | acnt: usize, 130 | ) -> IResult<'a, (Vec, Flags)> { 131 | let mut opt_rr_present = false; 132 | let mut error_flags = ErrorFlags::none(); 133 | let (input, answers) = custom_count( 134 | |input, reference_bytes| { 135 | let (input, (answer, inner_error_flags, inner_opt_rr_present)) = 136 | Answer::parse_additional(input, reference_bytes)?; 137 | if inner_opt_rr_present { 138 | if opt_rr_present { 139 | error_flags |= ErrorFlags::ExtraOptRr; 140 | } else { 141 | opt_rr_present = true; 142 | } 143 | } 144 | error_flags |= inner_error_flags; 145 | Ok((input, answer)) 146 | }, 147 | acnt, 148 | )(input, reference_bytes)?; 149 | 150 | Ok((input, (answers, error_flags))) 151 | } 152 | 153 | pub fn parse_answers<'a>( 154 | input: &'a [u8], 155 | reference_bytes: &'a [u8], 156 | acnt: usize, 157 | ) -> IResult<'a, (Vec, Flags)> { 158 | let mut error_flags = ErrorFlags::none(); 159 | let (input, answers) = custom_count( 160 | |input, reference_bytes| { 161 | let (input, (answer, inner_error_flags)) = Answer::parse(input, reference_bytes)?; 162 | error_flags |= inner_error_flags; 163 | Ok((input, answer)) 164 | }, 165 | acnt, 166 | )(input, reference_bytes)?; 167 | 168 | Ok((input, (answers, error_flags))) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /sawp-dns/src/edns.rs: -------------------------------------------------------------------------------- 1 | //! Extended DNS 2 | //! [RFC6891](https://tools.ietf.org/html/rfc6891) 3 | //! 4 | //! EDNS adds information to DNS messages in the form of pseudo-Resource Records 5 | //! ("pseudo-RR"s) included in the "additional data" section of a DNS message. 6 | //! This takes the form of an OPT RR which contains a UDP payload size the server supports, 7 | //! an EDNS version number, flags, an extended RCode, and possibly a variable number of KVP options. 8 | //! Most notably this allows servers to advertise that they can process UDP messages of size > 512 9 | //! bytes (the default maximum for DNS over UDP). 10 | 11 | use nom::bytes::streaming::take; 12 | use nom::number::streaming::be_u16; 13 | 14 | use num_enum::TryFromPrimitive; 15 | 16 | use sawp_flags::{Flag, Flags}; 17 | 18 | use std::convert::TryFrom; 19 | 20 | use crate::{custom_many0, ErrorFlags, IResult}; 21 | #[cfg(feature = "ffi")] 22 | use sawp_ffi::GenerateFFI; 23 | 24 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 25 | #[repr(u16)] 26 | pub enum OptionCode { 27 | /// Long-Lived Queries 28 | LLQ = 1, 29 | /// Update Leases 30 | UL = 2, 31 | /// Name Server Identifier 32 | NSID = 3, 33 | /// DNSSEC Algorithm Understood 34 | DAU = 5, 35 | /// DS Hash Understood 36 | DHU = 6, 37 | /// NSEC3 Hash Understood 38 | N3U = 7, 39 | /// EDNS0 option to allow Recursive Resolvers, if they are willing, to forward details about the origin network from which a query is coming when talking to other nameservers 40 | EDNSCLIENTSUBNET = 8, 41 | /// See https://tools.ietf.org/html/rfc7314 42 | EDNSEXPIRE = 9, 43 | /// See https://tools.ietf.org/html/rfc7873#section-4 44 | COOKIE = 10, 45 | /// Signals a variable idle timeout 46 | EDNSTCPKEEPALIVE = 11, 47 | /// Allows DNS clients and servers to pad requests and responses by a variable number of octets 48 | PADDING = 12, 49 | /// Allows a security-aware validating resolver to send a single query requesting a complete validation path along with the regular answer 50 | CHAIN = 13, 51 | /// Provides origin authentication using digital signatures 52 | EDNSKEYTAG = 14, 53 | /// Returning additional information about the cause of DNS errors 54 | EDNSERROR = 15, 55 | /// Draft, usage is being determined. See https://www.ietf.org/archive/id/draft-bellis-dnsop-edns-tags-01.txt 56 | EDNSCLIENTTAG = 16, 57 | /// Draft, usage is being determined. See https://www.ietf.org/archive/id/draft-bellis-dnsop-edns-tags-01.txt 58 | EDNSSERVERTAG = 17, 59 | /// A way of identifying a device via DNS in the OPT RDATA 60 | DEVICEID = 26946, 61 | UNKNOWN, 62 | } 63 | 64 | impl OptionCode { 65 | pub fn from_raw(val: u16) -> Self { 66 | OptionCode::try_from(val).unwrap_or(OptionCode::UNKNOWN) 67 | } 68 | } 69 | 70 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 71 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 72 | #[derive(Debug, PartialEq, Eq)] 73 | pub struct EdnsOption { 74 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 75 | pub code: OptionCode, 76 | pub data: Vec, 77 | } 78 | 79 | impl EdnsOption { 80 | pub fn parse(input: &[u8]) -> IResult<(EdnsOption, Flags)> { 81 | let (input, (code, inner_error_flags)) = EdnsOption::parse_option_code(input)?; 82 | let (input, option_length) = be_u16(input)?; 83 | let (input, data) = take(option_length)(input)?; 84 | 85 | Ok(( 86 | input, 87 | ( 88 | EdnsOption { 89 | code, 90 | data: data.to_vec(), 91 | }, 92 | inner_error_flags, 93 | ), 94 | )) 95 | } 96 | 97 | fn parse_option_code(input: &[u8]) -> IResult<(OptionCode, Flags)> { 98 | let mut error_flags = ErrorFlags::none(); 99 | 100 | let (input, raw_option_code) = be_u16(input)?; 101 | let code = OptionCode::from_raw(raw_option_code); 102 | if code == OptionCode::UNKNOWN { 103 | error_flags |= ErrorFlags::EdnsParseFail; 104 | } 105 | Ok((input, (code, error_flags))) 106 | } 107 | 108 | pub fn parse_options( 109 | input: &[u8], 110 | data_len: u16, 111 | ) -> IResult<(Vec, Flags)> { 112 | let mut error_flags = ErrorFlags::none(); 113 | if data_len < 4 { 114 | return Ok((input, (vec![], error_flags))); 115 | } 116 | 117 | let (input, options) = custom_many0(|input| { 118 | let (input, (option, inner_error_flags)) = EdnsOption::parse(input)?; 119 | error_flags |= inner_error_flags; 120 | Ok((input, option)) 121 | })(input)?; 122 | 123 | Ok((input, (options, error_flags))) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /sawp-dns/src/enums.rs: -------------------------------------------------------------------------------- 1 | use num_enum::TryFromPrimitive; 2 | 3 | use std::convert::TryFrom; 4 | 5 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 6 | #[repr(u16)] 7 | pub enum RecordType { 8 | /// Address record 9 | A = 1, 10 | /// Name server record 11 | NS = 2, 12 | /// Obsolete 13 | MD = 3, 14 | /// Obsolete 15 | MF = 4, 16 | /// Canonical name record 17 | CNAME = 5, 18 | /// Start of authority record 19 | SOA = 6, 20 | /// Unlikely to be adopted. See https://tools.ietf.org/html/rfc2505 21 | MB = 7, 22 | /// Unlikely to be adopted. See https://tools.ietf.org/html/rfc2505 23 | MG = 8, 24 | /// Unlikely to be adopted. See https://tools.ietf.org/html/rfc2505 25 | MR = 9, 26 | /// Obsolete 27 | NUL = 10, 28 | /// Not to be relied upon. See https://tools.ietf.org/html/rfc1123 29 | WKS = 11, 30 | /// PTR resource record (pointer to a canonical name) 31 | PTR = 12, 32 | /// Host information 33 | HINFO = 13, 34 | /// Unlikely to be adopted. See https://tools.ietf.org/html/rfc2505 35 | MINFO = 14, 36 | /// Mail exchange record 37 | MX = 15, 38 | /// Text record 39 | TXT = 16, 40 | /// Responsible person 41 | RP = 17, 42 | /// AFS database record 43 | AFSDB = 18, 44 | /// Maps a domain name to a PSDN address number 45 | X25 = 19, 46 | /// Maps a domain name to an ISDN telephone number 47 | ISDN = 20, 48 | /// Specifies intermediate host routing to host with the name of the RT-record 49 | RT = 21, 50 | /// Maps a domain name to an NSAP address 51 | NSAP = 22, 52 | /// Facilitates translation from NSAP address to DNS name 53 | NSAPPTR = 23, 54 | /// Signature - obsolete 55 | SIG = 24, 56 | /// Key record - obsolete 57 | KEY = 25, 58 | /// Pointer to X.400/RFC822 mapping information 59 | PX = 26, 60 | /// A more limited early version of LOC 61 | GPOS = 27, 62 | /// IPv6 address record 63 | AAAA = 28, 64 | /// Location record 65 | LOC = 29, 66 | /// Obsolete 67 | NXT = 30, 68 | /// Never made it to RFC status. See https://tools.ietf.org/html/draft-ietf-nimrod-dns-00 69 | EID = 31, 70 | /// Never made it to RFC status. See https://tools.ietf.org/html/draft-ietf-nimrod-dns-00 71 | NIMLOC = 32, 72 | /// Service locator 73 | SRV = 33, 74 | /// Defined by the ATM forum committee 75 | ATMA = 34, 76 | /// Naming authority pointer 77 | NAPTR = 35, 78 | /// Key exchanger record 79 | KX = 36, 80 | /// Certificate record 81 | CERT = 37, 82 | /// Obsolete 83 | A6 = 38, 84 | /// Delegation name record 85 | DNAME = 39, 86 | /// Never made it to RFC status. See https://tools.ietf.org/html/draft-eastlake-kitchen-sink 87 | SINK = 40, 88 | /// Option (needed to support EDNS) 89 | OPT = 41, 90 | /// Address prefix list 91 | APL = 42, 92 | /// Delegation signer 93 | DS = 43, 94 | /// SSH public key fingerprint 95 | SSHFP = 44, 96 | /// IPsec key 97 | IPSECKEY = 45, 98 | /// DNSSEC signature 99 | RRSIG = 46, 100 | /// Next Secure record 101 | NSEC = 47, 102 | /// DNS Key record 103 | DNSKEY = 48, 104 | /// DHCP identifier 105 | DHCID = 49, 106 | /// Next Secure record version 3 107 | NSEC3 = 50, 108 | /// NSEC3 parameters 109 | NSEC3PARAM = 51, 110 | /// TLSA certificate association 111 | TLSA = 52, 112 | /// S/MIME cert association 113 | SMIMEA = 53, 114 | /// Host Identity Protocol 115 | HIP = 55, 116 | /// Expired without adoption by IETF 117 | NINFO = 56, 118 | /// Expired without adoption by IETF 119 | RKEY = 57, 120 | /// Never made it to RFC status. See https://tools.ietf.org/html/draft-wijngaards-dnsop-trust-history-02 121 | TALINK = 58, 122 | /// Child DS 123 | CDS = 59, 124 | /// Child DNSKEY 125 | CDNSKEY = 60, 126 | /// OpenPGP public key record 127 | OPENPGPKEY = 61, 128 | /// Child-to-parent synchronization 129 | CSYNC = 62, 130 | /// Draft: see https://tools.ietf.org/html/draft-ietf-dnsop-dns-zone-digest-14 131 | ZONEMD = 63, 132 | /// Draft: see https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/00 133 | SVCB = 64, 134 | /// Draft: see https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/00 135 | HTTPS = 65, 136 | /// Obsolete 137 | SPF = 99, 138 | /// IANA reserved 139 | UINFO = 100, 140 | /// IANA reserved 141 | UID = 101, 142 | /// IANA reserved 143 | GID = 102, 144 | /// IANA reserved 145 | UNSPEC = 103, 146 | /// Experimental: see https://tools.ietf.org/html/rfc6742 147 | NID = 104, 148 | /// Experimental: see https://tools.ietf.org/html/rfc6742 149 | L32 = 105, 150 | /// Experimental: see https://tools.ietf.org/html/rfc6742 151 | L64 = 106, 152 | /// Experimental: see https://tools.ietf.org/html/rfc6742 153 | LP = 107, 154 | /// MAC address (EUI-48) 155 | EUI48 = 108, 156 | /// MAC address (EUI-64) 157 | EUI64 = 109, 158 | /// Transaction key record 159 | TKEY = 249, 160 | /// Transaction signature 161 | TSIG = 250, 162 | /// Incremental zone transfer 163 | IXFR = 251, 164 | /// Authoritative zone transfer 165 | AXFR = 252, 166 | /// Returns MB, MG, MR, or MINFO. Unlikely to be adopted. 167 | MAILB = 253, 168 | /// Obsolete 169 | MAILA = 254, 170 | /// All cached records 171 | ANY = 255, 172 | /// Uniform resource identifier 173 | URI = 256, 174 | /// Certification authority authorization 175 | CAA = 257, 176 | /// Application visibility and control 177 | AVC = 258, 178 | /// Digital object architecture 179 | DOA = 259, 180 | /// Automatic multicast tunneling relay 181 | AMTRELAY = 260, 182 | /// DNSSEC trust authorities 183 | TA = 32768, 184 | /// DNSSEC lookaside validation record 185 | DLV = 32769, 186 | UNKNOWN, 187 | } 188 | 189 | impl RecordType { 190 | pub fn from_raw(val: u16) -> Self { 191 | RecordType::try_from(val).unwrap_or(RecordType::UNKNOWN) 192 | } 193 | } 194 | 195 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 196 | #[repr(u16)] 197 | pub enum RecordClass { 198 | IN = 1, 199 | CH = 3, 200 | HS = 4, 201 | NONE = 254, 202 | ANY = 255, 203 | UNKNOWN, 204 | } 205 | 206 | impl RecordClass { 207 | pub fn from_raw(val: u16) -> Self { 208 | RecordClass::try_from(val).unwrap_or(RecordClass::UNKNOWN) 209 | } 210 | } 211 | 212 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 213 | #[repr(u16)] 214 | pub enum OpCode { 215 | QUERY = 0, 216 | IQUERY = 1, 217 | STATUS = 2, 218 | NOTIFY = 4, 219 | UPDATE = 5, 220 | DSO = 6, 221 | UNKNOWN, 222 | } 223 | 224 | impl OpCode { 225 | pub fn from_raw(val: u16) -> Self { 226 | OpCode::try_from(val).unwrap_or(OpCode::UNKNOWN) 227 | } 228 | } 229 | 230 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 231 | #[repr(u16)] 232 | pub enum ResponseCode { 233 | /// No error condition. 234 | NOERROR = 0, 235 | /// The name server was unable to interpret the query. 236 | FORMATERROR = 1, 237 | /// There was a problem with the name server. 238 | SERVERFAILURE = 2, 239 | /// The domain name referenced in the query does not exist. 240 | NAMEERROR = 3, 241 | /// The name server does not support the requested kind of query. 242 | NOTIMPLEMENTED = 4, 243 | /// The name server's policy forbids providing this information. 244 | REFUSED = 5, 245 | /// Name exists when it should not. 246 | YXDOMAIN = 6, 247 | /// RR set exists when it should not. 248 | YXRRSET = 7, 249 | /// RR set that should exist does not. 250 | NXRRSET = 8, 251 | /// Server not authoritative for zone or client not authorized. 252 | NOTAUTH = 9, 253 | /// Name not in contained zone. 254 | NOTZONE = 10, 255 | UNKNOWN, 256 | } 257 | 258 | impl ResponseCode { 259 | pub fn from_raw(val: u16) -> Self { 260 | ResponseCode::try_from(val).unwrap_or(ResponseCode::UNKNOWN) 261 | } 262 | } 263 | 264 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 265 | #[repr(u16)] 266 | pub enum OptResponseCode { 267 | /// No error condition. 268 | NOERROR = 0, 269 | /// The name server was unable to interpret the query. 270 | FORMATERROR = 1, 271 | /// There was a problem with the name server. 272 | SERVERFAILURE = 2, 273 | /// The domain name referenced in the query does not exist. 274 | NAMEERROR = 3, 275 | /// The name server does not support the requested kind of query. 276 | NOTIMPLEMENTED = 4, 277 | /// The name server's policy forbids providing this information. 278 | REFUSED = 5, 279 | /// Name exists when it should not. 280 | YXDOMAIN = 6, 281 | /// RR set exists when it should not. 282 | YXRRSET = 7, 283 | /// RR set that should exist does not. 284 | NXRRSET = 8, 285 | /// Server not authoritative for zone or client not authorized. 286 | NOTAUTH = 9, 287 | /// Name not in contained zone. 288 | NOTZONE = 10, 289 | /// Bad OPT version 290 | BADVERSION = 16, 291 | /// Bad/missing server cookie 292 | BADCOOKIE = 23, 293 | UNKNOWN, 294 | } 295 | 296 | impl OptResponseCode { 297 | pub fn from_raw(val: u16) -> Self { 298 | OptResponseCode::try_from(val).unwrap_or(OptResponseCode::UNKNOWN) 299 | } 300 | } 301 | 302 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 303 | #[repr(u16)] 304 | pub enum TSigResponseCode { 305 | /// No error condition. 306 | NOERROR = 0, 307 | /// The name server was unable to interpret the query. 308 | FORMATERROR = 1, 309 | /// There was a problem with the name server. 310 | SERVERFAILURE = 2, 311 | /// The domain name referenced in the query does not exist. 312 | NAMEERROR = 3, 313 | /// The name server does not support the requested kind of query. 314 | NOTIMPLEMENTED = 4, 315 | /// The name server's policy forbids providing this information. 316 | REFUSED = 5, 317 | /// Name exists when it should not. 318 | YXDOMAIN = 6, 319 | /// RR set exists when it should not. 320 | YXRRSET = 7, 321 | /// RR set that should exist does not. 322 | NXRRSET = 8, 323 | /// Server not authoritative for zone or client not authorized. 324 | NOTAUTH = 9, 325 | /// Name not in contained zone. 326 | NOTZONE = 10, 327 | /// Bad OPT version 328 | BADSIGNATURE = 16, 329 | /// Key not recognized 330 | BADKEY = 17, 331 | /// Signature out of time window 332 | BADTIME = 18, 333 | /// Bad TKEY mode 334 | BADMODE = 19, 335 | /// Duplicate key name 336 | BADNAME = 20, 337 | /// Algorithm not supported 338 | BADALG = 21, 339 | /// Bad truncation 340 | BADTRUNC = 22, 341 | /// Bad/missing server cookie 342 | BADCOOKIE = 23, 343 | UNKNOWN, 344 | } 345 | 346 | impl TSigResponseCode { 347 | pub fn from_raw(val: u16) -> Self { 348 | TSigResponseCode::try_from(val).unwrap_or(TSigResponseCode::UNKNOWN) 349 | } 350 | } 351 | 352 | /// Indicates whether the message is a query or response. 353 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 354 | #[repr(u8)] 355 | pub enum QueryResponse { 356 | Query = 0, 357 | Response = 1, 358 | } 359 | 360 | /// Helper enum for determining where to store parsed answers in the message 361 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 362 | #[repr(u8)] 363 | pub enum AnswerType { 364 | Answer = 0, 365 | Nameserver = 1, 366 | Additional = 2, 367 | } 368 | 369 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 370 | #[repr(u8)] 371 | pub enum SshfpAlgorithm { 372 | RESERVED = 0, 373 | RSA = 1, 374 | DSA = 2, 375 | ECDSA = 3, 376 | Ed25519 = 4, 377 | UNKNOWN, 378 | } 379 | 380 | impl SshfpAlgorithm { 381 | pub fn from_raw(val: u8) -> Self { 382 | SshfpAlgorithm::try_from(val).unwrap_or(SshfpAlgorithm::UNKNOWN) 383 | } 384 | } 385 | 386 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 387 | #[repr(u8)] 388 | pub enum SshfpFingerprint { 389 | RESERVED = 0, 390 | SHA1 = 1, 391 | SHA256 = 2, 392 | UNKNOWN, 393 | } 394 | 395 | impl SshfpFingerprint { 396 | pub fn from_raw(val: u8) -> Self { 397 | SshfpFingerprint::try_from(val).unwrap_or(SshfpFingerprint::UNKNOWN) 398 | } 399 | } 400 | 401 | #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] 402 | #[repr(u16)] 403 | pub enum TkeyMode { 404 | RESERVED = 0, 405 | ServerAssignment = 1, 406 | DiffieHelmanExchange = 2, 407 | GssApiNegotiation = 3, 408 | ResolverAssignment = 4, 409 | KeyDeletion = 5, 410 | UNKNOWN, 411 | } 412 | 413 | impl TkeyMode { 414 | pub fn from_raw(val: u16) -> Self { 415 | TkeyMode::try_from(val).unwrap_or(TkeyMode::UNKNOWN) 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /sawp-dns/src/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::*; 3 | use sawp::error::Error; 4 | use sawp::parser::Parse; 5 | use sawp_ffi::*; 6 | 7 | #[repr(C)] 8 | pub struct ParseResult { 9 | message: *mut Message, 10 | size_read: usize, 11 | error: *mut Error, 12 | } 13 | 14 | #[no_mangle] 15 | pub unsafe extern "C" fn sawp_dns_create() -> *mut Dns { 16 | let parser = Dns {}; 17 | parser.into_ffi_ptr() 18 | } 19 | 20 | #[no_mangle] 21 | pub unsafe extern "C" fn sawp_dns_destroy(d: *mut Dns) { 22 | if !d.is_null() { 23 | drop(Box::from_raw(d)); 24 | } 25 | } 26 | 27 | /// # Safety 28 | /// function will panic if called with null 29 | #[no_mangle] 30 | pub unsafe extern "C" fn sawp_dns_parse( 31 | parser: *const Dns, 32 | direction: Direction, 33 | data: *const u8, 34 | length: usize, 35 | ) -> *mut ParseResult { 36 | let input = std::slice::from_raw_parts(data, length); 37 | match (*parser).parse(input, direction) { 38 | Ok((sl, message)) => ParseResult { 39 | message: message.into_ffi_ptr(), 40 | size_read: length - sl.len(), 41 | error: std::ptr::null_mut(), 42 | } 43 | .into_ffi_ptr(), 44 | Err(e) => ParseResult { 45 | message: std::ptr::null_mut(), 46 | size_read: 0, 47 | error: e.into_ffi_ptr(), 48 | } 49 | .into_ffi_ptr(), 50 | } 51 | } 52 | 53 | impl Drop for ParseResult { 54 | fn drop(&mut self) { 55 | unsafe { 56 | sawp_dns_message_destroy(self.message); 57 | if !self.error.is_null() { 58 | drop(Box::from_raw(self.error)); 59 | } 60 | } 61 | } 62 | } 63 | 64 | /// Free ParseResult 65 | /// Will also destroy contained message and error 66 | #[no_mangle] 67 | pub unsafe extern "C" fn sawp_dns_parse_result_destroy(d: *mut ParseResult) { 68 | if !d.is_null() { 69 | drop(Box::from_raw(d)); 70 | } 71 | } 72 | 73 | #[no_mangle] 74 | pub unsafe extern "C" fn sawp_dns_message_destroy(d: *mut Message) { 75 | if !d.is_null() { 76 | drop(Box::from_raw(d)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sawp-dns/src/header.rs: -------------------------------------------------------------------------------- 1 | use nom::number::streaming::be_u16; 2 | 3 | use sawp::error::Result; 4 | 5 | use sawp_flags::{BitFlags, Flag, Flags}; 6 | 7 | use crate::enums::{OpCode, QueryResponse, ResponseCode}; 8 | 9 | use crate::ErrorFlags; 10 | #[cfg(feature = "ffi")] 11 | use sawp_ffi::GenerateFFI; 12 | 13 | /// Masks for extracting DNS header flags 14 | #[allow(non_camel_case_types)] 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BitFlags)] 16 | #[repr(u16)] 17 | pub enum header_masks { 18 | QUERY_RESPONSE = 0b1000_0000_0000_0000, 19 | OPCODE = 0b0111_1000_0000_0000, 20 | AUTH = 0b0000_0100_0000_0000, 21 | TRUNC = 0b0000_0010_0000_0000, 22 | RECUR_DESIRED = 0b0000_0001_0000_0000, 23 | RECUR_AVAIL = 0b0000_0000_1000_0000, 24 | Z = 0b0000_0000_0100_0000, 25 | AUTH_DATA = 0b0000_0000_0010_0000, 26 | CHECK_DISABLED = 0b0000_0000_0001_0000, 27 | RCODE = 0b0000_0000_0000_1111, 28 | } 29 | 30 | /// A parsed DNS header 31 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 32 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 33 | #[derive(Debug, PartialEq, Eq)] 34 | pub struct Header { 35 | /// Transaction ID 36 | pub transaction_id: u16, 37 | /// Raw header flags 38 | pub flags: u16, 39 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 40 | /// QueryResponse::Query or QueryResponse::Response 41 | pub query_response: QueryResponse, 42 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 43 | /// Type of query 44 | pub opcode: OpCode, 45 | /// Is the name server an authority for this domain name? 46 | pub authoritative: bool, 47 | /// Was this msg truncated? 48 | pub truncated: bool, 49 | /// Should the name server pursue the query recursively? 50 | pub recursion_desired: bool, 51 | /// Can the name server pursue the query recursively? 52 | pub recursion_available: bool, 53 | /// Z flag is set? 54 | pub zflag: bool, 55 | 56 | /// All data authenticated by the server 57 | pub authenticated_data: bool, 58 | 59 | pub check_disabled: bool, 60 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 61 | /// Name server success/error state 62 | pub rcode: ResponseCode, 63 | /// Number of questions provided 64 | pub qdcount: u16, 65 | /// Number of answers provided 66 | pub ancount: u16, 67 | /// Number of name server resource records in the auth records 68 | pub nscount: u16, 69 | /// Number of resource records in the additional records section 70 | pub arcount: u16, 71 | } 72 | 73 | impl Header { 74 | #[allow(clippy::type_complexity)] 75 | pub fn parse(input: &[u8]) -> Result<(&[u8], (Header, Flags))> { 76 | let mut error_flags = ErrorFlags::none(); 77 | 78 | let (input, txid) = be_u16(input)?; 79 | let (input, flags) = be_u16(input)?; 80 | let wrapped_flags = Flags::::from_bits(flags); 81 | let query = if wrapped_flags.intersects(header_masks::QUERY_RESPONSE) { 82 | QueryResponse::Response 83 | } else { 84 | QueryResponse::Query 85 | }; 86 | let opcode: OpCode = OpCode::from_raw((wrapped_flags & header_masks::OPCODE).bits() >> 10); 87 | if opcode == OpCode::UNKNOWN { 88 | error_flags |= ErrorFlags::UnknownOpcode; 89 | } 90 | let rcode: ResponseCode = 91 | ResponseCode::from_raw((wrapped_flags & header_masks::RCODE).bits()); 92 | if rcode == ResponseCode::UNKNOWN { 93 | error_flags |= ErrorFlags::UnknownRcode; 94 | } 95 | let (input, qcnt) = be_u16(input)?; 96 | let (input, acnt) = be_u16(input)?; 97 | let (input, nscnt) = be_u16(input)?; 98 | let (input, arcnt) = be_u16(input)?; 99 | 100 | Ok(( 101 | input, 102 | ( 103 | Header { 104 | transaction_id: txid, 105 | flags, 106 | query_response: query, 107 | opcode, 108 | authoritative: wrapped_flags.intersects(header_masks::AUTH), 109 | truncated: wrapped_flags.intersects(header_masks::TRUNC), 110 | recursion_desired: wrapped_flags.intersects(header_masks::RECUR_DESIRED), 111 | recursion_available: wrapped_flags.intersects(header_masks::RECUR_AVAIL), 112 | zflag: wrapped_flags.intersects(header_masks::Z), 113 | authenticated_data: wrapped_flags.intersects(header_masks::AUTH_DATA), 114 | check_disabled: wrapped_flags.intersects(header_masks::CHECK_DISABLED), 115 | rcode, 116 | qdcount: qcnt, 117 | ancount: acnt, 118 | nscount: nscnt, 119 | arcount: arcnt, 120 | }, 121 | error_flags, 122 | ), 123 | )) 124 | } 125 | } 126 | 127 | #[cfg(test)] 128 | mod test { 129 | #![allow(clippy::type_complexity)] 130 | 131 | use crate::{ErrorFlags, Header, OpCode, QueryResponse, ResponseCode}; 132 | use rstest::rstest; 133 | use sawp::error::{Error, Result}; 134 | use sawp_flags::{Flag, Flags}; 135 | 136 | #[rstest( 137 | input, 138 | expected, 139 | case::parse_simple_header( 140 | & [ 141 | 0x31, 0x21, // Transaction ID: 0x3121 142 | 0x81, 0x00, // Flags: response, recursion desired 143 | 0x00, 0x01, // QDCOUNT: 1 144 | 0x00, 0x01, // ANCOUNT: 1 145 | 0x00, 0x00, // NSCOUNT: 0 146 | 0x00, 0x00, // ARCOUNT: 0 147 | ], 148 | Ok(( 149 | b"".as_ref(), 150 | (Header { 151 | transaction_id: 0x3121, 152 | flags: 0b1000_0001_0000_0000, 153 | query_response: QueryResponse::Response, 154 | opcode: OpCode::QUERY, 155 | authoritative: false, 156 | truncated: false, 157 | recursion_desired: true, 158 | recursion_available: false, 159 | zflag: false, 160 | authenticated_data: false, 161 | check_disabled: false, 162 | rcode: ResponseCode::NOERROR, 163 | qdcount: 1, 164 | ancount: 1, 165 | nscount: 0, 166 | arcount: 0, 167 | }, 168 | ErrorFlags::none()) 169 | )) 170 | ), 171 | case::parse_too_short_header( 172 | & [ 173 | 0x31, 0x21, // Transaction ID: 0x3121 174 | 0x81, 0x00, // Flags: response, recursion desired 175 | 0x00, 0x01, // QDCOUNT: 1 176 | 0x00, 0x01, // ANCOUNT: 1 177 | 0x00, 0x00, // NSCOUNT: 0 178 | ], 179 | Err(Error::incomplete_needed(2)) 180 | ), 181 | case::parse_header_bad_opcode( 182 | & [ 183 | 0x31, 0x21, // Transaction ID: 0x3121 184 | 0xb1, 0x00, // Flags: invalid opcode, recursion desired, authenticated data, format error 185 | 0x00, 0x01, // QDCOUNT: 1 186 | 0x00, 0x01, // ANCOUNT: 1 187 | 0x00, 0x00, // NSCOUNT: 0 188 | 0x00, 0x00, // ARCOUNT: 0 189 | ], 190 | Ok(( 191 | b"".as_ref(), 192 | (Header { 193 | transaction_id: 0x3121, 194 | flags: 0b1011_0001_0000_0000, 195 | query_response: QueryResponse::Response, 196 | opcode: OpCode::UNKNOWN, 197 | authoritative: false, 198 | truncated: false, 199 | recursion_desired: true, 200 | recursion_available: false, 201 | zflag: false, 202 | authenticated_data: false, 203 | check_disabled: false, 204 | rcode: ResponseCode::NOERROR, 205 | qdcount: 1, 206 | ancount: 1, 207 | nscount: 0, 208 | arcount: 0, 209 | }, 210 | ErrorFlags::UnknownOpcode.into()) 211 | )) 212 | ), 213 | case::parse_header_bad_rcode( 214 | & [ 215 | 0x31, 0x21, // Transaction ID: 0x3121 216 | 0x81, 0x0c, // Flags: response, recursion desired, invalid rcode 217 | 0x00, 0x01, // QDCOUNT: 1 218 | 0x00, 0x01, // ANCOUNT: 1 219 | 0x00, 0x00, // NSCOUNT: 0 220 | 0x00, 0x00, // ARCOUNT: 0 221 | ], 222 | Ok(( 223 | b"".as_ref(), 224 | (Header { 225 | transaction_id: 0x3121, 226 | flags: 0b1000_0001_0000_1100, 227 | query_response: QueryResponse::Response, 228 | opcode: OpCode::QUERY, 229 | authoritative: false, 230 | truncated: false, 231 | recursion_desired: true, 232 | recursion_available: false, 233 | zflag: false, 234 | authenticated_data: false, 235 | check_disabled: false, 236 | rcode: ResponseCode::UNKNOWN, 237 | qdcount: 1, 238 | ancount: 1, 239 | nscount: 0, 240 | arcount: 0, 241 | }, 242 | ErrorFlags::UnknownRcode.into()) 243 | )) 244 | ), 245 | )] 246 | fn header(input: &[u8], expected: Result<(&[u8], (Header, Flags))>) { 247 | assert_eq!(Header::parse(input), expected); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /sawp-dns/src/question.rs: -------------------------------------------------------------------------------- 1 | use nom::number::streaming::be_u16; 2 | 3 | use sawp_flags::{Flag, Flags}; 4 | 5 | use crate::enums::{RecordClass, RecordType}; 6 | use crate::{custom_count, ErrorFlags, IResult, Name}; 7 | 8 | #[cfg(feature = "ffi")] 9 | use sawp_ffi::GenerateFFI; 10 | 11 | /// A parsed DNS question 12 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 13 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 14 | #[derive(Debug, PartialEq, Eq)] 15 | pub struct Question { 16 | pub name: Vec, 17 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 18 | pub record_type: RecordType, 19 | pub record_type_raw: u16, 20 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 21 | pub record_class: RecordClass, 22 | pub record_class_raw: u16, 23 | } 24 | 25 | impl Question { 26 | fn parse<'a>( 27 | input: &'a [u8], 28 | reference_bytes: &'a [u8], 29 | ) -> IResult<'a, (Question, Flags)> { 30 | let (input, (name, mut error_flags)) = Name::parse(reference_bytes)(input)?; 31 | let (input, working_qtype) = be_u16(input)?; 32 | let qtype: RecordType = RecordType::from_raw(working_qtype); 33 | if qtype == RecordType::UNKNOWN { 34 | error_flags |= ErrorFlags::UnknownRtype; 35 | } 36 | 37 | let (input, working_qclass) = be_u16(input)?; 38 | let qclass: RecordClass = RecordClass::from_raw(working_qclass); 39 | if qclass == RecordClass::UNKNOWN { 40 | error_flags |= ErrorFlags::UnknownRclass; 41 | } 42 | 43 | Ok(( 44 | input, 45 | ( 46 | Question { 47 | name, 48 | record_class: qclass, 49 | record_class_raw: working_qclass, 50 | record_type: qtype, 51 | record_type_raw: working_qtype, 52 | }, 53 | error_flags, 54 | ), 55 | )) 56 | } 57 | 58 | pub fn parse_questions<'a>( 59 | input: &'a [u8], 60 | reference_bytes: &'a [u8], 61 | qdcnt: usize, 62 | ) -> IResult<'a, (Vec, Flags)> { 63 | let mut error_flags = ErrorFlags::none(); 64 | 65 | let (input, questions) = custom_count( 66 | |input, reference_bytes| { 67 | let (input, (answer, inner_error_flags)) = Question::parse(input, reference_bytes)?; 68 | error_flags |= inner_error_flags; 69 | Ok((input, answer)) 70 | }, 71 | qdcnt, 72 | )(input, reference_bytes)?; 73 | Ok((input, (questions, error_flags))) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /sawp-dns/src/rdata.rs: -------------------------------------------------------------------------------- 1 | use nom::bytes::streaming::take; 2 | use nom::number::streaming::{be_u16, be_u32, be_u8}; 3 | 4 | use sawp_flags::{Flag, Flags}; 5 | 6 | use byteorder::{BigEndian, ByteOrder}; 7 | 8 | use crate::edns::EdnsOption; 9 | use crate::enums::{RecordType, SshfpAlgorithm, SshfpFingerprint, TSigResponseCode, TkeyMode}; 10 | 11 | use crate::{ErrorFlags, IResult, Name}; 12 | use nom::combinator::rest; 13 | #[cfg(feature = "ffi")] 14 | use sawp_ffi::GenerateFFI; 15 | 16 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 17 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 18 | #[derive(Debug, PartialEq, Eq)] 19 | pub struct RDataCAA { 20 | pub flags: u8, 21 | pub tag: Vec, 22 | pub value: Vec, 23 | } 24 | 25 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 26 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 27 | #[derive(Debug, PartialEq, Eq)] 28 | pub struct RDataOPT { 29 | /// Requestor's UDP payload size 30 | pub udp_payload_size: u16, 31 | pub extended_rcode: u8, 32 | /// EDNS version 33 | pub version: u8, 34 | pub flags: u16, // bit [0] = DO . bit[1..15] are reserved. 35 | pub data: Vec, 36 | } 37 | 38 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 39 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 40 | #[derive(Debug, PartialEq, Eq)] 41 | pub struct RDataSoa { 42 | /// Primary NS for this zone 43 | pub mname: Vec, 44 | /// Authority's mailbox 45 | pub rname: Vec, 46 | /// Serial version number 47 | pub serial: u32, 48 | /// Refresh interval in seconds 49 | pub refresh: u32, 50 | /// Retry interval in seconds 51 | pub retry: u32, 52 | /// Upper time limit until zone is no longer authoritative in seconds 53 | pub expire: u32, 54 | /// Minimum ttl for records in this zone in seconds 55 | pub minimum: u32, 56 | } 57 | 58 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 59 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 60 | #[derive(Debug, PartialEq, Eq)] 61 | pub struct RDataSSHFP { 62 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 63 | /// Algorithm number 64 | pub algorithm: SshfpAlgorithm, 65 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 66 | pub fingerprint_type: SshfpFingerprint, 67 | pub fingerprint: Vec, 68 | } 69 | 70 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 71 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 72 | #[derive(Debug, PartialEq, Eq)] 73 | pub struct RDataSRV { 74 | pub priority: u16, 75 | pub weight: u16, 76 | pub port: u16, 77 | pub target: Vec, 78 | } 79 | 80 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 81 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 82 | #[derive(Debug, PartialEq, Eq)] 83 | pub struct RDataTKEY { 84 | pub algorithm: Vec, 85 | /// Time signature incepted - seconds since epoch 86 | pub inception: u32, 87 | /// Time signature expires - seconds since epoch 88 | pub expiration: u32, 89 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 90 | pub mode: TkeyMode, 91 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 92 | pub error: TSigResponseCode, 93 | pub key_data: Vec, 94 | pub other_data: Vec, 95 | } 96 | 97 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 98 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 99 | #[derive(Debug, PartialEq, Eq)] 100 | pub struct RDataTSIG { 101 | pub algorithm_name: Vec, 102 | /// Seconds since epoch 103 | pub time_signed: u64, // only occupies 6 bytes in RData 104 | /// Seconds of error permitted 105 | pub fudge: u16, 106 | pub mac: Vec, 107 | /// Original message ID 108 | pub original_id: u16, 109 | #[cfg_attr(feature = "ffi", sawp_ffi(copy))] 110 | /// Extended rcode covering TSIG processing 111 | pub error: TSigResponseCode, 112 | /// Empty unless error == BADTIME 113 | pub other_data: Vec, 114 | } 115 | 116 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 117 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_dns"))] 118 | #[derive(Debug, PartialEq, Eq)] 119 | pub enum RDataType { 120 | /// Addresses 121 | A(Vec), 122 | AAAA(Vec), 123 | /// Domain names 124 | CNAME(Vec), 125 | PTR(Vec), 126 | MX(Vec), 127 | NS(Vec), 128 | /// Text 129 | TXT(Vec), 130 | NUL(Vec), 131 | /// Multiple field records 132 | CAA(RDataCAA), 133 | OPT(RDataOPT), 134 | SOA(RDataSoa), 135 | SRV(RDataSRV), 136 | SSHFP(RDataSSHFP), 137 | TKEY(RDataTKEY), 138 | TSIG(RDataTSIG), 139 | UNKNOWN(Vec), 140 | } 141 | 142 | impl RDataType { 143 | pub fn parse<'a>( 144 | input: &'a [u8], 145 | reference_bytes: &'a [u8], 146 | rtype: RecordType, 147 | ) -> IResult<'a, (RDataType, Flags)> { 148 | match rtype { 149 | RecordType::A => RDataType::parse_rdata_a(input) 150 | .map(|(input, rdata)| (input, (rdata, ErrorFlags::none()))), 151 | RecordType::AAAA => RDataType::parse_rdata_aaaa(input) 152 | .map(|(input, rdata)| (input, (rdata, ErrorFlags::none()))), 153 | RecordType::CAA => RDataType::parse_rdata_caa(input) 154 | .map(|(input, rdata)| (input, (rdata, ErrorFlags::none()))), 155 | RecordType::CNAME => RDataType::parse_rdata_cname(input, reference_bytes), 156 | RecordType::MX => RDataType::parse_rdata_mx(input, reference_bytes), 157 | RecordType::NS => RDataType::parse_rdata_ns(input, reference_bytes), 158 | RecordType::NUL => RDataType::parse_rdata_null(input) 159 | .map(|(input, rdata)| (input, (rdata, ErrorFlags::none()))), 160 | RecordType::OPT => RDataType::parse_rdata_opt(input), 161 | RecordType::PTR => RDataType::parse_rdata_ptr(input, reference_bytes), 162 | RecordType::SOA => RDataType::parse_rdata_soa(input, reference_bytes), 163 | RecordType::SRV => RDataType::parse_rdata_srv(input, reference_bytes), 164 | RecordType::SSHFP => RDataType::parse_rdata_sshfp(input) 165 | .map(|(input, rdata)| (input, (rdata, ErrorFlags::none()))), 166 | RecordType::TKEY => RDataType::parse_rdata_tkey(input, reference_bytes), 167 | RecordType::TSIG => RDataType::parse_rdata_tsig(input, reference_bytes), 168 | RecordType::TXT => RDataType::parse_rdata_txt(input) 169 | .map(|(input, rdata)| (input, (rdata, ErrorFlags::none()))), 170 | _ => RDataType::parse_rdata_unknown(input) 171 | .map(|(input, rdata)| (input, (rdata, ErrorFlags::none()))), 172 | } 173 | } 174 | 175 | fn parse_rdata_a(input: &[u8]) -> IResult { 176 | let (input, data) = rest(input)?; 177 | Ok((input, RDataType::A(data.to_vec()))) 178 | } 179 | 180 | fn parse_rdata_aaaa(input: &[u8]) -> IResult { 181 | let (input, data) = rest(input)?; 182 | Ok((input, RDataType::AAAA(data.to_vec()))) 183 | } 184 | 185 | fn parse_rdata_caa(input: &[u8]) -> IResult { 186 | let (input, flags) = be_u8(input)?; 187 | let (input, tag_length) = be_u8(input)?; 188 | let (input, tag) = take(tag_length)(input)?; 189 | let (input, value) = rest(input)?; 190 | 191 | Ok(( 192 | input, 193 | RDataType::CAA(RDataCAA { 194 | flags, 195 | tag: tag.to_vec(), 196 | value: value.to_vec(), 197 | }), 198 | )) 199 | } 200 | 201 | fn parse_rdata_cname<'a>( 202 | input: &'a [u8], 203 | reference_bytes: &'a [u8], 204 | ) -> IResult<'a, (RDataType, Flags)> { 205 | let (input, (name, error_flags)) = Name::parse(reference_bytes)(input)?; 206 | Ok((input, (RDataType::CNAME(name), error_flags))) 207 | } 208 | 209 | fn parse_rdata_ns<'a>( 210 | input: &'a [u8], 211 | reference_bytes: &'a [u8], 212 | ) -> IResult<'a, (RDataType, Flags)> { 213 | let (input, (name, error_flags)) = Name::parse(reference_bytes)(input)?; 214 | Ok((input, (RDataType::NS(name), error_flags))) 215 | } 216 | 217 | fn parse_rdata_ptr<'a>( 218 | input: &'a [u8], 219 | reference_bytes: &'a [u8], 220 | ) -> IResult<'a, (RDataType, Flags)> { 221 | let (input, (name, error_flags)) = Name::parse(reference_bytes)(input)?; 222 | Ok((input, (RDataType::PTR(name), error_flags))) 223 | } 224 | 225 | pub fn parse_rdata_opt(input: &[u8]) -> IResult<(RDataType, Flags)> { 226 | let (input, udp_payload_size) = be_u16(input)?; 227 | let (input, extended_rcode) = be_u8(input)?; 228 | let (input, version) = be_u8(input)?; 229 | let (input, flags) = be_u16(input)?; 230 | let (input, data_len) = be_u16(input)?; 231 | let (input, (data, options_error_flags)) = EdnsOption::parse_options(input, data_len)?; 232 | 233 | Ok(( 234 | input, 235 | ( 236 | RDataType::OPT(RDataOPT { 237 | udp_payload_size, 238 | extended_rcode, 239 | version, 240 | flags, 241 | data, 242 | }), 243 | options_error_flags, 244 | ), 245 | )) 246 | } 247 | 248 | fn parse_rdata_soa<'a>( 249 | input: &'a [u8], 250 | reference_bytes: &'a [u8], 251 | ) -> IResult<'a, (RDataType, Flags)> { 252 | let (input, (mname, mut error_flags)) = Name::parse(reference_bytes)(input)?; 253 | let (input, (rname, inner_error_flags)) = Name::parse(reference_bytes)(input)?; 254 | 255 | error_flags |= inner_error_flags; 256 | 257 | let (input, serial) = be_u32(input)?; 258 | let (input, refresh) = be_u32(input)?; 259 | let (input, retry) = be_u32(input)?; 260 | let (input, expire) = be_u32(input)?; 261 | let (input, minimum) = be_u32(input)?; 262 | 263 | Ok(( 264 | input, 265 | ( 266 | RDataType::SOA(RDataSoa { 267 | mname, 268 | rname, 269 | serial, 270 | refresh, 271 | retry, 272 | expire, 273 | minimum, 274 | }), 275 | error_flags, 276 | ), 277 | )) 278 | } 279 | 280 | fn parse_rdata_tkey<'a>( 281 | input: &'a [u8], 282 | reference_bytes: &'a [u8], 283 | ) -> IResult<'a, (RDataType, Flags)> { 284 | let (input, (algorithm, error_flags)) = Name::parse(reference_bytes)(input)?; 285 | let (input, inception) = be_u32(input)?; 286 | let (input, expiration) = be_u32(input)?; 287 | let (input, mode) = be_u16(input)?; 288 | let (input, error) = be_u16(input)?; 289 | let (input, key_size) = be_u16(input)?; 290 | let (input, key_data) = take(key_size)(input)?; 291 | let (input, other_size) = be_u16(input)?; 292 | let (input, other_data) = take(other_size)(input)?; 293 | 294 | Ok(( 295 | input, 296 | ( 297 | RDataType::TKEY(RDataTKEY { 298 | algorithm, 299 | inception, 300 | expiration, 301 | mode: TkeyMode::from_raw(mode), 302 | error: TSigResponseCode::from_raw(error), 303 | key_data: key_data.to_vec(), 304 | other_data: other_data.to_vec(), 305 | }), 306 | error_flags, 307 | ), 308 | )) 309 | } 310 | 311 | fn parse_rdata_tsig<'a>( 312 | input: &'a [u8], 313 | reference_bytes: &'a [u8], 314 | ) -> IResult<'a, (RDataType, Flags)> { 315 | let (input, (algorithm_name, error_flags)) = Name::parse(reference_bytes)(input)?; 316 | let (input, time_signed_raw) = take(6_usize)(input)?; 317 | let (input, fudge) = be_u16(input)?; 318 | let (input, mac_size) = be_u16(input)?; 319 | let (input, mac) = take(mac_size)(input)?; 320 | let (input, original_id) = be_u16(input)?; 321 | let (input, error) = be_u16(input)?; 322 | let (input, other_len) = be_u16(input)?; 323 | let (input, other_data) = take(other_len)(input)?; 324 | 325 | Ok(( 326 | input, 327 | ( 328 | RDataType::TSIG(RDataTSIG { 329 | algorithm_name, 330 | time_signed: BigEndian::read_uint(time_signed_raw, 6), 331 | fudge, 332 | mac: mac.to_vec(), 333 | original_id, 334 | error: TSigResponseCode::from_raw(error), 335 | other_data: other_data.to_vec(), 336 | }), 337 | error_flags, 338 | ), 339 | )) 340 | } 341 | 342 | fn parse_rdata_mx<'a>( 343 | input: &'a [u8], 344 | reference_bytes: &'a [u8], 345 | ) -> IResult<'a, (RDataType, Flags)> { 346 | // Skip the preference field 347 | let (input, _) = be_u16(input)?; 348 | let (input, (name, error_flags)) = Name::parse(reference_bytes)(input)?; 349 | Ok((input, (RDataType::MX(name), error_flags))) 350 | } 351 | 352 | fn parse_rdata_srv<'a>( 353 | input: &'a [u8], 354 | reference_bytes: &'a [u8], 355 | ) -> IResult<'a, (RDataType, Flags)> { 356 | let (input, priority) = be_u16(input)?; 357 | let (input, weight) = be_u16(input)?; 358 | let (input, port) = be_u16(input)?; 359 | let (input, (target, error_flags)) = Name::parse(reference_bytes)(input)?; 360 | 361 | Ok(( 362 | input, 363 | ( 364 | RDataType::SRV(RDataSRV { 365 | priority, 366 | weight, 367 | port, 368 | target, 369 | }), 370 | error_flags, 371 | ), 372 | )) 373 | } 374 | 375 | fn parse_rdata_txt(input: &[u8]) -> IResult { 376 | let (input, len) = be_u8(input)?; 377 | let (input, txt) = take(len)(input)?; 378 | Ok((input, RDataType::TXT(txt.to_vec()))) 379 | } 380 | 381 | fn parse_rdata_null(input: &[u8]) -> IResult { 382 | let (input, data) = rest(input)?; 383 | Ok((input, RDataType::NUL(data.to_vec()))) 384 | } 385 | 386 | fn parse_rdata_sshfp(input: &[u8]) -> IResult { 387 | let (input, algorithm) = be_u8(input)?; 388 | let (input, fingerprint_type) = be_u8(input)?; 389 | let (input, fingerprint) = rest(input)?; 390 | 391 | Ok(( 392 | input, 393 | RDataType::SSHFP(RDataSSHFP { 394 | algorithm: SshfpAlgorithm::from_raw(algorithm), 395 | fingerprint_type: SshfpFingerprint::from_raw(fingerprint_type), 396 | fingerprint: fingerprint.to_vec(), 397 | }), 398 | )) 399 | } 400 | 401 | fn parse_rdata_unknown(input: &[u8]) -> IResult { 402 | let (input, data) = rest(input)?; 403 | Ok((input, RDataType::UNKNOWN(data.to_vec()))) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /sawp-ffi-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-ffi-derive" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "Generate cbindgen compatible member accessors for structs and enums" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["ffi", "code-generation"] 12 | categories = [] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [dependencies] 21 | quote = "1.0" 22 | syn = "1.0" 23 | proc-macro-crate = "= 1.1.0" 24 | proc-macro2 = "1.0.36" 25 | heck = "0.3" 26 | 27 | [dev-dependencies] 28 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 29 | 30 | [lib] 31 | proc-macro = true 32 | 33 | # Override default replacements 34 | [package.metadata.release] 35 | pre-release-replacements = [] 36 | -------------------------------------------------------------------------------- /sawp-ffi-derive/src/attrs.rs: -------------------------------------------------------------------------------- 1 | /// This module handles parsing of `#[sawp_ffi(...)]` attributes. 2 | 3 | /// Get sawp_ffi meta attributes 4 | pub fn get_ffi_meta(attr: &syn::Attribute) -> Vec { 5 | if !attr.path.is_ident("sawp_ffi") { 6 | return Vec::new(); 7 | } 8 | 9 | if let Ok(syn::Meta::List(meta)) = attr.parse_meta() { 10 | meta.nested.into_iter().collect() 11 | } else { 12 | Vec::new() 13 | } 14 | } 15 | 16 | /// Get value of sawp_ffi(prefix) is set 17 | pub fn get_ffi_prefix(metas: &[syn::NestedMeta]) -> Option { 18 | for meta in metas { 19 | match meta { 20 | syn::NestedMeta::Meta(syn::Meta::NameValue(value)) if value.path.is_ident("prefix") => { 21 | match &value.lit { 22 | syn::Lit::Str(s) => return Some(s.value()), 23 | _ => panic!("sawp_ffi(prefix) expects string literal"), 24 | } 25 | } 26 | _ => (), 27 | } 28 | } 29 | None 30 | } 31 | 32 | /// Get value of sawp_ffi(flag) is set 33 | pub fn get_ffi_flag(metas: &[syn::NestedMeta]) -> Option { 34 | for meta in metas { 35 | match meta { 36 | syn::NestedMeta::Meta(syn::Meta::NameValue(value)) if value.path.is_ident("flag") => { 37 | return Some(parse_lit_into_ty("flag", &value.lit)) 38 | } 39 | _ => (), 40 | } 41 | } 42 | None 43 | } 44 | 45 | /// Has given sawp_ffi attribute 46 | pub fn has_ffi_meta(attribute: &str, metas: &[syn::NestedMeta]) -> bool { 47 | for meta in metas { 48 | match meta { 49 | syn::NestedMeta::Meta(syn::Meta::Path(word)) if word.is_ident(attribute) => { 50 | return true; 51 | } 52 | _ => (), 53 | } 54 | } 55 | false 56 | } 57 | 58 | /// Has sawp_ffi(copy) attribute 59 | pub fn has_ffi_copy_meta(metas: &[syn::NestedMeta]) -> bool { 60 | has_ffi_meta("copy", metas) 61 | } 62 | 63 | /// Has sawp_ffi(skip) attribute 64 | pub fn has_ffi_skip_meta(metas: &[syn::NestedMeta]) -> bool { 65 | has_ffi_meta("skip", metas) 66 | } 67 | 68 | /// Has sawp_ffi(type_only) attribute 69 | pub fn has_ffi_type_only_meta(metas: &[syn::NestedMeta]) -> bool { 70 | has_ffi_meta("type_only", metas) 71 | } 72 | 73 | fn parse_lit_into_ty(attr_name: &str, lit: &syn::Lit) -> syn::Type { 74 | if let syn::Lit::Str(lit) = lit { 75 | syn::parse_str(&lit.value()) 76 | .unwrap_or_else(|_| panic!("couldn't parse value for {}: {:?}", attr_name, lit.value())) 77 | } else { 78 | panic!("expected attribute to be a string: {}", attr_name); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sawp-ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-ffi" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "FFI helper macros and traits" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["ffi"] 12 | categories = [] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [dependencies] 21 | sawp-ffi-derive = { path = "../sawp-ffi-derive", version = "^0.13.1" } 22 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 23 | 24 | # Override default replacements 25 | [package.metadata.release] 26 | pre-release-replacements = [] 27 | -------------------------------------------------------------------------------- /sawp-ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unneeded_field_pattern)] 2 | 3 | extern crate sawp_ffi_derive; 4 | pub use sawp_ffi_derive::GenerateFFI; 5 | 6 | #[macro_export] 7 | macro_rules! nullcheck { 8 | ( $($ptr:expr),*) => { 9 | $( 10 | if $ptr.is_null() { 11 | panic!("{} is NULL in {}", stringify!($ptr), line!()); 12 | } 13 | )* 14 | } 15 | } 16 | 17 | #[macro_export] 18 | macro_rules! deref { 19 | ( $($ptr:expr),*) => { 20 | $( 21 | { 22 | $crate::nullcheck!($ptr); 23 | &*$ptr 24 | } 25 | )* 26 | } 27 | } 28 | 29 | #[macro_export] 30 | macro_rules! deref_mut { 31 | ( $($ptr:expr),*) => { 32 | $( 33 | { 34 | $crate::nullcheck!($ptr); 35 | &mut *$ptr 36 | } 37 | )* 38 | } 39 | } 40 | 41 | pub trait IntoFFIPtr { 42 | fn into_ffi_ptr(self) -> *mut T; 43 | } 44 | 45 | impl IntoFFIPtr for Option { 46 | fn into_ffi_ptr(self) -> *mut T { 47 | match self { 48 | Some(value) => value.into_ffi_ptr(), 49 | None => std::ptr::null_mut(), 50 | } 51 | } 52 | } 53 | 54 | impl IntoFFIPtr for T { 55 | fn into_ffi_ptr(self) -> *mut T { 56 | let boxed = Box::new(self); 57 | Box::into_raw(boxed) 58 | } 59 | } 60 | 61 | #[cfg(test)] 62 | extern crate sawp_flags; 63 | 64 | #[cfg(test)] 65 | #[allow(dead_code)] 66 | mod tests { 67 | use super::*; 68 | use sawp_flags::{BitFlags, Flag, Flags}; 69 | use std::ptr::null; 70 | 71 | #[test] 72 | #[should_panic] 73 | fn test_deref() { 74 | let ptr: *const u8 = std::ptr::null(); 75 | unsafe { 76 | let _ = crate::deref!(ptr); 77 | } 78 | } 79 | 80 | #[test] 81 | #[should_panic] 82 | fn test_nullcheck() { 83 | let ptr: *const u8 = std::ptr::null(); 84 | nullcheck!(ptr); 85 | } 86 | 87 | #[test] 88 | #[should_panic] 89 | fn test_derive_nullcheck() { 90 | #[derive(GenerateFFI)] 91 | pub struct MyStruct { 92 | pub field: u8, 93 | } 94 | 95 | unsafe { 96 | my_struct_get_field(std::ptr::null()); 97 | } 98 | } 99 | 100 | #[test] 101 | fn test_struct() { 102 | #[repr(u16)] 103 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 104 | pub enum Version { 105 | Ver1 = 0x0100, 106 | Ver1_1 = 0x0101, 107 | Ver2 = 0x0200, 108 | } 109 | 110 | #[repr(u8)] 111 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BitFlags)] 112 | pub enum FileType { 113 | Read = 0b0000_0001, 114 | Write = 0b0000_0010, 115 | } 116 | 117 | #[derive(GenerateFFI)] 118 | #[sawp_ffi(prefix = "sawp")] 119 | pub struct MyStruct { 120 | pub num: usize, 121 | #[sawp_ffi(copy)] 122 | pub version: Version, 123 | #[sawp_ffi(flag = "u8")] 124 | pub file_type: Flags, 125 | private: usize, 126 | #[sawp_ffi(skip)] 127 | pub skipped: usize, 128 | pub complex: Vec, 129 | pub string: String, 130 | pub option: Option, 131 | } 132 | 133 | let my_struct = MyStruct { 134 | num: 12, 135 | version: Version::Ver1, 136 | file_type: FileType::Write.into(), 137 | private: 0, 138 | skipped: 128, 139 | complex: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 140 | string: String::from("test"), 141 | option: Some(41), 142 | }; 143 | unsafe { 144 | assert_eq!(sawp_my_struct_get_num(&my_struct), 12); 145 | assert_eq!(sawp_my_struct_get_version(&my_struct), Version::Ver1); 146 | assert_eq!(sawp_my_struct_get_file_type(&my_struct), 0b0000_0010); 147 | assert_ne!(sawp_my_struct_get_complex(&my_struct), std::ptr::null()); 148 | assert_eq!((*sawp_my_struct_get_complex(&my_struct)).len(), 10); 149 | assert_ne!(sawp_my_struct_get_complex_ptr(&my_struct), std::ptr::null()); 150 | assert_eq!(sawp_my_struct_get_complex_len(&my_struct), 10); 151 | assert_ne!(sawp_my_struct_get_string(&my_struct), std::ptr::null()); 152 | assert_ne!(sawp_my_struct_get_string_ptr(&my_struct), std::ptr::null()); 153 | assert_eq!(sawp_my_struct_get_string_len(&my_struct), 4); 154 | 155 | assert_ne!(sawp_my_struct_get_option(&my_struct), std::ptr::null()); 156 | assert_eq!(*sawp_my_struct_get_option(&my_struct), 41); 157 | } 158 | } 159 | 160 | #[test] 161 | fn test_enum() { 162 | #[repr(u16)] 163 | #[derive(Copy, Clone)] 164 | pub enum Version { 165 | Ver1 = 0x0100, 166 | Ver1_1 = 0x0101, 167 | Ver2 = 0x0200, 168 | } 169 | 170 | #[repr(u8)] 171 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BitFlags)] 172 | pub enum FileType { 173 | Read = 0b0000_0001, 174 | Write = 0b0000_0010, 175 | } 176 | 177 | #[derive(GenerateFFI)] 178 | pub enum MyEnum { 179 | UnnamedSingle(u8), 180 | UnnamedMultiple(String, Vec), 181 | Named { 182 | a: u8, 183 | b: Vec, 184 | c: String, 185 | d: Option, 186 | #[sawp_ffi(flag = "u8")] 187 | file_type: Flags, 188 | }, 189 | Empty, 190 | } 191 | 192 | let single = MyEnum::UnnamedSingle(12); 193 | let multiple = MyEnum::UnnamedMultiple(String::from("test"), vec![34]); 194 | let named = MyEnum::Named { 195 | a: 2, 196 | b: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 197 | c: String::from("test"), 198 | d: Some(41), 199 | file_type: FileType::Write.into(), 200 | }; 201 | let empty = MyEnum::Empty; 202 | 203 | unsafe { 204 | assert_eq!(my_enum_get_type(&single), MyEnumType::UnnamedSingle); 205 | assert_eq!(my_enum_get_type(&multiple), MyEnumType::UnnamedMultiple); 206 | assert_eq!(my_enum_get_type(&named), MyEnumType::Named); 207 | assert_eq!(my_enum_get_type(&empty), MyEnumType::Empty); 208 | 209 | assert_ne!(my_enum_get_unnamed_single(&single), std::ptr::null()); 210 | assert_eq!(*my_enum_get_unnamed_single(&single), 12); 211 | 212 | assert_ne!(my_enum_get_unnamed_multiple_0(&multiple), std::ptr::null()); 213 | assert_eq!( 214 | *my_enum_get_unnamed_multiple_0(&multiple), 215 | String::from("test") 216 | ); 217 | assert_eq!(my_enum_get_unnamed_multiple_0_len(&multiple), 4); 218 | assert_ne!(my_enum_get_unnamed_multiple_1(&multiple), std::ptr::null()); 219 | assert_eq!(*my_enum_get_unnamed_multiple_1(&multiple), vec![34]); 220 | assert_eq!(my_enum_get_unnamed_multiple_1_len(&multiple), 1); 221 | 222 | assert_ne!(my_enum_get_named_a(&named), std::ptr::null()); 223 | assert_eq!(*my_enum_get_named_a(&named), 2); 224 | assert_ne!(my_enum_get_named_b(&named), std::ptr::null()); 225 | assert_eq!((*my_enum_get_named_b(&named)).len(), 10); 226 | assert_ne!(my_enum_get_named_b_ptr(&named), std::ptr::null()); 227 | assert_eq!(my_enum_get_named_b_len(&named), 10); 228 | 229 | assert_ne!(my_enum_get_named_c(&named), std::ptr::null()); 230 | assert_ne!(my_enum_get_named_c_ptr(&named), std::ptr::null()); 231 | assert_eq!(my_enum_get_named_c_len(&named), 4); 232 | 233 | assert_ne!(my_enum_get_named_d(&named), std::ptr::null()); 234 | assert_eq!(*my_enum_get_named_d(&named), 41); 235 | 236 | assert_ne!(my_enum_get_named_file_type(&named), std::ptr::null()); 237 | assert_eq!(*my_enum_get_named_file_type(&named), 0b0000_0010); 238 | 239 | assert_eq!(my_enum_get_unnamed_single(&multiple), std::ptr::null()); 240 | } 241 | } 242 | 243 | #[test] 244 | fn test_into_ffi_ptr() { 245 | let option: Option = Some(41); 246 | let null_option: Option = None; 247 | let non_option = 42; 248 | 249 | let option_ffi: *mut u8 = option.into_ffi_ptr(); 250 | let null_option_ffi: *mut u8 = null_option.into_ffi_ptr(); 251 | let non_option_ffi: *mut u8 = non_option.into_ffi_ptr(); 252 | 253 | unsafe { 254 | assert_ne!(option_ffi, std::ptr::null_mut()); 255 | assert_eq!(*option_ffi, 41); 256 | assert_eq!(null_option_ffi, std::ptr::null_mut()); 257 | assert_ne!(non_option_ffi, std::ptr::null_mut()); 258 | assert_eq!(*non_option_ffi, 42); 259 | 260 | drop(Box::from_raw(option_ffi)); 261 | drop(Box::from_raw(non_option_ffi)); 262 | } 263 | } 264 | 265 | #[test] 266 | fn test_get_vec_at_index() { 267 | #[derive(GenerateFFI)] 268 | #[sawp_ffi(prefix = "sawp")] 269 | pub struct MyStructTwo { 270 | pub num: usize, 271 | pub string: String, 272 | } 273 | 274 | #[derive(GenerateFFI)] 275 | #[sawp_ffi(prefix = "sawp")] 276 | pub struct SuperStruct { 277 | pub v: Vec, 278 | } 279 | 280 | #[derive(GenerateFFI)] 281 | #[sawp_ffi(prefix = "sawp")] 282 | pub enum MyEnum { 283 | A(Vec), 284 | B(usize), 285 | } 286 | 287 | let s = SuperStruct { 288 | v: vec![ 289 | MyStructTwo { 290 | num: 1, 291 | string: String::from("first"), 292 | }, 293 | MyStructTwo { 294 | num: 2, 295 | string: String::from("second"), 296 | }, 297 | ], 298 | }; 299 | 300 | let e = MyEnum::A(vec![MyEnum::B(1)]); 301 | 302 | // struct accessors 303 | unsafe { 304 | assert_eq!( 305 | sawp_my_struct_two_get_num(sawp_super_struct_get_v_ptr_to_idx(&s, 0)), 306 | 1 307 | ); 308 | assert_eq!( 309 | sawp_my_struct_two_get_num(sawp_super_struct_get_v_ptr_to_idx(&s, 1)), 310 | 2 311 | ); 312 | assert_ne!( 313 | sawp_my_struct_two_get_string(sawp_super_struct_get_v_ptr_to_idx(&s, 0)), 314 | std::ptr::null() 315 | ); 316 | assert_ne!( 317 | sawp_my_struct_two_get_string(sawp_super_struct_get_v_ptr_to_idx(&s, 1)), 318 | std::ptr::null() 319 | ); 320 | } 321 | 322 | // enum accessors 323 | unsafe { 324 | assert_eq!( 325 | *sawp_my_enum_get_b(sawp_my_enum_get_a_ptr_to_idx(sawp_my_enum_get_a(&e), 0)), 326 | 1 327 | ); 328 | } 329 | } 330 | 331 | #[test] 332 | #[should_panic] 333 | fn test_get_vec_at_index_panics_called_with_null() { 334 | #[derive(GenerateFFI)] 335 | #[sawp_ffi(prefix = "sawp")] 336 | pub enum MyEnumThree { 337 | A(Vec), 338 | B(usize), 339 | } 340 | 341 | unsafe { 342 | sawp_my_enum_three_get_a(null()); 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /sawp-file/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-file" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP File Format" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["parser", "protocols", "serialization"] 12 | categories = ["parsing", "network-programming", "encoding"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [dependencies] 21 | sawp = { path = "..", version = "^0.13.1" } 22 | rmp-serde = "1.1.1" 23 | serde = "1.0.116" 24 | serde_derive = "1.0.116" 25 | 26 | # Override default replacements 27 | [package.metadata.release] 28 | pre-release-replacements = [] 29 | -------------------------------------------------------------------------------- /sawp-file/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::Version; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Debug)] 6 | pub enum ErrorKind { 7 | IOError(std::io::Error), 8 | Serialization(String), 9 | // Failed to parse version string to integer. 10 | VersionParse, 11 | // Version did not match during deserialization (expected, actual). 12 | VersionMismatch((Version, Version)), 13 | } 14 | #[derive(Debug)] 15 | pub struct Error { 16 | kind: ErrorKind, 17 | } 18 | 19 | impl Error { 20 | pub fn new(kind: ErrorKind) -> Self { 21 | Self { kind } 22 | } 23 | } 24 | 25 | impl std::fmt::Display for Error { 26 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { 27 | match &self.kind { 28 | ErrorKind::IOError(err) => write!(fmt, "io error: {}", err), 29 | ErrorKind::Serialization(err) => write!(fmt, "serialization error: {}", err), 30 | ErrorKind::VersionParse => write!(fmt, "failed to parse version"), 31 | ErrorKind::VersionMismatch((expected, actual)) => { 32 | write!(fmt, "expected version {} got {}", expected, actual) 33 | } 34 | } 35 | } 36 | } 37 | 38 | impl std::error::Error for Error {} 39 | 40 | impl From for Error { 41 | fn from(other: std::io::Error) -> Self { 42 | Self::new(ErrorKind::IOError(other)) 43 | } 44 | } 45 | 46 | impl std::convert::From for Error { 47 | fn from(other: rmps::encode::Error) -> Self { 48 | Error::new(ErrorKind::Serialization(other.to_string())) 49 | } 50 | } 51 | 52 | impl std::convert::From for Error { 53 | fn from(other: rmps::decode::Error) -> Self { 54 | Error::new(ErrorKind::Serialization(other.to_string())) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sawp-file/src/format.rs: -------------------------------------------------------------------------------- 1 | //! Format Specification 2 | //! 3 | //! The format for serializing SAWP API Calls is a series of consecutive 4 | //! self-contained messages. 5 | //! 6 | //! The messages are of the following msgpack type where `N` is the total number 7 | //! messages ranging from two to infinity. 8 | //! 9 | //! | message | type | description | 10 | //! |---------|-------|-----------------------| 11 | //! | 1 | int | version number | 12 | //! | 2..N | call | call structure fields | 13 | //! 14 | //! Calls are stored in seperate messages to allow for a streaming format. Users 15 | //! _do not_ have to store the entire SAWP "file" into memory. Messages can be 16 | //! parsed asynchronously. 17 | //! 18 | //! This format is subject to change and other applications should not attempt 19 | //! to parse it. Use this library instead for encoding and decoding instead. 20 | 21 | use crate::error::{Error, ErrorKind, Result}; 22 | use crate::Version; 23 | use std::io::{Read, Write}; 24 | 25 | // Direction of a chunk of data or gap. 26 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Copy, Clone)] 27 | pub enum Direction { 28 | Unknown, 29 | ToServer, 30 | ToClient, 31 | } 32 | 33 | /// A chunk of input data to parse. 34 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] 35 | pub struct Data { 36 | direction: Direction, 37 | data: Vec, 38 | } 39 | 40 | /// Identifies a missing chunk of input data. 41 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] 42 | pub struct Gap { 43 | direction: Direction, 44 | gap: usize, 45 | } 46 | 47 | /// A list of all API calls we want to expose. 48 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] 49 | pub enum Call { 50 | /// Parse the input data. 51 | Parse(Data), 52 | /// Identify a gap. 53 | Gap(Gap), 54 | } 55 | 56 | /// Reads the expected format from a source. 57 | pub struct Reader { 58 | inner: R, 59 | } 60 | 61 | impl Reader { 62 | /// Creates a new reader. 63 | /// 64 | /// This will fail if the version in the format doesn't match the current 65 | /// version of this module. 66 | pub fn new(inner: R) -> Result { 67 | let mut reader = Reader { inner }; 68 | let expected_version = crate::version(); 69 | let actual_version: Version = rmp_serde::from_read(&mut reader.inner)?; 70 | if expected_version != actual_version { 71 | return Err(Error::new(ErrorKind::VersionMismatch(( 72 | expected_version, 73 | actual_version, 74 | )))); 75 | } 76 | Ok(reader) 77 | } 78 | } 79 | 80 | impl std::iter::Iterator for Reader { 81 | type Item = Call; 82 | 83 | fn next(&mut self) -> Option { 84 | rmp_serde::from_read(&mut self.inner).ok() 85 | } 86 | } 87 | 88 | /// Writes serialized API calls to a sink. 89 | pub struct Writer { 90 | inner: W, 91 | } 92 | 93 | impl Writer { 94 | /// Creates a writer. 95 | pub fn new(inner: W) -> Result { 96 | let mut writer = Writer { inner }; 97 | writer.version()?; 98 | Ok(writer) 99 | } 100 | 101 | /// Writes the format version number. 102 | fn version(&mut self) -> Result<()> { 103 | let bytes = rmp_serde::to_vec(&crate::version())?; 104 | self.inner.write_all(&bytes)?; 105 | Ok(()) 106 | } 107 | 108 | /// Writes the parse API call. 109 | pub fn parse(&mut self, direction: Direction, data: &[u8]) -> Result<()> { 110 | let call = Call::Parse(Data { 111 | direction, 112 | data: data.to_vec(), 113 | }); 114 | let bytes = rmp_serde::to_vec(&call)?; 115 | self.inner.write_all(&bytes)?; 116 | Ok(()) 117 | } 118 | 119 | /// Writes the gap API call. 120 | pub fn gap(&mut self, direction: Direction, gap: usize) -> Result<()> { 121 | let call = Call::Gap(Gap { direction, gap }); 122 | let bytes = rmp_serde::to_vec(&call)?; 123 | self.inner.write_all(&bytes)?; 124 | Ok(()) 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | 132 | #[test] 133 | fn test_read_write() { 134 | let data = b"GET /index.php HTTP/1.1\r\n\r\n"; 135 | let gap = 10; 136 | 137 | let mut buffer = Vec::new(); 138 | let mut writer = Writer::new(&mut buffer).expect("failed to create writer"); 139 | writer.parse(Direction::ToServer, data).unwrap(); 140 | writer.gap(Direction::ToServer, gap).unwrap(); 141 | 142 | let buffer = std::io::Cursor::new(buffer); 143 | let reader = Reader::new(buffer).expect("failed to create reader"); 144 | let result: Vec = reader.collect(); 145 | let expected: Vec = vec![ 146 | Call::Parse(Data { 147 | direction: Direction::ToServer, 148 | data: data.to_vec(), 149 | }), 150 | Call::Gap(Gap { 151 | direction: Direction::ToServer, 152 | gap, 153 | }), 154 | ]; 155 | assert_eq!(expected, result); 156 | } 157 | 158 | #[should_panic(expected = "VersionMismatch")] 159 | #[test] 160 | fn test_version_mismatch() { 161 | // Test a version number that is off by one 162 | let wrong_version = crate::version() + 1; 163 | let bytes = rmp_serde::to_vec(&wrong_version).unwrap(); 164 | let buffer = std::io::Cursor::new(bytes); 165 | let _ = Reader::new(buffer).unwrap(); 166 | } 167 | 168 | #[test] 169 | fn test_corrupt_bytes() { 170 | let data = b"GET /index.php HTTP/1.1\r\n\r\n"; 171 | let gap = 10; 172 | 173 | let mut buffer = Vec::new(); 174 | let mut writer = Writer::new(&mut buffer).expect("failed to create writer"); 175 | writer.parse(Direction::ToServer, data).unwrap(); 176 | writer.gap(Direction::ToServer, gap).unwrap(); 177 | 178 | // Process everything but the last byte. 179 | let buffer = std::io::Cursor::new(&buffer[..buffer.len() - 1]); 180 | let reader = Reader::new(buffer).expect("failed to create reader"); 181 | 182 | // Errors are ignored and the iterator will end prematurely 183 | assert_eq!(reader.count(), 1); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /sawp-file/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! SAWP File Format 2 | //! 3 | //! This module defines structs to serialize and deserialize arguments to SAWP 4 | //! calls in order to replay them into a parser. 5 | 6 | extern crate serde; 7 | 8 | #[macro_use] 9 | extern crate serde_derive; 10 | extern crate rmp_serde as rmps; 11 | 12 | pub mod error; 13 | pub mod format; 14 | 15 | pub type Version = usize; 16 | 17 | /// Get the version number of the format 18 | pub fn version() -> Version { 19 | // This should never fail because the compiler sets the environment variable. 20 | // There doesn't seem to be a "const fn" version of the parse function. 21 | env!("CARGO_PKG_VERSION_MAJOR") 22 | .parse() 23 | .expect("failed to parse version number") 24 | } 25 | -------------------------------------------------------------------------------- /sawp-flags-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-flags-derive" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP BitFlags Handling and Storage Derive Macro" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["bitflags", "bit", "flags", "bitmask", "derive"] 12 | include = [ 13 | "Cargo.toml", 14 | "../LICENSE", 15 | "../README.md", 16 | "src/**/*.rs", 17 | ] 18 | 19 | [dependencies] 20 | quote = "1.0" 21 | syn = "1.0" 22 | proc-macro-crate = "= 1.1.0" 23 | proc-macro2 = "1.0.36" 24 | 25 | [lib] 26 | proc-macro = true 27 | 28 | # Override default replacements 29 | [package.metadata.release] 30 | pre-release-replacements = [] 31 | -------------------------------------------------------------------------------- /sawp-flags-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use proc_macro2::{Ident, Span, TokenStream, TokenTree}; 3 | use quote::quote; 4 | 5 | #[proc_macro_derive(BitFlags)] 6 | pub fn derive_sawp_flags(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 7 | let ast: syn::DeriveInput = syn::parse(input).unwrap(); 8 | impl_sawp_flags(&ast).into() 9 | } 10 | 11 | fn impl_sawp_flags(ast: &syn::DeriveInput) -> TokenStream { 12 | let name = &ast.ident; 13 | let repr = if let Some(repr) = get_repr(ast) { 14 | repr 15 | } else { 16 | panic!("BitFlags enum must have a `repr` attribute with numeric argument"); 17 | }; 18 | match &ast.data { 19 | syn::Data::Enum(data) => impl_enum_traits(name, &repr, data), 20 | _ => panic!("Bitflags is only supported on enums"), 21 | } 22 | } 23 | 24 | fn get_repr(ast: &syn::DeriveInput) -> Option { 25 | ast.attrs.iter().find_map(|attr| { 26 | if let Some(path) = attr.path.get_ident() { 27 | if path == "repr" { 28 | if let Some(tree) = attr.tokens.clone().into_iter().next() { 29 | match tree { 30 | TokenTree::Group(group) => { 31 | if let Some(ident) = group.stream().into_iter().next() { 32 | match ident { 33 | TokenTree::Ident(ident) => Some(ident), 34 | _ => None, 35 | } 36 | } else { 37 | None 38 | } 39 | } 40 | _ => None, 41 | } 42 | } else { 43 | None 44 | } 45 | } else { 46 | None 47 | } 48 | } else { 49 | None 50 | } 51 | }) 52 | } 53 | 54 | fn impl_enum_traits(name: &syn::Ident, repr: &Ident, data: &syn::DataEnum) -> TokenStream { 55 | // TODO: compile error when these items are reused. 56 | let list_items = data.variants.iter().map(|variant| &variant.ident); 57 | let list_all = list_items.clone(); 58 | let display_items = list_items.clone(); 59 | let from_str_items = list_items.clone(); 60 | let from_str_items_str = list_items.clone().map(|variant| { 61 | Ident::new( 62 | variant.to_string().to_lowercase().as_str(), 63 | Span::call_site(), 64 | ) 65 | }); 66 | 67 | quote! { 68 | impl Flag for #name { 69 | type Primitive = #repr; 70 | 71 | const ITEMS: &'static [Self] = &[#(#name::#list_items),*]; 72 | 73 | fn bits(self) -> Self::Primitive { 74 | self as #repr 75 | } 76 | 77 | fn none() -> Flags { 78 | Flags::from_bits(0) 79 | } 80 | 81 | fn all() -> Flags { 82 | Flags::from_bits(#(#name::#list_all as Self::Primitive)|*) 83 | } 84 | } 85 | 86 | impl std::ops::BitOr for #name { 87 | type Output = Flags<#name>; 88 | 89 | fn bitor(self, other: Self) -> Self::Output { 90 | Flags::from_bits(self.bits() | other.bits()) 91 | } 92 | } 93 | 94 | impl std::ops::BitAnd for #name { 95 | type Output = Flags<#name>; 96 | 97 | fn bitand(self, other: Self) -> Self::Output { 98 | Flags::from_bits(self.bits() & other.bits()) 99 | } 100 | } 101 | 102 | impl std::ops::BitXor for #name { 103 | type Output = Flags<#name>; 104 | 105 | fn bitxor(self, other: Self) -> Self::Output { 106 | Flags::from_bits(self.bits() ^ other.bits()) 107 | } 108 | } 109 | 110 | impl std::ops::Not for #name { 111 | type Output = Flags<#name>; 112 | 113 | fn not(self) -> Self::Output { 114 | Flags::from_bits(!self.bits()) 115 | } 116 | } 117 | 118 | impl std::fmt::Display for #name { 119 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 120 | let empty = self.bits() == Self::none().bits(); 121 | let mut first = true; 122 | #( 123 | if self.bits() & #name::#display_items.bits() == #name::#display_items.bits() { 124 | write!(f, "{}{}", if first { "" } else { " | " }, stringify!(#display_items))?; 125 | first = false; 126 | 127 | if empty { 128 | return Ok(()); 129 | } 130 | } 131 | )* 132 | 133 | if empty { 134 | write!(f, "NONE")?; 135 | } 136 | 137 | Ok(()) 138 | } 139 | } 140 | 141 | impl std::str::FromStr for #name { 142 | type Err = (); 143 | fn from_str(val: &str) -> std::result::Result<#name, Self::Err> { 144 | match val.to_lowercase().as_str() { 145 | #(stringify!(#from_str_items_str) => Ok(#name::#from_str_items),)* 146 | _ => Err(()), 147 | } 148 | } 149 | } 150 | 151 | impl PartialEq> for #name { 152 | fn eq(&self, other: &Flags) -> bool { 153 | self.bits() == other.bits() 154 | } 155 | } 156 | 157 | impl std::fmt::Binary for #name { 158 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 159 | std::fmt::Binary::fmt(&self.bits(), f) 160 | } 161 | } 162 | } 163 | } 164 | 165 | /// BitFlags derive macro tests 166 | /// 167 | /// `#[derive(BitFlags)]` can't be used here and `impl_sawp_flags` 168 | /// is being called directly instead. 169 | #[cfg(test)] 170 | mod tests { 171 | use super::*; 172 | 173 | #[test] 174 | fn test_macro_enum() { 175 | let input = r#" 176 | #[repr(u8)] 177 | enum Test { 178 | A = 0b0000, 179 | B = 0b0001, 180 | C = 0b0010, 181 | D = 0b0100, 182 | } 183 | "#; 184 | let parsed: syn::DeriveInput = syn::parse_str(input).unwrap(); 185 | impl_sawp_flags(&parsed); 186 | } 187 | 188 | #[test] 189 | #[should_panic(expected = "BitFlags enum must have a `repr` attribute")] 190 | fn test_macro_repr_panic() { 191 | let input = r#" 192 | enum Test { 193 | A = 0b0000, 194 | B = 0b0001, 195 | C = 0b0010, 196 | D = 0b0100, 197 | } 198 | "#; 199 | let parsed: syn::DeriveInput = syn::parse_str(input).unwrap(); 200 | impl_sawp_flags(&parsed); 201 | } 202 | 203 | #[test] 204 | #[should_panic(expected = "Bitflags is only supported on enums")] 205 | fn test_macro_not_enum_panic() { 206 | let input = r#" 207 | #[repr(u8)] 208 | struct Test { 209 | } 210 | "#; 211 | let parsed: syn::DeriveInput = syn::parse_str(input).unwrap(); 212 | impl_sawp_flags(&parsed); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /sawp-flags/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-flags" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP BitFlags Handling and Storage" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["bitflags", "bit", "flags", "bitmask"] 12 | include = [ 13 | "Cargo.toml", 14 | "../LICENSE", 15 | "../README.md", 16 | "src/**/*.rs", 17 | ] 18 | 19 | [dependencies] 20 | sawp-flags-derive = { path = "../sawp-flags-derive", version = "^0.13.1" } 21 | 22 | # Override default replacements 23 | [package.metadata.release] 24 | pre-release-replacements = [] 25 | -------------------------------------------------------------------------------- /sawp-flags/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Bitflags handling and storage. 2 | //! 3 | //! This crate allows you to define flag values using an enum and derive 4 | //! `BitFlags` to add convenience methods. 5 | //! 6 | //! This implementation was heavily inspired by 7 | //! [enumflags2](https://crates.io/crates/enumflags2) and 8 | //! [bitflags](https://crates.io/crates/bitflags) and customized for use in a 9 | //! sawp parser. Consider using those two open source projects before resorting 10 | //! to this one. One key feature is that we are automatically generating ffi 11 | //! accessors using the [sawp-ffi](https://crates.io/crates/sawp-ffi) crate. 12 | //! 13 | //! This crate works as follows: 14 | //! - `enum YourEnum` with a numeric representation (e.g. `#[repr(u8)]`) is used 15 | //! to define bit fields. 16 | //! - deriving `BitFlags` on this enum will add convenience methods for bitwise 17 | //! operations and implement the `Flag` trait. 18 | //! - Flag values are transparently stored as `Flags` so you can perform 19 | //! more operations on this type. 20 | //! 21 | //! # Example 22 | //! See `example` module for a generated example as well. 23 | //! ``` 24 | //! use sawp_flags::{BitFlags, Flags, Flag}; 25 | //! 26 | //! /// Example enum 27 | //! #[derive(Debug, Clone, Copy, PartialEq, BitFlags)] 28 | //! #[repr(u8)] 29 | //! pub enum Test { 30 | //! A = 0b0001, 31 | //! B = 0b0010, 32 | //! C = 0b0100, 33 | //! D = 0b1000, 34 | //! /// Variants can be a bitmask of the other fields like so 35 | //! E = Test::A as u8 | Test::B as u8 | Test::C as u8 | Test::D as u8, 36 | //! } 37 | //! 38 | //! // `flags` will be of transparent type `Flags` 39 | //! let flags : Flags = Test::A | Test::C; 40 | //! 41 | //! // convert a number to flags using `from_bits()` 42 | //! assert_eq!(flags, Flags::::from_bits(0b101)); 43 | //! 44 | //! // convert flags to a number using `bits()` 45 | //! assert_eq!(0b101, flags.bits()); 46 | //! 47 | //! // perform bitwise operations 48 | //! assert_eq!(Test::A | Test::B | Test::C, flags | Test::B); 49 | //! assert_eq!(Test::A, flags & Test::A); 50 | //! assert_eq!(Test::C, flags ^ Test::A); 51 | //! 52 | //! // check which flags are set 53 | //! assert!(flags.contains(Test::A)); 54 | //! assert!(!flags.contains(Test::A | Test::B)); 55 | //! assert!(flags.intersects(Test::A)); 56 | //! assert!(flags.intersects(Test::A | Test::B)); 57 | //! ``` 58 | 59 | use std::ops::*; 60 | 61 | /// The `BitFlags` derive macro will implement the `Flags` Trait on your enum and 62 | /// provide convenience methods for bit operations and type conversions. 63 | /// 64 | // Re-export derive macro for convenience. 65 | pub use sawp_flags_derive::BitFlags; 66 | 67 | /// A primitive numeric type to be used for flag storage. 68 | pub trait Primitive: 69 | Default 70 | + BitOr 71 | + BitAnd 72 | + BitXor 73 | + Not 74 | + PartialOrd 75 | + std::fmt::Debug 76 | + std::fmt::Binary 77 | + Copy 78 | + Clone 79 | { 80 | } 81 | 82 | impl Primitive for u8 {} 83 | impl Primitive for u16 {} 84 | impl Primitive for u32 {} 85 | impl Primitive for u64 {} 86 | impl Primitive for u128 {} 87 | 88 | /// A trait implemented by all flag enums. 89 | pub trait Flag: Copy + Clone + std::fmt::Debug + std::fmt::Display + 'static { 90 | /// Associated primitive numeric type 91 | type Primitive: Primitive; 92 | 93 | /// A list of all flag variants in the enum 94 | const ITEMS: &'static [Self]; 95 | 96 | /// Numeric representation of the variant 97 | fn bits(self) -> Self::Primitive; 98 | 99 | /// Flag value when no variants are set 100 | fn none() -> Flags; 101 | 102 | /// Flag value when all variants are set 103 | fn all() -> Flags; 104 | } 105 | 106 | /// Storage type for handling flags 107 | #[derive(Copy, Clone, PartialEq, Eq)] 108 | #[repr(transparent)] 109 | pub struct Flags::Primitive> { 110 | val: Primitive, 111 | marker: std::marker::PhantomData, 112 | } 113 | 114 | impl std::fmt::Debug for Flags 115 | where 116 | Enum: Flag, 117 | { 118 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 119 | self.val.fmt(f) 120 | } 121 | } 122 | 123 | impl Default for Flags 124 | where 125 | Enum: Flag, 126 | { 127 | fn default() -> Self { 128 | Self { 129 | val: ::Primitive::default(), 130 | marker: std::marker::PhantomData, 131 | } 132 | } 133 | } 134 | 135 | impl Flags 136 | where 137 | Enum: Flag, 138 | { 139 | /// Get a flag from a single enum value 140 | pub fn from_flag(flag: Enum) -> Self { 141 | Self { 142 | val: flag.bits(), 143 | marker: std::marker::PhantomData, 144 | } 145 | } 146 | 147 | /// Get a flag from a numeric value 148 | /// 149 | /// Note: the value is unchecked so any bit may be set. Be 150 | /// careful because `PartialEq` is a direct comparison of 151 | /// underlying bits. 152 | pub fn from_bits(bits: ::Primitive) -> Self { 153 | Self { 154 | val: bits, 155 | marker: std::marker::PhantomData, 156 | } 157 | } 158 | 159 | /// Numeric representation of the variant 160 | pub fn bits(&self) -> ::Primitive { 161 | self.val 162 | } 163 | 164 | /// Reference to numeric representation of the variant 165 | pub fn bits_ref(&self) -> &::Primitive { 166 | &self.val 167 | } 168 | 169 | /// Check if at least one flag in common is set 170 | pub fn intersects>>(self, rhs: B) -> bool { 171 | (self & rhs.into()).bits() != Enum::none().bits() 172 | } 173 | 174 | /// Check if all flags provided in `rhs` are set 175 | pub fn contains>>(self, rhs: B) -> bool { 176 | let rhs = rhs.into(); 177 | (self & rhs).bits() == rhs.bits() 178 | } 179 | 180 | pub fn is_empty(&self) -> bool { 181 | self.bits() == ::none().bits() 182 | } 183 | 184 | pub fn is_all(&self) -> bool { 185 | self.bits() == ::all().bits() 186 | } 187 | } 188 | 189 | impl From for Flags { 190 | fn from(flag: Enum) -> Self { 191 | Self::from_flag(flag) 192 | } 193 | } 194 | 195 | impl PartialEq for Flags { 196 | fn eq(&self, other: &Enum) -> bool { 197 | self.bits() == other.bits() 198 | } 199 | } 200 | 201 | impl std::ops::BitOr for Flags 202 | where 203 | T: Flag, 204 | B: Into>, 205 | { 206 | type Output = Flags; 207 | fn bitor(self, other: B) -> Flags { 208 | Flags::from_bits(self.bits() | other.into().bits()) 209 | } 210 | } 211 | 212 | impl std::ops::BitOrAssign for Flags 213 | where 214 | T: Flag, 215 | B: Into>, 216 | { 217 | fn bitor_assign(&mut self, rhs: B) { 218 | *self = Flags::from_bits(self.bits() | rhs.into().bits()) 219 | } 220 | } 221 | 222 | impl std::ops::BitAnd for Flags 223 | where 224 | T: Flag, 225 | B: Into>, 226 | { 227 | type Output = Flags; 228 | fn bitand(self, other: B) -> Flags { 229 | Flags::from_bits(self.bits() & other.into().bits()) 230 | } 231 | } 232 | 233 | impl std::ops::BitAndAssign for Flags 234 | where 235 | T: Flag, 236 | B: Into>, 237 | { 238 | fn bitand_assign(&mut self, rhs: B) { 239 | *self = Flags::from_bits(self.bits() & rhs.into().bits()) 240 | } 241 | } 242 | 243 | impl std::ops::BitXor for Flags 244 | where 245 | T: Flag, 246 | B: Into>, 247 | { 248 | type Output = Flags; 249 | fn bitxor(self, other: B) -> Flags { 250 | Flags::from_bits(self.bits() ^ other.into().bits()) 251 | } 252 | } 253 | 254 | impl std::ops::BitXorAssign for Flags 255 | where 256 | T: Flag, 257 | B: Into>, 258 | { 259 | fn bitxor_assign(&mut self, rhs: B) { 260 | *self = Flags::from_bits(self.bits() ^ rhs.into().bits()) 261 | } 262 | } 263 | 264 | impl std::ops::Not for Flags { 265 | type Output = Flags; 266 | 267 | fn not(self) -> Self::Output { 268 | Flags::from_bits(!self.bits()) 269 | } 270 | } 271 | 272 | impl std::fmt::Display for Flags { 273 | /// A pipe-separated list of set flags. 274 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 275 | let none = self.bits() == T::none().bits(); 276 | let mut first = true; 277 | for val in ::ITEMS 278 | .iter() 279 | .cloned() 280 | .filter(move |&flag| self.contains(flag)) 281 | { 282 | write!(f, "{}{:?}", if first { "" } else { " | " }, val)?; 283 | first = false; 284 | 285 | if none { 286 | return Ok(()); 287 | } 288 | } 289 | 290 | if none { 291 | write!(f, "NONE")?; 292 | } 293 | 294 | Ok(()) 295 | } 296 | } 297 | 298 | impl std::fmt::Binary for Flags { 299 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 300 | std::fmt::Binary::fmt(&self.bits(), f) 301 | } 302 | } 303 | 304 | /// Example enum deriving `BitFlags` 305 | pub mod example { 306 | use super::*; 307 | 308 | /// Example enum 309 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BitFlags)] 310 | #[repr(u8)] 311 | pub enum Test { 312 | A = 0b0001, 313 | B = 0b0010, 314 | C = 0b0100, 315 | D = 0b1000, 316 | /// Variants can be bitmask of other fields 317 | E = Test::A as u8 | Test::B as u8 | Test::C as u8 | Test::D as u8, 318 | } 319 | } 320 | 321 | #[cfg(test)] 322 | mod test { 323 | use super::{example::*, *}; 324 | 325 | #[test] 326 | fn test_enum_bits() { 327 | let bits = 0b1010_1010; 328 | let flags = Flags::::from_bits(bits); 329 | assert_eq!(bits, flags.bits()); 330 | assert_eq!(&bits, flags.bits_ref()); 331 | } 332 | 333 | #[test] 334 | fn test_enum_or() { 335 | let mut flags = Test::A | Test::B; 336 | assert_eq!(0b0011, flags.bits()); 337 | 338 | flags |= Test::C; 339 | assert_eq!(0b0111, flags.bits()); 340 | 341 | flags |= Test::C | Test::D; 342 | assert_eq!(0b1111, flags.bits()); 343 | } 344 | 345 | #[test] 346 | fn test_enum_and() { 347 | let mut flags = Test::E & Test::B; 348 | assert_eq!(0b0010, flags.bits()); 349 | 350 | flags &= Test::B; 351 | assert_eq!(0b0010, flags.bits()); 352 | 353 | flags &= Test::E & Test::B; 354 | assert_eq!(0b0010, flags.bits()); 355 | } 356 | 357 | #[test] 358 | fn test_enum_xor() { 359 | let mut flags = Test::A ^ Test::B; 360 | assert_eq!(0b0011, flags.bits()); 361 | 362 | flags ^= Test::C; 363 | assert_eq!(0b0111, flags.bits()); 364 | 365 | flags ^= Test::D ^ Test::B; 366 | assert_eq!(0b1101, flags.bits()); 367 | } 368 | 369 | #[test] 370 | fn test_enum_not() { 371 | let flags = !Test::A; 372 | assert_eq!(0b1111_1110, flags.bits()); 373 | let flags = !(Test::A ^ Test::B); 374 | assert_eq!(0b1111_1100, flags.bits()); 375 | } 376 | 377 | #[test] 378 | fn test_contains() { 379 | let flags = Test::A | Test::C; 380 | assert!(flags.contains(Test::A)); 381 | assert!(!flags.contains(Test::B)); 382 | assert!(!flags.contains(Test::E)); 383 | assert!(!flags.contains(Test::B | Test::D)); 384 | assert!(!flags.contains(Test::A | Test::B)); 385 | assert!(flags.contains(Test::A | Test::C)); 386 | } 387 | 388 | #[test] 389 | fn test_intersects() { 390 | let flags = Test::A | Test::C; 391 | assert!(flags.intersects(Test::A)); 392 | assert!(flags.intersects(Test::E)); 393 | assert!(flags.intersects(Test::A | Test::B)); 394 | assert!(flags.intersects(Test::A | Test::C)); 395 | assert!(flags.intersects(Test::A | Test::B | Test::C)); 396 | assert!(!flags.intersects(Test::B | Test::D)); 397 | } 398 | 399 | #[test] 400 | fn test_eq() { 401 | let flags = Test::A; 402 | assert_eq!(flags, Test::A); 403 | assert_eq!(Test::A, flags); 404 | 405 | let flags = Test::A | Test::C; 406 | assert_ne!(flags, Test::A); 407 | assert_ne!(flags, Test::C); 408 | assert_ne!(Test::A, flags); 409 | assert_eq!(flags, Test::A | Test::C); 410 | assert_ne!(flags, Test::A | Test::C | Test::E); 411 | 412 | let flags = Flags::::from_bits(0b1000_0001); 413 | assert_ne!(flags, Test::A); 414 | } 415 | 416 | #[test] 417 | fn test_enum_string() { 418 | assert_eq!("NONE", Test::none().to_string()); 419 | assert_eq!("A", Test::A.to_string()); 420 | assert_eq!("A | B", (Test::A | Test::B).to_string()); 421 | assert_eq!("A | B | C | D | E", Test::E.to_string()); 422 | assert_eq!("A | B | C | D | E", Flags::from_flag(Test::E).to_string()); 423 | } 424 | 425 | #[test] 426 | fn test_enum_string_none() { 427 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BitFlags)] 428 | #[repr(u8)] 429 | pub enum Test { 430 | Zero = 0b0000, 431 | A = 0b0001, 432 | B = 0b0010, 433 | C = 0b0100, 434 | D = 0b1000, 435 | /// Variants can be bitmask of other fields 436 | E = Test::A as u8 | Test::B as u8 | Test::C as u8 | Test::D as u8, 437 | } 438 | assert_eq!("Zero", Test::Zero.to_string()); 439 | assert_eq!("Zero", Test::none().to_string()); 440 | assert_eq!("Zero", Flags::from_flag(Test::Zero).to_string()); 441 | } 442 | 443 | #[test] 444 | fn test_enum_format() { 445 | assert_eq!("A", format!("{:?}", Test::A)); 446 | assert_eq!("E", format!("{:?}", Test::E)); 447 | assert_eq!("0", format!("{:?}", Test::none())); 448 | 449 | assert_eq!("0", format!("{:b}", Test::none())); 450 | assert_eq!("1", format!("{:b}", Test::A)); 451 | assert_eq!("1111", format!("{:b}", Test::E)); 452 | } 453 | 454 | #[test] 455 | fn test_enum_from_str() { 456 | use std::str::FromStr; 457 | assert_eq!(Err(()), Test::from_str("")); 458 | assert_eq!(Ok(Test::A), Test::from_str("a")); 459 | assert_eq!(Ok(Test::A), Test::from_str("A")); 460 | } 461 | 462 | #[test] 463 | fn test_all() { 464 | assert_eq!(Test::E, Test::all()); 465 | assert!(!Flags::from_flag(Test::A).is_all()); 466 | assert!(Flags::from_flag(Test::E).is_all()); 467 | } 468 | 469 | #[test] 470 | fn test_none() { 471 | assert_eq!(Flags::from_bits(0), Test::none()); 472 | assert!(Flags::::from_bits(0).is_empty()); 473 | assert!(!Flags::from_flag(Test::A).is_empty()); 474 | assert!(!Flags::from_flag(Test::E).is_empty()); 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /sawp-gre/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-gre" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for GRE" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CyberCentreCanada/sawp" 10 | homepage = "https://github.com/CyberCentreCanada/sawp" 11 | keywords = ["gre", "parser", "protocol", "networking", "routing"] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | ffi = ["cbindgen", "sawp/ffi", "sawp-ffi"] 22 | verbose = ["sawp/verbose"] 23 | 24 | [build-dependencies] 25 | cbindgen = {version = "0.15.0", optional = true} 26 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 27 | 28 | [dependencies] 29 | sawp-ffi = { path = "../sawp-ffi", version = "^0.13.1", optional = true} 30 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 31 | sawp = {path = "..", version = "^0.13.1" } 32 | nom = "7.1.1" 33 | num_enum = "0.5.1" 34 | 35 | [lib] 36 | crate-type = ["staticlib", "rlib", "cdylib"] 37 | 38 | [dev-dependencies] 39 | rstest = "0.6.4" 40 | 41 | # Override default replacements 42 | [package.metadata.release] 43 | pre-release-replacements = [] 44 | -------------------------------------------------------------------------------- /sawp-ike/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-ike" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for IKEv2" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CyberCentreCanada/sawp" 10 | homepage = "https://github.com/CyberCentreCanada/sawp" 11 | keywords = ["ike", "parser", "protocol",] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | ffi = ["cbindgen", "sawp/ffi", "sawp-ffi"] 22 | verbose = ["sawp/verbose"] 23 | 24 | [build-dependencies] 25 | cbindgen = {version = "0.15", optional = true} 26 | 27 | [dependencies] 28 | sawp-ffi = {path = "../sawp-ffi", version = "^0.13.1", optional = true} 29 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 30 | sawp = {path = "..", version = "^0.13.1" } 31 | nom = "7.1.1" 32 | num_enum = "0.5.1" 33 | byteorder = "1.4.3" 34 | 35 | [lib] 36 | crate-type = ["staticlib", "rlib", "cdylib"] 37 | 38 | [dev-dependencies] 39 | rstest = "0.6.4" 40 | 41 | # Override default replacements 42 | [package.metadata.release] 43 | pre-release-replacements = [] 44 | -------------------------------------------------------------------------------- /sawp-ike/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C++" 2 | pragma_once = true 3 | 4 | includes = ["sawp.h"] 5 | namespaces = ["sawp", "ike"] 6 | 7 | autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Do NOT modify manually */" 8 | 9 | # If this option is true `usize` and `isize` will be converted into `size_t` and `ptrdiff_t` 10 | # instead of `uintptr_t` and `intptr_t` respectively. 11 | usize_is_size_t = true 12 | 13 | [export] 14 | exclude = ["Vec"] 15 | # Need includes for Flags, since they aren't referenced by public FFI functions 16 | include = ["ErrorFlags"] 17 | 18 | [parse.expand] 19 | crates = ["sawp", "sawp-ike"] 20 | all_features = true 21 | -------------------------------------------------------------------------------- /sawp-ike/src/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use super::{payloads::Attribute, Direction, Ike, Message}; 4 | 5 | use sawp::{error::Error, parser::Parse}; 6 | 7 | use sawp_ffi::IntoFFIPtr; 8 | 9 | #[repr(C)] 10 | pub struct ParseResult { 11 | message: *mut Message, 12 | size_read: usize, 13 | error: *mut Error, 14 | } 15 | 16 | #[no_mangle] 17 | pub unsafe extern "C" fn sawp_ike_create() -> *mut Ike { 18 | let parser = Ike::default(); 19 | parser.into_ffi_ptr() 20 | } 21 | 22 | #[no_mangle] 23 | pub unsafe extern "C" fn sawp_ike_destroy(d: *mut Ike) { 24 | if !d.is_null() { 25 | drop(Box::from_raw(d)); 26 | } 27 | } 28 | 29 | /// # Safety 30 | /// function will panic if called with null 31 | #[no_mangle] 32 | pub unsafe extern "C" fn sawp_ike_parse( 33 | parser: *const Ike, 34 | direction: Direction, 35 | data: *const u8, 36 | length: usize, 37 | ) -> *mut ParseResult { 38 | let input = std::slice::from_raw_parts(data, length); 39 | match (*parser).parse(input, direction) { 40 | Ok((sl, message)) => ParseResult { 41 | message: message.into_ffi_ptr(), 42 | // Should never actually underflow as parse cannot grow the input slice 43 | size_read: length.saturating_sub(sl.len()), 44 | error: std::ptr::null_mut(), 45 | } 46 | .into_ffi_ptr(), 47 | Err(e) => ParseResult { 48 | message: std::ptr::null_mut(), 49 | size_read: 0, 50 | error: e.into_ffi_ptr(), 51 | } 52 | .into_ffi_ptr(), 53 | } 54 | } 55 | 56 | impl Drop for ParseResult { 57 | fn drop(&mut self) { 58 | unsafe { 59 | sawp_ike_message_destroy(self.message); 60 | if !self.error.is_null() { 61 | drop(Box::from_raw(self.error)); 62 | } 63 | } 64 | } 65 | } 66 | 67 | /// Free ParseResult 68 | /// Will also destroy contained message and error 69 | #[no_mangle] 70 | pub unsafe extern "C" fn sawp_ike_parse_result_destroy(d: *mut ParseResult) { 71 | if !d.is_null() { 72 | drop(Box::from_raw(d)); 73 | } 74 | } 75 | 76 | #[no_mangle] 77 | pub unsafe extern "C" fn sawp_ike_message_destroy(d: *mut Message) { 78 | if !d.is_null() { 79 | drop(Box::from_raw(d)); 80 | } 81 | } 82 | 83 | #[no_mangle] 84 | pub unsafe extern "C" fn sawp_ike_vec_attributes_ptr_to_idx( 85 | #[allow(clippy::ptr_arg)] vec: &Vec, 86 | n: usize, 87 | ) -> *const Attribute { 88 | vec.get(n).unwrap() 89 | } 90 | 91 | #[no_mangle] 92 | pub unsafe extern "C" fn sawp_ike_vec_attributes_get_size( 93 | #[allow(clippy::ptr_arg)] vec: &Vec, 94 | ) -> usize { 95 | vec.len() 96 | } 97 | 98 | #[no_mangle] 99 | pub unsafe extern "C" fn sawp_ike_2d_vec_ptr_to_idx( 100 | #[allow(clippy::ptr_arg)] vec: &Vec>, 101 | n: usize, 102 | ) -> *const Vec { 103 | vec.get(n).unwrap() 104 | } 105 | 106 | #[no_mangle] 107 | pub unsafe extern "C" fn sawp_ike_2d_vec_get_size( 108 | #[allow(clippy::ptr_arg)] vec: &Vec>, 109 | ) -> usize { 110 | vec.len() 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use super::{super::payloads::AttributeFormat, *}; 116 | 117 | #[test] 118 | fn atttribute_array() { 119 | let vec = vec![ 120 | Attribute { 121 | att_format: AttributeFormat::TypeLengthValue, 122 | att_type: 1, 123 | att_length: 3, 124 | att_value: vec![1, 2, 3], 125 | }, 126 | Attribute { 127 | att_format: AttributeFormat::TypeValue, 128 | att_type: 2, 129 | att_length: 0, 130 | att_value: vec![1, 2], 131 | }, 132 | ]; 133 | 134 | assert_eq!(unsafe { sawp_ike_vec_attributes_get_size(&vec) }, 2); 135 | let vec_ptr = unsafe { sawp_ike_vec_attributes_ptr_to_idx(&vec, 0) }; 136 | assert_eq!( 137 | unsafe { &*vec_ptr }, 138 | &Attribute { 139 | att_format: AttributeFormat::TypeLengthValue, 140 | att_type: 1, 141 | att_length: 3, 142 | att_value: vec![1, 2, 3] 143 | } 144 | ); 145 | 146 | let vec_ptr = unsafe { sawp_ike_vec_attributes_ptr_to_idx(&vec, 1) }; 147 | assert_eq!( 148 | unsafe { &*vec_ptr }, 149 | &Attribute { 150 | att_format: AttributeFormat::TypeValue, 151 | att_type: 2, 152 | att_length: 0, 153 | att_value: vec![1, 2], 154 | } 155 | ); 156 | } 157 | 158 | #[test] 159 | fn two_d_array() { 160 | let vec = vec![vec![1u8, 2u8], vec![3u8, 4u8]]; 161 | 162 | assert_eq!(unsafe { sawp_ike_2d_vec_get_size(&vec) }, 2); 163 | 164 | let vec_ptr = unsafe { sawp_ike_2d_vec_ptr_to_idx(&vec, 0) }; 165 | assert_eq!(unsafe { &*vec_ptr }, &vec![1u8, 2u8]); 166 | let vec_ptr = unsafe { sawp_ike_2d_vec_ptr_to_idx(&vec, 1) }; 167 | assert_eq!(unsafe { &*vec_ptr }, &vec![3u8, 4u8]); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /sawp-ike/src/header.rs: -------------------------------------------------------------------------------- 1 | use crate::{ErrorFlags, PayloadType}; 2 | 3 | use sawp::error::Result; 4 | use sawp_flags::{BitFlags, Flag, Flags}; 5 | 6 | #[cfg(feature = "ffi")] 7 | use sawp_ffi::GenerateFFI; 8 | 9 | use nom::combinator::{map, verify}; 10 | use nom::number::streaming::{be_u32, be_u64, be_u8}; 11 | use nom::sequence::tuple; 12 | 13 | use num_enum::FromPrimitive; 14 | 15 | /// Length of an IKE header 16 | pub const HEADER_LEN: u32 = 28; 17 | 18 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 19 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_ike"))] 20 | #[derive(Debug, FromPrimitive, PartialEq, Eq, Copy, Clone)] 21 | #[repr(u8)] 22 | pub enum ExchangeType { 23 | None = 0, 24 | Base = 1, 25 | IdentityProtection = 2, 26 | AuthenticationOnly = 3, 27 | Aggressive = 4, 28 | InformationalV1 = 5, 29 | QuickMode = 32, 30 | IkeSaInit = 34, 31 | IkeAuth = 35, 32 | CreateChildSa = 36, 33 | Informational = 37, 34 | IkeSessionResume = 38, 35 | GsaAuth = 39, 36 | GsaRegistration = 40, 37 | GsaRekey = 41, 38 | IkeIntermediate = 43, 39 | IkeFollowupKe = 44, 40 | #[num_enum(default)] 41 | Unknown, 42 | } 43 | 44 | /// Flags that can be set for IKEv1 and IKEv2 45 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 46 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_ike"))] 47 | #[repr(u8)] 48 | #[derive(Clone, Copy, Debug, PartialEq, Eq, BitFlags)] 49 | pub enum IkeFlags { 50 | /// Body is encrypted 51 | ENCRYPTED = 0b0000_0001, 52 | /// Signal for Key Exchange synchronization 53 | COMMIT = 0b0000_0010, 54 | /// Authenticated but not Encrypted 55 | AUTHENTICATION = 0b0000_0100, 56 | /// Sender is the original initiator 57 | INITIATOR = 0b0000_1000, 58 | /// Version upgrade available from sender 59 | VERSION = 0b0001_0000, 60 | /// Message is a response to a message containing the same Message ID 61 | RESPONSE = 0b0010_0000, 62 | } 63 | 64 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 65 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_ike"))] 66 | #[derive(Debug, PartialEq, Eq)] 67 | pub struct Header { 68 | pub initiator_spi: u64, 69 | pub responder_spi: u64, 70 | pub next_payload: PayloadType, 71 | pub version: u8, 72 | pub major_version: u8, 73 | pub minor_version: u8, 74 | pub exchange_type: ExchangeType, 75 | #[cfg_attr(feature = "ffi", sawp_ffi(flag = "u8"))] 76 | pub flags: Flags, 77 | pub message_id: u32, 78 | pub length: u32, 79 | } 80 | 81 | impl Header { 82 | pub const MAJOR_VERSION_MASK: u8 = 0xF0; 83 | pub const MINOR_VERSION_MASK: u8 = 0x0F; 84 | 85 | // V1 Header - RFC2408 86 | // 1 2 3 87 | // 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 88 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 89 | // ! Initiator ! 90 | // ! Cookie ! 91 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 92 | // ! Responder ! 93 | // ! Cookie ! 94 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 95 | // ! Next Payload ! MjVer ! MnVer ! Exchange Type ! Flags ! 96 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 97 | // ! Message ID ! 98 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 99 | // ! Length ! 100 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 101 | // 102 | // V2 Header - RFC7296 103 | // 1 2 3 104 | // 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 105 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 106 | // | IKE SA Initiator's SPI | 107 | // | | 108 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 109 | // | IKE SA Responder's SPI | 110 | // | | 111 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 112 | // | Next Payload | MjVer | MnVer | Exchange Type | Flags | 113 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 114 | // | Message ID | 115 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 116 | // | Length | 117 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 118 | #[allow(clippy::type_complexity)] 119 | pub fn parse(input: &[u8]) -> Result<(&[u8], (Self, Flags))> { 120 | let mut error_flags = ErrorFlags::none(); 121 | 122 | let ( 123 | input, 124 | (initiator_spi, responder_spi, next_payload, (version, (major_version, minor_version))), 125 | ) = tuple(( 126 | be_u64, 127 | be_u64, 128 | map(be_u8, PayloadType::from), 129 | verify( 130 | map(be_u8, |version| (version, Self::split_version(version))), 131 | |(_, (major, _))| (1..=2).contains(major), 132 | ), 133 | ))(input)?; 134 | 135 | let next_payload = if (major_version == 1 && !next_payload.is_v1()) 136 | || (major_version == 2 && !next_payload.is_v2()) 137 | { 138 | PayloadType::Unknown 139 | } else { 140 | next_payload 141 | }; 142 | if next_payload == PayloadType::Unknown { 143 | error_flags |= ErrorFlags::UnknownPayload; 144 | } 145 | 146 | let (input, (exchange_type, flags, message_id, length)) = tuple(( 147 | map(be_u8, ExchangeType::from), 148 | map(be_u8, Flags::::from_bits), 149 | be_u32, 150 | verify(be_u32, |length| *length >= HEADER_LEN), 151 | ))(input)?; 152 | if exchange_type == ExchangeType::Unknown { 153 | error_flags |= ErrorFlags::UnknownExchange; 154 | } 155 | 156 | let ikev1_flags = IkeFlags::ENCRYPTED | IkeFlags::COMMIT | IkeFlags::AUTHENTICATION; 157 | let ikev2_flags = IkeFlags::INITIATOR | IkeFlags::VERSION | IkeFlags::RESPONSE; 158 | if flags.intersects(ikev1_flags) && flags.intersects(ikev2_flags) { 159 | error_flags |= ErrorFlags::InvalidFlags; 160 | } 161 | 162 | if exchange_type == ExchangeType::IkeSaInit && flags.intersects(IkeFlags::INITIATOR) { 163 | // message_id must be zero in an initiator request 164 | if message_id != 0 { 165 | error_flags |= ErrorFlags::NonZeroMessageIdInInit; 166 | } 167 | // responder_spi must be zero in an initiator request 168 | if flags.intersects(IkeFlags::INITIATOR) && responder_spi != 0 { 169 | error_flags |= ErrorFlags::NonZeroResponderSpiInInit; 170 | } 171 | } 172 | 173 | if flags.intersects(IkeFlags::RESPONSE) && responder_spi == 0 { 174 | error_flags |= ErrorFlags::ZeroResponderSpiInResponse; 175 | } 176 | 177 | Ok(( 178 | input, 179 | ( 180 | Self { 181 | initiator_spi, 182 | responder_spi, 183 | next_payload, 184 | version, 185 | major_version, 186 | minor_version, 187 | exchange_type, 188 | flags, 189 | message_id, 190 | length, 191 | }, 192 | error_flags, 193 | ), 194 | )) 195 | } 196 | 197 | fn major_version(version: u8) -> u8 { 198 | (version & Self::MAJOR_VERSION_MASK) 199 | .checked_shr(4) 200 | .unwrap_or(0) 201 | } 202 | 203 | fn minor_version(version: u8) -> u8 { 204 | version & Self::MINOR_VERSION_MASK 205 | } 206 | 207 | fn split_version(version: u8) -> (u8, u8) { 208 | (Self::major_version(version), Self::minor_version(version)) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /sawp-ike/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An Internet Key Exchange (IKE) v1 and v2 parser. 2 | //! 3 | //! Given bytes and a [`sawp::parser::Direction`], it will attempt to parse the bytes 4 | //! and return a [`Message`]. The parser will inform the caller about errors if no 5 | //! message is returned and warnings if it was parsed but had nonstandard or erroneous 6 | //! data (see [`sawp::parser::Parse`] for details on possible return types). 7 | //! 8 | //! This parser keeps state for the current session so it is expected to create one 9 | //! parser per session. 10 | //! 11 | //! The following references were used to create this module: 12 | //! 13 | //! [ISAKMP](https://www.rfc-editor.org/rfc/rfc2408.html) 14 | //! 15 | //! [IKE v1](https://www.rfc-editor.org/rfc/rfc2409.html) 16 | //! 17 | //! [IKE v2 Fibre Channel](https://www.rfc-editor.org/rfc/rfc4595.html) 18 | //! 19 | //! [IKE v2](https://www.rfc-editor.org/rfc/rfc7296.html) 20 | //! 21 | //! [Group Key Management using IKEv2](https://datatracker.ietf.org/doc/draft-yeung-g-ikev2) 22 | //! 23 | //! # Example 24 | //! ``` 25 | //! use sawp::parser::{Direction, Parse}; 26 | //! use sawp::error::Error; 27 | //! use sawp::error::ErrorKind; 28 | //! use sawp_ike::{Ike, Message}; 29 | //! 30 | //! fn parse_bytes(input: &[u8]) -> std::result::Result<&[u8], Error> { 31 | //! let ike = Ike::default(); 32 | //! let mut bytes = input; 33 | //! while bytes.len() > 0 { 34 | //! // If we know that this is a request or response, change the Direction 35 | //! // for a more accurate parsing 36 | //! match ike.parse(bytes, Direction::Unknown) { 37 | //! // The parser succeeded and returned the remaining bytes and the parsed ike message 38 | //! Ok((rest, Some(message))) => { 39 | //! println!("IKE message: {:?}", message); 40 | //! bytes = rest; 41 | //! } 42 | //! // The parser recognized that this might be ike and made some progress, 43 | //! // but more bytes are needed to parse a full message 44 | //! Ok((rest, None)) => return Ok(rest), 45 | //! // The parser was unable to determine whether this was ike or not and more 46 | //! // bytes are needed 47 | //! Err(Error { kind: ErrorKind::Incomplete(_) }) => return Ok(bytes), 48 | //! // The parser determined that this was not ike 49 | //! Err(e) => return Err(e) 50 | //! } 51 | //! } 52 | //! 53 | //! Ok(bytes) 54 | //! } 55 | //! ``` 56 | 57 | #![deny(clippy::integer_arithmetic)] 58 | 59 | pub mod header; 60 | pub mod payloads; 61 | 62 | use header::{Header, IkeFlags, HEADER_LEN}; 63 | use payloads::{Payload, PayloadType}; 64 | 65 | use sawp::error::Result; 66 | use sawp::parser::{Direction, Parse}; 67 | use sawp::probe::Probe; 68 | use sawp::protocol::Protocol; 69 | use sawp_flags::{BitFlags, Flag, Flags}; 70 | 71 | /// FFI structs and Accessors 72 | #[cfg(feature = "ffi")] 73 | mod ffi; 74 | 75 | #[cfg(feature = "ffi")] 76 | use sawp_ffi::GenerateFFI; 77 | 78 | use nom::bytes::streaming::{tag, take}; 79 | use nom::combinator::opt; 80 | use nom::number::streaming::be_u32; 81 | use nom::sequence::tuple; 82 | 83 | type IResult<'a, O> = nom::IResult<&'a [u8], O, sawp::error::NomError<&'a [u8]>>; 84 | 85 | /// Classes of errors that can be returned by this parser. 86 | #[repr(u16)] 87 | #[derive(Clone, Copy, Debug, PartialEq, Eq, BitFlags)] 88 | pub enum ErrorFlags { 89 | /// Unknown Exchange number 90 | UnknownExchange = 0b0000_0000_0000_0001, 91 | /// Unknown Payload number 92 | UnknownPayload = 0b0000_0000_0000_0010, 93 | /// Found a payload in an invalid location 94 | InvalidPayload = 0b0000_0000_0000_0100, 95 | /// Known payload found which we have no parser for 96 | UnimplementedPayload = 0b0000_0000_0000_1000, 97 | /// Message ID was nonzero in an initiation message 98 | NonZeroMessageIdInInit = 0b0000_0000_0001_0000, 99 | /// Responder SPI was nonzero in an initiation message 100 | NonZeroResponderSpiInInit = 0b0000_0000_0010_0000, 101 | /// Responder SPI was not set in a response message 102 | ZeroResponderSpiInResponse = 0b0000_0000_0100_0000, 103 | /// Non-Zero reserved field found 104 | NonZeroReserved = 0b0000_0000_1000_0000, 105 | /// Invalid length in a payload 106 | /// 107 | /// Typically indicative of a length which is too short to accomodate the generic payload 108 | /// header 109 | InvalidLength = 0b0000_0001_0000_0000, 110 | /// Header flags were invalid. 111 | /// 112 | /// Either a nonexistant flag bit was set or both IKEv1 and IKEv2 flags were set at the same 113 | /// time. 114 | InvalidFlags = 0b0000_0010_0000_0000, 115 | } 116 | 117 | impl ErrorFlags { 118 | fn flatten(input: &[Flags]) -> Flags { 119 | input.iter().fold(Self::none(), |acc, e| acc | *e) 120 | } 121 | } 122 | 123 | /// The parsed message. 124 | /// 125 | /// [`Message::Ike`] is a parsed IKE v1 or v2 message. 126 | /// 127 | /// [`Message::Esp`] is an encapsulated security payload. These are seen when the 128 | /// encrypted communications are sent over UDP on the same 5-tuple as the IKE messages, typically on port 4500. 129 | /// When IKE operates over TCP, no ESP will be parsed as they encrypted data is sent without a 130 | /// transport layer (i.e. layer 3 Ethernet header followed by encrypted payload). 131 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 132 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_ike"))] 133 | #[derive(Debug, PartialEq, Eq)] 134 | pub enum Message { 135 | /// An IKE payload 136 | Ike(IkeMessage), 137 | /// Encapsulating Security Payload 138 | Esp(EspMessage), 139 | } 140 | 141 | /// The parsed IKEv1 or v2 message 142 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 143 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_ike"))] 144 | #[derive(Debug, PartialEq, Eq)] 145 | pub struct IkeMessage { 146 | /// The header 147 | pub header: Header, 148 | /// The array of payloads following the header 149 | pub payloads: Vec, 150 | /// Encrypted Data, if IKEv1 and ENCRYPTED flag is set 151 | pub encrypted_data: Vec, 152 | /// Errors encountered while parsing 153 | #[cfg_attr(feature = "ffi", sawp_ffi(flag = "u16"))] 154 | pub error_flags: Flags, 155 | } 156 | 157 | /// If UDP encapsulation is present, the metadata associated with it is parsed. 158 | /// 159 | /// The full encrypted payload, tail padding, and integrity check is not parsed. 160 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 161 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp_ike"))] 162 | #[derive(Debug, PartialEq, Eq)] 163 | pub struct EspMessage { 164 | pub spi: u32, 165 | pub sequence: u32, 166 | } 167 | 168 | /// Parser handle. 169 | /// 170 | /// # Notes 171 | /// The parser assumes one parser per session as it stores session state. A given session should 172 | /// re-use the same parser as more data is made available and each session should have its own 173 | /// parser. 174 | /// 175 | /// # FFI SAFETY 176 | /// This type is not [`Sync`] and this must be considered in FFI uses. This struct may be sent from 177 | /// one thread to another but it may not be shared between threads without locking access. In C++ 178 | /// this means a std::shared_ptr is not enough! std::mutex or other locking primitives must be 179 | /// used to ensure data races do not occur. 180 | #[derive(Debug, Default)] 181 | pub struct Ike { 182 | // On port 4500 ESP payloads are encapsulated in UDP but on port 500 they are not. 183 | // When UDP encapsulation for ESP is present IKE payloads are prefixed with 4 octets 184 | // 0x00 to differentiate them from ESP. 185 | // 186 | // When an IKE payload was prefixed with 0x00 we should treat any seen packets without 187 | // it as ESP payloads. When IKE was not prefixed with 0x00 then all packets should 188 | // be IKE. As such we have 3 states - ESP encapsulation present (Some(true)), ESP 189 | // encapsulation not present (Some(false)), and not yet determined (None). 190 | saw_udp_encapsulation: std::cell::Cell>, 191 | } 192 | 193 | impl Probe<'_> for Ike {} 194 | 195 | impl Protocol<'_> for Ike { 196 | type Message = Message; 197 | 198 | fn name() -> &'static str { 199 | "ike" 200 | } 201 | } 202 | 203 | impl<'a> Parse<'a> for Ike { 204 | fn parse( 205 | &self, 206 | input: &'a [u8], 207 | _direction: Direction, 208 | ) -> Result<(&'a [u8], Option)> { 209 | let input = match self.saw_udp_encapsulation.get() { 210 | // Previously saw encapsulation 211 | Some(true) => { 212 | let (input, non_esp_marker) = opt(tag(b"\x00\x00\x00\x00"))(input)?; 213 | if non_esp_marker.is_some() { 214 | // marker present, must be IKE. Continue 215 | input 216 | } else { 217 | let (input, (spi, sequence)) = tuple((be_u32, be_u32))(input)?; 218 | return Ok((input, Some(Message::Esp(EspMessage { spi, sequence })))); 219 | } 220 | } 221 | // Previously saw no encapsulation 222 | Some(false) => { 223 | // Parse like normal 224 | input 225 | } 226 | // Not yet determined 227 | None => { 228 | let (input, non_esp_marker) = opt(tag(b"\x00\x00\x00\x00"))(input)?; 229 | self.saw_udp_encapsulation 230 | .set(Some(non_esp_marker.is_some())); 231 | input 232 | } 233 | }; 234 | 235 | let (input, (header, header_error_flags)) = Header::parse(input)?; 236 | 237 | // subtracting HEADER_LEN is safe, length verified in Header::parse 238 | let (input, mut payload_input) = take(header.length.saturating_sub(HEADER_LEN))(input)?; 239 | 240 | let mut next_payload = header.next_payload; 241 | let mut payloads = Vec::new(); 242 | let mut payload_error_flags = ErrorFlags::none(); 243 | 244 | if header.major_version == 1 && header.flags.contains(IkeFlags::ENCRYPTED) { 245 | let message = Message::Ike(IkeMessage { 246 | header, 247 | payloads: Vec::new(), 248 | encrypted_data: payload_input.to_vec(), 249 | error_flags: ErrorFlags::none(), 250 | }); 251 | return Ok((input, Some(message))); 252 | } 253 | 254 | let parse = if header.major_version == 1 { 255 | Payload::parse_v1 256 | } else { 257 | Payload::parse_v2 258 | }; 259 | 260 | // While we have a next payload and they are not encrypted 261 | // In the case of encryption, all the payloads are encrypted and 262 | // are inside the encrypted data block. 263 | while next_payload != PayloadType::NoNextPayload { 264 | let should_early_break = next_payload == PayloadType::EncryptedAndAuthenticated 265 | || next_payload == PayloadType::EncryptedAndAuthenticatedFragment; 266 | let (tmp_payload_input, (payload, errors)) = parse(payload_input, next_payload)?; 267 | // We need _payload_input as an interim variable until de structuring assignments are 268 | // supported in our MSRV rust version 269 | payload_input = tmp_payload_input; 270 | next_payload = payload.next_payload; 271 | payloads.push(payload); 272 | payload_error_flags |= errors; 273 | 274 | if should_early_break { 275 | break; 276 | } 277 | } 278 | 279 | let error_flags = header_error_flags | payload_error_flags; 280 | 281 | let message = Message::Ike(IkeMessage { 282 | header, 283 | payloads, 284 | encrypted_data: Vec::with_capacity(0), 285 | error_flags, 286 | }); 287 | 288 | Ok((input, Some(message))) 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /sawp-ike/tests/general.rs: -------------------------------------------------------------------------------- 1 | use sawp::protocol::Protocol; 2 | use sawp_ike::*; 3 | 4 | #[test] 5 | fn test_name() { 6 | assert_eq!(Ike::name(), "ike"); 7 | } 8 | -------------------------------------------------------------------------------- /sawp-json/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-json" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for Json" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["json", "parser", "protocols"] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | verbose = ["sawp/verbose"] 22 | 23 | [dependencies] 24 | sawp = { path = "..", version = "^0.13.1" } 25 | serde = "1.0" 26 | serde_json = "1.0" 27 | 28 | [lib] 29 | crate-type = ["cdylib", "rlib"] 30 | 31 | [dev-dependencies] 32 | criterion = "=0.3.4" 33 | rstest = "0.6" 34 | 35 | [[bench]] 36 | name = "json" 37 | path = "benches/json.rs" 38 | harness = false 39 | 40 | # Override default replacements 41 | [package.metadata.release] 42 | pre-release-replacements = [] 43 | -------------------------------------------------------------------------------- /sawp-json/benches/json.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use sawp::parser::{Direction, Parse}; 3 | use sawp_json::{Json, Message}; 4 | use serde_json::json; 5 | 6 | const SAMPLE_JSON: &[u8] = br#"{ 7 | "object": { 8 | "nested": [1, 2, 3] 9 | }, 10 | "bool_true": true, 11 | "bool_false": false, 12 | "null": null, 13 | "string": "test", 14 | "number": 123, 15 | "list": ["1"] 16 | }"#; 17 | 18 | fn parse_json<'a>(json: &'a Json, input: &'a [u8]) -> (&'a [u8], Option) { 19 | json.parse(input, Direction::Unknown).unwrap() 20 | } 21 | 22 | fn criterion_benchmark(c: &mut Criterion) { 23 | let expected = json!({ 24 | "object": { 25 | "nested": [1, 2, 3] 26 | }, 27 | "bool_true": true, 28 | "bool_false": false, 29 | "null": null, 30 | "string": "test", 31 | "number": 123, 32 | "list": ["1"] 33 | }); 34 | 35 | // Assert output is what we expect before benchmarking 36 | assert_eq!( 37 | ([].as_ref(), Some(Message::new(expected))), 38 | parse_json(&Json {}, SAMPLE_JSON) 39 | ); 40 | 41 | c.bench_function("json", |b| { 42 | b.iter(|| parse_json(&Json {}, black_box(SAMPLE_JSON))) 43 | }); 44 | } 45 | 46 | criterion_group!(benches, criterion_benchmark); 47 | criterion_main!(benches); 48 | -------------------------------------------------------------------------------- /sawp-json/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! SAWP JSON Parser 2 | 3 | #![allow(clippy::unneeded_field_pattern)] 4 | 5 | use sawp::error::{Error, ErrorKind, Result}; 6 | use sawp::parser::{Direction, Parse}; 7 | use sawp::probe::Probe; 8 | use sawp::protocol::Protocol; 9 | use serde_json::{Deserializer, Value}; 10 | 11 | #[derive(Debug)] 12 | pub struct Json {} 13 | 14 | #[derive(Debug, PartialEq, Eq)] 15 | pub struct Message { 16 | pub value: Value, 17 | } 18 | 19 | impl Message { 20 | pub fn new(value: Value) -> Self { 21 | Self { value } 22 | } 23 | } 24 | 25 | impl Protocol<'_> for Json { 26 | type Message = Message; 27 | 28 | fn name() -> &'static str { 29 | "json" 30 | } 31 | } 32 | 33 | impl<'a> Parse<'a> for Json { 34 | fn parse( 35 | &self, 36 | input: &'a [u8], 37 | _direction: Direction, 38 | ) -> Result<(&'a [u8], Option)> { 39 | let mut stream = Deserializer::from_slice(input).into_iter::(); 40 | 41 | match stream.next() { 42 | Some(Ok(value)) => Ok((&input[stream.byte_offset()..], Some(Message::new(value)))), 43 | _ => Err(Error::new(ErrorKind::InvalidData)), 44 | } 45 | } 46 | } 47 | 48 | impl<'a> Probe<'a> for Json {} 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | use rstest::rstest; 54 | use sawp::error::{Error, ErrorKind, Result}; 55 | use sawp::probe::Status; 56 | use serde_json::json; 57 | 58 | #[rstest( 59 | input, 60 | expected, 61 | case::empty(b"", Err(Error::new(ErrorKind::InvalidData))), 62 | case::singlequote(b"''", Err(Error::new(ErrorKind::InvalidData))), 63 | case::incomplete(b"{\"a\":", Err(Error::new(ErrorKind::InvalidData))), 64 | 65 | // Smoke tests 66 | case::number(b"1234", Ok((0, Some(Message::new(json!(1234)))))), 67 | case::null(b"null", Ok((0, Some(Message::new(json!(null)))))), 68 | case::bool_true(b"true", Ok((0, Some(Message::new(json!(true)))))), 69 | case::bool_false(b"false", Ok((0, Some(Message::new(json!(false)))))), 70 | case::empty_obj(b"{}", Ok((0, Some(Message::new(json!({})))))), 71 | case::empty_list(b"[]", Ok((0, Some(Message::new(json!([])))))), 72 | case::empty_string(b"\"\"", Ok((0, Some(Message::new(json!("")))))), 73 | case::object(b"{\"a\":\"b\"}", Ok((0, Some(Message::new(json!({"a": "b"})))))), 74 | case::list(b"[\"a\", \"b\"]", Ok((0, Some(Message::new(json!(["a", "b"])))))), 75 | case::whitespace(b"\n\t{\n\r\n\"a\": \t\"b\"\n}", Ok((0, Some(Message::new(json!({"a": "b"})))))), 76 | case::multi(b"{}[1]", Ok((3, Some(Message::new(json!({})))))), 77 | )] 78 | fn test_parse(input: &[u8], expected: Result<(usize, Option<::Message>)>) { 79 | let json = Json {}; 80 | assert_eq!( 81 | expected, 82 | json.parse(input, Direction::Unknown) 83 | .map(|(left, msg)| (left.len(), msg)), 84 | ); 85 | } 86 | 87 | #[rstest( 88 | input, 89 | expected, 90 | case::empty(b"", Status::Unrecognized), 91 | case::incomplete(b"{\"a\":", Status::Unrecognized), 92 | case::number(b"1234", Status::Recognized) 93 | )] 94 | fn test_probe(input: &[u8], expected: Status) { 95 | let json = Json {}; 96 | assert_eq!(expected, json.probe(input, Direction::Unknown)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /sawp-modbus/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-modbus" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for Modbus" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["modbus", "parser", "protocol", "hardware", "automation"] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | ffi = ["cbindgen", "sawp/ffi", "sawp-ffi"] 22 | verbose = ["sawp/verbose"] 23 | 24 | [build-dependencies] 25 | cbindgen = {version = "0.15", optional = true} 26 | 27 | [dependencies] 28 | sawp-ffi = { path = "../sawp-ffi", version = "^0.13.1", optional = true} 29 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 30 | sawp = { path = "..", version = "^0.13.1" } 31 | nom = "7.1.1" 32 | num_enum = "0.5.1" 33 | 34 | [lib] 35 | crate-type = ["staticlib", "rlib", "cdylib"] 36 | 37 | [dev-dependencies] 38 | rstest = "0.6.4" 39 | 40 | # Override default replacements 41 | [package.metadata.release] 42 | pre-release-replacements = [ 43 | {file="../README.md", search="sawp-modbus = .*", replace="sawp-modbus = \"{{version}}\""}, 44 | ] 45 | -------------------------------------------------------------------------------- /sawp-modbus/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C++" 2 | pragma_once = true 3 | 4 | includes = ["sawp.h"] 5 | namespaces = ["sawp", "modbus"] 6 | 7 | autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Do NOT modify manually */" 8 | 9 | # If this option is true `usize` and `isize` will be converted into `size_t` and `ptrdiff_t` 10 | # instead of `uintptr_t` and `intptr_t` respectively. 11 | usize_is_size_t = true 12 | 13 | [export] 14 | exclude = ["Vec"] 15 | # Need includes for Flags, since they aren't referenced by public FFI functions 16 | include = ["AccessType", "CodeCategory", "ErrorFlags"] 17 | 18 | [parse.expand] 19 | crates = ["sawp", "sawp-modbus"] 20 | all_features = true 21 | -------------------------------------------------------------------------------- /sawp-modbus/src/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::*; 3 | use sawp::error::Error; 4 | use sawp::parser::Parse; 5 | use sawp_ffi::*; 6 | 7 | #[repr(C)] 8 | pub struct ParseResult { 9 | message: *mut Message, 10 | size_read: usize, 11 | error: *mut Error, 12 | } 13 | 14 | #[no_mangle] 15 | pub unsafe extern "C" fn sawp_modbus_create(probe_strict: bool) -> *mut Modbus { 16 | let parser = Modbus { probe_strict }; 17 | parser.into_ffi_ptr() 18 | } 19 | 20 | #[no_mangle] 21 | pub unsafe extern "C" fn sawp_modbus_destroy(d: *mut Modbus) { 22 | if !d.is_null() { 23 | drop(Box::from_raw(d)); 24 | } 25 | } 26 | 27 | /// # Safety 28 | /// function will panic if called with null 29 | #[no_mangle] 30 | pub unsafe extern "C" fn sawp_modbus_parse( 31 | parser: *const Modbus, 32 | direction: Direction, 33 | data: *const u8, 34 | length: usize, 35 | ) -> *mut ParseResult { 36 | let input = std::slice::from_raw_parts(data, length); 37 | match (*parser).parse(input, direction) { 38 | Ok((sl, message)) => ParseResult { 39 | message: message.into_ffi_ptr(), 40 | size_read: length - sl.len(), 41 | error: std::ptr::null_mut(), 42 | } 43 | .into_ffi_ptr(), 44 | Err(e) => ParseResult { 45 | message: std::ptr::null_mut(), 46 | size_read: 0, 47 | error: e.into_ffi_ptr(), 48 | } 49 | .into_ffi_ptr(), 50 | } 51 | } 52 | 53 | impl Drop for ParseResult { 54 | fn drop(&mut self) { 55 | unsafe { 56 | sawp_modbus_message_destroy(self.message); 57 | if !self.error.is_null() { 58 | drop(Box::from_raw(self.error)); 59 | } 60 | } 61 | } 62 | } 63 | 64 | /// Free ParseResult 65 | /// Will also destroy contained message and error 66 | #[no_mangle] 67 | pub unsafe extern "C" fn sawp_modbus_parse_result_destroy(d: *mut ParseResult) { 68 | if !d.is_null() { 69 | drop(Box::from_raw(d)); 70 | } 71 | } 72 | 73 | #[no_mangle] 74 | pub unsafe extern "C" fn sawp_modbus_message_destroy(d: *mut Message) { 75 | if !d.is_null() { 76 | drop(Box::from_raw(d)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sawp-pop3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-pop3" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for POP3" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["pop3", "parser", "protocol", "email"] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | verbose = ["sawp/verbose"] 22 | ffi = ["cbindgen", "sawp/ffi", "sawp-ffi"] 23 | 24 | [build-dependencies] 25 | cbindgen = {version = "0.15", optional = true} 26 | 27 | [dependencies] 28 | sawp-ffi = { path = "../sawp-ffi", version = "^0.13.1", optional = true} 29 | sawp = { path = "..", version = "^0.13.1" } 30 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 31 | nom = "7.1.1" 32 | 33 | [lib] 34 | crate-type = ["cdylib", "rlib", "staticlib"] 35 | 36 | [dev-dependencies] 37 | rstest = "0.6.4" 38 | 39 | # Override default replacements 40 | [package.metadata.release] 41 | pre-release-replacements = [] 42 | -------------------------------------------------------------------------------- /sawp-pop3/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C++" 2 | pragma_once = true 3 | 4 | includes = ["sawp.h"] 5 | namespaces = ["sawp", "pop3"] 6 | 7 | autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Do NOT modify manually */" 8 | 9 | # If this option is true `usize` and `isize` will be converted into `size_t` and `ptrdiff_t` 10 | # instead of `uintptr_t` and `intptr_t` respectively. 11 | usize_is_size_t = true 12 | 13 | [export] 14 | exclude = ["Vec"] 15 | # Need includes for Flags, since they aren't referenced by public FFI functions 16 | include = ["ErrorFlags"] 17 | 18 | [parse.expand] 19 | crates = ["sawp", "sawp-pop3"] 20 | all_features = true 21 | -------------------------------------------------------------------------------- /sawp-pop3/src/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::*; 3 | use sawp::error::Error; 4 | use sawp::parser::Parse; 5 | use sawp_ffi::*; 6 | 7 | #[repr(C)] 8 | pub struct ParseResult { 9 | message: *mut Message, 10 | size_read: usize, 11 | error: *mut Error, 12 | } 13 | 14 | #[no_mangle] 15 | pub unsafe extern "C" fn sawp_pop3_create() -> *mut POP3 { 16 | let parser = POP3 {}; 17 | parser.into_ffi_ptr() 18 | } 19 | 20 | #[no_mangle] 21 | pub unsafe extern "C" fn sawp_pop3_destroy(d: *mut POP3) { 22 | if !d.is_null() { 23 | drop(Box::from_raw(d)) 24 | } 25 | } 26 | 27 | /// # Safety 28 | /// function will panic if called with null 29 | #[no_mangle] 30 | pub unsafe extern "C" fn sawp_pop3_parse( 31 | parser: *const POP3, 32 | direction: Direction, 33 | data: *const u8, 34 | length: usize, 35 | ) -> *mut ParseResult { 36 | let input = std::slice::from_raw_parts(data, length); 37 | match (*parser).parse(input, direction) { 38 | Ok((sl, message)) => ParseResult { 39 | message: message.into_ffi_ptr(), 40 | size_read: length - sl.len(), 41 | error: std::ptr::null_mut(), 42 | } 43 | .into_ffi_ptr(), 44 | Err(e) => ParseResult { 45 | message: std::ptr::null_mut(), 46 | size_read: 0, 47 | error: e.into_ffi_ptr(), 48 | } 49 | .into_ffi_ptr(), 50 | } 51 | } 52 | 53 | impl Drop for ParseResult { 54 | fn drop(&mut self) { 55 | unsafe { 56 | sawp_pop3_message_destroy(self.message); 57 | if !self.error.is_null() { 58 | drop(Box::from_raw(self.error)) 59 | } 60 | } 61 | } 62 | } 63 | 64 | /// Free ParseResult 65 | /// Will also destroy contained message and error 66 | #[no_mangle] 67 | pub unsafe extern "C" fn sawp_pop3_parse_result_destroy(d: *mut ParseResult) { 68 | if !d.is_null() { 69 | drop(Box::from_raw(d)) 70 | } 71 | } 72 | 73 | #[no_mangle] 74 | pub unsafe extern "C" fn sawp_pop3_message_destroy(d: *mut Message) { 75 | if !d.is_null() { 76 | drop(Box::from_raw(d)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sawp-resp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-resp" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for RESP" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CyberCentreCanada/sawp" 10 | homepage = "https://github.com/CyberCentreCanada/sawp" 11 | keywords = ["resp", "redis", "parser", "protocol",] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | ffi = ["cbindgen", "sawp/ffi", "sawp-ffi"] 22 | verbose = ["sawp/verbose"] 23 | 24 | [build-dependencies] 25 | cbindgen = {version = "0.15", optional = true} 26 | 27 | [dependencies] 28 | sawp-ffi = {path = "../sawp-ffi", version = "^0.13.1", optional = true} 29 | sawp-flags = { path = "../sawp-flags", version = "^0.13.1" } 30 | sawp = {path = "..", version = "^0.13.1" } 31 | nom = "7.1.1" 32 | num_enum = "0.5.1" 33 | byteorder = "1.4.3" 34 | 35 | [lib] 36 | crate-type = ["staticlib", "rlib", "cdylib"] 37 | 38 | [dev-dependencies] 39 | rstest = "0.6.4" 40 | 41 | # Override default replacements 42 | [package.metadata.release] 43 | pre-release-replacements = [] 44 | -------------------------------------------------------------------------------- /sawp-resp/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C++" 2 | pragma_once = true 3 | 4 | includes = ["sawp.h"] 5 | namespaces = ["sawp", "resp"] 6 | 7 | autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Do NOT modify manually */" 8 | 9 | # If this option is true `usize` and `isize` will be converted into `size_t` and `ptrdiff_t` 10 | # instead of `uintptr_t` and `intptr_t` respectively. 11 | usize_is_size_t = true 12 | 13 | [export] 14 | exclude = ["Vec"] 15 | # Need includes for Flags, since they aren't referenced by public FFI functions 16 | include = ["ErrorFlags"] 17 | 18 | [parse.expand] 19 | crates = ["sawp", "sawp-resp"] 20 | all_features = true 21 | -------------------------------------------------------------------------------- /sawp-resp/src/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::*; 3 | use sawp::error::Error; 4 | use sawp::parser::Parse; 5 | use sawp_ffi::*; 6 | 7 | #[repr(C)] 8 | pub struct ParseResult { 9 | message: *mut Message, 10 | size_read: usize, 11 | error: *mut Error, 12 | } 13 | 14 | #[no_mangle] 15 | pub unsafe extern "C" fn sawp_resp_create() -> *mut Resp { 16 | let parser = Resp {}; 17 | parser.into_ffi_ptr() 18 | } 19 | 20 | #[no_mangle] 21 | pub unsafe extern "C" fn sawp_resp_destroy(r: *mut Resp) { 22 | if !r.is_null() { 23 | drop(Box::from_raw(r)); 24 | } 25 | } 26 | 27 | /// # Safety 28 | /// function will panic if called with null 29 | #[no_mangle] 30 | pub unsafe extern "C" fn sawp_resp_parse( 31 | parser: *const Resp, 32 | direction: Direction, 33 | data: *const u8, 34 | length: usize, 35 | ) -> *mut ParseResult { 36 | let input = std::slice::from_raw_parts(data, length); 37 | match (*parser).parse(input, direction) { 38 | Ok((sl, message)) => ParseResult { 39 | message: message.into_ffi_ptr(), 40 | size_read: length - sl.len(), 41 | error: std::ptr::null_mut(), 42 | } 43 | .into_ffi_ptr(), 44 | Err(e) => ParseResult { 45 | message: std::ptr::null_mut(), 46 | size_read: 0, 47 | error: e.into_ffi_ptr(), 48 | } 49 | .into_ffi_ptr(), 50 | } 51 | } 52 | 53 | impl Drop for ParseResult { 54 | fn drop(&mut self) { 55 | unsafe { 56 | sawp_resp_message_destroy(self.message); 57 | if !self.error.is_null() { 58 | drop(Box::from_raw(self.error)); 59 | } 60 | } 61 | } 62 | } 63 | 64 | /// Free ParseResult 65 | /// Will also destroy contained message and error 66 | #[no_mangle] 67 | pub unsafe extern "C" fn sawp_resp_parse_result_destroy(d: *mut ParseResult) { 68 | if !d.is_null() { 69 | drop(Box::from_raw(d)); 70 | } 71 | } 72 | 73 | #[no_mangle] 74 | pub unsafe extern "C" fn sawp_resp_message_destroy(d: *mut Message) { 75 | if !d.is_null() { 76 | drop(Box::from_raw(d)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sawp-tftp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sawp-tftp" 3 | version = "0.13.1" 4 | authors = ["Canadian Centre for Cyber Security "] 5 | description = "SAWP Protocol Parser for TFTP" 6 | readme = "../README.md" 7 | edition = "2021" 8 | license = "MIT" 9 | repository = "https://github.com/CybercentreCanada/sawp" 10 | homepage = "https://github.com/CybercentreCanada/sawp" 11 | keywords = ["tftp", "parser", "protocol", "hardware", "automation"] 12 | categories = ["parsing", "network-programming"] 13 | include = [ 14 | "Cargo.toml", 15 | "../LICENSE", 16 | "../README.md", 17 | "src/**/*.rs", 18 | ] 19 | 20 | [features] 21 | verbose = ["sawp/verbose"] 22 | ffi = ["cbindgen", "sawp/ffi", "sawp-ffi"] 23 | 24 | [build-dependencies] 25 | cbindgen = {version = "0.15", optional = true} 26 | 27 | [dependencies] 28 | sawp-ffi = { path = "../sawp-ffi", version = "^0.13.1", optional = true} 29 | sawp = { path = "..", version = "^0.13.1" } 30 | nom = "7.1.1" 31 | num_enum = "0.5.1" 32 | 33 | [lib] 34 | crate-type = ["cdylib", "rlib", "staticlib"] 35 | 36 | [dev-dependencies] 37 | rstest = "0.6.4" 38 | 39 | # Override default replacements 40 | [package.metadata.release] 41 | pre-release-replacements = [] 42 | -------------------------------------------------------------------------------- /sawp-tftp/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C++" 2 | pragma_once = true 3 | 4 | includes = ["sawp.h"] 5 | namespaces = ["sawp", "tftp"] 6 | 7 | autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Do NOT modify manually */" 8 | 9 | # If this option is true `usize` and `isize` will be converted into `size_t` and `ptrdiff_t` 10 | # instead of `uintptr_t` and `intptr_t` respectively. 11 | usize_is_size_t = true 12 | 13 | [export] 14 | exclude = ["Vec", "String"] 15 | # Need includes for Flags, since they aren't referenced by public FFI functions 16 | include = ["AccessType", "CodeCategory", "ErrorFlags"] 17 | 18 | [parse.expand] 19 | crates = ["sawp", "sawp-tftp"] 20 | all_features = true 21 | -------------------------------------------------------------------------------- /sawp-tftp/src/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::*; 3 | use sawp::error::Error; 4 | use sawp::parser::Parse; 5 | use sawp_ffi::*; 6 | 7 | #[repr(C)] 8 | pub struct ParseResult { 9 | message: *mut Message, 10 | size_read: usize, 11 | error: *mut Error, 12 | } 13 | 14 | #[no_mangle] 15 | pub unsafe extern "C" fn sawp_tftp_create() -> *mut TFTP { 16 | let parser = TFTP {}; 17 | parser.into_ffi_ptr() 18 | } 19 | 20 | #[no_mangle] 21 | pub unsafe extern "C" fn sawp_tftp_destroy(d: *mut TFTP) { 22 | if !d.is_null() { 23 | drop(Box::from_raw(d)); 24 | } 25 | } 26 | 27 | /// # Safety 28 | /// function will panic if called with null 29 | #[no_mangle] 30 | pub unsafe extern "C" fn sawp_tftp_parse( 31 | parser: *const TFTP, 32 | direction: Direction, 33 | data: *const u8, 34 | length: usize, 35 | ) -> *mut ParseResult { 36 | let input = std::slice::from_raw_parts(data, length); 37 | match (*parser).parse(input, direction) { 38 | Ok((sl, message)) => ParseResult { 39 | message: message.into_ffi_ptr(), 40 | size_read: length - sl.len(), 41 | error: std::ptr::null_mut(), 42 | } 43 | .into_ffi_ptr(), 44 | Err(e) => ParseResult { 45 | message: std::ptr::null_mut(), 46 | size_read: 0, 47 | error: e.into_ffi_ptr(), 48 | } 49 | .into_ffi_ptr(), 50 | } 51 | } 52 | 53 | impl Drop for ParseResult { 54 | fn drop(&mut self) { 55 | unsafe { 56 | sawp_tftp_message_destroy(self.message); 57 | if !self.error.is_null() { 58 | drop(Box::from_raw(self.error)); 59 | } 60 | } 61 | } 62 | } 63 | 64 | /// Free ParseResult 65 | /// Will also destroy contained message and error 66 | #[no_mangle] 67 | pub unsafe extern "C" fn sawp_tftp_parse_result_destroy(d: *mut ParseResult) { 68 | if !d.is_null() { 69 | drop(Box::from_raw(d)); 70 | } 71 | } 72 | 73 | #[no_mangle] 74 | pub unsafe extern "C" fn sawp_tftp_message_destroy(d: *mut Message) { 75 | if !d.is_null() { 76 | drop(Box::from_raw(d)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "ffi")] 2 | use sawp_ffi::GenerateFFI; 3 | 4 | use std::num::NonZeroUsize; 5 | 6 | // Re-export types used for ErrorKind 7 | use nom::error::ErrorKind as NomErrorKind; 8 | use nom::Needed as NomNeeded; 9 | 10 | /// Helper that uses this module's error type 11 | pub type Result = std::result::Result; 12 | 13 | /// Helper for nom's default error type 14 | pub type NomError = nom::error::Error; 15 | 16 | /// Common protocol or parsing error 17 | /// 18 | /// This error type is meant to return the errors that 19 | /// are common across parsers and other sub packages. 20 | /// Sub packages may choose to implement their own error 21 | /// types if they wish to avoid adding extra dependencies 22 | /// to the base crate. 23 | #[derive(Debug, PartialEq, Eq)] 24 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 25 | #[cfg_attr(feature = "ffi", sawp_ffi(prefix = "sawp"))] 26 | pub struct Error { 27 | pub kind: ErrorKind, 28 | } 29 | 30 | impl Error { 31 | pub fn new(kind: ErrorKind) -> Self { 32 | Self { kind } 33 | } 34 | 35 | /// Helper for creating an error with a `ErrorKind::Incomplete` and a needed size. 36 | pub fn incomplete_needed(size: usize) -> Self { 37 | Error::new(ErrorKind::Incomplete( 38 | NonZeroUsize::new(size) 39 | .map(Needed::Size) 40 | .unwrap_or(Needed::Unknown), 41 | )) 42 | } 43 | 44 | /// Helper for creating an error with a `ErrorKind::Incomplete` and an unknown size. 45 | pub fn incomplete() -> Self { 46 | Error::new(ErrorKind::Incomplete(Needed::Unknown)) 47 | } 48 | 49 | /// Helper for creating a parse error. 50 | #[cfg(feature = "verbose")] 51 | pub fn parse(msg: Option) -> Self { 52 | Error::new(ErrorKind::ParseError(msg)) 53 | } 54 | 55 | /// Helper for creating a parse error. 56 | #[cfg(not(feature = "verbose"))] 57 | pub fn parse(_msg: Option) -> Self { 58 | Error::new(ErrorKind::ParseError(None)) 59 | } 60 | } 61 | 62 | impl From for Error { 63 | fn from(kind: ErrorKind) -> Self { 64 | Self::new(kind) 65 | } 66 | } 67 | 68 | /// Number of bytes needed for the next parsing attempt. 69 | /// 70 | /// Used in `ErrorKind::Incomplete` to tell the caller how many bytes to wait 71 | /// for before calling the parser with more data. 72 | #[derive(Debug, PartialEq, Eq)] 73 | pub enum Needed { 74 | Unknown, 75 | Size(NonZeroUsize), 76 | } 77 | 78 | /// Kinds of common errors used by the parsers 79 | #[derive(Debug, PartialEq, Eq)] 80 | #[non_exhaustive] 81 | #[cfg_attr(feature = "ffi", derive(GenerateFFI))] 82 | #[cfg_attr(feature = "ffi", sawp_ffi(type_only, prefix = "sawp"))] 83 | pub enum ErrorKind { 84 | /// Feature is not yet implemented. 85 | Unimplemented, 86 | /// Parser could not advance based on the data provided. 87 | /// 88 | /// Usually indicates the provided input bytes cannot be parsed 89 | /// for the protocol. 90 | // 91 | // Developer note: 92 | // 93 | // This error should only be used as a last resort. Consider 94 | // returning Ok and adding validation error flags to the 95 | // parser's `Message` instead. 96 | InvalidData, 97 | /// Generic parsing error with optional message. 98 | ParseError(Option), 99 | /// Parser did not advance because more data is required to 100 | /// make a decision. 101 | /// 102 | /// The caller should gather more data and try again. 103 | Incomplete(Needed), 104 | } 105 | 106 | impl From for ErrorKind { 107 | #[cfg(feature = "verbose")] 108 | fn from(kind: NomErrorKind) -> Self { 109 | Self::ParseError(Some(format!("{:?}", kind))) 110 | } 111 | 112 | #[cfg(not(feature = "verbose"))] 113 | fn from(_kind: NomErrorKind) -> Self { 114 | Self::ParseError(None) 115 | } 116 | } 117 | 118 | impl From>> for Error { 119 | fn from(nom_err: nom::Err>) -> Self { 120 | match nom_err { 121 | nom::Err::Error(err) | nom::Err::Failure(err) => Error::new(err.code.into()), 122 | nom::Err::Incomplete(needed) => match needed { 123 | NomNeeded::Unknown => Error::incomplete(), 124 | NomNeeded::Size(size) => Error::incomplete_needed(size.into()), 125 | }, 126 | } 127 | } 128 | } 129 | 130 | impl std::fmt::Display for Error { 131 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { 132 | match &self.kind { 133 | ErrorKind::Unimplemented => write!(f, "Unimplemented feature"), 134 | ErrorKind::InvalidData => write!(f, "Encountered invalid data"), 135 | ErrorKind::ParseError(err) if err.is_some() => { 136 | write!(f, "Parsing error: {}", err.clone().unwrap()) 137 | } 138 | ErrorKind::ParseError(_) => write!(f, "Parsing error"), 139 | ErrorKind::Incomplete(Needed::Unknown) => write!(f, "More bytes required to parse"), 140 | ErrorKind::Incomplete(Needed::Size(n)) => { 141 | write!(f, "{} more bytes required to parse", n) 142 | } 143 | } 144 | } 145 | } 146 | 147 | impl std::error::Error for Error {} 148 | 149 | impl From> for Error { 150 | fn from(nom_err: NomError) -> Self { 151 | Error::new(nom_err.code.into()) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/ffi.rs: -------------------------------------------------------------------------------- 1 | use sawp_ffi::deref; 2 | 3 | /// Note this function only works for Vec 4 | /// for other types, use the field_ptr accessor 5 | /// # Safety 6 | /// function will panic if called with null 7 | #[no_mangle] 8 | pub unsafe extern "C" fn sawp_vector_get_data(vec: *const Vec) -> *const u8 { 9 | deref!(vec).as_ptr() 10 | } 11 | 12 | /// # Safety 13 | /// function will panic if called with null 14 | #[no_mangle] 15 | pub unsafe extern "C" fn sawp_vector_get_size(vec: *const Vec) -> usize { 16 | deref!(vec).len() 17 | } 18 | 19 | /// Note: Returned string is not null terminated 20 | /// # Safety 21 | /// function will panic if called with null 22 | #[no_mangle] 23 | pub unsafe extern "C" fn sawp_string_get_ptr(s: *const String) -> *const u8 { 24 | deref!(s).as_ptr() 25 | } 26 | 27 | /// # Safety 28 | /// function will panic if called with null 29 | #[no_mangle] 30 | pub unsafe extern "C" fn sawp_string_get_size(s: *const String) -> usize { 31 | deref!(s).len() 32 | } 33 | 34 | /// # Safety 35 | /// function will panic if called with null 36 | #[no_mangle] 37 | pub unsafe extern "C" fn sawp_ipv4addr_get_data(ip: *const std::net::Ipv4Addr) -> u32 { 38 | u32::from_be_bytes(deref!(ip).octets()) 39 | } 40 | 41 | /// # Safety 42 | /// function will panic if called with null 43 | #[no_mangle] 44 | pub unsafe extern "C" fn sawp_ipv6addr_get_data(ip: *const std::net::Ipv6Addr) -> *const u8 { 45 | deref!(ip).octets().as_slice().as_ptr() 46 | } 47 | 48 | /// # Safety 49 | /// function will panic if called with null 50 | #[no_mangle] 51 | pub unsafe extern "C" fn sawp_ipaddr_is_v4(ip: *const std::net::IpAddr) -> bool { 52 | deref!(ip).is_ipv4() 53 | } 54 | 55 | /// # Safety 56 | /// function will panic if called with null 57 | #[no_mangle] 58 | pub unsafe extern "C" fn sawp_ipaddr_is_v6(ip: *const std::net::IpAddr) -> bool { 59 | deref!(ip).is_ipv6() 60 | } 61 | 62 | /// # Safety 63 | /// function will panic if called with null 64 | #[no_mangle] 65 | pub unsafe extern "C" fn sawp_ipaddr_as_v4( 66 | ip: *const std::net::IpAddr, 67 | ) -> *const std::net::Ipv4Addr { 68 | if let std::net::IpAddr::V4(addr) = deref!(ip) { 69 | addr 70 | } else { 71 | std::ptr::null() 72 | } 73 | } 74 | 75 | /// # Safety 76 | /// function will panic if called with null 77 | #[no_mangle] 78 | pub unsafe extern "C" fn sawp_ipaddr_as_v6( 79 | ip: *const std::net::IpAddr, 80 | ) -> *const std::net::Ipv6Addr { 81 | if let std::net::IpAddr::V6(addr) = deref!(ip) { 82 | addr 83 | } else { 84 | std::ptr::null() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # SAWP: Security Aware Wire Protocol parsing library 3 | 4 | This library contains parsers for various wire protocols 5 | and is intended to be used in network security sensors. 6 | 7 | The base library contains all of the common types and traits 8 | used by the parsers. 9 | 10 | Usage documentation can be found in the [README](https://github.com/CybercentreCanada/sawp/blob/main/README.md). 11 | 12 | ## Protocols 13 | 14 | Each protocol, along with certain features, are implemented 15 | in a separate package inside this workspace. This reduces the 16 | number dependencies needed for using various protocols or 17 | features. A practical use of this library can be found by 18 | referring to the protocol parser you wish to use: 19 | - [Diameter](/sawp-diameter) 20 | - [Json](/sawp-json) 21 | - [Modbus](/sawp-modbus) 22 | 23 | ## Utility 24 | 25 | The following utility packages also exist: 26 | - [File](/sawp-file) Serializes API calls for debugging 27 | */ 28 | 29 | #![allow(clippy::unneeded_field_pattern)] 30 | 31 | /// Return common errors 32 | pub mod error; 33 | 34 | /// Parse Messages 35 | pub mod parser; 36 | 37 | /// Probe Bytes 38 | pub mod probe; 39 | 40 | /// Describe a Protocol 41 | pub mod protocol; 42 | 43 | #[cfg(feature = "ffi")] 44 | pub mod ffi; 45 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::protocol::Protocol; 3 | 4 | /// Destination of the input byte stream. 5 | #[repr(C)] 6 | #[derive(Clone, Debug, PartialEq, Eq)] 7 | pub enum Direction { 8 | /// Message is destined to the client 9 | ToClient, 10 | /// Message is destined to the server 11 | ToServer, 12 | /// Direction is not known 13 | Unknown, 14 | } 15 | 16 | /// Trait for parsing message from an input byte stream. 17 | pub trait Parse<'a>: Protocol<'a> { 18 | /// Returns a tuple containing the remaining unparsed data and the parsed `Message`. 19 | /// 20 | /// A return value of `Result::Ok` indicates that the parser has *made progress* 21 | /// and should only be used when the remaining unparsed data is less than the input. 22 | /// 23 | /// A return value of `Result::Err` indicates that *no progress* was made 24 | /// and the user may call the parse function again with the same input in 25 | /// some scenarios: 26 | /// - `ErrorKind::Incomplete`: call `parse` once more input data is available. 27 | /// 28 | /// Consequently, `Result::Ok(None)` is used to indicate the parser made 29 | /// progress but needs more data to return a complete `Message`. Internal 30 | /// buffering may occur depending on the implementation. 31 | /// 32 | /// `Result::Err(ErrorKind::Incomplete(_))` must be used instead of `Result::Ok(None)` 33 | /// when no progress was made parsing the input. 34 | fn parse( 35 | &self, 36 | input: &'a [u8], 37 | direction: Direction, 38 | ) -> Result<(&'a [u8], Option)>; 39 | } 40 | -------------------------------------------------------------------------------- /src/probe.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, ErrorKind}; 2 | use crate::parser::{Direction, Parse}; 3 | use crate::protocol::Protocol; 4 | 5 | /// Result of probing the underlying bytes. 6 | #[derive(Debug, PartialEq, Eq)] 7 | pub enum Status { 8 | /// Data matches this protocol 9 | Recognized, 10 | /// Data does not match this protocol 11 | Unrecognized, 12 | /// More data is needed to make a decision 13 | Incomplete, 14 | } 15 | 16 | pub trait Probe<'a>: Protocol<'a> + Parse<'a> { 17 | /// Probes the input to recognize if the underlying bytes likely match this 18 | /// protocol. 19 | /// 20 | /// Returns a probe status. Probe again once more data is available when the 21 | /// status is `Status::Incomplete`. 22 | fn probe(&self, input: &'a [u8], direction: Direction) -> Status { 23 | match self.parse(input, direction) { 24 | Ok((_, _)) => Status::Recognized, 25 | Err(Error { 26 | kind: ErrorKind::Incomplete(_), 27 | }) => Status::Incomplete, 28 | Err(_) => Status::Unrecognized, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | /// Represents the basic elements of a protocol 2 | pub trait Protocol<'a> { 3 | /// Type of message returned when parsing 4 | type Message: 'a; 5 | 6 | /// Protocol name string 7 | fn name() -> &'static str; 8 | } 9 | --------------------------------------------------------------------------------