diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 2de27bb..622cfd9 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -70,6 +70,11 @@ jobs: OS: windows-latest ARTIFACT_NAME: windows-x86_64 + - TARGET: x86_64-unknown-freebsd + OS: ubuntu-latest + ARTIFACT_NAME: freebsd-13.2-x86_64 + BSD_VERSION: 13.2 + runs-on: ${{ matrix.OS }} env: NAME: easytier @@ -81,10 +86,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-node@v4 - with: - node-version: 21 - - name: Cargo cache uses: actions/cache@v4 with: @@ -93,9 +94,6 @@ jobs: ./target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install rust target - run: bash ./.github/workflows/install_rust.sh - - name: Setup protoc uses: arduino/setup-protoc@v2 with: @@ -103,13 +101,52 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Build Core & Cli + if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }} run: | + bash ./.github/workflows/install_rust.sh if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips else cargo build --release --verbose --target $TARGET fi + # Copied and slightly modified from @lmq8267 (https://github.com/lmq8267) + - name: Build Core & Cli (X86_64 FreeBSD) + uses: cross-platform-actions/action@v0.23.0 + if: ${{ endsWith(matrix.TARGET, 'freebsd') }} + env: + TARGET: ${{ matrix.TARGET }} + with: + operating_system: freebsd + environment_variables: TARGET + architecture: x86-64 + version: ${{ matrix.BSD_VERSION }} + shell: bash + memory: 5G + cpu_count: 4 + run: | + uname -a + echo $SHELL + pwd + ls -lah + whoami + env | sort + + sudo pkg install -y git protobuf + curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source $HOME/.cargo/env + + rustup set auto-self-update disable + + rustup install 1.77 + rustup default 1.77 + + export CC=clang + export CXX=clang++ + export CARGO_TERM_COLOR=always + + cargo build --release --verbose --target $TARGET + - name: Install UPX if: ${{ matrix.OS != 'macos-latest' }} uses: crazy-max/ghaction-upx@v3 @@ -132,7 +169,7 @@ jobs: TAG=$GITHUB_SHA fi - if [[ $OS =~ ^ubuntu.*$ ]]; then + if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX" upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX" fi diff --git a/Cargo.lock b/Cargo.lock index 5c9c239..835501b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -588,9 +588,9 @@ dependencies = [ [[package]] name = "boringtun-easytier" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a62bfb866a2a03e8aea22e83a0c1e385304563ee77c89ebd2043c67d0a73065" +checksum = "2f09b4d1ada8affba260cb185bbdf6d5acff42f924dea1a17f938cf3e8fbe475" dependencies = [ "aead", "atomic-shim", diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 4f484d3..38ee820 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -142,7 +142,10 @@ network-interface = "2.0" # for ospf route petgraph = "0.6.5" -boringtun = { package = "boringtun-easytier", version = "0.6.0", optional = true } # for encryption +# for wireguard +boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true } + +# for encryption ring = { version = "0.17", optional = true } bitflags = "2.5" aes-gcm = { version = "0.10.3", optional = true } @@ -219,7 +222,6 @@ full = [ "socks5", ] mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"] -bsd = ["aes-gcm", "mimalloc", "smoltcp", "socks5"] wireguard = ["dep:boringtun", "dep:ring"] quic = ["dep:quinn", "dep:rustls", "dep:rcgen"] mimalloc = ["dep:mimalloc-rust"] diff --git a/easytier/src/common/ifcfg.rs b/easytier/src/common/ifcfg.rs index df3c070..646185f 100644 --- a/easytier/src/common/ifcfg.rs +++ b/easytier/src/common/ifcfg.rs @@ -399,7 +399,7 @@ pub struct DummyIfConfiger {} #[async_trait] impl IfConfiguerTrait for DummyIfConfiger {} -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "freebsd"))] pub type IfConfiger = MacIfConfiger; #[cfg(target_os = "linux")] @@ -408,5 +408,10 @@ pub type IfConfiger = LinuxIfConfiger; #[cfg(target_os = "windows")] pub type IfConfiger = WindowsIfConfiger; -#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] +#[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "windows", + target_os = "freebsd" +)))] pub type IfConfiger = DummyIfConfiger; diff --git a/easytier/src/common/network.rs b/easytier/src/common/network.rs index e4cc887..40beb4c 100644 --- a/easytier/src/common/network.rs +++ b/easytier/src/common/network.rs @@ -60,7 +60,9 @@ impl InterfaceFilter { #[cfg(any(target_os = "macos", target_os = "freebsd"))] impl InterfaceFilter { - async fn is_interface_physical(interface_name: &str) -> bool { + #[cfg(target_os = "macos")] + async fn is_interface_physical(&self) -> bool { + let interface_name = &self.iface.name; let output = tokio::process::Command::new("networksetup") .args(&["-listallhardwareports"]) .output() @@ -87,11 +89,17 @@ impl InterfaceFilter { false } + #[cfg(target_os = "freebsd")] + async fn is_interface_physical(&self) -> bool { + // if mac addr is not zero, then it's physical interface + self.iface.mac.map(|mac| !mac.is_zero()).unwrap_or(false) + } + async fn filter_iface(&self) -> bool { !self.iface.is_point_to_point() && !self.iface.is_loopback() && self.iface.is_up() - && Self::is_interface_physical(&self.iface.name).await + && self.is_interface_physical().await } } diff --git a/easytier/src/instance/mod.rs b/easytier/src/instance/mod.rs index e504964..17f7f32 100644 --- a/easytier/src/instance/mod.rs +++ b/easytier/src/instance/mod.rs @@ -1,7 +1,5 @@ pub mod instance; pub mod listeners; -#[cfg(feature = "tun")] -pub mod tun_codec; #[cfg(feature = "tun")] pub mod virtual_nic; diff --git a/easytier/src/instance/tun_codec.rs b/easytier/src/instance/tun_codec.rs deleted file mode 100644 index e07448d..0000000 --- a/easytier/src/instance/tun_codec.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::io; - -use byteorder::{NativeEndian, NetworkEndian, WriteBytesExt}; -use tokio_util::bytes::{BufMut, Bytes, BytesMut}; -use tokio_util::codec::{Decoder, Encoder}; - -/// A packet protocol IP version -#[derive(Debug, Clone, Copy, Default)] -enum PacketProtocol { - #[default] - IPv4, - IPv6, - Other(u8), -} - -// Note: the protocol in the packet information header is platform dependent. -impl PacketProtocol { - #[cfg(any(target_os = "linux", target_os = "android"))] - fn into_pi_field(self) -> Result { - use nix::libc; - match self { - PacketProtocol::IPv4 => Ok(libc::ETH_P_IP as u16), - PacketProtocol::IPv6 => Ok(libc::ETH_P_IPV6 as u16), - PacketProtocol::Other(_) => Err(io::Error::new( - io::ErrorKind::Other, - "neither an IPv4 nor IPv6 packet", - )), - } - } - - #[cfg(any(target_os = "macos", target_os = "ios"))] - fn into_pi_field(self) -> Result { - use nix::libc; - match self { - PacketProtocol::IPv4 => Ok(libc::PF_INET as u16), - PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16), - PacketProtocol::Other(_) => Err(io::Error::new( - io::ErrorKind::Other, - "neither an IPv4 nor IPv6 packet", - )), - } - } - - #[cfg(target_os = "windows")] - fn into_pi_field(self) -> Result { - unimplemented!() - } -} - -#[derive(Debug)] -pub enum TunPacketBuffer { - Bytes(Bytes), - BytesMut(BytesMut), -} - -impl From for Bytes { - fn from(buf: TunPacketBuffer) -> Self { - match buf { - TunPacketBuffer::Bytes(bytes) => bytes, - TunPacketBuffer::BytesMut(bytes) => bytes.freeze(), - } - } -} - -impl AsRef<[u8]> for TunPacketBuffer { - fn as_ref(&self) -> &[u8] { - match self { - TunPacketBuffer::Bytes(bytes) => bytes.as_ref(), - TunPacketBuffer::BytesMut(bytes) => bytes.as_ref(), - } - } -} - -/// A Tun Packet to be sent or received on the TUN interface. -#[derive(Debug)] -pub struct TunPacket(PacketProtocol, TunPacketBuffer); - -/// Infer the protocol based on the first nibble in the packet buffer. -fn infer_proto(buf: &[u8]) -> PacketProtocol { - match buf[0] >> 4 { - 4 => PacketProtocol::IPv4, - 6 => PacketProtocol::IPv6, - p => PacketProtocol::Other(p), - } -} - -impl TunPacket { - /// Create a new `TunPacket` based on a byte slice. - pub fn new(buffer: TunPacketBuffer) -> TunPacket { - let proto = infer_proto(buffer.as_ref()); - TunPacket(proto, buffer) - } - - /// Return this packet's bytes. - pub fn get_bytes(&self) -> &[u8] { - match &self.1 { - TunPacketBuffer::Bytes(bytes) => bytes.as_ref(), - TunPacketBuffer::BytesMut(bytes) => bytes.as_ref(), - } - } - - pub fn into_bytes(self) -> Bytes { - match self.1 { - TunPacketBuffer::Bytes(bytes) => bytes, - TunPacketBuffer::BytesMut(bytes) => bytes.freeze(), - } - } - - pub fn into_bytes_mut(self) -> BytesMut { - match self.1 { - TunPacketBuffer::Bytes(_) => panic!("cannot into_bytes_mut from bytes"), - TunPacketBuffer::BytesMut(bytes) => bytes, - } - } -} - -/// A TunPacket Encoder/Decoder. -pub struct TunPacketCodec(bool, i32); - -impl TunPacketCodec { - /// Create a new `TunPacketCodec` specifying whether the underlying - /// tunnel Device has enabled the packet information header. - pub fn new(pi: bool, mtu: i32) -> TunPacketCodec { - TunPacketCodec(pi, mtu) - } -} - -impl Decoder for TunPacketCodec { - type Item = TunPacket; - type Error = io::Error; - - fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { - if buf.is_empty() { - return Ok(None); - } - - let mut pkt = buf.split_to(buf.len()); - - // reserve enough space for the next packet - if self.0 { - buf.reserve(self.1 as usize + 4); - } else { - buf.reserve(self.1 as usize); - } - - // if the packet information is enabled we have to ignore the first 4 bytes - if self.0 { - let _ = pkt.split_to(4); - } - - let proto = infer_proto(pkt.as_ref()); - Ok(Some(TunPacket(proto, TunPacketBuffer::BytesMut(pkt)))) - } -} - -impl Encoder for TunPacketCodec { - type Error = io::Error; - - fn encode(&mut self, item: TunPacket, dst: &mut BytesMut) -> Result<(), Self::Error> { - dst.reserve(item.get_bytes().len() + 4); - match item { - TunPacket(proto, bytes) if self.0 => { - // build the packet information header comprising of 2 u16 - // fields: flags and protocol. - let mut buf = Vec::::with_capacity(4); - - // flags is always 0 - buf.write_u16::(0)?; - // write the protocol as network byte order - buf.write_u16::(proto.into_pi_field()?)?; - - dst.put_slice(&buf); - dst.put(Bytes::from(bytes)); - } - TunPacket(_, bytes) => dst.put(Bytes::from(bytes)), - } - Ok(()) - } -} diff --git a/easytier/src/instance/virtual_nic.rs b/easytier/src/instance/virtual_nic.rs index 510c0aa..3034705 100644 --- a/easytier/src/instance/virtual_nic.rs +++ b/easytier/src/instance/virtual_nic.rs @@ -119,7 +119,7 @@ impl PacketProtocol { } } - #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))] fn into_pi_field(self) -> Result { use nix::libc; match self { @@ -338,7 +338,7 @@ impl VirtualNic { } } - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos"))] config.platform_config(|config| { // disable packet information so we can process the header by ourselves, see tun2 impl for more details config.packet_information(false); @@ -513,7 +513,8 @@ impl NicCtx { nic.link_up().await?; nic.remove_ip(None).await?; nic.add_ip(ipv4_addr, 24).await?; - if cfg!(target_os = "macos") { + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + { nic.add_route(ipv4_addr, 24).await?; } Ok(())