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
3 changes: 2 additions & 1 deletion src/crypto/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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 Down Expand Up @@ -144,7 +145,7 @@ impl Crypto {
Some(password) => {
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA256,
NonZeroU32::new(4096).unwrap(),
NonZeroU32::new(600000).unwrap(),
SALT,
password.as_bytes(),
&mut bytes,
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
13 changes: 9 additions & 4 deletions src/crypto/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ pub const STAGE_PENG: u8 = 3;
pub const WAITING_TO_CLOSE: u8 = 4;
pub const CLOSING: u8 = 5;

/// Maximum number of failed handshake retries before giving up (2 minutes at 1 retry/second)
pub const MAX_FAILED_RETRIES: usize = 120;

pub const SALTED_NODE_ID_HASH_LEN: usize = 20;

/// Maximum field length in init messages to prevent DoS attacks
const MAX_FIELD_LENGTH: usize = 65536; // 64KB
pub type SaltedNodeIdHash = [u8; SALTED_NODE_ID_HASH_LEN];

#[allow(clippy::large_enum_variant)]
Expand Down Expand Up @@ -221,6 +225,9 @@ impl InitMsg {
algorithms = Some(Algorithms { algorithm_speeds: algos, allow_unencrypted });
}
_ => {
if field_len > MAX_FIELD_LENGTH {
return Err(Error::Parse("Field length exceeds maximum allowed size"));
}
let mut data = vec![0; field_len];
r.read_exact(&mut data).map_err(|_| Error::Parse("Init message too short"))?;
}
Expand Down Expand Up @@ -459,10 +466,8 @@ impl<P: Payload> InitState<P> {
}

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()
agree_ephemeral(privk, pubk, |k| UnboundKey::new(algo, &k[..algo.key_len()]).map(LessSafeKey::new).unwrap())
.unwrap()
}

fn create_ecdh_keypair(&self) -> (EcdhPrivateKey, EcdhPublicKey) {
Expand Down
6 changes: 3 additions & 3 deletions src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use std::{
fs::{self, File},
io::{self, BufRead, BufReader, Cursor, Error as IoError, Read, Write},
net::{Ipv4Addr, UdpSocket},
os::{unix::io::AsRawFd, fd::RawFd},
os::{fd::RawFd, unix::io::AsRawFd},
str,
str::FromStr
str::FromStr,
};

use crate::{crypto, error::Error, util::MsgBuffer};
Expand All @@ -23,7 +23,7 @@ static TUNSETIFF: libc::c_ulong = 1074025674;
#[derive(Copy, Clone)]
struct IfReqDataAddr {
af: libc::c_int,
addr: Ipv4Addr
addr: Ipv4Addr,
}

#[repr(C)]
Expand Down
6 changes: 4 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,10 @@ fn run<P: Protocol, S: Socket>(config: Config, socket: S) {
Some(file)
}
};
let mut cloud =
GenericCloud::<TunTapDevice, P, S, SystemTimeSource>::new(&config, socket, device, port_forwarding, stats_file);
let mut cloud = try_fail!(
GenericCloud::<TunTapDevice, P, S, SystemTimeSource>::new(&config, socket, device, port_forwarding, stats_file),
"Failed to create VPN cloud: {}"
);
for mut addr in config.peers {
if addr.find(':').unwrap_or(0) <= addr.find(']').unwrap_or(0) {
// : not present or only in IPv6 address
Expand Down
Loading