Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
name: Release

on:
push:
tags:
- 'v*'

env:
CARGO_TERM_COLOR: always

jobs:
build:
name: Build ${{ matrix.target_name }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
target_name: amd64
target_name_rpm: x86_64
musl_target: x86_64-unknown-linux-musl
- target: i686-unknown-linux-gnu
target_name: i386
target_name_rpm: i686
musl_target: i686-unknown-linux-musl
- target: aarch64-unknown-linux-gnu
target_name: arm64
target_name_rpm: aarch64
musl_target: aarch64-unknown-linux-musl
- target: armv7-unknown-linux-gnueabihf
target_name: armhf
target_name_rpm: ""
musl_target: armv7-unknown-linux-musleabihf
- target: armv5te-unknown-linux-gnueabi
target_name: armel
target_name_rpm: ""
musl_target: armv5te-unknown-linux-musleabi

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- name: Install cross-compilation tools
run: |
cargo install cross
cargo install cargo-deb
cargo install cargo-generate-rpm

- name: Install UPX
run: |
UPX_VERSION=$(grep -e '^upx_version =' Cargo.toml | sed -e 's/upx_version = "\(.*\)"/\1/')
wget https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz
tar -xJf upx-${UPX_VERSION}-amd64_linux.tar.xz
sudo mv upx-${UPX_VERSION}-amd64_linux/upx /usr/local/bin/

- name: Generate manpage
run: |
sudo apt-get install -y asciidoctor
mkdir -p target
asciidoctor -b manpage -o target/vpncloud.1 vpncloud.adoc
gzip -f target/vpncloud.1

- name: Get version
id: version
run: |
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Build packages
run: |
VERSION=${{ steps.version.outputs.version }}
TARGET=${{ matrix.target }}
TARGET_NAME=${{ matrix.target_name }}
TARGET_DIR=target/$TARGET_NAME
MUSL_TARGET=${{ matrix.musl_target }}
MUSL_DIR=target/${TARGET_NAME}-musl

# Create dist directory
mkdir -p dist

# Build standard package
echo "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 package
echo "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 package if applicable
if [ -n "${{ matrix.target_name_rpm }}" ]; then
echo "Building rpm package"
cargo generate-rpm --target $TARGET --target-dir $TARGET_DIR
mv $TARGET_DIR/$TARGET/generate-rpm/vpncloud-${VERSION}-1.${{ matrix.target_name_rpm }}.rpm dist/vpncloud_${VERSION}-1.${{ matrix.target_name_rpm }}.rpm
fi

# Build static binary with musl
echo "Compiling for $TARGET_NAME musl"
cross build --release --features installer --target $MUSL_TARGET --target-dir $MUSL_DIR
upx --lzma $MUSL_DIR/$MUSL_TARGET/release/vpncloud
cp $MUSL_DIR/$MUSL_TARGET/release/vpncloud dist/vpncloud_${VERSION}_static_${TARGET_NAME}

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: packages-${{ matrix.target_name }}
path: dist/

release:
name: Create Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Get version
id: version
run: |
VERSION=$(grep -e '^version =' Cargo.toml | sed -e 's/version = "\(.*\)"/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Prepare release assets
run: |
VERSION=${{ steps.version.outputs.version }}
mkdir -p release

# Copy all packages to release directory
find artifacts -name "*.deb" -exec cp {} release/ \;
find artifacts -name "*.rpm" -exec cp {} release/ \;
find artifacts -name "*static*" -exec cp {} release/ \;

# Generate SHA256 checksums
cd release
sha256sum * > vpncloud_${VERSION}_SHA256SUMS.txt

- name: Check GPG key availability
id: gpg_check
run: |
if [ -n "${{ secrets.GPG_PRIVATE_KEY }}" ]; then
echo "available=true" >> $GITHUB_OUTPUT
else
echo "available=false" >> $GITHUB_OUTPUT
echo "::notice::GPG_PRIVATE_KEY not configured, skipping signature"
fi

- name: Import GPG key
if: steps.gpg_check.outputs.available == 'true'
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}

- name: Sign checksums
if: steps.gpg_check.outputs.available == 'true'
run: |
VERSION=${{ steps.version.outputs.version }}
cd release
gpg --armor --output vpncloud_${VERSION}_SHA256SUMS.txt.asc --detach-sig vpncloud_${VERSION}_SHA256SUMS.txt

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: VpnCloud ${{ steps.version.outputs.version }}
draft: false
prerelease: false
generate_release_notes: true
files: |
release/*.deb
release/*.rpm
release/*static*
release/*SHA256SUMS*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish-crate:
name: Publish to crates.io
needs: release
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Check crates.io token
id: crate_check
run: |
if [ -n "${{ secrets.CARGO_REGISTRY_TOKEN }}" ]; then
echo "available=true" >> $GITHUB_OUTPUT
else
echo "available=false" >> $GITHUB_OUTPUT
echo "::notice::CARGO_REGISTRY_TOKEN not configured, skipping crates.io publish"
fi

- name: Publish to crates.io
if: steps.crate_check.outputs.available == 'true'
run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
8 changes: 5 additions & 3 deletions src/cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
#[allow(clippy::too_many_arguments)]
pub fn new(
config: &Config, socket: S, device: D, port_forwarding: Option<PortForwarding>, stats_file: Option<File>,
) -> Self {
) -> Result<Self, Error> {
let (learning, broadcast) = match config.mode {
Mode::Normal => match config.device_type {
Type::Tap => (true, true),
Expand Down Expand Up @@ -131,7 +131,7 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
let now = TS::now();
let update_freq = config.get_keepalive() as u16;
let node_id = random();
let crypto = Crypto::new(node_id, &config.crypto).unwrap();
let crypto = Crypto::new(node_id, &config.crypto)?;
let beacon_key = config.beacon_password.as_ref().map(|s| s.as_bytes()).unwrap_or(&[]);
let mut res = GenericCloud {
node_id,
Expand Down Expand Up @@ -163,7 +163,7 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
_dummy_ts: PhantomData,
};
res.initialize();
res
Ok(res)
}

#[inline]
Expand All @@ -180,8 +180,10 @@ impl<D: Device, P: Protocol, S: Socket, TS: TimeSource> GenericCloud<D, P, S, TS
#[inline]
fn broadcast_msg(&mut self, type_: u8, msg: &mut MsgBuffer) -> Result<(), Error> {
debug!("Broadcasting message type {}, {:?} bytes to {} peers", type_, msg.len(), self.peers.len());
// Reuse a single buffer for all peers to avoid repeated allocations
let mut msg_data = MsgBuffer::new(100);
for (addr, peer) in &mut self.peers {
// Reset buffer to original message state for each peer
msg_data.set_start(msg.get_start());
msg_data.set_length(msg.len());
msg_data.message_mut().clone_from_slice(msg.message());
Expand Down
14 changes: 14 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ impl Config {
if !file.crypto.algorithms.is_empty() {
self.crypto.algorithms = file.crypto.algorithms.clone();
}
if let Some(val) = file.crypto.pbkdf2_iterations {
self.crypto.pbkdf2_iterations = Some(val);
}
if let Some(val) = file.hook {
self.hook = Some(val)
}
Expand Down Expand Up @@ -290,6 +293,9 @@ impl Config {
if !args.algorithms.is_empty() {
self.crypto.algorithms = args.algorithms.clone();
}
if let Some(val) = args.pbkdf2_iterations {
self.crypto.pbkdf2_iterations = Some(val);
}
for s in args.hook {
if s.contains(':') {
let pos = s.find(':').unwrap();
Expand Down Expand Up @@ -414,6 +420,10 @@ pub struct Args {
#[structopt(long = "algorithm", alias = "algo", use_delimiter=true, case_insensitive = true, possible_values=&["plain", "aes128", "aes256", "chacha20"])]
pub algorithms: Vec<String>,

/// PBKDF2 iteration count for password-based key derivation (default: 4096)
#[structopt(long)]
pub pbkdf2_iterations: Option<u32>,

/// The local subnets to claim (IP or IP/prefix)
#[structopt(long = "claim", use_delimiter = true)]
pub claims: Vec<String>,
Expand Down Expand Up @@ -542,6 +552,10 @@ pub enum Command {
/// The shared password to encrypt all traffic
#[structopt(short, long, env)]
password: Option<String>,

/// PBKDF2 iteration count for password-based key derivation (default: 4096)
#[structopt(long)]
pbkdf2_iterations: Option<u32>,
},

/// Run a websocket proxy
Expand Down
14 changes: 9 additions & 5 deletions src/crypto/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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;
const DEFAULT_PBKDF2_ITERATIONS: u32 = 4096;

pub type Ed25519PublicKey = [u8; ED25519_PUBLIC_KEY_LEN];
pub type EcdhPublicKey = UnparsedPublicKey<SmallVec<[u8; 96]>>;
Expand All @@ -34,6 +35,7 @@ const SPEED_TEST_TIME: f32 = 0.02;
#[cfg(not(test))]
const SPEED_TEST_TIME: f32 = 0.1;

/// Interval in seconds for symmetric key rotation (2 minutes)
const ROTATE_INTERVAL: usize = 120;

pub trait Payload: Debug + PartialEq + Sized {
Expand All @@ -55,6 +57,7 @@ pub struct Config {
pub public_key: Option<String>,
pub trusted_keys: Vec<String>,
pub algorithms: Vec<String>,
pub pbkdf2_iterations: Option<u32>,
}

pub struct Crypto {
Expand Down Expand Up @@ -94,7 +97,8 @@ impl Crypto {
Self::parse_private_key(priv_key)?
}
} else if let Some(password) = &config.password {
Self::keypair_from_password(password)
let iterations = config.pbkdf2_iterations.unwrap_or(DEFAULT_PBKDF2_ITERATIONS);
Self::keypair_from_password(password, iterations)
} else {
return Err(Error::InvalidConfig("Either private_key or password must be set"));
};
Expand Down Expand Up @@ -134,7 +138,7 @@ impl Crypto {
})
}

pub fn generate_keypair(password: Option<&str>) -> (String, String) {
pub fn generate_keypair(password: Option<&str>, iterations: u32) -> (String, String) {
let mut bytes = [0; 32];
match password {
None => {
Expand All @@ -144,7 +148,7 @@ impl Crypto {
Some(password) => {
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA256,
NonZeroU32::new(4096).unwrap(),
NonZeroU32::new(iterations).unwrap(),
SALT,
password.as_bytes(),
&mut bytes,
Expand All @@ -157,9 +161,9 @@ impl Crypto {
(privkey, pubkey)
}

fn keypair_from_password(password: &str) -> Ed25519KeyPair {
fn keypair_from_password(password: &str, iterations: u32) -> Ed25519KeyPair {
let mut key = [0; 32];
pbkdf2::derive(pbkdf2::PBKDF2_HMAC_SHA256, NonZeroU32::new(4096).unwrap(), SALT, password.as_bytes(), &mut key);
pbkdf2::derive(pbkdf2::PBKDF2_HMAC_SHA256, NonZeroU32::new(iterations).unwrap(), SALT, password.as_bytes(), &mut key);
Ed25519KeyPair::from_seed_unchecked(&key).unwrap()
}

Expand Down
3 changes: 3 additions & 0 deletions src/crypto/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ use std::{

use crate::{error::Error, util::MsgBuffer};

/// Nonce length for AES-GCM and ChaCha20-Poly1305 (96 bits)
const NONCE_LEN: usize = 12;
/// Authentication tag length for AES-GCM and ChaCha20-Poly1305 (128 bits)
pub const TAG_LEN: usize = 16;
/// Extra bytes for encrypted messages (nonce + tag)
pub const EXTRA_LEN: usize = 8;

fn random_data(size: usize) -> Vec<u8> {
Expand Down
Loading