commit a4dbe9c39969387ff8bfe818bf2465cc23de778f Author: BloxerHD Date: Thu Jul 16 21:25:41 2026 +0100 Initial Commit diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..15d2b65 --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,56 @@ +name: Build and Test + +on: + push: + branches: ["**"] + +env: + DOCKER_TLS_CERTDIR: /certs + IMAGE_TAG: ${{ github.sha }} + SHORT_SHA: ${{ github.sha }} + +jobs: + rust-boss: + runs-on: debian-trixie + + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Cache Container Storage + uses: actions/cache@v6 + with: + path: | + /var/lib/containers/storage + /run/containers/storage + ~/.local/share/containers/storage + key: image-cache + + - name: Login to Registry + run: $RNEX_CONTAINER_PLATFORM login -u ${{ secrets.PACKAGE_USER }} -p ${{ secrets.PACKAGE_PWD }} git.spbr.net + + - name: Set Short SHA + run: echo "SHORT_SHA=${GITHUB_SHA::6}" >> $GITHUB_ENV + + - name: Build and Push + env: + CI_REGISTRY_IMAGE: git.spbr.net/spacebar/nncs + CI_COMMIT_SHORT_SHA: ${{ env.SHORT_SHA }} + run: | + $RNEX_CONTAINER_PLATFORM login -u ${{ secrets.PACKAGE_USER }} -p ${{ secrets.PACKAGE_PWD }} git.spbr.net + $RNEX_CONTAINER_PLATFORM build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" . + $RNEX_CONTAINER_PLATFORM push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" + + - name: Push Retagged Latest + if: github.ref == 'refs/heads/main' + env: + CI_REGISTRY_IMAGE: git.spbr.net/spacebar/nncs + CI_COMMIT_SHORT_SHA: latest + CI_COMMIT_PREVIOUS_SHA: ${{ env.SHORT_SHA }} + run: | + $RNEX_CONTAINER_PLATFORM login -u ${{ secrets.PACKAGE_USER }} -p ${{ secrets.PACKAGE_PWD }} git.spbr.net + $RNEX_CONTAINER_PLATFORM pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_PREVIOUS_SHA" + $RNEX_CONTAINER_PLATFORM tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_PREVIOUS_SHA" "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" + $RNEX_CONTAINER_PLATFORM push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0c260e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.env +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..437aea9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "nncs" +version = "0.1.0" +edition = "2024" + +[dependencies] +dotenvy = "0.15.7" +tokio = { version = "1.52.3", features = ["full"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..461ea43 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:1 + +FROM rust:alpine AS builder + +RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static protobuf-dev lld + +WORKDIR /app + +COPY Cargo.toml ./ +RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo fetch + +COPY . . + +ENV SQLX_OFFLINE=true + +RUN OPENSSL_LIB_DIR=/usr/lib OPENSSL_INCLUDE_DIR=/usr/include/openssl OPENSSL_STATIC=1 RUSTFLAGS="-C target-feature=+aes,+sse -C relocation-model=static -C linker=ld.lld" cargo build --release --target x86_64-unknown-linux-musl + +FROM alpine:latest AS final +RUN apk add --no-cache ca-certificates +RUN update-ca-certificates +WORKDIR / +COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/nncs /nncs + +ENTRYPOINT ["/nncs"] diff --git a/src/ipc.rs b/src/ipc.rs new file mode 100644 index 0000000..6d161da --- /dev/null +++ b/src/ipc.rs @@ -0,0 +1,71 @@ +use std::net::{Ipv4Addr, SocketAddrV4}; +use std::sync::Arc; +use tokio::net::UdpSocket; +use crate::types::{Message, Sockets}; + +pub struct IpcFrame { + pub message: Message, + pub remote_addr: SocketAddrV4, +} + +impl IpcFrame { + pub fn to_bytes(&self, socket: Arc) -> [u8; 22] { + let message_bytes = self.message.to_response_bytes(socket, self.remote_addr); + + let mut buf = [0u8; 22]; + + buf[0..2].copy_from_slice(&self.remote_addr.port().to_be_bytes()); + buf[2..6].copy_from_slice(&u32::from(*self.remote_addr.ip()).to_be_bytes()); + buf[6..].copy_from_slice(&message_bytes.unwrap()); + + buf + } + + pub fn from_bytes(slice: &[u8]) -> Result { + if slice.len() < 6 { + return Err("Frame too small to extract client metadata".into()) + } + + let remote_port = u16::from_be_bytes(slice[0..2].try_into().unwrap()); + let remote_ip = Ipv4Addr::from(u32::from_be_bytes(slice[2..6].try_into().unwrap())); + + let remote_addr = SocketAddrV4::new(remote_ip, remote_port); + + let message = Message::from_bytes(&slice[6..])?; + + Ok(IpcFrame{message, remote_addr}) + } +} + +pub async fn run(host: String, port: u16, sockets: Arc) -> Result<(), Box> { + let ipc_socket = UdpSocket::bind(format!("{host}:{port}")).await?; + + let mut buf = [0u8; 2048]; + + tokio::spawn(async move { + loop { + match ipc_socket.recv_from(&mut buf).await { + Ok((len, _sender_addr)) => { + match IpcFrame::from_bytes(&buf[..len]) { + Ok(ipc_frame) => { + let socket = &sockets.alt; + let _ = socket.send_to( + &ipc_frame.message.to_response_bytes(socket.clone(), ipc_frame.remote_addr).unwrap(), + format!("{}:{}", ipc_frame.remote_addr.ip(), ipc_frame.remote_addr.port()), + ).await; + }, + Err(e) => { + eprintln!("Failed to decode IPC Frame: {e}") + } + } + }, + Err(e) => { + eprintln!("IPC Socket Error: {e}"); + break; + } + } + } + }); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..02df296 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,120 @@ +use std::net::{IpAddr, SocketAddrV4}; +use std::sync::Arc; +use std::env; +use dotenvy::dotenv; +use tokio::net::UdpSocket; + +use crate::ipc::IpcFrame; +use crate::types::{Message, Sockets}; + +pub mod types; +mod ipc; + +async fn respond(socket: Arc, message: Message, remote_addr: SocketAddrV4) { + let _ = socket.send_to( + &message.to_response_bytes(socket.clone(), remote_addr).unwrap(), + remote_addr + ).await; +} + +async fn respond_from_partner(socket: Arc, message: crate::types::Message, remote_addr: SocketAddrV4, sockets: Arc) -> Result<(), Box> { + let partner_addr = env::var("NNCS_PARTNER_IP")?; + + let ipc_sender_socket = sockets.alt.clone(); + + let frame = IpcFrame { + message, + remote_addr + }; + let frame_bytes = frame.to_bytes(socket); + + ipc_sender_socket.send_to(&frame_bytes, partner_addr).await?; + + Ok(()) +} + +fn init_port(socket: Arc, sockets: Arc) { + let mut buf = [0; 64]; + + tokio::spawn(async move { + loop { + match socket.recv_from(&mut buf).await { + Ok((len, src_addr)) => { + let remote_ip = match src_addr.ip() { + IpAddr::V4(ip) => ip, + IpAddr::V6(_) => break, + }; + let remote_port = src_addr.port(); + let remote_addr = SocketAddrV4::new(remote_ip, remote_port); + + match types::Message::from_bytes(&buf[..len]) { + Ok(packet) => { + match packet.r#type { + 1 => respond(socket.clone(), packet, remote_addr).await, // Server responds from regular ip and regular port + + 2 => match respond_from_partner(socket.clone(), packet, remote_addr, sockets.clone()).await { // Server responds from different ip and different port + Ok(_) => (), + Err(e) => eprintln!("Error responding from partner: {e}"), + }, + + 3 => { // Server responds from regular ip and different port + let socket = sockets.alt.clone(); + respond(socket, packet, remote_addr).await; + }, + + 4 => respond(socket.clone(), packet, remote_addr).await, // Server responds from regular ip and regular port + + 5 => respond(socket.clone(), packet, remote_addr).await, // Server responds from regular ip and regular port + + 101 => respond(socket.clone(), packet, remote_addr).await, // Server responds from regular ip and regular port + + 102 => { // Server responds from regular ip and different port + let socket = sockets.alt.clone(); + respond(socket, packet, remote_addr).await; + }, + + 103 => respond(socket.clone(), packet, remote_addr).await, // Server responds from regular ip and regular port + _ => { + eprintln!("[{}] Invalid message type from {src_addr}: {}", socket.local_addr().unwrap().port(), packet.r#type) + }, + } + }, + Err(e) => { + eprintln!("[{}] Error parsing message from {src_addr}: {e}", socket.local_addr().unwrap().port()) + } + } + }, + Err(e) => { + eprintln!("[{}] Socket error: {e}", socket.local_addr().unwrap().port()); + break; + } + } + } + }); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenv().ok(); + + let sockets = Arc::new(Sockets{ + primary: Arc::new(UdpSocket::bind("0.0.0.0:10025").await?), + secondary: Arc::new(UdpSocket::bind("0.0.0.0:10125").await?), + alt: Arc::new(UdpSocket::bind("0.0.0.0:0").await?), + p33334: Arc::new(UdpSocket::bind("0.0.0.0:33334").await?), + p33335: Arc::new(UdpSocket::bind("0.0.0.0:33335").await?), + }); + + init_port(sockets.primary.clone(), sockets.clone()); + init_port(sockets.secondary.clone(), sockets.clone()); + + println!("NNCS Running on Ports 10025 and 10125"); + + let ipc_port: u16= env::var("NNCS_IPC_PORT").unwrap_or("9001".into()).parse()?; + ipc::run("0.0.0.0".into(), ipc_port, sockets).await?; + + println!("IPC Running on Port {ipc_port}"); + + std::future::pending::<()>().await; + Ok(()) +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..ffb200b --- /dev/null +++ b/src/types.rs @@ -0,0 +1,55 @@ +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::sync::Arc; +use tokio::net::UdpSocket; + +pub struct Sockets { + pub primary: Arc, + pub secondary: Arc, + pub alt: Arc, + pub p33334: Arc, + pub p33335: Arc, +} + +pub struct Message { + pub r#type: u32, + pub external_port: u32, + pub external_address: Ipv4Addr, + pub local_address: u32, +} + +impl Message { + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < 16 { + return Err(format!("Buffer too short, expected 16 bytes but found {} bytes", bytes.len())) + }; + + let r#type = u32::from_be_bytes(bytes[0..4].try_into().unwrap()); + let external_port = u32::from_be_bytes(bytes[4..8].try_into().unwrap()); + let external_address = Ipv4Addr::from(u32::from_be_bytes(bytes[8..12].try_into().unwrap())); + let local_address = u32::from_be_bytes(bytes[12..16].try_into().unwrap()); + + Ok(Message{ + r#type, + external_port, + external_address, + local_address, + }) + } + + pub fn to_response_bytes(&self, socket: Arc, remote_addr: SocketAddrV4) -> Result<[u8; 16], String> { + let mut bytes = [0u8; 16]; + + bytes[0..4].copy_from_slice(&self.r#type.to_be_bytes()); + bytes[4..8].copy_from_slice(&remote_addr.port().to_be_bytes()); + bytes[8..12].copy_from_slice(&u32::from(*remote_addr.ip()).to_be_bytes()); + let local_ip = match socket.local_addr().unwrap() { + SocketAddr::V4(addr) => *addr.ip(), + SocketAddr::V6(_) => { + return Err("Ipv6 not supported".into()); + }, + }; + bytes[12..16].copy_from_slice(&u32::from(local_ip).to_be_bytes()); + + Ok(bytes) + } +}