diff --git a/Makefile b/Makefile index 88dddeb6d9..62fb7e3689 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ all: build-wireguard build-nym-vpn-core build-wireguard: ./wireguard/build-wireguard-go.sh +build-amnezia-wg: + ./wireguard/build-wireguard-go.sh --amnezia + build-wireguard-ios: ./wireguard/build-wireguard-go.sh --ios diff --git a/nym-vpn-core/crates/nym-vpn-lib/build.rs b/nym-vpn-core/crates/nym-vpn-lib/build.rs index 17b0b39446..425025e065 100644 --- a/nym-vpn-core/crates/nym-vpn-lib/build.rs +++ b/nym-vpn-core/crates/nym-vpn-lib/build.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use std::{env, path::PathBuf}; use vergen::EmitBuilder; fn main() -> Result<(), Box> { @@ -11,5 +12,39 @@ fn main() -> Result<(), Box> { .all_cargo() .emit() .expect("failed to extract build metadata"); + + let manifest_path = env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir is not set"); + let target = env::var("TARGET").expect("target is not set"); + let target_os = env::var("CARGO_CFG_TARGET_OS").expect("target os is not set"); + + let mut build_dir = PathBuf::from(manifest_path) + .join("../../../build/lib") + .canonicalize() + .expect("failed to canonicalize build dir path"); + + build_dir.push(target); + + // CI may only provide universal builds + if target_os == "macos" { + let target_dir_exists = build_dir + .try_exists() + .expect("failed to check existence of target dir"); + + if !target_dir_exists { + build_dir.pop(); + build_dir.push("universal-apple-darwin"); + } + } + + println!("cargo::rustc-link-search={}", build_dir.display()); + + let link_type = match target_os.as_str() { + "android" => "", + "linux" | "macos" | "ios" => "=static", + "windows" => "dylib", + _ => panic!("Unsupported platform: {}", target_os), + }; + println!("cargo:rustc-link-lib{}=wg", link_type); + Ok(()) } diff --git a/nym-vpn-core/crates/nym-vpnd/build.rs b/nym-vpn-core/crates/nym-vpnd/build.rs index 17b0b39446..425025e065 100644 --- a/nym-vpn-core/crates/nym-vpnd/build.rs +++ b/nym-vpn-core/crates/nym-vpnd/build.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use std::{env, path::PathBuf}; use vergen::EmitBuilder; fn main() -> Result<(), Box> { @@ -11,5 +12,39 @@ fn main() -> Result<(), Box> { .all_cargo() .emit() .expect("failed to extract build metadata"); + + let manifest_path = env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir is not set"); + let target = env::var("TARGET").expect("target is not set"); + let target_os = env::var("CARGO_CFG_TARGET_OS").expect("target os is not set"); + + let mut build_dir = PathBuf::from(manifest_path) + .join("../../../build/lib") + .canonicalize() + .expect("failed to canonicalize build dir path"); + + build_dir.push(target); + + // CI may only provide universal builds + if target_os == "macos" { + let target_dir_exists = build_dir + .try_exists() + .expect("failed to check existence of target dir"); + + if !target_dir_exists { + build_dir.pop(); + build_dir.push("universal-apple-darwin"); + } + } + + println!("cargo::rustc-link-search={}", build_dir.display()); + + let link_type = match target_os.as_str() { + "android" => "", + "linux" | "macos" | "ios" => "=static", + "windows" => "dylib", + _ => panic!("Unsupported platform: {}", target_os), + }; + println!("cargo:rustc-link-lib{}=wg", link_type); + Ok(()) } diff --git a/nym-vpn-core/crates/nym-wg-go/Cargo.toml b/nym-vpn-core/crates/nym-wg-go/Cargo.toml index 533ad587a5..aaed1be3e1 100644 --- a/nym-vpn-core/crates/nym-wg-go/Cargo.toml +++ b/nym-vpn-core/crates/nym-wg-go/Cargo.toml @@ -8,6 +8,10 @@ documentation.workspace = true edition.workspace = true license.workspace = true +[features] +default = [] +amnezia = [] + [dependencies] ipnetwork.workspace = true thiserror.workspace = true diff --git a/nym-vpn-core/crates/nym-wg-go/src/netstack.rs b/nym-vpn-core/crates/nym-wg-go/src/netstack.rs index 0da81c1825..003e2fd140 100644 --- a/nym-vpn-core/crates/nym-wg-go/src/netstack.rs +++ b/nym-vpn-core/crates/nym-wg-go/src/netstack.rs @@ -20,6 +20,8 @@ pub struct InterfaceConfig { pub local_addrs: Vec, pub dns_addrs: Vec, pub mtu: u16, + #[cfg(feature = "amnezia")] + pub azwg_config: Option, } impl fmt::Debug for InterfaceConfig { @@ -33,6 +35,47 @@ impl fmt::Debug for InterfaceConfig { } } +/// Hold Amnezia-wireguard configuration parameters. +/// +/// All parameters should be the same between Client and Server, except Jc - it can vary. +/// +/// - Jc — 1 ≤ Jc ≤ 128; recommended range is from 3 to 10 inclusive +/// - Jmin — Jmin < Jmax; recommended value is 50 +/// - Jmax — Jmin < Jmax ≤ 1280; recommended value is 1000 +/// - S1 — S1 < 1280; S1 + 56 ≠ S2; recommended range is from 15 to 150 inclusive +/// - S2 — S2 < 1280; recommended range is from 15 to 150 inclusive +/// - H1/H2/H3/H4 — must be unique among each other; +/// recommended range is from 5 to 2_147_483_647 (2^31 - 1 i.e. signed 32 bit int) inclusive +#[cfg(feature = "amnezia")] +#[derive(Debug)] +pub struct AmneziaConfig { + pub junk_packet_count: i32, // Jc + pub junk_packet_min_size: i32, // Jmin + pub junk_packet_max_size: i32, // Jmax + pub init_packet_junk_size: i32, // S0 + pub response_packet_junk_size: i32, // S1 + pub init_packet_magic_header: u32, // H1 + pub response_packet_magic_header: u32, // H2 + pub under_load_packet_magic_header: u32, // H3 + pub transport_packet_magic_header: u32, // H4 +} + +#[cfg(feature = "amnezia")] +impl Default for AmneziaConfig { + fn default() -> Self { + Self { + junk_packet_count: 4_i32, + junk_packet_min_size: 40_i32, + junk_packet_max_size: 70_i32, + init_packet_junk_size: 0_i32, + response_packet_junk_size: 0_i32, + init_packet_magic_header: 1_u32, + response_packet_magic_header: 2_u32, + under_load_packet_magic_header: 3_u32, + transport_packet_magic_header: 4_u32, + } + } +} /// Netstack configuration. #[derive(Debug)] pub struct Config { diff --git a/nym-vpn-core/crates/nym-wg-go/src/wireguard_go.rs b/nym-vpn-core/crates/nym-wg-go/src/wireguard_go.rs index f37a6154e8..2f347beb09 100644 --- a/nym-vpn-core/crates/nym-wg-go/src/wireguard_go.rs +++ b/nym-vpn-core/crates/nym-wg-go/src/wireguard_go.rs @@ -20,6 +20,8 @@ pub struct InterfaceConfig { pub mtu: u16, #[cfg(target_os = "linux")] pub fwmark: Option, + #[cfg(feature = "amnezia")] + pub azwg_config: Option, } impl fmt::Debug for InterfaceConfig { @@ -33,6 +35,47 @@ impl fmt::Debug for InterfaceConfig { d.finish() } } +/// Hold Amnezia-wireguard configuration parameters. +/// +/// All parameters should be the same between Client and Server, except Jc - it can vary. +/// +/// - Jc — 1 ≤ Jc ≤ 128; recommended range is from 3 to 10 inclusive +/// - Jmin — Jmin < Jmax; recommended value is 50 +/// - Jmax — Jmin < Jmax ≤ 1280; recommended value is 1000 +/// - S1 — S1 < 1280; S1 + 56 ≠ S2; recommended range is from 15 to 150 inclusive +/// - S2 — S2 < 1280; recommended range is from 15 to 150 inclusive +/// - H1/H2/H3/H4 — must be unique among each other; +/// recommended range is from 5 to 2_147_483_647 (2^31 - 1 i.e. signed 32 bit int) inclusive +#[cfg(feature = "amnezia")] +#[derive(Debug)] +pub struct AmneziaConfig { + pub junk_packet_count: i32, // Jc + pub junk_packet_min_size: i32, // Jmin + pub junk_packet_max_size: i32, // Jmax + pub init_packet_junk_size: i32, // S0 + pub response_packet_junk_size: i32, // S1 + pub init_packet_magic_header: u32, // H1 + pub response_packet_magic_header: u32, // H2 + pub under_load_packet_magic_header: u32, // H3 + pub transport_packet_magic_header: u32, // H4 +} + +#[cfg(feature = "amnezia")] +impl Default for AmneziaConfig { + fn default() -> Self { + Self { + junk_packet_count: 4_i32, + junk_packet_min_size: 40_i32, + junk_packet_max_size: 70_i32, + init_packet_junk_size: 0_i32, + response_packet_junk_size: 0_i32, + init_packet_magic_header: 1_u32, + response_packet_magic_header: 2_u32, + under_load_packet_magic_header: 3_u32, + transport_packet_magic_header: 4_u32, + } + } +} /// Classic WireGuard configuration. #[derive(Debug)] diff --git a/wireguard/build-wireguard-go.sh b/wireguard/build-wireguard-go.sh index f7cd2110bf..d16584f20e 100755 --- a/wireguard/build-wireguard-go.sh +++ b/wireguard/build-wireguard-go.sh @@ -2,52 +2,67 @@ # This script is used to build wireguard-go libraries for all the platforms. +TEMP=$(getopt -o aiz --long android,docker,ios,amnezia \ + -n 'build-wireguard-go.sh' -- "$@") + +if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi + +# Note the quotes around '$TEMP': they are essential! +eval set -- "$TEMP" + +ANDROID_BUILD=false +IOS_BUILD=false +DOCKER_BUILD=true +AMNEZIA_BUILD=false +while true; do + case "$1" in + "-a" | "--android" ) ANDROID_BUILD=true; shift ;; + "-i" | "--ios" ) IOS_BUILD=true; shift ;; + "-z" | "--amnezia" ) AMNEZIA_BUILD=true; shift ;; + "--no-docker" ) DOCKER_BUILD=false; shift ;; + -- ) shift; break ;; + * ) break ;; + esac +done + set -eu function is_android_build { - for arg in "$@" - do - case "$arg" in - "--android") - return 0 - esac - done + if [ "$ANDROID_BUILD" = true ]; then + return 0 + fi return 1 } function is_ios_build { - for arg in "$@" - do - case "$arg" in - "--ios") - return 0 - esac - done + if [ "$IOS_BUILD" = true ]; then + return 0 + fi return 1 } function is_docker_build { - for arg in "$@" - do - case "$arg" in - "--no-docker") - return 1 - esac - done - return 0 + if [ "$DOCKER_BUILD" = true ]; then + return 0 + fi + return 1 } -function is_win_arm64 { - for arg in "$@" - do - case "$arg" in - "--arm64") - return 0 - esac - done +function is_amnezia_build { + if [ "$AMNEZIA_BUILD" = true ]; then + return 0 + fi return 1 } +function win_deduce_lib_executable_path { + msbuild_path="$(which msbuild.exe)" + msbuild_dir=$(dirname "$msbuild_path") + find "$msbuild_dir/../../../../" -name "lib.exe" | \ + grep -i "hostx64/x64" | \ + head -n1 +} + function win_gather_export_symbols { grep -Eo "\/\/export \w+" libwg.go libwg_windows.go | cut -d' ' -f2 } @@ -75,8 +90,16 @@ function win_create_lib_file { } function build_windows { +<<<<<<< HEAD export CGO_ENABLED=1 export GOOS=windows +======= + echo "Building wireguard-go for Windows" + pushd $LIB_DIR + export CGO_ENABLED=1 + go build -trimpath -v -o libwg.dll -buildmode c-shared + win_create_lib_file +>>>>>>> c0b0f37b (initial compiling amnezia lib exchanged for wireguard-go) if is_win_arm64 $@; then local arch="aarch64" @@ -132,7 +155,7 @@ function build_unix { fi fi - pushd libwg + pushd $LIB_DIR create_folder_and_build $1 popd } @@ -141,14 +164,14 @@ function build_android { echo "Building for android" local docker_image_hash="992c4d5c7dcd00eacf6f3e3667ce86b8e185f011352bdd9f79e467fef3e27abd" - if is_docker_build $@; then + if is_docker_build; then docker run --rm \ -v "$(pwd)/../":/workspace \ - --entrypoint "/workspace/wireguard/libwg/build-android.sh" \ + --entrypoint "/workspace/wireguard/$LIB_DIR/build-android.sh" \ --env ANDROID_NDK_HOME="/opt/android/android-ndk-r20b" \ docker.io/pronebird1337/nymtech-android-app@sha256:$docker_image_hash else - ./libwg/build-android.sh + ./$LIB_DIR/build-android.sh fi } @@ -166,7 +189,7 @@ function build_macos_universal { export MACOSX_DEPLOYMENT_TARGET=10.13 echo "🍎 Building for aarch64" - pushd libwg + pushd $LIB_DIR export GOOS=darwin export GOARCH=arm64 create_folder_and_build "aarch64-apple-darwin" @@ -189,7 +212,7 @@ function build_ios { export CGO_ENABLED=1 export IPHONEOS_DEPLOYMENT_TARGET=16.0 - pushd libwg + pushd $LIB_DIR echo "🍎 Building for ios/aarch64" export GOARCH=arm64 @@ -240,17 +263,23 @@ function patch_darwin_goruntime { REAL_GOROOT=$(go env GOROOT 2>/dev/null) export GOROOT="$BUILDDIR/goroot" mkdir -p "$GOROOT" - rsync -a --delete --exclude=pkg/obj/go-build "$REAL_GOROOT/" "$GOROOT/" - cat libwg/goruntime-boottime-over-monotonic-darwin.diff | patch -p1 -f -N -r- -d "$GOROOT" + rsync -a --delete --exclude=pkg/obj/go-build "$REAL_GOROOT/" "$GOROOT/" + cat $LIB_DIR/goruntime-boottime-over-monotonic-darwin.diff | patch -p1 -f -N -r- -d "$GOROOT" } function build_wireguard_go { - if is_android_build $@; then + + if is_amnezia_build ; then + LIB_DIR=$AMNEZIA_DIR + echo "amnezia wireguard build enabled" + fi + + if is_android_build ; then build_android $@ return fi - if is_ios_build $@; then + if is_ios_build ; then build_ios $@ return fi @@ -263,6 +292,9 @@ function build_wireguard_go { esac } +AMNEZIA_DIR="libamnezia" +LIB_DIR="libwg" + # Ensure we are in the correct directory for the execution of this script script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $script_dir diff --git a/wireguard/libamnezia/Android.mk b/wireguard/libamnezia/Android.mk new file mode 100644 index 0000000000..1f8603ed54 --- /dev/null +++ b/wireguard/libamnezia/Android.mk @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright © 2017-2019 WireGuard LLC. All Rights Reserved. + +DESTDIR ?= $(CURDIR)/../../build/lib/$(RUST_TARGET_TRIPLE) + +NDK_GO_ARCH_MAP_x86 := 386 +NDK_GO_ARCH_MAP_x86_64 := amd64 +NDK_GO_ARCH_MAP_arm := arm +NDK_GO_ARCH_MAP_arm64 := arm64 +NDK_GO_ARCH_MAP_mips := mipsx +NDK_GO_ARCH_MAP_mips64 := mips64x + +export CGO_CFLAGS := $(CFLAGS) +export CGO_LDFLAGS := $(LDFLAGS) +export CC := $(ANDROID_C_COMPILER) +export GOARCH := $(NDK_GO_ARCH_MAP_$(ANDROID_ARCH_NAME)) +export GOOS := android +export CGO_ENABLED := 1 + +default: $(DESTDIR)/libwg.so + +GOBUILDARCH := $(NDK_GO_ARCH_MAP_$(shell uname -m)) +GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]') +GOBUILDVERSION := 1.22.6 +# TODO: Add checksum? +GOBUILDTARBALL := https://go.dev/dl/go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz +GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH) + +$(DESTDIR)/libwg.so: + mkdir -p $(DESTDIR) + go get -tags "linux android" + chmod -fR +w "$(GOPATH)/pkg/mod" + go build -tags "linux android" -ldflags="-X main.socketDirectory=/data/data/$(ANDROID_PACKAGE_NAME)/cache/wireguard" -v -o "$@" -buildmode c-shared + rm -f $(DESTDIR)/libwg.h + + +clean: + rm -f $(DESTDIR)/libwg.so diff --git a/wireguard/libamnezia/Dockerfile_AndroidPatchedGoruntime b/wireguard/libamnezia/Dockerfile_AndroidPatchedGoruntime new file mode 100644 index 0000000000..cbdec91784 --- /dev/null +++ b/wireguard/libamnezia/Dockerfile_AndroidPatchedGoruntime @@ -0,0 +1,50 @@ +# To build the image: +# docker build . -t docker.io/pronebird1337/nymtech-android-app -f Dockerfile_AndroidPatchedGoruntime +# To push the image to docker.io: +# docker push docker.io/pronebird1337/nymtech-android-app + +FROM debian@sha256:77f46c1cf862290e750e913defffb2828c889d291a93bdd10a7a0597720948fc + +RUN apt-get update -y && apt-get install -y \ + curl \ + file \ + gcc \ + git \ + make \ + python \ + unzip + +# Install Android NDK +RUN cd /tmp && \ + curl -sf -L -o ndk.zip https://dl.google.com/android/repository/android-ndk-r20b-linux-x86_64.zip && \ + echo "8381c440fe61fcbb01e209211ac01b519cd6adf51ab1c2281d5daad6ca4c8c8c ndk.zip" | sha256sum -c - && \ + mkdir /opt/android && \ + cd /opt/android && \ + unzip -q /tmp/ndk.zip && \ + rm /tmp/ndk.zip + + +ENV ANDROID_NDK_HOME="/opt/android/android-ndk-r20b" +ENV NDK_TOOLCHAIN_DIR="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin" + +ENV GOLANG_VERSION=1.22.6 +ENV GOLANG_HASH=999805bed7d9039ec3da1a53bfbcafc13e367da52aa823cb60b68ba22d44c616 + +# Install Go-lang and patch it to use the appropriate monotonic clock +COPY goruntime-boottime-over-monotonic.diff /opt/goruntime-boottime-over-monotonic.diff +RUN cd /tmp && \ + curl -sf -L -o go.tgz https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz && \ + echo "${GOLANG_HASH} go.tgz" | sha256sum -c - && \ + cd /opt && \ + tar -xzf /tmp/go.tgz && \ + rm /tmp/go.tgz && \ + patch -p1 -f -N -r- -d "/opt/go" < /opt/goruntime-boottime-over-monotonic.diff + +ENV PATH=${PATH}:/opt/go/bin +ENV GOROOT=/opt/go +ENV GOPATH=/opt/go-path + +RUN apt-get remove -y curl && \ + apt-get autoremove -y + +ENTRYPOINT [] diff --git a/wireguard/libamnezia/README.md b/wireguard/libamnezia/README.md new file mode 100644 index 0000000000..e5b96928f7 --- /dev/null +++ b/wireguard/libamnezia/README.md @@ -0,0 +1,26 @@ +# Introduction + +`libwg` is a tiny wrapper around `wireguard-go`, with the main purpose of providing a simple FFI-friendly interface. + +It currently offers support for the following platforms: + +- Linux +- macOS +- Android +- Windows + +# Organization + +`libwg.go` has shared code that is used on all platforms. + +`libwg_default.go` has default implementations for Linux-based systems. + +`libwg_android.go` has code specifically for Android. + +`libwg_windows.go` has code specifically for Windows. + +# Usage + +Call `wgTurnOn` to create and activate a tunnel. The prototype is different on different platforms, see the code for details. + +Call `wgTurnOff` to destroy the tunnel. diff --git a/wireguard/libamnezia/build-android.sh b/wireguard/libamnezia/build-android.sh new file mode 100755 index 0000000000..0d269919f2 --- /dev/null +++ b/wireguard/libamnezia/build-android.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +set -eu + +# Ensure we are in the correct directory for the execution of this script +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd $script_dir + +# Keep a GOPATH in the build directory to maintain a cache of downloaded libraries +export GOPATH=$script_dir/../../build/android-go-path/ +mkdir -p $GOPATH + +for arch in ${ARCHITECTURES:-armv7 aarch64 x86_64 i686}; do + case "$arch" in + "aarch64") + export ANDROID_C_COMPILER="${NDK_TOOLCHAIN_DIR}/aarch64-linux-android21-clang" + export ANDROID_STRIP_TOOL="${NDK_TOOLCHAIN_DIR}/aarch64-linux-android-strip" + export RUST_TARGET_TRIPLE="aarch64-linux-android" + export ANDROID_ABI="arm64-v8a" + export ANDROID_ARCH_NAME="arm64" + ;; + "x86_64") + export ANDROID_C_COMPILER="${NDK_TOOLCHAIN_DIR}/x86_64-linux-android21-clang" + export ANDROID_STRIP_TOOL="${NDK_TOOLCHAIN_DIR}/x86_64-linux-android-strip" + export RUST_TARGET_TRIPLE="x86_64-linux-android" + export ANDROID_ABI="x86_64" + export ANDROID_ARCH_NAME="x86_64" + ;; + "armv7") + export ANDROID_C_COMPILER="${NDK_TOOLCHAIN_DIR}/armv7a-linux-androideabi21-clang" + export ANDROID_STRIP_TOOL="${NDK_TOOLCHAIN_DIR}/arm-linux-androideabi-strip" + export RUST_TARGET_TRIPLE="armv7-linux-androideabi" + export ANDROID_ABI="armeabi-v7a" + export ANDROID_ARCH_NAME="arm" + ;; + "i686") + export ANDROID_C_COMPILER="${NDK_TOOLCHAIN_DIR}/i686-linux-android21-clang" + export ANDROID_STRIP_TOOL="${NDK_TOOLCHAIN_DIR}/i686-linux-android-strip" + export RUST_TARGET_TRIPLE="i686-linux-android" + export ANDROID_ABI="x86" + export ANDROID_ARCH_NAME="x86" + ;; + esac + + # Build Wireguard-Go + echo $(pwd) + make -f Android.mk clean + +# this is determined by the NDK +# export CFLAGS="-D__ANDROID_API__=21" + + make -f Android.mk + + # Strip and copy the libray to `android/build/extraJni/$ANDROID_ABI` to be able to build the APK + UNSTRIPPED_LIB_PATH="../../build/lib/$RUST_TARGET_TRIPLE/libwg.so" + STRIPPED_LIB_PATH="../../android/app/build/extraJni/$ANDROID_ABI/libwg.so" + + # Create the directories with RWX permissions for all users so that the build server can clean + # the directories afterwards + mkdir -m 777 -p "$(dirname "$STRIPPED_LIB_PATH")" + + cp "$UNSTRIPPED_LIB_PATH" "$STRIPPED_LIB_PATH" + +# this is not available in the newer NDK +# $ANDROID_STRIP_TOOL --strip-unneeded --strip-debug -o "$STRIPPED_LIB_PATH" "$UNSTRIPPED_LIB_PATH" + + # Set permissions so that the build server can clean the outputs afterwards + chmod 777 "$STRIPPED_LIB_PATH" + + rm -rf build +done + +# ensure `git clean -fd` does not require root permissions +find $GOPATH -exec chmod +rw {} \; diff --git a/wireguard/libamnezia/container/container.go b/wireguard/libamnezia/container/container.go new file mode 100644 index 0000000000..226616dbc0 --- /dev/null +++ b/wireguard/libamnezia/container/container.go @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Copyright (C) 2017-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2020 Mullvad VPN AB. All Rights Reserved. + * Copyright 2024 - Nym Technologies SA + */ + +package container + +import ( + "errors" + "math" +) + +// Generic index-based memory storage +type Container[Context any] struct { + tunnels map[int32]Context +} + +func New[Context any]() Container[Context] { + return Container[Context]{ + tunnels: make(map[int32]Context), + } +} + +func (wself *Container[Context]) Insert(context Context) (int32, error) { + var i int32 + for i = 0; i < math.MaxInt32; i++ { + if _, exists := wself.tunnels[i]; !exists { + break + } + } + + if i == math.MaxInt32 { + return 0, errors.New("container is full") + } + + wself.tunnels[i] = context + return i, nil +} + +func (wself *Container[Context]) Get(handle int32) (*Context, error) { + context, ok := wself.tunnels[handle] + if !ok { + return nil, errors.New("invalid context handle") + } + return &context, nil +} + +func (wself *Container[Context]) Remove(handle int32) (*Context, error) { + context, ok := wself.tunnels[handle] + if !ok { + return nil, errors.New("invalid context handle") + } + delete(wself.tunnels, handle) + return &context, nil +} + +func (wself *Container[Context]) ForEach(callback func(Context)) { + for _, tunnel := range wself.tunnels { + callback(tunnel) + } +} diff --git a/wireguard/libamnezia/go.mod b/wireguard/libamnezia/go.mod new file mode 100644 index 0000000000..56da2bbe59 --- /dev/null +++ b/wireguard/libamnezia/go.mod @@ -0,0 +1,21 @@ +module github.com/nymtech/nym-vpn-client/wireguard/libwg + +go 1.22.3 + +toolchain go1.23.1 + +require ( + // golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 + github.com/amnezia-vpn/amneziawg-go v0.2.12 + golang.org/x/sys v0.18.0 + gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 +) + +require ( + github.com/google/btree v1.0.1 // indirect + github.com/tevino/abool/v2 v2.1.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect +) diff --git a/wireguard/libamnezia/go.sum b/wireguard/libamnezia/go.sum new file mode 100644 index 0000000000..42f90fd40d --- /dev/null +++ b/wireguard/libamnezia/go.sum @@ -0,0 +1,18 @@ +github.com/amnezia-vpn/amneziawg-go v0.2.12 h1:CxIQETy5kZ0ip/dFBpmnDxAcS/KuIQaJkOxDv5OQhVI= +github.com/amnezia-vpn/amneziawg-go v0.2.12/go.mod h1:d7WpNfzCRLy7ufGElJBYpD58WRmNjyLyt3IDHPY8AmM= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= +github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= +gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= diff --git a/wireguard/libamnezia/goruntime-boottime-over-monotonic-darwin.diff b/wireguard/libamnezia/goruntime-boottime-over-monotonic-darwin.diff new file mode 100644 index 0000000000..2f7f54edd0 --- /dev/null +++ b/wireguard/libamnezia/goruntime-boottime-over-monotonic-darwin.diff @@ -0,0 +1,61 @@ +From 516dc0c15ff1ab781e0677606b5be72919251b3e Mon Sep 17 00:00:00 2001 +From: "Jason A. Donenfeld" +Date: Wed, 9 Dec 2020 14:07:06 +0100 +Subject: [PATCH] runtime: use libc_mach_continuous_time in nanotime on Darwin + +This makes timers account for having expired while a computer was +asleep, which is quite common on mobile devices. Note that +continuous_time absolute_time, except that it takes into account +time spent in suspend. + +Fixes #24595 + +Change-Id: Ia3282e8bd86f95ad2b76427063e60a005563f4eb +--- + src/runtime/sys_darwin.go | 2 +- + src/runtime/sys_darwin_amd64.s | 2 +- + src/runtime/sys_darwin_arm64.s | 2 +- + 3 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/src/runtime/sys_darwin.go b/src/runtime/sys_darwin.go +index 4a3f2fc453..4a69403b32 100644 +--- a/src/runtime/sys_darwin.go ++++ b/src/runtime/sys_darwin.go +@@ -440,7 +440,7 @@ func setNonblock(fd int32) { + //go:cgo_import_dynamic libc_usleep usleep "/usr/lib/libSystem.B.dylib" + + //go:cgo_import_dynamic libc_mach_timebase_info mach_timebase_info "/usr/lib/libSystem.B.dylib" +-//go:cgo_import_dynamic libc_mach_absolute_time mach_absolute_time "/usr/lib/libSystem.B.dylib" ++//go:cgo_import_dynamic libc_mach_continuous_time mach_continuous_time "/usr/lib/libSystem.B.dylib" + //go:cgo_import_dynamic libc_clock_gettime clock_gettime "/usr/lib/libSystem.B.dylib" + //go:cgo_import_dynamic libc_sigaction sigaction "/usr/lib/libSystem.B.dylib" + //go:cgo_import_dynamic libc_pthread_sigmask pthread_sigmask "/usr/lib/libSystem.B.dylib" +diff --git a/src/runtime/sys_darwin_amd64.s b/src/runtime/sys_darwin_amd64.s +index 630fb5df64..4499c88802 100644 +--- a/src/runtime/sys_darwin_amd64.s ++++ b/src/runtime/sys_darwin_amd64.s +@@ -114,7 +114,7 @@ TEXT runtime·nanotime_trampoline(SB),NOSPLIT,$0 + PUSHQ BP + MOVQ SP, BP + MOVQ DI, BX +- CALL libc_mach_absolute_time(SB) ++ CALL libc_mach_continuous_time(SB) + MOVQ AX, 0(BX) + MOVL timebase<>+machTimebaseInfo_numer(SB), SI + MOVL timebase<>+machTimebaseInfo_denom(SB), DI // atomic read +diff --git a/src/runtime/sys_darwin_arm64.s b/src/runtime/sys_darwin_arm64.s +index 96d2ed1076..f046545395 100644 +--- a/src/runtime/sys_darwin_arm64.s ++++ b/src/runtime/sys_darwin_arm64.s +@@ -143,7 +143,7 @@ GLOBL timebase<>(SB),NOPTR,$(machTimebaseInfo__size) + + TEXT runtime·nanotime_trampoline(SB),NOSPLIT,$40 + MOVD R0, R19 +- BL libc_mach_absolute_time(SB) ++ BL libc_mach_continuous_time(SB) + MOVD R0, 0(R19) + MOVW timebase<>+machTimebaseInfo_numer(SB), R20 + MOVD $timebase<>+machTimebaseInfo_denom(SB), R21 +-- +2.30.1 + diff --git a/wireguard/libamnezia/goruntime-boottime-over-monotonic.diff b/wireguard/libamnezia/goruntime-boottime-over-monotonic.diff new file mode 100644 index 0000000000..73eb999a95 --- /dev/null +++ b/wireguard/libamnezia/goruntime-boottime-over-monotonic.diff @@ -0,0 +1,170 @@ +From 61f3ae8298d1c503cbc31539e0f3a73446c7db9d Mon Sep 17 00:00:00 2001 +From: "Jason A. Donenfeld" +Date: Tue, 21 Mar 2023 15:33:56 +0100 +Subject: [PATCH] [release-branch.go1.20] runtime: use CLOCK_BOOTTIME in + nanotime on Linux + +This makes timers account for having expired while a computer was +asleep, which is quite common on mobile devices. Note that BOOTTIME is +identical to MONOTONIC, except that it takes into account time spent +in suspend. In Linux 4.17, the kernel will actually make MONOTONIC act +like BOOTTIME anyway, so this switch will additionally unify the +timer behavior across kernels. + +BOOTTIME was introduced into Linux 2.6.39-rc1 with 70a08cca1227d in +2011. + +Fixes #24595 + +Change-Id: I7b2a6ca0c5bc5fce57ec0eeafe7b68270b429321 +--- + src/runtime/sys_linux_386.s | 4 ++-- + src/runtime/sys_linux_amd64.s | 2 +- + src/runtime/sys_linux_arm.s | 4 ++-- + src/runtime/sys_linux_arm64.s | 4 ++-- + src/runtime/sys_linux_mips64x.s | 4 ++-- + src/runtime/sys_linux_mipsx.s | 2 +- + src/runtime/sys_linux_ppc64x.s | 2 +- + src/runtime/sys_linux_s390x.s | 2 +- + 8 files changed, 12 insertions(+), 12 deletions(-) + +diff --git a/src/runtime/sys_linux_386.s b/src/runtime/sys_linux_386.s +index 12a294153d..17e3524b40 100644 +--- a/src/runtime/sys_linux_386.s ++++ b/src/runtime/sys_linux_386.s +@@ -352,13 +352,13 @@ noswitch: + + LEAL 8(SP), BX // &ts (struct timespec) + MOVL BX, 4(SP) +- MOVL $1, 0(SP) // CLOCK_MONOTONIC ++ MOVL $7, 0(SP) // CLOCK_BOOTTIME + CALL AX + JMP finish + + fallback: + MOVL $SYS_clock_gettime, AX +- MOVL $1, BX // CLOCK_MONOTONIC ++ MOVL $7, BX // CLOCK_BOOTTIME + LEAL 8(SP), CX + INVOKE_SYSCALL + +diff --git a/src/runtime/sys_linux_amd64.s b/src/runtime/sys_linux_amd64.s +index c7a89ba536..01f0a6a26e 100644 +--- a/src/runtime/sys_linux_amd64.s ++++ b/src/runtime/sys_linux_amd64.s +@@ -255,7 +255,7 @@ noswitch: + SUBQ $16, SP // Space for results + ANDQ $~15, SP // Align for C code + +- MOVL $1, DI // CLOCK_MONOTONIC ++ MOVL $7, DI // CLOCK_BOOTTIME + LEAQ 0(SP), SI + MOVQ runtime·vdsoClockgettimeSym(SB), AX + CMPQ AX, $0 +diff --git a/src/runtime/sys_linux_arm.s b/src/runtime/sys_linux_arm.s +index 7b8c4f0e04..9798a1334e 100644 +--- a/src/runtime/sys_linux_arm.s ++++ b/src/runtime/sys_linux_arm.s +@@ -11,7 +11,7 @@ + #include "textflag.h" + + #define CLOCK_REALTIME 0 +-#define CLOCK_MONOTONIC 1 ++#define CLOCK_BOOTTIME 7 + + // for EABI, as we don't support OABI + #define SYS_BASE 0x0 +@@ -374,7 +374,7 @@ finish: + + // func nanotime1() int64 + TEXT runtime·nanotime1(SB),NOSPLIT,$12-8 +- MOVW $CLOCK_MONOTONIC, R0 ++ MOVW $CLOCK_BOOTTIME, R0 + MOVW $spec-12(SP), R1 // timespec + + MOVW runtime·vdsoClockgettimeSym(SB), R4 +diff --git a/src/runtime/sys_linux_arm64.s b/src/runtime/sys_linux_arm64.s +index 38ff6ac330..6b819c5441 100644 +--- a/src/runtime/sys_linux_arm64.s ++++ b/src/runtime/sys_linux_arm64.s +@@ -14,7 +14,7 @@ + #define AT_FDCWD -100 + + #define CLOCK_REALTIME 0 +-#define CLOCK_MONOTONIC 1 ++#define CLOCK_BOOTTIME 7 + + #define SYS_exit 93 + #define SYS_read 63 +@@ -338,7 +338,7 @@ noswitch: + BIC $15, R1 + MOVD R1, RSP + +- MOVW $CLOCK_MONOTONIC, R0 ++ MOVW $CLOCK_BOOTTIME, R0 + MOVD runtime·vdsoClockgettimeSym(SB), R2 + CBZ R2, fallback + +diff --git a/src/runtime/sys_linux_mips64x.s b/src/runtime/sys_linux_mips64x.s +index 47f2da524d..a8b387f193 100644 +--- a/src/runtime/sys_linux_mips64x.s ++++ b/src/runtime/sys_linux_mips64x.s +@@ -326,7 +326,7 @@ noswitch: + AND $~15, R1 // Align for C code + MOVV R1, R29 + +- MOVW $1, R4 // CLOCK_MONOTONIC ++ MOVW $7, R4 // CLOCK_BOOTTIME + MOVV $0(R29), R5 + + MOVV runtime·vdsoClockgettimeSym(SB), R25 +@@ -336,7 +336,7 @@ noswitch: + // see walltime for detail + BEQ R2, R0, finish + MOVV R0, runtime·vdsoClockgettimeSym(SB) +- MOVW $1, R4 // CLOCK_MONOTONIC ++ MOVW $7, R4 // CLOCK_BOOTTIME + MOVV $0(R29), R5 + JMP fallback + +diff --git a/src/runtime/sys_linux_mipsx.s b/src/runtime/sys_linux_mipsx.s +index 5e6b6c1504..7f5fd2a80e 100644 +--- a/src/runtime/sys_linux_mipsx.s ++++ b/src/runtime/sys_linux_mipsx.s +@@ -243,7 +243,7 @@ TEXT runtime·walltime(SB),NOSPLIT,$8-12 + RET + + TEXT runtime·nanotime1(SB),NOSPLIT,$8-8 +- MOVW $1, R4 // CLOCK_MONOTONIC ++ MOVW $7, R4 // CLOCK_BOOTTIME + MOVW $4(R29), R5 + MOVW $SYS_clock_gettime, R2 + SYSCALL +diff --git a/src/runtime/sys_linux_ppc64x.s b/src/runtime/sys_linux_ppc64x.s +index d0427a4807..05ee9fede9 100644 +--- a/src/runtime/sys_linux_ppc64x.s ++++ b/src/runtime/sys_linux_ppc64x.s +@@ -298,7 +298,7 @@ fallback: + JMP return + + TEXT runtime·nanotime1(SB),NOSPLIT,$16-8 +- MOVD $1, R3 // CLOCK_MONOTONIC ++ MOVD $7, R3 // CLOCK_BOOTTIME + + MOVD R1, R15 // R15 is unchanged by C code + MOVD g_m(g), R21 // R21 = m +diff --git a/src/runtime/sys_linux_s390x.s b/src/runtime/sys_linux_s390x.s +index 1448670b91..7d2ee3231c 100644 +--- a/src/runtime/sys_linux_s390x.s ++++ b/src/runtime/sys_linux_s390x.s +@@ -296,7 +296,7 @@ fallback: + RET + + TEXT runtime·nanotime1(SB),NOSPLIT,$32-8 +- MOVW $1, R2 // CLOCK_MONOTONIC ++ MOVW $7, R2 // CLOCK_BOOTTIME + + MOVD R15, R7 // Backup stack pointer + +-- +2.17.1 diff --git a/wireguard/libamnezia/libwg.go b/wireguard/libamnezia/libwg.go new file mode 100644 index 0000000000..a519822002 --- /dev/null +++ b/wireguard/libamnezia/libwg.go @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Copyright (C) 2017-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2024 Nym Technologies SA . All Rights Reserved. + */ + +package main + +// #include +import "C" + +import ( + "bufio" + "bytes" + "runtime" + "unsafe" + + "github.com/nymtech/nym-vpn-client/wireguard/libwg/container" + "github.com/amnezia-vpn/amneziawg-go/device" +) + +const ( + ERROR_GENERAL_FAILURE = -1 + ERROR_INTERMITTENT_FAILURE = -2 +) + +type TunnelContext struct { + Device *device.Device + Logger *device.Logger +} + +var tunnels container.Container[TunnelContext] + +func init() { + tunnels = container.New[TunnelContext]() +} + +//export wgTurnOff +func wgTurnOff(tunnelHandle int32) { + { + tunnel, err := tunnels.Remove(tunnelHandle) + if err != nil { + return + } + tunnel.Device.Close() + } + // Calling twice convinces the GC to release NOW. + runtime.GC() + runtime.GC() +} + +//export wgGetConfig +func wgGetConfig(tunnelHandle int32) *C.char { + tunnel, err := tunnels.Get(tunnelHandle) + if err != nil { + return nil + } + settings := new(bytes.Buffer) + writer := bufio.NewWriter(settings) + if err := tunnel.Device.IpcGetOperation(writer); err != nil { + tunnel.Logger.Errorf("Failed to get config for tunnel: %s\n", err) + return nil + } + writer.Flush() + return C.CString(settings.String()) +} + +//export wgFreePtr +func wgFreePtr(ptr unsafe.Pointer) { + C.free(ptr) +} + +func main() {} diff --git a/wireguard/libamnezia/libwg_android.go b/wireguard/libamnezia/libwg_android.go new file mode 100644 index 0000000000..9470ed378b --- /dev/null +++ b/wireguard/libamnezia/libwg_android.go @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Copyright (C) 2017-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2024 Nym Technologies SA . All Rights Reserved. + */ + +package main + +import ( + "C" + "bufio" + "strings" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun" + + "github.com/nymtech/nym-vpn-client/wireguard/libwg/logging" +) + +// Redefined here because otherwise the compiler doesn't realize it's a type alias for a type that's safe to export. +// Taken from the contained logging package. +type LogSink = unsafe.Pointer +type LogContext = unsafe.Pointer + +//export wgTurnOn +func wgTurnOn(cSettings *C.char, fd int, logSink LogSink, logContext LogContext) int32 { + logger := logging.NewLogger(logSink, logContext) + + if cSettings == nil { + logger.Errorf("cSettings is null\n") + return ERROR_GENERAL_FAILURE + } + settings := C.GoString(cSettings) + + tunDevice, _, err := tun.CreateUnmonitoredTUNFromFD(fd) + if err != nil { + logger.Errorf("%s\n", err) + unix.Close(fd) + if err.Error() == "bad file descriptor" { + return ERROR_INTERMITTENT_FAILURE + } + return ERROR_GENERAL_FAILURE + } + + device := device.NewDevice(tunDevice, conn.NewStdNetBind(), logger) + + setErr := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings))) + if setErr != nil { + logger.Errorf("%s\n", setErr) + device.Close() + return ERROR_INTERMITTENT_FAILURE + } + + device.DisableSomeRoamingForBrokenMobileSemantics() + device.Up() + + context := TunnelContext{ + Device: device, + Logger: logger, + } + + handle, err := tunnels.Insert(context) + if err != nil { + logger.Errorf("%s\n", err) + device.Close() + return ERROR_GENERAL_FAILURE + } + + return handle +} + +//export wgGetSocketV4 +func wgGetSocketV4(tunnelHandle int32) int32 { + tunnel, err := tunnels.Get(tunnelHandle) + if err != nil { + return ERROR_GENERAL_FAILURE + } + peek := tunnel.Device.Bind().(conn.PeekLookAtSocketFd) + fd, err := peek.PeekLookAtSocketFd4() + if err != nil { + return ERROR_GENERAL_FAILURE + } + return int32(fd) +} + +//export wgGetSocketV6 +func wgGetSocketV6(tunnelHandle int32) int32 { + tunnel, err := tunnels.Get(tunnelHandle) + if err != nil { + return ERROR_GENERAL_FAILURE + } + peek := tunnel.Device.Bind().(conn.PeekLookAtSocketFd) + fd, err := peek.PeekLookAtSocketFd6() + if err != nil { + return ERROR_GENERAL_FAILURE + } + return int32(fd) +} diff --git a/wireguard/libamnezia/libwg_default.go b/wireguard/libamnezia/libwg_default.go new file mode 100644 index 0000000000..050cdb8799 --- /dev/null +++ b/wireguard/libamnezia/libwg_default.go @@ -0,0 +1,97 @@ +//go:build (darwin || linux) && !android && !ios + +/* SPDX-License-Identifier: Apache-2.0 + * + * Copyright (C) 2017-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2024 Nym Technologies SA . All Rights Reserved. + */ + +package main + +// #include +import "C" +import ( + "bufio" + "os" + "strings" + "unsafe" + + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun" + + "github.com/nymtech/nym-vpn-client/wireguard/libwg/logging" +) + +// Redefined here because otherwise the compiler doesn't realize it's a type alias for a type that's safe to export. +// Taken from the contained logging package. +type LogSink = unsafe.Pointer +type LogContext = unsafe.Pointer + +//export wgTurnOn +func wgTurnOn(mtu int, cSettings *C.char, fd int, logSink LogSink, logContext LogContext) int32 { + logger := logging.NewLogger(logSink, logContext) + + if cSettings == nil { + logger.Errorf("cSettings is null\n") + return ERROR_GENERAL_FAILURE + } + settings := C.GoString(cSettings) + + file := os.NewFile(uintptr(fd), "") + tunDevice, err := tun.CreateTUNFromFile(file, mtu) + if err != nil { + logger.Errorf("%s\n", err) + if err.Error() == "bad file descriptor" { + return ERROR_INTERMITTENT_FAILURE + } + return ERROR_GENERAL_FAILURE + } + + device := device.NewDevice(tunDevice, conn.NewDefaultBind(), logger) + + setErr := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings))) + if setErr != nil { + logger.Errorf("%s\n", setErr) + device.Close() + return ERROR_INTERMITTENT_FAILURE + } + + device.Up() + + context := TunnelContext{ + Device: device, + Logger: logger, + } + + handle, err := tunnels.Insert(context) + if err != nil { + logger.Errorf("%s\n", err) + device.Close() + return ERROR_GENERAL_FAILURE + } + + return handle +} + +//export wgSetConfig +func wgSetConfig(tunnelHandle int32, cSettings *C.char) int32 { + tunnel, err := tunnels.Get(tunnelHandle) + if err != nil { + return ERROR_GENERAL_FAILURE + } + if cSettings == nil { + tunnel.Logger.Errorf("cSettings is null\n") + return ERROR_GENERAL_FAILURE + } + settings := C.GoString(cSettings) + + setError := tunnel.Device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings))) + if setError != nil { + tunnel.Logger.Errorf("Failed to set device configuration\n") + tunnel.Logger.Errorf("%s\n", setError) + return ERROR_GENERAL_FAILURE + } + return 0 +} diff --git a/wireguard/libamnezia/libwg_ios.go b/wireguard/libamnezia/libwg_ios.go new file mode 100644 index 0000000000..d45ef6a4cd --- /dev/null +++ b/wireguard/libamnezia/libwg_ios.go @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2018-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2024 Nym Technologies SA . All Rights Reserved. + */ + +package main + +import "C" +import ( + "os" + "time" + "unsafe" + + "github.com/nymtech/nym-vpn-client/wireguard/libwg/logging" + "golang.org/x/sys/unix" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun" +) + +// Redefined here because otherwise the compiler doesn't realize it's a type alias for a type that's safe to export. +// Taken from the contained logging package. +type LogSink = unsafe.Pointer +type LogContext = unsafe.Pointer + +//export wgTurnOn +func wgTurnOn(settings *C.char, tunFd int32, logSink LogSink, logContext LogContext) int32 { + logger := logging.NewLogger(logSink, logContext) + + dupTunFd, err := unix.Dup(int(tunFd)) + if err != nil { + logger.Errorf("Unable to dup tun fd: %v", err) + return ERROR_GENERAL_FAILURE + } + + err = unix.SetNonblock(dupTunFd, true) + if err != nil { + logger.Errorf("Unable to set tun fd as non blocking: %v", err) + unix.Close(dupTunFd) + return ERROR_GENERAL_FAILURE + } + tun, err := tun.CreateTUNFromFile(os.NewFile(uintptr(dupTunFd), "/dev/tun"), 0) + if err != nil { + logger.Errorf("Unable to create new tun device from fd: %v", err) + unix.Close(dupTunFd) + return ERROR_INTERMITTENT_FAILURE + } + logger.Verbosef("Attaching to interface") + dev := device.NewDevice(tun, conn.NewStdNetBind(), logger) + + err = dev.IpcSet(C.GoString(settings)) + if err != nil { + logger.Errorf("Unable to set IPC settings: %v", err) + unix.Close(dupTunFd) + return ERROR_GENERAL_FAILURE + } + + dev.DisableSomeRoamingForBrokenMobileSemantics() + dev.Up() + + logger.Verbosef("Device started") + + context := TunnelContext{ + Device: dev, + Logger: logger, + } + + handle, err := tunnels.Insert(context) + if err != nil { + logger.Errorf("%s", err) + dev.Close() + return ERROR_GENERAL_FAILURE + } + + return handle +} + +//export wgBumpSockets +func wgBumpSockets(tunnelHandle int32) { + tunnel, err := tunnels.Get(tunnelHandle) + if err != nil { + return + } + go func() { + for i := 0; i < 10; i++ { + err := tunnel.Device.BindUpdate() + if err == nil { + tunnel.Device.SendKeepalivesToPeersWithCurrentKeypair() + return + } + tunnel.Logger.Errorf("Unable to update bind, try %d: %v", i+1, err) + time.Sleep(time.Second / 2) + } + tunnel.Logger.Errorf("Gave up trying to update bind; tunnel is likely dysfunctional") + }() +} diff --git a/wireguard/libamnezia/libwg_mobile.go b/wireguard/libamnezia/libwg_mobile.go new file mode 100644 index 0000000000..d80d4b0126 --- /dev/null +++ b/wireguard/libamnezia/libwg_mobile.go @@ -0,0 +1,40 @@ +//go:build ios || android + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2018-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2024 Nym Technologies SA . All Rights Reserved. + */ + +package main + +import "C" + +import ( + "bufio" + "strings" +) + +//export wgSetConfig +func wgSetConfig(tunnelHandle int32, cSettings *C.char) int32 { + tunnel, err := tunnels.Get(tunnelHandle) + if err != nil { + return ERROR_GENERAL_FAILURE + } + if cSettings == nil { + tunnel.Logger.Errorf("cSettings is null\n") + return ERROR_GENERAL_FAILURE + } + settings := C.GoString(cSettings) + + err = tunnel.Device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings))) + if err != nil { + tunnel.Logger.Errorf("Failed to set device configuration\n") + tunnel.Logger.Errorf("%s\n", err) + return ERROR_GENERAL_FAILURE + } + + tunnel.Device.DisableSomeRoamingForBrokenMobileSemantics() + + return 0 +} diff --git a/wireguard/libamnezia/libwg_windows.go b/wireguard/libamnezia/libwg_windows.go new file mode 100644 index 0000000000..0a13431dd6 --- /dev/null +++ b/wireguard/libamnezia/libwg_windows.go @@ -0,0 +1,153 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Copyright (C) 2017-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + * Copyright (C) 2024 Nym Technologies SA . All Rights Reserved. + */ + +package main + +// #include +import "C" + +import ( + "bufio" + "strings" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun" + + "github.com/nymtech/nym-vpn-client/wireguard/libwg/logging" +) + +// Redefined here because otherwise the compiler doesn't realize it's a type alias for a type that's safe to export. +// Taken from the contained logging package. +type LogSink = unsafe.Pointer +type LogContext = unsafe.Pointer + +//export wgTurnOn +func wgTurnOn(cIfaceName *C.char, mtu int, cSettings *C.char, cIfaceNameOut **C.char, cLuidOut *uint64, logSink LogSink, logContext LogContext) int32 { + logger := logging.NewLogger(logSink, logContext) + if cIfaceNameOut != nil { + *cIfaceNameOut = nil + } + + if cIfaceName == nil { + logger.Errorf("cIfaceName is null\n") + return ERROR_GENERAL_FAILURE + } + + if cSettings == nil { + logger.Errorf("cSettings is null\n") + return ERROR_GENERAL_FAILURE + } + + settings := C.GoString(cSettings) + ifaceName := C.GoString(cIfaceName) + + // {AFE43773-E1F8-4EBB-8536-576AB86AFE9A} + networkId := windows.GUID{0xafe43773, 0xe1f8, 0x4ebb, [8]byte{0x85, 0x36, 0x57, 0x6a, 0xb8, 0x6a, 0xfe, 0x9a}} + + tun.WintunTunnelType = "Mullvad" + + wintun, err := tun.CreateTUNWithRequestedGUID(ifaceName, &networkId, mtu) + if err != nil { + logger.Errorf("Failed to create tunnel\n") + logger.Errorf("%s\n", err) + return ERROR_INTERMITTENT_FAILURE + } + + nativeTun := wintun.(*tun.NativeTun) + + actualInterfaceName, err := nativeTun.Name() + if err != nil { + nativeTun.Close() + logger.Errorf("Failed to determine name of wintun adapter\n") + return ERROR_GENERAL_FAILURE + } + if actualInterfaceName != ifaceName { + // WireGuard picked a different name for the adapter than the one we expected. + // This indicates there is already an adapter with the name we intended to use. + logger.Verbosef("Failed to create adapter with specific name\n") + } + + device := device.NewDevice(wintun, conn.NewDefaultBind(), logger) + + setError := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings))) + if setError != nil { + logger.Errorf("Failed to set device configuration\n") + logger.Errorf("%s\n", setError) + device.Close() + return ERROR_GENERAL_FAILURE + } + + device.Up() + + context := TunnelContext{ + Device: device, + Logger: logger, + } + + handle, err := tunnels.Insert(context) + if err != nil { + logger.Errorf("%s\n", err) + device.Close() + return ERROR_GENERAL_FAILURE + } + + if cIfaceNameOut != nil { + *cIfaceNameOut = C.CString(actualInterfaceName) + } + if cLuidOut != nil { + *cLuidOut = nativeTun.LUID() + } + + return handle +} + +//export wgSetConfig +func wgSetConfig(tunnelHandle int32, cSettings *C.char) int32 { + tunnel, err := tunnels.Get(tunnelHandle) + if err != nil { + return ERROR_GENERAL_FAILURE + } + if cSettings == nil { + tunnel.Logger.Errorf("cSettings is null\n") + return ERROR_GENERAL_FAILURE + } + settings := C.GoString(cSettings) + + setError := tunnel.Device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings))) + if setError != nil { + tunnel.Logger.Errorf("Failed to set device configuration\n") + tunnel.Logger.Errorf("%s\n", setError) + return ERROR_GENERAL_FAILURE + } + return 0 +} + +//export wgRebindTunnelSocket +func wgRebindTunnelSocket(family uint16, interfaceIndex uint32) { + tunnels.ForEach(func(tunnel TunnelContext) { + blackhole := (interfaceIndex == 0) + bind := tunnel.Device.Bind().(conn.BindSocketToInterface) + + if family == windows.AF_INET { + tunnel.Logger.Verbosef("Binding v4 socket to interface %d (blackhole=%v)\n", interfaceIndex, blackhole) + err := bind.BindSocketToInterface4(interfaceIndex, blackhole) + if err != nil { + tunnel.Logger.Verbosef("%s\n", err) + } + } else if family == windows.AF_INET6 { + tunnel.Logger.Verbosef("Binding v6 socket to interface %d (blackhole=%v)\n", interfaceIndex, blackhole) + err := bind.BindSocketToInterface6(interfaceIndex, blackhole) + if err != nil { + tunnel.Logger.Verbosef("%s\n", err) + } + } + }) +} diff --git a/wireguard/libamnezia/logging/logging.go b/wireguard/libamnezia/logging/logging.go new file mode 100644 index 0000000000..6d81ccf7a0 --- /dev/null +++ b/wireguard/libamnezia/logging/logging.go @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Copyright (C) 2017-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2021 Mullvad VPN AB. All Rights Reserved. + */ + +package logging + +// #include +// #include +// #ifndef WIN32 +// #define __stdcall +// #endif +// typedef void (__stdcall *LogSink)(unsigned int, const char *, void *); +// static void callLogSink(void *logSink, int level, const char *message, void *context) +// { +// ((LogSink)logSink)((unsigned int)level, message, context); +// } +import "C" + +import ( + "log" + "unsafe" + + "github.com/amnezia-vpn/amneziawg-go/device" +) + +// Define type aliases. +type LogSink = unsafe.Pointer +type LogContext = unsafe.Pointer + +type Logger struct { + sink LogSink + context LogContext + level C.int +} + +func (l *Logger) Write(message []byte) (int, error) { + msg := C.CString(string(message)) + C.callLogSink(l.sink, l.level, msg, l.context) + C.free(unsafe.Pointer(msg)) + return len(message), nil +} + +func NewLogger(logSink LogSink, logContext LogContext) *device.Logger { + logger := new(device.Logger) + + logger.Verbosef = log.New( + &Logger{sink: logSink, context: logContext, level: device.LogLevelVerbose}, + "", + 0, + ).Printf + logger.Errorf = log.New( + &Logger{sink: logSink, context: logContext, level: device.LogLevelError}, + "", + 0, + ).Printf + + return logger +} diff --git a/wireguard/libamnezia/netstack_android.go b/wireguard/libamnezia/netstack_android.go new file mode 100644 index 0000000000..1c2b8d2690 --- /dev/null +++ b/wireguard/libamnezia/netstack_android.go @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2018-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2024 Nym Technologies SA . All Rights Reserved. + */ + +package main + +import "C" + +import "github.com/amnezia-vpn/amneziawg-go/conn" + +//export wgNetGetSocketV4 +func wgNetGetSocketV4(tunnelHandle int32) int32 { + tunnel, err := netTunnelHandles.Get(tunnelHandle) + if err != nil { + return ERROR_GENERAL_FAILURE + } + peek := tunnel.Device.Bind().(conn.PeekLookAtSocketFd) + fd, err := peek.PeekLookAtSocketFd4() + if err != nil { + return ERROR_GENERAL_FAILURE + } + return int32(fd) +} + +//export wgNetGetSocketV6 +func wgNetGetSocketV6(tunnelHandle int32) int32 { + tunnel, err := netTunnelHandles.Get(tunnelHandle) + if err != nil { + return ERROR_GENERAL_FAILURE + } + peek := tunnel.Device.Bind().(conn.PeekLookAtSocketFd) + fd, err := peek.PeekLookAtSocketFd6() + if err != nil { + return ERROR_GENERAL_FAILURE + } + return int32(fd) +} diff --git a/wireguard/libamnezia/netstack_mobile.go b/wireguard/libamnezia/netstack_mobile.go new file mode 100644 index 0000000000..b1f197c8f6 --- /dev/null +++ b/wireguard/libamnezia/netstack_mobile.go @@ -0,0 +1,200 @@ +//go:build ios || android + +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2018-2019 Jason A. Donenfeld . All Rights Reserved. + * Copyright (C) 2024 Nym Technologies SA . All Rights Reserved. + */ + +package main + +import "C" + +import ( + "net/netip" + "strings" + + "github.com/nymtech/nym-vpn-client/wireguard/libwg/container" + "github.com/nymtech/nym-vpn-client/wireguard/libwg/logging" + "github.com/nymtech/nym-vpn-client/wireguard/libwg/udp_forwarder" + "github.com/amnezia-vpn/amneziawg-go/conn" + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun/netstack" +) + +type netTunnelHandle struct { + *device.Device + *netstack.Net + *device.Logger +} + +var netTunnelHandles container.Container[netTunnelHandle] +var udpForwarders container.Container[*udp_forwarder.UDPForwarder] + +func init() { + netTunnelHandles = container.New[netTunnelHandle]() + udpForwarders = container.New[*udp_forwarder.UDPForwarder]() +} + +//export wgNetTurnOn +func wgNetTurnOn(localAddresses *C.char, dnsAddresses *C.char, mtu int, settings *C.char, logSink LogSink, logContext LogContext) int32 { + logger := logging.NewLogger(logSink, logContext) + + // Parse comma separated list of IP addresses + tunAddrs, err := parseIPAddrs(C.GoString(localAddresses)) + if err != nil { + logger.Errorf("Failed to parse local addresses: %v", err) + return ERROR_GENERAL_FAILURE + } + + // Parse comma separated list of DNS addresses + dnsAddrs, err := parseIPAddrs(C.GoString(dnsAddresses)) + if err != nil { + logger.Errorf("Failed to parse dns addresses: %v", err) + return ERROR_GENERAL_FAILURE + } + + tun, tnet, err := netstack.CreateNetTUN(tunAddrs, dnsAddrs, mtu) + if err != nil { + logger.Errorf("Failed to create net tun: %v", err) + return ERROR_GENERAL_FAILURE + } + + dev := device.NewDevice( + tun, + conn.NewDefaultBind(), + logger, + ) + if dev == nil { + logger.Errorf("Failed to create device") + return ERROR_GENERAL_FAILURE + } + + err = dev.IpcSet(C.GoString(settings)) + if err != nil { + logger.Errorf("Unable to set IPC settings: %v", err) + dev.Close() + return ERROR_GENERAL_FAILURE + } + + dev.DisableSomeRoamingForBrokenMobileSemantics() + err = dev.Up() + if err != nil { + logger.Errorf("Failed to set device state to Up: %v", err) + dev.Close() + return ERROR_GENERAL_FAILURE + } + + logger.Verbosef("Net device started") + + i, err := netTunnelHandles.Insert(netTunnelHandle{dev, tnet, logger}) + if err != nil { + logger.Errorf("Failed to store tunnel: %v", err) + dev.Close() + return ERROR_GENERAL_FAILURE + } + + return i +} + +//export wgNetTurnOff +func wgNetTurnOff(tunnelHandle int32) { + dev, err := netTunnelHandles.Remove(tunnelHandle) + if err != nil { + return + } + dev.Close() +} + +//export wgNetSetConfig +func wgNetSetConfig(tunnelHandle int32, settings *C.char) int64 { + dev, err := netTunnelHandles.Get(tunnelHandle) + if err != nil { + return 0 + } + err = dev.IpcSet(C.GoString(settings)) + if err != nil { + dev.Errorf("Unable to set IPC settings: %v", err) + if ipcErr, ok := err.(*device.IPCError); ok { + return ipcErr.ErrorCode() + } + return ERROR_GENERAL_FAILURE + } + + dev.DisableSomeRoamingForBrokenMobileSemantics() + + return 0 +} + +//export wgNetGetConfig +func wgNetGetConfig(tunnelHandle int32) *C.char { + device, err := netTunnelHandles.Get(tunnelHandle) + if err != nil { + return nil + } + settings, err := device.IpcGet() + if err != nil { + return nil + } + return C.CString(settings) +} + +//export wgNetOpenConnectionThroughTunnel +func wgNetOpenConnectionThroughTunnel(entryTunnelHandle int32, listenPort uint16, clientPort uint16, exitEndpointStr *C.char, logSink LogSink, logContext LogContext) int32 { + logger := logging.NewLogger(logSink, logContext) + + dev, err := netTunnelHandles.Get(entryTunnelHandle) + if err != nil { + dev.Errorf("Invalid tunnel handle: %d", entryTunnelHandle) + return ERROR_GENERAL_FAILURE + } + + exitEndpoint, err := netip.ParseAddrPort(C.GoString(exitEndpointStr)) + if err != nil { + dev.Errorf("Failed to parse endpoint: %v", err) + return ERROR_GENERAL_FAILURE + } + + forwarderConfig := udp_forwarder.UDPForwarderConfig{ + ListenPort: listenPort, + ClientPort: clientPort, + ExitEndpoint: exitEndpoint, + } + + udpForwarder, err := udp_forwarder.New(forwarderConfig, dev.Net, logger) + if err != nil { + dev.Errorf("Failed to create udp forwarder: %v", err) + return ERROR_GENERAL_FAILURE + } + + forwarderHandle, err := udpForwarders.Insert(udpForwarder) + if err != nil { + dev.Errorf("Failed to store udp forwarder: %v", err) + udpForwarder.Close() + return ERROR_GENERAL_FAILURE + } + + return forwarderHandle +} + +//export wgNetCloseConnectionThroughTunnel +func wgNetCloseConnectionThroughTunnel(udpForwarderHandle int32) { + udpForwarder, err := udpForwarders.Remove(udpForwarderHandle) + if err != nil { + return + } + (*udpForwarder).Close() +} + +// Parse a list of comma-separated IP addresses into array of netip.Addr structs. +func parseIPAddrs(input string) ([]netip.Addr, error) { + addrs := []netip.Addr{} + for _, s := range strings.Split(input, ",") { + addr, err := netip.ParseAddr(strings.TrimSpace(s)) + if err != nil { + return addrs, err + } + addrs = append(addrs, addr) + } + return addrs, nil +} diff --git a/wireguard/libamnezia/udp_forwarder/udp_forwarder.go b/wireguard/libamnezia/udp_forwarder/udp_forwarder.go new file mode 100644 index 0000000000..54217ece85 --- /dev/null +++ b/wireguard/libamnezia/udp_forwarder/udp_forwarder.go @@ -0,0 +1,194 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * + * Copyright 2024 - Nym Technologies SA + */ + +package udp_forwarder + +import ( + "net" + "net/netip" + "sync" + "time" + + "github.com/amnezia-vpn/amneziawg-go/device" + "github.com/amnezia-vpn/amneziawg-go/tun/netstack" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" +) + +const UDP_WRITE_TIMEOUT = time.Duration(5) * time.Second +const MAX_UDP_DATAGRAM_LEN = 65535 + +type UDPForwarderConfig struct { + // Listen port for incoming WireGuard traffic. + // For IPv4 exit endpoint, the listening port is bound to 127.0.0.1, for IPv6 it's ::1. + ListenPort uint16 + + // Client port on loopback from which the incoming WireGuard connection will be received. + // Only packets from this port will be passed through to the exit endpoint. + ClientPort uint16 + + // Exit endpoint which will receive the raw WireGuard packets received on the listen port. + // The connection to exit endpoint is established over the entry tunnel, thus it creates + // a tunnel inside of tunnel. + ExitEndpoint netip.AddrPort +} + +// UDP forwarder that creates a bidirectional connection between a local and exit UDP endpoints +// over the netstack-based WireGuard tunnel. +type UDPForwarder struct { + // Logger. + logger *device.Logger + + // Netstack tunnel wrapping the inbound WireGuard traffic. + tnet *netstack.Net + + // UDP listener that receives inbound WireGuard traffic destined to exit endpoint. + listener *net.UDPConn + + // Outbound connection to the exit endpoint over the entry tunnel. + outbound *gonet.UDPConn + + // Wait group used to signal when all goroutines have finished execution. + waitGroup *sync.WaitGroup +} + +func New(config UDPForwarderConfig, tnet *netstack.Net, logger *device.Logger) (*UDPForwarder, error) { + var listenAddr *net.UDPAddr + var clientAddr *net.UDPAddr + + // Use the same ip protocol family as exit endpoint. + if config.ExitEndpoint.Addr().Is4() { + loopback := netip.AddrFrom4([4]byte{127, 0, 0, 1}) + listenAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(loopback, config.ListenPort)) + clientAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(loopback, config.ClientPort)) + } else { + listenAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv6Loopback(), config.ListenPort)) + clientAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv6Loopback(), config.ClientPort)) + } + + listener, err := net.ListenUDP("udp", listenAddr) + if err != nil { + return nil, err + } + + outbound, err := tnet.DialUDPAddrPort(netip.AddrPort{}, config.ExitEndpoint) + if err != nil { + return nil, err + } + + waitGroup := &sync.WaitGroup{} + wrapper := &UDPForwarder{ + logger, + tnet, + listener, + outbound, + waitGroup, + } + + waitGroup.Add(2) + go wrapper.RoutineHandleInbound(listener, outbound, clientAddr) + go wrapper.RoutineHandleOutbound(listener, outbound, clientAddr) + + return wrapper, nil +} + +func (w *UDPForwarder) Close() { + // Close all connections. This should release any blocking ReadFromUDP() calls. + w.listener.Close() + w.outbound.Close() + + // Wait for all routines to complete. + w.waitGroup.Wait() +} + +func (w *UDPForwarder) Wait() { + w.waitGroup.Wait() +} + +func (w *UDPForwarder) RoutineHandleInbound(inbound *net.UDPConn, outbound *gonet.UDPConn, clientAddr *net.UDPAddr) { + defer w.waitGroup.Done() + + inboundBuffer := make([]byte, MAX_UDP_DATAGRAM_LEN) + + w.logger.Verbosef("udpforwarder(inbound): listening on %s", inbound.LocalAddr().String()) + defer w.logger.Verbosef("udpforwarder(inbound): closed") + + for { + // Receive the WireGuard packet from local port + bytesRead, senderAddr, err := inbound.ReadFromUDP(inboundBuffer) + if err != nil { + w.logger.Errorf("udpforwarder(inbound): %s", err.Error()) + // todo: handle error + return + } + + // Drop packet from unknown sender. + if !senderAddr.IP.IsLoopback() || senderAddr.Port != clientAddr.Port { + w.logger.Verbosef("udpforwarder(inbound): drop packet from unknown sender: %s, expected: %s.", senderAddr.String(), clientAddr.String()) + continue + } + + // Set write timeout for outbound. + deadline := time.Now().Add(UDP_WRITE_TIMEOUT) + err = outbound.SetWriteDeadline(deadline) + if err != nil { + w.logger.Errorf("udpforwarder(inbound): %s", err.Error()) + // todo: handle error + continue + } + + // Forward the packet over the outbound connection via another WireGuard tunnel. + _, err = outbound.Write(inboundBuffer[:bytesRead]) + if err != nil { + w.logger.Errorf("udpforwarder(inbound): %s", err.Error()) + // todo: handle error + continue + } + } +} + +func (w *UDPForwarder) RoutineHandleOutbound(inbound *net.UDPConn, outbound *gonet.UDPConn, clientAddr *net.UDPAddr) { + defer w.waitGroup.Done() + + remoteAddr := outbound.RemoteAddr().(*net.UDPAddr) + w.logger.Verbosef("udpforwarder(outbound): dial %s", remoteAddr.String()) + defer w.logger.Verbosef("udpforwarder(outbound): closed") + + outboundBuffer := make([]byte, MAX_UDP_DATAGRAM_LEN) + + for { + // Receive WireGuard packet from remote server. + bytesRead, senderAddr, err := outbound.ReadFrom(outboundBuffer) + if err != nil { + w.logger.Errorf("udpforwarder(outbound): %s", err.Error()) + // todo: handle error + return + } + // Cast net.Addr to net.UDPAddr + senderUDPAddr := senderAddr.(*net.UDPAddr) + + // Drop packet from unknown sender. + if !senderUDPAddr.IP.Equal(remoteAddr.IP) || senderUDPAddr.Port != remoteAddr.Port { + w.logger.Verbosef("udpforwarder(outbound): drop packet from unknown sender: %s, expected: %s", senderUDPAddr.String(), remoteAddr.String()) + continue + } + + // Set write timeout for inbound. + deadline := time.Now().Add(UDP_WRITE_TIMEOUT) + err = inbound.SetWriteDeadline(deadline) + if err != nil { + w.logger.Errorf("udpforwarder(outbound): %s", err.Error()) + // todo: handle error + continue + } + + // Forward packet from remote to local client. + _, err = inbound.WriteToUDP(outboundBuffer[:bytesRead], clientAddr) + if err != nil { + w.logger.Errorf("udpforwarder(outbound): %s", err.Error()) + // todo: handle error + continue + } + } +}