├── .formatter.exs ├── .github ├── release-drafter.yml └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .tool-versions ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── lib ├── wireguardex.ex └── wireguardex │ ├── device.ex │ ├── device_config.ex │ ├── device_config_builder.ex │ ├── peer_config.ex │ ├── peer_config_builder.ex │ ├── peer_info.ex │ └── peer_stats.ex ├── mix.exs ├── mix.lock ├── native └── wireguard_nif │ ├── .cargo │ └── config │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Cross.toml │ ├── README.md │ └── src │ ├── device.rs │ ├── key.rs │ ├── lib.rs │ └── peer.rs ├── requirements.txt └── test ├── test_helper.exs └── wireguardex_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | categories: 4 | - title: "✨ Features" 5 | labels: 6 | - "feature" 7 | - "enhancement" 8 | - title: "🐛 Bug Fixes" 9 | labels: 10 | - "fix" 11 | - "bugfix" 12 | - "bug" 13 | - title: "🧰 Maintenance" 14 | label: 15 | - "chore" 16 | - "dependencies" 17 | - title: "📝 Documentation" 18 | label: "docs" 19 | exclude-labels: 20 | - "skip-changelog" 21 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 22 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 23 | version-resolver: 24 | major: 25 | labels: 26 | - "major" 27 | minor: 28 | labels: 29 | - "minor" 30 | patch: 31 | labels: 32 | - "patch" 33 | default: patch 34 | template: | 35 | $CHANGES 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | tags: 12 | - "*" 13 | 14 | jobs: 15 | static-analysis: 16 | runs-on: ubuntu-20.04 17 | env: 18 | MIX_ENV: dev 19 | WIREGUARDNIF_BUILD: true 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.9' 25 | - uses: erlef/setup-beam@v1 26 | with: 27 | otp-version: 26 28 | elixir-version: 1.15 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: stable 32 | components: clippy, rustfmt 33 | - uses: actions/cache@v2 34 | name: Setup Elixir cache 35 | with: 36 | path: | 37 | deps 38 | _build 39 | key: ${{ runner.os }}-mix-otp-25-${{ hashFiles('**/mix.lock') }} 40 | - uses: actions/cache@v2 41 | name: Setup Python cache 42 | with: 43 | path: ~/.cache/pip 44 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 45 | - uses: actions/cache@v2 46 | name: Setup Rust cache 47 | with: 48 | path: | 49 | ~/.cargo/registry 50 | ~/.cargo/git 51 | native/wireguard_nif/target 52 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/native/wireguard_nif/Cargo.lock') }} 53 | - name: Install Elixir dependencies 54 | run: mix deps.get --only dev 55 | - name: Install Python dependencies 56 | run: pip install -r requirements.txt 57 | - name: Run pre-commit 58 | run: | 59 | pre-commit install 60 | SKIP=no-commit-to-branch pre-commit run --all-files 61 | 62 | tests: 63 | env: 64 | MIX_ENV: test 65 | WIREGUARDNIF_BUILD: true 66 | runs-on: ubuntu-20.04 67 | name: "[${{matrix.otp}}/${{matrix.elixir}}] Tests on wireguard_nif [OTP/Elixir]" 68 | strategy: 69 | fail-fast: false 70 | matrix: 71 | otp: [22, 23, 24, 25, 26] 72 | elixir: [1.13, 1.14, 1.15] 73 | exclude: 74 | - otp: 26 75 | elixir: 1.13 76 | - otp: 22 77 | elixir: 1.14 78 | - otp: 22 79 | elixir: 1.15 80 | - otp: 23 81 | elixir: 1.15 82 | steps: 83 | - uses: actions/checkout@v2 84 | with: 85 | fetch-depth: 0 86 | - uses: actions-rs/toolchain@v1 87 | with: 88 | toolchain: stable 89 | - uses: erlef/setup-beam@v1 90 | with: 91 | otp-version: ${{matrix.otp}} 92 | elixir-version: ${{matrix.elixir}} 93 | 94 | - name: Cache hex deps 95 | id: mix-cache 96 | uses: actions/cache@v2 97 | with: 98 | path: | 99 | deps 100 | _build 101 | !_build/*/lib/wireguardex 102 | key: ${{ runner.os }}[${{ matrix.otp }}/${{ matrix.elixir }}]-mix-v1-${{ hashFiles('**/mix.lock') }} 103 | - run: sudo setcap 'cap_net_admin+eip' $(ls -1 $INSTALL_DIR_FOR_OTP/erts-*/bin/beam.smp) 104 | - run: mix local.hex --force 105 | - run: mix local.rebar --force 106 | - run: mix deps.get 107 | - run: mix deps.compile 108 | - run: mix compile --warning-as-errors 109 | env: 110 | RUST_BACKTRACE: 1 111 | - run: mix test 112 | 113 | draft-release: 114 | runs-on: ubuntu-20.04 115 | needs: 116 | - static-analysis 117 | - tests 118 | outputs: 119 | tag_name: ${{ steps.release_drafter.outputs.tag_name }} 120 | steps: 121 | - uses: release-drafter/release-drafter@v5 122 | id: release_drafter 123 | env: 124 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 125 | 126 | build-release-nifs: 127 | name: NIF ${{ matrix.nif }} - ${{ matrix.target }} (${{ matrix.os }}) 128 | runs-on: ${{ matrix.os }} 129 | needs: 130 | - draft-release 131 | defaults: 132 | run: 133 | working-directory: "./native/wireguard_nif" 134 | strategy: 135 | fail-fast: false 136 | matrix: 137 | target: 138 | - aarch64-unknown-linux-gnu 139 | - aarch64-unknown-linux-musl 140 | - x86_64-unknown-linux-gnu 141 | - x86_64-unknown-linux-musl 142 | - aarch64-apple-darwin 143 | - x86_64-apple-darwin 144 | os: 145 | - ubuntu-20.04 146 | - macos-11 147 | nif: 148 | - "2.15" 149 | - "2.16" 150 | - "2.17" 151 | 152 | # Exclude combinations of build OSes and targets that don't make sense 153 | exclude: 154 | - os: macos-11 155 | target: aarch64-unknown-linux-gnu 156 | - os: macos-11 157 | target: aarch64-unknown-linux-musl 158 | - os: macos-11 159 | target: x86_64-unknown-linux-gnu 160 | - os: macos-11 161 | target: x86_64-unknown-linux-musl 162 | - os: ubuntu-20.04 163 | target: aarch64-apple-darwin 164 | - os: ubuntu-20.04 165 | target: x86_64-apple-darwin 166 | 167 | env: 168 | NIF_DIRECTORY: "native/wireguard_nif" 169 | RUSTLER_NIF_VERSION: ${{ matrix.nif }} 170 | steps: 171 | - name: Checkout code 172 | uses: actions/checkout@v2 173 | 174 | - name: Install Rust 175 | uses: actions-rs/toolchain@v1 176 | with: 177 | toolchain: stable 178 | target: ${{ matrix.target }} 179 | override: true 180 | profile: minimal 181 | 182 | - name: Install cross 183 | if: ${{ contains(matrix.os, 'ubuntu') }} 184 | # Use main cross-rs to have Zig support 185 | run: cargo install cross --git https://github.com/cross-rs/cross --locked 186 | 187 | - name: Extract project info 188 | shell: bash 189 | run: | 190 | echo "PROJECT_NAME=$(sed -n 's/^name = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV 191 | echo "PROJECT_VERSION=$(sed -n 's/^ @version "\(.*\)"/\1/p' ../../mix.exs | head -n1)" >> $GITHUB_ENV 192 | 193 | - name: Build with Cross 194 | if: ${{ contains(matrix.os, 'ubuntu') }} 195 | run: cross build --release --target=${{ matrix.target }} 196 | 197 | - name: Build with Cargo 198 | shell: bash 199 | if: ${{ ! contains(matrix.os, 'ubuntu') }} 200 | run: cargo build --release --target=${{ matrix.target }} 201 | 202 | - name: Rename lib 203 | id: rename 204 | shell: bash 205 | run: | 206 | LIB_PREFIX="lib" 207 | LIB_SUFFIX=".so" 208 | 209 | case ${{ matrix.target }} in 210 | *-apple-darwin) LIB_SUFFIX=".dylib" ;; 211 | esac; 212 | 213 | CICD_INTERMEDIATES_DIR=$(mktemp -d) 214 | 215 | LIB_DIR="${CICD_INTERMEDIATES_DIR}/released" 216 | mkdir -p "${LIB_DIR}" 217 | LIB_NAME="${LIB_PREFIX}${{ env.PROJECT_NAME }}${LIB_SUFFIX}" 218 | LIB_PATH="${LIB_DIR}/${LIB_NAME}" 219 | 220 | cp "target/${{ matrix.target }}/release/${LIB_NAME}" "${LIB_DIR}" 221 | 222 | # Use ".so" for macOS 223 | # See: https://www.erlang.org/doc/man/erlang.html#load_nif-2 224 | LIB_FINAL_SUFFIX="${LIB_SUFFIX}" 225 | case ${{ matrix.target }} in 226 | *-apple-darwin) LIB_FINAL_SUFFIX=".so" ;; 227 | esac; 228 | 229 | LIB_FINAL_NAME="${LIB_PREFIX}${PROJECT_NAME}-v${PROJECT_VERSION}-nif-${RUSTLER_NIF_VERSION}-${{ matrix.target }}${LIB_FINAL_SUFFIX}" 230 | 231 | # Copy lib to final name 232 | cp "${LIB_PATH}" "${LIB_FINAL_NAME}" 233 | tar -cvzf "${LIB_FINAL_NAME}.tar.gz" "${LIB_FINAL_NAME}" 234 | 235 | LIB_FINAL_PATH="${NIF_DIRECTORY}/${LIB_FINAL_NAME}.tar.gz" 236 | 237 | echo ::set-output name=LIB_FINAL_PATH::${LIB_FINAL_PATH} 238 | echo ::set-output name=LIB_FINAL_NAME::${LIB_FINAL_NAME}.tar.gz 239 | 240 | - name: Upload release 241 | uses: actions/upload-artifact@v2 242 | with: 243 | name: ${{ steps.rename.outputs.LIB_FINAL_NAME }} 244 | path: ${{ steps.rename.outputs.LIB_FINAL_PATH }} 245 | 246 | - name: Release 247 | uses: softprops/action-gh-release@v1 248 | with: 249 | tag_name: ${{ needs.draft-release.outputs.tag_name }} 250 | draft: true 251 | files: ${{ steps.rename.outputs.LIB_FINAL_PATH }} 252 | if: startsWith(github.ref, 'refs/heads/main') || startsWith(github.ref, 'refs/tags/') 253 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | wireguard_nif-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # Compiled artifacts from rustler 29 | /priv/ 30 | 31 | # Checksum files for precompiled NIFs 32 | checksum-*.exs 33 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | # Elixir config 5 | - id: mix-format 6 | name: 'elixir: mix format' 7 | entry: mix format --check-formatted 8 | language: system 9 | files: \.exs*$ 10 | - id: mix-lint 11 | name: 'elixir: mix credo' 12 | entry: mix credo --strict 13 | language: system 14 | pass_filenames: false 15 | files: \.exs*$ 16 | - id: mix-compile 17 | name: 'elixir: mix compile' 18 | entry: mix compile --force --warnings-as-errors 19 | language: system 20 | pass_filenames: false 21 | files: \.ex$ 22 | # Rust config 23 | - id: rust-format 24 | name: 'rust: cargo fmt' 25 | entry: bash -c "cd native/wireguard_nif && cargo fmt --all --" 26 | language: system 27 | pass_filenames: false 28 | files: \.rs$ 29 | - id: rust-lint 30 | name: 'rust: cargo clippy' 31 | entry: bash -c "cd native/wireguard_nif && cargo clippy --all-targets --all-features -- -D warnings" 32 | language: system 33 | pass_filenames: false 34 | files: \.rs$ 35 | 36 | # Standard pre-commit hooks 37 | - repo: https://github.com/pre-commit/pre-commit-hooks 38 | rev: v2.3.0 39 | hooks: 40 | - id: mixed-line-ending 41 | args: ['--fix=lf'] 42 | description: Forces to replace line ending by the UNIX 'lf' character. 43 | - id: check-yaml 44 | - id: check-merge-conflict 45 | - id: end-of-file-fixer 46 | - id: trailing-whitespace 47 | - id: check-merge-conflict 48 | - id: no-commit-to-branch 49 | args: [-b, main] 50 | 51 | - repo: https://github.com/codespell-project/codespell 52 | rev: v2.2.2 53 | hooks: 54 | - id: codespell 55 | args: [-S, 'deps,_build', -L, crate] 56 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.13.4-otp-24 2 | erlang 24.3.4 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Wireguardex Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | `conduct AT firezone.dev`. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wireguardex 2 | 3 | ## Overview 4 | 5 | [![hex.pm](https://img.shields.io/hexpm/v/wireguardex.svg)](https://hex.pm/packages/wireguardex) 6 | [![hex.pm](https://img.shields.io/hexpm/dt/wireguardex.svg)](https://hex.pm/packages/wireguardex) 7 | [![hex.pm](https://img.shields.io/hexpm/l/wireguardex.svg)](https://hex.pm/packages/wireguardex) 8 | 9 | Wireguardex is an Elixir library for configuring [WireGuard®](https://www.wireguard.com/) interfaces. 10 | 11 | It is exposed as a native library via NIFs implemented in [Rust](https://rust-lang.org) using the [rustler](https://crates.io/crates/rustler) and [wireguard-control](https://docs.rs/wireguard-control/latest/wireguard_control/) crates. 12 | 13 | Used by [Firezone](https://github.com/firezone/firezone) to manage WireGuard interfaces in Elixir. 14 | 15 | ## Getting started 16 | 17 | Add `wireguardex` to your dependencies: 18 | 19 | ```elixir 20 | def deps do 21 | [ 22 | {:wireguardex, "~> 0.3"} 23 | ] 24 | end 25 | ``` 26 | 27 | Then you can just use wireguardex to manage your wireguard interfaces: 28 | 29 | ```elixir 30 | # Imports for cleanliness 31 | import Wireguardex.DeviceConfigBuilder 32 | import Wireguardex.PeerConfigBuilder 33 | import Wireguardex, only: [set_device: 2] 34 | 35 | interface_name = "wg0" 36 | private_key = Wireguardex.generate_private_key() 37 | {:ok, public_key} = Wireguardex.get_public_key(private_key) 38 | listen_port = 58210 39 | fwmark = 1234 40 | 41 | :ok = 42 | device_config() # <-- Start configuring the devices 43 | # Here we set configuration for the device 44 | |> private_key(private_key) 45 | |> public_key(public_key) 46 | |> listen_port(listen_port) 47 | |> fwmark(fwmark) 48 | |> set_device(interface_name) # <-- This actually creates the interface 49 | ``` 50 | 51 | After creation you could also add peers: 52 | 53 | ```elixir 54 | # Create a peer 55 | peer = 56 | peer_config() 57 | |> public_key(public_key) 58 | |> preshared_key(Wireguardex.generate_preshared_key()) 59 | |> endpoint("127.0.0.1:1234") 60 | |> persistent_keepalive_interval(30) 61 | |> allowed_ips(["255.0.0.0/24", "127.0.0.0/16"]) 62 | 63 | # Add peer to existing device 64 | :ok = Wireguardex.add_peer(interface_name, peer) 65 | ``` 66 | 67 | And easily remove it afterwards using its public key: 68 | 69 | ```elixir 70 | :ok = Wireguardex.remove_peer(interface_name, public_key) 71 | ``` 72 | 73 | To get information on an existing device: 74 | 75 | ```elixir 76 | {:ok, device} = Wireguardex.get_device(interface_name) 77 | ``` 78 | 79 | Finally to delete a device: 80 | 81 | ```elixir 82 | :ok = Wireguardex.delete_device(interface_name) 83 | ``` 84 | 85 | ## Installation 86 | 87 | The package can be installed by adding `wireguardex` to your list of dependencies 88 | in `mix.exs`: 89 | 90 | ```elixir 91 | def deps do 92 | [ 93 | {:wireguardex, "~> 0.3"} 94 | ] 95 | end 96 | ``` 97 | 98 | Wireguardex will try to download a precompiled NIF library. If you want to compile 99 | your own NIF, you'll need to have Rust installed. The common option is to use 100 | [Rustup](https://rustup.rs/). 101 | 102 | To force compilation you can set the environment variable `WIREGUARDNIF_BUILD` 103 | to `true` or `1`. Or you can set the application env to force the NIF to compile: 104 | 105 | ```elixir 106 | config :rustler_precompiled, :force_build, wireguardex: true 107 | ``` 108 | 109 | ### Note about privileges 110 | 111 | This library creates and modifies network interfaces. If you'd like to run this library as a non-root user, we recommend adding the `CAP_NET_ADMIN` Linux capability to the Erlang VM executable: 112 | 113 | ```sh 114 | sudo setcap 'cap_net_admin+eip' /bin/beam.smp 115 | ``` 116 | 117 | If you're using [asdf-vm](https://asdf-vm.com/) to manage dependencies you can do: 118 | 119 | ```sh 120 | sudo setcap 'cap_net_admin+eip' $(ls -1 `asdf where erlang 24.3.4`/erts-*/bin/beam.smp) 121 | ``` 122 | 123 | This can be handy for development and testing purposes. 124 | 125 | **Note**: This will also give `CAP_NET_ADMIN` to any other Erlang programs using this `beam.smp` executable. If this is undesired, consider using a dedicated Erlang installation or `beam.smp` executable for this library. 126 | 127 | ## Features 128 | 129 | * Manage WireGuard interfaces 130 | * Doesn't require a WireGuard installation 131 | 132 | ## Tests 133 | 134 | Running the tests in this library will also require a Rust installation, as the NIF is compiled 135 | locally before running the tests. 136 | 137 | Follow [these](https://www.rust-lang.org/learn/get-started) instructions to install Rust. 138 | 139 | Then you can run `mix test` as long as you have the [user privileges to create interfaces](#note-about-privileges). 140 | 141 | ### Pre-commit 142 | 143 | We use [pre-commit](https://pre-commit.com) to catch any static analysis issues before code is 144 | committed. Install with Homebrew: `brew install pre-commit` or pip: `pip install pre-commit`. 145 | 146 | ## Acknowledgments 147 | 148 | "WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld. 149 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We appreciate your help in making Wireguardex secure! We take security issues 4 | very seriously and strive to fix all security issues as soon as they're 5 | reported. 6 | 7 | ## Announcements 8 | 9 | We'll announce major security issues on our security mailing list located at: 10 | 11 | https://discourse.firez.one 12 | 13 | ## Supported Versions 14 | 15 | We release security patches for supported versions of Wireguardex. We recommend 16 | running the latest version of Wireguardex at all times. 17 | 18 | ## Reporting a Vulnerability 19 | 20 | Please **do not** open a Github Issue for security issues you encounter. 21 | Instead, please send an email to `security AT firezone.dev` describing the issue 22 | and we'll respond as soon as possible. 23 | 24 | ## PGP Key 25 | 26 | You may use the public key below to encrypt emails to `security AT firezone.dev`. 27 | You can also find this key at: 28 | 29 | https://pgp.mit.edu/pks/lookup?op=get&search=0x45113BA04AD83D8A 30 | 31 | ``` 32 | -----BEGIN PGP PUBLIC KEY BLOCK----- 33 | Version: SKS 1.1.6 34 | Comment: Hostname: pgp.mit.edu 35 | 36 | mDMEYYwK5BYJKwYBBAHaRw8BAQdA4ooDpwDy3V0wHCftM/LHD5e713LSr0SQy49joUMgHoS0 37 | JkZpcmV6b25lIFNlY3VyaXR5IDxzZWN1cml0eUBmaXJlei5vbmU+iJoEExYKAEIWIQQlD4tW 38 | gEEHBC38anNFETugStg9igUCYYwK5AIbAwUJA8JnAAULCQgHAgMiAgEGFQoJCAsCBBYCAwEC 39 | HgcCF4AACgkQRRE7oErYPYoORwEAiYi3arrcR2e5OfqsoAbCN0O6M0HWeo1K/ZoFWH2jLy0B 40 | AMsWk58vepKqNhUKhuDb8bSjK8TOr/IxB63lSkQaz9MIuDgEYYwK5BIKKwYBBAGXVQEFAQEH 41 | QPLzia/me7FOsFfAJKWm0X1qC5byv2GWn6LZPV013AdoAwEIB4h+BBgWCgAmFiEEJQ+LVoBB 42 | BwQt/GpzRRE7oErYPYoFAmGMCuQCGwwFCQPCZwAACgkQRRE7oErYPYr0ZQEAig86wu+zrNiT 43 | B4t3dk3psHRj+Kdn4uURLjUBZqYNvXoA+QEBUPtP7hNjum+1FrzYmHUFdCBA/cszz7x7PQ36 44 | 5gcE 45 | =0gEr 46 | -----END PGP PUBLIC KEY BLOCK----- 47 | ``` 48 | -------------------------------------------------------------------------------- /lib/wireguardex.ex: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex do 2 | @moduledoc """ 3 | Wireguardex is an Elixir library for configuring [WireGuard](https://wireguard.com) interfaces. It uses 4 | [Rust](https://rust-lang.org) NIFs for performance and safety. 5 | 6 | This is the main module, providing the API for interface configuration and 7 | utilities such as key generation. 8 | """ 9 | mix_config = Mix.Project.config() 10 | version = mix_config[:version] 11 | github_url = mix_config[:package][:links]["GitHub"] 12 | 13 | use RustlerPrecompiled, 14 | otp_app: :wireguardex, 15 | crate: "wireguard_nif", 16 | base_url: "#{github_url}/releases/download/v#{version}", 17 | force_build: System.get_env("WIREGUARDNIF_BUILD") in ["1", "true"], 18 | version: version, 19 | targets: [ 20 | "aarch64-unknown-linux-gnu", 21 | "x86_64-unknown-linux-gnu", 22 | "aarch64-unknown-linux-musl", 23 | "x86_64-unknown-linux-musl", 24 | "aarch64-apple-darwin", 25 | "x86_64-apple-darwin" 26 | ] 27 | 28 | @type device :: Wireguardex.Device.t() 29 | @type device_config :: Wireguardex.DeviceConfig.t() 30 | @type peer_config :: Wireguardex.PeerConfig.t() 31 | @type peer_stats :: Wireguardex.PeerStats.t() 32 | 33 | @type name :: String.t() 34 | @type key :: String.t() 35 | 36 | @doc """ 37 | Get a list of interface names of WireGuard devices. 38 | 39 | Returns `{:ok, [...]}` if successful. `{:error, error_info}` will be returned 40 | if listing device interface names fails. 41 | """ 42 | @spec list_devices() :: {:ok, [device()]} | {:error, String.t()} 43 | def list_devices, do: error() 44 | 45 | @doc """ 46 | Get a `Device` by its interface name. 47 | 48 | Returns `{:ok, Device}` if successful. `{:error, error_info}` will be returned 49 | if getting the device fails. 50 | """ 51 | @spec get_device(name()) :: {:ok, device()} | {:error, String.t()} 52 | def get_device(_name), do: error() 53 | 54 | @doc """ 55 | Set a `Device` by its interface name with a config. The config can be generated 56 | using a `DeviceConfigBuilder`. 57 | 58 | Note if no device is present, a new one will be created for the given interface name. 59 | 60 | Returns `:ok` if successful. `{:error, error_info}` will be returned if setting 61 | the device fails. 62 | """ 63 | @spec set_device(device_config(), name()) :: :ok | {:error, String.t()} 64 | def set_device(_device_config, _name), do: error() 65 | 66 | @doc """ 67 | Delete a `Device` by its interface name. 68 | 69 | Returns `:ok` if successful. `{:error, error_info}` will be returned if deleting 70 | the device fails. 71 | """ 72 | @spec delete_device(name()) :: :ok | {:error, String.t()} 73 | def delete_device(_name), do: error() 74 | 75 | @doc """ 76 | Remove a peer from a `Device` by the peer's public key. 77 | 78 | Returns `:ok` if successful. `{:error, error_info}` will be returned if removing 79 | the peer from the device fails. 80 | """ 81 | @spec remove_peer(name(), key()) :: :ok | {:error, String.t()} 82 | def remove_peer(_name, _public_key), do: error() 83 | 84 | @doc """ 85 | Add a peer to a `Device` using a config for the peer. A config can be generated 86 | using a `PeerConfigBuilder`. 87 | 88 | Returns `:ok` if successful. `{:error, error_info}` will be returned if adding 89 | the peer to the device fails. 90 | """ 91 | @spec add_peer(name(), peer_config()) :: :ok | {:error, String.t()} 92 | def add_peer(_name, _peer), do: error() 93 | 94 | @doc """ 95 | Generates a random private key. It is returned as a base64 `string`. 96 | """ 97 | @spec generate_private_key() :: key() 98 | def generate_private_key, do: error() 99 | 100 | @doc """ 101 | Generates a random preshared key. It is returned as a base64 `string`. 102 | """ 103 | @spec generate_preshared_key() :: key() 104 | def generate_preshared_key, do: error() 105 | 106 | @doc """ 107 | Return a private key's public key as a base64 `string`. 108 | 109 | Returns `{:ok, public_key}` if successful. `{:error, error_info}` will be returned if 110 | if getting the public key fails. 111 | """ 112 | @spec get_public_key(key()) :: {:ok, key()} | {:error, String.t()} 113 | def get_public_key(_private_key), do: error() 114 | 115 | defp error, do: :erlang.nif_error(:nif_not_loaded) 116 | end 117 | -------------------------------------------------------------------------------- /lib/wireguardex/device.ex: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex.Device do 2 | @moduledoc """ 3 | `Device` represents all available information about a WireGuard device (interface). 4 | 5 | This struct contains the current configuration of the interface and the current 6 | configuration and info for all of its peers. 7 | 8 | * `name` The interface name of the device. 9 | * `public_key` The public encryption key of the interface. 10 | * `private_key` The private encryption key of the interface. 11 | * `fwmark` The [fwmark](https://man7.org/linux/man-pages/man8/tc-fw.8.html) of 12 | this interface. 13 | * `listen_port` The port to listen for incoming connections. 14 | * `peers` The list of all peers registered to this interface with their configs 15 | and stats. 16 | * `linked_name` The associated "real" name of the interface. 17 | """ 18 | defstruct name: "", 19 | public_key: nil, 20 | private_key: nil, 21 | fwmark: nil, 22 | listen_port: nil, 23 | peers: [], 24 | linked_name: nil 25 | 26 | @type t :: %__MODULE__{} 27 | end 28 | -------------------------------------------------------------------------------- /lib/wireguardex/device_config.ex: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex.DeviceConfig do 2 | @moduledoc false 3 | defstruct public_key: nil, 4 | private_key: nil, 5 | fwmark: nil, 6 | listen_port: nil, 7 | peers: [], 8 | replace_peers: false 9 | 10 | @type t :: %__MODULE__{} 11 | end 12 | -------------------------------------------------------------------------------- /lib/wireguardex/device_config_builder.ex: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex.DeviceConfigBuilder do 2 | @moduledoc ~S""" 3 | This module contains functions to create a `DeviceConfig` which itself 4 | represents a configuration that can be set on a Wireguard device (interface). 5 | 6 | If an interface exists, the configuration is applied _on top_ of the existing 7 | interface's settings. Missing settings are not overwritten or set to defaults. 8 | 9 | To crate the default config use `device_config/0` then you can apply each 10 | function in this module to specify the details. 11 | 12 | ## Examples 13 | 14 | iex> res= 15 | ...> device_config() 16 | ...> |> listen_port(1234) 17 | ...> |> fwmark(11111) 18 | ...> |> Wireguardex.set_device("wg1234") 19 | ...> Wireguardex.delete_device("wg1234") 20 | ...> res 21 | :ok 22 | """ 23 | 24 | @doc """ 25 | Creates the default configuration to be configured with the provided functions. 26 | """ 27 | def device_config do 28 | %Wireguardex.DeviceConfig{} 29 | end 30 | 31 | @doc """ 32 | The public encryption key to set on the interface. 33 | """ 34 | def public_key(config_builder, public_key) do 35 | %{config_builder | public_key: public_key} 36 | end 37 | 38 | @doc """ 39 | The private encryption key to set on the interface. 40 | """ 41 | def private_key(config_builder, private_key) do 42 | %{config_builder | private_key: private_key} 43 | end 44 | 45 | @doc """ 46 | The [fwmark](https://man7.org/linux/man-pages/man8/tc-fw.8.html) to set on the 47 | interface. 48 | """ 49 | def fwmark(config_builder, fwmark) do 50 | %{config_builder | fwmark: fwmark} 51 | end 52 | 53 | @doc """ 54 | The listening port for incoming connections to set on the interface. 55 | """ 56 | def listen_port(config_builder, listen_port) do 57 | %{config_builder | listen_port: listen_port} 58 | end 59 | 60 | @doc """ 61 | A list of peers with their own configurations to set on this interface. 62 | """ 63 | def peers(config_builder, peers) do 64 | %{config_builder | peers: peers} 65 | end 66 | 67 | @doc """ 68 | If true, replace existing peer configurations on the interface. 69 | """ 70 | def replace_peers(config_builder, replace_peers) do 71 | %{config_builder | replace_peers: replace_peers} 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/wireguardex/peer_config.ex: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex.PeerConfig do 2 | @moduledoc false 3 | defstruct public_key: "", 4 | preshared_key: nil, 5 | endpoint: nil, 6 | persistent_keepalive_interval: nil, 7 | allowed_ips: [] 8 | 9 | @type t :: %__MODULE__{} 10 | end 11 | -------------------------------------------------------------------------------- /lib/wireguardex/peer_config_builder.ex: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex.PeerConfigBuilder do 2 | @moduledoc ~S""" 3 | This module contains functions to create a `PeerConfig` which represents 4 | a peer's configuration of persistent attributes. They do not change over 5 | time and are part of the configuration of a device. 6 | 7 | To create the default config use `peer_config/0` then you can apply each 8 | function in this module to specify the config. 9 | 10 | ## Examples 11 | 12 | iex> {:ok, public_key} = Wireguardex.get_public_key(Wireguardex.generate_private_key()) 13 | ...> res = 14 | ...> device_config() 15 | ...> |> peers([ 16 | ...> peer_config() 17 | ...> |> Wireguardex.PeerConfigBuilder.public_key(public_key) 18 | ...> |> endpoint("127.0.0.1:1234") 19 | ...> |> preshared_key(Wireguardex.generate_preshared_key()) 20 | ...> |> persistent_keepalive_interval(60) 21 | ...> |> allowed_ips(["127.0.0.1/16"]) 22 | ...> ]) 23 | ...> |> Wireguardex.set_device("wg12345") 24 | ...> Wireguardex.delete_device("wg12345") 25 | ...> res 26 | :ok 27 | """ 28 | 29 | @doc """ 30 | Creates the default peer configuration that you can then specify which each of 31 | the provided functions. 32 | """ 33 | def peer_config do 34 | %Wireguardex.PeerConfig{} 35 | end 36 | 37 | @doc """ 38 | The public key of the peer. 39 | """ 40 | def public_key(peer_config, public_key) do 41 | %{peer_config | public_key: public_key} 42 | end 43 | 44 | @doc """ 45 | The preshared key available to both peers (`nil` means no PSK is used). 46 | """ 47 | def preshared_key(peer_config, preshared_key) do 48 | %{peer_config | preshared_key: preshared_key} 49 | end 50 | 51 | @doc """ 52 | The endpoint this peer listens for connections on (`nil` means any). 53 | """ 54 | def endpoint(peer_config, endpoint) do 55 | %{peer_config | endpoint: endpoint} 56 | end 57 | 58 | @doc """ 59 | The interval for sending keepalive packets (`nil` means disabled). 60 | """ 61 | def persistent_keepalive_interval(peer_config, persistent_keepalive_interval) do 62 | %{peer_config | persistent_keepalive_interval: persistent_keepalive_interval} 63 | end 64 | 65 | @doc """ 66 | The allowed ip addresses this peer is allowed to have. 67 | """ 68 | def allowed_ips(peer_config, allowed_ips) do 69 | %{peer_config | allowed_ips: allowed_ips} 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/wireguardex/peer_info.ex: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex.PeerInfo do 2 | @moduledoc """ 3 | `PeerInfo` represents all of the available information of a peer. 4 | 5 | This struct is a simple pair of a peer's configuration and stats. 6 | 7 | * `config` The configuration belonging to this peer. 8 | * `stats` The current statistics of this peer. 9 | """ 10 | alias Wireguardex.PeerConfig 11 | alias Wireguardex.PeerStats 12 | 13 | defstruct config: %PeerConfig{}, 14 | stats: %PeerStats{} 15 | 16 | @type t :: %__MODULE__{} 17 | end 18 | -------------------------------------------------------------------------------- /lib/wireguardex/peer_stats.ex: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex.PeerStats do 2 | @moduledoc """ 3 | `PeerStats` represent a peer's statistics from the current session. 4 | 5 | These are the attributes of a peer that will change over time; to get updated 6 | stats re-read the information from the interface. 7 | 8 | * `last_handshake_time` Timestamp of the last handshake/rekey with this peer. 9 | * `rx_bytes` Number of bytes received from this peer. 10 | * `tx_bytes` Number of bytes transmitted to this peer. 11 | """ 12 | defstruct last_handshake_time: nil, 13 | rx_bytes: 0, 14 | tx_bytes: 0 15 | 16 | @type t :: %__MODULE__{} 17 | end 18 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Wireguardex.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/firezone/wireguardex" 5 | @version "0.4.0" 6 | 7 | def project do 8 | [ 9 | app: :wireguardex, 10 | version: @version, 11 | elixir: "~> 1.7", 12 | start_permanent: Mix.env() == :prod, 13 | compilers: Mix.compilers(), 14 | name: "Wireguardex", 15 | package: package(), 16 | deps: deps(), 17 | aliases: aliases(), 18 | docs: docs() 19 | ] 20 | end 21 | 22 | # Run "mix help compile.app" to learn about applications. 23 | def application do 24 | [ 25 | extra_applications: [:logger] 26 | ] 27 | end 28 | 29 | # Run "mix help deps" to learn about dependencies. 30 | defp deps do 31 | [ 32 | {:rustler_precompiled, "~> 0.7.0"}, 33 | {:rustler, ">= 0.0.0", optional: true}, 34 | {:ex_doc, "~> 0.27", only: :dev, runtime: false}, 35 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false} 36 | ] 37 | end 38 | 39 | defp package do 40 | [ 41 | description: "An Elixir library for configuring WireGuard interfaces via NIFs in Rust", 42 | maintainers: ["Jamil Bou Kheir "], 43 | licenses: ["Apache-2.0"], 44 | files: ~w(lib native .formatter.exs README* LICENSE* mix.exs checksum-*.exs), 45 | exclude_patterns: [".gitignore"], 46 | links: %{"GitHub" => @source_url} 47 | ] 48 | end 49 | 50 | defp docs do 51 | [ 52 | main: "readme", 53 | extras: ["README.md": [title: "Overview"]], 54 | source_url: @source_url, 55 | source_ref: "v#{@version}", 56 | hompage_url: @source_url, 57 | formatters: ["html"] 58 | ] 59 | end 60 | 61 | defp aliases do 62 | [ 63 | # force NIF compilation for tests 64 | test: [fn _ -> System.put_env("WIREGUARDNIF_BUILD", "true") end, "test"] 65 | ] 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, 4 | "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"}, 6 | "ex_doc": {:hex, :ex_doc, "0.30.9", "d691453495c47434c0f2052b08dd91cc32bc4e1a218f86884563448ee2502dd2", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d7aaaf21e95dc5cddabf89063327e96867d00013963eadf2c6ad135506a8bc10"}, 7 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 8 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 9 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 10 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 11 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, 12 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, 13 | "rustler": {:hex, :rustler, "0.30.0", "cefc49922132b072853fa9b0ca4dc2ffcb452f68fb73b779042b02d545e097fb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "9ef1abb6a7dda35c47cfc649e6a5a61663af6cf842a55814a554a84607dee389"}, 14 | "rustler_precompiled": {:hex, :rustler_precompiled, "0.7.0", "5d0834fc06dbc76dd1034482f17b1797df0dba9b491cef8bb045fcaca94bcade", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "fdf43a6835f4e4de5bfbc4c019bfb8c46d124bd4635fefa3e20d9a2bbbec1512"}, 15 | "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, 16 | } 17 | -------------------------------------------------------------------------------- /native/wireguard_nif/.cargo/config: -------------------------------------------------------------------------------- 1 | [profile.release] 2 | lto = true 3 | 4 | [target.'cfg(target_os = "macos")'] 5 | rustflags = [ 6 | "-C", "link-arg=-undefined", 7 | "-C", "link-arg=dynamic_lookup", 8 | ] 9 | 10 | [target.x86_64-unknown-linux-musl] 11 | rustflags = [ 12 | "-C", "target-feature=-crt-static" 13 | ] 14 | 15 | [target.aarch64-unknown-linux-musl] 16 | rustflags = [ 17 | "-C", "target-feature=-crt-static" 18 | ] 19 | -------------------------------------------------------------------------------- /native/wireguard_nif/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /native/wireguard_nif/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.57" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" 19 | 20 | [[package]] 21 | name = "base64" 22 | version = "0.13.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 25 | 26 | [[package]] 27 | name = "bitflags" 28 | version = "1.3.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 31 | 32 | [[package]] 33 | name = "byteorder" 34 | version = "1.4.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 37 | 38 | [[package]] 39 | name = "bytes" 40 | version = "1.1.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "crypto-common" 52 | version = "0.1.3" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" 55 | dependencies = [ 56 | "generic-array", 57 | "typenum", 58 | ] 59 | 60 | [[package]] 61 | name = "curve25519-dalek" 62 | version = "4.0.0-pre.2" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "12dc3116fe595d7847c701796ac1b189bd86b81f4f593c6f775f9d80fb2e29f4" 65 | dependencies = [ 66 | "byteorder", 67 | "digest", 68 | "rand_core", 69 | "subtle", 70 | "zeroize", 71 | ] 72 | 73 | [[package]] 74 | name = "digest" 75 | version = "0.10.3" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" 78 | dependencies = [ 79 | "crypto-common", 80 | ] 81 | 82 | [[package]] 83 | name = "generic-array" 84 | version = "0.14.5" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" 87 | dependencies = [ 88 | "typenum", 89 | "version_check", 90 | ] 91 | 92 | [[package]] 93 | name = "getrandom" 94 | version = "0.2.6" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" 97 | dependencies = [ 98 | "cfg-if", 99 | "libc", 100 | "wasi", 101 | ] 102 | 103 | [[package]] 104 | name = "heck" 105 | version = "0.4.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 108 | 109 | [[package]] 110 | name = "hex" 111 | version = "0.4.3" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 114 | 115 | [[package]] 116 | name = "lazy_static" 117 | version = "1.4.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 120 | 121 | [[package]] 122 | name = "libc" 123 | version = "0.2.126" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 126 | 127 | [[package]] 128 | name = "log" 129 | version = "0.4.17" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 132 | dependencies = [ 133 | "cfg-if", 134 | ] 135 | 136 | [[package]] 137 | name = "memchr" 138 | version = "2.5.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 141 | 142 | [[package]] 143 | name = "netlink-packet-core" 144 | version = "0.4.2" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "345b8ab5bd4e71a2986663e88c56856699d060e78e152e6e9d7966fcd5491297" 147 | dependencies = [ 148 | "anyhow", 149 | "byteorder", 150 | "libc", 151 | "netlink-packet-utils", 152 | ] 153 | 154 | [[package]] 155 | name = "netlink-packet-generic" 156 | version = "0.3.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "8678ffbbfef3dd88acbe85ed31d32f0de0a100854ee7d47fe5b250f81857a23b" 159 | dependencies = [ 160 | "anyhow", 161 | "byteorder", 162 | "libc", 163 | "netlink-packet-core", 164 | "netlink-packet-utils", 165 | ] 166 | 167 | [[package]] 168 | name = "netlink-packet-route" 169 | version = "0.11.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "733ea73609acfd7fa7ddadfb7bf709b0471668c456ad9513685af543a06342b2" 172 | dependencies = [ 173 | "anyhow", 174 | "bitflags", 175 | "byteorder", 176 | "libc", 177 | "netlink-packet-core", 178 | "netlink-packet-utils", 179 | ] 180 | 181 | [[package]] 182 | name = "netlink-packet-utils" 183 | version = "0.5.1" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "25af9cf0dc55498b7bd94a1508af7a78706aa0ab715a73c5169273e03c84845e" 186 | dependencies = [ 187 | "anyhow", 188 | "byteorder", 189 | "paste", 190 | "thiserror", 191 | ] 192 | 193 | [[package]] 194 | name = "netlink-packet-wireguard" 195 | version = "0.2.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "ebc4ffa5ccfab369b38b5aed9dabe51b62b39bc9c80de7bd1213c5a8cdae03c4" 198 | dependencies = [ 199 | "anyhow", 200 | "byteorder", 201 | "libc", 202 | "log", 203 | "netlink-packet-generic", 204 | "netlink-packet-utils", 205 | ] 206 | 207 | [[package]] 208 | name = "netlink-request" 209 | version = "1.5.4" 210 | source = "git+https://github.com/tonarino/innernet?rev=a6c918a581bc66824ca29e6c84be902f995d9726#a6c918a581bc66824ca29e6c84be902f995d9726" 211 | dependencies = [ 212 | "netlink-packet-core", 213 | "netlink-packet-generic", 214 | "netlink-packet-route", 215 | "netlink-sys", 216 | ] 217 | 218 | [[package]] 219 | name = "netlink-sys" 220 | version = "0.8.2" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "3e4c9f9547a08241bee7b6558b9b98e1f290d187de8b7cfca2bbb4937bcaa8f8" 223 | dependencies = [ 224 | "bytes", 225 | "libc", 226 | "log", 227 | ] 228 | 229 | [[package]] 230 | name = "paste" 231 | version = "1.0.7" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" 234 | 235 | [[package]] 236 | name = "proc-macro2" 237 | version = "1.0.69" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 240 | dependencies = [ 241 | "unicode-ident", 242 | ] 243 | 244 | [[package]] 245 | name = "quote" 246 | version = "1.0.33" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 249 | dependencies = [ 250 | "proc-macro2", 251 | ] 252 | 253 | [[package]] 254 | name = "rand_core" 255 | version = "0.6.3" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 258 | dependencies = [ 259 | "getrandom", 260 | ] 261 | 262 | [[package]] 263 | name = "regex" 264 | version = "1.5.6" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 267 | dependencies = [ 268 | "aho-corasick", 269 | "memchr", 270 | "regex-syntax", 271 | ] 272 | 273 | [[package]] 274 | name = "regex-syntax" 275 | version = "0.6.26" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 278 | 279 | [[package]] 280 | name = "rustler" 281 | version = "0.30.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "c4b4fea69e23de68c42c06769d6624d2d018da550c17244dd4b691f90ced4a7e" 284 | dependencies = [ 285 | "lazy_static", 286 | "rustler_codegen", 287 | "rustler_sys", 288 | ] 289 | 290 | [[package]] 291 | name = "rustler_codegen" 292 | version = "0.30.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "406061bd07aaf052c344257afed4988c5ec8efe4d2352b4c2cf27ea7c8575b12" 295 | dependencies = [ 296 | "heck", 297 | "proc-macro2", 298 | "quote", 299 | "syn 2.0.38", 300 | ] 301 | 302 | [[package]] 303 | name = "rustler_sys" 304 | version = "2.3.1" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "0a7c0740e5322b64e2b952d8f0edce5f90fcf6f6fe74cca3f6e78eb3de5ea858" 307 | dependencies = [ 308 | "regex", 309 | "unreachable", 310 | ] 311 | 312 | [[package]] 313 | name = "subtle" 314 | version = "2.4.1" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 317 | 318 | [[package]] 319 | name = "syn" 320 | version = "1.0.95" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" 323 | dependencies = [ 324 | "proc-macro2", 325 | "quote", 326 | "unicode-ident", 327 | ] 328 | 329 | [[package]] 330 | name = "syn" 331 | version = "2.0.38" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" 334 | dependencies = [ 335 | "proc-macro2", 336 | "quote", 337 | "unicode-ident", 338 | ] 339 | 340 | [[package]] 341 | name = "thiserror" 342 | version = "1.0.31" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 345 | dependencies = [ 346 | "thiserror-impl", 347 | ] 348 | 349 | [[package]] 350 | name = "thiserror-impl" 351 | version = "1.0.31" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 354 | dependencies = [ 355 | "proc-macro2", 356 | "quote", 357 | "syn 1.0.95", 358 | ] 359 | 360 | [[package]] 361 | name = "typenum" 362 | version = "1.15.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 365 | 366 | [[package]] 367 | name = "unicode-ident" 368 | version = "1.0.0" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" 371 | 372 | [[package]] 373 | name = "unreachable" 374 | version = "1.0.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" 377 | dependencies = [ 378 | "void", 379 | ] 380 | 381 | [[package]] 382 | name = "version_check" 383 | version = "0.9.4" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 386 | 387 | [[package]] 388 | name = "void" 389 | version = "1.0.2" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 392 | 393 | [[package]] 394 | name = "wasi" 395 | version = "0.10.2+wasi-snapshot-preview1" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 398 | 399 | [[package]] 400 | name = "wireguard-control" 401 | version = "1.5.4" 402 | source = "git+https://github.com/tonarino/innernet?rev=a6c918a581bc66824ca29e6c84be902f995d9726#a6c918a581bc66824ca29e6c84be902f995d9726" 403 | dependencies = [ 404 | "base64", 405 | "curve25519-dalek", 406 | "hex", 407 | "libc", 408 | "log", 409 | "netlink-packet-core", 410 | "netlink-packet-generic", 411 | "netlink-packet-route", 412 | "netlink-packet-wireguard", 413 | "netlink-request", 414 | "netlink-sys", 415 | "rand_core", 416 | ] 417 | 418 | [[package]] 419 | name = "wireguard_nif" 420 | version = "0.4.0" 421 | dependencies = [ 422 | "rustler", 423 | "wireguard-control", 424 | ] 425 | 426 | [[package]] 427 | name = "zeroize" 428 | version = "1.3.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" 431 | -------------------------------------------------------------------------------- /native/wireguard_nif/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wireguard_nif" 3 | version = "0.4.0" 4 | authors = ["Jamil Bou Kheir "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "wireguard_nif" 9 | path = "src/lib.rs" 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | rustler = "0.30" 14 | # wireguard-control 1.5.4 release on github instead of crates.io 15 | wireguard-control = { git = "https://github.com/tonarino/innernet", rev = "a6c918a581bc66824ca29e6c84be902f995d9726" } 16 | -------------------------------------------------------------------------------- /native/wireguard_nif/Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | # This allows us to build targets against GLIBC 2.17 from any host 3 | zig = "2.17" 4 | 5 | [target.aarch64-unknown-linux-gnu] 6 | # This allows us to build targets against GLIBC 2.17 from any host 7 | zig = "2.17" 8 | 9 | [build.env] 10 | passthrough = [ 11 | "RUSTLER_NIF_VERSION" 12 | ] 13 | -------------------------------------------------------------------------------- /native/wireguard_nif/README.md: -------------------------------------------------------------------------------- 1 | # NIF for Elixir.WireguardNative 2 | 3 | ## To build the NIF module: 4 | 5 | - Your NIF will now build along with your project. 6 | 7 | ## To load the NIF: 8 | 9 | ```elixir 10 | defmodule WireguardNative do 11 | use Rustler, otp_app: :wireguard_native, crate: "wireguard_nif" 12 | 13 | # When your NIF is loaded, it will override this function. 14 | def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded) 15 | end 16 | ``` 17 | 18 | ## Examples 19 | 20 | [This](https://github.com/hansihe/NifIo) is a complete example of a NIF written in Rust. 21 | -------------------------------------------------------------------------------- /native/wireguard_nif/src/device.rs: -------------------------------------------------------------------------------- 1 | //! nif bindings for wireguard devices 2 | 3 | // XXX: Disable false positive detection of extra_unused_lifetimes 4 | // See https://github.com/rusterlium/rustler/issues/470 5 | #![allow(clippy::extra_unused_lifetimes)] 6 | use std::convert::{TryFrom, TryInto}; 7 | 8 | use rustler::{types::atom, Atom, Error, NifResult, NifStruct}; 9 | use wireguard_control::{Backend, Device, DeviceUpdate, InterfaceName, PeerConfigBuilder}; 10 | 11 | use crate::key; 12 | use crate::peer::{NifPeerConfig, NifPeerInfo}; 13 | 14 | #[cfg(target_os = "linux")] 15 | const BACKEND: Backend = Backend::Kernel; 16 | #[cfg(not(target_os = "linux"))] 17 | const BACKEND: Backend = Backend::Userspace; 18 | 19 | #[derive(NifStruct)] 20 | #[module = "Wireguardex.Device"] 21 | struct NifDevice { 22 | name: String, 23 | public_key: Option, 24 | private_key: Option, 25 | fwmark: Option, 26 | listen_port: Option, 27 | peers: Vec, 28 | linked_name: Option, 29 | } 30 | 31 | impl From for NifDevice { 32 | fn from(d: Device) -> Self { 33 | Self { 34 | name: d.name.as_str_lossy().to_string(), 35 | public_key: d.public_key.map(|k| k.to_base64()), 36 | private_key: d.private_key.map(|k| k.to_base64()), 37 | fwmark: d.fwmark, 38 | listen_port: d.listen_port, 39 | peers: d.peers.into_iter().map(|p| p.into()).collect(), 40 | linked_name: d.linked_name, 41 | } 42 | } 43 | } 44 | 45 | impl TryFrom for DeviceUpdate { 46 | type Error = Error; 47 | 48 | fn try_from(config: NifDeviceConfig) -> NifResult { 49 | let mut device = DeviceUpdate::new(); 50 | let public_key = config.public_key; 51 | let private_key = config.private_key; 52 | let fwmark = config.fwmark; 53 | let listen_port = config.listen_port; 54 | let peers = config 55 | .peers 56 | .into_iter() 57 | .map(|c| c.try_into()) 58 | .collect::>>()?; 59 | 60 | if let Some(public_key) = public_key { 61 | device = device.set_public_key(key::from_base64(&public_key)?); 62 | } 63 | if let Some(private_key) = private_key { 64 | device = device.set_private_key(key::from_base64(&private_key)?); 65 | } 66 | if let Some(fwmark) = fwmark { 67 | device = device.set_fwmark(fwmark); 68 | } 69 | if let Some(listen_port) = listen_port { 70 | device = device.set_listen_port(listen_port); 71 | } 72 | if config.replace_peers { 73 | device = device.replace_peers(); 74 | } 75 | 76 | Ok(device.add_peers(&peers)) 77 | } 78 | } 79 | 80 | #[derive(NifStruct)] 81 | #[module = "Wireguardex.DeviceConfig"] 82 | struct NifDeviceConfig { 83 | public_key: Option, 84 | private_key: Option, 85 | fwmark: Option, 86 | listen_port: Option, 87 | peers: Vec, 88 | replace_peers: bool, 89 | } 90 | 91 | #[rustler::nif] 92 | fn list_devices() -> NifResult<(Atom, Vec)> { 93 | Ok(( 94 | atom::ok(), 95 | to_term_error(Device::list(BACKEND))? 96 | .iter() 97 | .map(|iname| iname.as_str_lossy().to_string()) 98 | .collect(), 99 | )) 100 | } 101 | 102 | #[rustler::nif] 103 | fn get_device(name: &str) -> NifResult<(Atom, NifDevice)> { 104 | let iname = parse_iname(name)?; 105 | let device = to_term_error(Device::get(&iname, BACKEND))?; 106 | 107 | Ok((atom::ok(), device.into())) 108 | } 109 | 110 | #[rustler::nif] 111 | fn set_device(config: NifDeviceConfig, name: &str) -> NifResult { 112 | let iname = parse_iname(name)?; 113 | let device: DeviceUpdate = config.try_into()?; 114 | 115 | to_term_error(device.apply(&iname, BACKEND))?; 116 | 117 | Ok(atom::ok()) 118 | } 119 | 120 | #[rustler::nif] 121 | fn delete_device(name: &str) -> NifResult { 122 | let iname = parse_iname(name)?; 123 | let device = to_term_error(Device::get(&iname, BACKEND))?; 124 | 125 | to_term_error(device.delete())?; 126 | 127 | Ok(atom::ok()) 128 | } 129 | 130 | #[rustler::nif] 131 | fn remove_peer(name: &str, public_key: &str) -> NifResult { 132 | let iname = parse_iname(name)?; 133 | let key = key::from_base64(public_key)?; 134 | let device = DeviceUpdate::new().remove_peer_by_key(&key); 135 | 136 | to_term_error(device.apply(&iname, BACKEND))?; 137 | 138 | Ok(atom::ok()) 139 | } 140 | 141 | #[rustler::nif] 142 | fn add_peer(name: &str, peer: NifPeerConfig) -> NifResult { 143 | let iname = parse_iname(name)?; 144 | let device = DeviceUpdate::new().add_peer(peer.try_into()?); 145 | 146 | to_term_error(device.apply(&iname, BACKEND))?; 147 | 148 | Ok(atom::ok()) 149 | } 150 | 151 | fn parse_iname(name: &str) -> NifResult { 152 | to_term_error(name.parse()) 153 | } 154 | 155 | pub(crate) fn to_term_error(res: Result) -> NifResult { 156 | res.map_err(|e| Error::Term(Box::new(e.to_string()))) 157 | } 158 | -------------------------------------------------------------------------------- /native/wireguard_nif/src/key.rs: -------------------------------------------------------------------------------- 1 | //! nif bindings for generating wireguard keys 2 | 3 | use rustler::{types::atom, Atom, NifResult}; 4 | use wireguard_control::Key; 5 | 6 | use crate::device::to_term_error; 7 | 8 | #[rustler::nif] 9 | fn generate_private_key() -> String { 10 | let key = Key::generate_private(); 11 | 12 | key.to_base64() 13 | } 14 | 15 | #[rustler::nif] 16 | fn generate_preshared_key() -> String { 17 | let key = Key::generate_preshared(); 18 | 19 | key.to_base64() 20 | } 21 | 22 | #[rustler::nif] 23 | fn get_public_key(key: &str) -> NifResult<(Atom, String)> { 24 | let key = from_base64(key)?; 25 | 26 | Ok((atom::ok(), key.get_public().to_base64())) 27 | } 28 | 29 | pub(crate) fn from_base64(key: &str) -> NifResult { 30 | to_term_error(Key::from_base64(key)) 31 | } 32 | -------------------------------------------------------------------------------- /native/wireguard_nif/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! nif bindings for wireguard 2 | 3 | mod device; 4 | mod key; 5 | mod peer; 6 | 7 | use device::{add_peer, delete_device, get_device, list_devices, remove_peer, set_device}; 8 | use key::{generate_preshared_key, generate_private_key, get_public_key}; 9 | 10 | rustler::init!( 11 | "Elixir.Wireguardex", 12 | [ 13 | list_devices, 14 | get_device, 15 | set_device, 16 | delete_device, 17 | remove_peer, 18 | generate_private_key, 19 | generate_preshared_key, 20 | get_public_key, 21 | add_peer, 22 | ] 23 | ); 24 | -------------------------------------------------------------------------------- /native/wireguard_nif/src/peer.rs: -------------------------------------------------------------------------------- 1 | //! nif bindings for wireguard peers 2 | 3 | // XXX: Disable false positive detection of extra_unused_lifetimes 4 | // See https://github.com/rusterlium/rustler/issues/470 5 | #![allow(clippy::extra_unused_lifetimes)] 6 | use std::convert::TryFrom; 7 | use std::time::SystemTime; 8 | 9 | use rustler::{Error, NifResult, NifStruct}; 10 | use wireguard_control::{AllowedIp, PeerConfig, PeerConfigBuilder, PeerInfo, PeerStats}; 11 | 12 | use crate::{device::to_term_error, key}; 13 | 14 | #[derive(NifStruct)] 15 | #[module = "Wireguardex.PeerConfig"] 16 | pub(crate) struct NifPeerConfig { 17 | public_key: String, 18 | preshared_key: Option, 19 | endpoint: Option, 20 | persistent_keepalive_interval: Option, 21 | allowed_ips: Vec, 22 | } 23 | 24 | impl From for NifPeerConfig { 25 | fn from(config: PeerConfig) -> Self { 26 | Self { 27 | public_key: config.public_key.to_base64(), 28 | preshared_key: config.preshared_key.map(|k| k.to_base64()), 29 | endpoint: config.endpoint.map(|e| e.to_string()), 30 | persistent_keepalive_interval: config.persistent_keepalive_interval, 31 | allowed_ips: config 32 | .allowed_ips 33 | .iter() 34 | .map(|ip| format!("{}/{}", ip.address, ip.cidr)) 35 | .collect(), 36 | } 37 | } 38 | } 39 | 40 | impl TryFrom for PeerConfigBuilder { 41 | type Error = Error; 42 | 43 | fn try_from(nif_config: NifPeerConfig) -> NifResult { 44 | let public_key = key::from_base64(&nif_config.public_key)?; 45 | let preshared_key = nif_config.preshared_key; 46 | let endpoint = nif_config.endpoint; 47 | let persistent_keepalive_interval = nif_config.persistent_keepalive_interval; 48 | let allowed_ips = nif_config 49 | .allowed_ips 50 | .iter() 51 | .map(|ip| { 52 | ip.parse().map_err(|_| { 53 | Error::Term(Box::new(format!("Allowed ip failed to parse: {0}", ip))) 54 | }) 55 | }) 56 | .collect::>>()?; 57 | 58 | let mut config = PeerConfigBuilder::new(&public_key); 59 | 60 | if let Some(preshared_key) = preshared_key { 61 | config = config.set_preshared_key(key::from_base64(&preshared_key)?); 62 | } 63 | if let Some(endpoint) = endpoint { 64 | config = config.set_endpoint(to_term_error(endpoint.parse())?); 65 | } 66 | if let Some(persistent_keepalive_interval) = persistent_keepalive_interval { 67 | config = config.set_persistent_keepalive_interval(persistent_keepalive_interval); 68 | } 69 | 70 | config = config.add_allowed_ips(&allowed_ips); 71 | 72 | Ok(config) 73 | } 74 | } 75 | 76 | #[derive(NifStruct)] 77 | #[module = "Wireguardex.PeerInfo"] 78 | pub(crate) struct NifPeerInfo { 79 | config: NifPeerConfig, 80 | stats: NifPeerStats, 81 | } 82 | 83 | impl From for NifPeerInfo { 84 | fn from(info: PeerInfo) -> Self { 85 | Self { 86 | config: info.config.into(), 87 | stats: info.stats.into(), 88 | } 89 | } 90 | } 91 | 92 | #[derive(NifStruct)] 93 | #[module = "Wireguardex.PeerStats"] 94 | struct NifPeerStats { 95 | last_handshake_time: Option, 96 | rx_bytes: u64, 97 | tx_bytes: u64, 98 | } 99 | 100 | impl From for NifPeerStats { 101 | fn from(stats: PeerStats) -> Self { 102 | let last_handshake_time = 103 | stats 104 | .last_handshake_time 105 | .map(|t| match t.duration_since(SystemTime::UNIX_EPOCH) { 106 | Ok(d) => d.as_secs(), 107 | // This should be very very rare if it's even possible. 108 | Err(_) => panic!("Last handshake time was before UNIX_EPOCH"), 109 | }); 110 | 111 | Self { 112 | last_handshake_time, 113 | rx_bytes: stats.rx_bytes, 114 | tx_bytes: stats.tx_bytes, 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pre-commit 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/wireguardex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WireguardexTest do 2 | use ExUnit.Case 3 | import Wireguardex.DeviceConfigBuilder, except: [public_key: 2] 4 | import Wireguardex.PeerConfigBuilder, except: [public_key: 2] 5 | import Wireguardex, only: [set_device: 2] 6 | doctest Wireguardex 7 | doctest Wireguardex.DeviceConfigBuilder 8 | doctest Wireguardex.PeerConfigBuilder 9 | 10 | test "set device" do 11 | interface_name = "wg0" 12 | private_key = Wireguardex.generate_private_key() 13 | {:ok, public_key} = Wireguardex.get_public_key(private_key) 14 | listen_port = 58_210 15 | fwmark = 1234 16 | 17 | :ok = 18 | device_config() 19 | |> private_key(private_key) 20 | |> Wireguardex.PeerConfigBuilder.public_key(public_key) 21 | |> listen_port(listen_port) 22 | |> fwmark(fwmark) 23 | |> set_device(interface_name) 24 | 25 | {:ok, device} = Wireguardex.get_device(interface_name) 26 | :ok = Wireguardex.delete_device(interface_name) 27 | 28 | assert device.name == interface_name 29 | assert device.public_key == public_key 30 | assert device.private_key == private_key 31 | assert device.fwmark == fwmark 32 | assert device.listen_port == listen_port 33 | end 34 | 35 | test "list devices" do 36 | interface_name = "wg1" 37 | :ok = Wireguardex.set_device(%Wireguardex.DeviceConfig{}, interface_name) 38 | {:ok, devices} = Wireguardex.list_devices() 39 | :ok = Wireguardex.delete_device(interface_name) 40 | 41 | assert List.first(devices) == interface_name 42 | end 43 | 44 | test "add peers to device" do 45 | interface_name = "wg2" 46 | {:ok, public_key0} = Wireguardex.get_public_key(Wireguardex.generate_private_key()) 47 | {:ok, public_key1} = Wireguardex.get_public_key(Wireguardex.generate_private_key()) 48 | 49 | peers = [ 50 | peer_config() 51 | |> Wireguardex.PeerConfigBuilder.public_key(public_key0) 52 | |> preshared_key(Wireguardex.generate_preshared_key()) 53 | |> endpoint("127.0.0.1:1234") 54 | |> persistent_keepalive_interval(60) 55 | |> allowed_ips(["192.168.0.0/24", "163.23.42.242/32"]), 56 | peer_config() 57 | |> Wireguardex.PeerConfigBuilder.public_key(public_key1) 58 | |> preshared_key(Wireguardex.generate_preshared_key()) 59 | |> endpoint("127.0.0.1:1234") 60 | |> persistent_keepalive_interval(30) 61 | |> allowed_ips(["255.0.0.0/24", "127.0.0.0/16"]) 62 | ] 63 | 64 | :ok = 65 | device_config() 66 | |> peers(peers) 67 | |> set_device(interface_name) 68 | 69 | {:ok, device} = Wireguardex.get_device(interface_name) 70 | :ok = Wireguardex.delete_device(interface_name) 71 | 72 | assert List.first(device.peers).config == List.first(peers) 73 | assert List.last(device.peers).config == List.last(peers) 74 | end 75 | 76 | test "add peer to device after creation" do 77 | interface_name = "wg3" 78 | {:ok, public_key} = Wireguardex.get_public_key(Wireguardex.generate_private_key()) 79 | 80 | peer = %Wireguardex.PeerConfig{ 81 | public_key: public_key, 82 | preshared_key: Wireguardex.generate_preshared_key(), 83 | endpoint: "127.0.0.1:1234", 84 | persistent_keepalive_interval: 60, 85 | allowed_ips: ["192.168.0.0/24", "163.23.42.242/32"] 86 | } 87 | 88 | :ok = 89 | device_config() 90 | |> set_device(interface_name) 91 | 92 | :ok = Wireguardex.add_peer(interface_name, peer) 93 | {:ok, device} = Wireguardex.get_device(interface_name) 94 | :ok = Wireguardex.delete_device(interface_name) 95 | 96 | assert List.first(device.peers).config == peer 97 | end 98 | 99 | test "remove peer to device after creation" do 100 | interface_name = "wg4" 101 | {:ok, public_key} = Wireguardex.get_public_key(Wireguardex.generate_private_key()) 102 | 103 | peer = %Wireguardex.PeerConfig{ 104 | public_key: public_key, 105 | preshared_key: Wireguardex.generate_preshared_key(), 106 | endpoint: "127.0.0.1:1234", 107 | persistent_keepalive_interval: 60, 108 | allowed_ips: ["192.168.0.0/24", "163.23.42.242/32"] 109 | } 110 | 111 | :ok = 112 | device_config() 113 | |> peers([peer]) 114 | |> set_device(interface_name) 115 | 116 | :ok = Wireguardex.remove_peer(interface_name, public_key) 117 | {:ok, device} = Wireguardex.get_device(interface_name) 118 | :ok = Wireguardex.delete_device(interface_name) 119 | 120 | assert device.peers == [] 121 | end 122 | end 123 | --------------------------------------------------------------------------------