mirror of https://github.com/dswd/vpncloud.git
Compare commits
395 Commits
Author | SHA1 | Date |
---|---|---|
Dennis Schwerdel | bef99162fe | |
Dennis Schwerdel | 24c48e2ef2 | |
Dennis Schwerdel | 7e55ec5df6 | |
Dennis Schwerdel | 18e4a22210 | |
Dennis Schwerdel | 06f3d3c761 | |
Dennis Schwerdel | 5eeb125d74 | |
Dennis Schwerdel | ca220ded0c | |
Dennis Schwerdel | 2505679021 | |
Dennis Schwerdel | 358183ef12 | |
Dennis Schwerdel | 28b1c76d70 | |
Dennis Schwerdel | 5c361d08ab | |
Dennis Schwerdel | 16d5d47f01 | |
Dennis Schwerdel | 50a6c01e93 | |
Dennis Schwerdel | 2dc774196e | |
Dennis Schwerdel | 79916104ba | |
Dennis Schwerdel | 798964030f | |
Dennis Schwerdel | c7c1ebc11b | |
Dennis Schwerdel | 4896a964ea | |
dswd | 59920ff627 | |
dswd | 665b4d505f | |
dswd | 6c5065c267 | |
dswd | 8d6c2b955e | |
Grzegorz Grasza | c61abbdb34 | |
dependabot[bot] | cd5c58239b | |
dependabot[bot] | e52c28794e | |
dependabot[bot] | e70a0412f9 | |
dswd | 0d19bb04e8 | |
dswd | bb97bfd2c5 | |
dependabot[bot] | 87446af0b2 | |
dswd | 85fc272ad4 | |
dswd | 1368ee50e5 | |
dependabot[bot] | 45216f7ef2 | |
dependabot[bot] | d71225b88d | |
dependabot[bot] | bd50eaec77 | |
dswd | 95dabab017 | |
dswd | e4956d209b | |
dswd | 46f083ed68 | |
dswd | 88d34e2fcd | |
dependabot[bot] | 4cbbc21a76 | |
dependabot[bot] | 4c9df625df | |
dependabot[bot] | 7cbbababce | |
dependabot[bot] | 1ddb18d63b | |
dswd | b584eb7163 | |
dswd | 115e42b553 | |
dswd | dd529f15ff | |
dswd | 062868dfd0 | |
dependabot[bot] | 2810eebca2 | |
dependabot[bot] | a69b6e94cf | |
dependabot[bot] | d70ff828bc | |
dependabot[bot] | 96cf585ca6 | |
dswd | ba0a903774 | |
dswd | 7213cc055d | |
dswd | 8bd1479904 | |
Kirill Isakov | 2adbc07cc3 | |
dependabot[bot] | 4868e8064c | |
dependabot[bot] | 671454f59c | |
dswd | 6e5f71b116 | |
dswd | 5e5522d6bd | |
dependabot[bot] | 883fae862f | |
dependabot[bot] | 9601aeffee | |
dswd | 7209e311c9 | |
dependabot[bot] | 1e9eb41ea6 | |
dswd | 06d29c8e3c | |
dependabot[bot] | 1ac20db0b3 | |
dswd | ba15bcfabf | |
dependabot[bot] | 129d0e5e93 | |
Dennis Schwerdel | 10ee616d89 | |
Dennis Schwerdel | ad75704f7b | |
dswd | 4d8286c54c | |
dswd | 00bf732be9 | |
dependabot[bot] | 817d060441 | |
dependabot[bot] | cc105daac6 | |
dswd | 36195cb582 | |
dependabot[bot] | f77ac6d6f4 | |
dswd | 8fe0815acd | |
dependabot[bot] | 0119820495 | |
dswd | e77cb6ef0f | |
dependabot[bot] | 213faa5649 | |
dswd | 396351b61c | |
dswd | 41bcb46579 | |
dswd | b0d73c44ed | |
dswd | ea53a4aad5 | |
dswd | 25e2f327e9 | |
Dennis Schwerdel | 37aebf64c3 | |
Dennis Schwerdel | 8d81a0117f | |
Dennis Schwerdel | 8c55e6c076 | |
Dennis Schwerdel | 7427be31c8 | |
Dennis Schwerdel | 5609a61ddb | |
Jeffrey Schiller | 0f9a0d8f91 | |
dswd | 7a55529cb9 | |
dependabot[bot] | eef6e528e2 | |
Dennis Schwerdel | 4fb92a36a6 | |
Dennis Schwerdel | 665b190257 | |
Dennis Schwerdel | ff75845dfb | |
Dennis Schwerdel | 97e168c856 | |
Dennis Schwerdel | 3b96380b42 | |
Dennis Schwerdel | 6d4591f685 | |
dswd | 41e8ecd962 | |
Dennis Schwerdel | ea3b2e22a6 | |
Martin Hauke | 151ca445c3 | |
dswd | b745735af8 | |
dependabot[bot] | 07d65df6ee | |
Dennis Schwerdel | d7d06941e5 | |
Dennis Schwerdel | 62def2de17 | |
dswd | 8df6214fd7 | |
dswd | 940707333c | |
dependabot[bot] | 8675fdee87 | |
dependabot[bot] | 5053bfa559 | |
dswd | bcc029afea | |
dswd | 35ac0339e7 | |
dependabot[bot] | 186f3fc215 | |
dependabot[bot] | e643217671 | |
dswd | 1e53e98c58 | |
dswd | dd4b38e274 | |
dependabot[bot] | a427ce70bd | |
dependabot[bot] | 2b79496e2c | |
dswd | 2f69450af7 | |
dependabot[bot] | c8192928dc | |
Dennis Schwerdel | 493dcad170 | |
Dennis Schwerdel | f11c0efb8b | |
dswd | 5176c32cc2 | |
whitesource-bolt-for-github[bot] | 8c1c04ddf3 | |
dswd | e47d7fb4c7 | |
dswd | 0ebc7fc7b6 | |
dependabot[bot] | 137a9d18e2 | |
dependabot[bot] | 34ecf9f9e9 | |
Dennis Schwerdel | 3da0d27eb7 | |
Dennis Schwerdel | 8e49311fef | |
Dennis Schwerdel | c63b2d1cd5 | |
dswd | ac95f34402 | |
Dennis Schwerdel | 4950753548 | |
Dennis Schwerdel | c4ff0ed198 | |
dependabot[bot] | 6b88ac5733 | |
Dennis Schwerdel | ad2e2014eb | |
dswd | 522d7d3f9c | |
dswd | ee618ba042 | |
dependabot[bot] | 61b66fa71c | |
dependabot[bot] | 506c0656ef | |
Dennis Schwerdel | 2955a80af4 | |
Dennis Schwerdel | 38c3ba1177 | |
Dennis Schwerdel | 0cffea7017 | |
Dennis Schwerdel | 7abeea1f95 | |
Dennis Schwerdel | 29630ab763 | |
Dennis Schwerdel | ecebd2aab8 | |
Dennis Schwerdel | d9a2ee28cc | |
Dennis Schwerdel | 5236adf7b3 | |
Dennis Schwerdel | 13688edd75 | |
Dennis Schwerdel | ed260d9a98 | |
Dennis Schwerdel | d154f85ecd | |
Dennis Schwerdel | 65eef143cd | |
Dennis Schwerdel | 791ecfb0fe | |
Dennis Schwerdel | e9122743e9 | |
Dennis Schwerdel | bd0d102358 | |
Dennis Schwerdel | a113b3ba22 | |
Dennis Schwerdel | f0d9ad2ccd | |
Dennis Schwerdel | e3fa631ed9 | |
dswd | 4c934ea028 | |
dswd | 03ec09d27c | |
dependabot[bot] | 221d4ed490 | |
dependabot[bot] | f637af0faa | |
dswd | 1e11e15ea6 | |
dependabot[bot] | 79c4f4ef9f | |
dswd | 0c3cbcf390 | |
dependabot[bot] | 7f135fbf24 | |
Dennis Schwerdel | cbd38ed712 | |
Dennis Schwerdel | ca7df77532 | |
dswd | 238b8a22f1 | |
dswd | 556126572c | |
dependabot[bot] | 1b79a9f115 | |
dependabot[bot] | a2c179d4fb | |
Dennis Schwerdel | e150443891 | |
dswd | cce43cbf49 | |
dswd | b855789584 | |
dependabot[bot] | f1d83fe1d4 | |
dependabot[bot] | 6e5dfc1b81 | |
Dennis Schwerdel | 13f7d02086 | |
Dennis Schwerdel | cd619d3980 | |
Dennis Schwerdel | 2a8ea5087b | |
Dennis Schwerdel | f95fb17dd4 | |
Dennis Schwerdel | b6f4460f29 | |
Dennis Schwerdel | 8c4d86a101 | |
Dennis Schwerdel | 8e3cdbddbf | |
Dennis Schwerdel | e6994e6939 | |
dswd | a6cc124c56 | |
dswd | 041368994f | |
dependabot[bot] | 4e146065e1 | |
dependabot[bot] | a664d60e1e | |
Dennis Schwerdel | 285940c60a | |
dswd | e6f10929b7 | |
dependabot[bot] | 386250576f | |
Dennis Schwerdel | c941f9c56d | |
dswd | c5626e75ac | |
dswd | 4c56917d1e | |
dependabot[bot] | 57d26944fb | |
dependabot[bot] | e2a2579322 | |
dswd | 3b95cc0fc3 | |
dswd | 895d14615a | |
dependabot[bot] | a06157930a | |
dependabot[bot] | 3d28d9e188 | |
dswd | 3ca6926124 | |
dswd | 4ea69b8a2e | |
dependabot[bot] | 107c3cac34 | |
dependabot[bot] | 3effd360a2 | |
dswd | 2450cc39c5 | |
dependabot[bot] | 23af339343 | |
dswd | 577500fb0b | |
dependabot[bot] | cc823ed330 | |
dswd | 8f77db8104 | |
dependabot[bot] | 80cb58f4c6 | |
dswd | c2dd3d9d08 | |
dswd | de1e556d02 | |
dependabot[bot] | 8252724feb | |
dependabot[bot] | b85c935d4d | |
dswd | 517348edee | |
dswd | e9770887af | |
dependabot[bot] | 8902db6a3a | |
dependabot[bot] | 99163cad80 | |
Dennis Schwerdel | 124f7cbff9 | |
Dennis Schwerdel | d50490ac51 | |
Dennis Schwerdel | edd0e7a29f | |
Dennis Schwerdel | f5888d9df6 | |
dswd | 2416f8e47a | |
dswd | 242b65f527 | |
dswd | 06014b4751 | |
dswd | 3e7b53a369 | |
Dennis Schwerdel | 6d1bb8fabb | |
dependabot[bot] | 4365f408b5 | |
dependabot[bot] | e71b6b7149 | |
dependabot[bot] | e6fdc737fa | |
dependabot[bot] | 87b54f1c3f | |
Dennis Schwerdel | 70ad35932c | |
dswd | e689d9f492 | |
dswd | 0fea65a25d | |
dependabot[bot] | 329bf4de3e | |
Dennis Schwerdel | 26689a3003 | |
dependabot[bot] | b13089d5c4 | |
Dennis Schwerdel | ea049e4a4c | |
Dennis Schwerdel | d3402e1f50 | |
dswd | 5751a2999f | |
dependabot[bot] | a4ebff84af | |
dswd | 7184a64ce3 | |
dependabot[bot] | 1e9ace409f | |
Dennis Schwerdel | ea5d515ffd | |
Dennis Schwerdel | 8e40e74914 | |
Dennis Schwerdel | f210f75a25 | |
Dennis Schwerdel | 46c8407db9 | |
Dennis Schwerdel | c31de207c7 | |
dswd | 61733ac648 | |
dependabot[bot] | 4137054360 | |
Dennis Schwerdel | 150f219e04 | |
Dennis Schwerdel | 35bdfafabf | |
Dennis Schwerdel | 3797106f14 | |
Dennis Schwerdel | cbc73d0c0e | |
dswd | fc18245fda | |
dependabot[bot] | a7ee935019 | |
Dennis Schwerdel | ac1aec9d25 | |
Dennis Schwerdel | 45ab0a4608 | |
dswd | bd839534e0 | |
Dennis Schwerdel | 69b1229856 | |
Dennis Schwerdel | 0e764df560 | |
Dennis Schwerdel | fb06381d04 | |
Dennis Schwerdel | a427b3fd12 | |
Dennis Schwerdel | ca0e5e9791 | |
Dennis Schwerdel | 16da58b8df | |
Dennis Schwerdel | 47f872d5f4 | |
Dennis Schwerdel | 800e284408 | |
dswd | 7bbf3ccdc4 | |
dependabot[bot] | 9755be3345 | |
Dennis Schwerdel | c5f25ba48e | |
Dennis Schwerdel | ab79ee58de | |
Dennis Schwerdel | 7cfc77a2d8 | |
Dennis Schwerdel | aab4f000b5 | |
Dennis Schwerdel | dd9444c5ef | |
Dennis Schwerdel | 7e8b042055 | |
Dennis Schwerdel | 28d26830c9 | |
Dennis Schwerdel | 2656820f37 | |
Dennis Schwerdel | 0677c7682e | |
Dennis Schwerdel | 43382bbe33 | |
Dennis Schwerdel | 35c2b3479f | |
Dennis Schwerdel | 4f2b09d92c | |
dswd | 8e581ad005 | |
dswd | 1f7617058e | |
dependabot[bot] | 43e8066500 | |
dependabot[bot] | 9565726a30 | |
dswd | a5a0fd1c30 | |
dswd | 47ac58f4ca | |
dependabot[bot] | 7f1de231cb | |
dependabot[bot] | 98d8ab382c | |
Dennis Schwerdel | 7b0718399c | |
Dennis Schwerdel | 5bb0187726 | |
Dennis Schwerdel | eb638efadf | |
Dennis Schwerdel | f824c1f44d | |
dswd | 1b489a7cec | |
dependabot[bot] | f6199bf124 | |
Dennis Schwerdel | c1436ab23c | |
Dennis Schwerdel | 36f0b94514 | |
Dennis Schwerdel | 59ebac3e9d | |
Dennis Schwerdel | a4ec6e73c1 | |
dswd | 5c6b50ac48 | |
dependabot[bot] | 87b0035405 | |
dswd | ec25e30cb7 | |
dependabot[bot] | 57e5cb36ce | |
dswd | 287396fee2 | |
Dennis Schwerdel | af7a7f6a29 | |
dependabot[bot] | fcab3a31a4 | |
Dennis Schwerdel | 941ac62bac | |
Dennis Schwerdel | 1298ea5765 | |
Dennis Schwerdel | 3ef8753085 | |
Dennis Schwerdel | 31a5bfc335 | |
Dennis Schwerdel | 9750ce035c | |
Dennis Schwerdel | cd1b17e968 | |
Dennis Schwerdel | 923269d057 | |
dswd | 176e1956e6 | |
dependabot[bot] | ffb0fc2c0e | |
dswd | ea6f0f5bc8 | |
dependabot[bot] | 9bf61bfd4a | |
dswd | 7eb9cc00df | |
dswd | b2249035a5 | |
dswd | 1a68ec8259 | |
dswd | ec49371543 | |
dependabot[bot] | 8bcea472bb | |
dependabot[bot] | a813d09527 | |
dependabot[bot] | 69322c9fa1 | |
dswd | 3f4ba555a9 | |
dependabot[bot] | 757ed34f43 | |
dswd | 1062560fbc | |
dependabot[bot] | b8543a0b79 | |
dswd | 21b66e0a27 | |
dependabot[bot] | e417135c71 | |
dswd | 147ff4232e | |
dswd | d7f6c4107a | |
dswd | d5626bdae2 | |
dependabot[bot] | 79f50dd5e9 | |
dependabot[bot] | 34174ab5c5 | |
dependabot[bot] | a5664b1363 | |
dswd | b8974634c1 | |
dswd | 33615927f1 | |
dependabot[bot] | 07b021ce0d | |
dependabot[bot] | 00ce9e7cf6 | |
Dennis Schwerdel | 2fcf2c1e01 | |
Dennis Schwerdel | 86223fb4f9 | |
dswd | 58ce003cb6 | |
dependabot[bot] | 6deb9ca876 | |
dswd | 7bd7e7b7e8 | |
dependabot[bot] | 2bf4fad89d | |
Dennis Schwerdel | 75d5f2bdfa | |
Dennis Schwerdel | 12c899b04d | |
Dennis Schwerdel | 29fe0911f2 | |
dswd | a37be82e35 | |
dependabot[bot] | c6a38e14d6 | |
Dennis Schwerdel | 6e46af1c03 | |
Dennis Schwerdel | 348a6d07ba | |
dswd | cae435e4e1 | |
dependabot[bot] | bd253c5c65 | |
Dennis Schwerdel | bf808f9be4 | |
Dennis Schwerdel | df394a259b | |
dswd | 44d1083993 | |
dependabot[bot] | 0f2e870714 | |
Dennis Schwerdel | 4d992d29c8 | |
Dennis Schwerdel | 0cead0fd08 | |
Dennis Schwerdel | 94c80acae1 | |
dswd | 13a766112d | |
dependabot[bot] | 0fbf0bf085 | |
Dennis Schwerdel | 2fd100fd58 | |
dswd | 7eeed2f6c2 | |
dependabot[bot] | 1ca48d1f9a | |
Dennis Schwerdel | 2f5ce0b194 | |
dswd | 6b30f9e77c | |
dependabot[bot] | 3c163cdfc0 | |
dswd | 5f6a1f92b6 | |
dependabot[bot] | 0df12b9f70 | |
Dennis Schwerdel | 4337fd8de8 | |
Dennis Schwerdel | b31cbcd81a | |
Dennis Schwerdel | 801afa4d35 | |
Dennis Schwerdel | 57c993e8d5 | |
Dennis Schwerdel | 2238a04cd3 | |
Dennis Schwerdel | 686da48fe8 | |
Dennis Schwerdel | ef81d2901a | |
dswd | 6fcc730ee6 | |
dswd | 4bd87ee62c | |
dependabot[bot] | bdc5d9156e | |
dependabot[bot] | e7f3272748 | |
dswd | 688390577b | |
dswd | 2a58e67ac1 | |
dependabot[bot] | a7ec386950 | |
dswd | 1291c78c48 | |
dswd | 46df0be25d | |
dependabot[bot] | 03c757c3f0 | |
dependabot[bot] | 202d1dee2e | |
dswd | daa094baca | |
Dennis Schwerdel | 32a506be2f | |
Dennis Schwerdel | dfad73065c | |
Dennis Schwerdel | b6dafdaeee | |
Dennis Schwerdel | 717c360ef1 | |
Dennis Schwerdel | 689d00ba20 |
|
@ -1,14 +0,0 @@
|
|||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
objcopy = { path = "arm-linux-gnueabihf-objcopy" }
|
||||
strip = { path = "arm-linux-gnueabihf-strip" }
|
||||
|
||||
[target.arm-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
objcopy = { path = "arm-linux-gnueabihf-objcopy" }
|
||||
strip = { path = "arm-linux-gnueabihf-strip" }
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path = "aarch64-linux-gnu-objcopy" }
|
||||
strip = { path = "aarch64-linux-gnu-strip" }
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report a bug
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### Details of your setup
|
||||
|
||||
**VpnCloud version:**
|
||||
|
||||
**Description of your VPN setup**
|
||||
- How many nodes?
|
||||
- Any NAT involved?
|
||||
- Any custom routing?
|
||||
|
||||
**Config file contents**
|
||||
normally in `/etc/vpncloud/MYNET.net`
|
||||
:bangbang: make sure to mask (`***`) passwords and public IPs/hostnames :bangbang:
|
||||
|
||||
**Log entries**
|
||||
normally in `/var/log/vpncloud.MYNET.log`
|
|
@ -0,0 +1,8 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: VpnCloud Documentation
|
||||
url: https://vpncloud.ddswd.de/docs
|
||||
about: Please check the documentation before asking how things work.
|
||||
- name: Discussion Group on GitHub
|
||||
url: https://github.com/dswd/vpncloud/discussions
|
||||
about: Please discuss or ask anything there that is not an issue.
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
** Describe the high-level goal you want to achieve **
|
||||
|
||||
I would like to...
|
||||
|
||||
** Describe the proposed solution **
|
||||
|
||||
I propose to...
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
name: Other issue
|
||||
about: Any other issue
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe your issue
|
||||
|
||||
👉 Please use the [Discussion group](https://github.com/dswd/vpncloud/discussions) if you...
|
||||
* just want to ask a question
|
||||
* want to discuss a new feature
|
|
@ -6,14 +6,14 @@ RUN apt-get update \
|
|||
curl \
|
||||
gcc-aarch64-linux-gnu \
|
||||
gcc-arm-linux-gnueabihf \
|
||||
gcc-arm-linux-gnueabi \
|
||||
libc6-dev-arm64-cross \
|
||||
libc6-dev-armhf-cross \
|
||||
libc6-dev-armel-cross \
|
||||
libc6-dev-i386 \
|
||||
gcc-5-multilib \
|
||||
ruby-ronn \
|
||||
&& rm -rf /var/cache/dpkg
|
||||
|
||||
RUN ln -s asm-generic/ /usr/include/asm
|
||||
asciidoctor \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ADD entrypoint.sh /entrypoint.sh
|
||||
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
name: 'build-deb'
|
||||
description: 'Create deb packages'
|
||||
inputs:
|
||||
rust:
|
||||
description: Rust version
|
||||
default: 'stable'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
env:
|
||||
RUST: ${{ inputs.rust }}
|
||||
|
|
|
@ -2,32 +2,36 @@
|
|||
|
||||
set -e
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${RUST}
|
||||
TOOLCHAIN=$(grep -e '^toolchain =' Cargo.toml | sed -e 's/toolchain = "\(.*\)"/\1/')
|
||||
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
DEB_VERSION=$(echo "$VERSION" | sed -e 's/-/~/g')
|
||||
|
||||
ln -s asm-generic/ /usr/include/asm
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${TOOLCHAIN}
|
||||
source $HOME/.cargo/env
|
||||
|
||||
rustup target add i686-unknown-linux-gnu
|
||||
rustup target add armv5te-unknown-linux-gnueabi
|
||||
rustup target add armv7-unknown-linux-gnueabihf
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
cargo install cargo-deb
|
||||
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
|
||||
mkdir dist
|
||||
|
||||
build_deb() {
|
||||
ARCH=$1
|
||||
TARGET=$2
|
||||
cargo deb --target ${TARGET}
|
||||
cp target/${TARGET}/debian/vpncloud_${DEB_VERSION}_${ARCH}.deb dist/vpncloud_${DEB_VERSION}_${ARCH}.deb
|
||||
}
|
||||
|
||||
cargo deb
|
||||
cp target/debian/vpncloud_${VERSION}_amd64.deb dist/vpncloud_${VERSION}_amd64.deb
|
||||
|
||||
# i386 deb
|
||||
cargo deb --target i686-unknown-linux-gnu
|
||||
cp target/i686-unknown-linux-gnu/debian/vpncloud_${VERSION}_i386.deb dist/vpncloud_${VERSION}_i386.deb
|
||||
|
||||
# arm7hf deb
|
||||
cargo deb --target armv7-unknown-linux-gnueabihf
|
||||
cp target/armv7-unknown-linux-gnueabihf/debian/vpncloud_${VERSION}_armhf.deb dist/vpncloud_${VERSION}_armhf.deb
|
||||
|
||||
# aarch64 deb
|
||||
cargo deb --target aarch64-unknown-linux-gnu
|
||||
cp target/aarch64-unknown-linux-gnu/debian/vpncloud_${VERSION}_arm64.deb dist/vpncloud_${VERSION}_arm64.deb
|
||||
|
||||
cp target/debian/vpncloud_${DEB_VERSION}_amd64.deb dist/vpncloud_${DEB_VERSION}_amd64.deb
|
||||
|
||||
build_deb i386 i686-unknown-linux-gnu
|
||||
build_deb armhf armv7-unknown-linux-gnueabihf
|
||||
build_deb armel armv5te-unknown-linux-gnueabi
|
||||
build_deb arm64 aarch64-unknown-linux-gnu
|
|
@ -1,9 +1,12 @@
|
|||
FROM centos:7
|
||||
|
||||
RUN yum groupinstall -y 'Development Tools'
|
||||
RUN yum install -y ruby && gem install asciidoctor -v 2.0.10
|
||||
RUN yum install -y libstdc++-*.i686 \
|
||||
&& yum install -y glibc-*.i686 \
|
||||
&& yum install -y libgcc.i686
|
||||
|
||||
RUN yum-config-manager --add-repo http://springdale.math.ias.edu/data/puias/computational/7/x86_64 \
|
||||
&& yum install --nogpgcheck -y rubygem-ronn
|
||||
RUN ln -s /usr/bin/gcc /usr/bin/i686-linux-gnu-gcc
|
||||
|
||||
ADD entrypoint.sh /entrypoint.sh
|
||||
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
name: 'build-deb'
|
||||
description: 'Create deb packages'
|
||||
inputs:
|
||||
rust:
|
||||
description: Rust version
|
||||
default: 'stable'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
env:
|
||||
RUST: ${{ inputs.rust }}
|
||||
|
|
|
@ -2,7 +2,16 @@
|
|||
|
||||
set -e
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${RUST}
|
||||
TOOLCHAIN=$(grep -e '^toolchain =' Cargo.toml | sed -e 's/toolchain = "\(.*\)"/\1/')
|
||||
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
if echo "$VERSION" | fgrep -q "-"; then
|
||||
RPM_VERSION=$(echo "$VERSION" | sed -e 's/-/-0./g')
|
||||
else
|
||||
RPM_VERSION="$VERSION-1"
|
||||
fi
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${TOOLCHAIN}
|
||||
source $HOME/.cargo/env
|
||||
|
||||
rustup target add i686-unknown-linux-gnu
|
||||
|
@ -10,10 +19,23 @@ rustup target add armv7-unknown-linux-gnueabihf
|
|||
|
||||
cargo install cargo-rpm
|
||||
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
|
||||
mkdir dist
|
||||
|
||||
cargo build --release
|
||||
cargo rpm build
|
||||
cp target/release/rpmbuild/RPMS/x86_64/vpncloud-${VERSION}-1.x86_64.rpm dist/vpncloud_${VERSION}.x86_64.rpm
|
||||
cp target/release/rpmbuild/RPMS/x86_64/vpncloud-${RPM_VERSION}.x86_64.rpm dist/vpncloud_${RPM_VERSION}.x86_64.rpm
|
||||
|
||||
|
||||
build_rpm() {
|
||||
ARCH=$1
|
||||
TARGET=$2
|
||||
if ! [ -f dist/vpncloud_${RPM_VERSION}.${ARCH}.rpm ]; then
|
||||
mkdir -p target
|
||||
[ -L target/assets ] || ln -s ../assets target/assets
|
||||
[ -L target/target ] || ln -s ../target target/target
|
||||
cargo rpm build --target ${TARGET}
|
||||
cp target/${TARGET}/release/rpmbuild/RPMS/${ARCH}/vpncloud-${RPM_VERSION}.${ARCH}.rpm dist/vpncloud_${RPM_VERSION}.${ARCH}.rpm
|
||||
fi
|
||||
}
|
||||
|
||||
build_rpm i686 i686-unknown-linux-gnu
|
|
@ -0,0 +1,21 @@
|
|||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
curl \
|
||||
gcc-aarch64-linux-gnu \
|
||||
gcc-arm-linux-gnueabihf \
|
||||
gcc-arm-linux-gnueabi \
|
||||
libc6-dev-arm64-cross \
|
||||
libc6-dev-armhf-cross \
|
||||
libc6-dev-armel-cross \
|
||||
libc6-dev-i386 \
|
||||
gcc-5-multilib \
|
||||
asciidoctor \
|
||||
musl musl-dev musl-tools \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ADD entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT /entrypoint.sh
|
|
@ -0,0 +1,5 @@
|
|||
name: 'build-static'
|
||||
description: 'Create static binaries'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
TOOLCHAIN=$(grep -e '^toolchain =' Cargo.toml | sed -e 's/toolchain = "\(.*\)"/\1/')
|
||||
UPX_VERSION=$(grep -e '^upx_version =' Cargo.toml | sed -e 's/upx_version = "\(.*\)"/\1/')
|
||||
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
DEB_VERSION=$(echo "$VERSION" | sed -e 's/-/~/g')
|
||||
|
||||
ln -s asm-generic/ /usr/include/asm
|
||||
ln -s /usr/bin/aarch64-linux-gnu-gcc /usr/bin/aarch64-linux-musl-gcc
|
||||
ln -s /usr/bin/arm-linux-gnueabihf-gcc /usr/bin/arm-linux-musleabihf-gcc
|
||||
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${TOOLCHAIN}
|
||||
source $HOME/.cargo/env
|
||||
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
rustup target add i686-unknown-linux-musl
|
||||
rustup target add armv5te-unknown-linux-musleabi
|
||||
rustup target add armv7-unknown-linux-musleabihf
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
|
||||
curl https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -Lf | tar -xJ --strip-components=1 -C /usr/bin
|
||||
|
||||
mkdir dist
|
||||
|
||||
build_static() {
|
||||
ARCH=$1
|
||||
TARGET=$2
|
||||
cargo build --release --features installer --target ${TARGET} && upx --lzma target/${TARGET}/release/vpncloud
|
||||
cp target/${TARGET}/release/vpncloud ../dist/vpncloud_${VERSION}_static_${ARCH}
|
||||
}
|
||||
|
||||
build_static amd64 x86_64-unknown-linux-musl
|
||||
#build_static i386 i686-unknown-linux-musl
|
||||
build_static armhf armv7-unknown-linux-musleabihf
|
||||
build_static armel armv5te-unknown-linux-musleabi
|
||||
build_static arm64 aarch64-unknown-linux-musl
|
|
@ -0,0 +1,11 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
|
@ -6,7 +6,7 @@ jobs:
|
|||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -18,6 +18,9 @@ jobs:
|
|||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: szenius/set-timezone@v1.0
|
||||
with:
|
||||
timezoneLinux: Europe/Berlin
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
|
@ -26,25 +29,4 @@ jobs:
|
|||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Run cargo-tarpaulin
|
||||
uses: actions-rs/tarpaulin@v0.1
|
||||
with:
|
||||
version: '0.9.0'
|
||||
args: '-o Html -- --test-threads=1'
|
||||
- name: Archive code coverage results
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: code-coverage-report
|
||||
path: tarpaulin-report.html
|
||||
command: test
|
|
@ -1,45 +0,0 @@
|
|||
on:
|
||||
release:
|
||||
types: [created]
|
||||
name: Build packages
|
||||
jobs:
|
||||
deb:
|
||||
name: "Build deb packages"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run builder
|
||||
uses: ./.github/actions/build-deb
|
||||
with:
|
||||
rust: '1.40.0'
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: packages
|
||||
path: dist
|
||||
- name: Upload artifacts
|
||||
uses: skx/github-action-publish-binaries@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: 'dist/*.deb'
|
||||
rpm:
|
||||
name: "Build rpm packages"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run builder
|
||||
uses: ./.github/actions/build-rpm
|
||||
with:
|
||||
rust: '1.40.0'
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: packages
|
||||
path: dist
|
||||
- name: Upload artifacts
|
||||
uses: skx/github-action-publish-binaries@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: 'dist/*.rpm'
|
|
@ -9,3 +9,6 @@ dist
|
|||
builder/cache
|
||||
.idea
|
||||
release.sh
|
||||
__pycache__
|
||||
*.pem
|
||||
.vscode
|
|
@ -1,44 +0,0 @@
|
|||
%define __spec_install_post %{nil}
|
||||
%define __os_install_post %{_dbpath}/brp-compress
|
||||
%define debug_package %{nil}
|
||||
|
||||
Name: vpncloud
|
||||
Summary: Peer-to-peer VPN
|
||||
Version: @@VERSION@@
|
||||
Release: 1
|
||||
License: GPL-3.0
|
||||
Group: Applications/System
|
||||
Source0: %{name}-%{version}.tar.gz
|
||||
URL: https://vpncloud.ddswd.de
|
||||
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
||||
|
||||
%description
|
||||
%{summary}
|
||||
|
||||
%prep
|
||||
%setup -q
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
mkdir -p %{buildroot}
|
||||
mkdir -p %{buildroot}/etc/vpncloud
|
||||
mkdir -p %{buildroot}/lib/systemd/system
|
||||
mkdir -p %{buildroot}/usr/share/man/man1
|
||||
cp %{buildroot}/../../../../../assets/example.net.disabled %{buildroot}/etc/vpncloud/example.net.disabled
|
||||
cp %{buildroot}/../../../../../assets/vpncloud@.service %{buildroot}/lib/systemd/system/vpncloud@.service
|
||||
cp %{buildroot}/../../../../../target/vpncloud.1.gz %{buildroot}/usr/share/man/man1/vpncloud.1.gz
|
||||
cp -a * %{buildroot}
|
||||
|
||||
%clean
|
||||
rm -rf %{buildroot}
|
||||
|
||||
%files
|
||||
/etc/vpncloud
|
||||
/etc/vpncloud/example.net.disabled
|
||||
/usr/bin/vpncloud
|
||||
/lib/systemd/system/vpncloud@.service
|
||||
/usr/share/man/man1/vpncloud.1.gz
|
||||
|
||||
%defattr(-,root,root,-)
|
||||
%{_bindir}/*
|
84
CHANGELOG.md
84
CHANGELOG.md
|
@ -2,6 +2,90 @@
|
|||
|
||||
This project follows [semantic versioning](http://semver.org).
|
||||
|
||||
### UNRELEASED
|
||||
|
||||
- [changed] Changed Rust version to 1.75.0
|
||||
- [changed] Updated dependencies
|
||||
- [fixed] Fix error when IPv6 is not available
|
||||
|
||||
### v2.3.0 (2021-12-23)
|
||||
|
||||
- [added] Added build for armv5te (thanks to xek)
|
||||
- [added] Option to specify advertised addresses
|
||||
- [added] Peers now learn their own address from peers
|
||||
- [changed] Changed Rust version to 1.57.0
|
||||
- [changed] Updated dependencies
|
||||
- [fixed] Fixed problem with IPv4 addresses in listen option
|
||||
- [fixed] Fixed periodic broadcast messages in switch mode
|
||||
|
||||
### v2.2.0 (2021-04-06)
|
||||
|
||||
- [added] Service target file (thanks to mnhauke)
|
||||
- [added] Added interactive configuration wizard
|
||||
- [added] Support for (un-)installation
|
||||
- [added] Building static binaries
|
||||
- [added] Building i686 rpm
|
||||
- [changed] Restructured example config
|
||||
- [changed] Changed Rust version to 1.51.0
|
||||
- [changed] Updated dependencies
|
||||
- [changed] Change permissions of /etc/vpncloud
|
||||
|
||||
### v2.1.0 (2021-02-06)
|
||||
|
||||
- [added] Support for websocket proxy mode
|
||||
- [added] Support for hook scripts to handle certain situations
|
||||
- [added] Support for creating shell completions
|
||||
- [removed] Removed dummy device type
|
||||
- [changed] Updated dependencies
|
||||
- [changed] Changed Rust version to 1.49.0
|
||||
- [fixed] Added missing peer address propagation
|
||||
- [fixed] Fixed problem with peer addresses without port
|
||||
|
||||
### v2.0.1 (2020-11-07)
|
||||
|
||||
- [changed] Changed documentation
|
||||
- [changed] Updated dependencies
|
||||
- [changed] Retrying connections for 120 secs
|
||||
- [changed] Resetting own addresses periodically
|
||||
- [changed] Using smallvec everywhere
|
||||
- [changed] Assume default port for peers without port
|
||||
- [fixed] Fixed corner case with lost init message
|
||||
- [fixed] Do not reconnect to timed out pending connections
|
||||
- [fixed] Most specific claims beat less specific claims
|
||||
- [fixed] Count all invalid protocol traffic
|
||||
- [fixed] Fixed compile with musl
|
||||
- [fixed] Fixed time format in logs
|
||||
|
||||
### v2.0.0 (2020-10-30)
|
||||
|
||||
- [added] **Add strong crypto, complete rewrite of crypto system**
|
||||
- [added] Automatically claim addresses based on interface addresses (disable with --no-auto-claim)
|
||||
- [added] Allow to give --ip instead of ifup cmd
|
||||
- [added] Automatically set optimal MTU on interface
|
||||
- [added] Warning for disabled or loose rp_filter setting
|
||||
- [added] Add --fix-rp-filter to fix rp filter settings
|
||||
- [added] Offer to migrate old configs
|
||||
- [changed] **Complete change of network protocol**
|
||||
- [changed] Negotiate crypto method per peer, select best method
|
||||
- [changed] Make encryption the default, no encryption must be stated explicitly
|
||||
- [changed] Changed default device type to TUN
|
||||
- [changed] Rename subnet to claim
|
||||
- [changed] Set peer exchange interval to 5 minutes
|
||||
- [changed] Periodically send claims with peer list
|
||||
- [changed] Changed Rust version to 1.47.0
|
||||
- [removed] Remove network-id parameter
|
||||
- [removed] Remove port config option in favor of --listen
|
||||
|
||||
### UNRELEASED v1.x.y
|
||||
|
||||
- [added] Added crypto option AES128
|
||||
- [added] Default port for peers
|
||||
- [changed] Updated dependencies
|
||||
- [changed] Removed C code, now 100% Rust
|
||||
- [fixed] Fixed keepalive for small timeouts
|
||||
- [fixed] Fixed problem with port forwarding
|
||||
- [fixed] Fixed problem with TUN on dynamic host addresses
|
||||
|
||||
### v1.4.0 (2020-06-03)
|
||||
|
||||
- [added] Added option to listen on specified IP
|
||||
|
|
File diff suppressed because it is too large
Load Diff
81
Cargo.toml
81
Cargo.toml
|
@ -1,48 +1,65 @@
|
|||
[package]
|
||||
name = "vpncloud"
|
||||
version = "1.4.0"
|
||||
version = "2.4.0"
|
||||
authors = ["Dennis Schwerdel <schwerdel@googlemail.com>"]
|
||||
build = "build.rs"
|
||||
license = "GPL-3.0"
|
||||
description = "Peer-to-peer VPN"
|
||||
homepage = "https://vpncloud.ddswd.de"
|
||||
repository = "https://github.com/dswd/vpncloud"
|
||||
keywords = ["vpn", "p2p", "tun", "tap", "network"]
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata]
|
||||
toolchain = "1.75.0"
|
||||
upx_version = "4.2.2"
|
||||
|
||||
[dependencies]
|
||||
time = "0.2"
|
||||
chrono = { version = "0.4", features = ["std", "clock"], default_features = false}
|
||||
structopt = "0.3"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
signal = "0.7"
|
||||
libc = "0.2"
|
||||
rand = "0.7"
|
||||
rand = "0.8"
|
||||
fnv = "1"
|
||||
yaml-rust = "0.4"
|
||||
igd = { version = "0.11", optional = true }
|
||||
siphasher = "0.3"
|
||||
daemonize = "0.4"
|
||||
ring = "0.16"
|
||||
privdrop = "0.3"
|
||||
daemonize = "0.5"
|
||||
ring = "0.17"
|
||||
privdrop = "0.5"
|
||||
byteorder = "1.4"
|
||||
thiserror = "1.0"
|
||||
smallvec = "1.7"
|
||||
dialoguer = { version = "0.11", optional = true }
|
||||
tungstenite = { version = "0.21", optional = true }
|
||||
url = { version = "2.2", optional = true }
|
||||
igd = { version = "0.12", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "^1"
|
||||
pkg-config = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
iai = "0.1"
|
||||
|
||||
[features]
|
||||
default = ["nat"]
|
||||
bench = []
|
||||
default = ["nat", "websocket", "wizard"]
|
||||
nat = ["igd"]
|
||||
websocket = ["tungstenite", "url"]
|
||||
wizard = ["dialoguer"]
|
||||
installer = []
|
||||
|
||||
[[bench]]
|
||||
name = "criterion"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "valgrind"
|
||||
harness = false
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
|
||||
[profile.dev]
|
||||
lto = false
|
||||
|
@ -52,23 +69,33 @@ lto = false
|
|||
|
||||
[package.metadata.deb]
|
||||
extended-description = """\
|
||||
VpnCloud is a simple VPN over UDP. It creates a virtual network interface on
|
||||
the host and forwards all received data via UDP to the destination. VpnCloud
|
||||
establishes a fully-meshed VPN network in a peer-to-peer manner. It can work
|
||||
on TUN devices (IP based) and TAP devices (Ethernet based)."""
|
||||
VpnCloud is a high performance peer-to-peer mesh VPN over UDP supporting strong encryption,
|
||||
NAT traversal and a simple configuration. It establishes a fully-meshed self-healing VPN
|
||||
network in a peer-to-peer manner with strong end-to-end encryption based on elliptic curve
|
||||
keys and AES-256. VpnCloud creates a virtual network interface on the host and forwards all
|
||||
received data via UDP to the destination. It can work on TUN devices (IP based) and TAP
|
||||
devices (Ethernet based)."""
|
||||
license-file = ["LICENSE.md", "1"]
|
||||
changelog = "assets/changelog.txt"
|
||||
section = "net"
|
||||
depends = "libc6 (>= 2.23), libgcc1 (>= 1:6.0.1)"
|
||||
maintainer-scripts = "assets/deb-scripts"
|
||||
assets = [
|
||||
["target/release/vpncloud", "/usr/bin/vpncloud", "755"],
|
||||
["assets/example.net.disabled", "/etc/vpncloud/example.net.disabled", "600"],
|
||||
["assets/vpncloud@.service", "/lib/systemd/system/vpncloud@.service", "644"],
|
||||
["assets/vpncloud.target", "/lib/systemd/system/vpncloud.target", "644"],
|
||||
["assets/vpncloud-wsproxy.service", "/lib/systemd/system/vpncloud-wsproxy.service", "644"],
|
||||
["target/vpncloud.1.gz", "/usr/share/man/man1/vpncloud.1.gz", "644"]
|
||||
]
|
||||
|
||||
[package.metadata.rpm.cargo]
|
||||
buildflags = ["--release"]
|
||||
|
||||
[package.metadata.rpm.targets]
|
||||
vpncloud = { path = "/usr/bin/vpncloud" }
|
||||
[package.metadata.generate-rpm]
|
||||
assets = [
|
||||
{ source = "target/release/vpncloud", dest = "/usr/bin/vpncloud", mode = "755" },
|
||||
{ source = "assets/example.net.disabled", dest = "/etc/vpncloud/example.net.disabled", mode = "600" },
|
||||
{ source = "assets/vpncloud@.service", dest = "/lib/systemd/system/vpncloud@.service", mode = "644" },
|
||||
{ source = "assets/vpncloud.target", dest = "/lib/systemd/system/vpncloud.target", mode = "644" },
|
||||
{ source = "assets/vpncloud-wsproxy.service", dest = "/lib/systemd/system/vpncloud-wsproxy.service", mode = "644" },
|
||||
{ source = "target/vpncloud.1.gz", dest = "/usr/share/man/man1/vpncloud.1.gz", mode = "644" }
|
||||
]
|
||||
auto-req = "no"
|
701
LICENSE.md
701
LICENSE.md
File diff suppressed because it is too large
Load Diff
44
README.md
44
README.md
|
@ -3,37 +3,44 @@ VpnCloud - Peer-to-Peer VPN
|
|||
![Checks](https://github.com/dswd/vpncloud/workflows/Checks/badge.svg?branch=master)
|
||||
![Security audit](https://github.com/dswd/vpncloud/workflows/Security%20audit/badge.svg?branch=master)
|
||||
|
||||
**VpnCloud** is a simple VPN over UDP. It creates a virtual network interface on
|
||||
the host and forwards all received data via UDP to the destination. VpnCloud
|
||||
establishes a fully-meshed VPN network in a peer-to-peer manner. It can work
|
||||
on TUN devices (IP based) and TAP devices (Ethernet based). Tunneling traffic
|
||||
between two nodes can be as easy as:
|
||||
**VpnCloud** is a high performance peer-to-peer mesh VPN over UDP supporting strong encryption, NAT traversal and a simple configuration. It establishes a fully-meshed self-healing VPN network in a peer-to-peer manner with strong end-to-end encryption based on elliptic curve keys and AES-256. VpnCloud creates a virtual network interface on the host and forwards all received data via UDP to the destination. It can work on TUN devices (IP based) and TAP devices (Ethernet based).
|
||||
|
||||
$> vpncloud -c REMOTE_HOST:PORT --ifup 'ifconfig $IFNAME 10.0.0.1/24 mtu 1400 up'
|
||||
```sh
|
||||
$> vpncloud -c REMOTE_HOST:PORT -p 'mypassword' --ip 10.0.0.1/24
|
||||
```
|
||||
|
||||
For more information, please see the [Website](https://vpncloud.ddswd.de).
|
||||
or as config file:
|
||||
|
||||
```yaml
|
||||
crypto:
|
||||
password: mysecret
|
||||
ip: 10.0.0.1
|
||||
peers:
|
||||
- REMOTE_HOST:PORT
|
||||
```
|
||||
|
||||
For more information, please see the [Website](https://vpncloud.ddswd.de) or the [Discussions group](https://github.com/dswd/vpncloud/discussions).
|
||||
|
||||
|
||||
### Project Status
|
||||
This project is still [under development](CHANGELOG.md) but has reached a
|
||||
somewhat stable state. VpnCloud features the following functionality:
|
||||
|
||||
* Setting up tunnels between two networks via Ethernet (TAP) and IP (TUN)
|
||||
* Connecting multiple networks with multiple forwarding behaviors (Hub, Switch,
|
||||
Router)
|
||||
* Encrypted connections
|
||||
* Automatic peer-to-peer meshing, no central servers
|
||||
* NAT and (limited) firewall traversal using hole punching
|
||||
* Automatic reconnecting when connections are lost
|
||||
* Non-native forwarding modes, e.g. IP based learning switch and prefix routed
|
||||
Ethernet networks.
|
||||
* Connecting hundreds of nodes with the VPN
|
||||
* High throughput and low additional latency (see [performance page](https://vpncloud.ddswd.de/features/performance))
|
||||
* Support for tunneled VLans (TAP device)
|
||||
* Option to hide protocol header
|
||||
* Creating virtual network interfaces based on Ethernet (TAP) and IP (TUN)
|
||||
* Strong end-to-end encryption using Curve25519 key pairs and AES methods
|
||||
* Support for different forwarding/routing behaviors (Hub, Switch, Router)
|
||||
* NAT and firewall traversal using hole punching
|
||||
* Automatic port forwarding via UPnP
|
||||
* Websocket proxy mode for restrictive environments
|
||||
* Support for tunneled VLans (TAP devices)
|
||||
* Support for publishing [beacons](https://vpncloud.ddswd.de/docs/beacons) to help nodes find each others
|
||||
* Support for statsd monitoring
|
||||
|
||||
* Low memory footprint
|
||||
* Single binary, no dependencies, no kernel module
|
||||
|
||||
### Installing
|
||||
|
||||
|
@ -55,6 +62,9 @@ contributions are very welcome:
|
|||
|
||||
* **Linux packages**: VpnCloud is stable enough to be packaged for Linux
|
||||
distributions. Maintainers who want to package VpnCloud are very welcome.
|
||||
* **Help with other platforms**: If you are a Rust developer with experience
|
||||
on Windows or MacOS your help on porting VpnCloud to those platforms is very
|
||||
welcome.
|
||||
* **Security review**: The security has been implemented with strong security
|
||||
primitives but it would be great if a cryptography expert could verify the
|
||||
system.
|
||||
|
|
|
@ -1,3 +1,81 @@
|
|||
vpncloud (2.3.0) stable; urgency=medium
|
||||
|
||||
* [added] Added build for armv5te (thanks to xek)
|
||||
* [added] Option to specify advertised addresses
|
||||
* [added] Peers now learn their own address from peers
|
||||
* [changed] Changed Rust version to 1.57.0
|
||||
* [changed] Updated dependencies
|
||||
* [fixed] Fixed problem with IPv4 addresses in listen option
|
||||
* [fixed] Fixed periodic broadcast messages in switch mode
|
||||
|
||||
-- Dennis Schwerdel <schwerdel+vpncloud@googlemail.com> Thu, 23 Dec 2021 20:41:00 +0100
|
||||
|
||||
vpncloud (2.2.0) stable; urgency=medium
|
||||
|
||||
* [added] Service target file (thanks to mnhauke)
|
||||
* [added] Added interactive configuration wizard
|
||||
* [added] Support for (un-)installation
|
||||
* [added] Building static binaries
|
||||
* [added] Building i686 rpm
|
||||
* [changed] Restructured example config
|
||||
* [changed] Changed Rust version to 1.51.0
|
||||
* [changed] Updated dependencies
|
||||
* [changed] Change permissions of /etc/vpncloud
|
||||
|
||||
-- Dennis Schwerdel <schwerdel+vpncloud@googlemail.com> Tue, 06 Apr 2021 12:27:00 +0200
|
||||
|
||||
vpncloud (2.1.0) stable; urgency=medium
|
||||
|
||||
* [added] Support for websocket proxy mode
|
||||
* [added] Support for hook scripts to handle certain situations
|
||||
* [added] Support for creating shell completions
|
||||
* [removed] Removed dummy device type
|
||||
* [changed] Updated dependencies
|
||||
* [changed] Changed Rust version to 1.49.0
|
||||
* [fixed] Added missing peer address propagation
|
||||
* [fixed] Fixed problem with peer addresses without port
|
||||
|
||||
-- Dennis Schwerdel <schwerdel@googlemail.com> Sat, 06 Feb 2021 13:13:00 +0100
|
||||
|
||||
vpncloud (2.0.1) stable; urgency=medium
|
||||
|
||||
* [changed] Changed documentation
|
||||
* [changed] Updated dependencies
|
||||
* [changed] Retrying connections for 120 secs
|
||||
* [changed] Resetting own addresses periodically
|
||||
* [changed] Using smallvec everywhere
|
||||
* [changed] Assume default port for peers without port
|
||||
* [fixed] Fixed corner case with lost init message
|
||||
* [fixed] Do not reconnect to timed out pending connections
|
||||
* [fixed] Most specific claims beat less specific claims
|
||||
* [fixed] Count all invalid protocol traffic
|
||||
* [fixed] Fixed compile with musl
|
||||
* [fixed] Fixed time format in logs
|
||||
|
||||
-- Dennis Schwerdel <schwerdel@googlemail.com> Sat, 07 Nov 2020 12:28:00 +0100
|
||||
|
||||
vpncloud (2.0.0) stable; urgency=medium
|
||||
|
||||
* [added] **Add strong crypto, complete rewrite of crypto system**
|
||||
* [added] Automatically claim addresses based on interface addresses (disable with --no-auto-claim)
|
||||
* [added] Allow to give --ip instead of ifup cmd
|
||||
* [added] Automatically set optimal MTU on interface
|
||||
* [added] Warning for disabled or loose rp_filter setting
|
||||
* [added] Add --fix-rp-filter to fix rp filter settings
|
||||
* [added] Offer to migrate old configs
|
||||
* [changed] **Complete change of network protocol**
|
||||
* [changed] Negotiate crypto method per peer, select best method
|
||||
* [changed] Make encryption the default, no encryption must be stated explicitly
|
||||
* [changed] Changed default device type to TUN
|
||||
* [changed] Rename subnet to claim
|
||||
* [changed] Set peer exchange interval to 5 minutes
|
||||
* [changed] Periodically send claims with peer list
|
||||
* [changed] Changed Rust version to 1.47.0
|
||||
* [removed] Remove network-id parameter
|
||||
* [removed] Remove port config option in favor of --listen
|
||||
|
||||
-- Dennis Schwerdel <schwerdel@googlemail.com> Fri, 30 Oct 2020 22:07:00 +0100
|
||||
|
||||
vpncloud (1.4.0) stable; urgency=medium
|
||||
|
||||
* [added] Added option to listen on specified IP
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
systemctl daemon-reload
|
||||
chmod 700 /etc/vpncloud
|
|
@ -1,88 +1,82 @@
|
|||
# This configuration file uses the YAML format.
|
||||
|
||||
# This configuration can be enabled/disabled and controlled by adding the
|
||||
# network to `/etc/default/vpncloud` and starting/stopping it via
|
||||
# `/etc/init.d/vpncloud start/stop` on non-systemd systems and via
|
||||
# `systemctl enable/disable vpncloud@NAME` and
|
||||
# `service vpncloud@NAME start/stop` on systemd systems.
|
||||
# ~ means "no value" (i.e. "default value")
|
||||
# Replace it by a value and put quotes (") around values with special characters
|
||||
# List items start with a dash and a space (- )
|
||||
# Note that the whitespace before the settings names is important for the file structure
|
||||
|
||||
|
||||
# The port number on which to listen for data.
|
||||
# Note: Every VPN needs a different port number.
|
||||
#port: 3210
|
||||
listen: 3210 # The port number or ip:port on which to listen for data.
|
||||
|
||||
# Address of a peer to connect to. The address should be in the form
|
||||
# `addr:port`. If the node is not started, the connection will be retried
|
||||
# periodically. This parameter can be repeated to connect to multiple peers.
|
||||
# Note: Several entries can be separated by spaces.
|
||||
#peers:
|
||||
# - node2.example.com:3210
|
||||
# - node3.example.com:3210
|
||||
peers: # Address of a peer to connect to.
|
||||
# The address should be in the form `addr:port`.
|
||||
# Put [] for an empty list
|
||||
- node2.example.com:3210
|
||||
- node3.example.com:3210
|
||||
|
||||
# Peer timeout in seconds. The peers will exchange information periodically
|
||||
# and drop peers that are silent for this period of time.
|
||||
#peer_timeout: 600
|
||||
crypto: # Crypto settings
|
||||
password: ~ # <-- CHANGE # A password to encrypt the VPN data.
|
||||
private-key: ~ # Private key (alternative to password)
|
||||
public-key: ~ # Public key (alternative to password)
|
||||
trusted-keys: [] # Trusted keys (alternative to password)
|
||||
# Replace [] with list of keys
|
||||
|
||||
# Switch table entry timeout in seconds. This parameter is only used in switch
|
||||
# mode. Addresses that have not been seen for the given period of time will
|
||||
# be forgot.
|
||||
#dst_timeout: 300
|
||||
ip: ~ # <-- CHANGE # An IP address to set on the device, e.g. 10.0.0.1
|
||||
# Must be different for every node on the VPN
|
||||
|
||||
# An optional token that identifies the network and helps to distinguish it
|
||||
# from other networks.
|
||||
#magic: "76706e01"
|
||||
|
||||
# An optional shared key to encrypt the VPN data. If this option is not set,
|
||||
# the traffic will be sent unencrypted.
|
||||
#shared_key: ""
|
||||
# ------------------ Advanced features ahead --------------------
|
||||
|
||||
# The encryption method to use ("aes256", or "chacha20"). Most current CPUs
|
||||
# have special support for AES256 so this should be faster. For older
|
||||
# computers lacking this support, only CHACHA20 is supported.
|
||||
#crypto: chacha20
|
||||
auto-claim: true # Whether to automatically claim the configured IP on tun devices
|
||||
|
||||
# Name of the virtual device. Any `%d` will be filled with a free number.
|
||||
#device_name: "vpncloud%d"
|
||||
|
||||
# Set the type of network. There are two options: **tap** devices process
|
||||
# Ethernet frames **tun** devices process IP packets. [default: `tap`]
|
||||
#device_type: tap
|
||||
|
||||
# The mode of the VPN. The VPN can like a router, a switch or a hub. A **hub**
|
||||
# will send all data always to all peers. A **switch** will learn addresses
|
||||
# from incoming data and only send data to all peers when the address is
|
||||
# unknown. A **router** will send data according to known subnets of the
|
||||
# peers and ignore them otherwise. The **normal** mode is switch for tap
|
||||
# devices and router for tun devices. [default: `normal`]
|
||||
#mode: normal
|
||||
|
||||
# The local subnets to use. This parameter should be in the form
|
||||
# `address/prefixlen` where address is an IPv4 address, an IPv6 address, or a
|
||||
# MAC address. The prefix length is the number of significant front bits that
|
||||
# distinguish the subnet from other subnets. Example: `10.1.1.0/24`.
|
||||
# Note: Several entries can be separated by spaces.
|
||||
#subnets:
|
||||
claims: # The local subnets to use. This parameter should be in the form
|
||||
# `address/prefixlen` where address is an IPv4 address, an IPv6 address, or a
|
||||
# MAC address. The prefix length is the number of significant front bits that
|
||||
# distinguish the subnet from other subnets.
|
||||
# - 10.1.1.0/24
|
||||
|
||||
# A command to setup the network interface. The command will be run (as
|
||||
# parameter to `sh -c`) when the device has been created to configure it.
|
||||
# The name of the allocated device will be available via the environment
|
||||
# variable `IFNAME`.
|
||||
#ifup: ""
|
||||
ifup: ~ # Command to setup the interface. Use $IFNAME for interface name.
|
||||
ifdown: ~ # Command to tear down the interface. Use $IFNAME for interface name.
|
||||
|
||||
# A command to bring down the network interface. The command will be run (as
|
||||
# parameter to `sh -c`) to remove any configuration from the device.
|
||||
# The name of the allocated device will be available via the environment
|
||||
# variable `IFNAME`.
|
||||
#ifdown: ""
|
||||
device: # Device settings
|
||||
name: "vpncloud%d" # Name of the virtual device. Any `%d` will be filled with a free number.
|
||||
type: tun # Set the type of network. There are two options: **tap** devices process
|
||||
# Ethernet frames **tun** devices process IP packets. [default: `tun`]
|
||||
path: "/dev/net/tun" # Path of the tun device
|
||||
fix-rp-filter: false # Whether to fix detected rp-filter problems
|
||||
|
||||
# Store the process id in this file when running in the background. If set,
|
||||
# the given file will be created containing the process id of the new
|
||||
# background process. This option is only used when running in background.
|
||||
#pid_file: ""
|
||||
mode: normal # Mode to run in, "normal", "hub", "switch", or "router" (see manpage)
|
||||
|
||||
# Change the user and/or group of the process once all the setup has been
|
||||
# done and before spawning the background process. This option is only used
|
||||
# when running in background.
|
||||
#user: ""
|
||||
#group: ""
|
||||
port-forwarding: true # Try to map a port on the router
|
||||
|
||||
switch-timeout: 300 # Switch timeout in seconds (switch mode only)
|
||||
|
||||
peer-timeout: 300 # Peer timeout in seconds
|
||||
keepalive: ~ # Keepalive interval in seconds
|
||||
|
||||
beacon: # Beacon settings
|
||||
store: ~ # File or command (prefix: "|") to use for storing beacons
|
||||
load: ~ # File or command (prefix: "|") to use for loading beacons
|
||||
interval: 3600 # How often to load and store beacons (in seconds)
|
||||
password: ~ # Password to encrypt beacon data with
|
||||
|
||||
statsd: # Statsd settings
|
||||
server: ~ # Statsd server name:port
|
||||
prefix: ~ # Prefix to use for stats keys
|
||||
|
||||
pid-file: ~ # Store the process id in this file when running in the background
|
||||
stats-file: ~ # Periodically write statistics on peers and current traffic to the given file
|
||||
|
||||
hook: ~ # Hook script to run for every event
|
||||
hooks: {} # Multiple hook scripts to run for specific events
|
||||
|
||||
|
||||
|
||||
# Copy this template and save it to a file named /etc/vpncloud/MYNET.net (replace MYNET with your network name)
|
||||
#
|
||||
# On systems using systemd (most common):
|
||||
# start/stop the network: service vpncloud@MYNET start/stop
|
||||
# enable/disable automatic startup: systemctl enable/disable vpncloud@MYNET
|
||||
#
|
||||
# On older systems (using sysv init):
|
||||
# Add the network name to /etc/default/vpncloud
|
||||
# start/stop all VpnCloud networks: /etc/init.d/vpncloud start/stop
|
|
@ -0,0 +1,22 @@
|
|||
[Unit]
|
||||
Description=VpnCloud websocket proxy
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Documentation=man:vpncloud(1)
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/vpncloud ws-proxy -l 3210
|
||||
RestartSec=5s
|
||||
Restart=on-failure
|
||||
TasksMax=10
|
||||
MemoryMax=50M
|
||||
PrivateTmp=yes
|
||||
ProtectHome=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/log /run
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SYS_CHROOT
|
||||
DeviceAllow=/dev/null rw
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -1,860 +0,0 @@
|
|||
'\" t
|
||||
.\" Title: vpncloud
|
||||
.\" Author: [see the "AUTHORS" section]
|
||||
.\" Generator: Asciidoctor 1.5.5
|
||||
.\" Date: 2020-06-03
|
||||
.\" Manual: \ \&
|
||||
.\" Source: \ \&
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "VPNCLOUD" "1" "2020-06-03" "\ \&" "\ \&"
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.ss \n[.ss] 0
|
||||
.nh
|
||||
.ad l
|
||||
.de URL
|
||||
\\$2 \(laURL: \\$1 \(ra\\$3
|
||||
..
|
||||
.if \n[.g] .mso www.tmac
|
||||
.LINKSTYLE blue R < >
|
||||
.SH "NAME"
|
||||
vpncloud \- Peer\-to\-peer VPN
|
||||
.SH "SYNOPSIS"
|
||||
.sp
|
||||
\fBvpncloud [options] [\-\-config <file>] [\-t <type>] [\-d <name>] [\-l <addr>] [\-c <addr>...]\fP
|
||||
.SH "OPTIONS"
|
||||
.sp
|
||||
\fB\-\-config <file>\fP
|
||||
.RS 4
|
||||
Read configuration options from the specified file. Please see the section
|
||||
\fBCONFIG FILES\fP for documentation on the file format.
|
||||
If the same option is defined in the config file and as a parameter, the
|
||||
parameter overrides the config file.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-t <type>\fP, \fB\-\-type <type>\fP
|
||||
.RS 4
|
||||
Set the type of network. There are two options: \fBtap\fP devices process
|
||||
Ethernet frames \fBtun\fP devices process IP packets. [default: \fBtap\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-d <name>\fP, \fB\-\-device <name>\fP
|
||||
.RS 4
|
||||
Name of the virtual device. Any \fB%d\fP will be filled with a free number.
|
||||
[default: \fBvpncloud%d\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-device\-path <path>\fP
|
||||
.RS 4
|
||||
The path of the base device inode, e.g. /dev/net/run.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-m <mode>\fP, \fB\-\-mode <mode>\fP
|
||||
.RS 4
|
||||
The mode of the VPN. The VPN can like a router, a switch or a hub. A \fBhub\fP
|
||||
will send all data always to all peers. A \fBswitch\fP will learn addresses
|
||||
from incoming data and only send data to all peers when the address is
|
||||
unknown. A \fBrouter\fP will send data according to known subnets of the
|
||||
peers and ignore them otherwise. The \fBnormal\fP mode is switch for tap
|
||||
devices and router for tun devices. [default: \fBnormal\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-l <addr>\fP, \fB\-\-listen <addr>\fP
|
||||
.RS 4
|
||||
The address on which to listen for data. This can be simply a port number
|
||||
or a full address in form IP:PORT. If the IP is specified as \(aq\(rs*\(aq or only
|
||||
a port number is given, then the socket will listen on all IPs (v4 and v6),
|
||||
otherwise the socket will only listen on the given IP. [default: \fB3210\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-c <addr>\fP, \fB\-\-connect <addr>\fP
|
||||
.RS 4
|
||||
Address of a peer to connect to. The address should be in the form
|
||||
\fBaddr:port\fP. If the node is not started, the connection will be retried
|
||||
periodically. This parameter can be repeated to connect to multiple peers.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-s <subnet>\fP, \fB\-\-subnet <subnet>\fP:
|
||||
The local subnets to use. This parameter should be in the form
|
||||
\fBaddress/prefixlen\fP where address is an IPv4 address, an IPv6 address, or a
|
||||
MAC address. The prefix length is the number of significant front bits that
|
||||
distinguish the subnet from other subnets. Example: \fB10.1.1.0/24\fP.
|
||||
.sp
|
||||
\fB\-\-shared\-key <key>\fP
|
||||
.RS 4
|
||||
An optional shared key to encrypt the VPN data. If this option is not set,
|
||||
the traffic will be sent unencrypted.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-crypto <method>\fP
|
||||
.RS 4
|
||||
The encryption method to use ("aes256", or "chacha20"). Most current CPUs
|
||||
have special support for AES256 so this should be faster. For older
|
||||
computers lacking this support, only CHACHA20 is supported.
|
||||
[default: \fBchacha20\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-magic <id>\fP
|
||||
.RS 4
|
||||
Override the 4\-byte magic header of each packet. This header identifies the
|
||||
network and helps to distinguish it from other networks and other
|
||||
applications. The id can either be a 4 byte / 8 character hexadecimal
|
||||
string or an arbitrary string prefixed with "hash:" which will then be
|
||||
hashed into 4 bytes.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-peer\-timeout <secs>\fP
|
||||
.RS 4
|
||||
Peer timeout in seconds. The peers will exchange information periodically
|
||||
and drop peers that are silent for this period of time. [default: \fB600\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-keepalive <secs>\fP
|
||||
.RS 4
|
||||
Interval of peer exchange messages in seconds. The peers will exchange
|
||||
information periodically to keep connections alive. This setting overrides
|
||||
how often this will happen. [default: \fBpeer\-timeout/2\-60\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-dst\-timeout <secs>\fP
|
||||
.RS 4
|
||||
Switch table entry timeout in seconds. This parameter is only used in switch
|
||||
mode. Addresses that have not been seen for the given period of time will
|
||||
be forgotten. [default: \fB300\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-beacon\-store <path|command>\fP
|
||||
.RS 4
|
||||
Periodically store beacons containing the address of this node in the given
|
||||
file or via the given command. If the parameter value starts with a pipe
|
||||
character (\fB|\fP), the rest of the value is interpreted as a shell command.
|
||||
Otherwise the value is interpreted as a file to write the beacon to.
|
||||
If this parameter is not given, beacon storage is disabled.
|
||||
Please see the section \fBBEACONS\fP for more information.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-beacon\-load <path|command>\fP
|
||||
.RS 4
|
||||
Periodically load beacons containing the addresses of other nodes from the
|
||||
given file or via the given command. If the parameter value starts with a
|
||||
pipe character (\fB|\fP), the rest of the value is interpreted as a shell
|
||||
command. Otherwise the value is interpreted as a file to read the beacon
|
||||
from.
|
||||
If this parameter is not given, beacon loading is disabled.
|
||||
Please see the section \fBBEACONS\fP for more information.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-beacon\-interval <secs>\fP
|
||||
.RS 4
|
||||
Beacon storage/loading interval in seconds. If configured to do so via
|
||||
\fB\-\-beacon\-store\fP and \fB\-\-beacon\-load\fP, the node will periodically store its
|
||||
beacon and load beacons of other nodes. This parameter defines the interval
|
||||
in seconds. [default: \fB3600\fP]
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-ifup <command>\fP
|
||||
.RS 4
|
||||
A command to setup the network interface. The command will be run (as
|
||||
parameter to \fBsh \-c\fP) when the device has been created to configure it.
|
||||
The name of the allocated device will be available via the environment
|
||||
variable \fBIFNAME\fP.
|
||||
Please note that this command is executed with the full permissions of the
|
||||
caller.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-ifdown <command>\fP
|
||||
.RS 4
|
||||
A command to bring down the network interface. The command will be run (as
|
||||
parameter to \fBsh \-c\fP) to remove any configuration from the device.
|
||||
The name of the allocated device will be available via the environment
|
||||
variable \fBIFNAME\fP.
|
||||
Please note that this command is executed with the (limited) permissions of
|
||||
the user and group given as \fB\-\-user\fP and \fB\-\-group\fP.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-pid\-file <file>\fP
|
||||
.RS 4
|
||||
Store the process id in this file when running in the background. If set,
|
||||
the given file will be created containing the process id of the new
|
||||
background process. This option is only used when running in background.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-user <user>\fP, \fB\-\-group <group>\fP
|
||||
.RS 4
|
||||
Change the user and/or group of the process once all the setup has been
|
||||
done.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-log\-file <file>\fP
|
||||
.RS 4
|
||||
If set, print logs also to the given file. The file will be created and
|
||||
truncated if is exists.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-stats\-file <file>\fP
|
||||
.RS 4
|
||||
If set, periodically write statistics on peers and current traffic to the
|
||||
given file. The file will be periodically overwritten with new data.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-daemon\fP
|
||||
.RS 4
|
||||
Spawn a background process instead of running the process in the foreground.
|
||||
If this flag is set, the process will first carry out all the
|
||||
initialization, then drop permissions if \fB\-\-user\fP or \fB\-\-group\fP is used and
|
||||
then spawn a background process and write its process id to a file if
|
||||
\fB\-\-pid\-file\fP is set. Then, the main process will exit and the background
|
||||
process continues to provide the VPN. At the time, when the main process
|
||||
exits, the interface exists and is properly configured to be used.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-no\-port\-forwarding\fP
|
||||
.RS 4
|
||||
Disable automatic port forward. If this option is not set, VpnCloud tries to
|
||||
detect a NAT router and automatically add a port forwarding to it.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-v\fP, \fB\-\-verbose\fP
|
||||
.RS 4
|
||||
Print debug information, including information for data being received and
|
||||
sent.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-q\fP, \fB\-\-quiet\fP
|
||||
.RS 4
|
||||
Only print errors and warnings.
|
||||
.RE
|
||||
.sp
|
||||
\fB\-h\fP, \fB\-\-help\fP
|
||||
.RS 4
|
||||
Display the help.
|
||||
.RE
|
||||
.SH "DESCRIPTION"
|
||||
.sp
|
||||
\fBVpnCloud\fP is a simple VPN over UDP. It creates a virtual network interface on
|
||||
the host and forwards all received data via UDP to the destination. It can work
|
||||
in 3 different modes:
|
||||
.sp
|
||||
\fBSwitch mode\fP
|
||||
.RS 4
|
||||
In this mode, the VPN will dynamically learn addresses
|
||||
as they are used as source addresses and use them to forward data to its
|
||||
destination. Addresses that have not been seen for some time
|
||||
(option \fBdst_timeout\fP) will be forgotten. Data for unknown addresses will be
|
||||
broadcast to all peers. This mode is the default mode for TAP devices that
|
||||
process Ethernet frames but it can also be used with TUN devices and IP
|
||||
packets.
|
||||
.RE
|
||||
.sp
|
||||
\fBHub mode\fP
|
||||
.RS 4
|
||||
In this mode, all data will always be broadcast to all peers.
|
||||
This mode uses lots of bandwidth and should only be used in special cases.
|
||||
.RE
|
||||
.sp
|
||||
\fBRouter mode\fP
|
||||
.RS 4
|
||||
In this mode, data will be forwarded based on preconfigured
|
||||
address ranges ("subnets"). Data for unknown nodes will be silently ignored.
|
||||
This mode is the default mode for TUN devices that work with IP packets but
|
||||
it can also be used with TAP devices and Ethernet frames.
|
||||
.RE
|
||||
.sp
|
||||
All connected VpnCloud nodes will form a peer\-to\-peer network and cross\-connect
|
||||
automatically until the network is fully connected. The nodes will periodically
|
||||
exchange information with the other nodes to signal that they are still active
|
||||
and to allow the automatic cross\-connect behavior. There are some important
|
||||
things to note:
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04' 1.\h'+01'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP " 1." 4.2
|
||||
.\}
|
||||
To avoid that different networks that reuse each others addresses merge due
|
||||
to the cross\-connect behavior, the \fBmagic\fP option can be used and set
|
||||
to any unique string to identify the network. The \fBmagic\fP must be the
|
||||
same on all nodes of the same VPN network.
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04' 2.\h'+01'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP " 2." 4.2
|
||||
.\}
|
||||
The cross\-connect behavior can be able to connect nodes that are behind
|
||||
firewalls or NATs as it can function as hole\-punching.
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04' 3.\h'+01'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP " 3." 4.2
|
||||
.\}
|
||||
The management traffic will increase with the peer number quadratically.
|
||||
It should still be reasonably small for high node numbers (below 10 KiB/s
|
||||
for 10.000 nodes). A longer \fBpeer_timeout\fP can be used to reduce the traffic
|
||||
further. For high node numbers, router mode should be used as it never
|
||||
broadcasts data.
|
||||
.RE
|
||||
.sp
|
||||
VpnCloud does not implement any loop\-avoidance. Since data received on the UDP
|
||||
socket will only be sent to the local network interface and vice versa, VpnCloud
|
||||
cannot produce loops on its own. On the TAP device, however STP data can be
|
||||
transported to avoid loops caused by other network components.
|
||||
.sp
|
||||
For TAP devices, IEEE 802.1q frames (VLAN tagged) are detected and forwarded
|
||||
based on separate MAC tables. Any nested tags (Q\-in\-Q) will be ignored.
|
||||
.SH "EXAMPLES"
|
||||
.SS "Switched TAP scenario"
|
||||
.sp
|
||||
In the example scenario, a simple layer 2 network tunnel is established. Most
|
||||
likely those commands need to be run as \fBroot\fP using \fBsudo\fP.
|
||||
.sp
|
||||
First, VpnCloud need to be started on both nodes (the address after \fB\-c\fP is the
|
||||
address of the remote node and the the \fBX\fP in the interface address must be
|
||||
unique among all nodes, e.g. 0, 1, 2, ...):
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
vpncloud \-c REMOTE_HOST:PORT \-\-ifup \(aqifconfig $IFNAME 10.0.0.X/24 mtu 1400 up\(aq
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.sp
|
||||
Afterwards, the interface can be used to communicate.
|
||||
.SS "Routed TUN example"
|
||||
.sp
|
||||
In this example, 2 nodes and their subnets should communicate using IP.
|
||||
First, VpnCloud need to be started on both nodes:
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
vpncloud \-t tun \-c REMOTE_HOST:PORT \-\-subnet 10.0.X.0/24 \-\-ifup \(aqifconfig $IFNAME 10.0.X.1/16 mtu 1400 up\(aq
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.sp
|
||||
It is important to configure the interface in a way that all addresses on the
|
||||
VPN can be reached directly. E.g. if subnets 10.0.1.0/24, 10.0.2.0/24 and so on
|
||||
are used, the interface needs to be configured as 10.0.1.1/16.
|
||||
For TUN devices, this means that the prefix length of the subnets
|
||||
(/24 in this example) must be different than the prefix length that the
|
||||
interface is configured with (/16 in this example).
|
||||
.SS "Important notes"
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04' 1.\h'+01'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP " 1." 4.2
|
||||
.\}
|
||||
VpnCloud can be used to connect two separate networks. TAP networks can be
|
||||
bridged using \fBbrctl\fP and TUN networks must be routed. It is very important
|
||||
to be careful when setting up such a scenario in order to avoid network loops,
|
||||
security issues, DHCP issues and many more problems.
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04' 2.\h'+01'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP " 2." 4.2
|
||||
.\}
|
||||
TAP devices will forward DHCP data. If done intentionally, this can be used
|
||||
to assign unique addresses to all participants. If this happens accidentally,
|
||||
it can conflict with DHCP servers of the local network and can have severe
|
||||
side effects.
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04' 3.\h'+01'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP " 3." 4.2
|
||||
.\}
|
||||
VpnCloud is not designed for high security use cases. Although the used crypto
|
||||
primitives are expected to be very secure, their application has not been
|
||||
reviewed.
|
||||
The shared key is hashed using \fIScryptSalsa208Sha256\fP to derive a key,
|
||||
which is used to encrypt the payload of messages using \fIChaCha20Poly1305\fP or
|
||||
\fIAES256\-GCM\fP. The encryption includes an authentication that also protects the
|
||||
header.
|
||||
This method does only protect against attacks on single messages but not
|
||||
against attacks that manipulate the message series itself (i.e. suppress
|
||||
messages, reorder them, or duplicate them).
|
||||
.RE
|
||||
.SH "CONFIG FILES"
|
||||
.sp
|
||||
The config file is a YAML file that contains configuration values. All entries
|
||||
are optional and override the defaults. Please see the section \fBOPTIONS\fP for
|
||||
detailed descriptions of the options.
|
||||
.sp
|
||||
\fBdevice_type\fP
|
||||
.RS 4
|
||||
Set the type of network. Same as \fB\-\-type\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBdevice_name\fP
|
||||
.RS 4
|
||||
Name of the virtual device. Same as \fB\-\-device\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBdevice_path\fP
|
||||
.RS 4
|
||||
Set the path of the base device. Same as \fB\-\-device\-path\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBifup\fP
|
||||
.RS 4
|
||||
A command to setup the network interface. Same as \fB\-\-ifup\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBifdown\fP
|
||||
.RS 4
|
||||
A command to bring down the network interface. Same as \fB\-\-ifdown\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBcrypto\fP
|
||||
.RS 4
|
||||
The encryption method to use. Same as \fB\-\-crypto\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBshared_key\fP
|
||||
.RS 4
|
||||
The shared key to encrypt all traffic. Same as \fB\-\-shared\-key\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBmagic\fP
|
||||
.RS 4
|
||||
Override the 4\-byte magic header of each packet. Same as \fB\-\-magic\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBport\fP
|
||||
.RS 4
|
||||
A port number to listen on. This option is DEPRECATED.
|
||||
.RE
|
||||
.sp
|
||||
\fBlisten\fP
|
||||
.RS 4
|
||||
The address on which to listen for data. Same as \fB\-\-listen\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBpeers\fP
|
||||
.RS 4
|
||||
A list of addresses to connect to. See \fB\-\-connect\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBpeer_timeout\fP
|
||||
.RS 4
|
||||
Peer timeout in seconds. Same as\fB\-\-peer\-timeout\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBbeacon_store\fP
|
||||
.RS 4
|
||||
Path or command to store beacons. Same as \fB\-\-beacon\-store\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBbeacon_load\fP
|
||||
.RS 4
|
||||
Path or command to load beacons. Same as \fB\-\-beacon\-load\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBbeacon_interval\fP
|
||||
.RS 4
|
||||
Interval for loading and storing beacons in seconds. Same as \fB\-\-beacon\-interval\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBmode\fP
|
||||
.RS 4
|
||||
The mode of the VPN. Same as \fB\-\-mode\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBdst_timeout\fP
|
||||
.RS 4
|
||||
Switch table entry timeout in seconds. Same as \fB\-\-dst\-timeout\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBsubnets\fP
|
||||
.RS 4
|
||||
A list of local subnets to use. See \fB\-\-subnet\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBport_forwarding\fP
|
||||
.RS 4
|
||||
Whether to activate port forwardig. See \fB\-\-no\-port\-forwarding\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBuser\fP
|
||||
.RS 4
|
||||
The name of a user to run the background process under. See \fB\-\-user\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBgroup\fP
|
||||
.RS 4
|
||||
The name of a group to run the background process under. See \fB\-\-group\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBpid_file\fP
|
||||
.RS 4
|
||||
The path of the pid file to create. See \fB\-\-pid\-file\fP
|
||||
.RE
|
||||
.sp
|
||||
\fBstats_file\fP
|
||||
.RS 4
|
||||
The path of the statistics file. See \fB\-\-stats\-file\fP
|
||||
.RE
|
||||
.SS "Example"
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
device_type: tun
|
||||
device_name: vpncloud%d
|
||||
ifup: ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up
|
||||
crypto: aes256
|
||||
shared_key: mysecret
|
||||
listen: 3210
|
||||
peers:
|
||||
\- remote.machine.foo:3210
|
||||
\- remote.machine.bar:3210
|
||||
peer_timeout: 600
|
||||
mode: normal
|
||||
subnets:
|
||||
\- 10.0.1.0/24
|
||||
port_forwarding: true
|
||||
user: nobody
|
||||
group: nogroup
|
||||
pid_file: /run/vpncloud.pid
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.SH "BEACONS"
|
||||
.sp
|
||||
Beacons are short character sequences that contain a timestamp and a list of
|
||||
addresses. They can be published and retrieved by other nodes to find peers
|
||||
without the need for static addresses.
|
||||
.sp
|
||||
The beacons are short (less than 100 characters), encrypted and encoded with
|
||||
printable characters to allow publishing them in various places on the
|
||||
internet, e.g.:
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
On shared drives or synchronized folders (e.g. on Dropbox)
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Via a dedicated database
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Via a general purpose message board of message service (e.g. Twitter)
|
||||
.RE
|
||||
.sp
|
||||
The beacons are very robust. They only consist of alphanumeric characters
|
||||
and can be interleaved with non\-alphanumeric characters (e.g. whitespace).
|
||||
Also the beacons contain a prefix and suffix that depends on the configured
|
||||
network magic and secret key (if set) so that all nodes can find beacons in
|
||||
a long text.
|
||||
.sp
|
||||
When beacons are stored or loaded via a command (using the pipe character \fB|\fP),
|
||||
the command is interpreted using the configured shell \fBsh\fP. This command has
|
||||
access to the following environment variables:
|
||||
.sp
|
||||
\fB$begin\fP
|
||||
.RS 4
|
||||
The prefix of the beacon.
|
||||
.RE
|
||||
.sp
|
||||
\fB$end\fP
|
||||
.RS 4
|
||||
The suffix of the beacon.
|
||||
.RE
|
||||
.sp
|
||||
\fB$data\fP (only on store)
|
||||
.RS 4
|
||||
The middle part of the beacon. Do not use this
|
||||
without prefix and suffix!
|
||||
.RE
|
||||
.sp
|
||||
\fB$beacon\fP (only on store)
|
||||
.RS 4
|
||||
The full beacon consisting of prefix, data and
|
||||
suffix.
|
||||
The commands are called in separate threads, so even longer running commands
|
||||
will not block the node.
|
||||
.RE
|
||||
.SH "NETWORK PROTOCOL"
|
||||
.sp
|
||||
The protocol of VpnCloud is kept as simple as possible to allow other
|
||||
implementations and to maximize the performance.
|
||||
.sp
|
||||
Every packet sent over UDP contains the following header (in order):
|
||||
.sp
|
||||
4 bytes \fBmagic\fP
|
||||
.RS 4
|
||||
This field is used to identify the packet and to sort out packets that do
|
||||
not belong. The default is \fB[0x76, 0x70, 0x6e, 0x01]\fP ("vpn\(rsx01").
|
||||
This field can be used to identify VpnCloud packets and might be set to
|
||||
something different to hide the protocol.
|
||||
.RE
|
||||
.sp
|
||||
1 byte \fBcrypto method\fP
|
||||
.RS 4
|
||||
This field specifies the method that must be used to decrypt the rest of the
|
||||
data. The currently supported methods are:
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Method \fB0\fP, \fBNo encryption\fP: Rest of the data can be read without
|
||||
decrypting it.
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Method \fB1\fP, \fBChaCha20\fP: The header is followed by a 12 byte
|
||||
\fInonce\fP. The rest of the data is encrypted with the
|
||||
\fBlibsodium::crypto_aead_chacha20poly1305_ietf\fP method, using the 8 byte
|
||||
header as additional data.
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Method \fB2\fP, \fBAES256\fP: The header is followed by a 12 byte \fInonce\fP.
|
||||
The rest of the data is encrypted with the
|
||||
\fBlibsodium::crypto_aead_aes256gcm\fP method, using the 8 byte header
|
||||
as additional data.
|
||||
.RE
|
||||
.RE
|
||||
.sp
|
||||
2 \fBreserved bytes\fP
|
||||
.RS 4
|
||||
that are currently unused and set to 0
|
||||
.RE
|
||||
.sp
|
||||
1 byte for the \fBmessage type\fP
|
||||
.RS 4
|
||||
This byte specifies the type of message that follows. Currently the
|
||||
following message types are supported:
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Type 0: Data packet
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Type 1: Peer list
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Type 2: Initial message
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
Type 3: Closing message
|
||||
.RE
|
||||
.RE
|
||||
.sp
|
||||
After this 8 byte header, the rest of the message follows. It is encrypted using
|
||||
the method specified in the header.
|
||||
.sp
|
||||
In the decrypted data, the message as specified in the \fBmessage type\fP field
|
||||
will follow:
|
||||
.sp
|
||||
\fBData packet\fP (message type 0)
|
||||
.RS 4
|
||||
This packet contains payload. The format of the data depends on the device
|
||||
type. For TUN devices, this data contains an IP packet. For TAP devices it
|
||||
contains an Ethernet frame. The data starts right after the header and ends
|
||||
at the end of the packet.
|
||||
If it is an Ethernet frame, it will start with the destination MAC and end
|
||||
with the payload. It does not contain the preamble, SFD, padding, and CRC
|
||||
fields.
|
||||
.RE
|
||||
.sp
|
||||
\fBPeer list\fP (message type 1)
|
||||
.RS 4
|
||||
This packet contains the peer list of the sender. The first byte after the
|
||||
switch byte contains the number of IPv4 addresses that follow.
|
||||
After that, the specified number of addresses follow, where each address
|
||||
is encoded in 6 bytes. The first 4 bytes are the IPv4 address and the later
|
||||
2 bytes are port number (both in network byte order).
|
||||
After those addresses, the next byte contains the number of IPv6 addresses
|
||||
that follow. After that, the specified number of addresses follow, where
|
||||
each address is encoded in 18 bytes. The first 16 bytes are the IPv6 address
|
||||
and the later 2 bytes are port number (both in network byte order).
|
||||
.RE
|
||||
.sp
|
||||
\fBInitial message\fP (message type 2)
|
||||
.RS 4
|
||||
This packet contains the following information:
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
The stage of the initialization process
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
A random node id to distinguish different nodes
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
All the local subnets claimed by the nodes
|
||||
.RE
|
||||
.sp
|
||||
Its first byte marks the stage of the initial handshake process.
|
||||
The next 16 bytes contain the unique node id. After that,
|
||||
the list of local subnets follows.
|
||||
The subnet list is encoded in the following way: Its first byte of data
|
||||
contains the number of encoded subnets that follow. After that, the given
|
||||
number of encoded subnets follow.
|
||||
For each subnet, the first byte is the length of bytes in the base address
|
||||
and is followed by the given number of base address bytes and one additional
|
||||
byte that is the prefix length of the subnet.
|
||||
The addresses for the subnet will be encoded like they are encoded in their
|
||||
native protocol (4 bytes for IPv4, 16 bytes for IPv6, and 6 bytes for a MAC
|
||||
address) with the exception of MAC addresses in a VLan which will be encoded
|
||||
in 8 bytes where the first 2 bytes are the VLan number in network byte order
|
||||
and the later 6 bytes are the MAC address.
|
||||
.RE
|
||||
.sp
|
||||
\fBClosing message\fP (message type 3)
|
||||
.RS 4
|
||||
This packet does not contain any more data.
|
||||
.RE
|
||||
.sp
|
||||
Nodes are expected to send an \fBinitial message\fP with stage 0 whenever they
|
||||
connect to a node they were not connected to before. As a reply to this message,
|
||||
another initial should be sent with stage 1. Also a \fBpeer list\fP message should
|
||||
be sent as a reply.
|
||||
.sp
|
||||
When connected, nodes should periodically send their \fBpeer list\fP to all
|
||||
of their peers to spread this information and to avoid peer timeouts.
|
||||
To avoid the cubic growth of management traffic, nodes should at a certain
|
||||
network size start sending partial peer lists instead of the full list. A
|
||||
reasonable number would be about 20 peers. The subsets should be selected
|
||||
randomly.
|
||||
.sp
|
||||
Nodes should remove peers from their peer list after a certain period of
|
||||
inactivity or when receiving a \fBclosing message\fP. Before shutting down, nodes
|
||||
should send the closing message to all of their peers in order to avoid
|
||||
receiving further data until the timeout is reached.
|
||||
.sp
|
||||
Nodes should only add nodes to their peer list after receiving an initial
|
||||
message from them instead of adding them right from the peer list of another
|
||||
peer. This is necessary to avoid the case of a large network keeping dead nodes
|
||||
alive.
|
||||
.SH "COPYRIGHT"
|
||||
.sp
|
||||
Copyright \(co 2015\-2020 Dennis Schwerdel
|
||||
This software is licensed under GPL\-3 or newer (see LICENSE.md)
|
|
@ -0,0 +1,2 @@
|
|||
[Unit]
|
||||
Description=VpnCloud target allowing to start/stop all vpncloud@.service instances at once
|
|
@ -2,6 +2,7 @@
|
|||
Description=VpnCloud network '%I'
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
PartOf=vpncloud.target
|
||||
Documentation=man:vpncloud(1)
|
||||
|
||||
[Service]
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
#[macro_use]
|
||||
mod util {
|
||||
include!("../src/util.rs");
|
||||
}
|
||||
mod error {
|
||||
include!("../src/error.rs");
|
||||
}
|
||||
mod payload {
|
||||
include!("../src/payload.rs");
|
||||
}
|
||||
mod types {
|
||||
include!("../src/types.rs");
|
||||
}
|
||||
mod table {
|
||||
include!("../src/table.rs");
|
||||
}
|
||||
mod cloud {
|
||||
include!("../src/cloud.rs");
|
||||
}
|
||||
mod config {
|
||||
include!("../src/config.rs");
|
||||
}
|
||||
mod device {
|
||||
include!("../src/device.rs");
|
||||
}
|
||||
mod net {
|
||||
include!("../src/net.rs");
|
||||
}
|
||||
mod beacon {
|
||||
include!("../src/beacon.rs");
|
||||
}
|
||||
mod messages {
|
||||
include!("../src/messages.rs");
|
||||
}
|
||||
mod port_forwarding {
|
||||
include!("../src/port_forwarding.rs");
|
||||
}
|
||||
mod traffic {
|
||||
include!("../src/traffic.rs");
|
||||
}
|
||||
mod poll {
|
||||
pub mod epoll{
|
||||
include!("../src/poll/epoll.rs");
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub use self::epoll::EpollWait as WaitImpl;
|
||||
|
||||
use std::io;
|
||||
|
||||
pub enum WaitResult {
|
||||
Timeout,
|
||||
Socket,
|
||||
Device,
|
||||
Error(io::Error)
|
||||
}
|
||||
}
|
||||
mod crypto {
|
||||
pub mod core {
|
||||
include!("../src/crypto/core.rs");
|
||||
}
|
||||
pub mod init {
|
||||
include!("../src/crypto/init.rs");
|
||||
}
|
||||
pub mod rotate {
|
||||
include!("../src/crypto/rotate.rs");
|
||||
}
|
||||
pub mod common {
|
||||
include!("../src/crypto/common.rs");
|
||||
}
|
||||
pub use common::*;
|
||||
pub use self::core::{EXTRA_LEN, TAG_LEN};
|
||||
}
|
||||
mod tests {
|
||||
pub mod common {
|
||||
include!("../src/tests/common.rs");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
#![allow(dead_code, unused_macros, unused_imports)]
|
||||
#[macro_use] extern crate serde;
|
||||
#[macro_use] extern crate log;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
|
||||
|
||||
use smallvec::smallvec;
|
||||
use ring::aead;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::net::{SocketAddr, Ipv4Addr, SocketAddrV4, UdpSocket};
|
||||
|
||||
include!(".code.rs");
|
||||
|
||||
pub use error::Error;
|
||||
use util::{MockTimeSource, MsgBuffer};
|
||||
use types::{Address, Range};
|
||||
use table::ClaimTable;
|
||||
use device::Type;
|
||||
use config::Config;
|
||||
use payload::{Packet, Frame, Protocol};
|
||||
use crypto::core::{create_dummy_pair, EXTRA_LEN};
|
||||
use tests::common::{TunSimulator, TapSimulator};
|
||||
|
||||
fn udp_send(c: &mut Criterion) {
|
||||
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let data = [0; 1400];
|
||||
let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 1);
|
||||
let mut g = c.benchmark_group("udp_send");
|
||||
g.throughput(Throughput::Bytes(1400));
|
||||
g.bench_function("udp_send", |b| {
|
||||
b.iter(|| sock.send_to(&data, addr).unwrap());
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
fn decode_ipv4(c: &mut Criterion) {
|
||||
let data = [0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1, 192, 168, 1, 2];
|
||||
let mut g = c.benchmark_group("payload");
|
||||
g.throughput(Throughput::Bytes(1400));
|
||||
g.bench_function("decode_ipv4", |b| {
|
||||
b.iter(|| Packet::parse(&data).unwrap());
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
fn decode_ipv6(c: &mut Criterion) {
|
||||
let data = [
|
||||
0x60, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5,
|
||||
4, 3, 2, 1
|
||||
];
|
||||
let mut g = c.benchmark_group("payload");
|
||||
g.throughput(Throughput::Bytes(1400));
|
||||
g.bench_function("decode_ipv6", |b| {
|
||||
b.iter(|| Packet::parse(&data).unwrap());
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
fn decode_ethernet(c: &mut Criterion) {
|
||||
let data = [6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let mut g = c.benchmark_group("payload");
|
||||
g.throughput(Throughput::Bytes(1400));
|
||||
g.bench_function("decode_ethernet", |b| {
|
||||
b.iter(|| Frame::parse(&data).unwrap());
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
fn decode_ethernet_with_vlan(c: &mut Criterion) {
|
||||
let data = [6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 0x81, 0, 4, 210, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let mut g = c.benchmark_group("payload");
|
||||
g.throughput(Throughput::Bytes(1400));
|
||||
g.bench_function("decode_ethernet_with_vlan", |b| {
|
||||
b.iter(|| Frame::parse(&data).unwrap());
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
fn lookup_warm(c: &mut Criterion) {
|
||||
let mut table = ClaimTable::<MockTimeSource>::new(60, 60);
|
||||
let addr = Address::from_str("1.2.3.4").unwrap();
|
||||
table.cache(addr, SocketAddr::from_str("1.2.3.4:3210").unwrap());
|
||||
let mut g = c.benchmark_group("table");
|
||||
g.throughput(Throughput::Bytes(1400));
|
||||
g.bench_function("lookup_warm", |b| {
|
||||
b.iter(|| table.lookup(addr));
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
fn lookup_cold(c: &mut Criterion) {
|
||||
let mut table = ClaimTable::<MockTimeSource>::new(60, 60);
|
||||
let addr = Address::from_str("1.2.3.4").unwrap();
|
||||
table.set_claims(SocketAddr::from_str("1.2.3.4:3210").unwrap(), smallvec![Range::from_str("1.2.3.4/32").unwrap()]);
|
||||
let mut g = c.benchmark_group("table");
|
||||
g.throughput(Throughput::Bytes(1400));
|
||||
g.bench_function("lookup_cold", |b| {
|
||||
b.iter(|| {
|
||||
table.clear_cache();
|
||||
table.lookup(addr)
|
||||
});
|
||||
});
|
||||
g.finish();
|
||||
}
|
||||
|
||||
fn crypto_bench(c: &mut Criterion, algo: &'static aead::Algorithm) {
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
buffer.set_length(1400);
|
||||
let (mut sender, mut receiver) = create_dummy_pair(algo);
|
||||
let mut g = c.benchmark_group("crypto");
|
||||
g.throughput(Throughput::Bytes(2*1400));
|
||||
g.bench_function(format!("{:?}", algo), |b| {
|
||||
b.iter(|| {
|
||||
sender.encrypt(&mut buffer);
|
||||
receiver.decrypt(&mut buffer).unwrap();
|
||||
});
|
||||
});
|
||||
g.finish()
|
||||
}
|
||||
|
||||
fn crypto_chacha20(c: &mut Criterion) {
|
||||
crypto_bench(c, &aead::CHACHA20_POLY1305)
|
||||
}
|
||||
|
||||
fn crypto_aes128(c: &mut Criterion) {
|
||||
crypto_bench(c, &aead::AES_128_GCM)
|
||||
}
|
||||
|
||||
fn crypto_aes256(c: &mut Criterion) {
|
||||
crypto_bench(c, &aead::AES_256_GCM)
|
||||
}
|
||||
|
||||
fn full_communication_tun_router(c: &mut Criterion) {
|
||||
log::set_max_level(log::LevelFilter::Error);
|
||||
let config1 = Config {
|
||||
device_type: Type::Tun,
|
||||
auto_claim: false,
|
||||
claims: vec!["1.1.1.1/32".to_string()],
|
||||
..Config::default()
|
||||
};
|
||||
let config2 = Config {
|
||||
device_type: Type::Tun,
|
||||
auto_claim: false,
|
||||
claims: vec!["2.2.2.2/32".to_string()],
|
||||
..Config::default()
|
||||
};
|
||||
let mut sim = TunSimulator::new();
|
||||
let node1 = sim.add_node(false, &config1);
|
||||
let node2 = sim.add_node(false, &config2);
|
||||
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
|
||||
let mut payload = vec![0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
|
||||
payload.append(&mut vec![0; 1400]);
|
||||
let mut g = c.benchmark_group("full_communication");
|
||||
g.throughput(Throughput::Bytes(2*1400));
|
||||
g.bench_function("tun_router", |b| {
|
||||
b.iter(|| {
|
||||
sim.put_payload(node1, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
assert_eq!(Some(&payload), sim.pop_payload(node2).as_ref());
|
||||
});
|
||||
});
|
||||
g.finish()
|
||||
}
|
||||
|
||||
fn full_communication_tap_switch(c: &mut Criterion) {
|
||||
log::set_max_level(log::LevelFilter::Error);
|
||||
let config = Config { device_type: Type::Tap, ..Config::default() };
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
|
||||
let mut payload = vec![2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5];
|
||||
payload.append(&mut vec![0; 1400]);
|
||||
let mut g = c.benchmark_group("full_communication");
|
||||
g.throughput(Throughput::Bytes(2*1400));
|
||||
g.bench_function("tap_switch", |b| {
|
||||
b.iter(|| {
|
||||
sim.put_payload(node1, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
assert_eq!(Some(&payload), sim.pop_payload(node2).as_ref());
|
||||
});
|
||||
});
|
||||
g.finish()
|
||||
}
|
||||
|
||||
criterion_group!(benches,
|
||||
udp_send,
|
||||
decode_ipv4, decode_ipv6, decode_ethernet, decode_ethernet_with_vlan,
|
||||
lookup_cold, lookup_warm,
|
||||
crypto_chacha20, crypto_aes128, crypto_aes256,
|
||||
full_communication_tun_router, full_communication_tap_switch
|
||||
);
|
||||
criterion_main!(benches);
|
|
@ -0,0 +1,155 @@
|
|||
#![allow(dead_code, unused_macros, unused_imports)]
|
||||
#[macro_use] extern crate serde;
|
||||
#[macro_use] extern crate log;
|
||||
|
||||
use iai::{black_box, main};
|
||||
|
||||
use smallvec::smallvec;
|
||||
use ring::aead;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::net::{SocketAddr, Ipv4Addr, SocketAddrV4, UdpSocket};
|
||||
|
||||
include!(".code.rs");
|
||||
|
||||
pub use error::Error;
|
||||
use util::{MockTimeSource, MsgBuffer};
|
||||
use config::Config;
|
||||
use types::{Address, Range};
|
||||
use device::Type;
|
||||
use table::ClaimTable;
|
||||
use payload::{Packet, Frame, Protocol};
|
||||
use crypto::core::{create_dummy_pair, EXTRA_LEN};
|
||||
use tests::common::{TunSimulator, TapSimulator};
|
||||
|
||||
fn udp_send() {
|
||||
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let data = [0; 1400];
|
||||
let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 1);
|
||||
sock.send_to(&data, black_box(addr)).unwrap();
|
||||
}
|
||||
|
||||
fn decode_ipv4() {
|
||||
let data = [0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1, 192, 168, 1, 2];
|
||||
Packet::parse(&black_box(data)).unwrap();
|
||||
}
|
||||
|
||||
fn decode_ipv6() {
|
||||
let data = [
|
||||
0x60, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5,
|
||||
4, 3, 2, 1
|
||||
];
|
||||
Packet::parse(&black_box(data)).unwrap();
|
||||
}
|
||||
|
||||
fn decode_ethernet() {
|
||||
let data = [6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
Frame::parse(&black_box(data)).unwrap();
|
||||
}
|
||||
|
||||
fn decode_ethernet_with_vlan() {
|
||||
let data = [6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 0x81, 0, 4, 210, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
Frame::parse(&black_box(data)).unwrap();
|
||||
}
|
||||
|
||||
fn lookup_warm() {
|
||||
let mut table = ClaimTable::<MockTimeSource>::new(60, 60);
|
||||
let addr = Address::from_str("1.2.3.4").unwrap();
|
||||
table.cache(addr, SocketAddr::from_str("1.2.3.4:3210").unwrap());
|
||||
for _ in 0..1000 {
|
||||
table.lookup(black_box(addr));
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_cold() {
|
||||
let mut table = ClaimTable::<MockTimeSource>::new(60, 60);
|
||||
let addr = Address::from_str("1.2.3.4").unwrap();
|
||||
table.set_claims(SocketAddr::from_str("1.2.3.4:3210").unwrap(), smallvec![Range::from_str("1.2.3.4/32").unwrap()]);
|
||||
for _ in 0..1000 {
|
||||
table.clear_cache();
|
||||
table.lookup(black_box(addr));
|
||||
}
|
||||
}
|
||||
|
||||
fn crypto_bench(algo: &'static aead::Algorithm) {
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
buffer.set_length(1400);
|
||||
let (mut sender, mut receiver) = create_dummy_pair(algo);
|
||||
for _ in 0..1000 {
|
||||
sender.encrypt(black_box(&mut buffer));
|
||||
receiver.decrypt(&mut buffer).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn crypto_chacha20() {
|
||||
crypto_bench(&aead::CHACHA20_POLY1305)
|
||||
}
|
||||
|
||||
fn crypto_aes128() {
|
||||
crypto_bench(&aead::AES_128_GCM)
|
||||
}
|
||||
|
||||
fn crypto_aes256() {
|
||||
crypto_bench(&aead::AES_256_GCM)
|
||||
}
|
||||
|
||||
fn full_communication_tun_router() {
|
||||
log::set_max_level(log::LevelFilter::Error);
|
||||
let config1 = Config {
|
||||
device_type: Type::Tun,
|
||||
auto_claim: false,
|
||||
claims: vec!["1.1.1.1/32".to_string()],
|
||||
..Config::default()
|
||||
};
|
||||
let config2 = Config {
|
||||
device_type: Type::Tun,
|
||||
auto_claim: false,
|
||||
claims: vec!["2.2.2.2/32".to_string()],
|
||||
..Config::default()
|
||||
};
|
||||
let mut sim = TunSimulator::new();
|
||||
let node1 = sim.add_node(false, &config1);
|
||||
let node2 = sim.add_node(false, &config2);
|
||||
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
|
||||
let mut payload = vec![0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
|
||||
payload.append(&mut vec![0; 1400]);
|
||||
for _ in 0..1000 {
|
||||
sim.put_payload(node1, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
assert_eq!(Some(&payload), black_box(sim.pop_payload(node2).as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
fn full_communication_tap_switch() {
|
||||
log::set_max_level(log::LevelFilter::Error);
|
||||
let config = Config { device_type: Type::Tap, ..Config::default() };
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
|
||||
let mut payload = vec![2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5];
|
||||
payload.append(&mut vec![0; 1400]);
|
||||
for _ in 0..1000 {
|
||||
sim.put_payload(node1, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
assert_eq!(Some(&payload), black_box(sim.pop_payload(node2).as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
iai::main!(
|
||||
udp_send,
|
||||
decode_ipv4, decode_ipv6, decode_ethernet, decode_ethernet_with_vlan,
|
||||
lookup_cold, lookup_warm,
|
||||
crypto_chacha20, crypto_aes128, crypto_aes256,
|
||||
full_communication_tun_router, full_communication_tap_switch
|
||||
);
|
33
build.rs
33
build.rs
|
@ -1,33 +0,0 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
extern crate cc;
|
||||
|
||||
use std::{env, fs, path::Path, process::Command};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
|
||||
// Compile tun/tap C code
|
||||
println!("cargo:rerun-if-changed=src/c/tuntap.c");
|
||||
cc::Build::new().file("src/c/tuntap.c").include("src").compile("libtuntap.a");
|
||||
|
||||
// Process manpage using asciidoctor command
|
||||
println!("cargo:rerun-if-changed=vpncloud.adoc");
|
||||
fs::copy("vpncloud.adoc", Path::new(&out_dir).join("vpncloud.adoc")).unwrap();
|
||||
match Command::new("asciidoctor")
|
||||
.args(&["-b", "manpage", "vpncloud.adoc"])
|
||||
.current_dir(&Path::new(&out_dir))
|
||||
.status()
|
||||
{
|
||||
Ok(_) => {
|
||||
Command::new("gzip").args(&["vpncloud.1"]).current_dir(&Path::new(&out_dir)).status().unwrap();
|
||||
fs::copy(Path::new(&out_dir).join("vpncloud.1.gz"), "target/vpncloud.1.gz").unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
println!("cargo:warning=Error building manpage: {}", err);
|
||||
println!("cargo:warning=The manpage will not be build. Do you have 'asciidoctor'?");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
cache
|
||||
build.sh
|
|
@ -1,38 +0,0 @@
|
|||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
curl \
|
||||
gcc-aarch64-linux-gnu \
|
||||
gcc-arm-linux-gnueabihf \
|
||||
libc6-dev-arm64-cross \
|
||||
libc6-dev-armhf-cross \
|
||||
libc6-dev-i386 \
|
||||
gcc-5-multilib \
|
||||
asciidoctor \
|
||||
&& rm -rf /var/cache/dpkg
|
||||
|
||||
RUN ln -s asm-generic/ /usr/include/asm
|
||||
|
||||
RUN useradd -ms /bin/bash user
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
ENV RUST=1.40.0
|
||||
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${RUST}
|
||||
|
||||
ENV PATH=/home/user/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
RUN rustup target add i686-unknown-linux-gnu \
|
||||
&& rustup target add armv7-unknown-linux-gnueabihf \
|
||||
&& rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
RUN cargo install cargo-deb \
|
||||
&& rm -rf /home/user/.cargo/{git,tmp,registry}
|
||||
|
||||
VOLUME /home/user/.cargo/tmp
|
||||
VOLUME /home/user/.cargo/git
|
||||
VOLUME /home/user/.cargo/registry
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
FROM centos:7
|
||||
|
||||
RUN yum groupinstall -y 'Development Tools'
|
||||
RUN yum install -y ruby && gem install asciidoctor
|
||||
|
||||
RUN useradd -ms /bin/bash user
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
ENV RUST=1.40.0
|
||||
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain ${RUST}
|
||||
|
||||
ENV PATH=/home/user/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
RUN rustup target add i686-unknown-linux-gnu \
|
||||
&& rustup target add armv7-unknown-linux-gnueabihf
|
||||
|
||||
RUN cargo install cargo-rpm \
|
||||
&& rm -rf /home/user/.cargo/{git,tmp,registry}
|
||||
|
||||
VOLUME /home/user/.cargo/tmp
|
||||
VOLUME /home/user/.cargo/git
|
||||
VOLUME /home/user/.cargo/registry
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
function docker_cmd() {
|
||||
DIST=$1
|
||||
CMD=$2
|
||||
mkdir -p $CACHE/$DIST/{target,registry,git,tmp}
|
||||
docker run -it --rm -v $(pwd)/..:/home/user/code \
|
||||
-v $CACHE/$DIST/target:/home/user/code/target \
|
||||
-v $CACHE/$DIST/registry:/home/user/.cargo/registry \
|
||||
-v $CACHE/$DIST/git:/home/user/.cargo/git \
|
||||
-v $CACHE/$DIST/tmp:/home/user/.cargo/tmp \
|
||||
vpncloud-builder-$DIST bash -c "$CMD"
|
||||
}
|
||||
|
||||
set -e
|
||||
|
||||
cd $(dirname $0)
|
||||
|
||||
VERSION=$(grep -e '^version =' ../Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
|
||||
mkdir -p cache/{rpm,deb}
|
||||
CACHE=$(pwd)/cache
|
||||
|
||||
mkdir -p ../dist
|
||||
|
||||
docker build --rm -f=Dockerfile-deb -t vpncloud-builder-deb .
|
||||
|
||||
# x86_64 deb
|
||||
docker_cmd deb 'cd code && cargo deb'
|
||||
cp $CACHE/deb/target/debian/vpncloud_${VERSION}_amd64.deb ../dist/vpncloud_${VERSION}_amd64.deb
|
||||
|
||||
# i386 deb
|
||||
docker_cmd deb 'cd code && cargo deb --target i686-unknown-linux-gnu'
|
||||
cp $CACHE/deb/target/i686-unknown-linux-gnu/debian/vpncloud_${VERSION}_i386.deb ../dist/vpncloud_${VERSION}_i386.deb
|
||||
|
||||
# arm7hf deb
|
||||
docker_cmd deb 'cd code && cargo deb --target armv7-unknown-linux-gnueabihf'
|
||||
cp $CACHE/deb/target/armv7-unknown-linux-gnueabihf/debian/vpncloud_${VERSION}_armhf.deb ../dist/vpncloud_${VERSION}_armhf.deb
|
||||
|
||||
# aarch64 deb
|
||||
docker_cmd deb 'cd code && cargo deb --target aarch64-unknown-linux-gnu'
|
||||
cp $CACHE/deb/target/aarch64-unknown-linux-gnu/debian/vpncloud_${VERSION}_arm64.deb ../dist/vpncloud_${VERSION}_arm64.deb
|
||||
|
||||
|
||||
docker build --rm -f=Dockerfile-rpm -t vpncloud-builder-rpm .
|
||||
|
||||
# x86_64 rpm
|
||||
docker_cmd rpm 'cd code && cargo rpm build'
|
||||
cp $CACHE/rpm/target/release/rpmbuild/RPMS/x86_64/vpncloud-${VERSION}-1.x86_64.rpm ../dist/vpncloud_${VERSION}.x86_64.rpm
|
|
@ -0,0 +1,13 @@
|
|||
FROM ubuntu
|
||||
|
||||
RUN apt-get update && apt-get install -y asciinema locales bash iputils-ping
|
||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
RUN mkdir /root/.asciinema
|
||||
RUN mkdir /etc/vpncloud
|
||||
|
||||
WORKDIR /data
|
||||
ADD config /root/.asciinema/config
|
||||
RUN echo 'PS1="\[\e[00;34m\]\[\e[01;31m\]\u\[\e[00;01;34m\]@\[\e[00;34m\]node\[\e[01;31m\]:\[\e[00;34m\]\w\[\e[01;31m\]> \[\e[00m\]"' >> /root/.bashrc
|
|
@ -0,0 +1,3 @@
|
|||
[record]
|
||||
command = bash -l
|
||||
idle_time_limit = 2.5
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd $(dirname $0)
|
||||
|
||||
docker build -t asciinema-recorder .
|
||||
docker run -it --rm --network host \
|
||||
-v $(pwd):/data \
|
||||
-v /etc/hosts:/etc/hosts \
|
||||
asciinema-recorder
|
|
@ -0,0 +1,81 @@
|
|||
AWSTemplateFormatVersion: 2010-09-09
|
||||
Description: |
|
||||
VpnCloud Websocket Proxy
|
||||
This will configure a websocket proxy to be used with VpnCloud.
|
||||
Versions: Ubuntu Server 20.04 LTS + VpnCloud 2.1.0
|
||||
Parameters:
|
||||
LatestAmiId:
|
||||
Description: "Image to use (just leave this as it is)"
|
||||
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
|
||||
Default: '/aws/service/canonical/ubuntu/server/20.04/stable/current/arm64/hvm/ebs-gp2/ami-id'
|
||||
AllowedValues:
|
||||
- '/aws/service/canonical/ubuntu/server/20.04/stable/current/arm64/hvm/ebs-gp2/ami-id'
|
||||
Resources:
|
||||
ProxySecurityGroup:
|
||||
Type: 'AWS::EC2::SecurityGroup'
|
||||
Properties:
|
||||
GroupDescription: Enable HTTP access via port 80 and any UDP port
|
||||
SecurityGroupIngress:
|
||||
- IpProtocol: tcp
|
||||
FromPort: '80'
|
||||
ToPort: '80'
|
||||
CidrIp: 0.0.0.0/0
|
||||
- IpProtocol: udp
|
||||
FromPort: '1024'
|
||||
ToPort: '65535'
|
||||
CidrIp: 0.0.0.0/0
|
||||
LaunchTemplate:
|
||||
Type: AWS::EC2::LaunchTemplate
|
||||
DependsOn:
|
||||
- ProxySecurityGroup
|
||||
Properties:
|
||||
LaunchTemplateData:
|
||||
ImageId: !Ref LatestAmiId
|
||||
SecurityGroups:
|
||||
- !Ref ProxySecurityGroup
|
||||
InstanceMarketOptions:
|
||||
MarketType: spot
|
||||
InstanceType: t4g.nano
|
||||
TagSpecifications:
|
||||
- ResourceType: instance
|
||||
Tags:
|
||||
- Key: Name
|
||||
Value: VpnCloud WS Proxy
|
||||
CreditSpecification:
|
||||
CpuCredits: standard
|
||||
BlockDeviceMappings:
|
||||
- DeviceName: /dev/sda1
|
||||
Ebs:
|
||||
VolumeType: standard
|
||||
VolumeSize: '8'
|
||||
DeleteOnTermination: 'true'
|
||||
Encrypted: 'false'
|
||||
UserData:
|
||||
Fn::Base64: !Sub |
|
||||
#cloud-config
|
||||
packages:
|
||||
- iperf3
|
||||
- socat
|
||||
runcmd:
|
||||
- wget https://github.com/dswd/vpncloud/releases/download/v2.1.0/vpncloud_2.1.0_arm64.deb -O /tmp/vpncloud.deb
|
||||
- dpkg -i /tmp/vpncloud.deb
|
||||
- nohup vpncloud ws-proxy -l 80 &
|
||||
ProxyInstance:
|
||||
Type: 'AWS::EC2::Instance'
|
||||
DependsOn:
|
||||
- LaunchTemplate
|
||||
Properties:
|
||||
LaunchTemplate:
|
||||
LaunchTemplateId:
|
||||
Ref: LaunchTemplate
|
||||
Version: 1
|
||||
Outputs:
|
||||
ProxyURL:
|
||||
Description: URL to use in VpnCloud config
|
||||
Value: !Join
|
||||
- ''
|
||||
- - 'ws://'
|
||||
- !GetAtt
|
||||
- ProxyInstance
|
||||
- PublicDnsName
|
||||
- ':80'
|
|
@ -0,0 +1,384 @@
|
|||
import boto3
|
||||
import atexit
|
||||
import paramiko
|
||||
import io
|
||||
import time
|
||||
import threading
|
||||
import re
|
||||
import json
|
||||
import base64
|
||||
import sys
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
MAX_WAIT = 300
|
||||
CREATE = "***CREATE***"
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
def run_cmd(connection, cmd):
|
||||
_stdin, stdout, stderr = connection.exec_command(cmd)
|
||||
out = stdout.read().decode('utf-8')
|
||||
err = stderr.read().decode('utf-8')
|
||||
code = stdout.channel.recv_exit_status()
|
||||
if code:
|
||||
raise Exception("Command failed", code, out, err)
|
||||
else:
|
||||
return out, err
|
||||
|
||||
def upload(connection, local, remote):
|
||||
ftp_client=connection.open_sftp()
|
||||
ftp_client.put(local, remote)
|
||||
ftp_client.close()
|
||||
|
||||
class SpotInstanceRequest:
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
def __str__(self):
|
||||
return str(self.id)
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, instance, connection):
|
||||
self.instance = instance
|
||||
self.connection = connection
|
||||
self.private_ip = instance.private_ip_address
|
||||
self.public_ip = instance.public_ip_address
|
||||
|
||||
def run_cmd(self, cmd):
|
||||
return run_cmd(self.connection, cmd)
|
||||
|
||||
def start_vpncloud(self, ip=None, crypto=None, password="test", device_type="tun", listen="3210", mode="normal", peers=[], claims=[]):
|
||||
args = [
|
||||
"--daemon",
|
||||
"--no-port-forwarding",
|
||||
"-t {}".format(device_type),
|
||||
"-m {}".format(mode),
|
||||
"-l {}".format(listen),
|
||||
"--password '{}'".format(password)
|
||||
]
|
||||
if ip:
|
||||
args.append("--ip {}".format(ip))
|
||||
if crypto:
|
||||
args.append("--algo {}".format(crypto))
|
||||
for p in peers:
|
||||
args.append("-c {}".format(p))
|
||||
for c in claims:
|
||||
args.append("--claim {}".format(c))
|
||||
args = " ".join(args)
|
||||
self.run_cmd("sudo vpncloud {}".format(args))
|
||||
|
||||
def stop_vpncloud(self, wait=True):
|
||||
self.run_cmd("sudo killall vpncloud")
|
||||
if wait:
|
||||
time.sleep(3.0)
|
||||
|
||||
def ping(self, dst, size=100, count=10, interval=0.001):
|
||||
(out, _) = self.run_cmd('sudo ping {dst} -c {count} -i {interval} -s {size} -U -q'.format(dst=dst, size=size, count=count, interval=interval))
|
||||
match = re.search(r'([\d]*\.[\d]*)/([\d]*\.[\d]*)/([\d]*\.[\d]*)/([\d]*\.[\d]*)', out)
|
||||
ping_min = float(match.group(1))
|
||||
ping_avg = float(match.group(2))
|
||||
ping_max = float(match.group(3))
|
||||
match = re.search(r'(\d*)% packet loss', out)
|
||||
pkt_loss = float(match.group(1))
|
||||
return {
|
||||
"rtt_min": ping_min,
|
||||
"rtt_max": ping_max,
|
||||
"rtt_avg": ping_avg,
|
||||
"pkt_loss": pkt_loss
|
||||
}
|
||||
|
||||
def start_iperf_server(self):
|
||||
self.run_cmd('iperf3 -s -D')
|
||||
time.sleep(0.1)
|
||||
|
||||
def stop_iperf_server(self):
|
||||
self.run_cmd('killall iperf3')
|
||||
|
||||
def run_iperf(self, dst, duration):
|
||||
(out, _) = self.run_cmd('iperf3 -c {dst} -t {duration} --json'.format(dst=dst, duration=duration))
|
||||
data = json.loads(out)
|
||||
return {
|
||||
"throughput": data['end']['streams'][0]['receiver']['bits_per_second'],
|
||||
"cpu_sender": data['end']['cpu_utilization_percent']['host_total'],
|
||||
"cpu_receiver": data['end']['cpu_utilization_percent']['remote_total']
|
||||
}
|
||||
|
||||
|
||||
def find_ami(region, owner, name_pattern, arch='x86_64'):
|
||||
ec2client = boto3.client('ec2', region_name=region)
|
||||
response = ec2client.describe_images(Owners=[owner], Filters=[
|
||||
{'Name': 'name', 'Values': [name_pattern]},
|
||||
{'Name': 'architecture', 'Values': ['x86_64']}
|
||||
])
|
||||
try:
|
||||
image = max(response['Images'], key=lambda i: i['CreationDate'])
|
||||
return image['ImageId']
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class EC2Environment:
|
||||
def __init__(self, vpncloud_version, region, node_count, instance_type, vpncloud_file=None, use_spot=True, max_price=0.1, ami=('amazon', 'al2023-ami-*-hvm-*'), username="ec2-user", subnet=CREATE, keyname=CREATE, privatekey=CREATE, tag="vpncloud", cluster_nodes=False):
|
||||
self.region = region
|
||||
self.node_count = node_count
|
||||
self.instance_type = instance_type
|
||||
self.use_spot = use_spot
|
||||
self.max_price = str(max_price)
|
||||
if isinstance(ami, tuple):
|
||||
owner, name = ami
|
||||
self.ami = find_ami(region, owner, name)
|
||||
assert self.ami
|
||||
else:
|
||||
self.ami = ami
|
||||
self.username = username
|
||||
self.vpncloud_version = vpncloud_version
|
||||
self.vpncloud_file = vpncloud_file
|
||||
self.cluster_nodes = cluster_nodes
|
||||
self.resources = []
|
||||
self.instances = []
|
||||
self.connections = []
|
||||
self.nodes = []
|
||||
self.subnet = subnet
|
||||
self.tag = tag
|
||||
self.keyname = keyname
|
||||
self.privatekey = privatekey
|
||||
self.rsa_key = None
|
||||
try:
|
||||
eprint("Setting up resources...")
|
||||
self.setup()
|
||||
self.wait_until_ready()
|
||||
for i in range(0, self.node_count):
|
||||
self.nodes.append(Node(self.instances[i], self.connections[i]))
|
||||
eprint("Setup done")
|
||||
atexit.register(lambda : self.terminate())
|
||||
eprint()
|
||||
except:
|
||||
eprint("Error, shutting down")
|
||||
self.terminate()
|
||||
raise
|
||||
|
||||
def track_resource(self, res):
|
||||
self.resources.append(res)
|
||||
eprint("\t{} {}".format(res.__class__.__name__, res.id if hasattr(res, "id") else ""))
|
||||
if hasattr(res, "create_tags") and not hasattr(res, "name"):
|
||||
res.create_tags(Tags=[{"Key": "Name", "Value": self.tag}])
|
||||
|
||||
def setup_vpc(self):
|
||||
ec2 = boto3.resource('ec2', region_name=self.region)
|
||||
ec2client = boto3.client('ec2', region_name=self.region)
|
||||
|
||||
vpc = ec2.create_vpc(CidrBlock='172.16.0.0/16')
|
||||
self.track_resource(vpc)
|
||||
vpc.wait_until_available()
|
||||
ec2client.modify_vpc_attribute(VpcId=vpc.id, EnableDnsSupport={'Value': True})
|
||||
ec2client.modify_vpc_attribute(VpcId=vpc.id, EnableDnsHostnames={'Value': True})
|
||||
|
||||
igw = ec2.create_internet_gateway()
|
||||
self.track_resource(igw)
|
||||
igw.attach_to_vpc(VpcId=vpc.id)
|
||||
|
||||
rtb = vpc.create_route_table()
|
||||
self.track_resource(rtb)
|
||||
rtb.create_route(DestinationCidrBlock='0.0.0.0/0', GatewayId=igw.id)
|
||||
|
||||
subnet = ec2.create_subnet(CidrBlock='172.16.1.0/24', VpcId=vpc.id)
|
||||
self.track_resource(subnet)
|
||||
rtb.associate_with_subnet(SubnetId=subnet.id)
|
||||
|
||||
self.subnet = subnet.id
|
||||
|
||||
|
||||
def setup(self):
|
||||
ec2 = boto3.resource('ec2', region_name=self.region)
|
||||
ec2client = boto3.client('ec2', region_name=self.region)
|
||||
|
||||
if self.subnet == CREATE:
|
||||
self.setup_vpc()
|
||||
else:
|
||||
eprint("\tUsing subnet {}".format(self.subnet))
|
||||
|
||||
vpc = ec2.Subnet(self.subnet).vpc
|
||||
|
||||
sg = ec2.create_security_group(GroupName='SSH-ONLY', Description='only allow SSH traffic', VpcId=vpc.id)
|
||||
self.track_resource(sg)
|
||||
sg.authorize_ingress(CidrIp='0.0.0.0/0', IpProtocol='tcp', FromPort=22, ToPort=22)
|
||||
sg.authorize_ingress(CidrIp='172.16.1.0/24', IpProtocol='icmp', FromPort=-1, ToPort=-1)
|
||||
sg.authorize_ingress(CidrIp='172.16.1.0/24', IpProtocol='tcp', FromPort=0, ToPort=65535)
|
||||
sg.authorize_ingress(CidrIp='172.16.1.0/24', IpProtocol='udp', FromPort=0, ToPort=65535)
|
||||
|
||||
if self.keyname == CREATE:
|
||||
key_pair = ec2.create_key_pair(KeyName="{}-keypair".format(self.tag))
|
||||
self.track_resource(key_pair)
|
||||
self.keyname = key_pair.name
|
||||
self.privatekey = key_pair.key_material
|
||||
self.rsa_key = paramiko.RSAKey.from_private_key(io.StringIO(self.privatekey))
|
||||
|
||||
placement = {}
|
||||
if self.cluster_nodes:
|
||||
placement_group = ec2.create_placement_group(GroupName="{}-placement".format(self.tag), Strategy="cluster")
|
||||
self.track_resource(placement_group)
|
||||
placement = { 'GroupName': placement_group.name }
|
||||
|
||||
userdata = """#cloud-config
|
||||
packages:
|
||||
- iperf3
|
||||
- socat
|
||||
"""
|
||||
if not self.vpncloud_file:
|
||||
userdata += """
|
||||
runcmd:
|
||||
- wget https://github.com/dswd/vpncloud/releases/download/v{version}/vpncloud_{version}.x86_64.rpm -O /tmp/vpncloud.rpm
|
||||
- yum install -y /tmp/vpncloud.rpm
|
||||
""".format(version=self.vpncloud_version)
|
||||
|
||||
if self.use_spot:
|
||||
response = ec2client.request_spot_instances(
|
||||
SpotPrice = self.max_price,
|
||||
Type = "one-time",
|
||||
InstanceCount = self.node_count,
|
||||
LaunchSpecification = {
|
||||
"ImageId": self.ami,
|
||||
"InstanceType": self.instance_type,
|
||||
"KeyName": key_pair.name,
|
||||
"UserData": base64.b64encode(userdata.encode("ascii")).decode('ascii'),
|
||||
"BlockDeviceMappings": [
|
||||
{
|
||||
"DeviceName": "/dev/xvda",
|
||||
"Ebs": {
|
||||
"DeleteOnTermination": True,
|
||||
"VolumeType": "gp2",
|
||||
}
|
||||
}
|
||||
],
|
||||
"NetworkInterfaces": [
|
||||
{
|
||||
'SubnetId': self.subnet,
|
||||
'DeviceIndex': 0,
|
||||
'AssociatePublicIpAddress': True,
|
||||
'Groups': [sg.group_id]
|
||||
}
|
||||
],
|
||||
"Placement": placement
|
||||
}
|
||||
)
|
||||
requests = []
|
||||
for req in response['SpotInstanceRequests']:
|
||||
request = SpotInstanceRequest(req['SpotInstanceRequestId'])
|
||||
self.track_resource(request)
|
||||
requests.append(request)
|
||||
eprint("Waiting for spot instance requests")
|
||||
waited = 0
|
||||
self.instances = [None] * len(requests)
|
||||
while waited < MAX_WAIT:
|
||||
time.sleep(1.0)
|
||||
for i, req in enumerate(requests):
|
||||
response = ec2client.describe_spot_instance_requests(SpotInstanceRequestIds=[req.id])
|
||||
data = response['SpotInstanceRequests'][0]
|
||||
if 'InstanceId' in data:
|
||||
self.instances[i] = ec2.Instance(data['InstanceId'])
|
||||
self.track_resource(self.instances[i])
|
||||
if min(map(bool, self.instances)):
|
||||
break
|
||||
if waited >= MAX_WAIT:
|
||||
raise Exception("Waited too long")
|
||||
else:
|
||||
self.instances = ec2.create_instances(
|
||||
ImageId=self.ami,
|
||||
InstanceType=self.instance_type,
|
||||
MaxCount=self.node_count,
|
||||
MinCount=self.node_count,
|
||||
NetworkInterfaces=[
|
||||
{
|
||||
'SubnetId': self.subnet,
|
||||
'DeviceIndex': 0,
|
||||
'AssociatePublicIpAddress': True,
|
||||
'Groups': [sg.group_id]
|
||||
}
|
||||
],
|
||||
Placement=placement,
|
||||
UserData=userdata,
|
||||
KeyName=key_pair.name
|
||||
)
|
||||
for instance in self.instances:
|
||||
self.track_resource(instance)
|
||||
|
||||
def wait_until_ready(self):
|
||||
waited = 0
|
||||
eprint("Waiting for instances to start...")
|
||||
for instance in self.instances:
|
||||
instance.wait_until_running()
|
||||
instance.reload()
|
||||
eprint("Waiting for SSH to be ready...")
|
||||
self.connections = [None] * len(self.instances)
|
||||
while waited < MAX_WAIT:
|
||||
for i, instance in enumerate(self.instances):
|
||||
if self.connections[i]:
|
||||
continue
|
||||
try:
|
||||
self.connections[i] = self._connect(instance)
|
||||
except:
|
||||
pass
|
||||
if min(map(bool, self.connections)):
|
||||
break
|
||||
time.sleep(1.0)
|
||||
waited += 1
|
||||
eprint("Waiting for instances to finish setup...")
|
||||
ready = [False] * len(self.connections)
|
||||
while waited < MAX_WAIT:
|
||||
for i, con in enumerate(self.connections):
|
||||
if ready[i]:
|
||||
continue
|
||||
try:
|
||||
run_cmd(con, 'test -f /var/lib/cloud/instance/boot-finished')
|
||||
ready[i] = True
|
||||
except:
|
||||
pass
|
||||
if min(map(bool, ready)):
|
||||
break
|
||||
time.sleep(1.0)
|
||||
waited += 1
|
||||
if waited >= MAX_WAIT:
|
||||
raise Exception("Waited too long")
|
||||
if self.vpncloud_file:
|
||||
eprint("Uploading vpncloud binary")
|
||||
for con in self.connections:
|
||||
upload(con, self.vpncloud_file, 'vpncloud')
|
||||
run_cmd(con, 'chmod +x vpncloud')
|
||||
run_cmd(con, 'sudo mv vpncloud /usr/bin/vpncloud')
|
||||
|
||||
|
||||
def terminate(self):
|
||||
if not self.resources:
|
||||
return
|
||||
eprint("Closing connections...")
|
||||
for con in self.connections:
|
||||
if con:
|
||||
con.close()
|
||||
self.connections = []
|
||||
eprint("Terminating instances...")
|
||||
for instance in self.instances:
|
||||
instance.terminate()
|
||||
for instance in self.instances:
|
||||
eprint("\t{}".format(instance.id))
|
||||
instance.wait_until_terminated()
|
||||
self.instances = []
|
||||
eprint("Deleting resources...")
|
||||
ec2client = boto3.client('ec2', region_name=self.region)
|
||||
for res in reversed(self.resources):
|
||||
eprint("\t{} {}".format(res.__class__.__name__, res.id if hasattr(res, "id") else ""))
|
||||
if isinstance(res, SpotInstanceRequest):
|
||||
ec2client.cancel_spot_instance_requests(SpotInstanceRequestIds=[res.id])
|
||||
if hasattr(res, "attachments"):
|
||||
for a in res.attachments:
|
||||
res.detach_from_vpc(VpcId=a['VpcId'])
|
||||
if hasattr(res, "delete"):
|
||||
res.delete()
|
||||
self.resources = []
|
||||
|
||||
def _connect(self, instance):
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect(hostname=instance.public_dns_name, username=self.username, pkey=self.rsa_key, timeout=1.0, banner_timeout=1.0)
|
||||
return client
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from common import EC2Environment, CREATE
|
||||
import time
|
||||
|
||||
setup = EC2Environment(
|
||||
region = "eu-central-1",
|
||||
node_count = 2,
|
||||
instance_type = 't3a.nano',
|
||||
vpncloud_version = "2.1.0"
|
||||
)
|
||||
|
||||
sender = setup.nodes[0]
|
||||
receiver = setup.nodes[1]
|
||||
|
||||
sender.start_vpncloud(ip="10.0.0.1/24")
|
||||
receiver.start_vpncloud(ip="10.0.0.2/24", peers=["{}:3210".format(sender.private_ip)])
|
||||
time.sleep(1.0)
|
||||
|
||||
sender.ping("10.0.0.2")
|
||||
|
||||
sender.stop_vpncloud()
|
||||
receiver.stop_vpncloud()
|
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-0a02ee601d742e89f",
|
||||
"version": "1.0.0",
|
||||
"duration": 495.34057664871216
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9680305000.0,
|
||||
"cpu_sender": 20.862659,
|
||||
"cpu_receiver": 65.856166
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.035,
|
||||
"rtt_max": 0.121,
|
||||
"rtt_avg": 0.04,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.036,
|
||||
"rtt_max": 0.151,
|
||||
"rtt_avg": 0.042,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.039,
|
||||
"rtt_max": 0.145,
|
||||
"rtt_avg": 0.043,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 4836527000.0,
|
||||
"cpu_sender": 9.380104,
|
||||
"cpu_receiver": 65.333537
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.06,
|
||||
"rtt_max": 0.165,
|
||||
"rtt_avg": 0.074,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.063,
|
||||
"rtt_max": 0.389,
|
||||
"rtt_avg": 0.076,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.066,
|
||||
"rtt_max": 0.185,
|
||||
"rtt_avg": 0.077,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3312712000.0,
|
||||
"cpu_sender": 7.639525,
|
||||
"cpu_receiver": 54.563243
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.062,
|
||||
"rtt_max": 1.352,
|
||||
"rtt_avg": 0.075,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.064,
|
||||
"rtt_max": 0.163,
|
||||
"rtt_avg": 0.077,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.067,
|
||||
"rtt_max": 0.334,
|
||||
"rtt_avg": 0.079,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 2418750000.0,
|
||||
"cpu_sender": 4.331642,
|
||||
"cpu_receiver": 49.452792
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.065,
|
||||
"rtt_max": 0.182,
|
||||
"rtt_avg": 0.08,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.072,
|
||||
"rtt_max": 0.201,
|
||||
"rtt_avg": 0.086,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.075,
|
||||
"rtt_max": 0.38,
|
||||
"rtt_avg": 0.09,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9680.305,
|
||||
"plain": 4836.527,
|
||||
"aes256": 3312.712,
|
||||
"chacha20": 2418.75
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 16.999999999999996,
|
||||
"500": 16.999999999999996,
|
||||
"1000": 17.0
|
||||
},
|
||||
"aes256": {
|
||||
"100": 17.499999999999996,
|
||||
"500": 17.499999999999996,
|
||||
"1000": 18.000000000000004
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 20.0,
|
||||
"500": 21.999999999999996,
|
||||
"1000": 23.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-0a02ee601d742e89f",
|
||||
"version": "1.1.0",
|
||||
"duration": 495.3976471424103
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9672876000.0,
|
||||
"cpu_sender": 21.627575,
|
||||
"cpu_receiver": 68.637173
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.049,
|
||||
"rtt_max": 0.869,
|
||||
"rtt_avg": 0.057,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.051,
|
||||
"rtt_max": 13.136,
|
||||
"rtt_avg": 0.059,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.054,
|
||||
"rtt_max": 0.179,
|
||||
"rtt_avg": 0.06,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 4983797000.0,
|
||||
"cpu_sender": 11.27702,
|
||||
"cpu_receiver": 65.580003
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.077,
|
||||
"rtt_max": 0.24,
|
||||
"rtt_avg": 0.093,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.078,
|
||||
"rtt_max": 13.188,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.083,
|
||||
"rtt_max": 0.223,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3226869000.0,
|
||||
"cpu_sender": 5.683309,
|
||||
"cpu_receiver": 54.09244
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.077,
|
||||
"rtt_max": 1.533,
|
||||
"rtt_avg": 0.095,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.082,
|
||||
"rtt_max": 13.21,
|
||||
"rtt_avg": 0.098,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.084,
|
||||
"rtt_max": 0.249,
|
||||
"rtt_avg": 0.099,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 2740356000.0,
|
||||
"cpu_sender": 7.041418,
|
||||
"cpu_receiver": 56.705647
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 0.299,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.087,
|
||||
"rtt_max": 13.089,
|
||||
"rtt_avg": 0.104,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.092,
|
||||
"rtt_max": 0.36,
|
||||
"rtt_avg": 0.108,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9672.876,
|
||||
"plain": 4983.797,
|
||||
"aes256": 3226.869,
|
||||
"chacha20": 2740.356
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 18.0,
|
||||
"500": 18.500000000000004,
|
||||
"1000": 18.500000000000004
|
||||
},
|
||||
"aes256": {
|
||||
"100": 19.0,
|
||||
"500": 19.500000000000004,
|
||||
"1000": 19.500000000000004
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 19.5,
|
||||
"500": 22.5,
|
||||
"1000": 24.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-0a02ee601d742e89f",
|
||||
"version": "1.2.0",
|
||||
"duration": 495.3082287311554
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9679928000.0,
|
||||
"cpu_sender": 14.518989,
|
||||
"cpu_receiver": 75.510689
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.044,
|
||||
"rtt_max": 0.212,
|
||||
"rtt_avg": 0.052,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.049,
|
||||
"rtt_max": 0.368,
|
||||
"rtt_avg": 0.053,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.049,
|
||||
"rtt_max": 0.422,
|
||||
"rtt_avg": 0.055,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 5670752000.0,
|
||||
"cpu_sender": 15.711913,
|
||||
"cpu_receiver": 69.661585
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.076,
|
||||
"rtt_max": 0.298,
|
||||
"rtt_avg": 0.092,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.269,
|
||||
"rtt_avg": 0.094,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.469,
|
||||
"rtt_avg": 0.095,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3531058000.0,
|
||||
"cpu_sender": 5.652232,
|
||||
"cpu_receiver": 58.238361
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.08,
|
||||
"rtt_max": 0.849,
|
||||
"rtt_avg": 0.095,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.08,
|
||||
"rtt_max": 0.205,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.083,
|
||||
"rtt_max": 0.418,
|
||||
"rtt_avg": 0.099,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 2384829000.0,
|
||||
"cpu_sender": 3.995289,
|
||||
"cpu_receiver": 47.656852
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.08,
|
||||
"rtt_max": 0.2,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.087,
|
||||
"rtt_max": 0.223,
|
||||
"rtt_avg": 0.102,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.092,
|
||||
"rtt_max": 0.416,
|
||||
"rtt_avg": 0.109,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9679.928,
|
||||
"plain": 5670.752,
|
||||
"aes256": 3531.058,
|
||||
"chacha20": 2384.829
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 20.0,
|
||||
"500": 20.5,
|
||||
"1000": 20.0
|
||||
},
|
||||
"aes256": {
|
||||
"100": 21.5,
|
||||
"500": 21.5,
|
||||
"1000": 22.000000000000004
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 22.000000000000004,
|
||||
"500": 24.499999999999996,
|
||||
"1000": 27.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-0a02ee601d742e89f",
|
||||
"version": "1.3.0",
|
||||
"duration": 495.4212408065796
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9673825000.0,
|
||||
"cpu_sender": 14.002781,
|
||||
"cpu_receiver": 74.93156
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.04,
|
||||
"rtt_max": 0.235,
|
||||
"rtt_avg": 0.047,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.042,
|
||||
"rtt_max": 0.161,
|
||||
"rtt_avg": 0.048,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.043,
|
||||
"rtt_max": 0.36,
|
||||
"rtt_avg": 0.049,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 5335762000.0,
|
||||
"cpu_sender": 14.483975,
|
||||
"cpu_receiver": 66.013613
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.069,
|
||||
"rtt_max": 0.183,
|
||||
"rtt_avg": 0.084,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.074,
|
||||
"rtt_max": 0.185,
|
||||
"rtt_avg": 0.088,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.077,
|
||||
"rtt_max": 0.203,
|
||||
"rtt_avg": 0.089,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3488220000.0,
|
||||
"cpu_sender": 5.49698,
|
||||
"cpu_receiver": 57.458403
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.073,
|
||||
"rtt_max": 1.55,
|
||||
"rtt_avg": 0.087,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.078,
|
||||
"rtt_max": 3.637,
|
||||
"rtt_avg": 0.091,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.233,
|
||||
"rtt_avg": 0.092,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 2784941000.0,
|
||||
"cpu_sender": 4.273576,
|
||||
"cpu_receiver": 59.274818
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.073,
|
||||
"rtt_max": 9.541,
|
||||
"rtt_avg": 0.092,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.084,
|
||||
"rtt_max": 0.196,
|
||||
"rtt_avg": 0.098,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.089,
|
||||
"rtt_max": 0.614,
|
||||
"rtt_avg": 0.104,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9673.825,
|
||||
"plain": 5335.762,
|
||||
"aes256": 3488.22,
|
||||
"chacha20": 2784.941
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 18.500000000000004,
|
||||
"500": 19.999999999999996,
|
||||
"1000": 19.999999999999996
|
||||
},
|
||||
"aes256": {
|
||||
"100": 19.999999999999996,
|
||||
"500": 21.5,
|
||||
"1000": 21.5
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 22.5,
|
||||
"500": 25.0,
|
||||
"1000": 27.499999999999996
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-0a02ee601d742e89f",
|
||||
"version": "1.4.0",
|
||||
"duration": 495.3894383907318
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9679070000.0,
|
||||
"cpu_sender": 12.871267,
|
||||
"cpu_receiver": 71.50818
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.039,
|
||||
"rtt_max": 0.219,
|
||||
"rtt_avg": 0.047,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.041,
|
||||
"rtt_max": 0.184,
|
||||
"rtt_avg": 0.048,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.042,
|
||||
"rtt_max": 0.303,
|
||||
"rtt_avg": 0.049,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 4991320000.0,
|
||||
"cpu_sender": 8.543496,
|
||||
"cpu_receiver": 65.730322
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.071,
|
||||
"rtt_max": 3.557,
|
||||
"rtt_avg": 0.087,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.075,
|
||||
"rtt_max": 0.726,
|
||||
"rtt_avg": 0.088,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.076,
|
||||
"rtt_max": 3.38,
|
||||
"rtt_avg": 0.091,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3824049000.0,
|
||||
"cpu_sender": 8.673973,
|
||||
"cpu_receiver": 62.240793
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.072,
|
||||
"rtt_max": 1.365,
|
||||
"rtt_avg": 0.09,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.078,
|
||||
"rtt_max": 0.236,
|
||||
"rtt_avg": 0.092,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.439,
|
||||
"rtt_avg": 0.094,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 2838553000.0,
|
||||
"cpu_sender": 6.9273,
|
||||
"cpu_receiver": 60.482437
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.075,
|
||||
"rtt_max": 0.783,
|
||||
"rtt_avg": 0.093,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.086,
|
||||
"rtt_max": 0.208,
|
||||
"rtt_avg": 0.098,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.088,
|
||||
"rtt_max": 0.214,
|
||||
"rtt_avg": 0.103,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9679.07,
|
||||
"plain": 4991.32,
|
||||
"aes256": 3824.049,
|
||||
"chacha20": 2838.553
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 19.999999999999996,
|
||||
"500": 19.999999999999996,
|
||||
"1000": 20.999999999999996
|
||||
},
|
||||
"aes256": {
|
||||
"100": 21.5,
|
||||
"500": 22.0,
|
||||
"1000": 22.5
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 23.0,
|
||||
"500": 25.0,
|
||||
"1000": 26.999999999999996
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-00a205cb8e06c3c4e",
|
||||
"version": "2.0.0",
|
||||
"duration": 621.3780446052551
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9681224000.0,
|
||||
"cpu_sender": 13.679709,
|
||||
"cpu_receiver": 71.69651
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.045,
|
||||
"rtt_max": 0.18,
|
||||
"rtt_avg": 0.051,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.047,
|
||||
"rtt_max": 0.184,
|
||||
"rtt_avg": 0.054,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.049,
|
||||
"rtt_max": 0.175,
|
||||
"rtt_avg": 0.056,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 5472962000.0,
|
||||
"cpu_sender": 15.087884,
|
||||
"cpu_receiver": 67.570992
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.078,
|
||||
"rtt_max": 0.257,
|
||||
"rtt_avg": 0.093,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.08,
|
||||
"rtt_max": 0.243,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.08,
|
||||
"rtt_max": 0.591,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3947676000.0,
|
||||
"cpu_sender": 6.859741,
|
||||
"cpu_receiver": 62.826154
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 1.653,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 1.259,
|
||||
"rtt_avg": 0.098,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.082,
|
||||
"rtt_max": 0.257,
|
||||
"rtt_avg": 0.099,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes128": {
|
||||
"iperf": {
|
||||
"throughput": 4200596000.0,
|
||||
"cpu_sender": 10.291266,
|
||||
"cpu_receiver": 64.395908
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 0.294,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.084,
|
||||
"rtt_max": 0.238,
|
||||
"rtt_avg": 0.099,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.086,
|
||||
"rtt_max": 0.291,
|
||||
"rtt_avg": 0.101,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 2854407000.0,
|
||||
"cpu_sender": 5.648368,
|
||||
"cpu_receiver": 58.473016
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.082,
|
||||
"rtt_max": 0.515,
|
||||
"rtt_avg": 0.098,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.089,
|
||||
"rtt_max": 3.457,
|
||||
"rtt_avg": 0.105,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.092,
|
||||
"rtt_max": 0.366,
|
||||
"rtt_avg": 0.108,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9681.224,
|
||||
"plain": 5472.962,
|
||||
"aes256": 3947.676,
|
||||
"aes128": 4200.596,
|
||||
"chacha20": 2854.407
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 21.0,
|
||||
"500": 21.5,
|
||||
"1000": 20.0
|
||||
},
|
||||
"aes256": {
|
||||
"100": 22.500000000000004,
|
||||
"500": 22.000000000000004,
|
||||
"1000": 21.5
|
||||
},
|
||||
"aes128": {
|
||||
"100": 23.000000000000004,
|
||||
"500": 22.500000000000004,
|
||||
"1000": 22.500000000000004
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 23.500000000000004,
|
||||
"500": 25.5,
|
||||
"1000": 26.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-0a6dc7529cd559185",
|
||||
"version": "2.1.0",
|
||||
"duration": 622.053159236908
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9672965000.0,
|
||||
"cpu_sender": 11.936759,
|
||||
"cpu_receiver": 70.348812
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.046,
|
||||
"rtt_max": 0.246,
|
||||
"rtt_avg": 0.053,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.048,
|
||||
"rtt_max": 0.183,
|
||||
"rtt_avg": 0.055,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.05,
|
||||
"rtt_max": 0.272,
|
||||
"rtt_avg": 0.057,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 5728527000.0,
|
||||
"cpu_sender": 11.004746,
|
||||
"cpu_receiver": 67.527328
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.078,
|
||||
"rtt_max": 0.372,
|
||||
"rtt_avg": 0.095,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.078,
|
||||
"rtt_max": 0.272,
|
||||
"rtt_avg": 0.094,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.082,
|
||||
"rtt_max": 0.217,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3706944000.0,
|
||||
"cpu_sender": 6.465523,
|
||||
"cpu_receiver": 60.216674
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.28,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 13.372,
|
||||
"rtt_avg": 0.099,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.086,
|
||||
"rtt_max": 0.358,
|
||||
"rtt_avg": 0.102,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes128": {
|
||||
"iperf": {
|
||||
"throughput": 3876646000.0,
|
||||
"cpu_sender": 6.800352,
|
||||
"cpu_receiver": 61.738244
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.078,
|
||||
"rtt_max": 0.219,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.083,
|
||||
"rtt_max": 0.232,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.087,
|
||||
"rtt_max": 0.327,
|
||||
"rtt_avg": 0.099,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 2917879000.0,
|
||||
"cpu_sender": 5.066722,
|
||||
"cpu_receiver": 55.171241
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 0.283,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.087,
|
||||
"rtt_max": 0.348,
|
||||
"rtt_avg": 0.103,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.088,
|
||||
"rtt_max": 0.309,
|
||||
"rtt_avg": 0.105,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9672.965,
|
||||
"plain": 5728.527,
|
||||
"aes256": 3706.944,
|
||||
"aes128": 3876.646,
|
||||
"chacha20": 2917.879
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 21.0,
|
||||
"500": 19.5,
|
||||
"1000": 19.5
|
||||
},
|
||||
"aes256": {
|
||||
"100": 22.000000000000004,
|
||||
"500": 22.000000000000004,
|
||||
"1000": 22.499999999999996
|
||||
},
|
||||
"aes128": {
|
||||
"100": 21.5,
|
||||
"500": 21.0,
|
||||
"1000": 21.0
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 22.000000000000004,
|
||||
"500": 23.999999999999996,
|
||||
"1000": 23.999999999999996
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-0db9040eb3ab74509",
|
||||
"version": "2.2.0",
|
||||
"duration": 623.0307722091675
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9680235000.0,
|
||||
"cpu_sender": 12.015535,
|
||||
"cpu_receiver": 71.982452
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.049,
|
||||
"rtt_max": 0.219,
|
||||
"rtt_avg": 0.058,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.053,
|
||||
"rtt_max": 0.247,
|
||||
"rtt_avg": 0.059,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.053,
|
||||
"rtt_max": 0.189,
|
||||
"rtt_avg": 0.06,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 5790600000.0,
|
||||
"cpu_sender": 14.109763,
|
||||
"cpu_receiver": 69.727033
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.291,
|
||||
"rtt_avg": 0.094,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.304,
|
||||
"rtt_avg": 0.096,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.082,
|
||||
"rtt_max": 0.367,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3917767000.0,
|
||||
"cpu_sender": 6.439156,
|
||||
"cpu_receiver": 64.267206
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 0.206,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.088,
|
||||
"rtt_max": 0.206,
|
||||
"rtt_avg": 0.1,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.089,
|
||||
"rtt_max": 0.319,
|
||||
"rtt_avg": 0.103,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes128": {
|
||||
"iperf": {
|
||||
"throughput": 3697142000.0,
|
||||
"cpu_sender": 7.417808,
|
||||
"cpu_receiver": 59.433831
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.083,
|
||||
"rtt_max": 0.265,
|
||||
"rtt_avg": 0.097,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 0.369,
|
||||
"rtt_avg": 0.102,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.086,
|
||||
"rtt_max": 0.448,
|
||||
"rtt_avg": 0.102,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 3194412000.0,
|
||||
"cpu_sender": 6.12856,
|
||||
"cpu_receiver": 61.223349
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 0.28,
|
||||
"rtt_avg": 0.098,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.088,
|
||||
"rtt_max": 0.264,
|
||||
"rtt_avg": 0.103,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.092,
|
||||
"rtt_max": 0.204,
|
||||
"rtt_avg": 0.106,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9680.235,
|
||||
"plain": 5790.6,
|
||||
"aes256": 3917.767,
|
||||
"aes128": 3697.142,
|
||||
"chacha20": 3194.412
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 18.0,
|
||||
"500": 18.500000000000004,
|
||||
"1000": 18.500000000000004
|
||||
},
|
||||
"aes256": {
|
||||
"100": 19.5,
|
||||
"500": 20.500000000000004,
|
||||
"1000": 21.5
|
||||
},
|
||||
"aes128": {
|
||||
"100": 19.5,
|
||||
"500": 21.5,
|
||||
"1000": 20.999999999999996
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 20.0,
|
||||
"500": 22.0,
|
||||
"1000": 23.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
{
|
||||
"meta": {
|
||||
"region": "eu-central-1",
|
||||
"instance_type": "m5.large",
|
||||
"ami": "ami-099ccc441b2ef41ec",
|
||||
"version": "2.3.0",
|
||||
"duration": 622.5463161468506
|
||||
},
|
||||
"native": {
|
||||
"iperf": {
|
||||
"throughput": 9529265000.0,
|
||||
"cpu_sender": 11.32918,
|
||||
"cpu_receiver": 61.870429
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.046,
|
||||
"rtt_max": 0.225,
|
||||
"rtt_avg": 0.053,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.047,
|
||||
"rtt_max": 10.98,
|
||||
"rtt_avg": 0.054,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.048,
|
||||
"rtt_max": 0.175,
|
||||
"rtt_avg": 0.056,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"plain": {
|
||||
"iperf": {
|
||||
"throughput": 6388312000.0,
|
||||
"cpu_sender": 16.955082,
|
||||
"cpu_receiver": 72.705695
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.076,
|
||||
"rtt_max": 11.973,
|
||||
"rtt_avg": 0.09,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.08,
|
||||
"rtt_max": 10.95,
|
||||
"rtt_avg": 0.094,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.081,
|
||||
"rtt_max": 1.638,
|
||||
"rtt_avg": 0.095,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes256": {
|
||||
"iperf": {
|
||||
"throughput": 3801851000.0,
|
||||
"cpu_sender": 5.826756,
|
||||
"cpu_receiver": 61.612033
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.075,
|
||||
"rtt_max": 0.9,
|
||||
"rtt_avg": 0.093,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.275,
|
||||
"rtt_avg": 0.091,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.08,
|
||||
"rtt_max": 1.015,
|
||||
"rtt_avg": 0.093,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"aes128": {
|
||||
"iperf": {
|
||||
"throughput": 3880325000.0,
|
||||
"cpu_sender": 6.219277,
|
||||
"cpu_receiver": 62.125445
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.077,
|
||||
"rtt_max": 11.656,
|
||||
"rtt_avg": 0.09,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.08,
|
||||
"rtt_max": 0.211,
|
||||
"rtt_avg": 0.095,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.082,
|
||||
"rtt_max": 1.398,
|
||||
"rtt_avg": 0.095,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"chacha20": {
|
||||
"iperf": {
|
||||
"throughput": 3126447000.0,
|
||||
"cpu_sender": 5.113819,
|
||||
"cpu_receiver": 58.58095
|
||||
},
|
||||
"ping_100": {
|
||||
"rtt_min": 0.079,
|
||||
"rtt_max": 0.271,
|
||||
"rtt_avg": 0.091,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_500": {
|
||||
"rtt_min": 0.083,
|
||||
"rtt_max": 0.272,
|
||||
"rtt_avg": 0.098,
|
||||
"pkt_loss": 0.0
|
||||
},
|
||||
"ping_1000": {
|
||||
"rtt_min": 0.087,
|
||||
"rtt_max": 1.615,
|
||||
"rtt_avg": 0.101,
|
||||
"pkt_loss": 0.0
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"throughput_mbits": {
|
||||
"native": 9529.265,
|
||||
"plain": 6388.312,
|
||||
"aes256": 3801.851,
|
||||
"aes128": 3880.325,
|
||||
"chacha20": 3126.447
|
||||
},
|
||||
"latency_us": {
|
||||
"plain": {
|
||||
"100": 18.5,
|
||||
"500": 20.0,
|
||||
"1000": 19.5
|
||||
},
|
||||
"aes256": {
|
||||
"100": 20.0,
|
||||
"500": 18.5,
|
||||
"1000": 18.5
|
||||
},
|
||||
"aes128": {
|
||||
"100": 18.5,
|
||||
"500": 20.5,
|
||||
"1000": 19.5
|
||||
},
|
||||
"chacha20": {
|
||||
"100": 19.0,
|
||||
"500": 22.000000000000004,
|
||||
"1000": 22.500000000000004
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from common import EC2Environment, CREATE, eprint
|
||||
import time, json
|
||||
from datetime import date
|
||||
|
||||
|
||||
# Note: this script will run for ~8 minutes and incur costs of about $ 0.02
|
||||
|
||||
FILE = "../../target/release/vpncloud"
|
||||
VERSION = "2.4.0"
|
||||
REGION = "eu-central-1"
|
||||
|
||||
env = EC2Environment(
|
||||
region = REGION,
|
||||
node_count = 2,
|
||||
instance_type = "m5.large",
|
||||
use_spot = False,
|
||||
max_price = "0.08", # USD per hour per VM
|
||||
vpncloud_version = VERSION,
|
||||
vpncloud_file = FILE,
|
||||
cluster_nodes = True,
|
||||
subnet = CREATE,
|
||||
keyname = CREATE
|
||||
)
|
||||
|
||||
|
||||
CRYPTO = ["plain", "aes256", "aes128", "chacha20"]
|
||||
|
||||
|
||||
class PerfTest:
|
||||
def __init__(self, sender, receiver, meta):
|
||||
self.sender = sender
|
||||
self.receiver = receiver
|
||||
self.sender_ip_vpncloud = "10.0.0.1"
|
||||
self.receiver_ip_vpncloud = "10.0.0.2"
|
||||
self.meta = meta
|
||||
|
||||
@classmethod
|
||||
def from_ec2_env(cls, env):
|
||||
meta = {
|
||||
"region": env.region,
|
||||
"instance_type": env.instance_type,
|
||||
"ami": env.ami,
|
||||
"version": env.vpncloud_version
|
||||
}
|
||||
return cls(env.nodes[0], env.nodes[1], meta)
|
||||
|
||||
def run_ping(self, dst, size):
|
||||
eprint("\tRunning ping {} with size {} ...".format(dst, size))
|
||||
return self.sender.ping(dst=dst, size=size, count=30000, interval=0.001)
|
||||
|
||||
def run_iperf(self, dst):
|
||||
eprint("\tRunning iperf on {} ...".format(dst))
|
||||
self.receiver.start_iperf_server()
|
||||
time.sleep(0.1)
|
||||
result = self.sender.run_iperf(dst=dst, duration=30)
|
||||
self.receiver.stop_iperf_server()
|
||||
return result
|
||||
|
||||
def run_suite(self, dst):
|
||||
return {
|
||||
"iperf": self.run_iperf(dst),
|
||||
"ping_100": self.run_ping(dst, 100),
|
||||
"ping_500": self.run_ping(dst, 500),
|
||||
"ping_1000": self.run_ping(dst, 1000),
|
||||
}
|
||||
|
||||
def start_vpncloud(self, crypto=None):
|
||||
eprint("\tSetting up vpncloud on receiver")
|
||||
self.receiver.start_vpncloud(crypto=crypto, ip="{}/24".format(self.receiver_ip_vpncloud))
|
||||
eprint("\tSetting up vpncloud on sender")
|
||||
self.sender.start_vpncloud(crypto=crypto, peers=["{}:3210".format(self.receiver.private_ip)], ip="{}/24".format(self.sender_ip_vpncloud))
|
||||
time.sleep(1.0)
|
||||
|
||||
def stop_vpncloud(self):
|
||||
self.sender.stop_vpncloud(wait=False)
|
||||
self.receiver.stop_vpncloud(wait=True)
|
||||
|
||||
def run(self):
|
||||
eprint("Testing native network")
|
||||
results = {
|
||||
"meta": self.meta,
|
||||
"native": self.run_suite(self.receiver.private_ip)
|
||||
}
|
||||
for crypto in CRYPTO:
|
||||
eprint("Running with crypto {}".format(crypto))
|
||||
self.start_vpncloud(crypto=crypto)
|
||||
res = self.run_suite(self.receiver_ip_vpncloud)
|
||||
self.stop_vpncloud()
|
||||
results[str(crypto)] = res
|
||||
results['results'] = {
|
||||
"throughput_mbits": dict([
|
||||
(k, results[k]["iperf"]["throughput"] / 1000000.0) for k in ["native"] + CRYPTO
|
||||
]),
|
||||
"latency_us": dict([
|
||||
(k, dict([
|
||||
(str(s), (results[k]["ping_%s" % s]["rtt_avg"] - results["native"]["ping_%s" % s]["rtt_avg"])*1000.0/2.0) for s in [100, 500, 1000]
|
||||
])) for k in CRYPTO
|
||||
])
|
||||
}
|
||||
return results
|
||||
|
||||
perf = PerfTest.from_ec2_env(env)
|
||||
|
||||
start = time.time()
|
||||
results = perf.run()
|
||||
duration = time.time() - start
|
||||
|
||||
results["meta"]["duration"] = duration
|
||||
|
||||
name = "measurements/{date}_{version}_perf.json".format(date=date.today().strftime('%Y-%m-%d'), version=VERSION)
|
||||
eprint('Storing results in {}'.format(name))
|
||||
with open(name, 'w') as fp:
|
||||
json.dump(results, fp, indent=2)
|
||||
eprint("done.")
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from common import EC2Environment, CREATE
|
||||
import atexit, argparse, os
|
||||
|
||||
REGION = "eu-central-1"
|
||||
|
||||
VERSION = "2.1.0"
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description='Create a test setup')
|
||||
parser.add_argument('--instancetype', default='t3a.nano', help='EC2 instance type')
|
||||
parser.add_argument('--version', default=VERSION, help='VpnCloud version to use')
|
||||
parser.add_argument('--count', '-c', dest="count", type=int, default=2, help='Number of instance to create')
|
||||
parser.add_argument('--cluster', action="store_true", help='Cluster instances to get reliable throughput')
|
||||
parser.add_argument('--subnet', help='AWS subnet id to use (empty = create new one)')
|
||||
parser.add_argument('--keyname', help='Name of AWS keypair to use (empty = create new one)')
|
||||
parser.add_argument('--keyfile', default="key.pem", help='Path of the private key file')
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
privatekey = None
|
||||
if args.keyname:
|
||||
with open(args.keyfile, 'r') as fp:
|
||||
privatekey = fp.read()
|
||||
|
||||
opts = {}
|
||||
if os.path.exists(args.version):
|
||||
opts["vpncloud_file"] = args.version
|
||||
opts["vpncloud_version"] = None
|
||||
else:
|
||||
opts["vpncloud_version"] = args.version
|
||||
|
||||
setup = EC2Environment(
|
||||
region = REGION,
|
||||
node_count = args.count,
|
||||
instance_type = args.instancetype,
|
||||
cluster_nodes = args.cluster,
|
||||
subnet = args.subnet or CREATE,
|
||||
keyname = args.keyname or CREATE,
|
||||
privatekey = privatekey,
|
||||
**opts
|
||||
)
|
||||
|
||||
if not args.keyname:
|
||||
assert not os.path.exists(args.keyfile)
|
||||
with open(args.keyfile, 'x') as fp:
|
||||
fp.write(setup.privatekey)
|
||||
os.chmod(args.keyfile, 0o400)
|
||||
print("SSH private key written to {}".format(args.keyfile))
|
||||
atexit.register(lambda : os.remove(args.keyfile))
|
||||
print()
|
||||
|
||||
print("Nodes:")
|
||||
for node in setup.nodes:
|
||||
print("\t {}@{}\tprivate: {}".format(setup.username, node.public_ip, node.private_ip))
|
||||
print()
|
||||
|
||||
print("Press ENTER to shut down")
|
||||
input()
|
|
@ -0,0 +1,209 @@
|
|||
# Commands
|
||||
|
||||
Needs [mask](https://github.com/jacobdeichert/mask) to run.
|
||||
|
||||
|
||||
## install-tools
|
||||
|
||||
> Install tools.
|
||||
|
||||
```sh
|
||||
set -e
|
||||
apt-get install -y asciidoctor
|
||||
cargo install cargo-binstall
|
||||
cargo binstall cross cargo-deb cargo-generate-rpm
|
||||
UPX_VERSION=$(grep -e '^upx_version =' Cargo.toml | sed -e 's/upx_version = "\(.*\)"/\1/')
|
||||
curl https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -Lf | tar -xJ --strip-components=1 -C /usr/local/bin
|
||||
```
|
||||
|
||||
## manpage
|
||||
|
||||
> Generate manpage.
|
||||
|
||||
```sh
|
||||
set -e
|
||||
echo >&2 "Generating manpage"
|
||||
if [ ! -f target/vpncloud.1.gz -o vpncloud.adoc -nt target/vpncloud.1.gz ]; then
|
||||
asciidoctor -b manpage -o target/vpncloud.1 vpncloud.adoc
|
||||
gzip -f target/vpncloud.1
|
||||
fi
|
||||
```
|
||||
|
||||
## build-packages-cross (target) (target_name) (target_name_rpm)
|
||||
|
||||
> Build the project packages for a given target.
|
||||
|
||||
```sh
|
||||
set -e
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
TARGET=$target
|
||||
TARGET_DIR=target/$target_name
|
||||
|
||||
# compile
|
||||
echo >&2 "Compiling for $target_name"
|
||||
cross build --release --target $TARGET --target-dir $TARGET_DIR
|
||||
mkdir -p target/$TARGET/release
|
||||
cp $TARGET_DIR/$TARGET/release/vpncloud target/$TARGET/release/
|
||||
|
||||
# build deb
|
||||
echo >&2 "Building deb package"
|
||||
cargo deb --no-build --no-strip --target $TARGET
|
||||
mv target/$TARGET/debian/vpncloud_${VERSION}-1_$target_name.deb dist/vpncloud_${VERSION}_$target_name.deb
|
||||
|
||||
# build rpm
|
||||
if [ -n "$target_name_rpm" ]; then
|
||||
echo >&2 "Building rpm package"
|
||||
cargo generate-rpm --target $TARGET --target-dir $TARGET_DIR
|
||||
mv $TARGET_DIR/$TARGET/generate-rpm/vpncloud-${VERSION}-1.$target_name_rpm.rpm dist/vpncloud_${VERSION}-1.$target_name_rpm.rpm
|
||||
fi
|
||||
```
|
||||
|
||||
## build-amd64-packages
|
||||
|
||||
```sh
|
||||
$MASK build-packages-cross x86_64-unknown-linux-gnu amd64 x86_64
|
||||
```
|
||||
|
||||
## build-i386-packages
|
||||
|
||||
```sh
|
||||
$MASK build-packages-cross i686-unknown-linux-gnu i386 i686
|
||||
```
|
||||
|
||||
## build-arm64-packages
|
||||
|
||||
```sh
|
||||
$MASK build-packages-cross aarch64-unknown-linux-gnu arm64 aarch64
|
||||
```
|
||||
|
||||
## build-armhf-packages
|
||||
|
||||
```sh
|
||||
$MASK build-packages-cross armv7-unknown-linux-gnueabihf armhf ""
|
||||
```
|
||||
|
||||
## build-armel-packages
|
||||
|
||||
```sh
|
||||
$MASK build-packages-cross armv5te-unknown-linux-gnueabi armel ""
|
||||
```
|
||||
|
||||
|
||||
|
||||
## build-static-cross (target) (target_name)
|
||||
|
||||
> Build the project statically for a given target.
|
||||
|
||||
```sh
|
||||
set -e
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
TARGET=$target
|
||||
TARGET_DIR=target/$target_name-musl
|
||||
BIN=$TARGET_DIR/$TARGET/release/vpncloud
|
||||
|
||||
echo >&2 "Compiling for $target_name musl"
|
||||
cross build --release --features installer --target $TARGET --target-dir $TARGET_DIR
|
||||
upx --lzma $BIN
|
||||
cp $BIN dist/vpncloud_${VERSION}_static_$target_name
|
||||
```
|
||||
|
||||
## build-amd64-static
|
||||
|
||||
```sh
|
||||
$MASK build-static-cross x86_64-unknown-linux-musl amd64
|
||||
```
|
||||
|
||||
|
||||
## build-i386-static
|
||||
|
||||
```sh
|
||||
$MASK build-static-cross i686-unknown-linux-musl i386
|
||||
```
|
||||
|
||||
|
||||
## build-arm64-static
|
||||
|
||||
```sh
|
||||
$MASK build-static-cross aarch64-unknown-linux-musl arm64
|
||||
```
|
||||
|
||||
## build-armhf-static
|
||||
|
||||
```sh
|
||||
$MASK build-static-cross armv7-unknown-linux-musleabihf armhf
|
||||
```
|
||||
|
||||
## build-armel-static
|
||||
|
||||
```sh
|
||||
$MASK build-static-cross armv5te-unknown-linux-musleabi armel
|
||||
```
|
||||
|
||||
|
||||
## build
|
||||
|
||||
> Build the project for all architectures.
|
||||
|
||||
```sh
|
||||
set -e
|
||||
$MASK manpage
|
||||
$MASK build-amd64-packages
|
||||
$MASK build-amd64-static
|
||||
$MASK build-i386-packages
|
||||
$MASK build-i386-static
|
||||
$MASK build-arm64-packages
|
||||
$MASK build-arm64-static
|
||||
$MASK build-armhf-packages
|
||||
$MASK build-armhf-static
|
||||
$MASK build-armel-packages
|
||||
$MASK build-armel-static
|
||||
```
|
||||
|
||||
## sign
|
||||
|
||||
> Sign the packages.
|
||||
|
||||
```sh
|
||||
set -e
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
cd dist
|
||||
sha256sum vpncloud_${VERSION}_static_* vpncloud_${VERSION}*.rpm vpncloud_${VERSION}*.deb > vpncloud_${VERSION}_SHA256SUMS.txt
|
||||
gpg --armor --output vpncloud_${VERSION}_SHA256SUMS.txt.asc --detach-sig vpncloud_${VERSION}_SHA256SUMS.txt
|
||||
```
|
||||
|
||||
## test
|
||||
|
||||
> Test the project.
|
||||
|
||||
```sh
|
||||
cargo test --all-features
|
||||
```
|
||||
|
||||
## release
|
||||
|
||||
> Release the project.
|
||||
|
||||
```sh
|
||||
set -e
|
||||
|
||||
$MASK test
|
||||
nano Cargo.toml
|
||||
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
|
||||
nano CHANGELOG.md
|
||||
nano assets/changelog.txt
|
||||
$MASK build
|
||||
$MASK sign
|
||||
git commit -a
|
||||
cargo publish
|
||||
git tag v$VERSION
|
||||
git push --tags
|
||||
```
|
||||
|
||||
|
||||
## count
|
||||
|
||||
> Count the lines of code.
|
||||
|
||||
```sh
|
||||
tokei
|
||||
```
|
21
perf.sh
21
perf.sh
|
@ -1,21 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
DST=$1
|
||||
|
||||
echo
|
||||
echo "----------"
|
||||
echo "Throughput"
|
||||
echo "----------"
|
||||
for i in 0 1 2; do
|
||||
iperf -c $DST -t 30
|
||||
done
|
||||
|
||||
for size in 100 500 1000; do
|
||||
echo
|
||||
echo "--------------------"
|
||||
echo "Latency ($size Bytes)"
|
||||
echo "--------------------"
|
||||
for i in 0 1 2 3 4; do
|
||||
ping $DST -c 30000 -i 0.001 -s $size -U -q
|
||||
done
|
||||
done
|
|
@ -14,4 +14,6 @@ wrap_comments = true
|
|||
overflow_delimited_expr = true
|
||||
blank_lines_upper_bound = 2
|
||||
normalize_doc_attributes = true
|
||||
inline_attribute_width = 50
|
||||
inline_attribute_width = 50
|
||||
edition = "2018"
|
||||
reorder_impl_items = true
|
172
src/beacon.rs
172
src/beacon.rs
|
@ -1,5 +1,5 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2019-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use ring::digest;
|
||||
|
@ -15,15 +15,15 @@ use std::{
|
|||
process::{Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread
|
||||
thread,
|
||||
};
|
||||
|
||||
use super::util::{from_base62, to_base62, Encoder, TimeSource};
|
||||
use smallvec::SmallVec;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
|
||||
const TYPE_BEGIN: u8 = 0;
|
||||
const TYPE_END: u8 = 1;
|
||||
const TYPE_DATA: u8 = 2;
|
||||
|
@ -33,30 +33,28 @@ fn base_62_sanitize(data: &str) -> String {
|
|||
data.chars().filter(|c| c.is_ascii_alphanumeric()).collect()
|
||||
}
|
||||
|
||||
fn sha512(data: &[u8]) -> Vec<u8> {
|
||||
digest::digest(&digest::SHA512, data).as_ref().to_vec()
|
||||
fn sha512(data: &[u8]) -> SmallVec<[u8; 64]> {
|
||||
digest::digest(&digest::SHA512, data).as_ref().into()
|
||||
}
|
||||
|
||||
struct FutureResult<T> {
|
||||
has_result: AtomicBool,
|
||||
result: Mutex<T>
|
||||
result: Mutex<T>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BeaconSerializer<TS> {
|
||||
magic: Vec<u8>,
|
||||
shared_key: Vec<u8>,
|
||||
future_peers: Arc<FutureResult<Vec<SocketAddr>>>,
|
||||
_dummy_ts: PhantomData<TS>
|
||||
_dummy_ts: PhantomData<TS>,
|
||||
}
|
||||
|
||||
impl<TS: TimeSource> BeaconSerializer<TS> {
|
||||
pub fn new(magic: &[u8], shared_key: &[u8]) -> Self {
|
||||
pub fn new(shared_key: &[u8]) -> Self {
|
||||
Self {
|
||||
magic: magic.to_owned(),
|
||||
shared_key: shared_key.to_owned(),
|
||||
future_peers: Arc::new(FutureResult { has_result: AtomicBool::new(false), result: Mutex::new(Vec::new()) }),
|
||||
_dummy_ts: PhantomData
|
||||
_dummy_ts: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,10 +62,9 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
((TS::now() / 3600) & 0xffff) as u16
|
||||
}
|
||||
|
||||
fn get_keystream(&self, type_: u8, seed: u8, iter: u8) -> Vec<u8> {
|
||||
let mut data = Vec::new();
|
||||
fn get_keystream(&self, type_: u8, seed: u8, iter: u8) -> SmallVec<[u8; 64]> {
|
||||
let mut data = SmallVec::<[u8; 128]>::new();
|
||||
data.extend_from_slice(&[type_, seed, iter]);
|
||||
data.extend_from_slice(&self.magic);
|
||||
data.extend_from_slice(&self.shared_key);
|
||||
sha512(&data)
|
||||
}
|
||||
|
@ -95,6 +92,7 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
fn end(&self) -> String {
|
||||
to_base62(&self.get_keystream(TYPE_END, 0, 0))[0..5].to_string()
|
||||
}
|
||||
|
||||
fn encrypt_data(&self, data: &mut Vec<u8>) {
|
||||
// Note: the 1 byte seed is only meant to protect from random changes,
|
||||
// not malicious ones. For full protection, at least 8 bytes (~12
|
||||
|
@ -106,7 +104,7 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
|
||||
fn decrypt_data(&self, data: &mut Vec<u8>) -> bool {
|
||||
if data.is_empty() {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
let seed = data.pop().unwrap() ^ self.get_keystream(TYPE_SEED, 0, 0)[0];
|
||||
self.mask_with_keystream(data as &mut [u8], TYPE_DATA, seed);
|
||||
|
@ -118,12 +116,12 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
// Add timestamp
|
||||
data.extend_from_slice(&Self::now_hour_16().to_be_bytes());
|
||||
// Split addresses into v4 and v6
|
||||
let mut v4addrs = Vec::new();
|
||||
let mut v6addrs = Vec::new();
|
||||
let mut v4addrs = SmallVec::<[SocketAddrV4; 256]>::new();
|
||||
let mut v6addrs = SmallVec::<[SocketAddrV6; 256]>::new();
|
||||
for p in peers {
|
||||
match *p {
|
||||
SocketAddr::V4(addr) => v4addrs.push(addr),
|
||||
SocketAddr::V6(addr) => v6addrs.push(addr)
|
||||
SocketAddr::V6(addr) => v6addrs.push(addr),
|
||||
}
|
||||
}
|
||||
// Add count of v4 addresses
|
||||
|
@ -159,23 +157,23 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
let mut peers = Vec::new();
|
||||
let mut pos = 0;
|
||||
if data.len() < 4 {
|
||||
return peers
|
||||
return peers;
|
||||
}
|
||||
if !self.decrypt_data(&mut data) {
|
||||
return peers
|
||||
return peers;
|
||||
}
|
||||
let then = Wrapping(Encoder::read_u16(&data[pos..=pos + 1]));
|
||||
if let Some(ttl) = ttl_hours {
|
||||
let now = Wrapping(Self::now_hour_16());
|
||||
if now - then > Wrapping(ttl) && then - now > Wrapping(ttl) {
|
||||
return peers
|
||||
return peers;
|
||||
}
|
||||
}
|
||||
pos += 2;
|
||||
let v4count = data[pos] as usize;
|
||||
pos += 1;
|
||||
if v4count * 6 > data.len() - pos || (data.len() - pos - v4count * 6) % 18 > 0 {
|
||||
return peers
|
||||
return peers;
|
||||
}
|
||||
for _ in 0..v4count {
|
||||
assert!(data.len() >= pos + 6);
|
||||
|
@ -199,7 +197,7 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
Ipv6Addr::new(ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]),
|
||||
port,
|
||||
0,
|
||||
0
|
||||
0,
|
||||
));
|
||||
peers.push(addr);
|
||||
}
|
||||
|
@ -230,7 +228,7 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
let beacon = format!("{}{}{}", begin, data, end);
|
||||
debug!("Calling beacon command: {}", cmd);
|
||||
let process = Command::new("sh")
|
||||
.args(&["-c", cmd])
|
||||
.args(["-c", cmd])
|
||||
.env("begin", begin)
|
||||
.env("data", data)
|
||||
.env("end", end)
|
||||
|
@ -263,14 +261,14 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
peers.append(&mut self.peerlist_decode(&data[start_pos..end_pos], ttl_hours));
|
||||
pos = start_pos
|
||||
} else {
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
peers
|
||||
}
|
||||
|
||||
pub fn read_from_file<P: AsRef<Path>>(
|
||||
&self, path: P, ttl_hours: Option<u16>
|
||||
&self, path: P, ttl_hours: Option<u16>,
|
||||
) -> Result<Vec<SocketAddr>, io::Error> {
|
||||
let mut f = File::open(&path)?;
|
||||
let mut contents = String::new();
|
||||
|
@ -283,7 +281,7 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
let end = self.end();
|
||||
debug!("Calling beacon command: {}", cmd);
|
||||
let process = Command::new("sh")
|
||||
.args(&["-c", cmd])
|
||||
.args(["-c", cmd])
|
||||
.env("begin", begin)
|
||||
.env("end", end)
|
||||
.stdout(Stdio::piped())
|
||||
|
@ -317,133 +315,117 @@ impl<TS: TimeSource> BeaconSerializer<TS> {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)] use crate::util::MockTimeSource;
|
||||
#[cfg(test)] use std::str::FromStr;
|
||||
#[cfg(test)] use std::time::Duration;
|
||||
#[cfg(test)]
|
||||
use crate::util::MockTimeSource;
|
||||
#[cfg(test)]
|
||||
use std::str::FromStr;
|
||||
#[cfg(test)]
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn encode() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
assert_eq!("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", ser.encode(&peers));
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
let mut peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:53").unwrap()];
|
||||
assert_eq!("WsHI31EWDMBYxvITiILIrm2k9gEik22E", ser.encode(&peers));
|
||||
peers.push(SocketAddr::from_str("[::1]:5678").unwrap());
|
||||
assert_eq!("3hRD8BKvg7jotek0FGLeYtIc1zj7jzPRyQscQAe9tCqnFJ0vyVfIxYMB", ser.encode(&peers));
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:54").unwrap());
|
||||
assert_eq!("3hRD86NwMC5dPp8bh5idzhMal4AIxYMB", ser.encode(&peers));
|
||||
assert_eq!("WsHI3GXKaXCveo6uejmZizZ72kR6Y0L9T7h49TXONp1ugfKvvvEik22E", ser.encode(&peers));
|
||||
let peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:54").unwrap()];
|
||||
assert_eq!("WsHI32gm9eMSHP3Lm1GXcdP7rD3ik22E", ser.encode(&peers));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
assert_eq!(format!("{:?}", peers), format!("{:?}", ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", None)));
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
let mut peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:53").unwrap()];
|
||||
assert_eq!(format!("{:?}", peers), format!("{:?}", ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", None)));
|
||||
peers.push(SocketAddr::from_str("[::1]:5678").unwrap());
|
||||
assert_eq!(
|
||||
format!("{:?}", peers),
|
||||
format!("{:?}", ser.decode("3hRD8BKvg7jotek0FGLeYtIc1zj7jzPRyQscQAe9tCqnFJ0vyVfIxYMB", None))
|
||||
format!("{:?}", ser.decode("WsHI3GXKaXCveo6uejmZizZ72kR6Y0L9T7h49TXONp1ugfKvvvEik22E", None))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_split() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
let peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:53").unwrap()];
|
||||
assert_eq!(
|
||||
format!("{:?}", peers),
|
||||
format!("{:?}", ser.decode("3hRD8-5V.3h:1P 0g\t5U\nn9(ZW)no[qR]Doü7ZäIxYMB", None))
|
||||
format!("{:?}", ser.decode("WsHI3-1E.WD:MB Yx\tvI\nTi(IL)Ir[m2]k9ügEäik22E", None))
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", peers),
|
||||
format!("{:?}", ser.decode("3 -, \nhR--D85V3h1P0g5Un9ZWnoqRDo7ZI(x}YÖÄÜ\nMB", None))
|
||||
format!("{:?}", ser.decode("W -, \nsH--I31EWDMBYxvITiILIrm2k9gEi(k)2ÖÄÜ\n2E", None))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_offset() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
let peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:53").unwrap()];
|
||||
assert_eq!(
|
||||
format!("{:?}", peers),
|
||||
format!("{:?}", ser.decode("Hello World: 3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB! End of the World", None))
|
||||
format!("{:?}", ser.decode("Hello World: WsHI31EWDMBYxvITiILIrm2k9gEik22E! End of the World", None))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_multiple() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
let peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:53").unwrap()];
|
||||
assert_eq!(
|
||||
format!("{:?}", peers),
|
||||
format!("{:?}", ser.decode("3hRD850fTOmqFffvcJEIxYMB 3hRD823uwTS47pupeONIxYMB", None))
|
||||
format!("{:?}", ser.decode("WsHI31HVpqxFNMNSPrvik22E WsHI34yOBcZIulKdtn2ik22E", None))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ttl() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
assert_eq!(2, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", None).len());
|
||||
assert_eq!(2, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", None).len());
|
||||
MockTimeSource::set_time(2100 * 3600);
|
||||
assert_eq!(2, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", None).len());
|
||||
assert_eq!(2, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", None).len());
|
||||
MockTimeSource::set_time(2005 * 3600);
|
||||
assert_eq!(2, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", None).len());
|
||||
assert_eq!(2, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", None).len());
|
||||
MockTimeSource::set_time(1995 * 3600);
|
||||
assert_eq!(2, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", None).len());
|
||||
assert_eq!(2, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", None).len());
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
assert_eq!(2, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", Some(24)).len());
|
||||
assert_eq!(2, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", Some(24)).len());
|
||||
MockTimeSource::set_time(1995 * 3600);
|
||||
assert_eq!(2, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", Some(24)).len());
|
||||
assert_eq!(2, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", Some(24)).len());
|
||||
MockTimeSource::set_time(2005 * 3600);
|
||||
assert_eq!(2, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", Some(24)).len());
|
||||
assert_eq!(2, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", Some(24)).len());
|
||||
MockTimeSource::set_time(2100 * 3600);
|
||||
assert_eq!(0, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", Some(24)).len());
|
||||
assert_eq!(0, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", Some(24)).len());
|
||||
MockTimeSource::set_time(1900 * 3600);
|
||||
assert_eq!(0, ser.decode("3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB", Some(24)).len());
|
||||
assert_eq!(0, ser.decode("WsHI31EWDMBYxvITiILIrm2k9gEik22E", Some(24)).len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
assert_eq!(0, ser.decode("", None).len());
|
||||
assert_eq!(0, ser.decode("3hRD8IxYMB", None).len());
|
||||
assert_eq!(0, ser.decode("3hRD8--", None).len());
|
||||
assert_eq!(0, ser.decode("--IxYMB", None).len());
|
||||
assert_eq!(0, ser.decode("3hRD85V3h1P0g5Un8ZWnoqRDo7ZIxYMB", None).len());
|
||||
assert_eq!(2, ser.decode("IxYMB3hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMB3hRD8", None).len());
|
||||
assert_eq!(2, ser.decode("3hRD83hRD85V3h1P0g5Un9ZWnoqRDo7ZIxYMBIxYMB", None).len());
|
||||
assert_eq!(0, ser.decode("WsHI3ik22E", None).len());
|
||||
assert_eq!(0, ser.decode("WsHI3--", None).len());
|
||||
assert_eq!(0, ser.decode("--ik22E", None).len());
|
||||
assert_eq!(0, ser.decode("WsHI32EWDMBYxvITiILIrm2k9gEik22E", None).len());
|
||||
assert_eq!(2, ser.decode("ik22EWsHI31EWDMBYxvITiILIrm2k9gEik22EWsHI3", None).len());
|
||||
assert_eq!(2, ser.decode("WsHI3WsHI31EWDMBYxvITiILIrm2k9gEik22Eik22E", None).len());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn encode_decode() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
let peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:53").unwrap()];
|
||||
let data = ser.encode(&peers);
|
||||
let peers2 = ser.decode(&data, None);
|
||||
assert_eq!(format!("{:?}", peers), format!("{:?}", peers2));
|
||||
|
@ -452,10 +434,8 @@ fn encode_decode() {
|
|||
#[test]
|
||||
fn encode_decode_file() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
let peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:53").unwrap()];
|
||||
let file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
|
||||
assert!(ser.write_to_file(&peers, file.path()).is_ok());
|
||||
let peers2 = ser.read_from_file(file.path(), None);
|
||||
|
@ -466,10 +446,8 @@ fn encode_decode_file() {
|
|||
#[test]
|
||||
fn encode_decode_cmd() {
|
||||
MockTimeSource::set_time(2000 * 3600);
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"vpnc", b"mysecretkey");
|
||||
let mut peers = Vec::new();
|
||||
peers.push(SocketAddr::from_str("1.2.3.4:5678").unwrap());
|
||||
peers.push(SocketAddr::from_str("6.6.6.6:53").unwrap());
|
||||
let ser = BeaconSerializer::<MockTimeSource>::new(b"mysecretkey");
|
||||
let peers = vec![SocketAddr::from_str("1.2.3.4:5678").unwrap(), SocketAddr::from_str("6.6.6.6:53").unwrap()];
|
||||
let file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
|
||||
assert!(ser.write_to_cmd(&peers, &format!("echo $beacon > {}", file.path().display())).is_ok());
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
|
182
src/benches.rs
182
src/benches.rs
|
@ -1,182 +0,0 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use test::Bencher;
|
||||
|
||||
use std::{
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket},
|
||||
str::FromStr
|
||||
};
|
||||
|
||||
use super::{
|
||||
cloud::GenericCloud,
|
||||
config::Config,
|
||||
crypto::{Crypto, CryptoMethod},
|
||||
device::{TunTapDevice, Type},
|
||||
ethernet::{self, SwitchTable},
|
||||
ip::Packet,
|
||||
net::MockSocket,
|
||||
poll::WaitImpl,
|
||||
types::{Address, Protocol, Table},
|
||||
udpmessage::{decode, encode, Message},
|
||||
util::{MockTimeSource, SystemTimeSource, TimeSource},
|
||||
MAGIC
|
||||
};
|
||||
|
||||
#[bench]
|
||||
fn crypto_chacha20(b: &mut Bencher) {
|
||||
let mut crypto = Crypto::from_shared_key(CryptoMethod::ChaCha20, "test");
|
||||
let mut payload = [0; 1500];
|
||||
let header = [0; 8];
|
||||
let mut nonce_bytes = [0; 12];
|
||||
b.iter(|| {
|
||||
let len = crypto.encrypt(&mut payload, 1400, &mut nonce_bytes, &header);
|
||||
assert!(crypto.decrypt(&mut payload[..len], &nonce_bytes, &header).is_ok())
|
||||
});
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn crypto_aes256(b: &mut Bencher) {
|
||||
let mut crypto = Crypto::from_shared_key(CryptoMethod::AES256, "test");
|
||||
let mut payload = [0; 1500];
|
||||
let header = [0; 8];
|
||||
let mut nonce_bytes = [0; 12];
|
||||
b.iter(|| {
|
||||
let len = crypto.encrypt(&mut payload, 1400, &mut nonce_bytes, &header);
|
||||
assert!(crypto.decrypt(&mut payload[..len], &nonce_bytes, &header).is_ok());
|
||||
});
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn message_encode(b: &mut Bencher) {
|
||||
let mut crypto = Crypto::None;
|
||||
let mut payload = [0; 1600];
|
||||
let mut msg = Message::Data(&mut payload, 64, 1464);
|
||||
let mut buf = [0; 1600];
|
||||
b.iter(|| {
|
||||
encode(&mut msg, &mut buf[..], MAGIC, &mut crypto);
|
||||
});
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn message_decode(b: &mut Bencher) {
|
||||
let mut crypto = Crypto::None;
|
||||
let mut payload = [0; 1600];
|
||||
let mut msg = Message::Data(&mut payload, 64, 1464);
|
||||
let mut buf = [0; 1600];
|
||||
let mut res = encode(&mut msg, &mut buf[..], MAGIC, &mut crypto);
|
||||
b.iter(|| {
|
||||
decode(&mut res, MAGIC, &mut crypto).unwrap();
|
||||
});
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn switch_learn(b: &mut Bencher) {
|
||||
let mut table = SwitchTable::<SystemTimeSource>::new(10, 0);
|
||||
let addr = Address::from_str("12:34:56:78:90:ab").unwrap();
|
||||
let peer = "1.2.3.4:5678".to_socket_addrs().unwrap().next().unwrap();
|
||||
b.iter(|| {
|
||||
table.learn(addr.clone(), None, peer);
|
||||
});
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn switch_lookup(b: &mut Bencher) {
|
||||
let mut table = SwitchTable::<SystemTimeSource>::new(10, 0);
|
||||
let addr = Address::from_str("12:34:56:78:90:ab").unwrap();
|
||||
let peer = "1.2.3.4:5678".to_socket_addrs().unwrap().next().unwrap();
|
||||
table.learn(addr.clone(), None, peer);
|
||||
b.iter(|| {
|
||||
table.lookup(&addr);
|
||||
});
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn ethernet_parse(b: &mut Bencher) {
|
||||
let mut data = [0; 1500];
|
||||
data[5] = 45;
|
||||
b.iter(|| ethernet::Frame::parse(&data).unwrap());
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn ipv4_parse(b: &mut Bencher) {
|
||||
let mut data = [0; 1500];
|
||||
data[0] = 4 * 16;
|
||||
b.iter(|| Packet::parse(&data).unwrap());
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn ipv6_parse(b: &mut Bencher) {
|
||||
let mut data = [0; 1500];
|
||||
data[0] = 6 * 16;
|
||||
b.iter(|| Packet::parse(&data).unwrap());
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn now(b: &mut Bencher) {
|
||||
b.iter(|| SystemTimeSource::now());
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn epoll_wait(b: &mut Bencher) {
|
||||
let socket = UdpSocket::bind("[::]:0").unwrap();
|
||||
let device = TunTapDevice::dummy("dummy", "/dev/zero", Type::Dummy).unwrap();
|
||||
let mut waiter = WaitImpl::testing(&socket, &device, 1000).unwrap();
|
||||
b.iter(|| assert!(waiter.next().is_some()));
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
type TestNode = GenericCloud<TunTapDevice, ethernet::Frame, SwitchTable<MockTimeSource>, MockSocket, MockTimeSource>;
|
||||
|
||||
fn create_test_node() -> TestNode {
|
||||
TestNode::new(
|
||||
&Config::default(),
|
||||
TunTapDevice::dummy("dummy", "/dev/null", Type::Tap).unwrap(),
|
||||
SwitchTable::new(1800, 10),
|
||||
true,
|
||||
true,
|
||||
vec![],
|
||||
Crypto::None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn handle_interface_data(b: &mut Bencher) {
|
||||
let mut node = create_test_node();
|
||||
let mut data = [0; 1500];
|
||||
data[105] = 45;
|
||||
b.iter(|| node.handle_interface_data(&mut data, 100, 1400).unwrap());
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn handle_net_message(b: &mut Bencher) {
|
||||
let mut node = create_test_node();
|
||||
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 1));
|
||||
let mut data = [0; 1500];
|
||||
data[105] = 45;
|
||||
b.iter(|| node.handle_net_message(addr.clone(), Message::Data(&mut data, 0, 1400)).unwrap());
|
||||
b.bytes = 1400;
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn udp_send(b: &mut Bencher) {
|
||||
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let data = [0; 1400];
|
||||
let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 1);
|
||||
b.iter(|| sock.send_to(&data, &addr).unwrap());
|
||||
b.bytes = 1400;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
default: libtuntap.a
|
||||
|
||||
tapdev.o: tuntap.c
|
||||
gcc -Os -c tuntap.c
|
||||
|
||||
libtapdev.a: tuntap.o
|
||||
ar rcs libtuntap.a tuntap.o
|
|
@ -1,27 +0,0 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
#include <stdint.h>
|
||||
#include <net/if.h>
|
||||
#include <linux/if_tun.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
int32_t setup_device(int32_t fd, char *ifname, int32_t flags) {
|
||||
struct ifreq ifr;
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
ifr.ifr_flags = flags;
|
||||
strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
|
||||
if (ioctl(fd, TUNSETIFF, (void *)&ifr) < 0) return 1;
|
||||
strncpy(ifname, ifr.ifr_name, IFNAMSIZ);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t setup_tap_device(int32_t fd, char *ifname) {
|
||||
return setup_device(fd, ifname, IFF_TAP | IFF_NO_PI);
|
||||
}
|
||||
|
||||
int32_t setup_tun_device(int32_t fd, char *ifname) {
|
||||
return setup_device(fd, ifname, IFF_TUN | IFF_NO_PI);
|
||||
}
|
1132
src/cloud.rs
1132
src/cloud.rs
File diff suppressed because it is too large
Load Diff
886
src/config.rs
886
src/config.rs
File diff suppressed because it is too large
Load Diff
219
src/crypto.rs
219
src/crypto.rs
|
@ -1,219 +0,0 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::{num::NonZeroU32, str::FromStr};
|
||||
|
||||
use ring::{aead::*, pbkdf2, rand::*};
|
||||
|
||||
use super::types::Error;
|
||||
|
||||
const SALT: &[u8; 32] = b"vpncloudVPNCLOUDvpncl0udVpnCloud";
|
||||
const HEX_PREFIX: &str = "hex:";
|
||||
const HASH_PREFIX: &str = "hash:";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
pub enum CryptoMethod {
|
||||
#[serde(rename = "chacha20")]
|
||||
ChaCha20,
|
||||
#[serde(rename = "aes256")]
|
||||
AES256
|
||||
}
|
||||
impl FromStr for CryptoMethod {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(text: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match &text.to_lowercase() as &str {
|
||||
"chacha20" | "chacha" => Self::ChaCha20,
|
||||
"aes256" | "aes" => Self::AES256,
|
||||
_ => return Err("Unknown method")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CryptoData {
|
||||
crypto_key: LessSafeKey,
|
||||
nonce: Vec<u8>,
|
||||
key: Vec<u8>
|
||||
}
|
||||
|
||||
#[allow(unknown_lints, clippy::large_enum_variant)]
|
||||
pub enum Crypto {
|
||||
None,
|
||||
ChaCha20Poly1305(CryptoData),
|
||||
AES256GCM(CryptoData)
|
||||
}
|
||||
|
||||
fn inc_nonce(nonce: &mut [u8]) {
|
||||
let l = nonce.len();
|
||||
for i in (0..l).rev() {
|
||||
let mut num = nonce[i];
|
||||
num = num.wrapping_add(1);
|
||||
nonce[i] = num;
|
||||
if num > 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
warn!("Nonce overflowed");
|
||||
}
|
||||
|
||||
impl Crypto {
|
||||
#[inline]
|
||||
pub fn method(&self) -> u8 {
|
||||
match *self {
|
||||
Crypto::None => 0,
|
||||
Crypto::ChaCha20Poly1305 { .. } => 1,
|
||||
Crypto::AES256GCM { .. } => 2
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn nonce_bytes(&self) -> usize {
|
||||
match *self {
|
||||
Crypto::None => 0,
|
||||
Crypto::ChaCha20Poly1305(ref data) | Crypto::AES256GCM(ref data) => data.crypto_key.algorithm().nonce_len()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_key(&self) -> &[u8] {
|
||||
match *self {
|
||||
Crypto::None => &[],
|
||||
Crypto::ChaCha20Poly1305(ref data) | Crypto::AES256GCM(ref data) => &data.key
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(unknown_lints, clippy::match_same_arms)]
|
||||
pub fn additional_bytes(&self) -> usize {
|
||||
match *self {
|
||||
Crypto::None => 0,
|
||||
Crypto::ChaCha20Poly1305(ref data) | Crypto::AES256GCM(ref data) => data.crypto_key.algorithm().tag_len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_shared_key(method: CryptoMethod, password: &str) -> Self {
|
||||
let algo = match method {
|
||||
CryptoMethod::ChaCha20 => &CHACHA20_POLY1305,
|
||||
CryptoMethod::AES256 => &AES_256_GCM
|
||||
};
|
||||
let mut key: Vec<u8> = Vec::with_capacity(algo.key_len());
|
||||
for _ in 0..algo.key_len() {
|
||||
key.push(0);
|
||||
}
|
||||
if password.starts_with(HEX_PREFIX) {
|
||||
let password = &password[HEX_PREFIX.len()..];
|
||||
if password.len() != 2 * algo.key_len() {
|
||||
fail!("Raw secret key must be exactly {} bytes long", algo.key_len());
|
||||
}
|
||||
for i in 0..algo.key_len() {
|
||||
key[i] = try_fail!(
|
||||
u8::from_str_radix(&password[2 * i..=2 * i + 1], 16),
|
||||
"Failed to parse raw secret key: {}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let password = if password.starts_with(HASH_PREFIX) { &password[HASH_PREFIX.len()..] } else { password };
|
||||
pbkdf2::derive(
|
||||
pbkdf2::PBKDF2_HMAC_SHA256,
|
||||
NonZeroU32::new(4096).unwrap(),
|
||||
SALT,
|
||||
password.as_bytes(),
|
||||
&mut key
|
||||
);
|
||||
}
|
||||
let crypto_key = LessSafeKey::new(UnboundKey::new(algo, &key[..algo.key_len()]).expect("Failed to create key"));
|
||||
let mut nonce: Vec<u8> = Vec::with_capacity(algo.nonce_len());
|
||||
for _ in 0..algo.nonce_len() {
|
||||
nonce.push(0);
|
||||
}
|
||||
if SystemRandom::new().fill(&mut nonce).is_err() {
|
||||
fail!("Randomizing nonce failed");
|
||||
}
|
||||
// make sure the nonce will not overflow
|
||||
if nonce[0] == 0xff {
|
||||
nonce[0] = 0
|
||||
}
|
||||
let data = CryptoData { crypto_key, nonce, key };
|
||||
match method {
|
||||
CryptoMethod::ChaCha20 => Crypto::ChaCha20Poly1305(data),
|
||||
CryptoMethod::AES256 => Crypto::AES256GCM(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, buf: &mut [u8], nonce: &[u8], header: &[u8]) -> Result<usize, Error> {
|
||||
match *self {
|
||||
Crypto::None => Ok(buf.len()),
|
||||
Crypto::ChaCha20Poly1305(ref data) | Crypto::AES256GCM(ref data) => {
|
||||
let nonce = Nonce::try_assume_unique_for_key(nonce).unwrap();
|
||||
match data.crypto_key.open_in_place(nonce, Aad::from(header), buf) {
|
||||
Ok(plaintext) => Ok(plaintext.len()),
|
||||
Err(_) => Err(Error::Crypto("Failed to decrypt"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&mut self, buf: &mut [u8], mlen: usize, nonce_bytes: &mut [u8], header: &[u8]) -> usize {
|
||||
let tag_len = self.additional_bytes();
|
||||
match *self {
|
||||
Crypto::None => mlen,
|
||||
Crypto::ChaCha20Poly1305(ref mut data) | Crypto::AES256GCM(ref mut data) => {
|
||||
inc_nonce(&mut data.nonce);
|
||||
assert!(buf.len() - mlen >= tag_len);
|
||||
let nonce = Nonce::try_assume_unique_for_key(&data.nonce).unwrap();
|
||||
let tag = data
|
||||
.crypto_key
|
||||
.seal_in_place_separate_tag(nonce, Aad::from(header), &mut buf[..mlen])
|
||||
.expect("Failed to encrypt");
|
||||
buf[mlen..mlen + tag_len].copy_from_slice(tag.as_ref());
|
||||
nonce_bytes.clone_from_slice(&data.nonce);
|
||||
mlen + tag_len
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypt_decrypt_chacha20poly1305() {
|
||||
let mut sender = Crypto::from_shared_key(CryptoMethod::ChaCha20, "test");
|
||||
let receiver = Crypto::from_shared_key(CryptoMethod::ChaCha20, "test");
|
||||
let msg = "HelloWorld0123456789";
|
||||
let msg_bytes = msg.as_bytes();
|
||||
let mut buffer = [0u8; 1024];
|
||||
let header = [0u8; 8];
|
||||
buffer[..msg_bytes.len()].clone_from_slice(&msg_bytes);
|
||||
let mut nonce1 = [0u8; 12];
|
||||
let size = sender.encrypt(&mut buffer, msg_bytes.len(), &mut nonce1, &header);
|
||||
assert_eq!(size, msg_bytes.len() + sender.additional_bytes());
|
||||
assert!(msg_bytes != &buffer[..msg_bytes.len()] as &[u8]);
|
||||
receiver.decrypt(&mut buffer[..size], &nonce1, &header).unwrap();
|
||||
assert_eq!(msg_bytes, &buffer[..msg_bytes.len()] as &[u8]);
|
||||
let mut nonce2 = [0u8; 12];
|
||||
let size = sender.encrypt(&mut buffer, msg_bytes.len(), &mut nonce2, &header);
|
||||
assert!(nonce1 != nonce2);
|
||||
receiver.decrypt(&mut buffer[..size], &nonce2, &header).unwrap();
|
||||
assert_eq!(msg_bytes, &buffer[..msg_bytes.len()] as &[u8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypt_decrypt_aes256() {
|
||||
let mut sender = Crypto::from_shared_key(CryptoMethod::AES256, "test");
|
||||
let receiver = Crypto::from_shared_key(CryptoMethod::AES256, "test");
|
||||
let msg = "HelloWorld0123456789";
|
||||
let msg_bytes = msg.as_bytes();
|
||||
let mut buffer = [0u8; 1024];
|
||||
let header = [0u8; 8];
|
||||
buffer[..msg_bytes.len()].clone_from_slice(&msg_bytes);
|
||||
let mut nonce1 = [0u8; 12];
|
||||
let size = sender.encrypt(&mut buffer, msg_bytes.len(), &mut nonce1, &header);
|
||||
assert_eq!(size, msg_bytes.len() + sender.additional_bytes());
|
||||
assert!(msg_bytes != &buffer[..msg_bytes.len()] as &[u8]);
|
||||
receiver.decrypt(&mut buffer[..size], &nonce1, &header).unwrap();
|
||||
assert_eq!(msg_bytes, &buffer[..msg_bytes.len()] as &[u8]);
|
||||
let mut nonce2 = [0u8; 12];
|
||||
let size = sender.encrypt(&mut buffer, msg_bytes.len(), &mut nonce2, &header);
|
||||
assert!(nonce1 != nonce2);
|
||||
receiver.decrypt(&mut buffer[..size], &nonce2, &header).unwrap();
|
||||
assert_eq!(msg_bytes, &buffer[..msg_bytes.len()] as &[u8]);
|
||||
}
|
|
@ -0,0 +1,507 @@
|
|||
use super::{
|
||||
core::{test_speed, CryptoCore},
|
||||
init::{self, InitResult, InitState, CLOSING},
|
||||
rotate::RotationState,
|
||||
};
|
||||
use crate::{
|
||||
error::Error,
|
||||
types::NodeId,
|
||||
util::{from_base62, to_base62, MsgBuffer},
|
||||
};
|
||||
use ring::{
|
||||
aead::{self, Algorithm, LessSafeKey, UnboundKey},
|
||||
agreement::{EphemeralPrivateKey, UnparsedPublicKey},
|
||||
pbkdf2,
|
||||
rand::{SecureRandom, SystemRandom},
|
||||
signature::{Ed25519KeyPair, KeyPair, ED25519_PUBLIC_KEY_LEN},
|
||||
};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{fmt::Debug, io::Read, num::NonZeroU32, sync::Arc, time::Duration};
|
||||
|
||||
const SALT: &[u8; 32] = b"vpncloudVPNCLOUDvpncl0udVpnCloud";
|
||||
const INIT_MESSAGE_FIRST_BYTE: u8 = 0xff;
|
||||
const MESSAGE_TYPE_ROTATION: u8 = 0x10;
|
||||
|
||||
pub type Ed25519PublicKey = [u8; ED25519_PUBLIC_KEY_LEN];
|
||||
pub type EcdhPublicKey = UnparsedPublicKey<SmallVec<[u8; 96]>>;
|
||||
pub type EcdhPrivateKey = EphemeralPrivateKey;
|
||||
pub type Key = SmallVec<[u8; 32]>;
|
||||
|
||||
const DEFAULT_ALGORITHMS: [&str; 3] = ["AES128", "AES256", "CHACHA20"];
|
||||
|
||||
#[cfg(test)]
|
||||
const SPEED_TEST_TIME: f32 = 0.02;
|
||||
#[cfg(not(test))]
|
||||
const SPEED_TEST_TIME: f32 = 0.1;
|
||||
|
||||
const ROTATE_INTERVAL: usize = 120;
|
||||
|
||||
pub trait Payload: Debug + PartialEq + Sized {
|
||||
fn write_to(&self, buffer: &mut MsgBuffer);
|
||||
fn read_from<R: Read>(r: R) -> Result<Self, Error>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Algorithms {
|
||||
pub algorithm_speeds: SmallVec<[(&'static Algorithm, f32); 3]>,
|
||||
pub allow_unencrypted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
|
||||
pub struct Config {
|
||||
pub password: Option<String>,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
pub trusted_keys: Vec<String>,
|
||||
pub algorithms: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct Crypto {
|
||||
node_id: NodeId,
|
||||
key_pair: Arc<Ed25519KeyPair>,
|
||||
trusted_keys: Arc<[Ed25519PublicKey]>,
|
||||
algorithms: Algorithms,
|
||||
}
|
||||
|
||||
impl Crypto {
|
||||
pub fn parse_algorithms(algos: &[String]) -> Result<(bool, Vec<&'static aead::Algorithm>), Error> {
|
||||
let algorithms = algos.iter().map(|a| a as &str).collect::<Vec<_>>();
|
||||
let allowed = if algorithms.is_empty() { &DEFAULT_ALGORITHMS } else { &algorithms as &[&str] };
|
||||
let mut algos = vec![];
|
||||
let mut unencrypted = false;
|
||||
for name in allowed {
|
||||
let algo = match &name.to_uppercase() as &str {
|
||||
"UNENCRYPTED" | "NONE" | "PLAIN" => {
|
||||
unencrypted = true;
|
||||
continue;
|
||||
}
|
||||
"AES128" | "AES128_GCM" | "AES_128" | "AES_128_GCM" => &aead::AES_128_GCM,
|
||||
"AES256" | "AES256_GCM" | "AES_256" | "AES_256_GCM" => &aead::AES_256_GCM,
|
||||
"CHACHA" | "CHACHA20" | "CHACHA20_POLY1305" => &aead::CHACHA20_POLY1305,
|
||||
_ => return Err(Error::InvalidConfig("Unknown crypto method")),
|
||||
};
|
||||
algos.push(algo)
|
||||
}
|
||||
Ok((unencrypted, algos))
|
||||
}
|
||||
|
||||
pub fn new(node_id: NodeId, config: &Config) -> Result<Self, Error> {
|
||||
let key_pair = if let Some(priv_key) = &config.private_key {
|
||||
if let Some(pub_key) = &config.public_key {
|
||||
Self::parse_keypair(priv_key, pub_key)?
|
||||
} else {
|
||||
Self::parse_private_key(priv_key)?
|
||||
}
|
||||
} else if let Some(password) = &config.password {
|
||||
Self::keypair_from_password(password)
|
||||
} else {
|
||||
return Err(Error::InvalidConfig("Either private_key or password must be set"));
|
||||
};
|
||||
let mut trusted_keys = vec![];
|
||||
for tn in &config.trusted_keys {
|
||||
trusted_keys.push(Self::parse_public_key(tn)?);
|
||||
}
|
||||
if trusted_keys.is_empty() {
|
||||
info!("Trusted keys not set, trusting only own public key");
|
||||
let mut key = [0; ED25519_PUBLIC_KEY_LEN];
|
||||
key.clone_from_slice(key_pair.public_key().as_ref());
|
||||
trusted_keys.push(key);
|
||||
}
|
||||
let (unencrypted, allowed_algos) = Self::parse_algorithms(&config.algorithms)?;
|
||||
if unencrypted {
|
||||
warn!("Crypto settings allow unencrypted connections")
|
||||
}
|
||||
let mut algos = Algorithms { algorithm_speeds: smallvec![], allow_unencrypted: unencrypted };
|
||||
let duration = Duration::from_secs_f32(SPEED_TEST_TIME);
|
||||
let mut speeds = Vec::new();
|
||||
for algo in allowed_algos {
|
||||
let speed = test_speed(algo, &duration);
|
||||
algos.algorithm_speeds.push((algo, speed as f32));
|
||||
speeds.push((format!("{:?}", algo), speed as f32));
|
||||
}
|
||||
if !speeds.is_empty() {
|
||||
info!(
|
||||
"Crypto speeds: {}",
|
||||
speeds.into_iter().map(|(a, s)| format!("{}: {:.1} MiB/s", a, s)).collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
}
|
||||
Ok(Self {
|
||||
node_id,
|
||||
key_pair: Arc::new(key_pair),
|
||||
trusted_keys: trusted_keys.into_boxed_slice().into(),
|
||||
algorithms: algos,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_keypair(password: Option<&str>) -> (String, String) {
|
||||
let mut bytes = [0; 32];
|
||||
match password {
|
||||
None => {
|
||||
let rng = SystemRandom::new();
|
||||
rng.fill(&mut bytes).unwrap();
|
||||
}
|
||||
Some(password) => {
|
||||
pbkdf2::derive(
|
||||
pbkdf2::PBKDF2_HMAC_SHA256,
|
||||
NonZeroU32::new(4096).unwrap(),
|
||||
SALT,
|
||||
password.as_bytes(),
|
||||
&mut bytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
let keypair = Ed25519KeyPair::from_seed_unchecked(&bytes).unwrap();
|
||||
let privkey = to_base62(&bytes);
|
||||
let pubkey = to_base62(keypair.public_key().as_ref());
|
||||
(privkey, pubkey)
|
||||
}
|
||||
|
||||
fn keypair_from_password(password: &str) -> Ed25519KeyPair {
|
||||
let mut key = [0; 32];
|
||||
pbkdf2::derive(pbkdf2::PBKDF2_HMAC_SHA256, NonZeroU32::new(4096).unwrap(), SALT, password.as_bytes(), &mut key);
|
||||
Ed25519KeyPair::from_seed_unchecked(&key).unwrap()
|
||||
}
|
||||
|
||||
fn parse_keypair(privkey: &str, pubkey: &str) -> Result<Ed25519KeyPair, Error> {
|
||||
let privkey = from_base62(privkey).map_err(|_| Error::InvalidConfig("Failed to parse private key"))?;
|
||||
let pubkey = from_base62(pubkey).map_err(|_| Error::InvalidConfig("Failed to parse public key"))?;
|
||||
let keypair = Ed25519KeyPair::from_seed_and_public_key(&privkey, &pubkey)
|
||||
.map_err(|_| Error::InvalidConfig("Keys rejected by crypto library"))?;
|
||||
Ok(keypair)
|
||||
}
|
||||
|
||||
fn parse_private_key(privkey: &str) -> Result<Ed25519KeyPair, Error> {
|
||||
let privkey = from_base62(privkey).map_err(|_| Error::InvalidConfig("Failed to parse private key"))?;
|
||||
let keypair = Ed25519KeyPair::from_seed_unchecked(&privkey)
|
||||
.map_err(|_| Error::InvalidConfig("Key rejected by crypto library"))?;
|
||||
Ok(keypair)
|
||||
}
|
||||
|
||||
fn parse_public_key(pubkey: &str) -> Result<Ed25519PublicKey, Error> {
|
||||
let pubkey = from_base62(pubkey).map_err(|_| Error::InvalidConfig("Failed to parse public key"))?;
|
||||
if pubkey.len() != ED25519_PUBLIC_KEY_LEN {
|
||||
return Err(Error::InvalidConfig("Failed to parse public key"));
|
||||
}
|
||||
let mut result = [0; ED25519_PUBLIC_KEY_LEN];
|
||||
result.clone_from_slice(&pubkey);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn public_key_from_private_key(privkey: &str) -> Result<String, Error> {
|
||||
let keypair = Self::parse_private_key(privkey)?;
|
||||
Ok(to_base62(keypair.public_key().as_ref()))
|
||||
}
|
||||
|
||||
pub fn peer_instance<P: Payload>(&self, payload: P) -> PeerCrypto<P> {
|
||||
PeerCrypto::new(
|
||||
self.node_id,
|
||||
payload,
|
||||
self.key_pair.clone(),
|
||||
self.trusted_keys.clone(),
|
||||
self.algorithms.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum MessageResult<P: Payload> {
|
||||
Message(u8),
|
||||
Initialized(P),
|
||||
InitializedWithReply(P),
|
||||
Reply,
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct PeerCrypto<P: Payload> {
|
||||
#[allow(dead_code)]
|
||||
node_id: NodeId,
|
||||
init: Option<InitState<P>>,
|
||||
rotation: Option<RotationState>,
|
||||
unencrypted: bool,
|
||||
core: Option<CryptoCore>,
|
||||
rotate_counter: usize,
|
||||
}
|
||||
|
||||
impl<P: Payload> PeerCrypto<P> {
|
||||
pub fn new(
|
||||
node_id: NodeId, init_payload: P, key_pair: Arc<Ed25519KeyPair>, trusted_keys: Arc<[Ed25519PublicKey]>,
|
||||
algorithms: Algorithms,
|
||||
) -> Self {
|
||||
Self {
|
||||
node_id,
|
||||
init: Some(InitState::new(node_id, init_payload, key_pair, trusted_keys, algorithms)),
|
||||
rotation: None,
|
||||
unencrypted: false,
|
||||
core: None,
|
||||
rotate_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_init(&mut self) -> Result<&mut InitState<P>, Error> {
|
||||
if let Some(init) = &mut self.init {
|
||||
Ok(init)
|
||||
} else {
|
||||
Err(Error::InvalidCryptoState("Initialization already finished"))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_core(&mut self) -> Result<&mut CryptoCore, Error> {
|
||||
if let Some(core) = &mut self.core {
|
||||
Ok(core)
|
||||
} else {
|
||||
Err(Error::InvalidCryptoState("Crypto core not ready yet"))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rotation(&mut self) -> Result<&mut RotationState, Error> {
|
||||
if let Some(rotation) = &mut self.rotation {
|
||||
Ok(rotation)
|
||||
} else {
|
||||
Err(Error::InvalidCryptoState("Key rotation not initialized"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self, out: &mut MsgBuffer) -> Result<(), Error> {
|
||||
let init = self.get_init()?;
|
||||
if init.stage() != init::STAGE_PING {
|
||||
Err(Error::InvalidCryptoState("Initialization already ongoing"))
|
||||
} else {
|
||||
init.send_ping(out);
|
||||
out.prepend_byte(INIT_MESSAGE_FIRST_BYTE);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_init(&self) -> bool {
|
||||
self.init.is_some()
|
||||
}
|
||||
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.core.is_some()
|
||||
}
|
||||
|
||||
pub fn algorithm_name(&self) -> &'static str {
|
||||
if let Some(ref core) = self.core {
|
||||
let algo = core.algorithm();
|
||||
if algo == &aead::CHACHA20_POLY1305 {
|
||||
"CHACHA20"
|
||||
} else if algo == &aead::AES_128_GCM {
|
||||
"AES128"
|
||||
} else if algo == &aead::AES_256_GCM {
|
||||
"AES256"
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
"PLAIN"
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_init_message(&mut self, buffer: &mut MsgBuffer) -> Result<MessageResult<P>, Error> {
|
||||
let result = self.get_init()?.handle_init(buffer)?;
|
||||
if !buffer.is_empty() {
|
||||
buffer.prepend_byte(INIT_MESSAGE_FIRST_BYTE);
|
||||
}
|
||||
match result {
|
||||
InitResult::Continue => Ok(MessageResult::Reply),
|
||||
InitResult::Success { peer_payload, is_initiator } => {
|
||||
self.core = self.get_init()?.take_core();
|
||||
if self.core.is_none() {
|
||||
self.unencrypted = true;
|
||||
}
|
||||
if self.get_init()?.stage() == init::CLOSING {
|
||||
self.init = None
|
||||
}
|
||||
if self.core.is_some() {
|
||||
self.rotation = Some(RotationState::new(!is_initiator, buffer));
|
||||
}
|
||||
if !is_initiator {
|
||||
if self.unencrypted {
|
||||
return Ok(MessageResult::Initialized(peer_payload));
|
||||
}
|
||||
assert!(!buffer.is_empty());
|
||||
buffer.prepend_byte(MESSAGE_TYPE_ROTATION);
|
||||
self.encrypt_message(buffer)?;
|
||||
}
|
||||
Ok(MessageResult::InitializedWithReply(peer_payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rotate_message(&mut self, data: &[u8]) -> Result<(), Error> {
|
||||
if self.unencrypted {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(rot) = self.get_rotation()?.handle_message(data)? {
|
||||
let core = self.get_core()?;
|
||||
let algo = core.algorithm();
|
||||
let key = LessSafeKey::new(UnboundKey::new(algo, &rot.key[..algo.key_len()]).unwrap());
|
||||
core.rotate_key(key, rot.id, rot.use_for_sending);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encrypt_message(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
|
||||
if self.unencrypted {
|
||||
return Ok(());
|
||||
}
|
||||
self.get_core()?.encrypt(buffer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_message(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
|
||||
// HOT PATH
|
||||
if self.unencrypted {
|
||||
return Ok(());
|
||||
}
|
||||
self.get_core()?.decrypt(buffer)
|
||||
}
|
||||
|
||||
pub fn handle_message(&mut self, buffer: &mut MsgBuffer) -> Result<MessageResult<P>, Error> {
|
||||
// HOT PATH
|
||||
if buffer.is_empty() {
|
||||
return Err(Error::InvalidCryptoState("No message in buffer"));
|
||||
}
|
||||
if is_init_message(buffer.buffer()) {
|
||||
// COLD PATH
|
||||
debug!("Received init message");
|
||||
buffer.take_prefix();
|
||||
self.handle_init_message(buffer)
|
||||
} else {
|
||||
// HOT PATH
|
||||
debug!("Received encrypted message");
|
||||
self.decrypt_message(buffer)?;
|
||||
let msg_type = buffer.take_prefix();
|
||||
if msg_type == MESSAGE_TYPE_ROTATION {
|
||||
// COLD PATH
|
||||
debug!("Received rotation message");
|
||||
self.handle_rotate_message(buffer.buffer())?;
|
||||
buffer.clear();
|
||||
Ok(MessageResult::None)
|
||||
} else {
|
||||
Ok(MessageResult::Message(msg_type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message(&mut self, type_: u8, buffer: &mut MsgBuffer) -> Result<(), Error> {
|
||||
// HOT PATH
|
||||
assert_ne!(type_, MESSAGE_TYPE_ROTATION);
|
||||
buffer.prepend_byte(type_);
|
||||
self.encrypt_message(buffer)
|
||||
}
|
||||
|
||||
pub fn every_second(&mut self, out: &mut MsgBuffer) -> Result<MessageResult<P>, Error> {
|
||||
out.clear();
|
||||
if let Some(ref mut core) = self.core {
|
||||
core.every_second()
|
||||
}
|
||||
if let Some(ref mut init) = self.init {
|
||||
init.every_second(out)?;
|
||||
}
|
||||
if self.init.as_ref().map(|i| i.stage()).unwrap_or(CLOSING) == CLOSING {
|
||||
self.init = None
|
||||
}
|
||||
if !out.is_empty() {
|
||||
out.prepend_byte(INIT_MESSAGE_FIRST_BYTE);
|
||||
return Ok(MessageResult::Reply);
|
||||
}
|
||||
if let Some(ref mut rotate) = self.rotation {
|
||||
self.rotate_counter += 1;
|
||||
if self.rotate_counter >= ROTATE_INTERVAL {
|
||||
self.rotate_counter = 0;
|
||||
if let Some(rot) = rotate.cycle(out) {
|
||||
let core = self.get_core()?;
|
||||
let algo = core.algorithm();
|
||||
let key = LessSafeKey::new(UnboundKey::new(algo, &rot.key[..algo.key_len()]).unwrap());
|
||||
core.rotate_key(key, rot.id, rot.use_for_sending);
|
||||
}
|
||||
if !out.is_empty() {
|
||||
out.prepend_byte(MESSAGE_TYPE_ROTATION);
|
||||
self.encrypt_message(out)?;
|
||||
return Ok(MessageResult::Reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(MessageResult::None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_init_message(msg: &[u8]) -> bool {
|
||||
// HOT PATH
|
||||
!msg.is_empty() && msg[0] == INIT_MESSAGE_FIRST_BYTE
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::types::NODE_ID_BYTES;
|
||||
|
||||
fn create_node(config: &Config) -> PeerCrypto<Vec<u8>> {
|
||||
let rng = SystemRandom::new();
|
||||
let mut node_id = [0; NODE_ID_BYTES];
|
||||
rng.fill(&mut node_id).unwrap();
|
||||
let crypto = Crypto::new(node_id, config).unwrap();
|
||||
crypto.peer_instance(vec![])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal() {
|
||||
let config = Config { password: Some("test".to_string()), ..Default::default() };
|
||||
let mut node1 = create_node(&config);
|
||||
let mut node2 = create_node(&config);
|
||||
let mut msg = MsgBuffer::new(16);
|
||||
|
||||
node1.initialize(&mut msg).unwrap();
|
||||
assert!(!msg.is_empty());
|
||||
|
||||
debug!("Node1 -> Node2");
|
||||
let res = node2.handle_message(&mut msg).unwrap();
|
||||
assert_eq!(res, MessageResult::Reply);
|
||||
assert!(!msg.is_empty());
|
||||
|
||||
debug!("Node1 <- Node2");
|
||||
let res = node1.handle_message(&mut msg).unwrap();
|
||||
assert_eq!(res, MessageResult::InitializedWithReply(vec![]));
|
||||
assert!(!msg.is_empty());
|
||||
|
||||
debug!("Node1 -> Node2");
|
||||
let res = node2.handle_message(&mut msg).unwrap();
|
||||
assert_eq!(res, MessageResult::InitializedWithReply(vec![]));
|
||||
assert!(!msg.is_empty());
|
||||
|
||||
debug!("Node1 <- Node2");
|
||||
let res = node1.handle_message(&mut msg).unwrap();
|
||||
assert_eq!(res, MessageResult::None);
|
||||
assert!(msg.is_empty());
|
||||
|
||||
let mut buffer = MsgBuffer::new(16);
|
||||
let rng = SystemRandom::new();
|
||||
buffer.set_length(1000);
|
||||
rng.fill(buffer.message_mut()).unwrap();
|
||||
for _ in 0..1000 {
|
||||
node1.send_message(1, &mut buffer).unwrap();
|
||||
let res = node2.handle_message(&mut buffer).unwrap();
|
||||
assert_eq!(res, MessageResult::Message(1));
|
||||
|
||||
match node1.every_second(&mut msg).unwrap() {
|
||||
MessageResult::None => (),
|
||||
MessageResult::Reply => {
|
||||
let res = node2.handle_message(&mut msg).unwrap();
|
||||
assert_eq!(res, MessageResult::None);
|
||||
}
|
||||
other => assert_eq!(other, MessageResult::None),
|
||||
}
|
||||
match node2.every_second(&mut msg).unwrap() {
|
||||
MessageResult::None => (),
|
||||
MessageResult::Reply => {
|
||||
let res = node1.handle_message(&mut msg).unwrap();
|
||||
assert_eq!(res, MessageResult::None);
|
||||
}
|
||||
other => assert_eq!(other, MessageResult::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,453 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
// This module implements a crypto core for encrypting and decrypting message streams
|
||||
//
|
||||
// The crypto core only encrypts and decrypts messages, using given keys. Negotiating and rotating the keys is out of
|
||||
// scope of the crypto core. The crypto core assumes that the remote node will always have the necessary key to decrypt
|
||||
// the message.
|
||||
//
|
||||
// The crypto core encrypts messages in place, writes some extra data (key id and nonce) into a given space and
|
||||
// includes the given header data in the authentication tag. When decrypting messages, the crypto core reads the extra
|
||||
// data, uses the key id to find the right key to decrypting the message and then decrypts the message, using the given
|
||||
// nonce and including the given header data in the verification of the authentication tag.
|
||||
//
|
||||
// While the core only uses a single key at a time for encrypting messages, it is ready to decrypt messages based on
|
||||
// one of 4 stored keys (the encryption key being one of them). An external key rotation is responsible for adding the
|
||||
// key to the remote peer before switching to the key on the local peer for encryption.
|
||||
//
|
||||
// As mentioned, the encryption and decryption works in place. Therefore the parameter payload_and_tag contains (when
|
||||
// decrypting) or provides space for (when encrypting) the payload and the authentication tag. When encrypting, that
|
||||
// means, that the last TAG_LEN bytes of payload_and_tag must be reserved for the tag and must not contain payload
|
||||
// bytes.
|
||||
//
|
||||
// The nonce is a value of 12 bytes (192 bits). Since both nodes can use the same key for encryption, the most
|
||||
// significant byte (msb) of the nonce is initialized differently on both peers: one peer uses the value 0x00 and the
|
||||
// other one 0x80. That means that the nonce space is essentially divided in two halves, one for each node.
|
||||
//
|
||||
// To save space and keep the encrypted data aligned to 64 bits, not all bytes of the nonce are transferred. Instead,
|
||||
// only 7 bytes are included in messages (another byte is used for the key id, hence 64 bit alignment). The rest of the
|
||||
// nonce is deduced by the nodes: All other bytes are assumed to be 0x00, except for the most significant byte, which
|
||||
// is assumed to be the opposite ones own msb. This has two nice effects:
|
||||
// 1) Long before the nonce could theoretically repeat, the messages can no longer be decrypted by the peer as the
|
||||
// higher bytes are no longer zero as assumed.
|
||||
// 2) By deducing the msb to be the opposite of ones own msb, it is no longer possible for an attacker to redirect a
|
||||
// message back to the sender because then the assumed nonce will be wrong and the message fails to decrypt. Otherwise,
|
||||
// this could lead to problems as nodes would be able to accidentally decrypt their own messages.
|
||||
//
|
||||
// In order to be resistent against replay attacks but allow for reordering of messages, the crypto core uses nonce
|
||||
// pinning. For every active key, the biggest nonce seen so far is being tracked. Every second, the biggest nonce seen
|
||||
// one second ago plus 1 becomes the minimum nonce that is accepted for that key. That means, that reordering can
|
||||
// happen within one second but after a second, old messages will not be accepted anymore.
|
||||
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
use ring::{
|
||||
aead::{self, LessSafeKey, UnboundKey},
|
||||
rand::{SecureRandom, SystemRandom},
|
||||
};
|
||||
|
||||
use std::{
|
||||
io::{Cursor, Read, Write},
|
||||
mem,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{error::Error, util::MsgBuffer};
|
||||
|
||||
const NONCE_LEN: usize = 12;
|
||||
pub const TAG_LEN: usize = 16;
|
||||
pub const EXTRA_LEN: usize = 8;
|
||||
|
||||
fn random_data(size: usize) -> Vec<u8> {
|
||||
let rand = SystemRandom::new();
|
||||
let mut data = vec![0; size];
|
||||
rand.fill(&mut data).expect("Failed to obtain random bytes");
|
||||
data
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, Ord, PartialEq, Debug, Eq, Clone)]
|
||||
struct Nonce([u8; NONCE_LEN]);
|
||||
|
||||
impl Nonce {
|
||||
fn zero() -> Self {
|
||||
Nonce([0; NONCE_LEN])
|
||||
}
|
||||
|
||||
fn random(rand: &SystemRandom) -> Self {
|
||||
let mut nonce = Nonce::zero();
|
||||
rand.fill(&mut nonce.0[6..]).expect("Failed to obtain random bytes");
|
||||
nonce
|
||||
}
|
||||
|
||||
fn set_msb(&mut self, val: u8) {
|
||||
self.0[0] = val
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> &[u8; NONCE_LEN] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn increment(&mut self) {
|
||||
for i in (0..NONCE_LEN).rev() {
|
||||
let mut num = self.0[i];
|
||||
num = num.wrapping_add(1);
|
||||
self.0[i] = num;
|
||||
if num > 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CryptoKey {
|
||||
key: LessSafeKey,
|
||||
send_nonce: Nonce,
|
||||
min_nonce: Nonce,
|
||||
next_min_nonce: Nonce,
|
||||
seen_nonce: Nonce,
|
||||
}
|
||||
|
||||
impl CryptoKey {
|
||||
fn new(rand: &SystemRandom, key: LessSafeKey, nonce_half: bool) -> Self {
|
||||
let mut send_nonce = Nonce::random(rand);
|
||||
send_nonce.set_msb(if nonce_half { 0x80 } else { 0x00 });
|
||||
CryptoKey {
|
||||
key,
|
||||
send_nonce,
|
||||
min_nonce: Nonce::zero(),
|
||||
next_min_nonce: Nonce::zero(),
|
||||
seen_nonce: Nonce::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_min_nonce(&mut self) {
|
||||
mem::swap(&mut self.min_nonce, &mut self.next_min_nonce);
|
||||
self.next_min_nonce = self.seen_nonce.clone();
|
||||
self.next_min_nonce.increment();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CryptoCore {
|
||||
rand: SystemRandom,
|
||||
keys: [CryptoKey; 4],
|
||||
current_key: usize,
|
||||
nonce_half: bool,
|
||||
}
|
||||
|
||||
impl CryptoCore {
|
||||
pub fn new(key: LessSafeKey, nonce_half: bool) -> Self {
|
||||
let rand = SystemRandom::new();
|
||||
let dummy_key_data = random_data(key.algorithm().key_len());
|
||||
let dummy_key1 = LessSafeKey::new(UnboundKey::new(key.algorithm(), &dummy_key_data).unwrap());
|
||||
let dummy_key2 = LessSafeKey::new(UnboundKey::new(key.algorithm(), &dummy_key_data).unwrap());
|
||||
let dummy_key3 = LessSafeKey::new(UnboundKey::new(key.algorithm(), &dummy_key_data).unwrap());
|
||||
Self {
|
||||
keys: [
|
||||
CryptoKey::new(&rand, key, nonce_half),
|
||||
CryptoKey::new(&rand, dummy_key1, nonce_half),
|
||||
CryptoKey::new(&rand, dummy_key2, nonce_half),
|
||||
CryptoKey::new(&rand, dummy_key3, nonce_half),
|
||||
],
|
||||
current_key: 0,
|
||||
nonce_half,
|
||||
rand,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&mut self, buffer: &mut MsgBuffer) {
|
||||
let data_start = buffer.get_start();
|
||||
let data_length = buffer.len();
|
||||
assert!(buffer.get_start() >= EXTRA_LEN);
|
||||
buffer.set_start(data_start - EXTRA_LEN);
|
||||
buffer.set_length(data_length + EXTRA_LEN + TAG_LEN);
|
||||
let (extra, data_and_tag) = buffer.message_mut().split_at_mut(EXTRA_LEN);
|
||||
let (data, tag_space) = data_and_tag.split_at_mut(data_length);
|
||||
let key = &mut self.keys[self.current_key];
|
||||
key.send_nonce.increment();
|
||||
{
|
||||
let mut extra = Cursor::new(extra);
|
||||
extra.write_u8(self.current_key as u8).unwrap();
|
||||
extra.write_all(&key.send_nonce.as_bytes()[5..]).unwrap();
|
||||
}
|
||||
let nonce = aead::Nonce::assume_unique_for_key(*key.send_nonce.as_bytes());
|
||||
let tag = key.key.seal_in_place_separate_tag(nonce, aead::Aad::empty(), data).expect("Failed to encrypt");
|
||||
tag_space.clone_from_slice(tag.as_ref());
|
||||
}
|
||||
|
||||
fn decrypt_with_key(key: &mut CryptoKey, nonce: Nonce, data_and_tag: &mut [u8]) -> Result<(), Error> {
|
||||
if nonce < key.min_nonce {
|
||||
return Err(Error::Crypto("Old nonce rejected"));
|
||||
}
|
||||
// decrypt
|
||||
let crypto_nonce = aead::Nonce::assume_unique_for_key(*nonce.as_bytes());
|
||||
key.key
|
||||
.open_in_place(crypto_nonce, aead::Aad::empty(), data_and_tag)
|
||||
.map_err(|_| Error::Crypto("Failed to decrypt data"))?;
|
||||
// last seen nonce
|
||||
if key.seen_nonce < nonce {
|
||||
key.seen_nonce = nonce;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decrypt(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
|
||||
assert!(buffer.len() >= EXTRA_LEN + TAG_LEN);
|
||||
let (extra, data_and_tag) = buffer.message_mut().split_at_mut(EXTRA_LEN);
|
||||
let key_id;
|
||||
let mut nonce;
|
||||
{
|
||||
let mut extra = Cursor::new(extra);
|
||||
key_id = extra.read_u8().map_err(|_| Error::Crypto("Input data too short"))? % 4;
|
||||
nonce = Nonce::zero();
|
||||
extra.read_exact(&mut nonce.0[5..]).map_err(|_| Error::Crypto("Input data too short"))?;
|
||||
nonce.set_msb(if self.nonce_half { 0x00 } else { 0x80 });
|
||||
}
|
||||
let key = &mut self.keys[key_id as usize];
|
||||
let result = Self::decrypt_with_key(key, nonce, data_and_tag);
|
||||
buffer.set_start(buffer.get_start() + EXTRA_LEN);
|
||||
buffer.set_length(buffer.len() - TAG_LEN);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn rotate_key(&mut self, key: LessSafeKey, id: u64, use_for_sending: bool) {
|
||||
debug!("Rotated key {} (use for sending: {})", id, use_for_sending);
|
||||
let id = (id % 4) as usize;
|
||||
self.keys[id] = CryptoKey::new(&self.rand, key, self.nonce_half);
|
||||
if use_for_sending {
|
||||
self.current_key = id
|
||||
}
|
||||
}
|
||||
|
||||
pub fn algorithm(&self) -> &'static aead::Algorithm {
|
||||
self.keys[self.current_key].key.algorithm()
|
||||
}
|
||||
|
||||
pub fn every_second(&mut self) {
|
||||
// Set min nonce on all keys
|
||||
for k in &mut self.keys {
|
||||
k.update_min_nonce();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_dummy_pair(algo: &'static aead::Algorithm) -> (CryptoCore, CryptoCore) {
|
||||
let key_data = random_data(algo.key_len());
|
||||
let sender = CryptoCore::new(LessSafeKey::new(UnboundKey::new(algo, &key_data).unwrap()), true);
|
||||
let receiver = CryptoCore::new(LessSafeKey::new(UnboundKey::new(algo, &key_data).unwrap()), false);
|
||||
(sender, receiver)
|
||||
}
|
||||
|
||||
pub fn test_speed(algo: &'static aead::Algorithm, max_time: &Duration) -> f64 {
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
buffer.set_length(1000);
|
||||
let (mut sender, mut receiver) = create_dummy_pair(algo);
|
||||
let mut iterations = 0;
|
||||
let start = Instant::now();
|
||||
while (Instant::now() - start).as_nanos() < max_time.as_nanos() {
|
||||
for _ in 0..1000 {
|
||||
sender.encrypt(&mut buffer);
|
||||
receiver.decrypt(&mut buffer).unwrap();
|
||||
}
|
||||
iterations += 1000;
|
||||
}
|
||||
let duration = (Instant::now() - start).as_secs_f64();
|
||||
let data = iterations * 1000 * 2;
|
||||
data as f64 / duration / 1_000_000.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ring::aead::{self, LessSafeKey, UnboundKey};
|
||||
|
||||
#[test]
|
||||
fn test_nonce() {
|
||||
let mut nonce = Nonce::zero();
|
||||
assert_eq!(nonce.as_bytes(), &[0; 12]);
|
||||
nonce.increment();
|
||||
assert_eq!(nonce.as_bytes(), &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
||||
nonce.increment();
|
||||
assert_eq!(nonce.as_bytes(), &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
|
||||
}
|
||||
|
||||
fn test_encrypt_decrypt(algo: &'static aead::Algorithm) {
|
||||
let (mut sender, mut receiver) = create_dummy_pair(algo);
|
||||
let plain = random_data(1000);
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
buffer.clone_from(&plain);
|
||||
assert_eq!(&plain[..], buffer.message());
|
||||
sender.encrypt(&mut buffer);
|
||||
assert_ne!(&plain[..], buffer.message());
|
||||
receiver.decrypt(&mut buffer).unwrap();
|
||||
assert_eq!(&plain[..], buffer.message());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_aes128() {
|
||||
test_encrypt_decrypt(&aead::AES_128_GCM)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_aes256() {
|
||||
test_encrypt_decrypt(&aead::AES_256_GCM)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_chacha() {
|
||||
test_encrypt_decrypt(&aead::CHACHA20_POLY1305)
|
||||
}
|
||||
|
||||
fn test_tampering(algo: &'static aead::Algorithm) {
|
||||
let (mut sender, mut receiver) = create_dummy_pair(algo);
|
||||
let plain = random_data(1000);
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
buffer.clone_from(&plain);
|
||||
sender.encrypt(&mut buffer);
|
||||
let mut d = buffer.clone();
|
||||
assert!(receiver.decrypt(&mut d,).is_ok());
|
||||
// Tamper with extra data byte 1 (subkey id)
|
||||
d = buffer.clone();
|
||||
d.message_mut()[0] ^= 1;
|
||||
assert!(receiver.decrypt(&mut d).is_err());
|
||||
// Tamper with extra data byte 2 (nonce)
|
||||
d = buffer.clone();
|
||||
d.message_mut()[1] ^= 1;
|
||||
assert!(receiver.decrypt(&mut d).is_err());
|
||||
// Tamper with data itself
|
||||
d = buffer.clone();
|
||||
d.message_mut()[EXTRA_LEN] ^= 1;
|
||||
assert!(receiver.decrypt(&mut d).is_err());
|
||||
// Check everything still works
|
||||
d = buffer;
|
||||
assert!(receiver.decrypt(&mut d).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tampering_aes128() {
|
||||
test_tampering(&aead::AES_128_GCM)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tampering_aes256() {
|
||||
test_tampering(&aead::AES_256_GCM)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tampering_chacha() {
|
||||
test_tampering(&aead::CHACHA20_POLY1305)
|
||||
}
|
||||
|
||||
fn test_nonce_pinning(algo: &'static aead::Algorithm) {
|
||||
let (mut sender, mut receiver) = create_dummy_pair(algo);
|
||||
let plain = random_data(1000);
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
buffer.clone_from(&plain);
|
||||
sender.encrypt(&mut buffer);
|
||||
{
|
||||
let mut d = buffer.clone();
|
||||
assert!(receiver.decrypt(&mut d).is_ok());
|
||||
}
|
||||
receiver.every_second();
|
||||
{
|
||||
let mut d = buffer.clone();
|
||||
assert!(receiver.decrypt(&mut d).is_ok());
|
||||
}
|
||||
receiver.every_second();
|
||||
{
|
||||
let mut d = buffer;
|
||||
assert!(receiver.decrypt(&mut d).is_err());
|
||||
}
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
buffer.clone_from(&plain);
|
||||
sender.encrypt(&mut buffer);
|
||||
assert!(receiver.decrypt(&mut buffer).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonce_pinning_aes128() {
|
||||
test_nonce_pinning(&aead::AES_128_GCM)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonce_pinning_aes256() {
|
||||
test_nonce_pinning(&aead::AES_256_GCM)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonce_pinning_chacha() {
|
||||
test_nonce_pinning(&aead::CHACHA20_POLY1305)
|
||||
}
|
||||
|
||||
fn test_key_rotation(algo: &'static aead::Algorithm) {
|
||||
let (mut sender, mut receiver) = create_dummy_pair(algo);
|
||||
let plain = random_data(1000);
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
buffer.clone_from(&plain);
|
||||
|
||||
sender.encrypt(&mut buffer);
|
||||
assert!(receiver.decrypt(&mut buffer).is_ok());
|
||||
|
||||
let new_key = random_data(algo.key_len());
|
||||
receiver.rotate_key(LessSafeKey::new(UnboundKey::new(algo, &new_key).unwrap()), 1, false);
|
||||
receiver.encrypt(&mut buffer);
|
||||
assert!(sender.decrypt(&mut buffer).is_ok());
|
||||
sender.encrypt(&mut buffer);
|
||||
assert!(receiver.decrypt(&mut buffer).is_ok());
|
||||
sender.rotate_key(LessSafeKey::new(UnboundKey::new(algo, &new_key).unwrap()), 1, true);
|
||||
receiver.encrypt(&mut buffer);
|
||||
assert!(sender.decrypt(&mut buffer).is_ok());
|
||||
sender.encrypt(&mut buffer);
|
||||
assert!(receiver.decrypt(&mut buffer).is_ok());
|
||||
let new_key = random_data(algo.key_len());
|
||||
sender.rotate_key(LessSafeKey::new(UnboundKey::new(algo, &new_key).unwrap()), 2, true);
|
||||
sender.encrypt(&mut buffer);
|
||||
assert!(receiver.decrypt(&mut buffer).is_err());
|
||||
receiver.encrypt(&mut buffer);
|
||||
assert!(sender.decrypt(&mut buffer).is_ok());
|
||||
|
||||
receiver.rotate_key(LessSafeKey::new(UnboundKey::new(algo, &new_key).unwrap()), 2, false);
|
||||
receiver.encrypt(&mut buffer);
|
||||
assert!(sender.decrypt(&mut buffer).is_ok());
|
||||
sender.encrypt(&mut buffer);
|
||||
assert!(receiver.decrypt(&mut buffer).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_rotation_aes128() {
|
||||
test_key_rotation(&aead::AES_128_GCM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_rotation_aes256() {
|
||||
test_key_rotation(&aead::AES_256_GCM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_rotation_chacha() {
|
||||
test_key_rotation(&aead::CHACHA20_POLY1305);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_core_size() {
|
||||
assert_eq!(2384, mem::size_of::<CryptoCore>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_aes128() {
|
||||
let speed = test_speed(&aead::AES_128_GCM, &Duration::from_secs_f32(0.2));
|
||||
assert!(speed > 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_aes256() {
|
||||
let speed = test_speed(&aead::AES_256_GCM, &Duration::from_secs_f32(0.2));
|
||||
assert!(speed > 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_speed_chacha() {
|
||||
let speed = test_speed(&aead::CHACHA20_POLY1305, &Duration::from_secs_f32(0.2));
|
||||
assert!(speed > 10.0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,929 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
// This module implements a 3-way handshake to initialize an authenticated and encrypted connection.
|
||||
//
|
||||
// The handshake assumes that each node has a asymmetric Curve 25519 key pair as well as a list of trusted public keys
|
||||
// and a set of supported crypto algorithms as well as the expected speed when using them. If successful, the handshake
|
||||
// will negotiate a crypto algorithm to use and a common ephemeral symmetric key and exchange a given payload between
|
||||
// the nodes.
|
||||
//
|
||||
// The handshake consists of 3 stages, "ping", "pong" and "peng". In the following description, the node that initiates
|
||||
// the connection is named "A" and the other node is named "B". Since a lot of things are going on in parallel in the
|
||||
// handshake, those aspects are described separately in the following paragraphs.
|
||||
//
|
||||
// Every message contains the node id of the sender. If a node receives a message with its own node id, it just ignores
|
||||
// it and closes the connection. This is the way nodes avoid to connect to themselves as it is not trivial for a node
|
||||
// to know its own addresses (especially in the case of NAT).
|
||||
//
|
||||
// All initialization messages are signed by the asymmetric key of the sender. Also the messages indicate the public
|
||||
// key being used, so the receiver can use the correct public key to verify the signature. The public key itself is not
|
||||
// attached to the message for privacy reasons (the public key is stable over multiple restarts while the node id is
|
||||
// only valid for a single run). Instead, a 2 byte salt value as well as the last 2 bytes of the salted sha 2 hash of
|
||||
// the public key are used to identify the public key. This way, a receiver that trusts this public key can identify
|
||||
// it but a random observer can't. If the public key is unknown or the signature can't be verified, the message is
|
||||
// ignored.
|
||||
//
|
||||
// Every message contains a byte that specifies the stage (ping = 1, pong = 2, peng = 3). If a message with an
|
||||
// unexpected stage is received, it is ignored and the last message that has been sent is repeated. There is only one
|
||||
// exception to this rule: if a "pong" message is expected, but a "ping" message is received instead AND the node id of
|
||||
// the sender is greater than the node id of the receiver, the receiving node will reset its state and assume the role
|
||||
// of a receiver of the initialization (i.e. "B"). This is used to "negotiate" the roles A and B when both nodes
|
||||
// initiate the connection in parallel and think they are A.
|
||||
//
|
||||
// Upon connection creation, both nodes create a random ephemeral ECDH key pair and exchange the public keys in the
|
||||
// ping and pong messages. A sends the ping message to B containing A's public key and B replies with a pong message
|
||||
// containing B's public key. That means, that after receiving the ping message B can calculate the shared key material
|
||||
// and after receiving the pong message A can calculate the shared key material.
|
||||
//
|
||||
// The ping message and the pong message contain a set of supported crypto algorithms together with the estimated
|
||||
// speeds of the algorithms. When B receives a ping message, or A receives a pong message, it can combine this
|
||||
// information with its own algorithm list and select the algorithm with the best expected speed for the crypto core.
|
||||
//
|
||||
// The pong and peng message contain the payload that the nodes want to exchange in the initialization phase apart from
|
||||
// the cryptographic initialization. This payload is encoded according to the application and encrypted using the key
|
||||
// material and the crypto algorithm that have been negotiated via the ping and pong messages. The pong message,
|
||||
// therefore contains information to set up symmetric encryption as well as a part that is already encrypted.
|
||||
//
|
||||
// The handshake ends for A after sending the peng message and for B after receiving this message. At this time both
|
||||
// nodes initialize the connection using the payload and enter normal operation. The negotiated crypto core is used for
|
||||
// future communication and the key rotation is started. Since the peng message can be lost, A needs to keep the
|
||||
// initialization state in order to repeat a lost peng message. After one second, A removes that state.
|
||||
//
|
||||
// Once every second, both nodes check whether they have already finished the initialization. If not, they repeat their
|
||||
// last message. After 5 seconds, the initialization is aborted as failed.
|
||||
|
||||
use super::{
|
||||
core::{CryptoCore, EXTRA_LEN},
|
||||
Algorithms, EcdhPrivateKey, EcdhPublicKey, Ed25519PublicKey, Payload,
|
||||
};
|
||||
use crate::{error::Error, types::NodeId, util::MsgBuffer};
|
||||
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
|
||||
use ring::{
|
||||
aead::{Algorithm, LessSafeKey, UnboundKey, AES_128_GCM, AES_256_GCM, CHACHA20_POLY1305},
|
||||
agreement::{agree_ephemeral, X25519},
|
||||
digest,
|
||||
rand::{SecureRandom, SystemRandom},
|
||||
signature::{self, Ed25519KeyPair, KeyPair, ED25519, ED25519_PUBLIC_KEY_LEN},
|
||||
};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
cmp, f32,
|
||||
fmt::Debug,
|
||||
io::{self, Cursor, Read, Write},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub const STAGE_PING: u8 = 1;
|
||||
pub const STAGE_PONG: u8 = 2;
|
||||
pub const STAGE_PENG: u8 = 3;
|
||||
pub const WAITING_TO_CLOSE: u8 = 4;
|
||||
pub const CLOSING: u8 = 5;
|
||||
|
||||
pub const MAX_FAILED_RETRIES: usize = 120;
|
||||
|
||||
pub const SALTED_NODE_ID_HASH_LEN: usize = 20;
|
||||
pub type SaltedNodeIdHash = [u8; SALTED_NODE_ID_HASH_LEN];
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum InitMsg {
|
||||
Ping {
|
||||
salted_node_id_hash: SaltedNodeIdHash,
|
||||
ecdh_public_key: EcdhPublicKey,
|
||||
algorithms: Algorithms,
|
||||
},
|
||||
Pong {
|
||||
salted_node_id_hash: SaltedNodeIdHash,
|
||||
ecdh_public_key: EcdhPublicKey,
|
||||
algorithms: Algorithms,
|
||||
encrypted_payload: MsgBuffer,
|
||||
},
|
||||
Peng {
|
||||
salted_node_id_hash: SaltedNodeIdHash,
|
||||
encrypted_payload: MsgBuffer,
|
||||
},
|
||||
}
|
||||
|
||||
impl InitMsg {
|
||||
const PART_ALGORITHMS: u8 = 4;
|
||||
const PART_ECDH_PUBLIC_KEY: u8 = 3;
|
||||
const PART_END: u8 = 0;
|
||||
const PART_PAYLOAD: u8 = 5;
|
||||
const PART_SALTED_NODE_ID_HASH: u8 = 2;
|
||||
const PART_STAGE: u8 = 1;
|
||||
|
||||
fn stage(&self) -> u8 {
|
||||
match self {
|
||||
InitMsg::Ping { .. } => STAGE_PING,
|
||||
InitMsg::Pong { .. } => STAGE_PONG,
|
||||
InitMsg::Peng { .. } => STAGE_PENG,
|
||||
}
|
||||
}
|
||||
|
||||
fn salted_node_id_hash(&self) -> &SaltedNodeIdHash {
|
||||
match self {
|
||||
InitMsg::Ping { salted_node_id_hash, .. }
|
||||
| InitMsg::Pong { salted_node_id_hash, .. }
|
||||
| InitMsg::Peng { salted_node_id_hash, .. } => salted_node_id_hash,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_hash(key: &Ed25519PublicKey, salt: &[u8; 4]) -> [u8; 4] {
|
||||
let mut data = [0; ED25519_PUBLIC_KEY_LEN + 4];
|
||||
data[..ED25519_PUBLIC_KEY_LEN].clone_from_slice(key);
|
||||
data[ED25519_PUBLIC_KEY_LEN..].clone_from_slice(salt);
|
||||
let hash = digest::digest(&digest::SHA256, &data);
|
||||
let mut short_hash = [0; 4];
|
||||
short_hash.clone_from_slice(&hash.as_ref()[..4]);
|
||||
short_hash
|
||||
}
|
||||
|
||||
fn read_from(buffer: &[u8], trusted_keys: &[Ed25519PublicKey]) -> Result<(Self, Ed25519PublicKey), Error> {
|
||||
let mut r = Cursor::new(buffer);
|
||||
|
||||
let mut public_key_salt = [0; 4];
|
||||
r.read_exact(&mut public_key_salt).map_err(|_| Error::Parse("Init message too short"))?;
|
||||
let mut public_key_hash = [0; 4];
|
||||
r.read_exact(&mut public_key_hash).map_err(|_| Error::Parse("Init message too short"))?;
|
||||
let mut public_key_data = [0; ED25519_PUBLIC_KEY_LEN];
|
||||
let mut found_key = false;
|
||||
for tk in trusted_keys {
|
||||
if Self::calculate_hash(tk, &public_key_salt) == public_key_hash {
|
||||
public_key_data.clone_from_slice(tk);
|
||||
found_key = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found_key {
|
||||
return Err(Error::Crypto("untrusted peer"));
|
||||
}
|
||||
|
||||
let mut stage = None;
|
||||
let mut salted_node_id_hash = None;
|
||||
let mut ecdh_public_key = None;
|
||||
let mut encrypted_payload = None;
|
||||
let mut algorithms = None;
|
||||
|
||||
loop {
|
||||
let field = r.read_u8().map_err(|_| Error::Parse("Init message too short"))?;
|
||||
if field == Self::PART_END {
|
||||
break;
|
||||
}
|
||||
let field_len = r.read_u16::<NetworkEndian>().map_err(|_| Error::Parse("Init message too short"))? as usize;
|
||||
match field {
|
||||
Self::PART_STAGE => {
|
||||
if field_len != 1 {
|
||||
return Err(Error::CryptoInit("Invalid size for stage field"));
|
||||
}
|
||||
stage = Some(r.read_u8().map_err(|_| Error::Parse("Init message too short"))?)
|
||||
}
|
||||
Self::PART_SALTED_NODE_ID_HASH => {
|
||||
if field_len != SALTED_NODE_ID_HASH_LEN {
|
||||
return Err(Error::CryptoInit("Invalid size for salted node id hash field"));
|
||||
}
|
||||
let mut id = [0; SALTED_NODE_ID_HASH_LEN];
|
||||
r.read_exact(&mut id).map_err(|_| Error::Parse("Init message too short"))?;
|
||||
salted_node_id_hash = Some(id)
|
||||
}
|
||||
Self::PART_ECDH_PUBLIC_KEY => {
|
||||
let mut pub_key_data = smallvec![0; field_len];
|
||||
r.read_exact(&mut pub_key_data).map_err(|_| Error::Parse("Init message too short"))?;
|
||||
ecdh_public_key = Some(EcdhPublicKey::new(&X25519, pub_key_data));
|
||||
}
|
||||
Self::PART_PAYLOAD => {
|
||||
let mut payload = MsgBuffer::new(0);
|
||||
payload.set_length(field_len);
|
||||
r.read_exact(payload.message_mut()).map_err(|_| Error::Parse("Init message too short"))?;
|
||||
encrypted_payload = Some(payload);
|
||||
}
|
||||
Self::PART_ALGORITHMS => {
|
||||
let count = field_len / 5;
|
||||
let mut algos = SmallVec::with_capacity(count);
|
||||
let mut allow_unencrypted = false;
|
||||
for _ in 0..count {
|
||||
let algo = match r.read_u8().map_err(|_| Error::Parse("Init message too short"))? {
|
||||
0 => {
|
||||
allow_unencrypted = true;
|
||||
None
|
||||
}
|
||||
1 => Some(&AES_128_GCM),
|
||||
2 => Some(&AES_256_GCM),
|
||||
3 => Some(&CHACHA20_POLY1305),
|
||||
_ => None,
|
||||
};
|
||||
let speed =
|
||||
r.read_f32::<NetworkEndian>().map_err(|_| Error::Parse("Init message too short"))?;
|
||||
if let Some(algo) = algo {
|
||||
algos.push((algo, speed));
|
||||
}
|
||||
}
|
||||
algorithms = Some(Algorithms { algorithm_speeds: algos, allow_unencrypted });
|
||||
}
|
||||
_ => {
|
||||
let mut data = vec![0; field_len];
|
||||
r.read_exact(&mut data).map_err(|_| Error::Parse("Init message too short"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pos = r.position() as usize;
|
||||
|
||||
let signature_len = r.read_u8().map_err(|_| Error::Parse("Init message too short"))? as usize;
|
||||
let mut signature: SmallVec<[u8; 32]> = smallvec![0; signature_len];
|
||||
r.read_exact(&mut signature).map_err(|_| Error::Parse("Init message too short"))?;
|
||||
|
||||
let signed_data = &r.into_inner()[0..pos];
|
||||
let public_key = signature::UnparsedPublicKey::new(&ED25519, &public_key_data);
|
||||
if public_key.verify(signed_data, &signature).is_err() {
|
||||
return Err(Error::Crypto("invalid signature"));
|
||||
}
|
||||
|
||||
let stage = match stage {
|
||||
Some(val) => val,
|
||||
None => return Err(Error::CryptoInit("Init message without stage")),
|
||||
};
|
||||
let salted_node_id_hash = match salted_node_id_hash {
|
||||
Some(val) => val,
|
||||
None => return Err(Error::CryptoInit("Init message without node id")),
|
||||
};
|
||||
|
||||
let msg = match stage {
|
||||
STAGE_PING => {
|
||||
let ecdh_public_key = match ecdh_public_key {
|
||||
Some(val) => val,
|
||||
None => return Err(Error::CryptoInit("Init message without ecdh public key")),
|
||||
};
|
||||
let algorithms = match algorithms {
|
||||
Some(val) => val,
|
||||
None => return Err(Error::CryptoInit("Init message without algorithms")),
|
||||
};
|
||||
Self::Ping { salted_node_id_hash, ecdh_public_key, algorithms }
|
||||
}
|
||||
STAGE_PONG => {
|
||||
let ecdh_public_key = match ecdh_public_key {
|
||||
Some(val) => val,
|
||||
None => return Err(Error::CryptoInit("Init message without ecdh public key")),
|
||||
};
|
||||
let algorithms = match algorithms {
|
||||
Some(val) => val,
|
||||
None => return Err(Error::CryptoInit("Init message without algorithms")),
|
||||
};
|
||||
let encrypted_payload = match encrypted_payload {
|
||||
Some(val) => val,
|
||||
None => return Err(Error::CryptoInit("Init message without payload")),
|
||||
};
|
||||
Self::Pong { salted_node_id_hash, ecdh_public_key, algorithms, encrypted_payload }
|
||||
}
|
||||
STAGE_PENG => {
|
||||
let encrypted_payload = match encrypted_payload {
|
||||
Some(val) => val,
|
||||
None => return Err(Error::CryptoInit("Init message without payload")),
|
||||
};
|
||||
Self::Peng { salted_node_id_hash, encrypted_payload }
|
||||
}
|
||||
_ => return Err(Error::CryptoInit("Invalid stage")),
|
||||
};
|
||||
|
||||
Ok((msg, public_key_data))
|
||||
}
|
||||
|
||||
fn write_to(&self, buffer: &mut [u8], key: &Ed25519KeyPair) -> Result<usize, io::Error> {
|
||||
let mut w = Cursor::new(buffer);
|
||||
|
||||
let rand = SystemRandom::new();
|
||||
let mut salt = [0; 4];
|
||||
rand.fill(&mut salt).unwrap();
|
||||
let mut public_key = [0; ED25519_PUBLIC_KEY_LEN];
|
||||
public_key.clone_from_slice(key.public_key().as_ref());
|
||||
let hash = Self::calculate_hash(&public_key, &salt);
|
||||
w.write_all(&salt)?;
|
||||
w.write_all(&hash)?;
|
||||
|
||||
w.write_u8(Self::PART_STAGE)?;
|
||||
w.write_u16::<NetworkEndian>(1)?;
|
||||
w.write_u8(self.stage())?;
|
||||
|
||||
match &self {
|
||||
Self::Ping { salted_node_id_hash, .. }
|
||||
| Self::Pong { salted_node_id_hash, .. }
|
||||
| Self::Peng { salted_node_id_hash, .. } => {
|
||||
w.write_u8(Self::PART_SALTED_NODE_ID_HASH)?;
|
||||
w.write_u16::<NetworkEndian>(SALTED_NODE_ID_HASH_LEN as u16)?;
|
||||
w.write_all(salted_node_id_hash)?;
|
||||
}
|
||||
}
|
||||
|
||||
match &self {
|
||||
Self::Ping { ecdh_public_key, .. } | Self::Pong { ecdh_public_key, .. } => {
|
||||
w.write_u8(Self::PART_ECDH_PUBLIC_KEY)?;
|
||||
let key_bytes = ecdh_public_key.bytes();
|
||||
w.write_u16::<NetworkEndian>(key_bytes.len() as u16)?;
|
||||
w.write_all(key_bytes)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match &self {
|
||||
Self::Ping { algorithms, .. } | Self::Pong { algorithms, .. } => {
|
||||
w.write_u8(Self::PART_ALGORITHMS)?;
|
||||
let mut len = algorithms.algorithm_speeds.len() * 5;
|
||||
if algorithms.allow_unencrypted {
|
||||
len += 5;
|
||||
}
|
||||
w.write_u16::<NetworkEndian>(len as u16)?;
|
||||
if algorithms.allow_unencrypted {
|
||||
w.write_u8(0)?;
|
||||
w.write_f32::<NetworkEndian>(f32::INFINITY)?;
|
||||
}
|
||||
for (algo, speed) in &algorithms.algorithm_speeds {
|
||||
if *algo == &AES_128_GCM {
|
||||
w.write_u8(1)?;
|
||||
} else if *algo == &AES_256_GCM {
|
||||
w.write_u8(2)?;
|
||||
} else if *algo == &CHACHA20_POLY1305 {
|
||||
w.write_u8(3)?;
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
w.write_f32::<NetworkEndian>(*speed)?;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
match &self {
|
||||
Self::Pong { encrypted_payload, .. } | Self::Peng { encrypted_payload, .. } => {
|
||||
w.write_u8(Self::PART_PAYLOAD)?;
|
||||
w.write_u16::<NetworkEndian>(encrypted_payload.len() as u16)?;
|
||||
w.write_all(encrypted_payload.message())?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
w.write_u8(Self::PART_END)?;
|
||||
|
||||
let pos = w.position() as usize;
|
||||
let signature = key.sign(&w.get_ref()[0..pos]);
|
||||
w.write_u8(signature.as_ref().len() as u8)?;
|
||||
w.write_all(signature.as_ref())?;
|
||||
|
||||
Ok(w.position() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum InitResult<P: Payload> {
|
||||
Continue,
|
||||
Success { peer_payload: P, is_initiator: bool },
|
||||
}
|
||||
|
||||
pub struct InitState<P: Payload> {
|
||||
node_id: NodeId,
|
||||
salted_node_id_hash: SaltedNodeIdHash,
|
||||
payload: P,
|
||||
key_pair: Arc<Ed25519KeyPair>,
|
||||
trusted_keys: Arc<[Ed25519PublicKey]>,
|
||||
ecdh_private_key: Option<EcdhPrivateKey>,
|
||||
next_stage: u8,
|
||||
close_time: usize,
|
||||
last_message: Option<Vec<u8>>,
|
||||
crypto: Option<CryptoCore>,
|
||||
algorithms: Algorithms,
|
||||
#[allow(dead_code)] // Used in tests
|
||||
selected_algorithm: Option<&'static Algorithm>,
|
||||
failed_retries: usize,
|
||||
}
|
||||
|
||||
impl<P: Payload> InitState<P> {
|
||||
pub fn new(
|
||||
node_id: NodeId, payload: P, key_pair: Arc<Ed25519KeyPair>, trusted_keys: Arc<[Ed25519PublicKey]>,
|
||||
algorithms: Algorithms,
|
||||
) -> Self {
|
||||
let mut hash = [0; SALTED_NODE_ID_HASH_LEN];
|
||||
let rng = SystemRandom::new();
|
||||
rng.fill(&mut hash[0..4]).unwrap();
|
||||
hash[4..].clone_from_slice(&node_id);
|
||||
let d = digest::digest(&digest::SHA256, &hash);
|
||||
hash[4..].clone_from_slice(&d.as_ref()[..16]);
|
||||
Self {
|
||||
node_id,
|
||||
salted_node_id_hash: hash,
|
||||
payload,
|
||||
key_pair,
|
||||
trusted_keys,
|
||||
next_stage: STAGE_PING,
|
||||
last_message: None,
|
||||
crypto: None,
|
||||
ecdh_private_key: None,
|
||||
selected_algorithm: None,
|
||||
algorithms,
|
||||
failed_retries: 0,
|
||||
close_time: 60,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_ping(&mut self, out: &mut MsgBuffer) {
|
||||
// create ecdh ephemeral key
|
||||
let (ecdh_private_key, ecdh_public_key) = self.create_ecdh_keypair();
|
||||
self.ecdh_private_key = Some(ecdh_private_key);
|
||||
|
||||
// create stage 1 msg
|
||||
self.send_message(STAGE_PING, Some(ecdh_public_key), out);
|
||||
|
||||
self.next_stage = STAGE_PONG;
|
||||
}
|
||||
|
||||
pub fn stage(&self) -> u8 {
|
||||
self.next_stage
|
||||
}
|
||||
|
||||
pub fn every_second(&mut self, out: &mut MsgBuffer) -> Result<(), Error> {
|
||||
if self.next_stage == WAITING_TO_CLOSE {
|
||||
if self.close_time == 0 {
|
||||
self.next_stage = CLOSING;
|
||||
} else {
|
||||
self.close_time -= 1;
|
||||
}
|
||||
Ok(())
|
||||
} else if self.next_stage == CLOSING {
|
||||
Ok(())
|
||||
} else if self.failed_retries < MAX_FAILED_RETRIES {
|
||||
self.failed_retries += 1;
|
||||
self.repeat_last_message(out);
|
||||
Ok(())
|
||||
} else {
|
||||
self.next_stage = CLOSING;
|
||||
Err(Error::CryptoInitFatal("Initialization timeout"))
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_master_key(&self, algo: &'static Algorithm, privk: EcdhPrivateKey, pubk: &EcdhPublicKey) -> LessSafeKey {
|
||||
agree_ephemeral(privk, pubk, |k| {
|
||||
UnboundKey::new(algo, &k[..algo.key_len()]).map(LessSafeKey::new).unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn create_ecdh_keypair(&self) -> (EcdhPrivateKey, EcdhPublicKey) {
|
||||
let rand = SystemRandom::new();
|
||||
let ecdh_private_key = EcdhPrivateKey::generate(&X25519, &rand).unwrap();
|
||||
let public_key = ecdh_private_key.compute_public_key().unwrap();
|
||||
let mut vec = SmallVec::<[u8; 96]>::new();
|
||||
vec.extend_from_slice(public_key.as_ref());
|
||||
let ecdh_public_key = EcdhPublicKey::new(&X25519, vec);
|
||||
(ecdh_private_key, ecdh_public_key)
|
||||
}
|
||||
|
||||
fn encrypt_payload(&mut self) -> MsgBuffer {
|
||||
let mut buffer = MsgBuffer::new(EXTRA_LEN);
|
||||
self.payload.write_to(&mut buffer);
|
||||
if let Some(crypto) = &mut self.crypto {
|
||||
crypto.encrypt(&mut buffer);
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
fn decrypt(&mut self, data: &mut MsgBuffer) -> Result<P, Error> {
|
||||
if let Some(crypto) = &mut self.crypto {
|
||||
crypto.decrypt(data)?;
|
||||
}
|
||||
P::read_from(Cursor::new(data.message()))
|
||||
}
|
||||
|
||||
fn check_salted_node_id_hash(&self, hash: &SaltedNodeIdHash, node_id: NodeId) -> bool {
|
||||
let mut h2 = [0; SALTED_NODE_ID_HASH_LEN];
|
||||
h2[0..4].clone_from_slice(&hash[0..4]);
|
||||
h2[4..].clone_from_slice(&node_id);
|
||||
let d = digest::digest(&digest::SHA256, &h2);
|
||||
hash == d.as_ref()
|
||||
}
|
||||
|
||||
fn send_message(&mut self, stage: u8, ecdh_public_key: Option<EcdhPublicKey>, out: &mut MsgBuffer) {
|
||||
debug!("Sending init with stage={}", stage);
|
||||
assert!(out.is_empty());
|
||||
let mut public_key = [0; ED25519_PUBLIC_KEY_LEN];
|
||||
public_key.clone_from_slice(self.key_pair.as_ref().public_key().as_ref());
|
||||
let msg = match stage {
|
||||
STAGE_PING => InitMsg::Ping {
|
||||
salted_node_id_hash: self.salted_node_id_hash,
|
||||
ecdh_public_key: ecdh_public_key.unwrap(),
|
||||
algorithms: self.algorithms.clone(),
|
||||
},
|
||||
STAGE_PONG => InitMsg::Pong {
|
||||
salted_node_id_hash: self.salted_node_id_hash,
|
||||
ecdh_public_key: ecdh_public_key.unwrap(),
|
||||
algorithms: self.algorithms.clone(),
|
||||
encrypted_payload: self.encrypt_payload(),
|
||||
},
|
||||
STAGE_PENG => InitMsg::Peng {
|
||||
salted_node_id_hash: self.salted_node_id_hash,
|
||||
encrypted_payload: self.encrypt_payload(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let bytes = out.buffer();
|
||||
let len = msg.write_to(bytes, &self.key_pair).expect("Buffer too small");
|
||||
self.last_message = Some(bytes[0..len].to_vec());
|
||||
out.set_length(len);
|
||||
}
|
||||
|
||||
fn repeat_last_message(&self, out: &mut MsgBuffer) {
|
||||
if let Some(ref bytes) = self.last_message {
|
||||
debug!("Repeating last init message");
|
||||
let buffer = out.buffer();
|
||||
buffer[0..bytes.len()].copy_from_slice(bytes);
|
||||
out.set_length(bytes.len());
|
||||
}
|
||||
}
|
||||
|
||||
fn select_algorithm(&self, peer_algos: &Algorithms) -> Result<Option<(&'static Algorithm, f32)>, Error> {
|
||||
if self.algorithms.allow_unencrypted && peer_algos.allow_unencrypted {
|
||||
return Ok(None);
|
||||
}
|
||||
// For each supported algorithm, find the algorithm in the list of the peer (ignore algorithm if not found).
|
||||
// Take the minimal speed reported by either us or the peer.
|
||||
// Select the algorithm with the greatest minimal speed.
|
||||
let algo = self
|
||||
.algorithms
|
||||
.algorithm_speeds
|
||||
.iter()
|
||||
.filter_map(|(a1, s1)| {
|
||||
peer_algos
|
||||
.algorithm_speeds
|
||||
.iter()
|
||||
.find(|(a2, _)| a1 == a2)
|
||||
.map(|(_, s2)| (*a1, if s1 < s2 { *s1 } else { *s2 }))
|
||||
})
|
||||
.max_by(|(_, s1), (_, s2)| if s1 < s2 { cmp::Ordering::Less } else { cmp::Ordering::Greater });
|
||||
if let Some(algo) = algo {
|
||||
debug!("Init: best algorithm is {:?} with speed {}", algo.0, algo.1);
|
||||
Ok(Some(algo))
|
||||
} else {
|
||||
Err(Error::CryptoInitFatal("No common algorithms"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_init(&mut self, out: &mut MsgBuffer) -> Result<InitResult<P>, Error> {
|
||||
let (msg, _peer_key) = InitMsg::read_from(out.buffer(), &self.trusted_keys)?;
|
||||
out.clear();
|
||||
let stage = msg.stage();
|
||||
let salted_node_id_hash = *msg.salted_node_id_hash();
|
||||
debug!("Received init with stage={}, expected stage={}", stage, self.next_stage);
|
||||
if self.salted_node_id_hash == salted_node_id_hash
|
||||
|| self.check_salted_node_id_hash(&salted_node_id_hash, self.node_id)
|
||||
{
|
||||
return Err(Error::CryptoInitFatal("Connected to self"));
|
||||
}
|
||||
if stage != self.next_stage {
|
||||
if self.next_stage == STAGE_PONG && stage == STAGE_PING {
|
||||
// special case for concurrent init messages in both directions
|
||||
// the node with the higher node_id "wins" and gets to initialize the connection
|
||||
if salted_node_id_hash > self.salted_node_id_hash {
|
||||
// reset to initial state
|
||||
self.next_stage = STAGE_PING;
|
||||
self.last_message = None;
|
||||
self.ecdh_private_key = None;
|
||||
} else {
|
||||
return Ok(InitResult::Continue);
|
||||
}
|
||||
} else if self.next_stage == CLOSING {
|
||||
return Ok(InitResult::Continue);
|
||||
} else if self.last_message.is_some() {
|
||||
self.repeat_last_message(out);
|
||||
return Ok(InitResult::Continue);
|
||||
} else {
|
||||
return Err(Error::CryptoInitFatal("Received invalid stage as first message"));
|
||||
}
|
||||
}
|
||||
self.failed_retries = 0;
|
||||
match msg {
|
||||
InitMsg::Ping { ecdh_public_key, algorithms, .. } => {
|
||||
// create ecdh ephemeral key
|
||||
let (my_ecdh_private_key, my_ecdh_public_key) = self.create_ecdh_keypair();
|
||||
|
||||
// do ecdh agreement and derive master key
|
||||
let algorithm = self.select_algorithm(&algorithms)?;
|
||||
self.selected_algorithm = algorithm.map(|a| a.0);
|
||||
if let Some((algorithm, _speed)) = algorithm {
|
||||
let master_key = self.derive_master_key(algorithm, my_ecdh_private_key, &ecdh_public_key);
|
||||
self.crypto = Some(CryptoCore::new(master_key, self.salted_node_id_hash > salted_node_id_hash));
|
||||
}
|
||||
|
||||
// create and send stage 2 reply
|
||||
self.send_message(STAGE_PONG, Some(my_ecdh_public_key), out);
|
||||
|
||||
self.next_stage = STAGE_PENG;
|
||||
Ok(InitResult::Continue)
|
||||
}
|
||||
InitMsg::Pong { ecdh_public_key, algorithms, mut encrypted_payload, .. } => {
|
||||
// do ecdh agreement and derive master key
|
||||
let ecdh_private_key = self.ecdh_private_key.take().unwrap();
|
||||
let algorithm = self.select_algorithm(&algorithms)?;
|
||||
self.selected_algorithm = algorithm.map(|a| a.0);
|
||||
if let Some((algorithm, _speed)) = algorithm {
|
||||
let master_key = self.derive_master_key(algorithm, ecdh_private_key, &ecdh_public_key);
|
||||
self.crypto = Some(CryptoCore::new(master_key, self.salted_node_id_hash > salted_node_id_hash));
|
||||
}
|
||||
|
||||
// decrypt the payload
|
||||
let peer_payload = self
|
||||
.decrypt(&mut encrypted_payload)
|
||||
.map_err(|_| Error::CryptoInitFatal("Failed to decrypt payload"))?;
|
||||
|
||||
// create and send stage 3 reply
|
||||
self.send_message(STAGE_PENG, None, out);
|
||||
|
||||
self.next_stage = WAITING_TO_CLOSE;
|
||||
self.close_time = 60;
|
||||
Ok(InitResult::Success { peer_payload, is_initiator: true })
|
||||
}
|
||||
InitMsg::Peng { mut encrypted_payload, .. } => {
|
||||
// decrypt the payload
|
||||
let peer_payload = self
|
||||
.decrypt(&mut encrypted_payload)
|
||||
.map_err(|_| Error::CryptoInitFatal("Failed to decrypt payload"))?;
|
||||
|
||||
self.next_stage = CLOSING; // force resend when receiving any message
|
||||
Ok(InitResult::Success { peer_payload, is_initiator: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_core(&mut self) -> Option<CryptoCore> {
|
||||
self.crypto.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::NODE_ID_BYTES;
|
||||
|
||||
impl Payload for Vec<u8> {
|
||||
fn write_to(&self, buffer: &mut MsgBuffer) {
|
||||
buffer.buffer().write_all(self).expect("Buffer too small");
|
||||
buffer.set_length(self.len())
|
||||
}
|
||||
|
||||
fn read_from<R: Read>(mut r: R) -> Result<Self, Error> {
|
||||
let mut data = Vec::new();
|
||||
r.read_to_end(&mut data).map_err(|_| Error::Parse("Buffer too small"))?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_pair() -> (InitState<Vec<u8>>, InitState<Vec<u8>>) {
|
||||
let rng = SystemRandom::new();
|
||||
let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
|
||||
let key_pair = Arc::new(Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()).unwrap());
|
||||
let mut public_key = [0; ED25519_PUBLIC_KEY_LEN];
|
||||
public_key.clone_from_slice(key_pair.public_key().as_ref());
|
||||
let trusted_nodes = Arc::new([public_key]);
|
||||
let mut node1 = [0; NODE_ID_BYTES];
|
||||
rng.fill(&mut node1).unwrap();
|
||||
let mut node2 = [0; NODE_ID_BYTES];
|
||||
rng.fill(&mut node2).unwrap();
|
||||
let algorithms = Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: false,
|
||||
};
|
||||
let sender = InitState::new(node1, vec![1], key_pair.clone(), trusted_nodes.clone(), algorithms.clone());
|
||||
let receiver = InitState::new(node2, vec![2], key_pair, trusted_nodes, algorithms);
|
||||
(sender, receiver)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_init() {
|
||||
let (mut sender, mut receiver) = create_pair();
|
||||
let mut out = MsgBuffer::new(8);
|
||||
sender.send_ping(&mut out);
|
||||
assert_eq!(sender.stage(), STAGE_PONG);
|
||||
let result = receiver.handle_init(&mut out).unwrap();
|
||||
assert_eq!(receiver.stage(), STAGE_PENG);
|
||||
assert_eq!(result, InitResult::Continue);
|
||||
let result = sender.handle_init(&mut out).unwrap();
|
||||
assert_eq!(sender.stage(), WAITING_TO_CLOSE);
|
||||
let result = match result {
|
||||
InitResult::Success { .. } => receiver.handle_init(&mut out).unwrap(),
|
||||
InitResult::Continue => unreachable!(),
|
||||
};
|
||||
assert_eq!(receiver.stage(), CLOSING);
|
||||
match result {
|
||||
InitResult::Success { .. } => assert!(out.is_empty()),
|
||||
InitResult::Continue => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lost_init_sender_recovers() {
|
||||
let (mut sender, mut receiver) = create_pair();
|
||||
let mut out = MsgBuffer::new(8);
|
||||
sender.send_ping(&mut out);
|
||||
assert_eq!(sender.stage(), STAGE_PONG);
|
||||
// lost ping, sender recovers
|
||||
out.clear();
|
||||
sender.every_second(&mut out).unwrap();
|
||||
let result = receiver.handle_init(&mut out).unwrap();
|
||||
assert_eq!(receiver.stage(), STAGE_PENG);
|
||||
assert_eq!(result, InitResult::Continue);
|
||||
// lost pong, sender recovers
|
||||
out.clear();
|
||||
receiver.every_second(&mut out).unwrap();
|
||||
let result = sender.handle_init(&mut out).unwrap();
|
||||
assert_eq!(sender.stage(), WAITING_TO_CLOSE);
|
||||
match result {
|
||||
InitResult::Success { .. } => {
|
||||
// lost peng, sender recovers
|
||||
out.clear();
|
||||
}
|
||||
InitResult::Continue => unreachable!(),
|
||||
};
|
||||
sender.every_second(&mut out).unwrap();
|
||||
let result = receiver.handle_init(&mut out).unwrap();
|
||||
assert_eq!(receiver.stage(), CLOSING);
|
||||
match result {
|
||||
InitResult::Success { .. } => assert!(out.is_empty()),
|
||||
InitResult::Continue => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lost_init_receiver_recovers() {
|
||||
let (mut sender, mut receiver) = create_pair();
|
||||
let mut out = MsgBuffer::new(8);
|
||||
sender.send_ping(&mut out);
|
||||
assert_eq!(sender.stage(), STAGE_PONG);
|
||||
let result = receiver.handle_init(&mut out).unwrap();
|
||||
assert_eq!(receiver.stage(), STAGE_PENG);
|
||||
assert_eq!(result, InitResult::Continue);
|
||||
// lost pong, receiver recovers
|
||||
out.clear();
|
||||
sender.every_second(&mut out).unwrap();
|
||||
receiver.handle_init(&mut out).unwrap();
|
||||
let result = sender.handle_init(&mut out).unwrap();
|
||||
assert_eq!(sender.stage(), WAITING_TO_CLOSE);
|
||||
match result {
|
||||
InitResult::Success { .. } => {
|
||||
// lost peng, sender recovers
|
||||
out.clear();
|
||||
}
|
||||
InitResult::Continue => unreachable!(),
|
||||
};
|
||||
receiver.every_second(&mut out).unwrap();
|
||||
sender.handle_init(&mut out).unwrap();
|
||||
let result = receiver.handle_init(&mut out).unwrap();
|
||||
assert_eq!(receiver.stage(), CLOSING);
|
||||
match result {
|
||||
InitResult::Success { .. } => assert!(out.is_empty()),
|
||||
InitResult::Continue => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeout() {
|
||||
let (mut sender, _receiver) = create_pair();
|
||||
let mut out = MsgBuffer::new(8);
|
||||
sender.send_ping(&mut out);
|
||||
assert_eq!(sender.stage(), STAGE_PONG);
|
||||
for _ in 0..120 {
|
||||
out.clear();
|
||||
sender.every_second(&mut out).unwrap();
|
||||
}
|
||||
out.clear();
|
||||
assert!(sender.every_second(&mut out).is_err());
|
||||
assert_eq!(sender.stage(), CLOSING);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn untrusted_peer() {
|
||||
let (mut sender, _) = create_pair();
|
||||
let (_, mut receiver) = create_pair();
|
||||
let mut out = MsgBuffer::new(8);
|
||||
sender.send_ping(&mut out);
|
||||
assert_eq!(sender.stage(), STAGE_PONG);
|
||||
assert!(receiver.handle_init(&mut out).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manipulated_message() {
|
||||
let (mut sender, mut receiver) = create_pair();
|
||||
let mut out = MsgBuffer::new(8);
|
||||
sender.send_ping(&mut out);
|
||||
assert_eq!(sender.stage(), STAGE_PONG);
|
||||
out.message_mut()[10] ^= 0x01;
|
||||
assert!(receiver.handle_init(&mut out).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_to_self() {
|
||||
let (mut sender, _) = create_pair();
|
||||
let mut out = MsgBuffer::new(8);
|
||||
sender.send_ping(&mut out);
|
||||
assert_eq!(sender.stage(), STAGE_PONG);
|
||||
assert!(sender.handle_init(&mut out).is_err());
|
||||
}
|
||||
|
||||
fn test_algorithm_negotiation(
|
||||
algos1: Algorithms, algos2: Algorithms, success: bool, selected: Option<&'static Algorithm>,
|
||||
) {
|
||||
let (mut sender, mut receiver) = create_pair();
|
||||
sender.algorithms = algos1;
|
||||
receiver.algorithms = algos2;
|
||||
let mut out = MsgBuffer::new(8);
|
||||
sender.send_ping(&mut out);
|
||||
let res = receiver.handle_init(&mut out);
|
||||
assert_eq!(res.is_ok(), success);
|
||||
if !success {
|
||||
return;
|
||||
}
|
||||
sender.handle_init(&mut out).unwrap();
|
||||
receiver.handle_init(&mut out).unwrap();
|
||||
assert_eq!(sender.selected_algorithm, selected);
|
||||
assert_eq!(sender.selected_algorithm, receiver.selected_algorithm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn algorithm_negotiation() {
|
||||
// Equal algorithms
|
||||
test_algorithm_negotiation(
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: false,
|
||||
},
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: false,
|
||||
},
|
||||
true,
|
||||
Some(&AES_128_GCM),
|
||||
);
|
||||
|
||||
// Overlapping but different
|
||||
test_algorithm_negotiation(
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: false,
|
||||
},
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0)],
|
||||
allow_unencrypted: false,
|
||||
},
|
||||
true,
|
||||
Some(&AES_256_GCM),
|
||||
);
|
||||
|
||||
// Select fastest pair
|
||||
test_algorithm_negotiation(
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: false,
|
||||
},
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 40.0), (&AES_256_GCM, 50.0), (&CHACHA20_POLY1305, 60.0)],
|
||||
allow_unencrypted: false,
|
||||
},
|
||||
true,
|
||||
Some(&CHACHA20_POLY1305),
|
||||
);
|
||||
|
||||
// Select unencrypted if supported by both
|
||||
test_algorithm_negotiation(
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: true,
|
||||
},
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: true,
|
||||
},
|
||||
true,
|
||||
None,
|
||||
);
|
||||
|
||||
// Do not select unencrypted if only supported by one
|
||||
test_algorithm_negotiation(
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: true,
|
||||
},
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_128_GCM, 600.0), (&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: false,
|
||||
},
|
||||
true,
|
||||
Some(&AES_128_GCM),
|
||||
);
|
||||
|
||||
// Fail if no match
|
||||
test_algorithm_negotiation(
|
||||
Algorithms { algorithm_speeds: smallvec![(&AES_128_GCM, 600.0)], allow_unencrypted: true },
|
||||
Algorithms {
|
||||
algorithm_speeds: smallvec![(&AES_256_GCM, 500.0), (&CHACHA20_POLY1305, 400.0)],
|
||||
allow_unencrypted: false,
|
||||
},
|
||||
false,
|
||||
Some(&AES_128_GCM),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
mod common;
|
||||
mod core;
|
||||
mod init;
|
||||
mod rotate;
|
||||
|
||||
pub use self::core::{EXTRA_LEN, TAG_LEN};
|
||||
pub use common::*;
|
|
@ -0,0 +1,412 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
// This module implements a turn based key rotation.
|
||||
//
|
||||
// The main idea is that both peers periodically create ecdh key pairs and exchange their public keys to create
|
||||
// common key material. There are always two separate ecdh handshakes going on: one initiated by each peer.
|
||||
// However, one handshake is always one step ahead of the other. That means that every message being sent contains a
|
||||
// public key from step 1 of the handshake "proposed key" and a public key from step 2 of the handshake "confirmed
|
||||
// key" (all messages except first message).
|
||||
//
|
||||
// When receiving a message from the peer, the node will create a new ecdh key pair and perform the key
|
||||
// calculation for the proposed key. The peer will store the public key for the confirmation as pending to be
|
||||
// confirmed in the next cycle. Also, if the message contains a confirmation (all but the very first message do),
|
||||
// the node will use the stored private key to perform the ecdh key calculation and emit that key to be used in
|
||||
// the crypto stream.
|
||||
//
|
||||
// Upon each cycle, a node first checks if it still has a proposed key that has not been confirmed by the remote
|
||||
// peer. If so, a message must have been lost and the whole last message including the proposed key as well as the
|
||||
// last confirmed key is being resent. If no proposed key is stored, the node will create a new ecdh key pair, and
|
||||
// store the private key as proposed key. It then sends out a message containing the public key as proposal, as
|
||||
// well as confirming the pending key. This key is also emitted to be added to the crypto stream but not to be
|
||||
// used for encrypting.
|
||||
//
|
||||
// Monotonically increasing message ids guard the communication from message duplication and also serve as
|
||||
// identifiers for the keys to be used in the crypto stream. Since the keys are rotating, the last 2 bits of the
|
||||
// id are enough to identify the key.
|
||||
//
|
||||
// The whole communication is sent via the crypto stream and is therefore encrypted and protected against tampering.
|
||||
|
||||
use super::Key;
|
||||
use crate::{error::Error, util::MsgBuffer};
|
||||
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
|
||||
use ring::{
|
||||
agreement::{agree_ephemeral, EphemeralPrivateKey, UnparsedPublicKey, X25519},
|
||||
rand::SystemRandom,
|
||||
};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::io::{self, Cursor, Read, Write};
|
||||
|
||||
type EcdhPublicKey = UnparsedPublicKey<SmallVec<[u8; 96]>>;
|
||||
type EcdhPrivateKey = EphemeralPrivateKey;
|
||||
|
||||
pub struct RotationMessage {
|
||||
message_id: u64,
|
||||
propose: EcdhPublicKey,
|
||||
confirm: Option<EcdhPublicKey>,
|
||||
}
|
||||
|
||||
impl RotationMessage {
|
||||
#[allow(dead_code)]
|
||||
pub fn read_from<R: Read>(mut r: R) -> Result<Self, io::Error> {
|
||||
let message_id = r.read_u64::<NetworkEndian>()?;
|
||||
let key_len = r.read_u8()? as usize;
|
||||
let mut key_data = smallvec![0; key_len];
|
||||
r.read_exact(&mut key_data)?;
|
||||
let propose = EcdhPublicKey::new(&X25519, key_data);
|
||||
let key_len = r.read_u8()? as usize;
|
||||
let confirm = if key_len > 0 {
|
||||
let mut key_data = smallvec![0; key_len];
|
||||
r.read_exact(&mut key_data)?;
|
||||
Some(EcdhPublicKey::new(&X25519, key_data))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(RotationMessage { message_id, propose, confirm })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn write_to<W: Write>(&self, mut w: W) -> Result<(), io::Error> {
|
||||
w.write_u64::<NetworkEndian>(self.message_id)?;
|
||||
let key_bytes = self.propose.bytes();
|
||||
w.write_u8(key_bytes.len() as u8)?;
|
||||
w.write_all(key_bytes)?;
|
||||
if let Some(ref key) = self.confirm {
|
||||
let key_bytes = key.bytes();
|
||||
w.write_u8(key_bytes.len() as u8)?;
|
||||
w.write_all(key_bytes)?;
|
||||
} else {
|
||||
w.write_u8(0)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RotationState {
|
||||
confirmed: Option<(EcdhPublicKey, u64)>, // sent by remote, already confirmed
|
||||
pending: Option<(Key, EcdhPublicKey)>, // sent by remote, to be confirmed
|
||||
proposed: Option<EcdhPrivateKey>, // my own, proposed but not confirmed
|
||||
message_id: u64,
|
||||
timeout: bool,
|
||||
}
|
||||
|
||||
pub struct RotatedKey {
|
||||
pub key: Key,
|
||||
pub id: u64,
|
||||
pub use_for_sending: bool,
|
||||
}
|
||||
|
||||
impl RotationState {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(initiator: bool, out: &mut MsgBuffer) -> Self {
|
||||
if initiator {
|
||||
let (private_key, public_key) = Self::create_key();
|
||||
Self::send(&RotationMessage { message_id: 1, confirm: None, propose: public_key }, out);
|
||||
Self { confirmed: None, pending: None, proposed: Some(private_key), message_id: 1, timeout: false }
|
||||
} else {
|
||||
Self { confirmed: None, pending: None, proposed: None, message_id: 0, timeout: false }
|
||||
}
|
||||
}
|
||||
|
||||
fn send(msg: &RotationMessage, out: &mut MsgBuffer) {
|
||||
assert!(out.is_empty());
|
||||
debug!("Rotation sending message with id {}", msg.message_id);
|
||||
let len;
|
||||
{
|
||||
let mut cursor = Cursor::new(out.buffer());
|
||||
msg.write_to(&mut cursor).expect("Buffer too small");
|
||||
len = cursor.position() as usize;
|
||||
}
|
||||
out.set_length(len);
|
||||
}
|
||||
|
||||
fn create_key() -> (EcdhPrivateKey, EcdhPublicKey) {
|
||||
let rand = SystemRandom::new();
|
||||
let private_key = EcdhPrivateKey::generate(&X25519, &rand).unwrap();
|
||||
let public_key = Self::compute_public_key(&private_key);
|
||||
(private_key, public_key)
|
||||
}
|
||||
|
||||
fn compute_public_key(private_key: &EcdhPrivateKey) -> EcdhPublicKey {
|
||||
let public_key = private_key.compute_public_key().unwrap();
|
||||
let mut vec = SmallVec::<[u8; 96]>::new();
|
||||
vec.extend_from_slice(public_key.as_ref());
|
||||
EcdhPublicKey::new(&X25519, vec)
|
||||
}
|
||||
|
||||
fn derive_key(private_key: EcdhPrivateKey, public_key: EcdhPublicKey) -> Key {
|
||||
agree_ephemeral(private_key, &public_key, |k| {
|
||||
let mut vec = Key::new();
|
||||
vec.extend_from_slice(k);
|
||||
vec
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn handle_message(&mut self, msg: &[u8]) -> Result<Option<RotatedKey>, Error> {
|
||||
let msg =
|
||||
RotationMessage::read_from(Cursor::new(msg)).map_err(|_| Error::Crypto("Rotation message too short"))?;
|
||||
Ok(self.process_message(msg))
|
||||
}
|
||||
|
||||
pub fn process_message(&mut self, msg: RotationMessage) -> Option<RotatedKey> {
|
||||
if msg.message_id <= self.message_id {
|
||||
return None;
|
||||
}
|
||||
debug!("Received rotation message with id {}", msg.message_id);
|
||||
self.timeout = false;
|
||||
// Create key from proposal and store reply as pending
|
||||
let (private_key, public_key) = Self::create_key();
|
||||
let key = Self::derive_key(private_key, msg.propose);
|
||||
self.pending = Some((key, public_key));
|
||||
// If proposed key has been confirmed, derive and use key
|
||||
if let Some(peer_key) = msg.confirm {
|
||||
if let Some(private_key) = self.proposed.take() {
|
||||
let key = Self::derive_key(private_key, peer_key);
|
||||
return Some(RotatedKey { key, id: msg.message_id, use_for_sending: true });
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn cycle(&mut self, out: &mut MsgBuffer) -> Option<RotatedKey> {
|
||||
if let Some(ref private_key) = self.proposed {
|
||||
// Still a proposed key that has not been confirmed, proposal must have been lost
|
||||
if self.timeout {
|
||||
let proposed_key = Self::compute_public_key(private_key);
|
||||
if let Some((ref confirmed_key, message_id)) = self.confirmed {
|
||||
// Reconfirm last confirmed key
|
||||
Self::send(
|
||||
&RotationMessage { confirm: Some(confirmed_key.clone()), propose: proposed_key, message_id },
|
||||
out,
|
||||
);
|
||||
} else {
|
||||
// First message has been lost
|
||||
Self::send(&RotationMessage { confirm: None, propose: proposed_key, message_id: 1 }, out);
|
||||
}
|
||||
} else {
|
||||
self.timeout = true;
|
||||
}
|
||||
} else {
|
||||
// No proposed key, our turn to propose a new one
|
||||
if let Some((key, confirm_key)) = self.pending.take() {
|
||||
// Send out pending confirmation and register key for receiving
|
||||
self.message_id += 2;
|
||||
let message_id = self.message_id;
|
||||
let (private_key, propose_key) = Self::create_key();
|
||||
self.proposed = Some(private_key);
|
||||
self.confirmed = Some((confirm_key.clone(), message_id));
|
||||
Self::send(&RotationMessage { confirm: Some(confirm_key), propose: propose_key, message_id }, out);
|
||||
return Some(RotatedKey { key, id: message_id, use_for_sending: false });
|
||||
} else {
|
||||
// Nothing pending nor proposed, still waiting to receive message 1
|
||||
// Do nothing, peer will retry
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
impl MsgBuffer {
|
||||
fn msg(&mut self) -> Option<RotationMessage> {
|
||||
if self.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let msg = RotationMessage::read_from(Cursor::new(self.message())).unwrap();
|
||||
self.set_length(0);
|
||||
Some(msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode_message() {
|
||||
let mut data = Vec::with_capacity(100);
|
||||
let (_, key) = RotationState::create_key();
|
||||
let msg = RotationMessage { message_id: 1, propose: key, confirm: None };
|
||||
msg.write_to(&mut data).unwrap();
|
||||
let msg2 = RotationMessage::read_from(Cursor::new(&data)).unwrap();
|
||||
assert_eq!(msg.message_id, msg2.message_id);
|
||||
assert_eq!(msg.propose.bytes(), msg2.propose.bytes());
|
||||
assert_eq!(msg.confirm.map(|v| v.bytes().to_vec()), msg2.confirm.map(|v| v.bytes().to_vec()));
|
||||
let mut data = Vec::with_capacity(100);
|
||||
let (_, key1) = RotationState::create_key();
|
||||
let (_, key2) = RotationState::create_key();
|
||||
let msg = RotationMessage { message_id: 2, propose: key1, confirm: Some(key2) };
|
||||
msg.write_to(&mut data).unwrap();
|
||||
let msg2 = RotationMessage::read_from(Cursor::new(&data)).unwrap();
|
||||
assert_eq!(msg.message_id, msg2.message_id);
|
||||
assert_eq!(msg.propose.bytes(), msg2.propose.bytes());
|
||||
assert_eq!(msg.confirm.map(|v| v.bytes().to_vec()), msg2.confirm.map(|v| v.bytes().to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normal_rotation() {
|
||||
let mut out1 = MsgBuffer::new(8);
|
||||
let mut out2 = MsgBuffer::new(8);
|
||||
|
||||
// Initialization
|
||||
let mut node1 = RotationState::new(true, &mut out1);
|
||||
let mut node2 = RotationState::new(false, &mut out2);
|
||||
assert!(!out1.is_empty());
|
||||
let msg1 = out1.msg().unwrap();
|
||||
assert_eq!(msg1.message_id, 1);
|
||||
assert!(out2.is_empty());
|
||||
// Message 1
|
||||
let key = node2.process_message(msg1);
|
||||
assert!(key.is_none());
|
||||
// Cycle 1
|
||||
let key1 = node1.cycle(&mut out1);
|
||||
let key2 = node2.cycle(&mut out2);
|
||||
assert!(key1.is_none());
|
||||
assert!(out1.is_empty());
|
||||
assert!(key2.is_some());
|
||||
let key2 = key2.unwrap();
|
||||
assert_eq!(key2.id, 2);
|
||||
assert!(!key2.use_for_sending);
|
||||
assert!(!out2.is_empty());
|
||||
let msg2 = out2.msg().unwrap();
|
||||
assert_eq!(msg2.message_id, 2);
|
||||
assert!(msg2.confirm.is_some());
|
||||
// Message 2
|
||||
let key = node1.process_message(msg2);
|
||||
assert!(key.is_some());
|
||||
let key = key.unwrap();
|
||||
assert_eq!(key.id, 2);
|
||||
assert!(key.use_for_sending);
|
||||
// Cycle 2
|
||||
let key1 = node1.cycle(&mut out1);
|
||||
let key2 = node2.cycle(&mut out2);
|
||||
assert!(key1.is_some());
|
||||
let key1 = key1.unwrap();
|
||||
assert_eq!(key1.id, 3);
|
||||
assert!(!key1.use_for_sending);
|
||||
assert!(!out1.is_empty());
|
||||
let msg1 = out1.msg().unwrap();
|
||||
assert_eq!(msg1.message_id, 3);
|
||||
assert!(msg1.confirm.is_some());
|
||||
assert!(key2.is_none());
|
||||
assert!(out2.is_empty());
|
||||
// Message 3
|
||||
let key = node2.process_message(msg1);
|
||||
assert!(key.is_some());
|
||||
let key = key.unwrap();
|
||||
assert_eq!(key.id, 3);
|
||||
assert!(key.use_for_sending);
|
||||
// Cycle 3
|
||||
let key1 = node1.cycle(&mut out1);
|
||||
let key2 = node2.cycle(&mut out2);
|
||||
assert!(key1.is_none());
|
||||
assert!(out1.is_empty());
|
||||
assert!(key2.is_some());
|
||||
let key2 = key2.unwrap();
|
||||
assert_eq!(key2.id, 4);
|
||||
assert!(!key2.use_for_sending);
|
||||
assert!(!out2.is_empty());
|
||||
let msg2 = out2.msg().unwrap();
|
||||
assert_eq!(msg2.message_id, 4);
|
||||
assert!(msg2.confirm.is_some());
|
||||
// Message 4
|
||||
let key = node1.process_message(msg2);
|
||||
assert!(key.is_some());
|
||||
let key = key.unwrap();
|
||||
assert_eq!(key.id, 4);
|
||||
assert!(key.use_for_sending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplication() {
|
||||
let mut out1 = MsgBuffer::new(8);
|
||||
let mut out2 = MsgBuffer::new(8);
|
||||
|
||||
let mut node1 = RotationState::new(true, &mut out1);
|
||||
let mut node2 = RotationState::new(false, &mut out2);
|
||||
let msg1 = out1.clone().msg().unwrap();
|
||||
let msg1_copy = out1.msg().unwrap();
|
||||
node2.process_message(msg1);
|
||||
assert!(node2.process_message(msg1_copy).is_none());
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
let msg2 = out2.clone().msg().unwrap();
|
||||
let msg2_copy = out2.msg().unwrap();
|
||||
// Message 2
|
||||
assert!(node1.process_message(msg2).is_some());
|
||||
assert!(node1.process_message(msg2_copy).is_none());
|
||||
// Cycle 2
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
let msg1 = out1.clone().msg().unwrap();
|
||||
let msg1_copy = out1.msg().unwrap();
|
||||
// Message 3
|
||||
assert!(node2.process_message(msg1).is_some());
|
||||
assert!(node2.process_message(msg1_copy).is_none());
|
||||
// Cycle 3
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
let msg2 = out2.clone().msg().unwrap();
|
||||
let msg2_copy = out2.msg().unwrap();
|
||||
// Message 4
|
||||
assert!(node1.process_message(msg2).is_some());
|
||||
assert!(node1.process_message(msg2_copy).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lost_message() {
|
||||
let mut out1 = MsgBuffer::new(8);
|
||||
let mut out2 = MsgBuffer::new(8);
|
||||
|
||||
let mut node1 = RotationState::new(true, &mut out1);
|
||||
let mut node2 = RotationState::new(false, &mut out2);
|
||||
let _msg1 = out1.msg().unwrap();
|
||||
// drop msg1
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
assert!(out2.msg().is_none());
|
||||
// Cycle 2
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
let msg1 = out1.msg().unwrap();
|
||||
// Message 3
|
||||
assert!(node2.process_message(msg1).is_none());
|
||||
// Cycle 3
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
let msg2 = out2.msg().unwrap();
|
||||
// Message 4
|
||||
assert!(node1.process_message(msg2).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reflect_back() {
|
||||
let mut out1 = MsgBuffer::new(8);
|
||||
let mut out2 = MsgBuffer::new(8);
|
||||
|
||||
let mut node1 = RotationState::new(true, &mut out1);
|
||||
let mut node2 = RotationState::new(false, &mut out2);
|
||||
let msg1 = out1.msg().unwrap();
|
||||
assert!(node1.process_message(msg1).is_none());
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
assert!(out2.msg().is_none());
|
||||
// Cycle 2
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
let msg1 = out1.msg().unwrap();
|
||||
// Message 3
|
||||
assert!(node2.process_message(msg1).is_none());
|
||||
// Cycle 3
|
||||
node1.cycle(&mut out1);
|
||||
node2.cycle(&mut out2);
|
||||
let msg2 = out2.msg().unwrap();
|
||||
// Message 4
|
||||
assert!(node1.process_message(msg2).is_some());
|
||||
}
|
||||
}
|
384
src/device.rs
384
src/device.rs
|
@ -1,22 +1,53 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
collections::VecDeque,
|
||||
fmt, fs,
|
||||
io::{self, Error as IoError, ErrorKind, Read, Write},
|
||||
os::unix::io::{AsRawFd, RawFd},
|
||||
convert::TryInto,
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
io::{self, BufRead, BufReader, Cursor, Error as IoError, Read, Write},
|
||||
net::{Ipv4Addr, UdpSocket},
|
||||
os::{unix::io::AsRawFd, fd::RawFd},
|
||||
str,
|
||||
str::FromStr
|
||||
};
|
||||
|
||||
use super::types::Error;
|
||||
use crate::{crypto, error::Error, util::MsgBuffer};
|
||||
|
||||
extern "C" {
|
||||
fn setup_tap_device(fd: i32, ifname: *mut u8) -> i32;
|
||||
fn setup_tun_device(fd: i32, ifname: *mut u8) -> i32;
|
||||
static TUNSETIFF: libc::c_ulong = 1074025674;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
struct IfReqDataAddr {
|
||||
af: libc::c_int,
|
||||
addr: Ipv4Addr
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
union IfReqData {
|
||||
flags: libc::c_short,
|
||||
value: libc::c_int,
|
||||
addr: IfReqDataAddr,
|
||||
_dummy: [u8; 24],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct IfReq {
|
||||
ifr_name: [u8; libc::IF_NAMESIZE],
|
||||
data: IfReqData,
|
||||
}
|
||||
|
||||
impl IfReq {
|
||||
fn new(name: &str) -> Self {
|
||||
assert!(name.len() < libc::IF_NAMESIZE);
|
||||
let mut ifr_name = [0; libc::IF_NAMESIZE];
|
||||
ifr_name[..name.len()].clone_from_slice(name.as_bytes());
|
||||
Self { ifr_name, data: IfReqData { _dummy: [0; 24] } }
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of a tun/tap device
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
|
@ -27,9 +58,6 @@ pub enum Type {
|
|||
/// Tap interface: This interface transports Ethernet frames.
|
||||
#[serde(rename = "tap")]
|
||||
Tap,
|
||||
/// Dummy interface: This interface does nothing.
|
||||
#[serde(rename = "dummy")]
|
||||
Dummy
|
||||
}
|
||||
|
||||
impl fmt::Display for Type {
|
||||
|
@ -37,7 +65,6 @@ impl fmt::Display for Type {
|
|||
match *self {
|
||||
Type::Tun => write!(formatter, "tun"),
|
||||
Type::Tap => write!(formatter, "tap"),
|
||||
Type::Dummy => write!(formatter, "dummy")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,8 +76,7 @@ impl FromStr for Type {
|
|||
Ok(match &text.to_lowercase() as &str {
|
||||
"tun" => Self::Tun,
|
||||
"tap" => Self::Tap,
|
||||
"dummy" => Self::Dummy,
|
||||
_ => return Err("Unknown device type")
|
||||
_ => return Err("Unknown device type"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +99,7 @@ pub trait Device: AsRawFd {
|
|||
///
|
||||
/// # Errors
|
||||
/// This method will return an error if the underlying read call fails.
|
||||
fn read(&mut self, buffer: &mut [u8]) -> Result<(usize, usize), Error>;
|
||||
fn read(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error>;
|
||||
|
||||
/// Writes a packet/frame to the device
|
||||
///
|
||||
|
@ -84,18 +110,18 @@ pub trait Device: AsRawFd {
|
|||
///
|
||||
/// # Errors
|
||||
/// This method will return an error if the underlying read call fails.
|
||||
fn write(&mut self, data: &mut [u8], start: usize) -> Result<(), Error>;
|
||||
}
|
||||
fn write(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error>;
|
||||
|
||||
fn get_ip(&self) -> Result<Ipv4Addr, Error>;
|
||||
}
|
||||
|
||||
/// Represents a tun/tap device
|
||||
pub struct TunTapDevice {
|
||||
fd: fs::File,
|
||||
fd: File,
|
||||
ifname: String,
|
||||
type_: Type
|
||||
type_: Type,
|
||||
}
|
||||
|
||||
|
||||
impl TunTapDevice {
|
||||
/// Creates a new tun/tap device
|
||||
///
|
||||
|
@ -114,32 +140,26 @@ impl TunTapDevice {
|
|||
///
|
||||
/// # Panics
|
||||
/// This method panics if the interface name is longer than 31 bytes.
|
||||
#[allow(clippy::useless_conversion)]
|
||||
pub fn new(ifname: &str, type_: Type, path: Option<&str>) -> io::Result<Self> {
|
||||
let path = path.unwrap_or_else(|| Self::default_path(type_));
|
||||
if type_ == Type::Dummy {
|
||||
return Self::dummy(ifname, path, type_)
|
||||
}
|
||||
let fd = fs::OpenOptions::new().read(true).write(true).open(path)?;
|
||||
// Add trailing \0 to interface name
|
||||
let mut ifname_string = String::with_capacity(32);
|
||||
ifname_string.push_str(ifname);
|
||||
ifname_string.push('\0');
|
||||
assert!(ifname_string.len() <= 32);
|
||||
let mut ifname_c = ifname_string.into_bytes();
|
||||
let res = match type_ {
|
||||
Type::Tun => unsafe { setup_tun_device(fd.as_raw_fd(), ifname_c.as_mut_ptr()) },
|
||||
Type::Tap => unsafe { setup_tap_device(fd.as_raw_fd(), ifname_c.as_mut_ptr()) },
|
||||
Type::Dummy => unreachable!()
|
||||
let flags = match type_ {
|
||||
Type::Tun => libc::IFF_TUN | libc::IFF_NO_PI,
|
||||
Type::Tap => libc::IFF_TAP | libc::IFF_NO_PI,
|
||||
};
|
||||
let mut ifreq = IfReq::new(ifname);
|
||||
ifreq.data.flags = flags as libc::c_short;
|
||||
let res = unsafe { libc::ioctl(fd.as_raw_fd(), TUNSETIFF.try_into().unwrap(), &mut ifreq) };
|
||||
match res {
|
||||
0 => {
|
||||
// Remove trailing \0 from name
|
||||
while ifname_c.last() == Some(&0) {
|
||||
ifname_c.pop();
|
||||
}
|
||||
Ok(Self { fd, ifname: String::from_utf8(ifname_c).unwrap(), type_ })
|
||||
let mut ifname = String::with_capacity(32);
|
||||
let mut cursor = Cursor::new(ifreq.ifr_name);
|
||||
cursor.read_to_string(&mut ifname)?;
|
||||
ifname = ifname.trim_end_matches('\0').to_owned();
|
||||
Ok(Self { fd, ifname, type_ })
|
||||
}
|
||||
_ => Err(IoError::last_os_error())
|
||||
_ => Err(IoError::last_os_error()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,37 +168,12 @@ impl TunTapDevice {
|
|||
pub fn default_path(type_: Type) -> &'static str {
|
||||
match type_ {
|
||||
Type::Tun | Type::Tap => "/dev/net/tun",
|
||||
Type::Dummy => "/dev/null"
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a dummy device based on an existing file
|
||||
///
|
||||
/// This method opens a regular or special file and reads from it to receive packets and
|
||||
/// writes to it to send packets. This method does not use a networking device and therefore
|
||||
/// can be used for testing.
|
||||
///
|
||||
/// The parameter `path` is the file that should be used. Special files like `/dev/null`,
|
||||
/// named pipes and unix sockets can be used with this method.
|
||||
///
|
||||
/// Both `ifname` and `type_` parameters have no effect.
|
||||
///
|
||||
/// # Errors
|
||||
/// This method will return an error if the file can not be opened for reading and writing.
|
||||
#[allow(dead_code)]
|
||||
pub fn dummy(ifname: &str, path: &str, type_: Type) -> io::Result<Self> {
|
||||
Ok(TunTapDevice {
|
||||
fd: fs::OpenOptions::new().create(true).read(true).write(true).open(path)?,
|
||||
ifname: ifname.to_string(),
|
||||
type_
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[inline]
|
||||
fn correct_data_after_read(&mut self, _buffer: &mut [u8], start: usize, read: usize) -> (usize, usize) {
|
||||
(start, read)
|
||||
}
|
||||
fn correct_data_after_read(&mut self, _buffer: &mut MsgBuffer) {}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "bitrig",
|
||||
|
@ -190,21 +185,17 @@ impl TunTapDevice {
|
|||
target_os = "openbsd"
|
||||
))]
|
||||
#[inline]
|
||||
fn correct_data_after_read(&mut self, buffer: &mut [u8], start: usize, read: usize) -> (usize, usize) {
|
||||
fn correct_data_after_read(&mut self, buffer: &mut MsgBuffer) {
|
||||
if self.type_ == Type::Tun {
|
||||
// BSD-based systems add a 4-byte header containing the Ethertype for TUN
|
||||
assert!(read >= 4);
|
||||
(start + 4, read - 4)
|
||||
buffer.set_start(buffer.get_start() + 4);
|
||||
} else {
|
||||
(start, read)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[inline]
|
||||
fn correct_data_before_write(&mut self, _buffer: &mut [u8], start: usize) -> usize {
|
||||
start
|
||||
}
|
||||
fn correct_data_before_write(&mut self, _buffer: &mut MsgBuffer) {}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "bitrig",
|
||||
|
@ -216,21 +207,63 @@ impl TunTapDevice {
|
|||
target_os = "openbsd"
|
||||
))]
|
||||
#[inline]
|
||||
fn correct_data_before_write(&mut self, buffer: &mut [u8], start: usize) -> usize {
|
||||
fn correct_data_before_write(&mut self, buffer: &mut MsgBuffer) {
|
||||
if self.type_ == Type::Tun {
|
||||
// BSD-based systems add a 4-byte header containing the Ethertype for TUN
|
||||
assert!(start >= 4);
|
||||
match buffer[start] >> 4 {
|
||||
buffer.set_start(buffer.get_start() - 4);
|
||||
match buffer.message()[4] >> 4 {
|
||||
// IP version
|
||||
4 => buffer[start - 4..start].copy_from_slice(&[0x00, 0x00, 0x08, 0x00]),
|
||||
6 => buffer[start - 4..start].copy_from_slice(&[0x00, 0x00, 0x86, 0xdd]),
|
||||
_ => unreachable!()
|
||||
4 => buffer.message_mut()[0..4].copy_from_slice(&[0x00, 0x00, 0x08, 0x00]),
|
||||
6 => buffer.message_mut()[0..4].copy_from_slice(&[0x00, 0x00, 0x86, 0xdd]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
start - 4
|
||||
} else {
|
||||
start
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_overhead(&self) -> usize {
|
||||
40 /* for outer IPv6 header, can't be sure to only have IPv4 peers */
|
||||
+ 8 /* for outer UDP header */
|
||||
+ crypto::EXTRA_LEN + crypto::TAG_LEN /* crypto overhead */
|
||||
+ 1 /* message type header */
|
||||
+ match self.type_ {
|
||||
Type::Tap => 14, /* inner ethernet header */
|
||||
Type::Tun => 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mtu(&self, value: Option<usize>) -> io::Result<()> {
|
||||
let value = match value {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
let default_device = get_default_device()?;
|
||||
get_device_mtu(&default_device)? - self.get_overhead()
|
||||
}
|
||||
};
|
||||
info!("Setting MTU {} on device {}", value, self.ifname);
|
||||
set_device_mtu(&self.ifname, value)
|
||||
}
|
||||
|
||||
pub fn configure(&self, addr: Ipv4Addr, netmask: Ipv4Addr) -> io::Result<()> {
|
||||
set_device_addr(&self.ifname, addr)?;
|
||||
set_device_netmask(&self.ifname, netmask)?;
|
||||
set_device_enabled(&self.ifname, true)
|
||||
}
|
||||
|
||||
pub fn get_rp_filter(&self) -> io::Result<u8> {
|
||||
Ok(cmp::max(get_rp_filter("all")?, get_rp_filter(&self.ifname)?))
|
||||
}
|
||||
|
||||
pub fn fix_rp_filter(&self) -> io::Result<()> {
|
||||
if get_rp_filter("all")? > 1 {
|
||||
info!("Setting net.ipv4.conf.all.rp_filter=1");
|
||||
set_rp_filter("all", 1)?
|
||||
}
|
||||
if get_rp_filter(&self.ifname)? != 1 {
|
||||
info!("Setting net.ipv4.conf.{}.rp_filter=1", self.ifname);
|
||||
set_rp_filter(&self.ifname, 1)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for TunTapDevice {
|
||||
|
@ -242,19 +275,25 @@ impl Device for TunTapDevice {
|
|||
&self.ifname
|
||||
}
|
||||
|
||||
fn read(&mut self, mut buffer: &mut [u8]) -> Result<(usize, usize), Error> {
|
||||
let read = self.fd.read(&mut buffer).map_err(|e| Error::TunTapDev("Read error", e))?;
|
||||
let (start, read) = self.correct_data_after_read(&mut buffer, 0, read);
|
||||
Ok((start, read))
|
||||
fn read(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
|
||||
buffer.clear();
|
||||
let read = self.fd.read(buffer.buffer()).map_err(|e| Error::DeviceIo("Read error", e))?;
|
||||
buffer.set_length(read);
|
||||
self.correct_data_after_read(buffer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write(&mut self, mut data: &mut [u8], start: usize) -> Result<(), Error> {
|
||||
let start = self.correct_data_before_write(&mut data, start);
|
||||
match self.fd.write_all(&data[start..]) {
|
||||
Ok(_) => self.fd.flush().map_err(|e| Error::TunTapDev("Flush error", e)),
|
||||
Err(e) => Err(Error::TunTapDev("Write error", e))
|
||||
fn write(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
|
||||
self.correct_data_before_write(buffer);
|
||||
match self.fd.write_all(buffer.message()) {
|
||||
Ok(_) => self.fd.flush().map_err(|e| Error::DeviceIo("Flush error", e)),
|
||||
Err(e) => Err(Error::DeviceIo("Write error", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ip(&self) -> Result<Ipv4Addr, Error> {
|
||||
get_device_addr(&self.ifname).map_err(|e| Error::DeviceIo("Error getting IP address", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for TunTapDevice {
|
||||
|
@ -264,10 +303,9 @@ impl AsRawFd for TunTapDevice {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct MockDevice {
|
||||
inbound: VecDeque<Vec<u8>>,
|
||||
outbound: VecDeque<Vec<u8>>
|
||||
outbound: VecDeque<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl MockDevice {
|
||||
|
@ -290,31 +328,37 @@ impl MockDevice {
|
|||
|
||||
impl Device for MockDevice {
|
||||
fn get_type(&self) -> Type {
|
||||
Type::Dummy
|
||||
Type::Tun
|
||||
}
|
||||
|
||||
fn ifname(&self) -> &str {
|
||||
unimplemented!()
|
||||
"mock0"
|
||||
}
|
||||
|
||||
fn read(&mut self, buffer: &mut [u8]) -> Result<(usize, usize), Error> {
|
||||
fn read(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
|
||||
if let Some(data) = self.inbound.pop_front() {
|
||||
buffer[0..data.len()].copy_from_slice(&data);
|
||||
Ok((0, data.len()))
|
||||
buffer.clear();
|
||||
buffer.set_length(data.len());
|
||||
buffer.message_mut().copy_from_slice(&data);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::TunTapDev("empty", io::Error::from(ErrorKind::UnexpectedEof)))
|
||||
Err(Error::Device("empty"))
|
||||
}
|
||||
}
|
||||
|
||||
fn write(&mut self, data: &mut [u8], start: usize) -> Result<(), Error> {
|
||||
self.outbound.push_back(data[start..].to_owned());
|
||||
fn write(&mut self, buffer: &mut MsgBuffer) -> Result<(), Error> {
|
||||
self.outbound.push_back(buffer.message().into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_ip(&self) -> Result<Ipv4Addr, Error> {
|
||||
Err(Error::Device("Dummy devices have no IP address"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MockDevice {
|
||||
fn default() -> Self {
|
||||
Self { outbound: VecDeque::new(), inbound: VecDeque::new() }
|
||||
Self { outbound: VecDeque::with_capacity(10), inbound: VecDeque::with_capacity(10) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,3 +368,141 @@ impl AsRawFd for MockDevice {
|
|||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
fn set_device_mtu(ifname: &str, mtu: usize) -> io::Result<()> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let mut ifreq = IfReq::new(ifname);
|
||||
ifreq.data.value = mtu as libc::c_int;
|
||||
let res = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCSIFMTU.try_into().unwrap(), &mut ifreq) };
|
||||
match res {
|
||||
0 => Ok(()),
|
||||
_ => Err(IoError::last_os_error()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
fn get_device_mtu(ifname: &str) -> io::Result<usize> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let mut ifreq = IfReq::new(ifname);
|
||||
let res = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCGIFMTU.try_into().unwrap(), &mut ifreq) };
|
||||
match res {
|
||||
0 => Ok(unsafe { ifreq.data.value as usize }),
|
||||
_ => Err(IoError::last_os_error()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
fn get_device_addr(ifname: &str) -> io::Result<Ipv4Addr> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let mut ifreq = IfReq::new(ifname);
|
||||
let res = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCGIFADDR.try_into().unwrap(), &mut ifreq) };
|
||||
match res {
|
||||
0 => {
|
||||
let af = unsafe { ifreq.data.addr.af };
|
||||
if af as libc::c_int != libc::AF_INET {
|
||||
return Err(io::Error::new(io::ErrorKind::AddrNotAvailable, "Invalid address family".to_owned()));
|
||||
}
|
||||
let ip = unsafe { ifreq.data.addr.addr };
|
||||
Ok(ip)
|
||||
}
|
||||
_ => Err(IoError::last_os_error()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
fn set_device_addr(ifname: &str, addr: Ipv4Addr) -> io::Result<()> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let mut ifreq = IfReq::new(ifname);
|
||||
ifreq.data.addr.af = libc::AF_INET as libc::c_int;
|
||||
ifreq.data.addr.addr = addr;
|
||||
let res = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCSIFADDR.try_into().unwrap(), &mut ifreq) };
|
||||
match res {
|
||||
0 => Ok(()),
|
||||
_ => Err(IoError::last_os_error()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::useless_conversion)]
|
||||
fn get_device_netmask(ifname: &str) -> io::Result<Ipv4Addr> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let mut ifreq = IfReq::new(ifname);
|
||||
let res = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCGIFNETMASK.try_into().unwrap(), &mut ifreq) };
|
||||
match res {
|
||||
0 => {
|
||||
let af = unsafe { ifreq.data.addr.af };
|
||||
if af as libc::c_int != libc::AF_INET {
|
||||
return Err(io::Error::new(io::ErrorKind::AddrNotAvailable, "Invalid address family".to_owned()));
|
||||
}
|
||||
let ip = unsafe { ifreq.data.addr.addr };
|
||||
Ok(ip)
|
||||
}
|
||||
_ => Err(IoError::last_os_error()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
fn set_device_netmask(ifname: &str, addr: Ipv4Addr) -> io::Result<()> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let mut ifreq = IfReq::new(ifname);
|
||||
ifreq.data.addr.af = libc::AF_INET as libc::c_int;
|
||||
ifreq.data.addr.addr = addr;
|
||||
let res = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCSIFNETMASK.try_into().unwrap(), &mut ifreq) };
|
||||
match res {
|
||||
0 => Ok(()),
|
||||
_ => Err(IoError::last_os_error()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
fn set_device_enabled(ifname: &str, up: bool) -> io::Result<()> {
|
||||
let sock = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let mut ifreq = IfReq::new(ifname);
|
||||
if unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCGIFFLAGS.try_into().unwrap(), &mut ifreq) } != 0 {
|
||||
return Err(IoError::last_os_error());
|
||||
}
|
||||
if up {
|
||||
unsafe { ifreq.data.value |= libc::IFF_UP | libc::IFF_RUNNING }
|
||||
} else {
|
||||
unsafe { ifreq.data.value &= !libc::IFF_UP }
|
||||
}
|
||||
let res = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCSIFFLAGS.try_into().unwrap(), &mut ifreq) };
|
||||
match res {
|
||||
0 => Ok(()),
|
||||
_ => Err(IoError::last_os_error()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_default_device() -> io::Result<String> {
|
||||
let fd = BufReader::new(File::open("/proc/net/route")?);
|
||||
let mut best = None;
|
||||
for line in fd.lines() {
|
||||
let line = line?;
|
||||
let parts = line.split('\t').collect::<Vec<_>>();
|
||||
if parts[1] == "00000000" {
|
||||
best = Some(parts[0].to_string());
|
||||
break;
|
||||
}
|
||||
if parts[2] != "00000000" {
|
||||
best = Some(parts[0].to_string())
|
||||
}
|
||||
}
|
||||
if let Some(ifname) = best {
|
||||
Ok(ifname)
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::NotFound, "No default interface found".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rp_filter(device: &str) -> io::Result<u8> {
|
||||
let mut fd = File::open(format!("/proc/sys/net/ipv4/conf/{}/rp_filter", device))?;
|
||||
let mut contents = String::with_capacity(10);
|
||||
fd.read_to_string(&mut contents)?;
|
||||
u8::from_str(contents.trim()).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid rp_filter value"))
|
||||
}
|
||||
|
||||
fn set_rp_filter(device: &str, val: u8) -> io::Result<()> {
|
||||
let mut fd = File::create(format!("/proc/sys/net/ipv4/conf/{}/rp_filter", device))?;
|
||||
writeln!(fd, "{}", val)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use std::io;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// Crypto init error, this is recoverable
|
||||
#[error("Crypto initialization error: {0}")]
|
||||
CryptoInit(&'static str),
|
||||
|
||||
/// Crypto init error, this is fatal and the init needs to be aborted
|
||||
#[error("Fatal crypto initialization error: {0}")]
|
||||
CryptoInitFatal(&'static str),
|
||||
|
||||
/// Crypto error with this one message, no permanent error
|
||||
#[error("Crypto error: {0}")]
|
||||
Crypto(&'static str),
|
||||
|
||||
#[error("Invalid crypto state: {0}")]
|
||||
InvalidCryptoState(&'static str),
|
||||
|
||||
#[error("Invalid config: {0}")]
|
||||
InvalidConfig(&'static str),
|
||||
|
||||
#[error("Socker error: {0}")]
|
||||
Socket(&'static str),
|
||||
|
||||
#[error("Socker error: {0} ({1})")]
|
||||
SocketIo(&'static str, #[source] io::Error),
|
||||
|
||||
#[error("Device error: {0}")]
|
||||
Device(&'static str),
|
||||
|
||||
#[error("Device error: {0} ({1})")]
|
||||
DeviceIo(&'static str, #[source] io::Error),
|
||||
|
||||
#[error("File error: {0}")]
|
||||
FileIo(&'static str, #[source] io::Error),
|
||||
|
||||
#[error("Message error: {0}")]
|
||||
Message(&'static str),
|
||||
|
||||
#[error("Beacon error: {0} ({1})")]
|
||||
BeaconIo(&'static str, #[source] io::Error),
|
||||
|
||||
#[error("Parse error: {0}")]
|
||||
Parse(&'static str),
|
||||
|
||||
#[error("Name can not be resolved: {0}")]
|
||||
NameUnresolvable(String),
|
||||
}
|
227
src/ethernet.rs
227
src/ethernet.rs
|
@ -1,227 +0,0 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
hash::BuildHasherDefault,
|
||||
io::{self, Write},
|
||||
marker::PhantomData,
|
||||
net::SocketAddr
|
||||
};
|
||||
|
||||
use fnv::FnvHasher;
|
||||
|
||||
use super::{
|
||||
types::{Address, Error, Protocol, Table},
|
||||
util::{addr_nice, Duration, Time, TimeSource}
|
||||
};
|
||||
|
||||
/// An ethernet frame dissector
|
||||
///
|
||||
/// This dissector is able to extract the source and destination addresses of ethernet frames.
|
||||
///
|
||||
/// If the ethernet frame contains a VLAN tag, both addresses will be prefixed with that tag,
|
||||
/// resulting in 8-byte addresses. Additional nested tags will be ignored.
|
||||
pub struct Frame;
|
||||
|
||||
impl Protocol for Frame {
|
||||
/// Parses an ethernet frame and extracts the source and destination addresses
|
||||
///
|
||||
/// # Errors
|
||||
/// This method will fail when the given data is not a valid ethernet frame.
|
||||
fn parse(data: &[u8]) -> Result<(Address, Address), Error> {
|
||||
if data.len() < 14 {
|
||||
return Err(Error::Parse("Frame is too short"))
|
||||
}
|
||||
let mut pos = 0;
|
||||
let dst_data = &data[pos..pos + 6];
|
||||
pos += 6;
|
||||
let src_data = &data[pos..pos + 6];
|
||||
pos += 6;
|
||||
if data[pos] == 0x81 && data[pos + 1] == 0x00 {
|
||||
pos += 2;
|
||||
if data.len() < pos + 2 {
|
||||
return Err(Error::Parse("Vlan frame is too short"))
|
||||
}
|
||||
let mut src = [0; 16];
|
||||
let mut dst = [0; 16];
|
||||
src[0] = data[pos];
|
||||
src[1] = data[pos + 1];
|
||||
dst[0] = data[pos];
|
||||
dst[1] = data[pos + 1];
|
||||
src[2..8].copy_from_slice(src_data);
|
||||
dst[2..8].copy_from_slice(dst_data);
|
||||
Ok((Address { data: src, len: 8 }, Address { data: dst, len: 8 }))
|
||||
} else {
|
||||
let src = Address::read_from_fixed(src_data, 6)?;
|
||||
let dst = Address::read_from_fixed(dst_data, 6)?;
|
||||
Ok((src, dst))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SwitchTableValue {
|
||||
address: SocketAddr,
|
||||
timeout: Time
|
||||
}
|
||||
|
||||
type Hash = BuildHasherDefault<FnvHasher>;
|
||||
|
||||
|
||||
/// A table used to implement a learning switch
|
||||
///
|
||||
/// This table is a simple hash map between an address and the destination peer. It learns
|
||||
/// addresses as they are seen and forgets them after some time.
|
||||
pub struct SwitchTable<TS> {
|
||||
/// The table storing the actual mapping
|
||||
table: HashMap<Address, SwitchTableValue, Hash>,
|
||||
/// Timeout period for forgetting learnt addresses
|
||||
timeout: Duration,
|
||||
// Timeout period for not overwriting learnt addresses
|
||||
protection_period: Duration,
|
||||
_dummy_ts: PhantomData<TS>
|
||||
}
|
||||
|
||||
impl<TS: TimeSource> SwitchTable<TS> {
|
||||
/// Creates a new switch table
|
||||
pub fn new(timeout: Duration, protection_period: Duration) -> Self {
|
||||
Self { table: HashMap::default(), timeout, protection_period, _dummy_ts: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<TS: TimeSource> Table for SwitchTable<TS> {
|
||||
/// Forget addresses that have not been seen for the configured timeout
|
||||
fn housekeep(&mut self) {
|
||||
let now = TS::now();
|
||||
let mut del: Vec<Address> = Vec::new();
|
||||
for (key, val) in &self.table {
|
||||
if val.timeout < now {
|
||||
del.push(*key);
|
||||
}
|
||||
}
|
||||
for key in del {
|
||||
info!("Forgot address {}", key);
|
||||
self.table.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write out the table
|
||||
fn write_out<W: Write>(&self, out: &mut W) -> Result<(), io::Error> {
|
||||
let now = TS::now();
|
||||
writeln!(out, "switch_table:")?;
|
||||
for (addr, val) in &self.table {
|
||||
writeln!(
|
||||
out,
|
||||
" - \"{}\": {{ peer: \"{}\", ttl_secs: {} }}",
|
||||
addr,
|
||||
addr_nice(val.address),
|
||||
val.timeout - now
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Learns the given address, inserting it in the hash map
|
||||
#[inline]
|
||||
fn learn(&mut self, key: Address, _prefix_len: Option<u8>, addr: SocketAddr) {
|
||||
let deadline = TS::now() + Time::from(self.timeout);
|
||||
match self.table.entry(key) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(SwitchTableValue { address: addr, timeout: deadline });
|
||||
info!("Learned address {} => {}", key, addr_nice(addr));
|
||||
}
|
||||
Entry::Occupied(mut entry) => {
|
||||
let mut entry = entry.get_mut();
|
||||
if entry.timeout + Time::from(self.protection_period) > deadline {
|
||||
// Do not override recently learnt entries
|
||||
return
|
||||
}
|
||||
entry.timeout = deadline;
|
||||
entry.address = addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a peer for an address if it is inside the hash map
|
||||
#[inline]
|
||||
fn lookup(&mut self, key: &Address) -> Option<SocketAddr> {
|
||||
match self.table.get(key) {
|
||||
Some(value) => Some(value.address),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes an address from the map and returns whether something has been removed
|
||||
#[inline]
|
||||
fn remove(&mut self, key: &Address) -> bool {
|
||||
self.table.remove(key).is_some()
|
||||
}
|
||||
|
||||
/// Removed all addresses associated with a certain peer
|
||||
fn remove_all(&mut self, addr: &SocketAddr) {
|
||||
let mut remove = Vec::new();
|
||||
for (key, val) in &self.table {
|
||||
if &val.address == addr {
|
||||
remove.push(*key);
|
||||
}
|
||||
}
|
||||
for key in remove {
|
||||
self.table.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.table.len()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)] use super::util::MockTimeSource;
|
||||
#[cfg(test)] use std::net::ToSocketAddrs;
|
||||
#[cfg(test)] use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn decode_frame_without_vlan() {
|
||||
let data = [6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let (src, dst) = Frame::parse(&data).unwrap();
|
||||
assert_eq!(src, Address { data: [1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 6 });
|
||||
assert_eq!(dst, Address { data: [6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 6 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_frame_with_vlan() {
|
||||
let data = [6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 0x81, 0, 4, 210, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let (src, dst) = Frame::parse(&data).unwrap();
|
||||
assert_eq!(src, Address { data: [4, 210, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0], len: 8 });
|
||||
assert_eq!(dst, Address { data: [4, 210, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0], len: 8 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_frame() {
|
||||
assert!(Frame::parse(&[6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8]).is_ok());
|
||||
// truncated frame
|
||||
assert!(Frame::parse(&[]).is_err());
|
||||
// truncated vlan frame
|
||||
assert!(Frame::parse(&[6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 0x81, 0x00]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch() {
|
||||
MockTimeSource::set_time(1000);
|
||||
let mut table = SwitchTable::<MockTimeSource>::new(10, 1);
|
||||
let addr = Address::from_str("12:34:56:78:90:ab").unwrap();
|
||||
let peer = "1.2.3.4:5678".to_socket_addrs().unwrap().next().unwrap();
|
||||
let peer2 = "1.2.3.5:7890".to_socket_addrs().unwrap().next().unwrap();
|
||||
assert!(table.lookup(&addr).is_none());
|
||||
MockTimeSource::set_time(1000);
|
||||
table.learn(addr, None, peer);
|
||||
assert_eq!(table.lookup(&addr), Some(peer));
|
||||
MockTimeSource::set_time(1000);
|
||||
table.learn(addr, None, peer2);
|
||||
assert_eq!(table.lookup(&addr), Some(peer));
|
||||
MockTimeSource::set_time(1010);
|
||||
table.learn(addr, None, peer2);
|
||||
assert_eq!(table.lookup(&addr), Some(peer2));
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
use crate::{error::Error, util::run_cmd};
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
os::unix::fs::PermissionsExt,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
const MANPAGE: &[u8] = include_bytes!("../target/vpncloud.1.gz");
|
||||
const SERVICE_FILE: &[u8] = include_bytes!("../assets/vpncloud@.service");
|
||||
const TARGET_FILE: &[u8] = include_bytes!("../assets/vpncloud.target");
|
||||
const WS_PROXY_SERVICE_FILE: &[u8] = include_bytes!("../assets/vpncloud-wsproxy.service");
|
||||
const EXAMPLE_CONFIG: &[u8] = include_bytes!("../assets/example.net.disabled");
|
||||
|
||||
fn systemctl_daemon_reload() {
|
||||
let mut cmd = Command::new("systemctl");
|
||||
cmd.arg("daemon-reload");
|
||||
run_cmd(cmd);
|
||||
}
|
||||
|
||||
pub fn install() -> Result<(), Error> {
|
||||
env::current_exe()
|
||||
.and_then(|p| fs::copy(p, "/usr/bin/vpncloud"))
|
||||
.map_err(|e| Error::FileIo("Failed to copy binary", e))?;
|
||||
fs::set_permissions("/usr/bin/vpncloud", fs::Permissions::from_mode(0o755))
|
||||
.map_err(|e| Error::FileIo("Failed to set permissions for binary", e))?;
|
||||
fs::create_dir_all("/etc/vpncloud").map_err(|e| Error::FileIo("Failed to create config folder", e))?;
|
||||
fs::set_permissions("/etc/vpncloud", fs::Permissions::from_mode(0o700))
|
||||
.map_err(|e| Error::FileIo("Failed to set permissions for config folder", e))?;
|
||||
File::create("/etc/vpncloud/example.net.disabled")
|
||||
.and_then(|mut f| f.write_all(EXAMPLE_CONFIG))
|
||||
.map_err(|e| Error::FileIo("Failed to create example config", e))?;
|
||||
File::create("/usr/share/man/man1/vpncloud.1.gz")
|
||||
.and_then(|mut f| f.write_all(MANPAGE))
|
||||
.map_err(|e| Error::FileIo("Failed to create manpage", e))?;
|
||||
File::create("/lib/systemd/system/vpncloud@.service")
|
||||
.and_then(|mut f| f.write_all(SERVICE_FILE))
|
||||
.map_err(|e| Error::FileIo("Failed to create service file", e))?;
|
||||
File::create("/lib/systemd/system/vpncloud.target")
|
||||
.and_then(|mut f| f.write_all(TARGET_FILE))
|
||||
.map_err(|e| Error::FileIo("Failed to create service target file", e))?;
|
||||
File::create("/lib/systemd/system/vpncloud-wsproxy.service")
|
||||
.and_then(|mut f| f.write_all(WS_PROXY_SERVICE_FILE))
|
||||
.map_err(|e| Error::FileIo("Failed to create wsporxy service file", e))?;
|
||||
systemctl_daemon_reload();
|
||||
info!("Install successful");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninstall() -> Result<(), Error> {
|
||||
fs::remove_file("/etc/vpncloud/example.net.disabled").map_err(|e| Error::FileIo("Failed to remove binary", e))?;
|
||||
fs::remove_file("/usr/share/man/man1/vpncloud.1.gz").map_err(|e| Error::FileIo("Failed to remove manpage", e))?;
|
||||
fs::remove_file("/lib/systemd/system/vpncloud@.service")
|
||||
.map_err(|e| Error::FileIo("Failed to remove service file", e))?;
|
||||
fs::remove_file("/lib/systemd/system/vpncloud.target")
|
||||
.map_err(|e| Error::FileIo("Failed to remove service target file", e))?;
|
||||
fs::remove_file("/lib/systemd/system/vpncloud-wsproxy.service")
|
||||
.map_err(|e| Error::FileIo("Failed to remove wsproxy service file", e))?;
|
||||
fs::remove_file("/usr/bin/vpncloud").map_err(|e| Error::FileIo("Failed to remove binary", e))?;
|
||||
systemctl_daemon_reload();
|
||||
info!("Uninstall successful");
|
||||
Ok(())
|
||||
}
|
308
src/ip.rs
308
src/ip.rs
|
@ -1,308 +0,0 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::{
|
||||
collections::{hash_map, HashMap},
|
||||
hash::BuildHasherDefault,
|
||||
io::{self, Write},
|
||||
net::SocketAddr
|
||||
};
|
||||
|
||||
use fnv::FnvHasher;
|
||||
|
||||
use super::{
|
||||
types::{Address, Error, Protocol, Table},
|
||||
util::addr_nice
|
||||
};
|
||||
|
||||
|
||||
/// An IP packet dissector
|
||||
///
|
||||
/// This dissector is able to extract the source and destination ip addresses of ipv4 packets and
|
||||
/// ipv6 packets.
|
||||
#[allow(dead_code)]
|
||||
pub struct Packet;
|
||||
|
||||
impl Protocol for Packet {
|
||||
/// Parses an ip packet and extracts the source and destination addresses
|
||||
///
|
||||
/// # Errors
|
||||
/// This method will fail when the given data is not a valid ipv4 and ipv6 packet.
|
||||
fn parse(data: &[u8]) -> Result<(Address, Address), Error> {
|
||||
if data.is_empty() {
|
||||
return Err(Error::Parse("Empty header"))
|
||||
}
|
||||
let version = data[0] >> 4;
|
||||
match version {
|
||||
4 => {
|
||||
if data.len() < 20 {
|
||||
return Err(Error::Parse("Truncated IPv4 header"))
|
||||
}
|
||||
let src = Address::read_from_fixed(&data[12..], 4)?;
|
||||
let dst = Address::read_from_fixed(&data[16..], 4)?;
|
||||
Ok((src, dst))
|
||||
}
|
||||
6 => {
|
||||
if data.len() < 40 {
|
||||
return Err(Error::Parse("Truncated IPv6 header"))
|
||||
}
|
||||
let src = Address::read_from_fixed(&data[8..], 16)?;
|
||||
let dst = Address::read_from_fixed(&data[24..], 16)?;
|
||||
Ok((src, dst))
|
||||
}
|
||||
_ => Err(Error::Parse("Invalid version"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct RoutingEntry {
|
||||
address: SocketAddr,
|
||||
bytes: Address,
|
||||
prefix_len: u8
|
||||
}
|
||||
|
||||
type Hash = BuildHasherDefault<FnvHasher>;
|
||||
|
||||
/// A prefix-based routing table
|
||||
///
|
||||
/// This table contains a mapping of prefixes associated with peer addresses.
|
||||
/// To speed up lookup, prefixes are grouped into full bytes and map to a list of prefixes with
|
||||
/// more fine grained prefixes.
|
||||
#[derive(Default)]
|
||||
pub struct RoutingTable(HashMap<[u8; 16], Vec<RoutingEntry>, Hash>);
|
||||
|
||||
impl RoutingTable {
|
||||
/// Creates a new empty routing table
|
||||
pub fn new() -> Self {
|
||||
RoutingTable(HashMap::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for RoutingTable {
|
||||
/// Learns the given address, inserting it in the hash map
|
||||
fn learn(&mut self, addr: Address, prefix_len: Option<u8>, address: SocketAddr) {
|
||||
// If prefix length is not set, treat the whole addess as significant
|
||||
let prefix_len = match prefix_len {
|
||||
Some(val) => val,
|
||||
None => addr.len * 8
|
||||
};
|
||||
info!("New routing entry: {}/{} => {}", addr, prefix_len, addr_nice(address));
|
||||
// Round the prefix length down to the next multiple of 8 and extraxt a prefix of that
|
||||
// length.
|
||||
let group_len = prefix_len as usize / 8;
|
||||
assert!(group_len <= 16);
|
||||
let mut group_bytes = [0; 16];
|
||||
group_bytes[..group_len].copy_from_slice(&addr.data[..group_len]);
|
||||
// Create an entry
|
||||
let routing_entry = RoutingEntry { address, bytes: addr, prefix_len };
|
||||
// Add the entry to the routing table, creating a new list of the prefix group is empty.
|
||||
match self.0.entry(group_bytes) {
|
||||
hash_map::Entry::Occupied(mut entry) => entry.get_mut().push(routing_entry),
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(vec![routing_entry]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a peer for an address if it is inside the routing table
|
||||
#[allow(unknown_lints, clippy::needless_range_loop)]
|
||||
fn lookup(&mut self, addr: &Address) -> Option<SocketAddr> {
|
||||
let len = addr.len as usize;
|
||||
let mut found = None;
|
||||
let mut found_len: isize = -1;
|
||||
// Iterate over the prefix length from longest prefix group to shortest (empty) prefix
|
||||
// group
|
||||
let mut group_bytes = addr.data;
|
||||
for i in len..16 {
|
||||
group_bytes[i] = 0;
|
||||
}
|
||||
for i in (0..=len).rev() {
|
||||
if i < len {
|
||||
group_bytes[i] = 0;
|
||||
}
|
||||
if let Some(group) = self.0.get(&group_bytes) {
|
||||
// If the group is not empty, check every entry
|
||||
for entry in group {
|
||||
// Calculate the match length of the address and the prefix
|
||||
let mut match_len = 0;
|
||||
for j in 0..addr.len as usize {
|
||||
let b = addr.data[j] ^ entry.bytes.data[j];
|
||||
if b == 0 {
|
||||
match_len += 8;
|
||||
} else {
|
||||
match_len += b.leading_zeros();
|
||||
break
|
||||
}
|
||||
}
|
||||
// If the full prefix matches and the match is longer than the longest prefix
|
||||
// found so far, remember the peer
|
||||
if match_len as u8 >= entry.prefix_len && entry.prefix_len as isize > found_len {
|
||||
found = Some(entry.address);
|
||||
found_len = entry.prefix_len as isize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return the longest match found (if any).
|
||||
found
|
||||
}
|
||||
|
||||
/// This method does not do anything.
|
||||
fn housekeep(&mut self) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
/// Write out the table
|
||||
fn write_out<W: Write>(&self, out: &mut W) -> Result<(), io::Error> {
|
||||
writeln!(out, "routing_table:")?;
|
||||
for entries in self.0.values() {
|
||||
for entry in entries {
|
||||
writeln!(
|
||||
out,
|
||||
" - \"{}/{}\": {{ peer: \"{}\" }}",
|
||||
entry.bytes,
|
||||
entry.prefix_len,
|
||||
addr_nice(entry.address)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes an address from the map and returns whether something has been removed
|
||||
#[inline]
|
||||
fn remove(&mut self, _addr: &Address) -> bool {
|
||||
// Do nothing, removing single address from prefix-based routing tables does not make sense
|
||||
false
|
||||
}
|
||||
|
||||
/// Removed all addresses associated with a certain peer
|
||||
fn remove_all(&mut self, addr: &SocketAddr) {
|
||||
for entry in &mut self.0.values_mut() {
|
||||
entry.retain(|entr| &entr.address != addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)] use std::net::ToSocketAddrs;
|
||||
#[cfg(test)] use std::str::FromStr;
|
||||
|
||||
|
||||
#[test]
|
||||
fn decode_ipv4_packet() {
|
||||
let data = [0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1, 192, 168, 1, 2];
|
||||
let (src, dst) = Packet::parse(&data).unwrap();
|
||||
assert_eq!(src, Address { data: [192, 168, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 4 });
|
||||
assert_eq!(dst, Address { data: [192, 168, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 4 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ipv6_packet() {
|
||||
let data = [
|
||||
0x60, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5,
|
||||
4, 3, 2, 1
|
||||
];
|
||||
let (src, dst) = Packet::parse(&data).unwrap();
|
||||
assert_eq!(src, Address { data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6], len: 16 });
|
||||
assert_eq!(dst, Address { data: [0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1], len: 16 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_packet() {
|
||||
assert!(Packet::parse(&[0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1, 192, 168, 1, 2]).is_ok());
|
||||
assert!(Packet::parse(&[
|
||||
0x60, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5,
|
||||
4, 3, 2, 1
|
||||
])
|
||||
.is_ok());
|
||||
// no data
|
||||
assert!(Packet::parse(&[]).is_err());
|
||||
// wrong version
|
||||
assert!(Packet::parse(&[0x20]).is_err());
|
||||
// truncated ipv4
|
||||
assert!(Packet::parse(&[0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1, 192, 168, 1]).is_err());
|
||||
// truncated ipv6
|
||||
assert!(Packet::parse(&[
|
||||
0x60, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5,
|
||||
4, 3, 2
|
||||
])
|
||||
.is_err());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn routing_table_ipv4() {
|
||||
let mut table = RoutingTable::new();
|
||||
let peer1 = "1.2.3.4:1".to_socket_addrs().unwrap().next().unwrap();
|
||||
let peer2 = "1.2.3.4:2".to_socket_addrs().unwrap().next().unwrap();
|
||||
let peer3 = "1.2.3.4:3".to_socket_addrs().unwrap().next().unwrap();
|
||||
assert!(table.lookup(&Address::from_str("192.168.1.1").unwrap()).is_none());
|
||||
table.learn(Address::from_str("192.168.1.1").unwrap(), Some(32), peer1);
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.1").unwrap()), Some(peer1));
|
||||
table.learn(Address::from_str("192.168.1.2").unwrap(), None, peer2);
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.2").unwrap()), Some(peer2));
|
||||
table.learn(Address::from_str("192.168.1.0").unwrap(), Some(24), peer3);
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.2").unwrap()), Some(peer2));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.3").unwrap()), Some(peer3));
|
||||
table.learn(Address::from_str("192.168.0.0").unwrap(), Some(16), peer1);
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.2.1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.2").unwrap()), Some(peer2));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.3").unwrap()), Some(peer3));
|
||||
table.learn(Address::from_str("0.0.0.0").unwrap(), Some(0), peer2);
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.2.1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.2").unwrap()), Some(peer2));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.1.3").unwrap()), Some(peer3));
|
||||
assert_eq!(table.lookup(&Address::from_str("1.2.3.4").unwrap()), Some(peer2));
|
||||
table.learn(Address::from_str("192.168.2.0").unwrap(), Some(27), peer3);
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.2.31").unwrap()), Some(peer3));
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.2.32").unwrap()), Some(peer1));
|
||||
table.learn(Address::from_str("192.168.2.0").unwrap(), Some(28), peer3);
|
||||
assert_eq!(table.lookup(&Address::from_str("192.168.2.1").unwrap()), Some(peer3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routing_table_ipv6() {
|
||||
let mut table = RoutingTable::new();
|
||||
let peer1 = "::1:1".to_socket_addrs().unwrap().next().unwrap();
|
||||
let peer2 = "::1:2".to_socket_addrs().unwrap().next().unwrap();
|
||||
let peer3 = "::1:3".to_socket_addrs().unwrap().next().unwrap();
|
||||
assert!(table.lookup(&Address::from_str("::1").unwrap()).is_none());
|
||||
table.learn(Address::from_str("dead:beef:dead:beef:dead:beef:dead:1").unwrap(), Some(128), peer1);
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:1").unwrap()), Some(peer1));
|
||||
table.learn(Address::from_str("dead:beef:dead:beef:dead:beef:dead:2").unwrap(), None, peer2);
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:2").unwrap()), Some(peer2));
|
||||
table.learn(Address::from_str("dead:beef:dead:beef::").unwrap(), Some(64), peer3);
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:2").unwrap()), Some(peer2));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:3").unwrap()), Some(peer3));
|
||||
table.learn(Address::from_str("dead:beef:dead:be00::").unwrap(), Some(56), peer1);
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:1::").unwrap()), Some(peer3));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:be01::").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:2").unwrap()), Some(peer2));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:3").unwrap()), Some(peer3));
|
||||
table.learn(Address::from_str("::").unwrap(), Some(0), peer2);
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:1::").unwrap()), Some(peer3));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:be01::").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:1").unwrap()), Some(peer1));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:2").unwrap()), Some(peer2));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:3").unwrap()), Some(peer3));
|
||||
assert_eq!(table.lookup(&Address::from_str("::1").unwrap()), Some(peer2));
|
||||
table.learn(Address::from_str("dead:beef:dead:beef:dead:beef:dead:be00").unwrap(), Some(123), peer2);
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:be1f").unwrap()), Some(peer2));
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:be20").unwrap()), Some(peer3));
|
||||
table.learn(Address::from_str("dead:beef:dead:beef:dead:beef:dead:be00").unwrap(), Some(124), peer3);
|
||||
assert_eq!(table.lookup(&Address::from_str("dead:beef:dead:beef:dead:beef:dead:be01").unwrap()), Some(peer3));
|
||||
}
|
448
src/main.rs
448
src/main.rs
|
@ -1,14 +1,14 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
#![cfg_attr(feature = "bench", feature(test))]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
||||
#[cfg(test)] extern crate tempfile;
|
||||
#[cfg(feature = "bench")] extern crate test;
|
||||
#[cfg(test)]
|
||||
extern crate tempfile;
|
||||
|
||||
#[macro_use]
|
||||
pub mod util;
|
||||
|
@ -16,183 +16,56 @@ pub mod util;
|
|||
#[macro_use]
|
||||
mod tests;
|
||||
pub mod beacon;
|
||||
#[cfg(feature = "bench")] mod benches;
|
||||
pub mod cloud;
|
||||
pub mod config;
|
||||
pub mod crypto;
|
||||
pub mod device;
|
||||
pub mod ethernet;
|
||||
pub mod ip;
|
||||
pub mod error;
|
||||
#[cfg(feature = "installer")]
|
||||
pub mod installer;
|
||||
pub mod messages;
|
||||
pub mod net;
|
||||
pub mod oldconfig;
|
||||
pub mod payload;
|
||||
pub mod poll;
|
||||
pub mod port_forwarding;
|
||||
pub mod table;
|
||||
pub mod traffic;
|
||||
pub mod types;
|
||||
pub mod udpmessage;
|
||||
#[cfg(feature = "wizard")]
|
||||
pub mod wizard;
|
||||
#[cfg(feature = "websocket")]
|
||||
pub mod wsproxy;
|
||||
|
||||
use structopt::StructOpt;
|
||||
|
||||
use std::{
|
||||
fs::{self, File, Permissions},
|
||||
io::{self, Write},
|
||||
net::UdpSocket,
|
||||
net::{Ipv4Addr, UdpSocket},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::Path,
|
||||
process::Command,
|
||||
process,
|
||||
str::FromStr,
|
||||
sync::Mutex,
|
||||
thread
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cloud::GenericCloud,
|
||||
config::Config,
|
||||
crypto::{Crypto, CryptoMethod},
|
||||
config::{Args, Command, Config, DEFAULT_PORT},
|
||||
crypto::Crypto,
|
||||
device::{Device, TunTapDevice, Type},
|
||||
ethernet::SwitchTable,
|
||||
ip::RoutingTable,
|
||||
port_forwarding::PortForwarding,
|
||||
types::{Error, HeaderMagic, Mode, Protocol, Range},
|
||||
util::{Duration, SystemTimeSource}
|
||||
net::Socket,
|
||||
oldconfig::OldConfigFile,
|
||||
payload::Protocol,
|
||||
util::SystemTimeSource,
|
||||
};
|
||||
|
||||
|
||||
const VERSION: u8 = 1;
|
||||
const MAGIC: HeaderMagic = *b"vpn\x01";
|
||||
|
||||
|
||||
#[derive(StructOpt, Debug, Default)]
|
||||
pub struct Args {
|
||||
/// Read configuration options from the specified file.
|
||||
#[structopt(long)]
|
||||
config: Option<String>,
|
||||
|
||||
/// Set the type of network ("tap" or "tun")
|
||||
#[structopt(name = "type", short, long)]
|
||||
type_: Option<Type>,
|
||||
|
||||
/// Set the path of the base device
|
||||
#[structopt(long)]
|
||||
device_path: Option<String>,
|
||||
|
||||
/// The mode of the VPN ("normal", "router", "switch", or "hub")
|
||||
#[structopt(short, long)]
|
||||
mode: Option<Mode>,
|
||||
|
||||
/// The shared key to encrypt all traffic
|
||||
#[structopt(short, long, aliases=&["shared-key", "secret-key", "secret"])]
|
||||
key: Option<String>,
|
||||
|
||||
/// The encryption method to use ("aes256", or "chacha20")
|
||||
#[structopt(long)]
|
||||
crypto: Option<CryptoMethod>,
|
||||
|
||||
/// The local subnets to use
|
||||
#[structopt(short, long)]
|
||||
subnets: Vec<String>,
|
||||
|
||||
/// Name of the virtual device
|
||||
#[structopt(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// The port number (or ip:port) on which to listen for data
|
||||
#[structopt(short, long)]
|
||||
listen: Option<String>,
|
||||
|
||||
/// Optional token that identifies the network. (DEPRECATED)
|
||||
#[structopt(long)]
|
||||
network_id: Option<String>,
|
||||
|
||||
/// Override the 4-byte magic header of each packet
|
||||
#[structopt(long)]
|
||||
magic: Option<String>,
|
||||
|
||||
/// Address of a peer to connect to
|
||||
#[structopt(short, long)]
|
||||
connect: Vec<String>,
|
||||
|
||||
/// Peer timeout in seconds
|
||||
#[structopt(long)]
|
||||
peer_timeout: Option<Duration>,
|
||||
/// Periodically send message to keep connections alive
|
||||
#[structopt(long)]
|
||||
keepalive: Option<Duration>,
|
||||
|
||||
/// Switch table entry timeout in seconds
|
||||
#[structopt(long)]
|
||||
dst_timeout: Option<Duration>,
|
||||
|
||||
/// The file path or |command to store the beacon
|
||||
#[structopt(long)]
|
||||
beacon_store: Option<String>,
|
||||
|
||||
/// The file path or |command to load the beacon
|
||||
#[structopt(long)]
|
||||
beacon_load: Option<String>,
|
||||
|
||||
/// Beacon store/load interval in seconds
|
||||
#[structopt(long)]
|
||||
beacon_interval: Option<Duration>,
|
||||
|
||||
/// Print debug information
|
||||
#[structopt(short, long, conflicts_with = "quiet")]
|
||||
verbose: bool,
|
||||
|
||||
/// Only print errors and warnings
|
||||
#[structopt(short, long)]
|
||||
quiet: bool,
|
||||
|
||||
/// A command to setup the network interface
|
||||
#[structopt(long)]
|
||||
ifup: Option<String>,
|
||||
|
||||
/// A command to bring down the network interface
|
||||
#[structopt(long)]
|
||||
ifdown: Option<String>,
|
||||
|
||||
/// Print the version and exit
|
||||
#[structopt(long)]
|
||||
version: bool,
|
||||
|
||||
/// Disable automatic port forwarding
|
||||
#[structopt(long)]
|
||||
no_port_forwarding: bool,
|
||||
|
||||
/// Run the process in the background
|
||||
#[structopt(long)]
|
||||
daemon: bool,
|
||||
|
||||
/// Store the process id in this file when daemonizing
|
||||
#[structopt(long)]
|
||||
pid_file: Option<String>,
|
||||
|
||||
/// Print statistics to this file
|
||||
#[structopt(long)]
|
||||
stats_file: Option<String>,
|
||||
|
||||
/// Send statistics to this statsd server
|
||||
#[structopt(long)]
|
||||
statsd_server: Option<String>,
|
||||
|
||||
/// Use the given prefix for statsd records
|
||||
#[structopt(long)]
|
||||
statsd_prefix: Option<String>,
|
||||
|
||||
/// Run as other user
|
||||
#[structopt(long)]
|
||||
user: Option<String>,
|
||||
|
||||
/// Run as other group
|
||||
#[structopt(long)]
|
||||
group: Option<String>,
|
||||
|
||||
/// Print logs also to this file
|
||||
#[structopt(long)]
|
||||
log_file: Option<String>
|
||||
}
|
||||
#[cfg(feature = "websocket")]
|
||||
use crate::wsproxy::ProxyConnection;
|
||||
|
||||
struct DualLogger {
|
||||
file: Mutex<Option<File>>
|
||||
file: Option<Mutex<File>>,
|
||||
}
|
||||
|
||||
impl DualLogger {
|
||||
|
@ -203,9 +76,9 @@ impl DualLogger {
|
|||
fs::remove_file(path)?
|
||||
}
|
||||
let file = File::create(path)?;
|
||||
Ok(DualLogger { file: Mutex::new(Some(file)) })
|
||||
Ok(DualLogger { file: Some(Mutex::new(file)) })
|
||||
} else {
|
||||
Ok(DualLogger { file: Mutex::new(None) })
|
||||
Ok(DualLogger { file: None })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -220,9 +93,9 @@ impl log::Log for DualLogger {
|
|||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
println!("{} - {}", record.level(), record.args());
|
||||
let mut file = self.file.lock().expect("Lock poisoned");
|
||||
if let Some(ref mut file) = *file {
|
||||
let time = time::OffsetDateTime::now_local().format("%F %T");
|
||||
if let Some(ref file) = self.file {
|
||||
let mut file = file.lock().expect("Lock poisoned");
|
||||
let time = chrono::Local::now().format("%F %H:%M:%S");
|
||||
writeln!(file, "{} - {} - {}", time, record.level(), record.args())
|
||||
.expect("Failed to write to logfile");
|
||||
}
|
||||
|
@ -231,16 +104,16 @@ impl log::Log for DualLogger {
|
|||
|
||||
#[inline]
|
||||
fn flush(&self) {
|
||||
let mut file = self.file.lock().expect("Lock poisoned");
|
||||
if let Some(ref mut file) = *file {
|
||||
if let Some(ref file) = self.file {
|
||||
let mut file = file.lock().expect("Lock poisoned");
|
||||
try_fail!(file.flush(), "Logging error: {}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_script(script: &str, ifname: &str) {
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.arg("-c").arg(&script).env("IFNAME", ifname);
|
||||
let mut cmd = process::Command::new("sh");
|
||||
cmd.arg("-c").arg(script).env("IFNAME", ifname);
|
||||
debug!("Running script: {:?}", cmd);
|
||||
match cmd.status() {
|
||||
Ok(status) => {
|
||||
|
@ -248,95 +121,25 @@ fn run_script(script: &str, ifname: &str) {
|
|||
error!("Script returned with error: {:?}", status.code())
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to execute script {:?}: {}", script, e)
|
||||
Err(e) => error!("Failed to execute script {:?}: {}", script, e),
|
||||
}
|
||||
}
|
||||
|
||||
enum AnyTable {
|
||||
Switch(SwitchTable<SystemTimeSource>),
|
||||
Routing(RoutingTable)
|
||||
fn parse_ip_netmask(addr: &str) -> Result<(Ipv4Addr, Ipv4Addr), String> {
|
||||
let (ip_str, len_str) = match addr.find('/') {
|
||||
Some(pos) => (&addr[..pos], &addr[pos + 1..]),
|
||||
None => (addr, "24"),
|
||||
};
|
||||
let prefix_len = u8::from_str(len_str).map_err(|_| format!("Invalid prefix length: {}", len_str))?;
|
||||
if prefix_len > 32 {
|
||||
return Err(format!("Invalid prefix length: {}", prefix_len));
|
||||
}
|
||||
let ip = Ipv4Addr::from_str(ip_str).map_err(|_| format!("Invalid ip address: {}", ip_str))?;
|
||||
let netmask = Ipv4Addr::from(u32::max_value().checked_shl(32 - prefix_len as u32).unwrap());
|
||||
Ok((ip, netmask))
|
||||
}
|
||||
|
||||
enum AnyCloud<P: Protocol> {
|
||||
Switch(GenericCloud<TunTapDevice, P, SwitchTable<SystemTimeSource>, UdpSocket, SystemTimeSource>),
|
||||
Routing(GenericCloud<TunTapDevice, P, RoutingTable, UdpSocket, SystemTimeSource>)
|
||||
}
|
||||
|
||||
impl<P: Protocol> AnyCloud<P> {
|
||||
#[allow(unknown_lints, clippy::too_many_arguments)]
|
||||
fn new(
|
||||
config: &Config, device: TunTapDevice, table: AnyTable, learning: bool, broadcast: bool, addresses: Vec<Range>,
|
||||
crypto: Crypto, port_forwarding: Option<PortForwarding>, stats_file: Option<File>
|
||||
) -> Self
|
||||
{
|
||||
match table {
|
||||
AnyTable::Switch(t) => {
|
||||
AnyCloud::Switch(GenericCloud::<
|
||||
TunTapDevice,
|
||||
P,
|
||||
SwitchTable<SystemTimeSource>,
|
||||
UdpSocket,
|
||||
SystemTimeSource
|
||||
>::new(
|
||||
config,
|
||||
device,
|
||||
t,
|
||||
learning,
|
||||
broadcast,
|
||||
addresses,
|
||||
crypto,
|
||||
port_forwarding,
|
||||
stats_file
|
||||
))
|
||||
}
|
||||
AnyTable::Routing(t) => {
|
||||
AnyCloud::Routing(GenericCloud::<TunTapDevice, P, RoutingTable, UdpSocket, SystemTimeSource>::new(
|
||||
config,
|
||||
device,
|
||||
t,
|
||||
learning,
|
||||
broadcast,
|
||||
addresses,
|
||||
crypto,
|
||||
port_forwarding,
|
||||
stats_file
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ifname(&self) -> &str {
|
||||
match *self {
|
||||
AnyCloud::Switch(ref c) => c.ifname(),
|
||||
AnyCloud::Routing(ref c) => c.ifname()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&mut self) {
|
||||
match *self {
|
||||
AnyCloud::Switch(ref mut c) => c.run(),
|
||||
AnyCloud::Routing(ref mut c) => c.run()
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(&mut self, a: &str) -> Result<(), Error> {
|
||||
match *self {
|
||||
AnyCloud::Switch(ref mut c) => c.connect(a),
|
||||
AnyCloud::Routing(ref mut c) => c.connect(a)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_reconnect_peer(&mut self, a: String) {
|
||||
match *self {
|
||||
AnyCloud::Switch(ref mut c) => c.add_reconnect_peer(a),
|
||||
AnyCloud::Routing(ref mut c) => c.add_reconnect_peer(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn run<P: Protocol>(config: Config) {
|
||||
fn setup_device(config: &Config) -> TunTapDevice {
|
||||
let device = try_fail!(
|
||||
TunTapDevice::new(&config.device_name, config.device_type, config.device_path.as_ref().map(|s| s as &str)),
|
||||
"Failed to open virtual {} interface {}: {}",
|
||||
|
@ -344,28 +147,34 @@ fn run<P: Protocol>(config: Config) {
|
|||
config.device_name
|
||||
);
|
||||
info!("Opened device {}", device.ifname());
|
||||
let mut ranges = Vec::with_capacity(config.subnets.len());
|
||||
for s in &config.subnets {
|
||||
ranges.push(try_fail!(Range::from_str(s), "Invalid subnet format: {} ({})", s));
|
||||
config.call_hook("device_setup", vec![("IFNAME", device.ifname())], true);
|
||||
if let Err(err) = device.set_mtu(None) {
|
||||
error!("Error setting optimal MTU on {}: {}", device.ifname(), err);
|
||||
}
|
||||
let dst_timeout = config.dst_timeout;
|
||||
let (learning, broadcasting, table) = match config.mode {
|
||||
Mode::Normal => {
|
||||
match config.device_type {
|
||||
Type::Tap => (true, true, AnyTable::Switch(SwitchTable::new(dst_timeout, 10))),
|
||||
Type::Tun => (false, false, AnyTable::Routing(RoutingTable::new())),
|
||||
Type::Dummy => (false, false, AnyTable::Switch(SwitchTable::new(dst_timeout, 10)))
|
||||
}
|
||||
if let Some(ip) = &config.ip {
|
||||
let (ip, netmask) = try_fail!(parse_ip_netmask(ip), "Invalid ip address given: {}");
|
||||
info!("Configuring device with ip {}, netmask {}", ip, netmask);
|
||||
try_fail!(device.configure(ip, netmask), "Failed to configure device: {}");
|
||||
}
|
||||
if let Some(script) = &config.ifup {
|
||||
run_script(script, device.ifname());
|
||||
}
|
||||
if config.fix_rp_filter {
|
||||
try_fail!(device.fix_rp_filter(), "Failed to change rp_filter settings: {}");
|
||||
}
|
||||
if let Ok(val) = device.get_rp_filter() {
|
||||
if val != 1 {
|
||||
warn!("Your networking configuration might be affected by a vulnerability (https://vpncloud.ddswd.de/docs/security/cve-2019-14899/), please change your rp_filter setting to 1 (currently {}).", val);
|
||||
}
|
||||
Mode::Router => (false, false, AnyTable::Routing(RoutingTable::new())),
|
||||
Mode::Switch => (true, true, AnyTable::Switch(SwitchTable::new(dst_timeout, 10))),
|
||||
Mode::Hub => (false, true, AnyTable::Switch(SwitchTable::new(dst_timeout, 10)))
|
||||
};
|
||||
let crypto = match config.shared_key {
|
||||
Some(ref key) => Crypto::from_shared_key(config.crypto, key),
|
||||
None => Crypto::None
|
||||
};
|
||||
let port_forwarding = if config.port_forwarding { PortForwarding::new(config.listen.port()) } else { None };
|
||||
}
|
||||
config.call_hook("device_configured", vec![("IFNAME", device.ifname())], true);
|
||||
device
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn run<P: Protocol, S: Socket>(config: Config, socket: S) {
|
||||
let device = setup_device(&config);
|
||||
let port_forwarding = if config.port_forwarding { socket.create_port_forwarding() } else { None };
|
||||
let stats_file = match config.stats_file {
|
||||
None => None,
|
||||
Some(ref name) => {
|
||||
|
@ -382,11 +191,12 @@ fn run<P: Protocol>(config: Config) {
|
|||
}
|
||||
};
|
||||
let mut cloud =
|
||||
AnyCloud::<P>::new(&config, device, table, learning, broadcasting, ranges, crypto, port_forwarding, stats_file);
|
||||
if let Some(script) = config.ifup {
|
||||
run_script(&script, cloud.ifname());
|
||||
}
|
||||
for addr in config.peers {
|
||||
GenericCloud::<TunTapDevice, P, S, SystemTimeSource>::new(&config, socket, device, port_forwarding, stats_file);
|
||||
for mut addr in config.peers {
|
||||
if addr.find(':').unwrap_or(0) <= addr.find(']').unwrap_or(0) {
|
||||
// : not present or only in IPv6 address
|
||||
addr = format!("{}:{}", addr, DEFAULT_PORT)
|
||||
}
|
||||
try_fail!(cloud.connect(&addr as &str), "Failed to send message to {}: {}", &addr);
|
||||
cloud.add_reconnect_peer(addr);
|
||||
}
|
||||
|
@ -401,8 +211,6 @@ fn run<P: Protocol>(config: Config) {
|
|||
}
|
||||
if let Some(pid_file) = config.pid_file {
|
||||
daemonize = daemonize.pid_file(pid_file).chown_pid_file(true);
|
||||
// Give child process some time to write PID file
|
||||
daemonize = daemonize.exit_action(|| thread::sleep(std::time::Duration::from_millis(10)));
|
||||
}
|
||||
try_fail!(daemonize.start(), "Failed to daemonize: {}");
|
||||
} else if config.user.is_some() || config.group.is_some() {
|
||||
|
@ -425,8 +233,8 @@ fn run<P: Protocol>(config: Config) {
|
|||
fn main() {
|
||||
let args: Args = Args::from_args();
|
||||
if args.version {
|
||||
println!("VpnCloud v{}, protocol version {}", env!("CARGO_PKG_VERSION"), VERSION);
|
||||
return
|
||||
println!("VpnCloud v{}", env!("CARGO_PKG_VERSION"));
|
||||
return;
|
||||
}
|
||||
let logger = try_fail!(DualLogger::new(args.log_file.as_ref()), "Failed to open logfile: {}");
|
||||
log::set_boxed_logger(Box::new(logger)).unwrap();
|
||||
|
@ -438,18 +246,94 @@ fn main() {
|
|||
} else {
|
||||
log::LevelFilter::Info
|
||||
});
|
||||
if let Some(cmd) = args.cmd {
|
||||
match cmd {
|
||||
Command::GenKey { password } => {
|
||||
let (privkey, pubkey) = Crypto::generate_keypair(password.as_deref());
|
||||
println!("Private key: {}\nPublic key: {}\n", privkey, pubkey);
|
||||
println!(
|
||||
"Attention: Keep the private key secret and use only the public key on other nodes to establish trust."
|
||||
);
|
||||
}
|
||||
Command::MigrateConfig { config_file } => {
|
||||
info!("Trying to convert from old config format");
|
||||
let f = try_fail!(File::open(&config_file), "Failed to open config file: {:?}");
|
||||
let config_file_old: OldConfigFile =
|
||||
try_fail!(serde_yaml::from_reader(f), "Config file not valid for version 1: {:?}");
|
||||
let new_config = config_file_old.convert();
|
||||
info!("Successfully converted from old format");
|
||||
info!("Renaming original file to {}.orig", config_file);
|
||||
try_fail!(
|
||||
fs::rename(&config_file, format!("{}.orig", config_file)),
|
||||
"Failed to rename original file: {:?}"
|
||||
);
|
||||
info!("Writing new config back into {}", config_file);
|
||||
let f = try_fail!(File::create(&config_file), "Failed to open config file: {:?}");
|
||||
try_fail!(
|
||||
fs::set_permissions(&config_file, fs::Permissions::from_mode(0o600)),
|
||||
"Failed to set permissions on file: {:?}"
|
||||
);
|
||||
try_fail!(serde_yaml::to_writer(f, &new_config), "Failed to write converted config: {:?}");
|
||||
}
|
||||
Command::Completion { shell } => {
|
||||
Args::clap().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut io::stdout());
|
||||
}
|
||||
#[cfg(feature = "websocket")]
|
||||
Command::WsProxy { listen } => {
|
||||
try_fail!(wsproxy::run_proxy(&listen), "Failed to run websocket proxy: {:?}");
|
||||
}
|
||||
#[cfg(feature = "wizard")]
|
||||
Command::Config { name } => {
|
||||
try_fail!(wizard::configure(name), "Wizard failed: {}");
|
||||
}
|
||||
#[cfg(feature = "installer")]
|
||||
Command::Install { uninstall } => {
|
||||
if uninstall {
|
||||
try_fail!(installer::uninstall(), "Uninstall failed: {}");
|
||||
} else {
|
||||
try_fail!(installer::install(), "Install failed: {}");
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut config = Config::default();
|
||||
if let Some(ref file) = args.config {
|
||||
info!("Reading config file '{}'", file);
|
||||
let f = try_fail!(File::open(file), "Failed to open config file: {:?}");
|
||||
let config_file = try_fail!(serde_yaml::from_reader(f), "Failed to load config file: {:?}");
|
||||
let config_file = match serde_yaml::from_reader(f) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
error!("Failed to read config file: {}", err);
|
||||
info!("Trying to convert from old config format");
|
||||
let f = try_fail!(File::open(file), "Failed to open config file: {:?}");
|
||||
let config_file_old: OldConfigFile =
|
||||
try_fail!(serde_yaml::from_reader(f), "Config file is neither version 2 nor version 1: {:?}");
|
||||
let new_config = config_file_old.convert();
|
||||
info!("Successfully converted from old format, please migrate your config using migrate-config");
|
||||
new_config
|
||||
}
|
||||
};
|
||||
config.merge_file(config_file)
|
||||
}
|
||||
config.merge_args(args);
|
||||
debug!("Config: {:?}", config);
|
||||
if config.crypto.password.is_none() && config.crypto.private_key.is_none() {
|
||||
error!("Either password or private key must be set in config or given as parameter");
|
||||
return;
|
||||
}
|
||||
#[cfg(feature = "websocket")]
|
||||
if config.listen.starts_with("ws://") {
|
||||
let socket = try_fail!(ProxyConnection::listen(&config.listen), "Failed to open socket {}: {}", config.listen);
|
||||
match config.device_type {
|
||||
Type::Tap => run::<payload::Frame, _>(config, socket),
|
||||
Type::Tun => run::<payload::Packet, _>(config, socket),
|
||||
}
|
||||
return;
|
||||
}
|
||||
let socket = try_fail!(UdpSocket::listen(&config.listen), "Failed to open socket {}: {}", config.listen);
|
||||
match config.device_type {
|
||||
Type::Tap => run::<ethernet::Frame>(config),
|
||||
Type::Tun => run::<ip::Packet>(config),
|
||||
Type::Dummy => run::<ethernet::Frame>(config)
|
||||
Type::Tap => run::<payload::Frame, _>(config, socket),
|
||||
Type::Tun => run::<payload::Packet, _>(config, socket),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use crate::{
|
||||
crypto::Payload,
|
||||
error::Error,
|
||||
types::{NodeId, Range, RangeList, NODE_ID_BYTES},
|
||||
util::MsgBuffer,
|
||||
};
|
||||
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Take, Write},
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||
};
|
||||
|
||||
pub const MESSAGE_TYPE_DATA: u8 = 0;
|
||||
pub const MESSAGE_TYPE_NODE_INFO: u8 = 1;
|
||||
pub const MESSAGE_TYPE_KEEPALIVE: u8 = 2;
|
||||
pub const MESSAGE_TYPE_CLOSE: u8 = 0xff;
|
||||
|
||||
pub type AddrList = SmallVec<[SocketAddr; 4]>;
|
||||
pub type PeerList = SmallVec<[PeerInfo; 16]>;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct PeerInfo {
|
||||
pub node_id: Option<NodeId>,
|
||||
pub addrs: AddrList,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct NodeInfo {
|
||||
pub node_id: NodeId,
|
||||
pub peers: PeerList,
|
||||
pub claims: RangeList,
|
||||
pub peer_timeout: Option<u16>,
|
||||
pub addrs: AddrList,
|
||||
}
|
||||
|
||||
impl NodeInfo {
|
||||
const PART_CLAIMS: u8 = 2;
|
||||
const PART_END: u8 = 0;
|
||||
const PART_NODEID: u8 = 4;
|
||||
const PART_PEERS: u8 = 1;
|
||||
const PART_PEER_TIMEOUT: u8 = 3;
|
||||
const PART_ADDRS: u8 = 5;
|
||||
|
||||
fn read_addr_list<R: Read>(r: &mut Take<R>) -> Result<AddrList, io::Error> {
|
||||
let flags = r.read_u8()?;
|
||||
Self::read_addr_list_inner(r, flags)
|
||||
}
|
||||
|
||||
fn read_addr_list_inner<R: Read>(r: &mut Take<R>, flags: u8) -> Result<AddrList, io::Error> {
|
||||
let num_ipv4_addrs = (flags & 0x07) as usize;
|
||||
let num_ipv6_addrs = (flags & 0x38) as usize / 8;
|
||||
let mut addrs = SmallVec::with_capacity(num_ipv4_addrs + num_ipv6_addrs);
|
||||
for _ in 0..num_ipv6_addrs {
|
||||
let mut ip = [0u8; 16];
|
||||
r.read_exact(&mut ip)?;
|
||||
let port = r.read_u16::<NetworkEndian>()?;
|
||||
let addr = SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::from(ip), port, 0, 0));
|
||||
addrs.push(addr);
|
||||
}
|
||||
for _ in 0..num_ipv4_addrs {
|
||||
let mut ip = [0u8; 4];
|
||||
r.read_exact(&mut ip)?;
|
||||
let port = r.read_u16::<NetworkEndian>()?;
|
||||
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), port));
|
||||
addrs.push(addr);
|
||||
}
|
||||
Ok(addrs)
|
||||
}
|
||||
|
||||
fn decode_peer_list_part<R: Read>(r: &mut Take<R>) -> Result<PeerList, io::Error> {
|
||||
let mut peers = smallvec![];
|
||||
while r.limit() > 0 {
|
||||
let flags = r.read_u8()?;
|
||||
let has_node_id = (flags & 0x80) != 0;
|
||||
let mut node_id = None;
|
||||
if has_node_id {
|
||||
let mut id = [0; NODE_ID_BYTES];
|
||||
r.read_exact(&mut id)?;
|
||||
node_id = Some(id)
|
||||
}
|
||||
let addrs = Self::read_addr_list_inner(r, flags)?;
|
||||
peers.push(PeerInfo { addrs, node_id })
|
||||
}
|
||||
Ok(peers)
|
||||
}
|
||||
|
||||
fn decode_claims_part<R: Read>(mut r: &mut Take<R>) -> Result<RangeList, Error> {
|
||||
let mut claims = smallvec![];
|
||||
while r.limit() > 0 {
|
||||
claims.push(Range::read_from(&mut r)?);
|
||||
}
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
fn decode_internal<R: Read>(mut r: R) -> Result<Self, Error> {
|
||||
let mut peers = smallvec![];
|
||||
let mut claims = smallvec![];
|
||||
let mut peer_timeout = None;
|
||||
let mut node_id = None;
|
||||
let mut addrs = smallvec![];
|
||||
loop {
|
||||
let part = r.read_u8().map_err(|_| Error::Message("Truncated message"))?;
|
||||
if part == Self::PART_END {
|
||||
break;
|
||||
}
|
||||
let part_len = r.read_u16::<NetworkEndian>().map_err(|_| Error::Message("Truncated message"))? as usize;
|
||||
let mut rp = r.take(part_len as u64);
|
||||
match part {
|
||||
Self::PART_PEERS => {
|
||||
peers = Self::decode_peer_list_part(&mut rp).map_err(|_| Error::Message("Truncated message"))?
|
||||
}
|
||||
Self::PART_CLAIMS => claims = Self::decode_claims_part(&mut rp)?,
|
||||
Self::PART_PEER_TIMEOUT => {
|
||||
peer_timeout =
|
||||
Some(rp.read_u16::<NetworkEndian>().map_err(|_| Error::Message("Truncated message"))?)
|
||||
}
|
||||
Self::PART_NODEID => {
|
||||
let mut data = [0; NODE_ID_BYTES];
|
||||
rp.read_exact(&mut data).map_err(|_| Error::Message("Truncated message"))?;
|
||||
node_id = Some(data);
|
||||
}
|
||||
Self::PART_ADDRS => {
|
||||
addrs = Self::read_addr_list(&mut rp).map_err(|_| Error::Message("Truncated message"))?;
|
||||
}
|
||||
_ => {
|
||||
let mut data = vec![0; part_len];
|
||||
rp.read_exact(&mut data).map_err(|_| Error::Message("Truncated message"))?;
|
||||
}
|
||||
}
|
||||
r = rp.into_inner();
|
||||
}
|
||||
let node_id = match node_id {
|
||||
Some(node_id) => node_id,
|
||||
None => return Err(Error::Message("Payload without node_id")),
|
||||
};
|
||||
Ok(Self { node_id, peers, claims, peer_timeout, addrs })
|
||||
}
|
||||
|
||||
pub fn decode<R: Read>(r: R) -> Result<Self, Error> {
|
||||
Self::decode_internal(r).map_err(|_| Error::Message("Input data too short"))
|
||||
}
|
||||
|
||||
fn encode_peer_list_part<W: Write>(&self, mut out: W) -> Result<(), io::Error> {
|
||||
for p in &self.peers {
|
||||
let mut addr_ipv4: SmallVec<[SocketAddrV4; 16]> = smallvec![];
|
||||
let mut addr_ipv6: SmallVec<[SocketAddrV6; 16]> = smallvec![];
|
||||
for a in &p.addrs {
|
||||
match a {
|
||||
SocketAddr::V4(addr) => addr_ipv4.push(*addr),
|
||||
SocketAddr::V6(addr) => addr_ipv6.push(*addr),
|
||||
}
|
||||
}
|
||||
while addr_ipv4.len() >= 8 {
|
||||
addr_ipv4.pop();
|
||||
}
|
||||
while addr_ipv6.len() >= 8 {
|
||||
addr_ipv6.pop();
|
||||
}
|
||||
let mut flags = addr_ipv6.len() as u8 * 8 + addr_ipv4.len() as u8;
|
||||
if p.node_id.is_some() {
|
||||
flags += 0x80;
|
||||
}
|
||||
out.write_u8(flags)?;
|
||||
if let Some(node_id) = &p.node_id {
|
||||
out.write_all(node_id)?;
|
||||
}
|
||||
for a in addr_ipv6 {
|
||||
out.write_all(&a.ip().octets())?;
|
||||
out.write_u16::<NetworkEndian>(a.port())?;
|
||||
}
|
||||
for a in addr_ipv4 {
|
||||
out.write_all(&a.ip().octets())?;
|
||||
out.write_u16::<NetworkEndian>(a.port())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_addrs_part<W: Write>(&self, mut out: W) -> Result<(), io::Error> {
|
||||
let mut addr_ipv4: SmallVec<[SocketAddrV4; 16]> = smallvec![];
|
||||
let mut addr_ipv6: SmallVec<[SocketAddrV6; 16]> = smallvec![];
|
||||
for a in &self.addrs {
|
||||
match a {
|
||||
SocketAddr::V4(addr) => addr_ipv4.push(*addr),
|
||||
SocketAddr::V6(addr) => addr_ipv6.push(*addr),
|
||||
}
|
||||
}
|
||||
while addr_ipv4.len() >= 8 {
|
||||
addr_ipv4.pop();
|
||||
}
|
||||
while addr_ipv6.len() >= 8 {
|
||||
addr_ipv6.pop();
|
||||
}
|
||||
let flags = addr_ipv6.len() as u8 * 8 + addr_ipv4.len() as u8;
|
||||
out.write_u8(flags)?;
|
||||
for a in addr_ipv6 {
|
||||
out.write_all(&a.ip().octets())?;
|
||||
out.write_u16::<NetworkEndian>(a.port())?;
|
||||
}
|
||||
for a in addr_ipv4 {
|
||||
out.write_all(&a.ip().octets())?;
|
||||
out.write_u16::<NetworkEndian>(a.port())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_part<F: FnOnce(&mut Cursor<&mut [u8]>) -> Result<(), io::Error>>(
|
||||
cursor: &mut Cursor<&mut [u8]>, part: u8, f: F,
|
||||
) -> Result<(), io::Error> {
|
||||
cursor.write_u8(part)?;
|
||||
cursor.write_u16::<NetworkEndian>(0)?;
|
||||
let part_start = cursor.position();
|
||||
f(cursor)?;
|
||||
let part_end = cursor.position();
|
||||
let len = part_end - part_start;
|
||||
cursor.seek(SeekFrom::Start(part_start - 2))?;
|
||||
cursor.write_u16::<NetworkEndian>(len as u16)?;
|
||||
cursor.seek(SeekFrom::Start(part_end))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_internal(&self, buffer: &mut MsgBuffer) -> Result<(), io::Error> {
|
||||
let len;
|
||||
{
|
||||
let mut cursor = Cursor::new(buffer.buffer());
|
||||
Self::encode_part(&mut cursor, Self::PART_NODEID, |cursor| cursor.write_all(&self.node_id))?;
|
||||
Self::encode_part(&mut cursor, Self::PART_PEERS, |cursor| self.encode_peer_list_part(cursor))?;
|
||||
Self::encode_part(&mut cursor, Self::PART_CLAIMS, |mut cursor| {
|
||||
for c in &self.claims {
|
||||
c.write_to(&mut cursor);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if let Some(timeout) = self.peer_timeout {
|
||||
Self::encode_part(&mut cursor, Self::PART_PEER_TIMEOUT, |cursor| {
|
||||
cursor.write_u16::<NetworkEndian>(timeout)
|
||||
})?
|
||||
}
|
||||
Self::encode_part(&mut cursor, Self::PART_ADDRS, |cursor| self.encode_addrs_part(cursor))?;
|
||||
cursor.write_u8(Self::PART_END)?;
|
||||
len = cursor.position() as usize;
|
||||
}
|
||||
buffer.set_length(len);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn encode(&self, buffer: &mut MsgBuffer) {
|
||||
self.encode_internal(buffer).expect("Buffer too small")
|
||||
}
|
||||
}
|
||||
|
||||
impl Payload for NodeInfo {
|
||||
fn write_to(&self, buffer: &mut MsgBuffer) {
|
||||
self.encode(buffer)
|
||||
}
|
||||
|
||||
fn read_from<R: Read>(r: R) -> Result<Self, Error> {
|
||||
Self::decode(r)
|
||||
}
|
||||
}
|
108
src/net.rs
108
src/net.rs
|
@ -1,37 +1,79 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
io::{self, ErrorKind},
|
||||
net::{SocketAddr, UdpSocket},
|
||||
net::{IpAddr, Ipv6Addr, SocketAddr, UdpSocket},
|
||||
os::unix::io::{AsRawFd, RawFd},
|
||||
sync::atomic::{AtomicBool, Ordering}
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use super::util::{MockTimeSource, Time, TimeSource};
|
||||
use super::util::{MockTimeSource, MsgBuffer, Time, TimeSource};
|
||||
use crate::{config::DEFAULT_PORT, port_forwarding::PortForwarding};
|
||||
|
||||
pub fn mapped_addr(addr: SocketAddr) -> SocketAddr {
|
||||
// HOT PATH
|
||||
match addr {
|
||||
SocketAddr::V4(addr4) => SocketAddr::new(IpAddr::V6(addr4.ip().to_ipv6_mapped()), addr4.port()),
|
||||
_ => addr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ip() -> IpAddr {
|
||||
let s = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
s.connect("8.8.8.8:0").unwrap();
|
||||
s.local_addr().unwrap().ip()
|
||||
}
|
||||
|
||||
pub trait Socket: AsRawFd + Sized {
|
||||
fn listen(addr: SocketAddr) -> Result<Self, io::Error>;
|
||||
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), io::Error>;
|
||||
fn listen(addr: &str) -> Result<Self, io::Error>;
|
||||
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error>;
|
||||
fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error>;
|
||||
fn address(&self) -> Result<SocketAddr, io::Error>;
|
||||
fn create_port_forwarding(&self) -> Option<PortForwarding>;
|
||||
}
|
||||
|
||||
pub fn parse_listen(addr: &str, default_port: u16) -> SocketAddr {
|
||||
if let Some(addr) = addr.strip_prefix("*:") {
|
||||
let port = try_fail!(addr.parse::<u16>(), "Invalid port: {}");
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)
|
||||
} else if addr.contains(':') {
|
||||
try_fail!(addr.parse::<SocketAddr>(), "Invalid address: {}: {}", addr)
|
||||
} else if let Ok(port) = addr.parse::<u16>() {
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)
|
||||
} else {
|
||||
let ip = try_fail!(addr.parse::<IpAddr>(), "Invalid addr: {}");
|
||||
SocketAddr::new(ip, default_port)
|
||||
}
|
||||
}
|
||||
|
||||
impl Socket for UdpSocket {
|
||||
fn listen(addr: SocketAddr) -> Result<Self, io::Error> {
|
||||
fn listen(addr: &str) -> Result<Self, io::Error> {
|
||||
let addr = mapped_addr(parse_listen(addr, DEFAULT_PORT));
|
||||
UdpSocket::bind(addr)
|
||||
}
|
||||
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), io::Error> {
|
||||
self.recv_from(buffer)
|
||||
|
||||
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error> {
|
||||
buffer.clear();
|
||||
let (size, addr) = self.recv_from(buffer.buffer())?;
|
||||
buffer.set_length(size);
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error> {
|
||||
self.send_to(data, addr)
|
||||
}
|
||||
|
||||
fn address(&self) -> Result<SocketAddr, io::Error> {
|
||||
self.local_addr()
|
||||
let mut addr = self.local_addr()?;
|
||||
addr.set_ip(get_ip());
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
fn create_port_forwarding(&self) -> Option<PortForwarding> {
|
||||
PortForwarding::new(self.address().unwrap().port())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +86,7 @@ pub struct MockSocket {
|
|||
nat_peers: HashMap<SocketAddr, Time>,
|
||||
address: SocketAddr,
|
||||
outbound: VecDeque<(SocketAddr, Vec<u8>)>,
|
||||
inbound: VecDeque<(SocketAddr, Vec<u8>)>
|
||||
inbound: VecDeque<(SocketAddr, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl MockSocket {
|
||||
|
@ -53,8 +95,8 @@ impl MockSocket {
|
|||
nat: Self::get_nat(),
|
||||
nat_peers: HashMap::new(),
|
||||
address,
|
||||
outbound: VecDeque::new(),
|
||||
inbound: VecDeque::new()
|
||||
outbound: VecDeque::with_capacity(10),
|
||||
inbound: VecDeque::with_capacity(10),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,12 +111,12 @@ impl MockSocket {
|
|||
pub fn put_inbound(&mut self, from: SocketAddr, data: Vec<u8>) -> bool {
|
||||
if !self.nat {
|
||||
self.inbound.push_back((from, data));
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
if let Some(timeout) = self.nat_peers.get(&from) {
|
||||
if *timeout >= MockTimeSource::now() {
|
||||
self.inbound.push_back((from, data));
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
warn!("Sender {:?} is filtered out by NAT", from);
|
||||
|
@ -93,25 +135,49 @@ impl AsRawFd for MockSocket {
|
|||
}
|
||||
|
||||
impl Socket for MockSocket {
|
||||
fn listen(addr: SocketAddr) -> Result<Self, io::Error> {
|
||||
Ok(Self::new(addr))
|
||||
fn listen(addr: &str) -> Result<Self, io::Error> {
|
||||
Ok(Self::new(mapped_addr(parse_listen(addr, DEFAULT_PORT))))
|
||||
}
|
||||
fn receive(&mut self, buffer: &mut [u8]) -> Result<(usize, SocketAddr), io::Error> {
|
||||
|
||||
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error> {
|
||||
if let Some((addr, data)) = self.inbound.pop_front() {
|
||||
buffer[0..data.len()].copy_from_slice(&data);
|
||||
Ok((data.len(), addr))
|
||||
buffer.clear();
|
||||
buffer.set_length(data.len());
|
||||
buffer.message_mut().copy_from_slice(&data);
|
||||
Ok(addr)
|
||||
} else {
|
||||
Err(io::Error::new(ErrorKind::Other, "nothing in queue"))
|
||||
}
|
||||
}
|
||||
|
||||
fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error> {
|
||||
self.outbound.push_back((addr, data.to_owned()));
|
||||
self.outbound.push_back((addr, data.into()));
|
||||
if self.nat {
|
||||
self.nat_peers.insert(addr, MockTimeSource::now() + 300);
|
||||
}
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
fn address(&self) -> Result<SocketAddr, io::Error> {
|
||||
Ok(self.address)
|
||||
}
|
||||
|
||||
fn create_port_forwarding(&self) -> Option<PortForwarding> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bench")]
|
||||
mod bench {
|
||||
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn udp_send(b: &mut Bencher) {
|
||||
let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
let data = [0; 1400];
|
||||
let addr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 1);
|
||||
b.iter(|| sock.send_to(&data, &addr).unwrap());
|
||||
b.bytes = 1400;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use super::{device::Type, types::Mode, util::Duration};
|
||||
use crate::config::{ConfigFile, ConfigFileBeacon, ConfigFileDevice, ConfigFileStatsd, CryptoConfig};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
pub enum OldCryptoMethod {
|
||||
#[serde(rename = "chacha20")]
|
||||
ChaCha20,
|
||||
#[serde(rename = "aes256")]
|
||||
AES256,
|
||||
#[serde(rename = "aes128")]
|
||||
AES128,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
|
||||
pub struct OldConfigFile {
|
||||
#[serde(alias = "device-type")]
|
||||
pub device_type: Option<Type>,
|
||||
#[serde(alias = "device-name")]
|
||||
pub device_name: Option<String>,
|
||||
#[serde(alias = "device-path")]
|
||||
pub device_path: Option<String>,
|
||||
pub ifup: Option<String>,
|
||||
pub ifdown: Option<String>,
|
||||
pub crypto: Option<OldCryptoMethod>,
|
||||
#[serde(alias = "shared-key")]
|
||||
pub shared_key: Option<String>,
|
||||
pub magic: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub listen: Option<String>,
|
||||
pub peers: Option<Vec<String>>,
|
||||
#[serde(alias = "peer-timeout")]
|
||||
pub peer_timeout: Option<Duration>,
|
||||
pub keepalive: Option<Duration>,
|
||||
#[serde(alias = "beacon-store")]
|
||||
pub beacon_store: Option<String>,
|
||||
#[serde(alias = "beacon-load")]
|
||||
pub beacon_load: Option<String>,
|
||||
#[serde(alias = "beacon-interval")]
|
||||
pub beacon_interval: Option<Duration>,
|
||||
pub mode: Option<Mode>,
|
||||
#[serde(alias = "dst-timeout")]
|
||||
pub dst_timeout: Option<Duration>,
|
||||
pub subnets: Option<Vec<String>>,
|
||||
#[serde(alias = "port-forwarding")]
|
||||
pub port_forwarding: Option<bool>,
|
||||
#[serde(alias = "pid-file")]
|
||||
pub pid_file: Option<String>,
|
||||
#[serde(alias = "stats-file")]
|
||||
pub stats_file: Option<String>,
|
||||
#[serde(alias = "statsd-server")]
|
||||
pub statsd_server: Option<String>,
|
||||
#[serde(alias = "statsd-prefix")]
|
||||
pub statsd_prefix: Option<String>,
|
||||
pub user: Option<String>,
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
impl OldConfigFile {
|
||||
#[allow(clippy::or_fun_call)]
|
||||
pub fn convert(self) -> ConfigFile {
|
||||
if self.device_type.is_none() {
|
||||
warn!("The default device type changed from TAP to TUN")
|
||||
}
|
||||
if self.ifup.is_some() {
|
||||
info!("There is a new option --ip that can handle most use cases of --ifup")
|
||||
}
|
||||
info!("The converted config enables all available encryption algorithms");
|
||||
if self.shared_key.is_none() {
|
||||
warn!("Operation without a password is no longer supported, password set to 'none'");
|
||||
}
|
||||
if self.magic.is_some() {
|
||||
warn!("The magic header functionality is no longer supported")
|
||||
}
|
||||
if self.listen.is_some() && self.port.is_some() {
|
||||
warn!("The port option is no longer available, using listen instead")
|
||||
}
|
||||
if self.peer_timeout.is_none() {
|
||||
info!("The default peer timeout changed from 10 minutes to 5 minutes")
|
||||
}
|
||||
warn!("Even with a converted config file version 2 nodes can not communicate with version 1 nodes");
|
||||
ConfigFile {
|
||||
auto_claim: None,
|
||||
beacon: Some(ConfigFileBeacon {
|
||||
interval: self.beacon_interval,
|
||||
load: self.beacon_load,
|
||||
store: self.beacon_store,
|
||||
password: self.shared_key.clone(),
|
||||
}),
|
||||
claims: self.subnets,
|
||||
crypto: CryptoConfig {
|
||||
algorithms: vec![],
|
||||
password: Some(self.shared_key.unwrap_or_else(|| "none".to_string())),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
trusted_keys: vec![],
|
||||
},
|
||||
device: Some(ConfigFileDevice {
|
||||
fix_rp_filter: None,
|
||||
name: self.device_name,
|
||||
path: self.device_path,
|
||||
type_: self.device_type,
|
||||
}),
|
||||
group: self.group,
|
||||
ifdown: self.ifdown,
|
||||
ifup: self.ifup,
|
||||
ip: None,
|
||||
advertise_addresses: None,
|
||||
keepalive: self.keepalive,
|
||||
listen: self.listen.or(self.port.map(|p| format!("{}", p))),
|
||||
mode: self.mode,
|
||||
peer_timeout: self.peer_timeout,
|
||||
peers: self.peers,
|
||||
pid_file: self.pid_file,
|
||||
port_forwarding: self.port_forwarding,
|
||||
stats_file: self.stats_file,
|
||||
statsd: Some(ConfigFileStatsd { prefix: self.statsd_prefix, server: self.statsd_server }),
|
||||
switch_timeout: self.dst_timeout,
|
||||
user: self.user,
|
||||
hook: None,
|
||||
hooks: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use crate::{error::Error, types::Address};
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
pub trait Protocol: Sized {
|
||||
fn parse(_: &[u8]) -> Result<(Address, Address), Error>;
|
||||
}
|
||||
|
||||
/// An ethernet frame dissector
|
||||
///
|
||||
/// This dissector is able to extract the source and destination addresses of ethernet frames.
|
||||
///
|
||||
/// If the ethernet frame contains a VLAN tag, both addresses will be prefixed with that tag,
|
||||
/// resulting in 8-byte addresses. Additional nested tags will be ignored.
|
||||
pub struct Frame;
|
||||
|
||||
impl Protocol for Frame {
|
||||
/// Parses an ethernet frame and extracts the source and destination addresses
|
||||
///
|
||||
/// # Errors
|
||||
/// This method will fail when the given data is not a valid ethernet frame.
|
||||
fn parse(data: &[u8]) -> Result<(Address, Address), Error> {
|
||||
// HOT PATH
|
||||
let mut cursor = Cursor::new(data);
|
||||
let mut src = [0; 16];
|
||||
let mut dst = [0; 16];
|
||||
let mut proto = [0; 2];
|
||||
cursor
|
||||
.read_exact(&mut dst[..6])
|
||||
.and_then(|_| cursor.read_exact(&mut src[..6]))
|
||||
.and_then(|_| cursor.read_exact(&mut proto))
|
||||
.map_err(|_| Error::Parse("Frame is too short"))?;
|
||||
if proto == [0x81, 0x00] {
|
||||
src.copy_within(..6, 2);
|
||||
dst.copy_within(..6, 2);
|
||||
cursor.read_exact(&mut src[..2]).map_err(|_| Error::Parse("Vlan frame is too short"))?;
|
||||
src[0] &= 0x0f; // restrict vlan id to 12 bits
|
||||
dst[..2].copy_from_slice(&src[..2]);
|
||||
if src[0..1] == [0, 0] {
|
||||
// treat vlan id 0x000 as untagged
|
||||
src.copy_within(2..8, 0);
|
||||
dst.copy_within(2..8, 0);
|
||||
return Ok((Address { data: src, len: 6 }, Address { data: dst, len: 6 }));
|
||||
}
|
||||
Ok((Address { data: src, len: 8 }, Address { data: dst, len: 8 }))
|
||||
} else {
|
||||
Ok((Address { data: src, len: 6 }, Address { data: dst, len: 6 }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_frame_without_vlan() {
|
||||
let data = [6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let (src, dst) = Frame::parse(&data).unwrap();
|
||||
assert_eq!(src, Address { data: [1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 6 });
|
||||
assert_eq!(dst, Address { data: [6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 6 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_frame_with_vlan() {
|
||||
let data = [6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 0x81, 0, 4, 210, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let (src, dst) = Frame::parse(&data).unwrap();
|
||||
assert_eq!(src, Address { data: [4, 210, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0], len: 8 });
|
||||
assert_eq!(dst, Address { data: [4, 210, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0], len: 8 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_frame() {
|
||||
assert!(Frame::parse(&[6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 7, 8]).is_ok());
|
||||
// truncated frame
|
||||
assert!(Frame::parse(&[]).is_err());
|
||||
// truncated vlan frame
|
||||
assert!(Frame::parse(&[6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 0x81, 0x00]).is_err());
|
||||
}
|
||||
|
||||
/// An IP packet dissector
|
||||
///
|
||||
/// This dissector is able to extract the source and destination ip addresses of ipv4 packets and
|
||||
/// ipv6 packets.
|
||||
#[allow(dead_code)]
|
||||
pub struct Packet;
|
||||
|
||||
impl Protocol for Packet {
|
||||
/// Parses an ip packet and extracts the source and destination addresses
|
||||
///
|
||||
/// # Errors
|
||||
/// This method will fail when the given data is not a valid ipv4 and ipv6 packet.
|
||||
fn parse(data: &[u8]) -> Result<(Address, Address), Error> {
|
||||
// HOT PATH
|
||||
if data.is_empty() {
|
||||
return Err(Error::Parse("Empty header"));
|
||||
}
|
||||
let version = data[0] >> 4;
|
||||
match version {
|
||||
4 => {
|
||||
if data.len() < 20 {
|
||||
return Err(Error::Parse("Truncated IPv4 header"));
|
||||
}
|
||||
let src = Address::read_from_fixed(&data[12..], 4)?;
|
||||
let dst = Address::read_from_fixed(&data[16..], 4)?;
|
||||
Ok((src, dst))
|
||||
}
|
||||
6 => {
|
||||
if data.len() < 40 {
|
||||
return Err(Error::Parse("Truncated IPv6 header"));
|
||||
}
|
||||
let src = Address::read_from_fixed(&data[8..], 16)?;
|
||||
let dst = Address::read_from_fixed(&data[24..], 16)?;
|
||||
Ok((src, dst))
|
||||
}
|
||||
_ => Err(Error::Parse("Invalid IP protocol version")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ipv4_packet() {
|
||||
let data = [0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1, 192, 168, 1, 2];
|
||||
let (src, dst) = Packet::parse(&data).unwrap();
|
||||
assert_eq!(src, Address { data: [192, 168, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 4 });
|
||||
assert_eq!(dst, Address { data: [192, 168, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 4 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ipv6_packet() {
|
||||
let data = [
|
||||
0x60, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5,
|
||||
4, 3, 2, 1,
|
||||
];
|
||||
let (src, dst) = Packet::parse(&data).unwrap();
|
||||
assert_eq!(src, Address { data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6], len: 16 });
|
||||
assert_eq!(dst, Address { data: [0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1], len: 16 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_invalid_packet() {
|
||||
assert!(Packet::parse(&[0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1, 192, 168, 1, 2]).is_ok());
|
||||
assert!(Packet::parse(&[
|
||||
0x60, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5,
|
||||
4, 3, 2, 1
|
||||
])
|
||||
.is_ok());
|
||||
// no data
|
||||
assert!(Packet::parse(&[]).is_err());
|
||||
// wrong version
|
||||
assert!(Packet::parse(&[0x20]).is_err());
|
||||
// truncated ipv4
|
||||
assert!(Packet::parse(&[0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1, 192, 168, 1]).is_err());
|
||||
// truncated ipv6
|
||||
assert!(Packet::parse(&[
|
||||
0x60, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 6, 5,
|
||||
4, 3, 2
|
||||
])
|
||||
.is_err());
|
||||
}
|
|
@ -1,50 +1,43 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use crate::device::Device;
|
||||
use std::{io, os::unix::io::RawFd};
|
||||
|
||||
use super::WaitResult;
|
||||
use crate::{device::Type, net::Socket};
|
||||
|
||||
pub struct EpollWait {
|
||||
poll_fd: RawFd,
|
||||
event: libc::epoll_event,
|
||||
socket: RawFd,
|
||||
device: RawFd,
|
||||
timeout: u32
|
||||
timeout: u32,
|
||||
}
|
||||
|
||||
impl EpollWait {
|
||||
pub fn new<S: Socket>(socket: &S, device: &dyn Device, timeout: u32) -> io::Result<Self> {
|
||||
pub fn new(socket: RawFd, device: RawFd, timeout: u32) -> io::Result<Self> {
|
||||
Self::create(socket, device, timeout, libc::EPOLLIN as u32)
|
||||
}
|
||||
|
||||
pub fn testing<S: Socket>(socket: &S, device: &dyn Device, timeout: u32) -> io::Result<Self> {
|
||||
pub fn testing(socket: RawFd, device: RawFd, timeout: u32) -> io::Result<Self> {
|
||||
Self::create(socket, device, timeout, (libc::EPOLLIN | libc::EPOLLOUT) as u32)
|
||||
}
|
||||
|
||||
fn create<S: Socket>(socket: &S, device: &dyn Device, timeout: u32, flags: u32) -> io::Result<Self> {
|
||||
fn create(socket: RawFd, device: RawFd, timeout: u32, flags: u32) -> io::Result<Self> {
|
||||
let mut event = libc::epoll_event { u64: 0, events: 0 };
|
||||
let poll_fd = unsafe { libc::epoll_create(3) };
|
||||
if poll_fd == -1 {
|
||||
return Err(io::Error::last_os_error())
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let raw_fds = if device.get_type() != Type::Dummy {
|
||||
vec![socket.as_raw_fd(), device.as_raw_fd()]
|
||||
} else {
|
||||
vec![socket.as_raw_fd()]
|
||||
};
|
||||
for fd in raw_fds {
|
||||
event.u64 = fd as u64;
|
||||
for fd in &[socket, device] {
|
||||
event.u64 = *fd as u64;
|
||||
event.events = flags;
|
||||
let res = unsafe { libc::epoll_ctl(poll_fd, libc::EPOLL_CTL_ADD, fd, &mut event) };
|
||||
let res = unsafe { libc::epoll_ctl(poll_fd, libc::EPOLL_CTL_ADD, *fd, &mut event) };
|
||||
if res == -1 {
|
||||
return Err(io::Error::last_os_error())
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(Self { poll_fd, event, socket: socket.as_raw_fd(), device: device.as_raw_fd(), timeout })
|
||||
Ok(Self { poll_fd, event, socket, device, timeout })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +63,7 @@ impl Iterator for EpollWait {
|
|||
unreachable!()
|
||||
}
|
||||
}
|
||||
_ => unreachable!()
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
|
@ -8,12 +8,11 @@ mod epoll;
|
|||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub use self::epoll::EpollWait as WaitImpl;
|
||||
|
||||
|
||||
use std::io;
|
||||
|
||||
pub enum WaitResult {
|
||||
Timeout,
|
||||
Socket,
|
||||
Device,
|
||||
Error(io::Error)
|
||||
Error(io::Error),
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
#[cfg(feature = "nat")]
|
||||
|
@ -19,7 +19,7 @@ mod internal {
|
|||
pub internal_addr: SocketAddrV4,
|
||||
pub external_addr: SocketAddrV4,
|
||||
gateway: Gateway,
|
||||
pub next_extension: Option<Time>
|
||||
pub next_extension: Option<Time>,
|
||||
}
|
||||
|
||||
impl PortForwarding {
|
||||
|
@ -32,27 +32,28 @@ mod internal {
|
|||
if err.kind() == io::ErrorKind::WouldBlock {
|
||||
// Why this code?
|
||||
info!("Port-forwarding: no router found");
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
}
|
||||
error!("Port-forwarding: failed to find router: {}", err);
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
};
|
||||
info!("Port-forwarding: found router at {}", gateway.addr);
|
||||
debug!("Port-forwarding: found router at {}", gateway.addr);
|
||||
let internal_addr = SocketAddrV4::new(get_internal_ip(), port);
|
||||
// Query the external address
|
||||
let external_ip = match gateway.get_external_ip() {
|
||||
Ok(ip) => ip,
|
||||
Err(err) => {
|
||||
error!("Port-forwarding: failed to obtain external IP: {}", err);
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if let Ok((port, timeout)) = Self::get_any_forwarding(&gateway, internal_addr, port) {
|
||||
info!("Port-forwarding: external IP is {}", external_ip);
|
||||
debug!("Port-forwarding: external IP is {}", external_ip);
|
||||
let external_addr = SocketAddrV4::new(external_ip, port);
|
||||
info!("Port-forwarding: sucessfully activated port forward on {}, timeout: {}", external_addr, timeout);
|
||||
debug!("Port-forwarding has timeout {}", timeout);
|
||||
info!("Port-forwarding: successfully activated port forward on {}", external_addr);
|
||||
let next_extension =
|
||||
if timeout > 0 { Some(SystemTimeSource::now() + Time::from(timeout) - 60) } else { None };
|
||||
Some(PortForwarding { internal_addr, external_addr, gateway, next_extension })
|
||||
|
@ -63,26 +64,27 @@ mod internal {
|
|||
|
||||
fn get_any_forwarding(gateway: &Gateway, addr: SocketAddrV4, port: u16) -> Result<(u16, u32), ()> {
|
||||
if let Ok(a) = Self::get_forwarding(gateway, addr, port) {
|
||||
return Ok(a)
|
||||
return Ok(a);
|
||||
}
|
||||
if let Ok(a) = Self::get_forwarding(gateway, addr, 0) {
|
||||
return Ok(a)
|
||||
return Ok(a);
|
||||
}
|
||||
for i in 1..5 {
|
||||
if let Ok(a) = Self::get_forwarding(gateway, addr, port + i) {
|
||||
return Ok(a)
|
||||
return Ok(a);
|
||||
}
|
||||
}
|
||||
for _ in 0..5 {
|
||||
if let Ok(a) = Self::get_forwarding(gateway, addr, rand::random()) {
|
||||
return Ok(a)
|
||||
return Ok(a);
|
||||
}
|
||||
}
|
||||
warn!("Failed to activate port forwarding");
|
||||
Err(())
|
||||
}
|
||||
|
||||
fn get_forwarding(gateway: &Gateway, addr: SocketAddrV4, port: u16) -> Result<(u16, u32), ()> {
|
||||
info!("Trying external port {}", port);
|
||||
debug!("Trying external port {}", port);
|
||||
if port == 0 {
|
||||
match gateway.add_any_port(PortMappingProtocol::UDP, addr, LEASE_TIME, DESCRIPTION) {
|
||||
Ok(port) => Ok((port, LEASE_TIME)),
|
||||
|
@ -90,13 +92,13 @@ mod internal {
|
|||
match gateway.add_any_port(PortMappingProtocol::UDP, addr, 0, DESCRIPTION) {
|
||||
Ok(port) => Ok((port, 0)),
|
||||
Err(err) => {
|
||||
error!("Port-forwarding: failed to activate port forwarding: {}", err);
|
||||
debug!("Port-forwarding: failed to activate port forwarding: {}", err);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Port-forwarding: failed to activate port forwarding: {}", err);
|
||||
debug!("Port-forwarding: failed to activate port forwarding: {}", err);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
@ -107,13 +109,13 @@ mod internal {
|
|||
match gateway.add_port(PortMappingProtocol::UDP, port, addr, 0, DESCRIPTION) {
|
||||
Ok(()) => Ok((port, 0)),
|
||||
Err(err) => {
|
||||
error!("Port-forwarding: failed to activate port forwarding: {}", err);
|
||||
debug!("Port-forwarding: failed to activate port forwarding: {}", err);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Port-forwarding: failed to activate port forwarding: {}", err);
|
||||
debug!("Port-forwarding: failed to activate port forwarding: {}", err);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
@ -123,20 +125,20 @@ mod internal {
|
|||
pub fn check_extend(&mut self) {
|
||||
if let Some(deadline) = self.next_extension {
|
||||
if deadline > SystemTimeSource::now() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return
|
||||
return;
|
||||
}
|
||||
match self.gateway.add_port(
|
||||
PortMappingProtocol::UDP,
|
||||
self.external_addr.port(),
|
||||
self.internal_addr,
|
||||
LEASE_TIME,
|
||||
DESCRIPTION
|
||||
DESCRIPTION,
|
||||
) {
|
||||
Ok(()) => debug!("Port-forwarding: extended port forwarding"),
|
||||
Err(err) => error!("Port-forwarding: failed to extend port forwarding: {}", err)
|
||||
Err(err) => debug!("Port-forwarding: failed to extend port forwarding: {}", err),
|
||||
};
|
||||
self.next_extension = Some(SystemTimeSource::now() + Time::from(LEASE_TIME) - 60);
|
||||
}
|
||||
|
@ -144,9 +146,17 @@ mod internal {
|
|||
fn deactivate(&self) {
|
||||
match self.gateway.remove_port(PortMappingProtocol::UDP, self.external_addr.port()) {
|
||||
Ok(()) => info!("Port-forwarding: successfully deactivated port forwarding"),
|
||||
Err(err) => error!("Port-forwarding: failed to deactivate port forwarding: {}", err)
|
||||
Err(err) => debug!("Port-forwarding: failed to deactivate port forwarding: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_internal_ip(&self) -> SocketAddrV4 {
|
||||
self.internal_addr
|
||||
}
|
||||
|
||||
pub fn get_external_ip(&self) -> SocketAddrV4 {
|
||||
self.external_addr
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PortForwarding {
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use fnv::FnvHasher;
|
||||
use std::{
|
||||
cmp::min, collections::HashMap, hash::BuildHasherDefault, io, io::Write, marker::PhantomData, net::SocketAddr,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
types::{Address, Range, RangeList},
|
||||
util::{addr_nice, Duration, Time, TimeSource},
|
||||
};
|
||||
|
||||
type Hash = BuildHasherDefault<FnvHasher>;
|
||||
|
||||
struct CacheValue {
|
||||
peer: SocketAddr,
|
||||
timeout: Time,
|
||||
}
|
||||
|
||||
struct ClaimEntry {
|
||||
peer: SocketAddr,
|
||||
claim: Range,
|
||||
timeout: Time,
|
||||
}
|
||||
|
||||
pub struct ClaimTable<TS: TimeSource> {
|
||||
cache: HashMap<Address, CacheValue, Hash>,
|
||||
cache_timeout: Duration,
|
||||
claims: Vec<ClaimEntry>,
|
||||
claim_timeout: Duration,
|
||||
_dummy: PhantomData<TS>,
|
||||
}
|
||||
|
||||
impl<TS: TimeSource> ClaimTable<TS> {
|
||||
pub fn new(cache_timeout: Duration, claim_timeout: Duration) -> Self {
|
||||
Self { cache: HashMap::default(), cache_timeout, claims: vec![], claim_timeout, _dummy: PhantomData }
|
||||
}
|
||||
|
||||
pub fn cache(&mut self, addr: Address, peer: SocketAddr) {
|
||||
// HOT PATH
|
||||
self.cache.insert(addr, CacheValue { peer, timeout: TS::now() + self.cache_timeout as Time });
|
||||
}
|
||||
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.cache.clear()
|
||||
}
|
||||
|
||||
pub fn set_claims(&mut self, peer: SocketAddr, mut claims: RangeList) {
|
||||
let mut removed_claim = false;
|
||||
for entry in &mut self.claims {
|
||||
if entry.peer == peer {
|
||||
let pos = claims.iter().position(|r| r == &entry.claim);
|
||||
if let Some(pos) = pos {
|
||||
entry.timeout = TS::now() + self.claim_timeout as Time;
|
||||
claims.swap_remove(pos);
|
||||
if claims.is_empty() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
entry.timeout = 0;
|
||||
removed_claim = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for claim in claims {
|
||||
self.claims.push(ClaimEntry { peer, claim, timeout: TS::now() + self.claim_timeout as Time })
|
||||
}
|
||||
if removed_claim {
|
||||
for entry in self.cache.values_mut() {
|
||||
if entry.peer == peer {
|
||||
entry.timeout = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
self.housekeep()
|
||||
}
|
||||
|
||||
pub fn remove_claims(&mut self, peer: SocketAddr) {
|
||||
for entry in &mut self.claims {
|
||||
if entry.peer == peer {
|
||||
entry.timeout = 0
|
||||
}
|
||||
}
|
||||
for entry in self.cache.values_mut() {
|
||||
if entry.peer == peer {
|
||||
entry.timeout = 0
|
||||
}
|
||||
}
|
||||
self.housekeep()
|
||||
}
|
||||
|
||||
pub fn lookup(&mut self, addr: Address) -> Option<SocketAddr> {
|
||||
// HOT PATH
|
||||
if let Some(entry) = self.cache.get(&addr) {
|
||||
return Some(entry.peer);
|
||||
}
|
||||
// COLD PATH
|
||||
let mut found = None;
|
||||
let mut prefix_len = -1;
|
||||
for entry in &self.claims {
|
||||
if entry.claim.prefix_len as isize > prefix_len && entry.claim.matches(addr) {
|
||||
found = Some(entry);
|
||||
prefix_len = entry.claim.prefix_len as isize;
|
||||
}
|
||||
}
|
||||
if let Some(entry) = found {
|
||||
self.cache.insert(
|
||||
addr,
|
||||
CacheValue { peer: entry.peer, timeout: min(TS::now() + self.cache_timeout as Time, entry.timeout) },
|
||||
);
|
||||
return Some(entry.peer);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn housekeep(&mut self) {
|
||||
let now = TS::now();
|
||||
self.cache.retain(|_, v| v.timeout >= now);
|
||||
self.claims.retain(|e| e.timeout >= now);
|
||||
}
|
||||
|
||||
pub fn cache_len(&self) -> usize {
|
||||
self.cache.len()
|
||||
}
|
||||
|
||||
pub fn claim_len(&self) -> usize {
|
||||
self.claims.len()
|
||||
}
|
||||
|
||||
/// Write out the table
|
||||
pub fn write_out<W: Write>(&self, out: &mut W) -> Result<(), io::Error> {
|
||||
let now = TS::now();
|
||||
writeln!(out, "forwarding_table:")?;
|
||||
writeln!(out, " claims:")?;
|
||||
for entry in &self.claims {
|
||||
writeln!(
|
||||
out,
|
||||
" - \"{}\": {{ peer: \"{}\", timeout: {} }}",
|
||||
entry.claim,
|
||||
addr_nice(entry.peer),
|
||||
entry.timeout - now
|
||||
)?;
|
||||
}
|
||||
writeln!(out, " cache:")?;
|
||||
for (addr, entry) in &self.cache {
|
||||
writeln!(
|
||||
out,
|
||||
" - \"{}\": {{ peer: \"{}\", timeout: {} }}",
|
||||
addr,
|
||||
addr_nice(entry.peer),
|
||||
entry.timeout - now
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
|
@ -0,0 +1,210 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
io::Write,
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Once,
|
||||
},
|
||||
};
|
||||
|
||||
pub use crate::{
|
||||
cloud::GenericCloud,
|
||||
config::{Config, CryptoConfig},
|
||||
device::{MockDevice, Type},
|
||||
net::MockSocket,
|
||||
payload::{Frame, Packet, Protocol},
|
||||
util::{MockTimeSource, Time, TimeSource},
|
||||
};
|
||||
|
||||
static INIT_LOGGER: Once = Once::new();
|
||||
|
||||
pub fn init_debug_logger() {
|
||||
INIT_LOGGER.call_once(|| {
|
||||
log::set_boxed_logger(Box::new(DebugLogger)).unwrap();
|
||||
log::set_max_level(log::LevelFilter::Debug);
|
||||
})
|
||||
}
|
||||
|
||||
static CURRENT_NODE: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
struct DebugLogger;
|
||||
|
||||
impl DebugLogger {
|
||||
pub fn set_node(node: usize) {
|
||||
CURRENT_NODE.store(node, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl log::Log for DebugLogger {
|
||||
#[inline]
|
||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||
log::max_level() > metadata.level()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
eprintln!("Node {} - {} - {}", CURRENT_NODE.load(Ordering::SeqCst), record.level(), record.args());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn flush(&self) {
|
||||
std::io::stderr().flush().expect("Failed to flush")
|
||||
}
|
||||
}
|
||||
|
||||
type TestNode<P> = GenericCloud<MockDevice, P, MockSocket, MockTimeSource>;
|
||||
|
||||
pub struct Simulator<P: Protocol> {
|
||||
next_port: u16,
|
||||
nodes: HashMap<SocketAddr, TestNode<P>>,
|
||||
messages: VecDeque<(SocketAddr, SocketAddr, Vec<u8>)>,
|
||||
}
|
||||
|
||||
pub type TapSimulator = Simulator<Frame>;
|
||||
#[allow(dead_code)]
|
||||
pub type TunSimulator = Simulator<Packet>;
|
||||
|
||||
impl<P: Protocol> Simulator<P> {
|
||||
pub fn new() -> Self {
|
||||
init_debug_logger();
|
||||
MockTimeSource::set_time(0);
|
||||
Self { next_port: 1, nodes: HashMap::default(), messages: VecDeque::with_capacity(10) }
|
||||
}
|
||||
|
||||
pub fn add_node(&mut self, nat: bool, config: &Config) -> SocketAddr {
|
||||
let mut config = config.clone();
|
||||
MockSocket::set_nat(nat);
|
||||
config.listen = format!("[::]:{}", self.next_port);
|
||||
let addr = config.listen.parse::<SocketAddr>().unwrap();
|
||||
if config.crypto.password.is_none() && config.crypto.private_key.is_none() {
|
||||
config.crypto.password = Some("test123".to_string())
|
||||
}
|
||||
DebugLogger::set_node(self.next_port as usize);
|
||||
self.next_port += 1;
|
||||
let node = TestNode::new(&config, MockSocket::new(addr), MockDevice::new(), None, None);
|
||||
DebugLogger::set_node(0);
|
||||
self.nodes.insert(addr, node);
|
||||
addr
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_node(&mut self, addr: SocketAddr) -> &mut TestNode<P> {
|
||||
let node = self.nodes.get_mut(&addr).unwrap();
|
||||
DebugLogger::set_node(node.get_num());
|
||||
node
|
||||
}
|
||||
|
||||
pub fn simulate_next_message(&mut self) {
|
||||
if let Some((src, dst, data)) = self.messages.pop_front() {
|
||||
if let Some(node) = self.nodes.get_mut(&dst) {
|
||||
if node.socket().put_inbound(src, data) {
|
||||
DebugLogger::set_node(node.get_num());
|
||||
node.trigger_socket_event();
|
||||
DebugLogger::set_node(0);
|
||||
let sock = node.socket();
|
||||
let src = dst;
|
||||
while let Some((dst, data)) = sock.pop_outbound() {
|
||||
self.messages.push_back((src, dst, data));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Message to unknown node {}", dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulate_all_messages(&mut self) {
|
||||
while !self.messages.is_empty() {
|
||||
self.simulate_next_message()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger_node_housekeep(&mut self, addr: SocketAddr) {
|
||||
let node = self.nodes.get_mut(&addr).unwrap();
|
||||
DebugLogger::set_node(node.get_num());
|
||||
node.trigger_housekeep();
|
||||
DebugLogger::set_node(0);
|
||||
let sock = node.socket();
|
||||
while let Some((dst, data)) = sock.pop_outbound() {
|
||||
self.messages.push_back((addr, dst, data));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger_housekeep(&mut self) {
|
||||
for (src, node) in &mut self.nodes {
|
||||
DebugLogger::set_node(node.get_num());
|
||||
node.trigger_housekeep();
|
||||
DebugLogger::set_node(0);
|
||||
let sock = node.socket();
|
||||
while let Some((dst, data)) = sock.pop_outbound() {
|
||||
self.messages.push_back((*src, dst, data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_time(&mut self, time: Time) {
|
||||
MockTimeSource::set_time(time);
|
||||
}
|
||||
|
||||
pub fn simulate_time(&mut self, time: Time) {
|
||||
let mut t = MockTimeSource::now();
|
||||
while t < time {
|
||||
t += 1;
|
||||
self.set_time(t);
|
||||
self.trigger_housekeep();
|
||||
self.simulate_all_messages();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect(&mut self, src: SocketAddr, dst: SocketAddr) {
|
||||
let node = self.nodes.get_mut(&src).unwrap();
|
||||
DebugLogger::set_node(node.get_num());
|
||||
node.connect(dst).unwrap();
|
||||
DebugLogger::set_node(0);
|
||||
let sock = node.socket();
|
||||
while let Some((dst, data)) = sock.pop_outbound() {
|
||||
self.messages.push_back((src, dst, data));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connected(&self, src: SocketAddr, dst: SocketAddr) -> bool {
|
||||
self.nodes.get(&src).unwrap().is_connected(&dst)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn node_addresses(&self) -> Vec<SocketAddr> {
|
||||
self.nodes.keys().copied().collect()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn message_count(&self) -> usize {
|
||||
self.messages.len()
|
||||
}
|
||||
|
||||
pub fn put_payload(&mut self, addr: SocketAddr, data: Vec<u8>) {
|
||||
let node = self.nodes.get_mut(&addr).unwrap();
|
||||
node.device().put_inbound(data);
|
||||
DebugLogger::set_node(node.get_num());
|
||||
node.trigger_device_event();
|
||||
DebugLogger::set_node(0);
|
||||
let sock = node.socket();
|
||||
while let Some((dst, data)) = sock.pop_outbound() {
|
||||
self.messages.push_back((addr, dst, data));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_payload(&mut self, node: SocketAddr) -> Option<Vec<u8>> {
|
||||
self.nodes.get_mut(&node).unwrap().device().pop_outbound()
|
||||
}
|
||||
|
||||
pub fn drop_message(&mut self) {
|
||||
self.messages.pop_front();
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
macro_rules! assert_clean {
|
||||
($($node: expr),*) => {
|
||||
$(
|
||||
assert_eq!($node.socket().pop_outbound().map(|(addr, mut msg)| (addr, $node.decode_message(&mut msg).unwrap().without_data())), None);
|
||||
assert_eq!($node.device().pop_outbound(), None);
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_message4 {
|
||||
($from: expr, $from_addr: expr, $to: expr, $to_addr: expr, $message: expr) => {
|
||||
let (addr, mut data) = msg_get(&mut $from);
|
||||
assert_eq!($to_addr, addr);
|
||||
{
|
||||
let message = $from.decode_message(&mut data).unwrap();
|
||||
assert_eq!($message, message.without_data());
|
||||
}
|
||||
msg_put(&mut $to, $from_addr, data);
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! assert_message6 {
|
||||
($from: expr, $from_addr: expr, $to: expr, $to_addr: expr, $message: expr) => {
|
||||
let (addr, mut data) = msg6_get(&mut $from);
|
||||
assert_eq!($to_addr, addr);
|
||||
{
|
||||
let message = $from.decode_message(&mut data).unwrap();
|
||||
assert_eq!($message, message.without_data());
|
||||
}
|
||||
msg6_put(&mut $to, $from_addr, data);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! simulate {
|
||||
($($node: expr => $addr: expr),*) => {
|
||||
simulate(&mut [$((&mut $node, $addr)),*]);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! simulate_time {
|
||||
($time:expr, $($node: expr => $addr: expr),*) => {
|
||||
for _ in 0..$time {
|
||||
use crate::util::{MockTimeSource, TimeSource};
|
||||
MockTimeSource::set_time(MockTimeSource::now()+1);
|
||||
$(
|
||||
$node.trigger_housekeep();
|
||||
)*
|
||||
simulate(&mut [$((&mut $node, $addr)),*]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
macro_rules! assert_connected {
|
||||
($($node:expr),*) => {
|
||||
for node1 in [$(&$node),*].iter() {
|
||||
for node2 in [$(&$node),*].iter() {
|
||||
if node1.node_id() == node2.node_id() {
|
||||
continue
|
||||
}
|
||||
assert!(node1.peers().contains_node(&node2.node_id()));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
142
src/tests/mod.rs
142
src/tests/mod.rs
|
@ -1,144 +1,8 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
#[macro_use]
|
||||
mod helper;
|
||||
mod common;
|
||||
mod nat;
|
||||
mod payload;
|
||||
mod peers;
|
||||
|
||||
use std::{
|
||||
io::Write,
|
||||
net::{IpAddr, Ipv6Addr, SocketAddr},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Once
|
||||
}
|
||||
};
|
||||
|
||||
pub use super::{
|
||||
cloud::GenericCloud,
|
||||
config::Config,
|
||||
crypto::Crypto,
|
||||
device::MockDevice,
|
||||
ethernet::{self, SwitchTable},
|
||||
ip::{self, RoutingTable},
|
||||
net::MockSocket,
|
||||
types::{Protocol, Range, Table},
|
||||
udpmessage::Message,
|
||||
util::MockTimeSource
|
||||
};
|
||||
|
||||
|
||||
static INIT_LOGGER: Once = Once::new();
|
||||
|
||||
pub fn init_debug_logger() {
|
||||
INIT_LOGGER.call_once(|| {
|
||||
log::set_boxed_logger(Box::new(DebugLogger)).unwrap();
|
||||
log::set_max_level(log::LevelFilter::Debug);
|
||||
})
|
||||
}
|
||||
|
||||
struct DebugLogger;
|
||||
|
||||
impl log::Log for DebugLogger {
|
||||
#[inline]
|
||||
fn enabled(&self, _metadata: &log::Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn log(&self, record: &log::Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
eprintln!("{} - {}", record.level(), record.args());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn flush(&self) {
|
||||
std::io::stderr().flush().expect("Failed to flush")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type TestNode<P, T> = GenericCloud<MockDevice, P, T, MockSocket, MockTimeSource>;
|
||||
|
||||
type TapTestNode = TestNode<ethernet::Frame, SwitchTable<MockTimeSource>>;
|
||||
#[allow(dead_code)]
|
||||
type TunTestNode = TestNode<ip::Packet, RoutingTable>;
|
||||
|
||||
|
||||
thread_local! {
|
||||
static NEXT_PORT: AtomicUsize = AtomicUsize::new(1);
|
||||
}
|
||||
|
||||
fn next_sock_addr() -> SocketAddr {
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), NEXT_PORT.with(|p| p.fetch_add(1, Ordering::Relaxed)) as u16)
|
||||
}
|
||||
|
||||
fn create_tap_node(nat: bool) -> TapTestNode {
|
||||
create_tap_node_with_config(nat, Config::default())
|
||||
}
|
||||
|
||||
fn create_tap_node_with_config(nat: bool, mut config: Config) -> TapTestNode {
|
||||
MockSocket::set_nat(nat);
|
||||
config.listen = next_sock_addr();
|
||||
TestNode::new(&config, MockDevice::new(), SwitchTable::new(1800, 10), true, true, vec![], Crypto::None, None, None)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn create_tun_node(nat: bool, addresses: Vec<Range>) -> TunTestNode {
|
||||
MockSocket::set_nat(nat);
|
||||
TestNode::new(
|
||||
&Config { listen: next_sock_addr(), ..Config::default() },
|
||||
MockDevice::new(),
|
||||
RoutingTable::new(),
|
||||
false,
|
||||
false,
|
||||
addresses,
|
||||
Crypto::None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fn msg_get<P: Protocol, T: Table>(node: &mut TestNode<P, T>) -> (SocketAddr, Vec<u8>) {
|
||||
let msg = node.socket().pop_outbound();
|
||||
assert!(msg.is_some());
|
||||
msg.unwrap()
|
||||
}
|
||||
|
||||
fn msg_put<P: Protocol, T: Table>(node: &mut TestNode<P, T>, from: SocketAddr, msg: Vec<u8>) {
|
||||
if node.socket().put_inbound(from, msg) {
|
||||
node.trigger_socket_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn simulate<P: Protocol, T: Table>(nodes: &mut [(&mut TestNode<P, T>, SocketAddr)]) {
|
||||
for (ref mut node, ref _from_addr) in nodes.iter_mut() {
|
||||
while node.device().has_inbound() {
|
||||
node.trigger_device_event();
|
||||
}
|
||||
}
|
||||
let mut clean = false;
|
||||
while !clean {
|
||||
clean = true;
|
||||
let mut msgs = Vec::new();
|
||||
for (ref mut node, ref from_addr) in nodes.iter_mut() {
|
||||
while let Some((to_addr, msg)) = node.socket().pop_outbound() {
|
||||
msgs.push((msg, *from_addr, to_addr));
|
||||
}
|
||||
}
|
||||
clean &= msgs.is_empty();
|
||||
for (msg, from_addr, to_addr) in msgs {
|
||||
for (ref mut node, ref addr) in nodes.iter_mut() {
|
||||
if *addr == to_addr {
|
||||
msg_put(node, from_addr, msg);
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mod peers;
|
114
src/tests/nat.rs
114
src/tests/nat.rs
|
@ -1,87 +1,73 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use super::*;
|
||||
use super::common::*;
|
||||
|
||||
#[test]
|
||||
fn connect_nat_2_peers() {
|
||||
init_debug_logger();
|
||||
MockTimeSource::set_time(0);
|
||||
let mut node1 = create_tap_node(true);
|
||||
let node1_addr = addr!("1.2.3.4:5678");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.3.4.5:6789");
|
||||
let config = Config { port_forwarding: false, ..Default::default() };
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(true, &config);
|
||||
let node2 = sim.add_node(true, &config);
|
||||
|
||||
node2.connect("1.2.3.4:5678").unwrap();
|
||||
sim.connect(node1, node2);
|
||||
sim.connect(node2, node1);
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
sim.simulate_time(60);
|
||||
|
||||
assert!(!node1.peers().contains_node(&node2.node_id()));
|
||||
assert!(!node2.peers().contains_node(&node1.node_id()));
|
||||
|
||||
|
||||
node1.connect("2.3.4.5:6789").unwrap();
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
|
||||
assert_connected!(node1, node2);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_nat_3_peers() {
|
||||
init_debug_logger();
|
||||
MockTimeSource::set_time(0);
|
||||
let mut node1 = create_tap_node(true);
|
||||
let node1_addr = addr!("1.2.3.4:5678");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.3.4.5:6789");
|
||||
let mut node3 = create_tap_node(false);
|
||||
let node3_addr = addr!("3.4.5.6:7890");
|
||||
node2.connect("1.2.3.4:5678").unwrap();
|
||||
node3.connect("1.2.3.4:5678").unwrap();
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr, node3 => node3_addr);
|
||||
let config = Config::default();
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(true, &config);
|
||||
let node2 = sim.add_node(true, &config);
|
||||
let node3 = sim.add_node(true, &config);
|
||||
|
||||
assert!(!node1.peers().contains_node(&node2.node_id()));
|
||||
assert!(!node2.peers().contains_node(&node1.node_id()));
|
||||
assert!(!node3.peers().contains_node(&node1.node_id()));
|
||||
assert!(!node3.peers().contains_node(&node2.node_id()));
|
||||
assert!(!node1.peers().contains_node(&node3.node_id()));
|
||||
assert!(!node2.peers().contains_node(&node3.node_id()));
|
||||
sim.connect(node1, node2);
|
||||
sim.connect(node2, node1);
|
||||
sim.connect(node1, node3);
|
||||
sim.connect(node3, node1);
|
||||
|
||||
node1.connect("3.4.5.6:7890").unwrap();
|
||||
node2.connect("3.4.5.6:7890").unwrap();
|
||||
|
||||
simulate_time!(1000, node1 => node1_addr, node2 => node2_addr, node3 => node3_addr);
|
||||
|
||||
assert_connected!(node1, node3);
|
||||
assert_connected!(node2, node3);
|
||||
assert_connected!(node1, node2);
|
||||
sim.simulate_time(300);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
assert!(sim.is_connected(node1, node3));
|
||||
assert!(sim.is_connected(node3, node1));
|
||||
assert!(sim.is_connected(node2, node3));
|
||||
assert!(sim.is_connected(node3, node2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn nat_keepalive() {
|
||||
init_debug_logger();
|
||||
MockTimeSource::set_time(0);
|
||||
let mut node1 = create_tap_node(true);
|
||||
let node1_addr = addr!("1.2.3.4:5678");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.3.4.5:6789");
|
||||
let mut node3 = create_tap_node(false);
|
||||
let node3_addr = addr!("3.4.5.6:7890");
|
||||
node1.connect("3.4.5.6:7890").unwrap();
|
||||
node2.connect("3.4.5.6:7890").unwrap();
|
||||
let config = Config::default();
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(true, &config);
|
||||
let node2 = sim.add_node(true, &config);
|
||||
let node3 = sim.add_node(true, &config);
|
||||
|
||||
simulate_time!(1000, node1 => node1_addr, node2 => node2_addr, node3 => node3_addr);
|
||||
sim.connect(node1, node2);
|
||||
sim.connect(node2, node1);
|
||||
sim.connect(node1, node3);
|
||||
sim.connect(node3, node1);
|
||||
|
||||
assert_connected!(node1, node3);
|
||||
assert_connected!(node2, node3);
|
||||
assert_connected!(node1, node2);
|
||||
sim.simulate_time(1000);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
assert!(sim.is_connected(node1, node3));
|
||||
assert!(sim.is_connected(node3, node1));
|
||||
assert!(sim.is_connected(node2, node3));
|
||||
assert!(sim.is_connected(node3, node2));
|
||||
|
||||
simulate_time!(10000, node1 => node1_addr, node2 => node2_addr, node3 => node3_addr);
|
||||
|
||||
assert_connected!(node1, node3);
|
||||
assert_connected!(node2, node3);
|
||||
assert_connected!(node1, node2);
|
||||
sim.simulate_time(10000);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
assert!(sim.is_connected(node1, node3));
|
||||
assert!(sim.is_connected(node3, node1));
|
||||
assert!(sim.is_connected(node2, node3));
|
||||
assert!(sim.is_connected(node3, node2));
|
||||
}
|
||||
|
|
|
@ -1,86 +1,184 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use super::*;
|
||||
use super::common::*;
|
||||
|
||||
#[test]
|
||||
fn ethernet_delivers() {
|
||||
let mut node1 = create_tap_node(false);
|
||||
let node1_addr = addr!("1.2.3.4:5678");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.3.4.5:6789");
|
||||
fn switch_delivers() {
|
||||
let config = Config { device_type: Type::Tap, ..Config::default() };
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
node1.connect("2.3.4.5:6789").unwrap();
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
assert_connected!(node1, node2);
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
|
||||
let payload = vec![2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5];
|
||||
|
||||
node1.device().put_inbound(payload.clone());
|
||||
sim.put_payload(node1, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
|
||||
assert_eq!(Some(payload), node2.device().pop_outbound());
|
||||
|
||||
assert_clean!(node1, node2);
|
||||
assert_eq!(Some(payload), sim.pop_payload(node2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch_learns() {
|
||||
let mut node1 = create_tap_node(false);
|
||||
let node1_addr = addr!("1.2.3.4:5678");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.3.4.5:6789");
|
||||
let mut node3 = create_tap_node(false);
|
||||
let node3_addr = addr!("3.4.5.6:7890");
|
||||
let config = Config { device_type: Type::Tap, ..Config::default() };
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
let node3 = sim.add_node(false, &config);
|
||||
|
||||
node1.connect("2.3.4.5:6789").unwrap();
|
||||
node1.connect("3.4.5.6:7890").unwrap();
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr, node3 => node3_addr);
|
||||
assert_connected!(node1, node2, node3);
|
||||
sim.connect(node1, node2);
|
||||
sim.connect(node1, node3);
|
||||
sim.connect(node2, node3);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
assert!(sim.is_connected(node1, node3));
|
||||
assert!(sim.is_connected(node3, node1));
|
||||
assert!(sim.is_connected(node2, node3));
|
||||
assert!(sim.is_connected(node3, node2));
|
||||
|
||||
let payload = vec![2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5];
|
||||
|
||||
// Nothing learnt so far, node1 broadcasts
|
||||
|
||||
node1.device().put_inbound(payload.clone());
|
||||
sim.put_payload(node1, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr, node3 => node3_addr);
|
||||
|
||||
assert_eq!(Some(&payload), node2.device().pop_outbound().as_ref());
|
||||
assert_eq!(Some(&payload), node3.device().pop_outbound().as_ref());
|
||||
assert_eq!(Some(payload.clone()), sim.pop_payload(node2));
|
||||
assert_eq!(Some(payload), sim.pop_payload(node3));
|
||||
|
||||
let payload = vec![1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 5, 4, 3, 2, 1];
|
||||
|
||||
// Node 2 learned the address by receiving it, does not broadcast
|
||||
|
||||
node2.device().put_inbound(payload.clone());
|
||||
sim.put_payload(node2, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr, node3 => node3_addr);
|
||||
|
||||
assert_eq!(Some(&payload), node1.device().pop_outbound().as_ref());
|
||||
assert_clean!(node3);
|
||||
|
||||
assert_clean!(node1, node2, node3);
|
||||
assert_eq!(Some(payload), sim.pop_payload(node1));
|
||||
assert_eq!(None, sim.pop_payload(node3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switch_honours_vlans() {
|
||||
// TODO
|
||||
let config = Config { device_type: Type::Tap, ..Config::default() };
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
let node3 = sim.add_node(false, &config);
|
||||
|
||||
sim.connect(node1, node2);
|
||||
sim.connect(node1, node3);
|
||||
sim.connect(node2, node3);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
assert!(sim.is_connected(node1, node3));
|
||||
assert!(sim.is_connected(node3, node1));
|
||||
assert!(sim.is_connected(node2, node3));
|
||||
assert!(sim.is_connected(node3, node2));
|
||||
|
||||
let payload = vec![2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0x81, 0, 0, 0x67, 1, 2, 3, 4, 5];
|
||||
|
||||
// Nothing learnt so far, node1 broadcasts
|
||||
|
||||
sim.put_payload(node1, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
|
||||
assert_eq!(Some(payload.clone()), sim.pop_payload(node2));
|
||||
assert_eq!(Some(payload), sim.pop_payload(node3));
|
||||
|
||||
let payload = vec![1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0x81, 0, 0, 0x67, 5, 4, 3, 2, 1];
|
||||
|
||||
// Node 2 learned the address by receiving it, does not broadcast
|
||||
|
||||
sim.put_payload(node2, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
|
||||
assert_eq!(Some(payload), sim.pop_payload(node1));
|
||||
assert_eq!(None, sim.pop_payload(node3));
|
||||
|
||||
let payload = vec![1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0x81, 0, 0, 0x68, 5, 4, 3, 2, 1];
|
||||
|
||||
// Different VLANs, node 2 does not learn, still broadcasts
|
||||
|
||||
sim.put_payload(node2, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
|
||||
assert_eq!(Some(payload.clone()), sim.pop_payload(node1));
|
||||
assert_eq!(Some(payload), sim.pop_payload(node3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn switch_forgets() {
|
||||
// TODO
|
||||
// TODO Test
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_delivers() {
|
||||
// TODO
|
||||
let config1 = Config {
|
||||
device_type: Type::Tun,
|
||||
auto_claim: false,
|
||||
claims: vec!["1.1.1.1/32".to_string()],
|
||||
..Config::default()
|
||||
};
|
||||
let config2 = Config {
|
||||
device_type: Type::Tun,
|
||||
auto_claim: false,
|
||||
claims: vec!["2.2.2.2/32".to_string()],
|
||||
..Config::default()
|
||||
};
|
||||
let mut sim = TunSimulator::new();
|
||||
let node1 = sim.add_node(false, &config1);
|
||||
let node2 = sim.add_node(false, &config2);
|
||||
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
|
||||
let payload = vec![0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
|
||||
|
||||
sim.put_payload(node1, payload.clone());
|
||||
sim.simulate_all_messages();
|
||||
|
||||
assert_eq!(Some(payload), sim.pop_payload(node2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_drops_unknown_dest() {
|
||||
// TODO
|
||||
let config1 = Config {
|
||||
device_type: Type::Tun,
|
||||
auto_claim: false,
|
||||
claims: vec!["1.1.1.1/32".to_string()],
|
||||
..Config::default()
|
||||
};
|
||||
let config2 = Config {
|
||||
device_type: Type::Tun,
|
||||
auto_claim: false,
|
||||
claims: vec!["2.2.2.2/32".to_string()],
|
||||
..Config::default()
|
||||
};
|
||||
let mut sim = TunSimulator::new();
|
||||
let node1 = sim.add_node(false, &config1);
|
||||
let node2 = sim.add_node(false, &config2);
|
||||
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
|
||||
let payload = vec![0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 3, 3, 3, 3];
|
||||
|
||||
sim.put_payload(node1, payload);
|
||||
sim.simulate_all_messages();
|
||||
|
||||
assert_eq!(None, sim.pop_payload(node2));
|
||||
}
|
||||
|
|
|
@ -1,213 +1,179 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use super::*;
|
||||
use super::common::*;
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn connect_v4() {
|
||||
let mut node1 = create_tap_node(false);
|
||||
let node1_addr = addr!("1.2.3.4:5678");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.3.4.5:6789");
|
||||
assert_clean!(node1, node2);
|
||||
assert!(!node1.peers().contains_node(&node2.node_id()));
|
||||
assert!(!node2.peers().contains_node(&node1.node_id()));
|
||||
fn direct_connect() {
|
||||
let config = Config::default();
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
node1.connect("2.3.4.5:6789").unwrap();
|
||||
|
||||
// Node 1 -> Node 2: Init 0
|
||||
assert_message4!(node1, node1_addr, node2, node2_addr, Message::Init(0, node1.node_id(), vec![], 600));
|
||||
assert_clean!(node1);
|
||||
assert!(node2.peers().contains_node(&node1.node_id()));
|
||||
|
||||
// Node 2 -> Node 1: Init 1 | Node 2 -> Node 1: Peers
|
||||
assert_message4!(node2, node2_addr, node1, node1_addr, Message::Init(1, node2.node_id(), vec![], 600));
|
||||
assert!(node1.peers().contains_node(&node2.node_id()));
|
||||
assert_message4!(node2, node2_addr, node1, node1_addr, Message::Peers(vec![node1_addr]));
|
||||
assert_clean!(node2);
|
||||
|
||||
// Node 1 -> Node 2: Peers | Node 1 -> Node 1: Init 0
|
||||
assert_message4!(node1, node1_addr, node2, node2_addr, Message::Peers(vec![node2_addr]));
|
||||
assert_message4!(node1, node1_addr, node1, node1_addr, Message::Init(0, node1.node_id(), vec![], 600));
|
||||
assert!(node1.own_addresses().contains(&node1_addr));
|
||||
assert_clean!(node1);
|
||||
|
||||
// Node 2 -> Node 2: Init 0
|
||||
assert_message4!(node2, node2_addr, node2, node2_addr, Message::Init(0, node2.node_id(), vec![], 600));
|
||||
assert_clean!(node2);
|
||||
assert!(node2.own_addresses().contains(&node2_addr));
|
||||
|
||||
assert_connected!(node1, node2);
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_v6() {
|
||||
let mut node1 = create_tap_node(false);
|
||||
let node1_addr = addr!("[::1]:5678");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("[::2]:6789");
|
||||
fn direct_connect_unencrypted() {
|
||||
let config = Config {
|
||||
crypto: CryptoConfig { algorithms: vec!["plain".to_string()], ..CryptoConfig::default() },
|
||||
..Config::default()
|
||||
};
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
node1.connect("[::2]:6789").unwrap();
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
|
||||
assert_connected!(node1, node2);
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn cross_connect() {
|
||||
let mut node1 = create_tap_node(false);
|
||||
let node1_addr = addr!("1.1.1.1:1111");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.2.2.2:2222");
|
||||
let mut node3 = create_tap_node(false);
|
||||
let node3_addr = addr!("3.3.3.3:3333");
|
||||
let mut node4 = create_tap_node(false);
|
||||
let node4_addr = addr!("4.4.4.4:4444");
|
||||
let config = Config::default();
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
let node3 = sim.add_node(false, &config);
|
||||
|
||||
node1.connect("2.2.2.2:2222").unwrap();
|
||||
node3.connect("4.4.4.4:4444").unwrap();
|
||||
sim.connect(node1, node2);
|
||||
sim.connect(node1, node3);
|
||||
sim.simulate_all_messages();
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr, node3 => node3_addr, node4 => node4_addr);
|
||||
sim.simulate_time(120);
|
||||
|
||||
assert_connected!(node1, node2);
|
||||
assert_connected!(node3, node4);
|
||||
|
||||
node1.connect("3.3.3.3:3333").unwrap();
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr, node3 => node3_addr, node4 => node4_addr);
|
||||
|
||||
// existing connections
|
||||
assert_connected!(node1, node2);
|
||||
assert_connected!(node3, node4);
|
||||
|
||||
// new connection
|
||||
assert_connected!(node1, node3);
|
||||
|
||||
// transient connections 1st degree
|
||||
assert_connected!(node1, node4);
|
||||
assert_connected!(node3, node2);
|
||||
|
||||
// transient connections 2nd degree
|
||||
assert_connected!(node2, node4);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
assert!(sim.is_connected(node1, node3));
|
||||
assert!(sim.is_connected(node3, node1));
|
||||
assert!(sim.is_connected(node2, node3));
|
||||
assert!(sim.is_connected(node3, node2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_via_beacons() {
|
||||
MockTimeSource::set_time(0);
|
||||
let mut sim = TapSimulator::new();
|
||||
let beacon_path = "target/.vpncloud_test";
|
||||
let mut node1 =
|
||||
create_tap_node_with_config(false, Config { beacon_store: Some(beacon_path.to_string()), ..Config::default() });
|
||||
let node1_addr = node1.address().unwrap();
|
||||
let mut node2 =
|
||||
create_tap_node_with_config(false, Config { beacon_load: Some(beacon_path.to_string()), ..Config::default() });
|
||||
let node2_addr = addr!("2.2.2.2:2222");
|
||||
let config1 = Config { beacon_store: Some(beacon_path.to_string()), ..Default::default() };
|
||||
let node1 = sim.add_node(false, &config1);
|
||||
let config2 = Config { beacon_load: Some(beacon_path.to_string()), ..Default::default() };
|
||||
let node2 = sim.add_node(false, &config2);
|
||||
|
||||
assert!(!node1.peers().contains_node(&node2.node_id()));
|
||||
assert!(!node2.peers().contains_node(&node1.node_id()));
|
||||
sim.set_time(100);
|
||||
sim.trigger_node_housekeep(node1);
|
||||
sim.trigger_node_housekeep(node2);
|
||||
sim.simulate_all_messages();
|
||||
|
||||
MockTimeSource::set_time(5000);
|
||||
node1.trigger_housekeep();
|
||||
|
||||
MockTimeSource::set_time(10000);
|
||||
node2.trigger_housekeep();
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
|
||||
assert_clean!(node1, node2);
|
||||
assert_connected!(node1, node2);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconnect_after_timeout() {
|
||||
MockTimeSource::set_time(0);
|
||||
let mut node1 = create_tap_node(false);
|
||||
let node1_addr = addr!("1.1.1.1:1111");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.2.2.2:2222");
|
||||
let config = Config::default();
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
node1.add_reconnect_peer("2.2.2.2:2222".to_string());
|
||||
node1.connect(node2_addr).unwrap();
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
sim.set_time(5000);
|
||||
sim.trigger_housekeep();
|
||||
assert!(!sim.is_connected(node1, node2));
|
||||
assert!(!sim.is_connected(node2, node1));
|
||||
|
||||
assert_connected!(node1, node2);
|
||||
|
||||
MockTimeSource::set_time(5000);
|
||||
node1.trigger_housekeep();
|
||||
node2.trigger_housekeep();
|
||||
|
||||
assert!(!node1.peers().contains_node(&node2.node_id()));
|
||||
assert!(!node2.peers().contains_node(&node1.node_id()));
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
|
||||
assert_connected!(node1, node2);
|
||||
sim.simulate_all_messages();
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lost_init1() {
|
||||
let mut node1 = create_tap_node(false);
|
||||
let node1_addr = addr!("1.2.3.4:5678");
|
||||
let mut node2 = create_tap_node(false);
|
||||
let node2_addr = addr!("2.3.4.5:6789");
|
||||
fn lost_init_ping() {
|
||||
let config = Config::default();
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
node1.connect("2.3.4.5:6789").unwrap();
|
||||
sim.connect(node1, node2);
|
||||
sim.drop_message(); // drop init ping
|
||||
|
||||
// Node 1 -> Node 2: Init 0
|
||||
assert_message4!(node1, node1_addr, node2, node2_addr, Message::Init(0, node1.node_id(), vec![], 600));
|
||||
assert_clean!(node1);
|
||||
|
||||
// Node 2 -> Node 1: Init 1 | Node 2 -> Node 1: Peers
|
||||
assert!(node2.socket().pop_outbound().is_some());
|
||||
assert!(!node1.peers().contains_node(&node2.node_id()));
|
||||
|
||||
simulate!(node1 => node1_addr, node2 => node2_addr);
|
||||
|
||||
assert_connected!(node1, node2);
|
||||
sim.simulate_time(120);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_magic() {
|
||||
let mut node1 = create_tap_node(false);
|
||||
let node1_addr = addr!("1.2.3.4:5678");
|
||||
let mut node2 =
|
||||
create_tap_node_with_config(false, Config { magic: Some("hash:different".to_string()), ..Config::default() });
|
||||
let node2_addr = addr!("2.3.4.5:6789");
|
||||
node1.connect("2.3.4.5:6789").unwrap();
|
||||
fn lost_init_pong() {
|
||||
let config = Config::default();
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
assert_message4!(node1, node1_addr, node2, node2_addr, Message::Init(0, node1.node_id(), vec![], 600));
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_next_message(); // init ping
|
||||
sim.drop_message(); // drop init pong
|
||||
|
||||
assert_clean!(node1, node2);
|
||||
|
||||
assert!(!node1.peers().contains_node(&node2.node_id()));
|
||||
assert!(!node2.peers().contains_node(&node1.node_id()));
|
||||
sim.simulate_time(120);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lost_init_peng() {
|
||||
let config = Config::default();
|
||||
let mut sim = TapSimulator::new();
|
||||
let node1 = sim.add_node(false, &config);
|
||||
let node2 = sim.add_node(false, &config);
|
||||
|
||||
sim.connect(node1, node2);
|
||||
sim.simulate_next_message(); // init ping
|
||||
sim.simulate_next_message(); // init pong
|
||||
sim.drop_message(); // drop init peng
|
||||
|
||||
sim.simulate_time(120);
|
||||
assert!(sim.is_connected(node1, node2));
|
||||
assert!(sim.is_connected(node2, node1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn peer_exchange() {
|
||||
// TODO
|
||||
// TODO Test
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn lost_peer_exchange() {
|
||||
// TODO
|
||||
// TODO Test
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn remove_dead_peers() {
|
||||
// TODO
|
||||
// TODO Test
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn update_primary_address() {
|
||||
// TODO
|
||||
// TODO Test
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn automatic_peer_timeout() {
|
||||
// TODO
|
||||
// TODO Test
|
||||
unimplemented!()
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2018-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{self, Write},
|
||||
net::SocketAddr,
|
||||
ops::AddAssign
|
||||
ops::AddAssign,
|
||||
};
|
||||
|
||||
use super::{
|
||||
cloud::{Hash, STATS_INTERVAL},
|
||||
types::Address,
|
||||
util::{addr_nice, Bytes}
|
||||
util::{addr_nice, Bytes},
|
||||
};
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TrafficEntry {
|
||||
pub out_bytes_total: u64,
|
||||
|
@ -26,7 +25,7 @@ pub struct TrafficEntry {
|
|||
pub in_packets_total: usize,
|
||||
pub in_bytes: u64,
|
||||
pub in_packets: usize,
|
||||
pub idle_periods: usize
|
||||
pub idle_periods: usize,
|
||||
}
|
||||
|
||||
impl AddAssign<&TrafficEntry> for TrafficEntry {
|
||||
|
@ -72,33 +71,36 @@ impl TrafficEntry {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TrafficStats {
|
||||
peers: HashMap<SocketAddr, TrafficEntry, Hash>,
|
||||
payload: HashMap<(Address, Address), TrafficEntry, Hash>,
|
||||
pub dropped: TrafficEntry
|
||||
pub dropped: TrafficEntry,
|
||||
}
|
||||
|
||||
impl TrafficStats {
|
||||
#[inline]
|
||||
pub fn count_out_traffic(&mut self, peer: SocketAddr, bytes: usize) {
|
||||
self.peers.entry(peer).or_insert_with(TrafficEntry::default).count_out(bytes);
|
||||
// HOT PATH
|
||||
self.peers.entry(peer).or_default().count_out(bytes);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn count_in_traffic(&mut self, peer: SocketAddr, bytes: usize) {
|
||||
self.peers.entry(peer).or_insert_with(TrafficEntry::default).count_in(bytes);
|
||||
// HOT PATH
|
||||
self.peers.entry(peer).or_default().count_in(bytes);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn count_out_payload(&mut self, remote: Address, local: Address, bytes: usize) {
|
||||
self.payload.entry((remote, local)).or_insert_with(TrafficEntry::default).count_out(bytes);
|
||||
// HOT PATH
|
||||
self.payload.entry((remote, local)).or_default().count_out(bytes);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn count_in_payload(&mut self, remote: Address, local: Address, bytes: usize) {
|
||||
self.payload.entry((remote, local)).or_insert_with(TrafficEntry::default).count_in(bytes);
|
||||
// HOT PATH
|
||||
self.payload.entry((remote, local)).or_default().count_in(bytes);
|
||||
}
|
||||
|
||||
pub fn count_invalid_protocol(&mut self, bytes: usize) {
|
||||
|
|
306
src/types.rs
306
src/types.rs
|
@ -1,64 +1,61 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
util::{bytes_to_hex, Encoder},
|
||||
};
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
io::{self, Write},
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
str::FromStr
|
||||
io::{Read, Write},
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use super::util::{bytes_to_hex, Encoder};
|
||||
|
||||
pub const NODE_ID_BYTES: usize = 16;
|
||||
|
||||
pub type HeaderMagic = [u8; 4];
|
||||
pub type NodeId = [u8; NODE_ID_BYTES];
|
||||
|
||||
|
||||
#[derive(Eq, Clone, Copy)]
|
||||
pub struct Address {
|
||||
pub data: [u8; 16],
|
||||
pub len: u8
|
||||
pub len: u8,
|
||||
}
|
||||
|
||||
impl Address {
|
||||
#[inline]
|
||||
pub fn read_from(data: &[u8]) -> Result<(Address, usize), Error> {
|
||||
if data.is_empty() {
|
||||
return Err(Error::Parse("Address too short"))
|
||||
}
|
||||
let len = data[0] as usize;
|
||||
let addr = Address::read_from_fixed(&data[1..], len)?;
|
||||
Ok((addr, len + 1))
|
||||
pub fn read_from<R: Read>(mut r: R) -> Result<Address, Error> {
|
||||
let len = r.read_u8().map_err(|_| Error::Parse("Address too short"))?;
|
||||
Address::read_from_fixed(r, len)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read_from_fixed(data: &[u8], len: usize) -> Result<Address, Error> {
|
||||
pub fn read_from_fixed<R: Read>(mut r: R, len: u8) -> Result<Address, Error> {
|
||||
if len > 16 {
|
||||
return Err(Error::Parse("Invalid address, too long"))
|
||||
return Err(Error::Parse("Invalid address, too long"));
|
||||
}
|
||||
if data.len() < len {
|
||||
return Err(Error::Parse("Address too short"))
|
||||
}
|
||||
let mut bytes = [0; 16];
|
||||
bytes[0..len].copy_from_slice(&data[0..len]);
|
||||
Ok(Address { data: bytes, len: len as u8 })
|
||||
let mut data = [0; 16];
|
||||
r.read_exact(&mut data[..len as usize]).map_err(|_| Error::Parse("Address too short"))?;
|
||||
Ok(Address { data, len })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_to(&self, data: &mut [u8]) -> usize {
|
||||
assert!(data.len() > self.len as usize);
|
||||
data[0] = self.len;
|
||||
let len = self.len as usize;
|
||||
data[1..=len].copy_from_slice(&self.data[0..len]);
|
||||
self.len as usize + 1
|
||||
pub fn write_to<W: Write>(&self, mut w: W) {
|
||||
w.write_u8(self.len).expect("Buffer too small");
|
||||
w.write_all(&self.data[..self.len as usize]).expect("Buffer too small");
|
||||
}
|
||||
|
||||
pub fn from_ipv4(ip: Ipv4Addr) -> Self {
|
||||
let mut data = [0; 16];
|
||||
data[0..4].copy_from_slice(&ip.octets());
|
||||
Self { data, len: 4 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl PartialEq for Address {
|
||||
#[inline]
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
|
@ -66,15 +63,13 @@ impl PartialEq for Address {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
impl Hash for Address {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||
hasher.write(&self.data[0..self.len as usize])
|
||||
hasher.write(&self.data[..self.len as usize])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl fmt::Display for Address {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
let d = &self.data;
|
||||
|
@ -89,7 +84,7 @@ impl fmt::Display for Address {
|
|||
},
|
||||
16 => write!(formatter, "{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}",
|
||||
d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15]),
|
||||
_ => write!(formatter, "{}", bytes_to_hex(&d[0..self.len as usize]))
|
||||
_ => write!(formatter, "{}", bytes_to_hex(&d[..self.len as usize]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +104,7 @@ impl FromStr for Address {
|
|||
let ip = addr.octets();
|
||||
let mut res = [0; 16];
|
||||
res[0..4].copy_from_slice(&ip);
|
||||
return Ok(Address { data: res, len: 4 })
|
||||
return Ok(Address { data: res, len: 4 });
|
||||
}
|
||||
if let Ok(addr) = Ipv6Addr::from_str(text) {
|
||||
let segments = addr.segments();
|
||||
|
@ -117,44 +112,55 @@ impl FromStr for Address {
|
|||
for i in 0..8 {
|
||||
Encoder::write_u16(segments[i], &mut res[2 * i..]);
|
||||
}
|
||||
return Ok(Address { data: res, len: 16 })
|
||||
return Ok(Address { data: res, len: 16 });
|
||||
}
|
||||
let parts: Vec<&str> = text.split(':').collect();
|
||||
let parts: SmallVec<[&str; 10]> = text.split(':').collect();
|
||||
if parts.len() == 6 {
|
||||
let mut bytes = [0; 16];
|
||||
for i in 0..6 {
|
||||
bytes[i] = u8::from_str_radix(parts[i], 16).map_err(|_| Error::Parse("Failed to parse mac"))?;
|
||||
}
|
||||
return Ok(Address { data: bytes, len: 6 })
|
||||
return Ok(Address { data: bytes, len: 6 });
|
||||
}
|
||||
Err(Error::Parse("Failed to parse address"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub struct Range {
|
||||
pub base: Address,
|
||||
pub prefix_len: u8
|
||||
pub prefix_len: u8,
|
||||
}
|
||||
|
||||
pub type RangeList = SmallVec<[Range; 4]>;
|
||||
|
||||
impl Range {
|
||||
#[inline]
|
||||
pub fn read_from(data: &[u8]) -> Result<(Range, usize), Error> {
|
||||
let (address, read) = Address::read_from(data)?;
|
||||
if data.len() < read + 1 {
|
||||
return Err(Error::Parse("Range too short"))
|
||||
pub fn matches(&self, addr: Address) -> bool {
|
||||
if self.base.len != addr.len {
|
||||
return false;
|
||||
}
|
||||
let prefix_len = data[read];
|
||||
Ok((Range { base: address, prefix_len }, read + 1))
|
||||
let mut match_len = 0;
|
||||
for i in 0..addr.len as usize {
|
||||
let m = addr.data[i] ^ self.base.data[i];
|
||||
match_len += m.leading_zeros() as u8;
|
||||
if m != 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match_len >= self.prefix_len
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_to(&self, data: &mut [u8]) -> usize {
|
||||
let pos = self.base.write_to(data);
|
||||
assert!(data.len() > pos);
|
||||
data[pos] = self.prefix_len;
|
||||
pos + 1
|
||||
pub fn read_from<R: Read>(mut r: R) -> Result<Range, Error> {
|
||||
let base = Address::read_from(&mut r)?;
|
||||
let prefix_len = r.read_u8().map_err(|_| Error::Parse("Address too short"))?;
|
||||
Ok(Range { base, prefix_len })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_to<W: Write>(&self, mut w: W) {
|
||||
self.base.write_to(&mut w);
|
||||
w.write_u8(self.prefix_len).expect("Buffer too small")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,7 +170,7 @@ impl FromStr for Range {
|
|||
fn from_str(text: &str) -> Result<Self, Self::Err> {
|
||||
let pos = match text.find('/') {
|
||||
Some(pos) => pos,
|
||||
None => return Err(Error::Parse("Invalid range format"))
|
||||
None => return Err(Error::Parse("Invalid range format")),
|
||||
};
|
||||
let prefix_len = u8::from_str(&text[pos + 1..]).map_err(|_| Error::Parse("Failed to parse prefix length"))?;
|
||||
let base = Address::from_str(&text[..pos])?;
|
||||
|
@ -184,7 +190,6 @@ impl fmt::Debug for Range {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Mode {
|
||||
#[serde(rename = "normal")]
|
||||
|
@ -194,7 +199,7 @@ pub enum Mode {
|
|||
#[serde(rename = "switch")]
|
||||
Switch,
|
||||
#[serde(rename = "router")]
|
||||
Router
|
||||
Router,
|
||||
}
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
|
@ -202,7 +207,7 @@ impl fmt::Display for Mode {
|
|||
Mode::Normal => write!(formatter, "normal"),
|
||||
Mode::Hub => write!(formatter, "hub"),
|
||||
Mode::Switch => write!(formatter, "switch"),
|
||||
Mode::Router => write!(formatter, "router")
|
||||
Mode::Router => write!(formatter, "router"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -215,120 +220,85 @@ impl FromStr for Mode {
|
|||
"hub" => Self::Hub,
|
||||
"switch" => Self::Switch,
|
||||
"router" => Self::Router,
|
||||
_ => return Err("Unknown mode")
|
||||
_ => return Err("Unknown mode"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Table {
|
||||
fn learn(&mut self, _: Address, _: Option<u8>, _: SocketAddr);
|
||||
fn lookup(&mut self, _: &Address) -> Option<SocketAddr>;
|
||||
fn housekeep(&mut self);
|
||||
fn write_out<W: Write>(&self, out: &mut W) -> Result<(), io::Error>;
|
||||
fn remove(&mut self, _: &Address) -> bool;
|
||||
fn remove_all(&mut self, _: &SocketAddr);
|
||||
fn len(&self) -> usize;
|
||||
fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn address_parse_fmt() {
|
||||
assert_eq!(format!("{}", Address::from_str("120.45.22.5").unwrap()), "120.45.22.5");
|
||||
assert_eq!(format!("{}", Address::from_str("78:2d:16:05:01:02").unwrap()), "78:2d:16:05:01:02");
|
||||
assert_eq!(
|
||||
format!("{}", Address { data: [3, 56, 120, 45, 22, 5, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0], len: 8 }),
|
||||
"vlan824/78:2d:16:05:01:02"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Address::from_str("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f").unwrap()),
|
||||
"0001:0203:0405:0607:0809:0a0b:0c0d:0e0f"
|
||||
);
|
||||
assert_eq!(format!("{:?}", Address { data: [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 2 }), "0102");
|
||||
assert!(Address::from_str("").is_err()); // Failed to parse address
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_decode_encode() {
|
||||
let mut buf = vec![];
|
||||
let addr = Address::from_str("120.45.22.5").unwrap();
|
||||
addr.write_to(Cursor::new(&mut buf));
|
||||
assert_eq!(&buf[0..5], &[4, 120, 45, 22, 5]);
|
||||
assert_eq!(addr, Address::read_from(Cursor::new(&buf)).unwrap());
|
||||
assert_eq!(addr, Address::read_from_fixed(Cursor::new(&buf[1..]), 4).unwrap());
|
||||
buf.clear();
|
||||
let addr = Address::from_str("78:2d:16:05:01:02").unwrap();
|
||||
addr.write_to(Cursor::new(&mut buf));
|
||||
assert_eq!(&buf[0..7], &[6, 0x78, 0x2d, 0x16, 0x05, 0x01, 0x02]);
|
||||
assert_eq!(addr, Address::read_from(Cursor::new(&buf)).unwrap());
|
||||
assert_eq!(addr, Address::read_from_fixed(Cursor::new(&buf[1..]), 6).unwrap());
|
||||
assert!(Address::read_from(Cursor::new(&buf[0..1])).is_err()); // Address too short
|
||||
buf[0] = 100;
|
||||
assert!(Address::read_from(Cursor::new(&buf)).is_err()); // Invalid address, too long
|
||||
buf[0] = 5;
|
||||
assert!(Address::read_from(Cursor::new(&buf[0..4])).is_err()); // Address too short
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_eq() {
|
||||
assert_eq!(
|
||||
Address::read_from_fixed(Cursor::new(&[1, 2, 3, 4]), 4).unwrap(),
|
||||
Address::read_from_fixed(Cursor::new(&[1, 2, 3, 4]), 4).unwrap()
|
||||
);
|
||||
assert_ne!(
|
||||
Address::read_from_fixed(Cursor::new(&[1, 2, 3, 4]), 4).unwrap(),
|
||||
Address::read_from_fixed(Cursor::new(&[1, 2, 3, 5]), 4).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Address::read_from_fixed(Cursor::new(&[1, 2, 3, 4]), 3).unwrap(),
|
||||
Address::read_from_fixed(Cursor::new(&[1, 2, 3, 5]), 3).unwrap()
|
||||
);
|
||||
assert_ne!(
|
||||
Address::read_from_fixed(Cursor::new(&[1, 2, 3, 4]), 3).unwrap(),
|
||||
Address::read_from_fixed(Cursor::new(&[1, 2, 3, 4]), 4).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_range_decode_encode() {
|
||||
let mut buf = vec![];
|
||||
let range =
|
||||
Range { base: Address { data: [0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 4 }, prefix_len: 24 };
|
||||
range.write_to(Cursor::new(&mut buf));
|
||||
assert_eq!(&buf[0..6], &[4, 0, 1, 2, 3, 24]);
|
||||
assert_eq!(range, Range::read_from(Cursor::new(&buf)).unwrap());
|
||||
assert!(Range::read_from(Cursor::new(&buf[..5])).is_err()); // Missing prefix length
|
||||
buf[0] = 17;
|
||||
assert!(Range::read_from(Cursor::new(&buf)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Protocol: Sized {
|
||||
fn parse(_: &[u8]) -> Result<(Address, Address), Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Parse(&'static str),
|
||||
WrongHeaderMagic(HeaderMagic),
|
||||
Socket(&'static str, io::Error),
|
||||
Name(String),
|
||||
TunTapDev(&'static str, io::Error),
|
||||
Crypto(&'static str),
|
||||
File(&'static str, io::Error),
|
||||
Beacon(&'static str, io::Error)
|
||||
}
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Error::Parse(msg) => write!(formatter, "{}", msg),
|
||||
Error::Socket(msg, ref err) => write!(formatter, "{}: {:?}", msg, err),
|
||||
Error::TunTapDev(msg, ref err) => write!(formatter, "{}: {:?}", msg, err),
|
||||
Error::Crypto(msg) => write!(formatter, "{}", msg),
|
||||
Error::Name(ref name) => write!(formatter, "failed to resolve name '{}'", name),
|
||||
Error::WrongHeaderMagic(net) => write!(formatter, "wrong header magic: {}", bytes_to_hex(&net)),
|
||||
Error::File(msg, ref err) => write!(formatter, "{}: {:?}", msg, err),
|
||||
Error::Beacon(msg, ref err) => write!(formatter, "{}: {:?}", msg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn address_parse_fmt() {
|
||||
assert_eq!(format!("{}", Address::from_str("120.45.22.5").unwrap()), "120.45.22.5");
|
||||
assert_eq!(format!("{}", Address::from_str("78:2d:16:05:01:02").unwrap()), "78:2d:16:05:01:02");
|
||||
assert_eq!(
|
||||
format!("{}", Address { data: [3, 56, 120, 45, 22, 5, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0], len: 8 }),
|
||||
"vlan824/78:2d:16:05:01:02"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Address::from_str("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f").unwrap()),
|
||||
"0001:0203:0405:0607:0809:0a0b:0c0d:0e0f"
|
||||
);
|
||||
assert_eq!(format!("{:?}", Address { data: [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 2 }), "0102");
|
||||
assert!(Address::from_str("").is_err()); // Failed to parse address
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_decode_encode() {
|
||||
let mut buf = [0; 32];
|
||||
let addr = Address::from_str("120.45.22.5").unwrap();
|
||||
assert_eq!(addr.write_to(&mut buf), 5);
|
||||
assert_eq!(&buf[0..5], &[4, 120, 45, 22, 5]);
|
||||
assert_eq!((addr, 5), Address::read_from(&buf).unwrap());
|
||||
assert_eq!(addr, Address::read_from_fixed(&buf[1..], 4).unwrap());
|
||||
let addr = Address::from_str("78:2d:16:05:01:02").unwrap();
|
||||
assert_eq!(addr.write_to(&mut buf), 7);
|
||||
assert_eq!(&buf[0..7], &[6, 0x78, 0x2d, 0x16, 0x05, 0x01, 0x02]);
|
||||
assert_eq!((addr, 7), Address::read_from(&buf).unwrap());
|
||||
assert_eq!(addr, Address::read_from_fixed(&buf[1..], 6).unwrap());
|
||||
assert!(Address::read_from(&buf[0..0]).is_err()); // Address too short
|
||||
buf[0] = 100;
|
||||
assert!(Address::read_from(&buf).is_err()); // Invalid address, too long
|
||||
buf[0] = 5;
|
||||
assert!(Address::read_from(&buf[0..4]).is_err()); // Address too short
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_eq() {
|
||||
assert_eq!(
|
||||
Address::read_from_fixed(&[1, 2, 3, 4], 4).unwrap(),
|
||||
Address::read_from_fixed(&[1, 2, 3, 4], 4).unwrap()
|
||||
);
|
||||
assert_ne!(
|
||||
Address::read_from_fixed(&[1, 2, 3, 4], 4).unwrap(),
|
||||
Address::read_from_fixed(&[1, 2, 3, 5], 4).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Address::read_from_fixed(&[1, 2, 3, 4], 3).unwrap(),
|
||||
Address::read_from_fixed(&[1, 2, 3, 5], 3).unwrap()
|
||||
);
|
||||
assert_ne!(
|
||||
Address::read_from_fixed(&[1, 2, 3, 4], 3).unwrap(),
|
||||
Address::read_from_fixed(&[1, 2, 3, 4], 4).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_range_decode_encode() {
|
||||
let mut buf = [0; 32];
|
||||
let range =
|
||||
Range { base: Address { data: [0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 4 }, prefix_len: 24 };
|
||||
assert_eq!(range.write_to(&mut buf), 6);
|
||||
assert_eq!(&buf[0..6], &[4, 0, 1, 2, 3, 24]);
|
||||
assert_eq!((range, 6), Range::read_from(&buf).unwrap());
|
||||
assert!(Range::read_from(&buf[..5]).is_err()); // Missing prefix length
|
||||
buf[0] = 17;
|
||||
assert!(Range::read_from(&buf).is_err());
|
||||
}
|
||||
|
|
|
@ -1,516 +0,0 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}
|
||||
};
|
||||
|
||||
use super::{
|
||||
config::DEFAULT_PEER_TIMEOUT,
|
||||
crypto::Crypto,
|
||||
types::{Error, HeaderMagic, NodeId, Range, NODE_ID_BYTES},
|
||||
util::{bytes_to_hex, Encoder}
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
#[repr(packed)]
|
||||
struct TopHeader {
|
||||
magic: HeaderMagic,
|
||||
crypto_method: u8,
|
||||
_reserved1: u8,
|
||||
_reserved2: u8,
|
||||
msgtype: u8
|
||||
}
|
||||
|
||||
impl TopHeader {
|
||||
#[inline]
|
||||
pub fn size() -> usize {
|
||||
8
|
||||
}
|
||||
|
||||
pub fn read_from(data: &[u8]) -> Result<(TopHeader, usize), Error> {
|
||||
if data.len() < TopHeader::size() {
|
||||
return Err(Error::Parse("Empty message"))
|
||||
}
|
||||
let mut header = TopHeader::default();
|
||||
header.magic.copy_from_slice(&data[0..4]);
|
||||
header.crypto_method = data[4];
|
||||
header.msgtype = data[7];
|
||||
Ok((header, TopHeader::size()))
|
||||
}
|
||||
|
||||
#[allow(unknown_lints, clippy::trivially_copy_pass_by_ref)]
|
||||
pub fn write_to(&self, data: &mut [u8]) -> usize {
|
||||
assert!(data.len() >= 8);
|
||||
data[0..4].copy_from_slice(&self.magic);
|
||||
data[4] = self.crypto_method;
|
||||
data[5] = 0;
|
||||
data[6] = 0;
|
||||
data[7] = self.msgtype;
|
||||
TopHeader::size()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message<'a> {
|
||||
Data(&'a mut [u8], usize, usize), // data, start, end
|
||||
Peers(Vec<SocketAddr>), // peers
|
||||
Init(u8, NodeId, Vec<Range>, u16), // step, node_id, ranges
|
||||
Close
|
||||
}
|
||||
|
||||
impl<'a> Message<'a> {
|
||||
pub fn without_data(self) -> Message<'static> {
|
||||
match self {
|
||||
Message::Data(_, start, end) => Message::Data(&mut [], start, end),
|
||||
Message::Peers(peers) => Message::Peers(peers),
|
||||
Message::Init(step, node_id, ranges, timeout) => Message::Init(step, node_id, ranges, timeout),
|
||||
Message::Close => Message::Close
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for Message<'a> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Message::Data(_, start, end) => write!(formatter, "Data({} bytes)", end - start),
|
||||
Message::Peers(ref peers) => {
|
||||
write!(formatter, "Peers [")?;
|
||||
let mut first = true;
|
||||
for p in peers {
|
||||
if !first {
|
||||
write!(formatter, ", ")?;
|
||||
}
|
||||
first = false;
|
||||
write!(formatter, "{}", p)?;
|
||||
}
|
||||
write!(formatter, "]")
|
||||
}
|
||||
Message::Init(stage, ref node_id, ref peers, ref peer_timeout) => {
|
||||
write!(
|
||||
formatter,
|
||||
"Init(stage={}, node_id={}, peer_timeout={}, {:?})",
|
||||
stage,
|
||||
bytes_to_hex(node_id),
|
||||
peer_timeout,
|
||||
peers
|
||||
)
|
||||
}
|
||||
Message::Close => write!(formatter, "Close")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unknown_lints, clippy::needless_range_loop)]
|
||||
pub fn decode<'a>(data: &'a mut [u8], magic: HeaderMagic, crypto: &Crypto) -> Result<Message<'a>, Error> {
|
||||
let mut end = data.len();
|
||||
let (header, mut pos) = TopHeader::read_from(&data[..end])?;
|
||||
if header.magic != magic {
|
||||
return Err(Error::WrongHeaderMagic(header.magic))
|
||||
}
|
||||
if header.crypto_method != crypto.method() {
|
||||
return Err(Error::Crypto("Wrong crypto method"))
|
||||
}
|
||||
if crypto.method() > 0 {
|
||||
let len = crypto.nonce_bytes();
|
||||
if end < pos + len {
|
||||
return Err(Error::Parse("Truncated crypto header"))
|
||||
}
|
||||
{
|
||||
let (before, after) = data.split_at_mut(pos);
|
||||
let (nonce, crypto_data) = after.split_at_mut(len);
|
||||
pos += len;
|
||||
end = crypto.decrypt(crypto_data, nonce, &before[..TopHeader::size()])? + pos;
|
||||
}
|
||||
assert_eq!(end, data.len() - crypto.additional_bytes());
|
||||
}
|
||||
let msg = match header.msgtype {
|
||||
0 => Message::Data(data, pos, end),
|
||||
1 => {
|
||||
if end < pos + 1 {
|
||||
return Err(Error::Parse("Missing IPv4 count"))
|
||||
}
|
||||
let mut peers = Vec::new();
|
||||
let count = data[pos];
|
||||
pos += 1;
|
||||
let len = count as usize * 6;
|
||||
if end < pos + len {
|
||||
return Err(Error::Parse("IPv4 peer data too short"))
|
||||
}
|
||||
for _ in 0..count {
|
||||
let ip = &data[pos..];
|
||||
assert!(ip.len() >= 4);
|
||||
pos += 4;
|
||||
let port = Encoder::read_u16(&data[pos..]);
|
||||
pos += 2;
|
||||
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), port));
|
||||
peers.push(addr);
|
||||
}
|
||||
if end < pos + 1 {
|
||||
return Err(Error::Parse("Missing IPv6 count"))
|
||||
}
|
||||
let count = data[pos];
|
||||
pos += 1;
|
||||
let len = count as usize * 18;
|
||||
if end < pos + len {
|
||||
return Err(Error::Parse("IPv6 peer data too short"))
|
||||
}
|
||||
for _ in 0..count {
|
||||
let mut ip = [0u16; 8];
|
||||
for i in 0..8 {
|
||||
ip[i] = Encoder::read_u16(&data[pos..]);
|
||||
pos += 2;
|
||||
}
|
||||
let port = Encoder::read_u16(&data[pos..]);
|
||||
pos += 2;
|
||||
let addr = SocketAddr::V6(SocketAddrV6::new(
|
||||
Ipv6Addr::new(ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]),
|
||||
port,
|
||||
0,
|
||||
0
|
||||
));
|
||||
peers.push(addr);
|
||||
}
|
||||
Message::Peers(peers)
|
||||
}
|
||||
2 => {
|
||||
if end < pos + 2 + NODE_ID_BYTES {
|
||||
return Err(Error::Parse("Init data too short"))
|
||||
}
|
||||
let stage = data[pos];
|
||||
pos += 1;
|
||||
let mut node_id = [0; NODE_ID_BYTES];
|
||||
node_id.copy_from_slice(&data[pos..pos + NODE_ID_BYTES]);
|
||||
pos += NODE_ID_BYTES;
|
||||
let count = data[pos] as usize;
|
||||
pos += 1;
|
||||
let mut addrs = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let (range, read) = Range::read_from(&data[pos..end])?;
|
||||
pos += read;
|
||||
addrs.push(range);
|
||||
}
|
||||
let peer_timeout =
|
||||
if data.len() >= pos + 2 { Encoder::read_u16(&data[pos..]) } else { DEFAULT_PEER_TIMEOUT };
|
||||
Message::Init(stage, node_id, addrs, peer_timeout)
|
||||
}
|
||||
3 => Message::Close,
|
||||
_ => return Err(Error::Parse("Unknown message type"))
|
||||
};
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
#[allow(unknown_lints, clippy::needless_range_loop)]
|
||||
pub fn encode<'a>(
|
||||
msg: &'a mut Message, mut buf: &'a mut [u8], magic: HeaderMagic, crypto: &mut Crypto
|
||||
) -> &'a mut [u8] {
|
||||
let header_type = match msg {
|
||||
Message::Data(_, _, _) => 0,
|
||||
Message::Peers(_) => 1,
|
||||
Message::Init(_, _, _, _) => 2,
|
||||
Message::Close => 3
|
||||
};
|
||||
let mut start = 64;
|
||||
let mut end = 64;
|
||||
match *msg {
|
||||
Message::Data(ref mut data, data_start, data_end) => {
|
||||
buf = data;
|
||||
start = data_start;
|
||||
end = data_end;
|
||||
}
|
||||
Message::Peers(ref peers) => {
|
||||
let mut v4addrs = Vec::new();
|
||||
let mut v6addrs = Vec::new();
|
||||
for p in peers {
|
||||
match *p {
|
||||
SocketAddr::V4(addr) => v4addrs.push(addr),
|
||||
SocketAddr::V6(addr) => v6addrs.push(addr)
|
||||
}
|
||||
}
|
||||
assert!(v4addrs.len() <= 255);
|
||||
assert!(v6addrs.len() <= 255);
|
||||
let mut pos = start;
|
||||
assert!(buf.len() >= pos + 2 + v4addrs.len() * 6 + v6addrs.len() * 18);
|
||||
buf[pos] = v4addrs.len() as u8;
|
||||
pos += 1;
|
||||
for addr in v4addrs {
|
||||
let ip = addr.ip().octets();
|
||||
buf[pos..pos + 4].copy_from_slice(&ip);
|
||||
pos += 4;
|
||||
Encoder::write_u16(addr.port(), &mut buf[pos..]);
|
||||
pos += 2;
|
||||
}
|
||||
buf[pos] = v6addrs.len() as u8;
|
||||
pos += 1;
|
||||
for addr in v6addrs {
|
||||
let ip = addr.ip().segments();
|
||||
for i in 0..8 {
|
||||
Encoder::write_u16(ip[i], &mut buf[pos..]);
|
||||
pos += 2;
|
||||
}
|
||||
Encoder::write_u16(addr.port(), &mut buf[pos..]);
|
||||
pos += 2;
|
||||
}
|
||||
end = pos;
|
||||
}
|
||||
Message::Init(stage, ref node_id, ref ranges, peer_timeout) => {
|
||||
let mut pos = start;
|
||||
assert!(buf.len() >= pos + 2 + NODE_ID_BYTES);
|
||||
buf[pos] = stage;
|
||||
pos += 1;
|
||||
buf[pos..pos + NODE_ID_BYTES].copy_from_slice(node_id);
|
||||
pos += NODE_ID_BYTES;
|
||||
assert!(ranges.len() <= 255);
|
||||
buf[pos] = ranges.len() as u8;
|
||||
pos += 1;
|
||||
for range in ranges {
|
||||
pos += range.write_to(&mut buf[pos..]);
|
||||
}
|
||||
Encoder::write_u16(peer_timeout, &mut buf[pos..]);
|
||||
pos += 2;
|
||||
end = pos;
|
||||
}
|
||||
Message::Close => {}
|
||||
}
|
||||
assert!(start >= 64);
|
||||
assert!(buf.len() >= end + 64);
|
||||
let crypto_start = start;
|
||||
start -= crypto.nonce_bytes();
|
||||
let mut header = TopHeader::default();
|
||||
header.magic = magic;
|
||||
header.msgtype = header_type;
|
||||
header.crypto_method = crypto.method();
|
||||
start -= TopHeader::size();
|
||||
header.write_to(&mut buf[start..]);
|
||||
if crypto.method() > 0 {
|
||||
let (junk_before, rest) = buf.split_at_mut(start);
|
||||
let (header, rest) = rest.split_at_mut(TopHeader::size());
|
||||
let (nonce, rest) = rest.split_at_mut(crypto.nonce_bytes());
|
||||
debug_assert_eq!(junk_before.len() + header.len() + crypto.nonce_bytes(), crypto_start);
|
||||
assert!(rest.len() >= end - crypto_start + crypto.additional_bytes());
|
||||
end = crypto.encrypt(rest, end - crypto_start, nonce, header) + crypto_start;
|
||||
}
|
||||
&mut buf[start..end]
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for Message<'a> {
|
||||
fn eq(&self, other: &Message) -> bool {
|
||||
match *self {
|
||||
Message::Data(ref data1, start1, end1) => {
|
||||
if let Message::Data(ref data2, start2, end2) = *other {
|
||||
data1[start1..end1] == data2[start2..end2]
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Message::Peers(ref peers1) => {
|
||||
if let Message::Peers(ref peers2) = *other {
|
||||
peers1 == peers2
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Message::Init(step1, node_id1, ref ranges1, peer_timeout1) => {
|
||||
if let Message::Init(step2, node_id2, ref ranges2, peer_timeout2) = *other {
|
||||
step1 == step2 && node_id1 == node_id2 && ranges1 == ranges2 && peer_timeout1 == peer_timeout2
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Message::Close => {
|
||||
if let Message::Close = *other {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)] use super::crypto::CryptoMethod;
|
||||
#[cfg(test)] use super::types::Address;
|
||||
#[cfg(test)] use super::MAGIC;
|
||||
#[cfg(test)] use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
#[allow(unused_assignments)]
|
||||
fn udpmessage_packet() {
|
||||
let mut crypto = Crypto::None;
|
||||
let mut payload = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
];
|
||||
let mut msg = Message::Data(&mut payload, 64, 69);
|
||||
let mut buf = [0; 1024];
|
||||
let mut len = 0;
|
||||
{
|
||||
let res = encode(&mut msg, &mut [], MAGIC, &mut crypto);
|
||||
assert_eq!(res.len(), 13);
|
||||
assert_eq!(&res[..8], &[118, 112, 110, 1, 0, 0, 0, 0]);
|
||||
buf[..res.len()].clone_from_slice(&res);
|
||||
len = res.len();
|
||||
}
|
||||
let msg2 = decode(&mut buf[..len], MAGIC, &crypto).unwrap();
|
||||
assert_eq!(msg, msg2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(unused_assignments)]
|
||||
fn udpmessage_encrypted() {
|
||||
let mut crypto = Crypto::from_shared_key(CryptoMethod::ChaCha20, "test");
|
||||
let mut payload = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
];
|
||||
let mut orig_payload = [0; 133];
|
||||
orig_payload[..payload.len()].clone_from_slice(&payload);
|
||||
let orig_msg = Message::Data(&mut orig_payload, 64, 69);
|
||||
let mut msg = Message::Data(&mut payload, 64, 69);
|
||||
let mut buf = [0; 1024];
|
||||
let mut len = 0;
|
||||
{
|
||||
let res = encode(&mut msg, &mut [], MAGIC, &mut crypto);
|
||||
assert_eq!(res.len(), 41);
|
||||
assert_eq!(&res[..8], &[118, 112, 110, 1, 1, 0, 0, 0]);
|
||||
buf[..res.len()].clone_from_slice(&res);
|
||||
len = res.len();
|
||||
}
|
||||
let msg2 = decode(&mut buf[..len], MAGIC, &crypto).unwrap();
|
||||
assert_eq!(orig_msg, msg2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udpmessage_peers() {
|
||||
use std::str::FromStr;
|
||||
let mut crypto = Crypto::None;
|
||||
let mut msg = Message::Peers(vec![
|
||||
SocketAddr::from_str("1.2.3.4:123").unwrap(),
|
||||
SocketAddr::from_str("5.6.7.8:12345").unwrap(),
|
||||
SocketAddr::from_str("[0001:0203:0405:0607:0809:0a0b:0c0d:0e0f]:6789").unwrap(),
|
||||
]);
|
||||
let mut should = [
|
||||
118, 112, 110, 1, 0, 0, 0, 1, 2, 1, 2, 3, 4, 0, 123, 5, 6, 7, 8, 48, 57, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 26, 133
|
||||
];
|
||||
{
|
||||
let mut buf = [0; 1024];
|
||||
let res = encode(&mut msg, &mut buf[..], MAGIC, &mut crypto);
|
||||
assert_eq!(res.len(), 40);
|
||||
for i in 0..res.len() {
|
||||
assert_eq!(res[i], should[i]);
|
||||
}
|
||||
}
|
||||
let msg2 = decode(&mut should, MAGIC, &crypto).unwrap();
|
||||
assert_eq!(msg, msg2);
|
||||
// Missing IPv4 count
|
||||
assert!(decode(&mut [118, 112, 110, 1, 0, 0, 0, 1], MAGIC, &crypto).is_err());
|
||||
// Truncated IPv4
|
||||
assert!(decode(&mut [118, 112, 110, 1, 0, 0, 0, 1, 1], MAGIC, &crypto).is_err());
|
||||
// Missing IPv6 count
|
||||
assert!(decode(&mut [118, 112, 110, 1, 0, 0, 0, 1, 1, 1, 2, 3, 4, 0, 0], MAGIC, &crypto).is_err());
|
||||
// Truncated IPv6
|
||||
assert!(decode(&mut [118, 112, 110, 1, 0, 0, 0, 1, 1, 1, 2, 3, 4, 0, 0, 1], MAGIC, &crypto).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udpmessage_init() {
|
||||
use super::types::Address;
|
||||
let mut crypto = Crypto::None;
|
||||
let addrs = vec![
|
||||
Range { base: Address { data: [0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 4 }, prefix_len: 24 },
|
||||
Range { base: Address { data: [0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 6 }, prefix_len: 16 },
|
||||
];
|
||||
let node_id = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
let mut msg = Message::Init(0, node_id, addrs, 1800);
|
||||
let mut should = [
|
||||
118, 112, 110, 1, 0, 0, 0, 2, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 2, 4, 0, 1, 2, 3, 24, 6,
|
||||
0, 1, 2, 3, 4, 5, 16, 7, 8
|
||||
];
|
||||
{
|
||||
let mut buf = [0; 1024];
|
||||
let res = encode(&mut msg, &mut buf[..], MAGIC, &mut crypto);
|
||||
assert_eq!(res.len(), 42);
|
||||
for i in 0..res.len() {
|
||||
assert_eq!(res[i], should[i]);
|
||||
}
|
||||
}
|
||||
let msg2 = decode(&mut should, MAGIC, &crypto).unwrap();
|
||||
assert_eq!(msg, msg2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udpmessage_close() {
|
||||
let mut crypto = Crypto::None;
|
||||
let mut msg = Message::Close;
|
||||
let mut should = [118, 112, 110, 1, 0, 0, 0, 3];
|
||||
{
|
||||
let mut buf = [0; 1024];
|
||||
let res = encode(&mut msg, &mut buf[..], MAGIC, &mut crypto);
|
||||
assert_eq!(res.len(), 8);
|
||||
assert_eq!(&res, &should);
|
||||
}
|
||||
let msg2 = decode(&mut should, MAGIC, &crypto).unwrap();
|
||||
assert_eq!(msg, msg2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udpmessage_invalid() {
|
||||
let crypto = Crypto::None;
|
||||
assert!(decode(&mut [0x76, 0x70, 0x6e, 1, 0, 0, 0, 0], MAGIC, &crypto).is_ok());
|
||||
// too short
|
||||
assert!(decode(&mut [], MAGIC, &crypto).is_err());
|
||||
// invalid protocol
|
||||
assert!(decode(&mut [0, 1, 2, 0, 0, 0, 0, 0], MAGIC, &crypto).is_err());
|
||||
// invalid version
|
||||
assert!(decode(&mut [0x76, 0x70, 0x6e, 0xaa, 0, 0, 0, 0], MAGIC, &crypto).is_err());
|
||||
// invalid crypto
|
||||
assert!(decode(&mut [0x76, 0x70, 0x6e, 1, 0xaa, 0, 0, 0], MAGIC, &crypto).is_err());
|
||||
// invalid msg type
|
||||
assert!(decode(&mut [0x76, 0x70, 0x6e, 1, 0, 0, 0, 0xaa], MAGIC, &crypto).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udpmessage_invalid_crypto() {
|
||||
let crypto = Crypto::from_shared_key(CryptoMethod::ChaCha20, "test");
|
||||
// truncated crypto
|
||||
assert!(decode(&mut [0x76, 0x70, 0x6e, 1, 1, 0, 0, 0], MAGIC, &crypto).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_fmt() {
|
||||
assert_eq!(format!("{:?}", Message::Data(&mut [1, 2, 3, 4, 5], 0, 5)), "Data(5 bytes)");
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{:?}",
|
||||
Message::Peers(vec![
|
||||
SocketAddr::from_str("1.2.3.4:123").unwrap(),
|
||||
SocketAddr::from_str("5.6.7.8:12345").unwrap(),
|
||||
SocketAddr::from_str("[0001:0203:0405:0607:0809:0a0b:0c0d:0e0f]:6789").unwrap()
|
||||
])
|
||||
),
|
||||
"Peers [1.2.3.4:123, 5.6.7.8:12345, [1:203:405:607:809:a0b:c0d:e0f]:6789]"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{:?}",
|
||||
Message::Init(0, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], vec![
|
||||
Range {
|
||||
base: Address { data: [0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 4 },
|
||||
prefix_len: 24
|
||||
},
|
||||
Range {
|
||||
base: Address { data: [0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], len: 6 },
|
||||
prefix_len: 16
|
||||
}
|
||||
], 1800)
|
||||
),
|
||||
"Init(stage=0, node_id=000102030405060708090a0b0c0d0e0f, peer_timeout=1800, [0.1.2.3/24, 00:01:02:03:04:05/16])"
|
||||
);
|
||||
assert_eq!(format!("{:?}", Message::Close), "Close");
|
||||
}
|
142
src/util.rs
142
src/util.rs
|
@ -1,24 +1,102 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
fmt,
|
||||
net::{Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket},
|
||||
sync::atomic::{AtomicIsize, Ordering}
|
||||
sync::atomic::{AtomicIsize, Ordering},
|
||||
};
|
||||
|
||||
use super::types::Error;
|
||||
use crate::error::Error;
|
||||
|
||||
#[cfg(not(target_os = "linux"))] use time;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use time;
|
||||
|
||||
use signal::{trap::Trap, Signal};
|
||||
use smallvec::SmallVec;
|
||||
use std::time::Instant;
|
||||
|
||||
|
||||
pub type Duration = u32;
|
||||
pub type Time = i64;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MsgBuffer {
|
||||
space_before: usize,
|
||||
buffer: [u8; 65535],
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl MsgBuffer {
|
||||
pub fn new(space_before: usize) -> Self {
|
||||
Self { buffer: [0; 65535], space_before, start: space_before, end: space_before }
|
||||
}
|
||||
|
||||
pub fn get_start(&self) -> usize {
|
||||
self.start
|
||||
}
|
||||
|
||||
pub fn set_start(&mut self, start: usize) {
|
||||
self.start = start
|
||||
}
|
||||
|
||||
pub fn prepend_byte(&mut self, byte: u8) {
|
||||
self.start -= 1;
|
||||
self.buffer[self.start] = byte
|
||||
}
|
||||
|
||||
pub fn take_prefix(&mut self) -> u8 {
|
||||
let byte = self.buffer[self.start];
|
||||
self.start += 1;
|
||||
byte
|
||||
}
|
||||
|
||||
pub fn buffer(&mut self) -> &mut [u8] {
|
||||
&mut self.buffer[self.start..]
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &[u8] {
|
||||
&self.buffer[self.start..self.end]
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> Option<&[u8]> {
|
||||
if self.start != self.end {
|
||||
let end = self.end;
|
||||
self.end = self.start;
|
||||
Some(&self.buffer[self.start..end])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.buffer[self.start..self.end]
|
||||
}
|
||||
|
||||
pub fn set_length(&mut self, length: usize) {
|
||||
self.end = self.start + length
|
||||
}
|
||||
|
||||
pub fn clone_from(&mut self, other: &[u8]) {
|
||||
self.set_length(other.len());
|
||||
self.message_mut().clone_from_slice(other);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.end - self.start
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.start == self.end
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.set_start(self.space_before);
|
||||
self.set_length(0)
|
||||
}
|
||||
}
|
||||
|
||||
const HEX_CHARS: &[u8] = b"0123456789abcdef";
|
||||
|
||||
|
@ -34,13 +112,12 @@ pub fn bytes_to_hex(bytes: &[u8]) -> String {
|
|||
pub fn addr_nice(addr: SocketAddr) -> SocketAddr {
|
||||
if let SocketAddr::V6(v6addr) = addr {
|
||||
if let Some(ip) = v6addr.ip().to_ipv4() {
|
||||
return (ip, addr.port()).into()
|
||||
return (ip, addr.port()).into();
|
||||
}
|
||||
}
|
||||
addr
|
||||
}
|
||||
|
||||
|
||||
pub struct Encoder;
|
||||
|
||||
impl Encoder {
|
||||
|
@ -93,7 +170,6 @@ impl Encoder {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
macro_rules! fail {
|
||||
($format:expr) => ( {
|
||||
use std::process;
|
||||
|
@ -136,19 +212,16 @@ pub fn get_internal_ip() -> Ipv4Addr {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[allow(unknown_lints, clippy::needless_pass_by_value)]
|
||||
pub fn resolve<Addr: ToSocketAddrs + fmt::Debug>(addr: Addr) -> Result<Vec<SocketAddr>, Error> {
|
||||
let addrs = addr.to_socket_addrs().map_err(|_| Error::Name(format!("{:?}", addr)))?;
|
||||
// Remove duplicates in addrs (why are there duplicates???)
|
||||
let mut addrs = addrs.collect::<Vec<_>>();
|
||||
pub fn resolve<Addr: ToSocketAddrs + fmt::Debug>(addr: Addr) -> Result<SmallVec<[SocketAddr; 4]>, Error> {
|
||||
let mut addrs =
|
||||
addr.to_socket_addrs().map_err(|_| Error::NameUnresolvable(format!("{:?}", addr)))?.collect::<SmallVec<_>>();
|
||||
// Try IPv4 first as it usually is faster
|
||||
addrs.sort_by_key(|addr| {
|
||||
match *addr {
|
||||
SocketAddr::V4(_) => 4,
|
||||
SocketAddr::V6(_) => 6
|
||||
}
|
||||
addrs.sort_by_key(|addr| match *addr {
|
||||
SocketAddr::V4(_) => 4,
|
||||
SocketAddr::V6(_) => 6,
|
||||
});
|
||||
// Remove duplicates in addrs (why are there duplicates???)
|
||||
addrs.dedup();
|
||||
Ok(addrs)
|
||||
}
|
||||
|
@ -160,7 +233,6 @@ macro_rules! addr {
|
|||
}};
|
||||
}
|
||||
|
||||
|
||||
pub struct Bytes(pub u64);
|
||||
|
||||
impl fmt::Display for Bytes {
|
||||
|
@ -169,31 +241,30 @@ impl fmt::Display for Bytes {
|
|||
if size >= 512.0 {
|
||||
size /= 1024.0;
|
||||
} else {
|
||||
return write!(formatter, "{:.0} B", size)
|
||||
return write!(formatter, "{:.0} B", size);
|
||||
}
|
||||
if size >= 512.0 {
|
||||
size /= 1024.0;
|
||||
} else {
|
||||
return write!(formatter, "{:.1} KiB", size)
|
||||
return write!(formatter, "{:.1} KiB", size);
|
||||
}
|
||||
if size >= 512.0 {
|
||||
size /= 1024.0;
|
||||
} else {
|
||||
return write!(formatter, "{:.1} MiB", size)
|
||||
return write!(formatter, "{:.1} MiB", size);
|
||||
}
|
||||
if size >= 512.0 {
|
||||
size /= 1024.0;
|
||||
} else {
|
||||
return write!(formatter, "{:.1} GiB", size)
|
||||
return write!(formatter, "{:.1} GiB", size);
|
||||
}
|
||||
write!(formatter, "{:.1} TiB", size)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct CtrlC {
|
||||
dummy_time: Instant,
|
||||
trap: Trap
|
||||
trap: Trap,
|
||||
}
|
||||
|
||||
impl CtrlC {
|
||||
|
@ -214,7 +285,6 @@ impl Default for CtrlC {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait TimeSource: Sync + Copy + Send + 'static {
|
||||
fn now() -> Time;
|
||||
}
|
||||
|
@ -257,7 +327,6 @@ impl TimeSource for MockTimeSource {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// Helper function that multiplies the base62 data in buf[0..buflen] by 16 and adds m to it
|
||||
fn base62_add_mult_16(buf: &mut [u8], mut buflen: usize, m: u8) -> usize {
|
||||
let mut d: usize = m as usize;
|
||||
|
@ -277,7 +346,7 @@ fn base62_add_mult_16(buf: &mut [u8], mut buflen: usize, m: u8) -> usize {
|
|||
const BASE62: [char; 62] = [
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
||||
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
|
||||
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
|
||||
pub fn to_base62(data: &[u8]) -> String {
|
||||
|
@ -300,10 +369,10 @@ pub fn from_base62(data: &str) -> Result<Vec<u8>, char> {
|
|||
let mut buf = Vec::with_capacity(data.len() / 2 + data.len() / 4);
|
||||
for c in data.chars() {
|
||||
let mut val = match c {
|
||||
'0'..='9' => ((c as usize) % ('0' as usize)),
|
||||
'0'..='9' => (c as usize) % ('0' as usize),
|
||||
'A'..='Z' => ((c as usize) % ('A' as usize)) + 10,
|
||||
'a'..='z' => ((c as usize) % ('a' as usize)) + 36,
|
||||
_ => return Err(c)
|
||||
_ => return Err(c),
|
||||
};
|
||||
for item in &mut buf {
|
||||
val += *item as usize * 62;
|
||||
|
@ -318,11 +387,10 @@ pub fn from_base62(data: &str) -> Result<Vec<u8>, char> {
|
|||
Ok(buf)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StatsdMsg {
|
||||
entries: Vec<String>,
|
||||
key: Vec<String>
|
||||
key: Vec<String>,
|
||||
}
|
||||
|
||||
impl StatsdMsg {
|
||||
|
@ -347,6 +415,16 @@ impl StatsdMsg {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn run_cmd(mut cmd: Command) {
|
||||
match cmd.status() {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
error!("Command returned error: {:?}", status.code())
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to execute command {:?}: {}", cmd, e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base62() {
|
||||
|
|
|
@ -0,0 +1,509 @@
|
|||
use crate::{config::Config, crypto::Crypto, device, types::Mode};
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Password, Select};
|
||||
use ring::aead;
|
||||
use std::{collections::HashMap, fs, io, os::unix::fs::PermissionsExt, path::Path};
|
||||
|
||||
const MODE_SIMPLE: usize = 0;
|
||||
const MODE_ADVANCED: usize = 1;
|
||||
const MODE_EXPERT: usize = 2;
|
||||
|
||||
fn str_list(s: String) -> Vec<String> {
|
||||
if s.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
s.split(',').map(|k| k.trim().to_string()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn str_opt(s: String) -> Option<String> {
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_connectivity(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), dialoguer::Error> {
|
||||
if mode >= MODE_ADVANCED {
|
||||
config.listen =
|
||||
Input::with_theme(theme).with_prompt("Listen address").default(config.listen.clone()).interact_text()?;
|
||||
}
|
||||
config.peers = str_list(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Peer addresses (comma separated)")
|
||||
.default(config.peers.join(","))
|
||||
.interact_text()?,
|
||||
);
|
||||
if mode >= MODE_ADVANCED {
|
||||
config.port_forwarding = Confirm::with_theme(theme)
|
||||
.with_prompt("Enable automatic port forwarding?")
|
||||
.default(config.port_forwarding)
|
||||
.interact()?;
|
||||
}
|
||||
if mode == MODE_EXPERT {
|
||||
config.advertise_addresses = str_list(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Advertise addresses (comma separated)")
|
||||
.default(config.advertise_addresses.join(","))
|
||||
.interact_text()?,
|
||||
);
|
||||
config.peer_timeout = Input::with_theme(theme)
|
||||
.with_prompt("Peer timeout (in seconds)")
|
||||
.default(config.peer_timeout)
|
||||
.interact_text()?;
|
||||
let val = Input::with_theme(theme)
|
||||
.with_prompt("Keepalive interval (in seconds, 0 for default)")
|
||||
.default(config.keepalive.unwrap_or_default())
|
||||
.interact_text()?;
|
||||
config.keepalive = if val == 0 { None } else { Some(val) };
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_crypto(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), dialoguer::Error> {
|
||||
if (config.crypto.password.is_some() || config.crypto.private_key.is_some())
|
||||
&& !Confirm::with_theme(theme).with_prompt("Create new crypto config?").default(false).interact()?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let mut use_password = true;
|
||||
if mode >= MODE_ADVANCED {
|
||||
use_password = Select::with_theme(theme)
|
||||
.with_prompt("Crypto configuration method")
|
||||
.items(&["Simple (Password)", "Complex (Key pair)"])
|
||||
.default(if config.crypto.private_key.is_some() { 1 } else { 0 })
|
||||
.interact()?
|
||||
== 0
|
||||
}
|
||||
if use_password {
|
||||
config.crypto.password = Some(
|
||||
Password::with_theme(theme)
|
||||
.with_prompt("Password")
|
||||
.with_confirmation("Confirm password", "Passwords do not match")
|
||||
.interact()?,
|
||||
);
|
||||
config.crypto.private_key = None;
|
||||
config.crypto.public_key = None;
|
||||
config.crypto.trusted_keys = vec![];
|
||||
} else {
|
||||
config.crypto.password = None;
|
||||
let (priv_key, pub_key) = match Select::with_theme(theme)
|
||||
.with_prompt("Specify key pair")
|
||||
.items(&["Generate new key pair", "Enter private key", "Generate from password"])
|
||||
.default(0)
|
||||
.interact()?
|
||||
{
|
||||
0 => {
|
||||
let (priv_key, pub_key) = Crypto::generate_keypair(None);
|
||||
info!("Private key: {}", priv_key);
|
||||
info!("Public key: {}", pub_key);
|
||||
(priv_key, pub_key)
|
||||
}
|
||||
1 => {
|
||||
let priv_key = Password::with_theme(theme)
|
||||
.with_prompt("Private key")
|
||||
.with_confirmation("Confirm private key", "Keys do not match")
|
||||
.interact()?;
|
||||
let pub_key = try_fail!(Crypto::public_key_from_private_key(&priv_key), "Invalid private key: {:?}");
|
||||
info!("Public key: {}", pub_key);
|
||||
(priv_key, pub_key)
|
||||
}
|
||||
2 => {
|
||||
let password = Password::with_theme(theme)
|
||||
.with_prompt("Password")
|
||||
.with_confirmation("Confirm password", "Passwords do not match")
|
||||
.interact()?;
|
||||
let (priv_key, pub_key) = Crypto::generate_keypair(Some(&password));
|
||||
info!("Private key: {}", priv_key);
|
||||
info!("Public key: {}", pub_key);
|
||||
(priv_key, pub_key)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
config.crypto.trusted_keys = str_list(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Trusted keys (public keys, comma separated)")
|
||||
.default(pub_key.clone())
|
||||
.interact_text()?,
|
||||
);
|
||||
config.crypto.private_key = Some(priv_key);
|
||||
config.crypto.public_key = Some(pub_key);
|
||||
}
|
||||
if mode == MODE_EXPERT {
|
||||
let (unencrypted, allowed_algos) = Crypto::parse_algorithms(&config.crypto.algorithms)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid crypto algorithms"))?;
|
||||
let algos = MultiSelect::with_theme(theme)
|
||||
.with_prompt("Allowed encryption algorithms (select multiple)")
|
||||
.items_checked(&[
|
||||
("Unencrypted (dangerous)", unencrypted),
|
||||
("AES-128 in GCM mode", allowed_algos.contains(&&aead::AES_128_GCM)),
|
||||
("AES-256 in GCM mode", allowed_algos.contains(&&aead::AES_256_GCM)),
|
||||
("ChaCha20-Poly1305 (RFC 7539)", allowed_algos.contains(&&aead::CHACHA20_POLY1305)),
|
||||
])
|
||||
.interact()?;
|
||||
config.crypto.algorithms = vec![];
|
||||
for (id, name) in &[(0, "PLAIN"), (1, "AES128"), (2, "AES256"), (3, "CHACHA20")] {
|
||||
if algos.contains(id) {
|
||||
config.crypto.algorithms.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_device(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), dialoguer::Error> {
|
||||
if mode >= MODE_ADVANCED {
|
||||
config.device_type = match Select::with_theme(theme)
|
||||
.with_prompt("Device type")
|
||||
.items(&["Tun (IP based)", "Tap (Ethernet based)"])
|
||||
.default(if config.device_type == device::Type::Tun { 0 } else { 1 })
|
||||
.interact()?
|
||||
{
|
||||
0 => device::Type::Tun,
|
||||
1 => device::Type::Tap,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
if mode == MODE_EXPERT {
|
||||
config.device_name =
|
||||
Input::with_theme(theme).with_prompt("Device name").default(config.device_name.clone()).interact_text()?;
|
||||
config.device_path = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Device path (empty for default)")
|
||||
.default(config.device_path.as_ref().cloned().unwrap_or_default())
|
||||
.interact_text()?,
|
||||
);
|
||||
config.fix_rp_filter = Confirm::with_theme(theme)
|
||||
.with_prompt("Automatically fix insecure rp_filter settings")
|
||||
.default(config.fix_rp_filter)
|
||||
.interact()?;
|
||||
config.mode = match Select::with_theme(theme)
|
||||
.with_prompt("Operation mode")
|
||||
.items(&["Normal", "Router", "Switch", "Hub"])
|
||||
.default(match config.mode {
|
||||
Mode::Normal => 0,
|
||||
Mode::Router => 1,
|
||||
Mode::Switch => 2,
|
||||
Mode::Hub => 3,
|
||||
})
|
||||
.interact()?
|
||||
{
|
||||
0 => Mode::Normal,
|
||||
1 => Mode::Router,
|
||||
2 => Mode::Switch,
|
||||
3 => Mode::Hub,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if config.mode == Mode::Switch {
|
||||
config.switch_timeout = Input::with_theme(theme)
|
||||
.with_prompt("Switch timeout (in seconds")
|
||||
.default(config.switch_timeout)
|
||||
.interact_text()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_addresses(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), dialoguer::Error> {
|
||||
config.ip = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Virtual IP address (e.g. 10.0.0.1, leave empty for none)")
|
||||
.allow_empty(true)
|
||||
.default(config.ip.as_ref().cloned().unwrap_or_default())
|
||||
.interact_text()?,
|
||||
);
|
||||
if config.device_type == device::Type::Tun {
|
||||
if mode >= MODE_ADVANCED {
|
||||
config.auto_claim = Confirm::with_theme(theme)
|
||||
.with_prompt("Automatically claim IP set on virtual interface?")
|
||||
.default(config.auto_claim)
|
||||
.interact()?;
|
||||
}
|
||||
if mode == MODE_EXPERT {
|
||||
config.claims = str_list(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Claim additional addresses (e.g. 10.0.0.0/24, comma separated, leave empty for none)")
|
||||
.allow_empty(true)
|
||||
.default(config.claims.join(","))
|
||||
.interact_text()?,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
config.claims = vec![];
|
||||
}
|
||||
if mode == MODE_EXPERT {
|
||||
config.ifup = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Interface setup command (leave empty for none)")
|
||||
.allow_empty(true)
|
||||
.default(config.ifup.as_ref().cloned().unwrap_or_default())
|
||||
.interact_text()?,
|
||||
);
|
||||
config.ifdown = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Interface tear down command (leave empty for none)")
|
||||
.allow_empty(true)
|
||||
.default(config.ifdown.as_ref().cloned().unwrap_or_default())
|
||||
.interact_text()?,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_beacon(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), dialoguer::Error> {
|
||||
if mode == MODE_EXPERT
|
||||
&& Confirm::with_theme(theme)
|
||||
.with_prompt("Configure beacons?")
|
||||
.default(config.beacon_load.is_some() || config.beacon_store.is_some())
|
||||
.interact()?
|
||||
{
|
||||
config.beacon_store = match Select::with_theme(theme)
|
||||
.with_prompt("How to store beacons")
|
||||
.items(&["Do not store beacons", "Store to file", "Execute command"])
|
||||
.default(if let Some(v) = &config.beacon_store {
|
||||
if v.starts_with('|') {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
} else {
|
||||
0
|
||||
})
|
||||
.interact()?
|
||||
{
|
||||
0 => None,
|
||||
1 => Some(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("File path")
|
||||
.default(config.beacon_store.clone().unwrap_or_default())
|
||||
.interact_text()?,
|
||||
),
|
||||
2 => Some(format!(
|
||||
"|{}",
|
||||
Input::<String>::with_theme(theme)
|
||||
.with_prompt("Command")
|
||||
.default(config.beacon_store.clone().unwrap_or_default().trim_start_matches('|').to_string())
|
||||
.interact_text()?
|
||||
)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
config.beacon_load = match Select::with_theme(theme)
|
||||
.with_prompt("How to load beacons")
|
||||
.items(&["Do not load beacons", "Load from file", "Execute command"])
|
||||
.default(if let Some(v) = &config.beacon_load {
|
||||
if v.starts_with('|') {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
} else {
|
||||
0
|
||||
})
|
||||
.interact()?
|
||||
{
|
||||
0 => None,
|
||||
1 => Some(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("File path")
|
||||
.default(config.beacon_load.clone().unwrap_or_default())
|
||||
.interact_text()?,
|
||||
),
|
||||
2 => Some(format!(
|
||||
"|{}",
|
||||
Input::<String>::with_theme(theme)
|
||||
.with_prompt("Command")
|
||||
.default(config.beacon_load.clone().unwrap_or_default().trim_start_matches('|').to_string())
|
||||
.interact_text()?
|
||||
)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
config.beacon_interval = Input::with_theme(theme)
|
||||
.with_prompt("Beacon interval (in seconds)")
|
||||
.default(config.beacon_interval)
|
||||
.interact_text()?;
|
||||
config.beacon_password = str_opt(
|
||||
Password::with_theme(theme)
|
||||
.with_prompt("Beacon password (leave empty for none)")
|
||||
.with_confirmation("Confirm password", "Passwords do not match")
|
||||
.allow_empty_password(true)
|
||||
.interact()?,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_stats(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), dialoguer::Error> {
|
||||
if mode >= MODE_ADVANCED {
|
||||
config.stats_file = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Write stats to file (empty to disable)")
|
||||
.default(config.stats_file.clone().unwrap_or_default())
|
||||
.allow_empty(true)
|
||||
.interact_text()?,
|
||||
);
|
||||
}
|
||||
if mode == MODE_EXPERT {
|
||||
if Confirm::with_theme(theme)
|
||||
.with_prompt("Send statistics to statsd server?")
|
||||
.default(config.statsd_server.is_some())
|
||||
.interact()?
|
||||
{
|
||||
config.statsd_server = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Statsd server URL")
|
||||
.default(config.statsd_server.clone().unwrap_or_default())
|
||||
.allow_empty(true)
|
||||
.interact_text()?,
|
||||
);
|
||||
config.statsd_prefix = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Statsd prefix")
|
||||
.default(config.statsd_prefix.clone().unwrap_or_default())
|
||||
.allow_empty(true)
|
||||
.interact_text()?,
|
||||
);
|
||||
} else {
|
||||
config.statsd_server = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_process(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), dialoguer::Error> {
|
||||
if mode == MODE_EXPERT {
|
||||
config.user = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Run as different user (empty to disable)")
|
||||
.default(config.user.clone().unwrap_or_default())
|
||||
.allow_empty(true)
|
||||
.interact_text()?,
|
||||
);
|
||||
config.group = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Run as different group (empty to disable)")
|
||||
.default(config.group.clone().unwrap_or_default())
|
||||
.allow_empty(true)
|
||||
.interact_text()?,
|
||||
);
|
||||
config.pid_file = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Write process id to file (empty to disable)")
|
||||
.default(config.pid_file.clone().unwrap_or_default())
|
||||
.allow_empty(true)
|
||||
.interact_text()?,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_hooks(config: &mut Config, mode: usize, theme: &ColorfulTheme) -> Result<(), dialoguer::Error> {
|
||||
if mode == MODE_EXPERT {
|
||||
if Confirm::with_theme(theme)
|
||||
.with_prompt("Set hooks to react on events?")
|
||||
.default(config.hook.is_some() || !config.hooks.is_empty())
|
||||
.interact()?
|
||||
{
|
||||
config.hook = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt("Command to execute for all events (empty to disable)")
|
||||
.default(config.hook.clone().unwrap_or_default())
|
||||
.allow_empty(true)
|
||||
.interact_text()?,
|
||||
);
|
||||
let mut hooks: HashMap<String, String> = Default::default();
|
||||
for event in &[
|
||||
"peer_connecting",
|
||||
"peer_connected",
|
||||
"peer_disconnected",
|
||||
"device_setup",
|
||||
"device_configured",
|
||||
"vpn_started",
|
||||
"vpn_shutdown",
|
||||
] {
|
||||
if let Some(cmd) = str_opt(
|
||||
Input::with_theme(theme)
|
||||
.with_prompt(format!("Command to execute for event '{}' (empty to disable)", event))
|
||||
.default(config.hooks.get(*event).cloned().unwrap_or_default())
|
||||
.allow_empty(true)
|
||||
.interact_text()?,
|
||||
) {
|
||||
hooks.insert(event.to_string(), cmd);
|
||||
}
|
||||
}
|
||||
config.hooks = hooks;
|
||||
} else {
|
||||
config.hook = None;
|
||||
config.hooks = Default::default();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn configure(name: Option<String>) -> Result<(), dialoguer::Error> {
|
||||
let theme = ColorfulTheme::default();
|
||||
|
||||
let name = if let Some(name) = name {
|
||||
name
|
||||
} else {
|
||||
let mut names = vec![];
|
||||
for file in fs::read_dir("/etc/vpncloud")? {
|
||||
names.push(file?.path().file_stem().unwrap().to_str().unwrap().to_string());
|
||||
}
|
||||
let selection =
|
||||
Select::with_theme(&theme).with_prompt("Which network?").item("New network").items(&names).interact()?;
|
||||
if selection > 0 {
|
||||
names[selection - 1].clone()
|
||||
} else {
|
||||
Input::with_theme(&theme).with_prompt("Network name").interact_text()?
|
||||
}
|
||||
};
|
||||
|
||||
let mut config = Config::default();
|
||||
let file = Path::new("/etc/vpncloud").join(format!("{}.net", name));
|
||||
if file.exists() {
|
||||
let f = fs::File::open(&file)?;
|
||||
let config_file = serde_yaml::from_reader(f)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Failed to parse config file"))?;
|
||||
config.merge_file(config_file);
|
||||
}
|
||||
if file.parent().unwrap().metadata()?.permissions().readonly() {
|
||||
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "Config file not writable").into());
|
||||
}
|
||||
|
||||
loop {
|
||||
let mode = Select::with_theme(&theme)
|
||||
.with_prompt("Configuration mode")
|
||||
.items(&["Simple (minimal options)", "Advanced (some more options)", "Expert (all options)"])
|
||||
.default(MODE_SIMPLE)
|
||||
.interact()?;
|
||||
|
||||
configure_connectivity(&mut config, mode, &theme)?;
|
||||
configure_crypto(&mut config, mode, &theme)?;
|
||||
configure_device(&mut config, mode, &theme)?;
|
||||
configure_addresses(&mut config, mode, &theme)?;
|
||||
configure_beacon(&mut config, mode, &theme)?;
|
||||
configure_stats(&mut config, mode, &theme)?;
|
||||
configure_process(&mut config, mode, &theme)?;
|
||||
configure_hooks(&mut config, mode, &theme)?;
|
||||
if Confirm::with_theme(&theme).with_prompt("Finish configuration?").default(true).interact()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if Confirm::with_theme(&theme).with_prompt("Save config?").default(true).interact()? {
|
||||
let config_file = config.into_config_file();
|
||||
let f = fs::File::create(&file)?;
|
||||
serde_yaml::to_writer(f, &config_file)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Failed to parse config file"))?;
|
||||
fs::set_permissions(file, fs::Permissions::from_mode(0o600))?;
|
||||
println!();
|
||||
println!("Use the following commands to control your VPN:");
|
||||
println!(" start the VPN: sudo service vpncloud@{0} start", name);
|
||||
println!(" stop the VPN: sudo service vpncloud@{0} stop", name);
|
||||
println!(" get the status: sudo service vpncloud@{0} status", name);
|
||||
println!(" add VPN to autostart: sudo systemctl enable vpncloud@{0}", name);
|
||||
println!(" remove VPN from autostart: sudo systemctl disable vpncloud@{0}", name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
// VpnCloud - Peer-to-Peer VPN
|
||||
// Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
// This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
||||
use super::{
|
||||
net::{get_ip, mapped_addr, parse_listen, Socket},
|
||||
poll::{WaitImpl, WaitResult},
|
||||
port_forwarding::PortForwarding,
|
||||
util::MsgBuffer,
|
||||
};
|
||||
use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt};
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Write},
|
||||
net::{Ipv6Addr, SocketAddr, SocketAddrV6, TcpListener, TcpStream, UdpSocket},
|
||||
os::unix::io::{AsRawFd, RawFd},
|
||||
thread::spawn,
|
||||
};
|
||||
use tungstenite::{connect, protocol::WebSocket, Message, accept, stream::{MaybeTlsStream, NoDelay}};
|
||||
use url::Url;
|
||||
|
||||
macro_rules! io_error {
|
||||
($val:expr, $format:expr) => ( {
|
||||
$val.map_err(|err| io::Error::new(io::ErrorKind::Other, format!($format, err)))
|
||||
} );
|
||||
($val:expr, $format:expr, $( $arg:expr ),+) => ( {
|
||||
$val.map_err(|err| io::Error::new(io::ErrorKind::Other, format!($format, $( $arg ),+, err)))
|
||||
} );
|
||||
}
|
||||
|
||||
fn write_addr<W: Write>(addr: SocketAddr, mut out: W) -> Result<(), io::Error> {
|
||||
let addr = mapped_addr(addr);
|
||||
match mapped_addr(addr) {
|
||||
SocketAddr::V6(addr) => {
|
||||
out.write_all(&addr.ip().octets())?;
|
||||
out.write_u16::<NetworkEndian>(addr.port())?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_addr<R: Read>(mut r: R) -> Result<SocketAddr, io::Error> {
|
||||
let mut ip = [0u8; 16];
|
||||
r.read_exact(&mut ip)?;
|
||||
let port = r.read_u16::<NetworkEndian>()?;
|
||||
let addr = SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::from(ip), port, 0, 0));
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
fn serve_proxy_connection(stream: TcpStream) -> Result<(), io::Error> {
|
||||
let peer = stream.peer_addr()?;
|
||||
info!("WS client {} connected", peer);
|
||||
stream.set_nodelay(true)?;
|
||||
let mut websocket = io_error!(accept(stream), "Failed to initialize websocket with {}: {}", peer)?;
|
||||
let udpsocket = UdpSocket::bind("[::]:0")?;
|
||||
let mut msg = Vec::with_capacity(18);
|
||||
let mut addr = udpsocket.local_addr()?;
|
||||
info!("Listening on {} for peer {}", addr, peer);
|
||||
addr.set_ip(get_ip());
|
||||
write_addr(addr, &mut msg)?;
|
||||
io_error!(websocket.send(Message::Binary(msg)), "Failed to write to ws connection: {}")?;
|
||||
let websocketfd = websocket.get_ref().as_raw_fd();
|
||||
let poll = WaitImpl::new(websocketfd, udpsocket.as_raw_fd(), 60 * 1000)?;
|
||||
let mut buffer = [0; 65535];
|
||||
for evt in poll {
|
||||
match evt {
|
||||
WaitResult::Socket => {
|
||||
let msg = io_error!(websocket.read(), "Failed to read message on websocket {}: {}", peer)?;
|
||||
match msg {
|
||||
Message::Binary(data) => {
|
||||
let dst = read_addr(Cursor::new(&data))?;
|
||||
udpsocket.send_to(&data[18..], dst)?;
|
||||
}
|
||||
Message::Close(_) => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
WaitResult::Device => {
|
||||
let (size, addr) = udpsocket.recv_from(&mut buffer)?;
|
||||
let mut data = Vec::with_capacity(18 + size);
|
||||
write_addr(addr, &mut data)?;
|
||||
data.write_all(&buffer[..size])?;
|
||||
io_error!(websocket.send(Message::Binary(data)), "Failed to write to {}: {}", peer)?;
|
||||
}
|
||||
WaitResult::Timeout => {
|
||||
io_error!(websocket.send(Message::Ping(vec![])), "Failed to send ping: {}")?;
|
||||
}
|
||||
WaitResult::Error(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_proxy(listen: &str) -> Result<(), io::Error> {
|
||||
let addr = parse_listen(listen, 8080);
|
||||
let server = TcpListener::bind(addr)?;
|
||||
info!("Listening on ws://{}", server.local_addr()?);
|
||||
for stream in server.incoming() {
|
||||
let stream = stream?;
|
||||
let peer = stream.peer_addr()?;
|
||||
spawn(move || {
|
||||
if let Err(err) = serve_proxy_connection(stream) {
|
||||
error!("Error on connection {}: {}", peer, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct ProxyConnection {
|
||||
addr: SocketAddr,
|
||||
socket: WebSocket<MaybeTlsStream<TcpStream>>,
|
||||
}
|
||||
|
||||
impl ProxyConnection {
|
||||
fn read_message(&mut self) -> Result<Vec<u8>, io::Error> {
|
||||
loop {
|
||||
if let Message::Binary(data) = io_error!(self.socket.read(), "Failed to read from ws proxy: {}")? {
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for ProxyConnection {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
match self.socket.get_ref() {
|
||||
MaybeTlsStream::Plain(stream) => stream.as_raw_fd(),
|
||||
_ => unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Socket for ProxyConnection {
|
||||
fn listen(url: &str) -> Result<Self, io::Error> {
|
||||
let parsed_url = io_error!(Url::parse(url), "Invalid URL {}: {}", url)?;
|
||||
let (mut socket, _) = io_error!(connect(parsed_url), "Failed to connect to URL {}: {}", url)?;
|
||||
socket.get_mut().set_nodelay(true)?;
|
||||
let addr = "0.0.0.0:0".parse::<SocketAddr>().unwrap();
|
||||
let mut con = ProxyConnection { addr, socket };
|
||||
let addr_data = con.read_message()?;
|
||||
con.addr = read_addr(Cursor::new(&addr_data))?;
|
||||
Ok(con)
|
||||
}
|
||||
|
||||
fn receive(&mut self, buffer: &mut MsgBuffer) -> Result<SocketAddr, io::Error> {
|
||||
buffer.clear();
|
||||
let data = self.read_message()?;
|
||||
let addr = read_addr(Cursor::new(&data))?;
|
||||
buffer.clone_from(&data[18..]);
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
fn send(&mut self, data: &[u8], addr: SocketAddr) -> Result<usize, io::Error> {
|
||||
let mut msg = Vec::with_capacity(data.len() + 18);
|
||||
write_addr(addr, &mut msg)?;
|
||||
msg.write_all(data)?;
|
||||
io_error!(self.socket.send(Message::Binary(msg)), "Failed to write to ws proxy: {}")?;
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
fn address(&self) -> Result<SocketAddr, io::Error> {
|
||||
Ok(self.addr)
|
||||
}
|
||||
|
||||
fn create_port_forwarding(&self) -> Option<PortForwarding> {
|
||||
None
|
||||
}
|
||||
}
|
495
vpncloud.adoc
495
vpncloud.adoc
|
@ -7,7 +7,7 @@ vpncloud - Peer-to-peer VPN
|
|||
|
||||
== SYNOPSIS
|
||||
|
||||
*vpncloud [options] [--config <file>] [-t <type>] [-d <name>] [-l <addr>] [-c <addr>...]*
|
||||
*vpncloud [options] [--config <file>] [-p <password>] [-l <addr>] [-c <addr>...]*
|
||||
|
||||
|
||||
== OPTIONS
|
||||
|
@ -20,14 +20,18 @@ vpncloud - Peer-to-peer VPN
|
|||
|
||||
*-t <type>*, *--type <type>*::
|
||||
Set the type of network. There are two options: *tap* devices process
|
||||
Ethernet frames *tun* devices process IP packets. [default: *tap*]
|
||||
Ethernet frames *tun* devices process IP packets. [default: *tun*]
|
||||
|
||||
*-d <name>*, *--device <name>*::
|
||||
Name of the virtual device. Any *%d* will be filled with a free number.
|
||||
[default: *vpncloud%d*]
|
||||
|
||||
*--device-path <path>*::
|
||||
The path of the base device inode, e.g. /dev/net/run.
|
||||
The path of the base device inode, e.g. /dev/net/tun.
|
||||
|
||||
*--fix-rp-filter*::
|
||||
If this option is set, VpnCloud will change the rp_filter settings to protect
|
||||
against a potential system vulnerability. See *SECURITY* for more info.
|
||||
|
||||
*-m <mode>*, *--mode <mode>*::
|
||||
The mode of the VPN. The VPN can like a router, a switch or a hub. A *hub*
|
||||
|
@ -41,49 +45,67 @@ vpncloud - Peer-to-peer VPN
|
|||
The address on which to listen for data. This can be simply a port number
|
||||
or a full address in form IP:PORT. If the IP is specified as \'\*' or only
|
||||
a port number is given, then the socket will listen on all IPs (v4 and v6),
|
||||
otherwise the socket will only listen on the given IP. [default: **3210**]
|
||||
otherwise the socket will only listen on the given IP.
|
||||
Alternatively, a websocket proxy URL (starting with ws://) can be given
|
||||
here. Please see the section *WEBSOCKET PROXY* for more info.
|
||||
[default: **3210**]
|
||||
|
||||
*-c <addr>*, *--connect <addr>*::
|
||||
*-c <addr>*, *--peer <addr>*, *--connect <addr>*::
|
||||
Address of a peer to connect to. The address should be in the form
|
||||
*addr:port*. If the node is not started, the connection will be retried
|
||||
periodically. This parameter can be repeated to connect to multiple peers.
|
||||
|
||||
*-s <subnet>*, *--subnet <subnet>*:
|
||||
The local subnets to use. This parameter should be in the form
|
||||
*--claim <subnet>*::
|
||||
The local subnets to claim. This parameter should be in the form
|
||||
*address/prefixlen* where address is an IPv4 address, an IPv6 address, or a
|
||||
MAC address. The prefix length is the number of significant front bits that
|
||||
distinguish the subnet from other subnets. Example: *10.1.1.0/24*.
|
||||
|
||||
*--shared-key <key>*::
|
||||
An optional shared key to encrypt the VPN data. If this option is not set,
|
||||
the traffic will be sent unencrypted.
|
||||
*--no-auto-claim*::
|
||||
Do not automatically claim the IP set on the virtual interface (on TUN
|
||||
devices).
|
||||
|
||||
*--crypto <method>*::
|
||||
The encryption method to use ("aes256", or "chacha20"). Most current CPUs
|
||||
have special support for AES256 so this should be faster. For older
|
||||
computers lacking this support, only CHACHA20 is supported.
|
||||
[default: *chacha20*]
|
||||
*-p <password>*, *--password <password>*::
|
||||
A password to encrypt the VPN data. This parameter must be set unless a
|
||||
password is given in a config file or a private key is set.
|
||||
See *SECURITY* for more info.
|
||||
|
||||
*--magic <id>*::
|
||||
Override the 4-byte magic header of each packet. This header identifies the
|
||||
network and helps to distinguish it from other networks and other
|
||||
applications. The id can either be a 4 byte / 8 character hexadecimal
|
||||
string or an arbitrary string prefixed with "hash:" which will then be
|
||||
hashed into 4 bytes.
|
||||
*--key <key>*, *--private-key <key>*::
|
||||
A private key to use for encryption. The key must be given as base62 as
|
||||
generated by *genkey*. See *SECURITY* for more info.
|
||||
|
||||
*--public-key <key>*::
|
||||
A public key matching the given private key. The key must be given as base62
|
||||
as generated by *genkey*. This argument is purely optional. See *SECURITY*
|
||||
for more info.
|
||||
|
||||
*--trust <key>*, **--trusted-key <key>*::
|
||||
A public key to trust. Any peer must have a key pair that is trusted by this
|
||||
node, otherwise it will be rejected. The key must be given as base62 as
|
||||
generated by *genkey*. This argument can be given multiple times. If it is
|
||||
not set, only the own public key will be trusted. See *SECURITY* for more
|
||||
info.
|
||||
|
||||
*--algo <method>*, *--algorithm <method>*::
|
||||
Supported encryption algorithms ("plain", "aes128", "aes256", or "chacha20").
|
||||
Nodes exchange the supported algorithms and select the one that is fastest on
|
||||
both ends. This parameter can be given multiple times to enable multiple
|
||||
algorithms. *Warning:* "plain" means unencrypted and needs to be enabled
|
||||
explicitly. As default, all algorithms except "plain" are enabled.
|
||||
|
||||
*--peer-timeout <secs>*::
|
||||
Peer timeout in seconds. The peers will exchange information periodically
|
||||
and drop peers that are silent for this period of time. [default: **600**]
|
||||
and drop peers that are silent for this period of time. [default: *300*]
|
||||
|
||||
*--keepalive <secs>*::
|
||||
Interval of peer exchange messages in seconds. The peers will exchange
|
||||
information periodically to keep connections alive. This setting overrides
|
||||
how often this will happen. [default: *peer-timeout/2-60*]
|
||||
|
||||
*--dst-timeout <secs>*::
|
||||
*--switch-timeout <secs>*::
|
||||
Switch table entry timeout in seconds. This parameter is only used in switch
|
||||
mode. Addresses that have not been seen for the given period of time will
|
||||
be forgotten. [default: **300**]
|
||||
be forgotten. [default: *300*]
|
||||
|
||||
*--beacon-store <path|command>*::
|
||||
Periodically store beacons containing the address of this node in the given
|
||||
|
@ -106,7 +128,18 @@ vpncloud - Peer-to-peer VPN
|
|||
Beacon storage/loading interval in seconds. If configured to do so via
|
||||
*--beacon-store* and *--beacon-load*, the node will periodically store its
|
||||
beacon and load beacons of other nodes. This parameter defines the interval
|
||||
in seconds. [default: **3600**]
|
||||
in seconds. [default: *3600*]
|
||||
|
||||
*--beacon-password <password>*::
|
||||
An optional password to use to encrypt all beacon data. See the section
|
||||
*BEACONS* for more information.
|
||||
|
||||
*--ip <address>*::
|
||||
An IP address (plus optional prefix length) for the interface. If this
|
||||
argument is given, the address (and if a prefix length is given, also the
|
||||
netmask) is configured on the device and the device is activated.
|
||||
If also *--ifup* is given, the interface is configured before the ifup
|
||||
command is executed. Please see *DEVICE SETUP* for more info.
|
||||
|
||||
*--ifup <command>*::
|
||||
A command to setup the network interface. The command will be run (as
|
||||
|
@ -114,7 +147,7 @@ vpncloud - Peer-to-peer VPN
|
|||
The name of the allocated device will be available via the environment
|
||||
variable *IFNAME*.
|
||||
Please note that this command is executed with the full permissions of the
|
||||
caller.
|
||||
caller. Please see *DEVICE SETUP* for more info.
|
||||
|
||||
*--ifdown <command>*::
|
||||
A command to bring down the network interface. The command will be run (as
|
||||
|
@ -142,6 +175,15 @@ vpncloud - Peer-to-peer VPN
|
|||
If set, periodically write statistics on peers and current traffic to the
|
||||
given file. The file will be periodically overwritten with new data.
|
||||
|
||||
*--statsd-server <server>*::
|
||||
If set, periodically send statistics on current traffic and some important
|
||||
events to the given statsd server (host:port).
|
||||
Please see *STATSD SUPPORT* for more info.
|
||||
|
||||
*--statsd-prefix <prefix>*::
|
||||
Sets the prefix to use for all statsd entries. [default: **vpncloud**]
|
||||
Please see *STATSD SUPPORT* for more info.
|
||||
|
||||
*--daemon*::
|
||||
Spawn a background process instead of running the process in the foreground.
|
||||
If this flag is set, the process will first carry out all the
|
||||
|
@ -155,6 +197,12 @@ vpncloud - Peer-to-peer VPN
|
|||
Disable automatic port forward. If this option is not set, VpnCloud tries to
|
||||
detect a NAT router and automatically add a port forwarding to it.
|
||||
|
||||
*--hook <script>*::
|
||||
Call the given script on an event. If the script is in the format *event:script*,
|
||||
it will only be called for the specified event type, otherwise it will be called
|
||||
for all events. This parameter can be given multiple times.
|
||||
Please see the section *HOOK SCRIPTS* for more info.
|
||||
|
||||
*-v*, *--verbose*::
|
||||
Print debug information, including information for data being received and
|
||||
sent.
|
||||
|
@ -166,25 +214,60 @@ vpncloud - Peer-to-peer VPN
|
|||
Display the help.
|
||||
|
||||
|
||||
== SUBCOMMANDS
|
||||
|
||||
The following subcommands can be given to run some special action instead of
|
||||
running a VpnCloud instance. Any parameters must be given after the subcommand
|
||||
name (except for -v -q and -h). Only the listed parameters are accepted for the
|
||||
subcommands.
|
||||
|
||||
*genkey*::
|
||||
Generate and print a random key pair and exit. The key pair is printed as
|
||||
base62 and can be used as private-key, public-key and trusted-key options.
|
||||
See *SECURITY* for more info.
|
||||
|
||||
*-p <password>*, *--password <password>*:::
|
||||
Derive the key pair from the given password instead of creating randomly.
|
||||
|
||||
*ws-proxy*::
|
||||
Run a websocket proxy instead of the normal VpnCloud instance.
|
||||
See *WEBSOCKET PROXY* for more info.
|
||||
|
||||
*-l <addr>*, *--listen <addr>*:::
|
||||
Listen on the given TCP address (IP:PORT or PORT). [default: **3210**]
|
||||
|
||||
*migrate-config*::
|
||||
Migrate an old config to the current config format.
|
||||
|
||||
*--config-file*:::
|
||||
The path of the config file to convert.
|
||||
|
||||
*completion*::
|
||||
Output shell completions for the VpnCloud command.
|
||||
|
||||
*--shell*:::
|
||||
The shell type to create completions for. [default: **bash**]
|
||||
|
||||
|
||||
== DESCRIPTION
|
||||
|
||||
*VpnCloud* is a simple VPN over UDP. It creates a virtual network interface on
|
||||
the host and forwards all received data via UDP to the destination. It can work
|
||||
in 3 different modes:
|
||||
*VpnCloud* is a peer-to-peer VPN over UDP. It creates a virtual network
|
||||
interface on the host and forwards all received data via UDP to the
|
||||
destination. It can work in 3 different modes:
|
||||
|
||||
*Switch mode*:: In this mode, the VPN will dynamically learn addresses
|
||||
as they are used as source addresses and use them to forward data to its
|
||||
destination. Addresses that have not been seen for some time
|
||||
(option *dst_timeout*) will be forgotten. Data for unknown addresses will be
|
||||
as they are used as source addresses by peers and use them to forward data to
|
||||
its destination. Addresses that have not been seen for some time
|
||||
(option *switch_timeout*) will be forgotten. Data for unknown addresses will be
|
||||
broadcast to all peers. This mode is the default mode for TAP devices that
|
||||
process Ethernet frames but it can also be used with TUN devices and IP
|
||||
packets.
|
||||
*Hub mode*:: In this mode, all data will always be broadcast to all peers.
|
||||
This mode uses lots of bandwidth and should only be used in special cases.
|
||||
*Router mode*:: In this mode, data will be forwarded based on preconfigured
|
||||
address ranges ("subnets"). Data for unknown nodes will be silently ignored.
|
||||
This mode is the default mode for TUN devices that work with IP packets but
|
||||
it can also be used with TAP devices and Ethernet frames.
|
||||
address ranges ("claims"). Data for unclaimed addresses will be silently
|
||||
ignored. This mode is the default mode for TUN devices that work with IP
|
||||
packets but it can also be used with TAP devices and Ethernet frames.
|
||||
|
||||
All connected VpnCloud nodes will form a peer-to-peer network and cross-connect
|
||||
automatically until the network is fully connected. The nodes will periodically
|
||||
|
@ -192,10 +275,6 @@ exchange information with the other nodes to signal that they are still active
|
|||
and to allow the automatic cross-connect behavior. There are some important
|
||||
things to note:
|
||||
|
||||
. To avoid that different networks that reuse each others addresses merge due
|
||||
to the cross-connect behavior, the *magic* option can be used and set
|
||||
to any unique string to identify the network. The *magic* must be the
|
||||
same on all nodes of the same VPN network.
|
||||
. The cross-connect behavior can be able to connect nodes that are behind
|
||||
firewalls or NATs as it can function as hole-punching.
|
||||
. The management traffic will increase with the peer number quadratically.
|
||||
|
@ -206,17 +285,18 @@ broadcasts data.
|
|||
|
||||
VpnCloud does not implement any loop-avoidance. Since data received on the UDP
|
||||
socket will only be sent to the local network interface and vice versa, VpnCloud
|
||||
cannot produce loops on its own. On the TAP device, however STP data can be
|
||||
cannot produce loops on its own. On a TAP device, however STP data can be
|
||||
transported to avoid loops caused by other network components.
|
||||
|
||||
For TAP devices, IEEE 802.1q frames (VLAN tagged) are detected and forwarded
|
||||
based on separate MAC tables. Any nested tags (Q-in-Q) will be ignored.
|
||||
|
||||
|
||||
== EXAMPLES
|
||||
|
||||
=== Switched TAP scenario
|
||||
=== Simple multi-node connectivity
|
||||
|
||||
In the example scenario, a simple layer 2 network tunnel is established. Most
|
||||
In the example scenario, a simple layer-3 network tunnel is established. Most
|
||||
likely those commands need to be run as *root* using *sudo*.
|
||||
|
||||
First, VpnCloud need to be started on both nodes (the address after *-c* is the
|
||||
|
@ -224,7 +304,7 @@ address of the remote node and the the *X* in the interface address must be
|
|||
unique among all nodes, e.g. 0, 1, 2, ...):
|
||||
|
||||
----
|
||||
vpncloud -c REMOTE_HOST:PORT --ifup 'ifconfig $IFNAME 10.0.0.X/24 mtu 1400 up'
|
||||
vpncloud -c REMOTE_HOST:PORT --ip 10.0.0.X/24 --password PASSWORD
|
||||
----
|
||||
|
||||
Afterwards, the interface can be used to communicate.
|
||||
|
@ -235,7 +315,7 @@ In this example, 2 nodes and their subnets should communicate using IP.
|
|||
First, VpnCloud need to be started on both nodes:
|
||||
|
||||
----
|
||||
vpncloud -t tun -c REMOTE_HOST:PORT --subnet 10.0.X.0/24 --ifup 'ifconfig $IFNAME 10.0.X.1/16 mtu 1400 up'
|
||||
vpncloud -t tun -c REMOTE_HOST:PORT --ip 10.0.X.1 --claim 10.0.X.0/24 --password PASSWORD
|
||||
----
|
||||
|
||||
It is important to configure the interface in a way that all addresses on the
|
||||
|
@ -255,16 +335,7 @@ security issues, DHCP issues and many more problems.
|
|||
to assign unique addresses to all participants. If this happens accidentally,
|
||||
it can conflict with DHCP servers of the local network and can have severe
|
||||
side effects.
|
||||
. VpnCloud is not designed for high security use cases. Although the used crypto
|
||||
primitives are expected to be very secure, their application has not been
|
||||
reviewed.
|
||||
The shared key is hashed using _ScryptSalsa208Sha256_ to derive a key,
|
||||
which is used to encrypt the payload of messages using _ChaCha20Poly1305_ or
|
||||
_AES256-GCM_. The encryption includes an authentication that also protects the
|
||||
header.
|
||||
This method does only protect against attacks on single messages but not
|
||||
against attacks that manipulate the message series itself (i.e. suppress
|
||||
messages, reorder them, or duplicate them).
|
||||
|
||||
|
||||
== CONFIG FILES
|
||||
|
||||
|
@ -272,50 +343,110 @@ The config file is a YAML file that contains configuration values. All entries
|
|||
are optional and override the defaults. Please see the section *OPTIONS* for
|
||||
detailed descriptions of the options.
|
||||
|
||||
*device_type*:: Set the type of network. Same as *--type*
|
||||
*device_name*:: Name of the virtual device. Same as *--device*
|
||||
*device_path*:: Set the path of the base device. Same as *--device-path*
|
||||
*device*:: A key-value map with device settings
|
||||
*type*::: Set the type of network. Same as *--type*
|
||||
*name*::: Name of the virtual device. Same as *--device*
|
||||
*path*::: Set the path of the base device. Same as *--device-path*
|
||||
*fix-rp-filter*::: Fix the rp_filter settings on the host. Same as *--fix-rp-filter*
|
||||
*ip*:: An IP address (plus optional prefix length) for the interface. Same as *--ip*
|
||||
*ifup*:: A command to setup the network interface. Same as *--ifup*
|
||||
*ifdown*:: A command to bring down the network interface. Same as *--ifdown*
|
||||
*crypto*:: The encryption method to use. Same as *--crypto*
|
||||
*shared_key*:: The shared key to encrypt all traffic. Same as *--shared-key*
|
||||
*magic*:: Override the 4-byte magic header of each packet. Same as *--magic*
|
||||
*port*:: A port number to listen on. This option is DEPRECATED.
|
||||
*crypto*:: A key-value map with crypto settings
|
||||
*algorithms*::: The encryption algorithms to support. See *--algorithm*
|
||||
*password*::: The password to use for encryption. Same as *--password*
|
||||
*private-key*::: The private key to use. Same as *--private-key*
|
||||
*public-key*::: The public key to use. Same as *--public-key*
|
||||
*trusted-keys*::: Other public keys to trust. See *--trusted-key*
|
||||
*listen*:: The address on which to listen for data. Same as *--listen*
|
||||
*peers*:: A list of addresses to connect to. See *--connect*
|
||||
*peer_timeout*:: Peer timeout in seconds. Same as**--peer-timeout**
|
||||
*beacon_store*:: Path or command to store beacons. Same as *--beacon-store*
|
||||
*beacon_load*:: Path or command to load beacons. Same as *--beacon-load*
|
||||
*beacon_interval*:: Interval for loading and storing beacons in seconds. Same as *--beacon-interval*
|
||||
*peer_timeout*:: Peer timeout in seconds. Same as *--peer-timeout*
|
||||
*keepalive*:: Periodically send message to keep connections alive. Same as *--keepalive*
|
||||
*beacon*:: A key-value map with beacon settings
|
||||
*store*::: Path or command to store beacons. Same as *--beacon-store*
|
||||
*load*::: Path or command to load beacons. Same as *--beacon-load*
|
||||
*interval*::: Interval for loading and storing beacons in seconds. Same as *--beacon-interval*
|
||||
*password*::: Password to encrypt the beacon with. Same as *--beacon-password*
|
||||
*mode*:: The mode of the VPN. Same as *--mode*
|
||||
*dst_timeout*:: Switch table entry timeout in seconds. Same as *--dst-timeout*
|
||||
*subnets*:: A list of local subnets to use. See *--subnet*
|
||||
*switch_timeout*:: Switch table entry timeout in seconds. Same as *--switch-timeout*
|
||||
*claims*:: A list of local subnets to claim. See *--claim*
|
||||
*auto-claim*:: Whether to automatically claim the device ip. See *--no-auto-claim*
|
||||
*port_forwarding*:: Whether to activate port forwardig. See *--no-port-forwarding*
|
||||
*user*:: The name of a user to run the background process under. See *--user*
|
||||
*group*:: The name of a group to run the background process under. See *--group*
|
||||
*pid_file*:: The path of the pid file to create. See *--pid-file*
|
||||
*stats_file*:: The path of the statistics file. See *--stats-file*
|
||||
*user*:: The name of a user to run the background process under. Same as *--user*
|
||||
*group*:: The name of a group to run the background process under. Same as *--group*
|
||||
*pid_file*:: The path of the pid file to create. Same as *--pid-file*
|
||||
*stats_file*:: The path of the statistics file. Same as *--stats-file*
|
||||
*statsd*:: A key-value map with statsd settings
|
||||
*server*::: Server to report statistics to. Same as *--statsd-server*
|
||||
*prefix*::: Prefix to use when reporting to statsd. Same as *--statsd-prefix*
|
||||
*hook*:: A hook script to be called for every event type. See *HOOK SCRIPTS* for info.
|
||||
*hooks*:: A map of event type to script for scripts that only fire for one event type. See *HOOK SCRIPTS* for info.
|
||||
|
||||
=== Example
|
||||
|
||||
device_type: tun
|
||||
device_name: vpncloud%d
|
||||
ifup: ifconfig $IFNAME 10.0.1.1/16 mtu 1400 up
|
||||
crypto: aes256
|
||||
shared_key: mysecret
|
||||
device:
|
||||
type: tun
|
||||
name: vpncloud%d
|
||||
ip: 10.0.1.1/16
|
||||
crypto:
|
||||
password: mysecret
|
||||
listen: 3210
|
||||
peers:
|
||||
- remote.machine.foo:3210
|
||||
- remote.machine.bar:3210
|
||||
peer_timeout: 600
|
||||
mode: normal
|
||||
subnets:
|
||||
claims:
|
||||
- 10.0.1.0/24
|
||||
port_forwarding: true
|
||||
user: nobody
|
||||
group: nogroup
|
||||
pid_file: /run/vpncloud.pid
|
||||
|
||||
|
||||
== SECURITY
|
||||
|
||||
VpnCloud uses strong cryptography based on modern cryptographic primitives.
|
||||
|
||||
Before exchanging any payload data with peers a secure connection is
|
||||
initialized based on key pairs. Each node has a key pair consisting of a
|
||||
private and a public key (*--private-key* and *--public-key*). Those key pairs
|
||||
can be generated via *genkey*.
|
||||
To allow connections, nodes need to list the public keys of all other nodes as
|
||||
trusted keys (*--trusted-key*). To simplify the key exchange, key pairs can be
|
||||
derived from passwords (*--password*). If no trusted keys are configured, nodes
|
||||
will only trust their own public key. Nodes configured with the same password
|
||||
will therefore trust each others.
|
||||
|
||||
In the initialization phase of the connection, nodes agree on a temporary key
|
||||
that is used to encrypt the next messages using a fast encryption algorithm.
|
||||
VpnCloud automatically benchmarks all supported algorithms and negotiates to
|
||||
use the fastest algorithm for each connection. Users can limit the supported
|
||||
algorithms if they wish using *--algorithm*. Although highly discouraged, users
|
||||
can opt out of encryption altogether by enabling the *plain* algorithm. (Note:
|
||||
both nodes in a connection must support this, otherwise encryption will take
|
||||
place.)
|
||||
|
||||
The temporary encryption keys are rotated periodically so they are never used
|
||||
for a longer time.
|
||||
|
||||
Please refer to the security whitepaper for more details.
|
||||
|
||||
=== CVE-2019-14899
|
||||
|
||||
The Linux kernel contains a vulnerability that affects all VPNs disregarding of
|
||||
the specific technology being used. Under some circumstances, the kernel accepts
|
||||
packets for the address range configured on the vpn interface also on other
|
||||
interfaces. This way, an attacker can test the presence of a VPN and find out
|
||||
the IPs being used. Also the attacker can with some effort inject data and
|
||||
manipulate connections that should be protected by the VPN.
|
||||
To mitigate this, the rp_filter setting should be configured to strict mode,
|
||||
which unfortunately a lot of distributions do not set as default.
|
||||
VpnCloud will detect this misconfiguration and offers to fix it via
|
||||
*--fix-rp-filter*.
|
||||
Note: This vulnerability affects all VPN technologies as it is not located in
|
||||
the VPN software but in the Linux kernel.
|
||||
|
||||
|
||||
== BEACONS
|
||||
|
||||
Beacons are short character sequences that contain a timestamp and a list of
|
||||
|
@ -349,117 +480,139 @@ suffix.
|
|||
The commands are called in separate threads, so even longer running commands
|
||||
will not block the node.
|
||||
|
||||
== NETWORK PROTOCOL
|
||||
|
||||
The protocol of VpnCloud is kept as simple as possible to allow other
|
||||
implementations and to maximize the performance.
|
||||
== STATSD SUPPORT
|
||||
|
||||
Every packet sent over UDP contains the following header (in order):
|
||||
When a statsd server is configured (either via **--statsd-server** or the
|
||||
config option **statsd_server**), VpnCloud sends out the following statistics
|
||||
every minute.
|
||||
|
||||
4 bytes *magic*::
|
||||
This field is used to identify the packet and to sort out packets that do
|
||||
not belong. The default is *[0x76, 0x70, 0x6e, 0x01]* ("vpn\x01").
|
||||
This field can be used to identify VpnCloud packets and might be set to
|
||||
something different to hide the protocol.
|
||||
Gauge values:
|
||||
*peer_count*:: Current number of peers
|
||||
*table_entries*:: Number of routing table / switch table entries
|
||||
|
||||
1 byte *crypto method*::
|
||||
This field specifies the method that must be used to decrypt the rest of the
|
||||
data. The currently supported methods are:
|
||||
The following statistics consist of two keys: *.bytes* and *.packets* that hold
|
||||
the values in bytes and packets. All values refer to the traffic during the
|
||||
last minute:
|
||||
*traffic.protocol.inbound*:: Complete incoming traffic with all peers
|
||||
*traffic.protocol.outbound*:: Complete outgoing traffic with all peers
|
||||
*traffic.payload.inbound*:: Incoming payload traffic with all peers
|
||||
*traffic.payload.outbound*:: Outgoing payload traffic with all peers
|
||||
*invalid_protocol_traffic*:: Invalid incoming protocol traffic
|
||||
*dropped_payload*:: Outgoing traffic that could not be routed
|
||||
|
||||
** Method *0*, *No encryption*: Rest of the data can be read without
|
||||
decrypting it.
|
||||
** Method *1*, *ChaCha20*: The header is followed by a 12 byte
|
||||
_nonce_. The rest of the data is encrypted with the
|
||||
*libsodium::crypto_aead_chacha20poly1305_ietf* method, using the 8 byte
|
||||
header as additional data.
|
||||
** Method *2*, *AES256*: The header is followed by a 12 byte _nonce_.
|
||||
The rest of the data is encrypted with the
|
||||
*libsodium::crypto_aead_aes256gcm* method, using the 8 byte header
|
||||
as additional data.
|
||||
All keys are prefixed by a common prefix. The prefix defaults to *vpncloud* but
|
||||
can be changed via **--statsd-prefix** or the config option **statsd_prefix**.
|
||||
|
||||
2 *reserved bytes*::
|
||||
that are currently unused and set to 0
|
||||
|
||||
1 byte for the *message type*::
|
||||
This byte specifies the type of message that follows. Currently the
|
||||
following message types are supported:
|
||||
== WEBSOCKET PROXY
|
||||
|
||||
** Type 0: Data packet
|
||||
** Type 1: Peer list
|
||||
** Type 2: Initial message
|
||||
** Type 3: Closing message
|
||||
The websocket proxy mode replaces the local UDP port by a websocket proxy to allow
|
||||
connectivity even in very restricted environments.
|
||||
|
||||
After this 8 byte header, the rest of the message follows. It is encrypted using
|
||||
the method specified in the header.
|
||||
This means that instead of listening on a local port for incoming messages and
|
||||
sending outgoing messages via this port, all UDP traffic will be forwarded to and
|
||||
received from a remote proxy via the websocket protocol. This proxy opens a UDP
|
||||
port for each VpnCloud instance that connects to it. The instance can use this port
|
||||
remotely just like it would use a real local UDP port.
|
||||
|
||||
In the decrypted data, the message as specified in the *message type* field
|
||||
will follow:
|
||||
The proxy is transparent, it does not manipulate or even decrypt the messages it
|
||||
forwards. Trust relations are still created between VpnCloud instances, not between
|
||||
an instance and the proxy. The proxy only ever sees encrypted messages. Therefore,
|
||||
the connection to it uses plain HTTP.
|
||||
|
||||
*Data packet* (message type 0)::
|
||||
This packet contains payload. The format of the data depends on the device
|
||||
type. For TUN devices, this data contains an IP packet. For TAP devices it
|
||||
contains an Ethernet frame. The data starts right after the header and ends
|
||||
at the end of the packet.
|
||||
If it is an Ethernet frame, it will start with the destination MAC and end
|
||||
with the payload. It does not contain the preamble, SFD, padding, and CRC
|
||||
fields.
|
||||
*Peer list* (message type 1)::
|
||||
This packet contains the peer list of the sender. The first byte after the
|
||||
switch byte contains the number of IPv4 addresses that follow.
|
||||
After that, the specified number of addresses follow, where each address
|
||||
is encoded in 6 bytes. The first 4 bytes are the IPv4 address and the later
|
||||
2 bytes are port number (both in network byte order).
|
||||
After those addresses, the next byte contains the number of IPv6 addresses
|
||||
that follow. After that, the specified number of addresses follow, where
|
||||
each address is encoded in 18 bytes. The first 16 bytes are the IPv6 address
|
||||
and the later 2 bytes are port number (both in network byte order).
|
||||
*Initial message* (message type 2)::
|
||||
This packet contains the following information:
|
||||
** The stage of the initialization process
|
||||
** A random node id to distinguish different nodes
|
||||
** All the local subnets claimed by the nodes
|
||||
A websocket proxy can be stared by using the *ws-proxy* subcommand. A custom port
|
||||
can be set using the *--listen* parameter. (Note that this port never conflicts
|
||||
with a VpnCloud port on the same machine since VpnCloud uses UDP and the proxy uses
|
||||
TCP.)
|
||||
|
||||
+
|
||||
Its first byte marks the stage of the initial handshake process.
|
||||
The next 16 bytes contain the unique node id. After that,
|
||||
the list of local subnets follows.
|
||||
The subnet list is encoded in the following way: Its first byte of data
|
||||
contains the number of encoded subnets that follow. After that, the given
|
||||
number of encoded subnets follow.
|
||||
For each subnet, the first byte is the length of bytes in the base address
|
||||
and is followed by the given number of base address bytes and one additional
|
||||
byte that is the prefix length of the subnet.
|
||||
The addresses for the subnet will be encoded like they are encoded in their
|
||||
native protocol (4 bytes for IPv4, 16 bytes for IPv6, and 6 bytes for a MAC
|
||||
address) with the exception of MAC addresses in a VLan which will be encoded
|
||||
in 8 bytes where the first 2 bytes are the VLan number in network byte order
|
||||
and the later 6 bytes are the MAC address.
|
||||
*Closing message* (message type 3)::
|
||||
This packet does not contain any more data.
|
||||
A VpnCloud instance can use a websocket proxy instead of opening a local port by
|
||||
specifying the websocket proxy via its *--listen* parameter
|
||||
(e.g. *--listen ws://example.com:3210*). Note that the websocket URL must start with
|
||||
*ws:\/\/*, not *http:\/\/*.
|
||||
|
||||
Nodes are expected to send an *initial message* with stage 0 whenever they
|
||||
connect to a node they were not connected to before. As a reply to this message,
|
||||
another initial should be sent with stage 1. Also a *peer list* message should
|
||||
be sent as a reply.
|
||||
|
||||
When connected, nodes should periodically send their *peer list* to all
|
||||
of their peers to spread this information and to avoid peer timeouts.
|
||||
To avoid the cubic growth of management traffic, nodes should at a certain
|
||||
network size start sending partial peer lists instead of the full list. A
|
||||
reasonable number would be about 20 peers. The subsets should be selected
|
||||
randomly.
|
||||
== HOOK SCRIPTS
|
||||
|
||||
Nodes should remove peers from their peer list after a certain period of
|
||||
inactivity or when receiving a *closing message*. Before shutting down, nodes
|
||||
should send the closing message to all of their peers in order to avoid
|
||||
receiving further data until the timeout is reached.
|
||||
VpnCloud supports calling hook scripts on certain events. The scripts can either be
|
||||
configured on the command line or in the config file. Hook scripts can either be
|
||||
configured per event type or globally.
|
||||
|
||||
When an event occurs, the specified hook script is executed using
|
||||
the current permissions of the user that started the instance. Note that this means
|
||||
that if VpnCloud is configured to drop permissions, only the events *device_setup*
|
||||
and *device_configured* will be executed using root permissions.
|
||||
|
||||
Hook scripts are executed using *sh -c*, so either binaries, shell scripts and even
|
||||
shell commands can be used. The script will be executed in parallel to the VpnCloud
|
||||
instance. Its output will be printed to the stdout of VpnCloud and the return code
|
||||
is ignored.
|
||||
|
||||
The hook script will receive information on the event using environment variables.
|
||||
The variable *EVENT* will contain the name of the event. Other variables depend on
|
||||
the event type.
|
||||
|
||||
The following event types
|
||||
|
||||
*peer_connecting*::
|
||||
A new peer connection is in the process of being established. The variable
|
||||
*PEER* contains the address of the peer but no other information is known at
|
||||
that point in time.
|
||||
Variables: *IFNAME*, *PEER*
|
||||
|
||||
*peer_connected*::
|
||||
A new peer successfully connected to this instance. Besides the peer address,
|
||||
also a list of claims (*CLAIMS*, space separated) and the node id of the new
|
||||
peer (*NODE_ID*) are given to the script.
|
||||
Variables: *IFNAME*, *PEER*, *CLAIMS*, *NODE_ID*
|
||||
|
||||
*peer_disconnected*::
|
||||
A peer connection has been closed. If the peer has been fully connected, the
|
||||
node id is given (*NODE_ID*).
|
||||
Variables: *IFNAME*, *PEER*, (*NODE_ID*)
|
||||
|
||||
*device_setup*::
|
||||
This event is fired when the virtual device has been created but not yet
|
||||
configured.
|
||||
Variables: *IFNAME*
|
||||
|
||||
*device_configured*::
|
||||
This event is fired when the virtual device is fully configured.
|
||||
Variables: *IFNAME*
|
||||
|
||||
*vpn_started*::
|
||||
This event is fired when the VPN is ready to be used.
|
||||
Variables: *IFNAME*
|
||||
|
||||
*vpn_shutdown*::
|
||||
This event is fired when the VPN s shutting down.
|
||||
Variables: *IFNAME*
|
||||
|
||||
|
||||
== DEVICE SETUP
|
||||
|
||||
The device is setup using the following steps:
|
||||
|
||||
. The device is created with the type and name given as *--type* and *--device*.
|
||||
. Depending on the device type and the main network device of the systme, the
|
||||
optimal MTU is determined and configured on the device.
|
||||
. If and IP address (and optional prefix length) is given via *--ip*, the
|
||||
interface is configured with the address and the given netmask (default:
|
||||
255.255.255.0). Also the interface is set to be active.
|
||||
. If a command is given as *--ifup*, the given command will be executed. The
|
||||
name of the interface is stored in an environment variable as "IFNAME". Note
|
||||
that VpnCloud waits for the command to exit before starting its normal
|
||||
operation.
|
||||
|
||||
Note that most of the steps will need elevated permissions, so the vpncloud
|
||||
command needs to be executed as root (e.g. via sudo). Beware that the ifup
|
||||
command will also be executed using those permissions.
|
||||
|
||||
VpnCloud can drop the elevated permissions when *--user* and *--group* is
|
||||
given.
|
||||
|
||||
Nodes should only add nodes to their peer list after receiving an initial
|
||||
message from them instead of adding them right from the peer list of another
|
||||
peer. This is necessary to avoid the case of a large network keeping dead nodes
|
||||
alive.
|
||||
|
||||
== COPYRIGHT
|
||||
|
||||
Copyright (C) 2015-2020 Dennis Schwerdel
|
||||
Copyright (C) 2015-2021 Dennis Schwerdel
|
||||
This software is licensed under GPL-3 or newer (see LICENSE.md)
|
||||
|
|
Loading…
Reference in New Issue