├── .github └── workflows │ ├── build.yml │ └── clippy.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── backend ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ ├── bin │ └── soxy.rs │ ├── lib.rs │ └── svc │ ├── high.rs │ ├── low.rs │ └── mod.rs ├── big_picture.png ├── common ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ ├── api.rs │ ├── clipboard │ ├── backend.rs │ ├── frontend.rs │ ├── mod.rs │ └── protocol.rs │ ├── command │ ├── backend.rs │ ├── frontend.rs │ └── mod.rs │ ├── ftp │ ├── backend.rs │ ├── frontend.rs │ ├── mod.rs │ └── protocol.rs │ ├── lib.rs │ ├── log.rs │ ├── service.rs │ ├── socks5 │ ├── backend.rs │ ├── frontend.rs │ ├── mod.rs │ └── protocol.rs │ ├── stage0 │ ├── backend.rs │ ├── frontend.rs │ └── mod.rs │ └── util.rs ├── frontend ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs └── src │ ├── config.rs │ ├── control.rs │ ├── lib.rs │ ├── svc │ ├── citrix │ │ ├── headers.h │ │ ├── headers.rs │ │ ├── mod.rs │ │ └── vd.rs │ ├── mod.rs │ ├── rdp │ │ ├── headers.h │ │ ├── headers.rs │ │ └── mod.rs │ └── semaphore.rs │ └── windows.rs ├── standalone ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ └── bin │ └── standalone.rs └── tools └── stage0 └── stage0.ps1 /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build for Linux, Windows and macOS 2 | 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | tags: [ 'v*' ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | SUFFIX: ${{ github.ref_type == 'tag' && github.ref_name || github.sha }} 15 | 16 | 17 | jobs: 18 | build-linux-windows: 19 | name: Build for Linux and Windows 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Dependencies 25 | run: sudo apt install gcc-multilib libc-dev clang mingw-w64 clang 26 | 27 | - name: Rustup update 28 | run: rustup update 29 | 30 | - name: Setup 31 | run: make setup 32 | 33 | - name: Build debug 34 | run: make debug 35 | 36 | - name: Build release 37 | run: make release 38 | 39 | - name: Build release 40 | run: make win7 41 | 42 | - name: Gather artifacts 43 | run: | 44 | mkdir artifacts 45 | mv debug release artifacts/ 46 | 47 | - name: Upload artifacts 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: linux-windows 51 | path: artifacts 52 | retention-days: 0 53 | 54 | 55 | build-frontend-macos-arm64: 56 | name: Build frontend for macOS arm64 57 | runs-on: macos-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | 61 | - name: Rustup update 62 | run: rustup update 63 | 64 | - name: Build frontend debug 65 | run: cd frontend ; cargo build --features=log 66 | 67 | - name: Build frontend release 68 | run: cd frontend ; cargo build --release 69 | 70 | - name: Build standalone debug 71 | run: cd standalone ; cargo build --features=log 72 | 73 | - name: Build standalone release 74 | run: cd standalone ; cargo build --release --features=log 75 | 76 | - name: Gather artifacts 77 | run: | 78 | mkdir -p artifacts/debug/frontend/macos-arm64 79 | mv frontend/target/debug/libsoxy.dylib artifacts/debug/frontend/macos-arm64/ 80 | mkdir -p artifacts/release/frontend/macos-arm64 81 | mv frontend/target/release/libsoxy.dylib artifacts/release/frontend/macos-arm64/ 82 | mkdir -p artifacts/debug/standalone/macos-arm64 83 | mv standalone/target/debug/soxy_standalone artifacts/debug/standalone/macos-arm64/ 84 | mkdir -p artifacts/release/standalone/macos-arm64 85 | mv standalone/target/release/soxy_standalone artifacts/release/standalone/macos-arm64/ 86 | 87 | - name: Upload debug artifacts 88 | uses: actions/upload-artifact@v4 89 | with: 90 | name: macos-arm64 91 | path: artifacts 92 | retention-days: 0 93 | 94 | 95 | build-frontend-macos-x86_64: 96 | name: Build frontend for macOS x86_64 97 | runs-on: macos-13 98 | steps: 99 | - uses: actions/checkout@v4 100 | 101 | - name: Rustup update 102 | run: rustup update 103 | 104 | - name: Build frontend debug 105 | run: cd frontend ; cargo build --features=log 106 | 107 | - name: Build frontend release 108 | run: cd frontend ; cargo build --release 109 | 110 | - name: Build standalone debug 111 | run: cd standalone ; cargo build --features=log 112 | 113 | - name: Build standalone release 114 | run: cd standalone ; cargo build --release --features=log 115 | 116 | - name: Gather artifacts 117 | run: | 118 | mkdir -p artifacts/debug/frontend/macos-x86_64 119 | mv frontend/target/debug/libsoxy.dylib artifacts/debug/frontend/macos-x86_64/ 120 | mkdir -p artifacts/release/frontend/macos-x86_64 121 | mv frontend/target/release/libsoxy.dylib artifacts/release/frontend/macos-x86_64/ 122 | mkdir -p artifacts/debug/standalone/macos-x86_64 123 | mv standalone/target/debug/soxy_standalone artifacts/debug/standalone/macos-x86_64/ 124 | mkdir -p artifacts/release/standalone/macos-x86_64 125 | mv standalone/target/release/soxy_standalone artifacts/release/standalone/macos-x86_64/ 126 | 127 | - name: Upload debug artifacts 128 | uses: actions/upload-artifact@v4 129 | with: 130 | name: macos-x86_64 131 | path: artifacts 132 | retention-days: 0 133 | 134 | 135 | package: 136 | name: Package artifacts 137 | runs-on: ubuntu-latest 138 | needs: [ build-linux-windows, build-frontend-macos-arm64, build-frontend-macos-x86_64] 139 | steps: 140 | - uses: actions/checkout@v4 141 | 142 | - uses: actions/download-artifact@v4 143 | with: 144 | merge-multiple: true 145 | path: artifacts 146 | 147 | - name: Gathering artifacts 148 | run: | 149 | mkdir -p debug/soxy-debug-${{ env.SUFFIX }} 150 | mv artifacts/debug/* debug/soxy-debug-${{ env.SUFFIX }}/ 151 | cp LICENSE README.md debug/soxy-debug-${{ env.SUFFIX }}/ 152 | mkdir -p release/soxy-release-${{ env.SUFFIX }} 153 | mv artifacts/release/* release/soxy-release-${{ env.SUFFIX }}/ 154 | cp LICENSE README.md release/soxy-release-${{ env.SUFFIX }}/ 155 | 156 | - name: Upload debug artifacts 157 | uses: actions/upload-artifact@v4 158 | with: 159 | name: soxy-debug-${{ env.SUFFIX }} 160 | path: debug 161 | 162 | - name: Upload release artifacts 163 | uses: actions/upload-artifact@v4 164 | with: 165 | name: soxy-release-${{ env.SUFFIX }} 166 | path: release 167 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Clippy 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | clippy: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Dependencies 21 | run: sudo apt install gcc-multilib libc-dev clang mingw-w64 clang 22 | 23 | - name: Rustup update 24 | run: rustup update 25 | 26 | - name: Setup 27 | run: make setup 28 | 29 | - name: Run clippy 30 | run: make clippy 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | release 2 | debug 3 | .ccls-cache 4 | *~ 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGETS_FRONTEND ?= i686-pc-windows-gnu x86_64-pc-windows-gnu i686-unknown-linux-gnu x86_64-unknown-linux-gnu 2 | TARGETS_BACKEND ?= i686-pc-windows-gnu x86_64-pc-windows-gnu i686-unknown-linux-gnu x86_64-unknown-linux-gnu 3 | TARGETS_STANDALONE ?= i686-pc-windows-gnu x86_64-pc-windows-gnu i686-unknown-linux-gnu x86_64-unknown-linux-gnu 4 | TARGETS_WIN7_BACKEND ?= x86_64-pc-windows-gnu 5 | 6 | RELEASE_DIR:=release 7 | DEBUG_DIR:=debug 8 | 9 | BACKEND_RELEASE_BASE_RUST_FLAGS:=--remap-path-prefix ${HOME}=/foo -Zlocation-detail=none 10 | BACKEND_WIN7_BASE_RUST_FLAGS:=--remap-path-prefix ${HOME}=/foo 11 | 12 | BACKEND_RELEASE_LIB_RUST_FLAGS:=$(BACKEND_RELEASE_BASE_RUST_FLAGS) 13 | BACKEND_WIN7_LIB_RUST_FLAGS:=$(BACKEND_WIN7_BASE_RUST_FLAGS) 14 | 15 | BACKEND_RELEASE_BIN_RUST_FLAGS:=$(BACKEND_RELEASE_BASE_RUST_FLAGS) 16 | BACKEND_RELEASE_BIN_WINDOWS_RUST_FLAGS=$(BACKEND_RELEASE_BIN_RUST_FLAGS) -Ctarget-feature=+crt-static 17 | BACKEND_WIN7_BIN_RUST_FLAGS:=$(BACKEND_WIN7_BASE_RUST_FLAGS) -Ctarget-feature=+crt-static 18 | 19 | BACKEND_BUILD_FLAGS:=-Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort 20 | 21 | TOOLCHAIN_FRONTEND_DEBUG ?= stable 22 | TOOLCHAIN_FRONTEND_RELEASE ?= stable 23 | TOOLCHAIN_BACKEND_DEBUG ?= stable 24 | TOOLCHAIN_BACKEND_RELEASE ?= nightly 25 | TOOLCHAIN_STANDALONE_DEBUG ?= stable 26 | TOOLCHAIN_STANDALONE_RELEASE ?= stable 27 | TOOLCHAIN_WIN7_BACKEND ?= 1.75 28 | 29 | BACKEND_WIN7_TARGET_DIR:=target/win7 30 | 31 | SHELL:=bash 32 | 33 | .PHONY: setup 34 | setup: 35 | echo $(TOOLCHAIN_FRONTEND_DEBUG) $(TOOLCHAIN_FRONTEND_RELEASE) $(TOOLCHAIN_BACKEND_DEBUG) $(TOOLCHAIN_BACKEND_RELEASE) $(TOOLCHAIN_STANDALONE_DEBUG) $(TOOLCHAIN_STANDALONE_RELEASE) $(TOOLCHAIN_WIN7_BACKEND) | tr ' ' '\n' | sort -u | while read toolchain ; do \ 36 | rustup toolchain add $$toolchain || exit 1 ; \ 37 | done 38 | @echo $(TARGETS_FRONTEND) $(TARGETS_BACKEND) $(TARGETS_STANDALONE) | tr ' ' '\n' | sort -u | while read target ; do \ 39 | echo $(TOOLCHAIN_FRONTEND_DEBUG) $(TOOLCHAIN_FRONTEND_RELEASE) $(TOOLCHAIN_BACKEND_DEBUG) $(TOOLCHAIN_BACKEND_RELEASE) $(TOOLCHAIN_STANDALONE_DEBUG) $(TOOLCHAIN_STANDALONE_RELEASE) | tr ' ' '\n' | sort -u | while read toolchain ; do \ 40 | echo ; echo "# Installing component $$target for $$toolchain" ; echo ; \ 41 | rustup target add --toolchain $$toolchain $$target || exit 1 ; \ 42 | if [[ ! "$$target" =~ "llvm" ]] ; then \ 43 | rustup component add --toolchain $${toolchain}-$$target rust-src || exit 1 ; \ 44 | fi ; \ 45 | done ; \ 46 | done 47 | @echo $(TARGETS_WIN7_BACKEND) | tr ' ' '\n' | sort -u | while read target ; do \ 48 | echo $(TOOLCHAIN_WIN7_BACKEND) | tr ' ' '\n' | sort -u | while read toolchain ; do \ 49 | echo ; echo "# Installing component $$target for $$toolchain" ; echo ; \ 50 | rustup target add --toolchain $$toolchain $$target || exit 1 ; \ 51 | if [[ ! "$$target" =~ "llvm" ]] ; then \ 52 | rustup component add --toolchain $${toolchain}-$$target rust-src || exit 1 ; \ 53 | fi ; \ 54 | done ; \ 55 | done 56 | 57 | 58 | .PHONY: release 59 | release: build-release 60 | @for t in $(TARGETS_FRONTEND) ; do \ 61 | for f in frontend/target/$$t/release/*soxy{,.dll,.exe,.so} ; do \ 62 | if [[ -f "$$f" ]] ; then \ 63 | mkdir -p $(RELEASE_DIR)/frontend/$$t && \ 64 | cp "$$f" $(RELEASE_DIR)/frontend/$$t/ ; \ 65 | fi ; \ 66 | done ; \ 67 | done 68 | @for t in $(TARGETS_BACKEND) ; do \ 69 | for f in backend/target/$$t/release/*soxy{,.dll,.exe,.so} ; do \ 70 | if [[ -f "$$f" ]] ; then \ 71 | mkdir -p $(RELEASE_DIR)/backend/$$t && \ 72 | cp "$$f" $(RELEASE_DIR)/backend/$$t/ ; \ 73 | fi ; \ 74 | done ; \ 75 | done 76 | @for t in $(TARGETS_STANDALONE) ; do \ 77 | for f in standalone/target/$$t/release/*standalone{,.exe} ; do \ 78 | if [[ -f "$$f" ]] ; then \ 79 | mkdir -p $(RELEASE_DIR)/standalone/$$t && \ 80 | cp "$$f" $(RELEASE_DIR)/standalone/$$t/ ; \ 81 | fi ; \ 82 | done ; \ 83 | done 84 | 85 | .PHONY: debug 86 | debug: build-debug 87 | @for t in $(TARGETS_FRONTEND) ; do \ 88 | for f in frontend/target/$$t/debug/*soxy{,.dll,.exe,.so} ; do \ 89 | if [[ -f "$$f" ]] ; then \ 90 | mkdir -p $(DEBUG_DIR)/frontend/$$t && \ 91 | cp "$$f" $(DEBUG_DIR)/frontend/$$t/ ; \ 92 | fi ; \ 93 | done ; \ 94 | done 95 | @for t in $(TARGETS_BACKEND) ; do \ 96 | for f in backend/target/$$t/debug/*soxy{,.dll,.exe,.so} ; do \ 97 | if [[ -f "$$f" ]] ; then \ 98 | mkdir -p $(DEBUG_DIR)/backend/$$t && \ 99 | cp "$$f" $(DEBUG_DIR)/backend/$$t/ ; \ 100 | fi ; \ 101 | done ; \ 102 | done 103 | @for t in $(TARGETS_STANDALONE) ; do \ 104 | for f in standalone/target/$$t/debug/*standalone{,.exe} ; do \ 105 | if [[ -f "$$f" ]] ; then \ 106 | mkdir -p $(DEBUG_DIR)/standalone/$$t && \ 107 | cp "$$f" $(DEBUG_DIR)/standalone/$$t/ ; \ 108 | fi ; \ 109 | done ; \ 110 | done 111 | 112 | .PHONY: win7 113 | win7: build-win7 114 | @for t in $(TARGETS_WIN7_BACKEND) ; do \ 115 | w7t=$$(sed -e 's/windows/win7/' <<< $$t); \ 116 | for f in backend/$(BACKEND_WIN7_TARGET_DIR)/$$t/release/*soxy{,.dll,.exe,.so} ; do \ 117 | if [[ -f "$$f" ]] ; then \ 118 | mkdir -p $(RELEASE_DIR)/backend/$$w7t && \ 119 | cp "$$f" $(RELEASE_DIR)/backend/$$w7t/ ; \ 120 | fi ; \ 121 | done ; \ 122 | done 123 | 124 | .PHONY: distclean 125 | distclean: clean 126 | rm -rf ${RELEASE_DIR} ${DEBUG_DIR} 127 | 128 | ############# 129 | 130 | .PHONY: build-release 131 | build-release: 132 | @for t in $(TARGETS_FRONTEND) ; do \ 133 | echo ; echo "# Building release frontend for $$t with $(TOOLCHAIN_FRONTEND_RELEASE)" ; echo ; \ 134 | (cd frontend && cargo +$(TOOLCHAIN_FRONTEND_RELEASE) build --release --features log --target $$t && cd ..) || exit 1 ; \ 135 | done 136 | @for t in $(TARGETS_BACKEND) ; do \ 137 | echo ; echo "# Building release backend library for $$t with $(TOOLCHAIN_BACKEND_RELEASE)" ; echo ; \ 138 | (cd backend && RUSTFLAGS="$(BACKEND_RELEASE_LIB_RUST_FLAGS)" cargo +$(TOOLCHAIN_BACKEND_RELEASE) build --lib --release --target $$t $(BACKEND_BUILD_FLAGS) && cd ..) || exit 1 ; \ 139 | echo ; echo "# Building release backend binary for $$t with $(TOOLCHAIN_BACKEND_RELEASE)" ; echo ; \ 140 | FLAGS="$(BACKEND_RELEASE_BIN_RUST_FLAGS)" ; \ 141 | if echo $$t | grep -q windows ; then \ 142 | FLAGS="$(BACKEND_RELEASE_BIN_WINDOWS_RUST_FLAGS)" ; \ 143 | fi ; \ 144 | (cd backend && RUSTFLAGS="$$FLAGS" cargo +$(TOOLCHAIN_BACKEND_RELEASE) build --bins --release --target $$t $(BACKEND_BUILD_FLAGS) && cd ..) ; \ 145 | done 146 | @for t in $(TARGETS_STANDALONE) ; do \ 147 | echo ; echo "# Building release standalone for $$t with $(TOOLCHAIN_STANDALONE_RELEASE)" ; echo ; \ 148 | (cd standalone && cargo +$(TOOLCHAIN_STANDALONE_RELEASE) build --release --features log --target $$t && cd ..) || exit 1 ; \ 149 | done 150 | 151 | .PHONY: build-debug 152 | build-debug: 153 | @for t in $(TARGETS_FRONTEND) ; do \ 154 | echo ; echo "# Building debug frontend for $$t with $(TOOLCHAIN_FRONTEND_DEBUG)" ; echo ; \ 155 | (cd frontend && cargo +$(TOOLCHAIN_FRONTEND_DEBUG) build --features log --target $$t && cd ..) || exit 1 ; \ 156 | done 157 | @for t in $(TARGETS_BACKEND) ; do \ 158 | echo ; echo "# Building debug backend library for $$t with $(TOOLCHAIN_BACKEND_DEBUG)" ; echo ; \ 159 | (cd backend && cargo +$(TOOLCHAIN_BACKEND_DEBUG) build --lib --features log --target $$t && cd ..) || exit 1 ; \ 160 | echo ; echo "# Building debug backend binary for $$t with $(TOOLCHAIN_BACKEND_DEBUG)" ; echo ; \ 161 | (cd backend && cargo +$(TOOLCHAIN_BACKEND_DEBUG) build --bins --features log --target $$t && cd ..) || exit 1 ; \ 162 | done 163 | @for t in $(TARGETS_STANDALONE) ; do \ 164 | echo ; echo "# Building debug standalone for $$t with $(TOOLCHAIN_STANDALONE_DEBUG)" ; echo ; \ 165 | (cd standalone && cargo +$(TOOLCHAIN_STANDALONE_DEBUG) build --features log --target $$t && cd ..) || exit 1 ; \ 166 | done 167 | 168 | .PHONY: build-win7 169 | build-win7: 170 | for t in $(TARGETS_WIN7_BACKEND) ; do \ 171 | w7t=$$(sed -e 's/windows/win7/' <<< $$t); \ 172 | echo ; echo "# Building release backend library for $$t with $(TOOLCHAIN_WIN7_BACKEND)" ; echo ; \ 173 | (cd backend && \ 174 | RUSTFLAGS="$(BACKEND_WIN7_LIB_RUST_FLAGS)" \ 175 | CARGO_TARGET_DIR="$(BACKEND_WIN7_TARGET_DIR)" \ 176 | cargo +$(TOOLCHAIN_WIN7_BACKEND) build --lib --release --target $$t \ 177 | ) || exit 1 ; \ 178 | echo ; echo "# Building release backend binary for $$t with $(TOOLCHAIN_WIN7_BACKEND)" ; echo ; \ 179 | (cd backend && \ 180 | RUSTFLAGS="$(BACKEND_WIN7_BIN_RUST_FLAGS)" \ 181 | CARGO_TARGET_DIR="$(BACKEND_WIN7_TARGET_DIR)" \ 182 | cargo +$(TOOLCHAIN_WIN7_BACKEND) build --bins --release --target $$t \ 183 | ) ; \ 184 | done 185 | 186 | 187 | ############# 188 | 189 | .PHONY: clippy 190 | clippy: 191 | @for t in $(TARGETS_FRONTEND) ; do \ 192 | echo ; echo "# Clippy on frontend for $$t with $(TOOLCHAIN_FRONTEND_DEBUG)" ; echo ; \ 193 | (cd frontend && cargo +$(TOOLCHAIN_FRONTEND_DEBUG) $@ --target $$t && cd ..) || exit 1 ; \ 194 | done 195 | @for t in $(TARGETS_BACKEND) ; do \ 196 | echo ; echo "# Clippy on backend for $$t with $(TOOLCHAIN_BACKEND_DEBUG)" ; echo ; \ 197 | (cd backend && cargo +$(TOOLCHAIN_BACKEND_DEBUG) $@ --target $$t && cd ..) || exit 1 ; \ 198 | done 199 | @for t in $(TARGETS_STANDALONE) ; do \ 200 | echo ; echo "# Clippy on standalone for $$t with $(TOOLCHAIN_STANDALONE_DEBUG)" ; echo ; \ 201 | (cd standalone && cargo +$(TOOLCHAIN_STANDALONE_DEBUG) $@ --target $$t && cd ..) || exit 1 ; \ 202 | done 203 | 204 | .PHONY: cargo-fmt 205 | cargo-fmt: 206 | @for c in common frontend backend standalone ; do \ 207 | (cd $$c && $@ +nightly && cd ..) || exit 1 ; \ 208 | done 209 | 210 | %: 211 | @for c in common frontend backend standalone ; do \ 212 | (cd $$c && cargo $@ && cd ..) || exit 1 ; \ 213 | done 214 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "2.3.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | common = { path = "../common", features = [ "backend" ] } 8 | crossbeam-channel = "0" 9 | libloading = "0" 10 | log = { version = "0", optional = true } 11 | windows-sys = { version = "0", features = [ 12 | "Win32_Networking_WinSock", 13 | "Win32_Security", 14 | "Win32_Storage_FileSystem", 15 | "Win32_System_Console", 16 | "Win32_System_IO", 17 | "Win32_System_LibraryLoader", 18 | "Win32_System_RemoteDesktop", 19 | "Win32_System_SystemServices", 20 | "Win32_System_Threading", 21 | ] } 22 | 23 | [build-dependencies] 24 | bindgen = "0" 25 | 26 | [lints.clippy] 27 | pedantic = { level = "deny", priority = -1 } 28 | must_use_candidate = "allow" 29 | enum-glob-use = "allow" 30 | missing-errors-doc = "allow" 31 | 32 | [[bin]] 33 | name = "soxy" 34 | 35 | [lib] 36 | crate-type = [ "cdylib", "lib" ] 37 | name = "soxy" 38 | 39 | [profile.release] 40 | opt-level = "z" 41 | debug = false 42 | strip = true 43 | lto = true 44 | panic = "abort" 45 | codegen-units = 1 46 | 47 | [features] 48 | log = [ "common/log", "dep:log" ] 49 | -------------------------------------------------------------------------------- /backend/src/bin/soxy.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(debug_assertions)] 3 | soxy::main(common::Level::Debug); 4 | #[cfg(not(debug_assertions))] 5 | soxy::main(common::Level::Info); 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | use common::{self, api, service}; 2 | use std::{ffi, fmt, mem, sync, thread, time}; 3 | use svc::Handler; 4 | use windows_sys as ws; 5 | 6 | mod svc; 7 | 8 | const TO_SVC_CHANNEL_SIZE: usize = 256; 9 | 10 | enum Error { 11 | Svc(svc::Error), 12 | PipelineBroken, 13 | } 14 | 15 | impl fmt::Display for Error { 16 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 17 | match self { 18 | Self::Svc(e) => write!(f, "virtual channel error: {e}"), 19 | Self::PipelineBroken => write!(f, "broken pipeline"), 20 | } 21 | } 22 | } 23 | 24 | impl From for Error { 25 | fn from(e: svc::Error) -> Self { 26 | Self::Svc(e) 27 | } 28 | } 29 | 30 | impl From for Error { 31 | fn from(_e: crossbeam_channel::RecvError) -> Self { 32 | Self::PipelineBroken 33 | } 34 | } 35 | 36 | impl From> for Error { 37 | fn from(_e: crossbeam_channel::SendError) -> Self { 38 | Self::PipelineBroken 39 | } 40 | } 41 | 42 | fn backend_to_frontend( 43 | channel: &sync::RwLock>>, 44 | from_backend: &crossbeam_channel::Receiver, 45 | ) -> Result<(), Error> { 46 | let mut disconnect = false; 47 | 48 | loop { 49 | match from_backend.recv()? { 50 | api::ChunkControl::Shutdown => { 51 | common::info!("received shutdown, closing"); 52 | disconnect = true; 53 | } 54 | api::ChunkControl::Chunk(chunk) => { 55 | common::trace!("{chunk}"); 56 | 57 | let data = chunk.serialized(); 58 | 59 | match channel.read().unwrap().as_ref() { 60 | None => { 61 | common::debug!("cannot write on disconnected channel"); 62 | } 63 | Some(svc) => { 64 | if let Err(e) = svc.write(&data) { 65 | common::error!("failed to write on channel: {e}"); 66 | disconnect = true; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | if disconnect { 74 | common::info!("disconnecting from channel"); 75 | channel.write().unwrap().take(); 76 | disconnect = false; 77 | } 78 | } 79 | } 80 | 81 | fn frontend_to_backend<'a>( 82 | svc: &'a svc::Svc<'a>, 83 | channel: &'a sync::RwLock>>, 84 | to_backend: &crossbeam_channel::Sender, 85 | ) -> Result<(), Error> { 86 | let mut connect = true; 87 | let mut disconnect = false; 88 | 89 | let mut received_data = Vec::with_capacity(2 * api::CHUNK_LENGTH); 90 | let mut buf = [0u8; api::CHUNK_LENGTH]; 91 | 92 | loop { 93 | if connect { 94 | common::debug!("open static channel {:?}", common::VIRTUAL_CHANNEL_NAME); 95 | match svc.open(common::VIRTUAL_CHANNEL_NAME) { 96 | Err(e) => { 97 | common::error!("failed to open channel handle: {e}"); 98 | thread::sleep(time::Duration::from_secs(1)); 99 | continue; 100 | } 101 | Ok(svc_handle) => { 102 | common::info!("static channel {:?} opened", common::VIRTUAL_CHANNEL_NAME); 103 | channel.write().unwrap().replace(svc_handle); 104 | connect = false; 105 | } 106 | } 107 | } 108 | 109 | match channel.read().unwrap().as_ref() { 110 | None => { 111 | common::debug!("cannot read on disconnected channel"); 112 | connect = true; 113 | } 114 | Some(svc) => match svc.read(&mut buf) { 115 | Err(e) => { 116 | common::error!("failed to read from channel: {e}"); 117 | disconnect = true; 118 | } 119 | Ok(mut read) => { 120 | common::trace!("read {read} bytes"); 121 | 122 | if received_data.is_empty() { 123 | let mut off = 0; 124 | loop { 125 | match api::Chunk::can_deserialize_from(&buf[off..off + read]) { 126 | None => { 127 | received_data.extend_from_slice(&buf[off..off + read]); 128 | break; 129 | } 130 | Some(len) => { 131 | match api::Chunk::deserialize_from(&buf[off..off + len]) { 132 | Err(e) => { 133 | common::error!("failed to deserialize chunk: {e}"); 134 | disconnect = true; 135 | } 136 | Ok(chunk) => { 137 | common::trace!("{chunk}"); 138 | to_backend.send(api::ChunkControl::Chunk(chunk))?; 139 | } 140 | } 141 | off += len; 142 | read -= len; 143 | if read == 0 { 144 | break; 145 | } 146 | } 147 | } 148 | } 149 | } else { 150 | received_data.extend_from_slice(&buf[0..read]); 151 | loop { 152 | match api::Chunk::can_deserialize_from(&received_data) { 153 | None => break, 154 | Some(len) => { 155 | // tmp contains the tail, i.e. what will 156 | // not be deserialized 157 | let mut tmp = received_data.split_off(len); 158 | // tmp contains data to deserialize, 159 | // remaining data are back in 160 | // self.svc_received_data 161 | mem::swap(&mut tmp, &mut received_data); 162 | 163 | match api::Chunk::deserialize(tmp) { 164 | Err(e) => { 165 | common::error!("failed to deserialize chunk: {e}"); 166 | disconnect = true; 167 | } 168 | Ok(chunk) => { 169 | common::trace!("{chunk}"); 170 | to_backend.send(api::ChunkControl::Chunk(chunk))?; 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | }, 179 | } 180 | 181 | if disconnect { 182 | common::info!("disconnecting from channel"); 183 | received_data.clear(); 184 | channel.write().unwrap().take(); 185 | to_backend.send(api::ChunkControl::Shutdown)?; 186 | disconnect = false; 187 | } 188 | } 189 | } 190 | 191 | #[allow(clippy::too_many_lines)] 192 | fn main_res() -> Result<(), Error> { 193 | #[cfg(target_os = "windows")] 194 | { 195 | common::debug!("calling WSAStartup"); 196 | 197 | let mut data = ws::Win32::Networking::WinSock::WSADATA { 198 | wVersion: 0, 199 | wHighVersion: 0, 200 | iMaxSockets: 0, 201 | iMaxUdpDg: 0, 202 | lpVendorInfo: std::ptr::null_mut(), 203 | szDescription: [0i8; 257], 204 | szSystemStatus: [0i8; 129], 205 | }; 206 | 207 | let ret = unsafe { ws::Win32::Networking::WinSock::WSAStartup(0x0202, &mut data) }; 208 | if ret != 0 { 209 | return Err(Error::Svc(svc::Error::WsaStartupFailed(ret))); 210 | } 211 | } 212 | 213 | let lib = svc::Implementation::load()?; 214 | let svc = svc::Svc::load(&lib)?; 215 | 216 | let (backend_to_frontend_send, backend_to_frontend_receive) = 217 | crossbeam_channel::bounded(TO_SVC_CHANNEL_SIZE); 218 | let (frontend_to_backend_send, frontend_to_backend_receive) = crossbeam_channel::unbounded(); 219 | 220 | let backend_channel = service::Channel::new(backend_to_frontend_send); 221 | 222 | thread::Builder::new() 223 | .name("backend".into()) 224 | .spawn(move || { 225 | if let Err(e) = 226 | backend_channel.start(service::Kind::Backend, &frontend_to_backend_receive) 227 | { 228 | common::error!("error: {e}"); 229 | } else { 230 | common::debug!("stopped"); 231 | } 232 | }) 233 | .unwrap(); 234 | 235 | let channel = sync::RwLock::new(None); 236 | 237 | thread::scope(|scope| { 238 | thread::Builder::new() 239 | .name("backend to frontend".into()) 240 | .spawn_scoped(scope, || { 241 | if let Err(e) = backend_to_frontend(&channel, &backend_to_frontend_receive) { 242 | common::error!("error: {e}"); 243 | } else { 244 | common::warn!("stopped"); 245 | } 246 | }) 247 | .unwrap(); 248 | 249 | if let Err(e) = frontend_to_backend(&svc, &channel, &frontend_to_backend_send) { 250 | common::error!("error: {e}"); 251 | Err(e) 252 | } else { 253 | common::warn!("stopped"); 254 | Ok(()) 255 | } 256 | }) 257 | } 258 | 259 | pub fn main(level: common::Level) { 260 | common::init_logs(level, None); 261 | 262 | common::debug!("starting up"); 263 | 264 | if let Err(e) = main_res() { 265 | common::error!("{e}"); 266 | } 267 | } 268 | 269 | // The Main in only there to maintain the library loaded while loaded 270 | // through rundll32.exe, which executes at loading time the DllMain 271 | // function below. The DllMain function is called by the loader and 272 | // must return ASAP to unlock the loading process. That is why we 273 | // create a thread in it. 274 | 275 | // rundll32.exe soxy.dll,Main 276 | 277 | #[no_mangle] 278 | #[allow(non_snake_case, unused_variables)] 279 | extern "system" fn Main() { 280 | loop { 281 | thread::sleep(time::Duration::from_secs(60)); 282 | } 283 | } 284 | 285 | #[no_mangle] 286 | #[allow(non_snake_case, unused_variables, clippy::missing_safety_doc)] 287 | pub unsafe extern "system" fn DllMain( 288 | dll_module: ws::Win32::Foundation::HINSTANCE, 289 | call_reason: u32, 290 | _reserverd: *mut ffi::c_void, 291 | ) -> ws::Win32::Foundation::BOOL { 292 | match call_reason { 293 | ws::Win32::System::SystemServices::DLL_PROCESS_ATTACH => unsafe { 294 | ws::Win32::System::LibraryLoader::DisableThreadLibraryCalls(dll_module); 295 | ws::Win32::System::Console::AllocConsole(); 296 | thread::spawn(|| { 297 | #[cfg(debug_assertions)] 298 | main(common::Level::Debug); 299 | #[cfg(not(debug_assertions))] 300 | main(common::Level::Info); 301 | }); 302 | }, 303 | ws::Win32::System::SystemServices::DLL_PROCESS_DETACH => {} 304 | _ => (), 305 | } 306 | 307 | ws::Win32::Foundation::TRUE 308 | } 309 | -------------------------------------------------------------------------------- /backend/src/svc/high.rs: -------------------------------------------------------------------------------- 1 | use std::{io, os, ptr, thread}; 2 | use windows_sys as ws; 3 | 4 | pub struct Svc<'a> { 5 | open: libloading::Symbol<'a, super::VirtualChannelOpen>, 6 | query: libloading::Symbol<'a, super::VirtualChannelQuery>, 7 | read: libloading::Symbol<'a, super::VirtualChannelRead>, 8 | write: libloading::Symbol<'a, super::VirtualChannelWrite>, 9 | } 10 | 11 | impl<'a> Svc<'a> { 12 | pub(crate) fn load( 13 | lib: &'a libloading::Library, 14 | symbols: &super::SymbolNames, 15 | ) -> Result { 16 | unsafe { 17 | Ok(Self { 18 | open: lib.get(symbols.open.as_bytes())?, 19 | query: lib.get(symbols.query.as_bytes())?, 20 | read: lib.get(symbols.read.as_bytes())?, 21 | write: lib.get(symbols.write.as_bytes())?, 22 | }) 23 | } 24 | } 25 | 26 | pub(crate) fn open(&self, mut name: [i8; 8]) -> Result { 27 | let wtshandle = unsafe { 28 | (self.open)( 29 | ws::Win32::System::RemoteDesktop::WTS_CURRENT_SERVER_HANDLE, 30 | ws::Win32::System::RemoteDesktop::WTS_CURRENT_SESSION, 31 | name.as_mut_ptr(), 32 | ) 33 | }; 34 | 35 | if wtshandle.is_null() { 36 | let err = io::Error::last_os_error(); 37 | return Err(super::Error::VirtualChannelOpenStaticChannelFailed(err)); 38 | } 39 | 40 | let mut client_dataptr = ptr::null_mut(); 41 | let mut len = 0; 42 | 43 | let ret = unsafe { 44 | (self.query)( 45 | wtshandle, 46 | ws::Win32::System::RemoteDesktop::WTSVirtualClientData, 47 | &mut client_dataptr, 48 | &mut len, 49 | ) 50 | }; 51 | 52 | if ret == ws::Win32::Foundation::FALSE { 53 | let err = io::Error::last_os_error(); 54 | common::warn!("virtual channel query failed (len = {len}, last error = {err})"); 55 | } 56 | 57 | Ok(Handle { 58 | read: self.read.clone(), 59 | write: self.write.clone(), 60 | wtshandle, 61 | }) 62 | } 63 | } 64 | 65 | pub struct Handle<'a> { 66 | read: libloading::Symbol<'a, super::VirtualChannelRead>, 67 | write: libloading::Symbol<'a, super::VirtualChannelWrite>, 68 | wtshandle: ws::Win32::Foundation::HANDLE, 69 | } 70 | 71 | // Because of the *mut content (handle) Rust does not derive Send and 72 | // Sync. Since we know how those data will be used (especially in 73 | // terms of concurrency) we assume to unsafely implement Send and 74 | // Sync. 75 | unsafe impl Send for Handle<'_> {} 76 | unsafe impl Sync for Handle<'_> {} 77 | 78 | impl super::Handler for Handle<'_> { 79 | fn read(&self, data: &mut [u8]) -> Result { 80 | let to_read = os::raw::c_ulong::try_from(data.len()) 81 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 82 | 83 | let timeout = os::raw::c_ulong::MAX; 84 | 85 | let mut read = 0; 86 | 87 | let ret = unsafe { 88 | (self.read)( 89 | self.wtshandle, 90 | timeout, 91 | data.as_mut_ptr(), 92 | to_read, 93 | &mut read, 94 | ) 95 | }; 96 | 97 | if ret == 0 { 98 | let err = io::Error::last_os_error(); 99 | Err(super::Error::VirtualChannelReadFailed(err)) 100 | } else { 101 | Ok(usize::try_from(read) 102 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?) 103 | } 104 | } 105 | 106 | fn write(&self, data: &[u8]) -> Result { 107 | let to_write = os::raw::c_ulong::try_from(data.len()) 108 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 109 | 110 | let mut written = 0; 111 | 112 | common::trace!("write {to_write} bytes"); 113 | 114 | loop { 115 | let ret = 116 | unsafe { (self.write)(self.wtshandle, data.as_ptr(), to_write, &mut written) }; 117 | 118 | if ret == 0 || written != to_write { 119 | if written == 0 { 120 | common::trace!("send buffer seems full, yield now"); 121 | thread::yield_now(); 122 | continue; 123 | } 124 | if written != to_write { 125 | common::error!("partial write: {written} / {to_write}"); 126 | } 127 | let err = io::Error::last_os_error(); 128 | return Err(super::Error::VirtualChannelWriteFailed(err)); 129 | } 130 | 131 | return Ok(usize::try_from(written) 132 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /backend/src/svc/low.rs: -------------------------------------------------------------------------------- 1 | use std::{cell, io, os, ptr}; 2 | use windows_sys as ws; 3 | 4 | pub struct Svc<'a> { 5 | open: libloading::Symbol<'a, super::VirtualChannelOpen>, 6 | query: libloading::Symbol<'a, super::VirtualChannelQuery>, 7 | } 8 | 9 | impl<'a> Svc<'a> { 10 | pub(crate) fn load( 11 | lib: &'a libloading::Library, 12 | symbols: &super::SymbolNames, 13 | ) -> Result { 14 | unsafe { 15 | Ok(Self { 16 | open: lib.get(symbols.open.as_bytes())?, 17 | query: lib.get(symbols.query.as_bytes())?, 18 | }) 19 | } 20 | } 21 | 22 | pub(crate) fn open(&self, mut name: [i8; 8]) -> Result { 23 | let wtshandle = unsafe { 24 | (self.open)( 25 | ws::Win32::System::RemoteDesktop::WTS_CURRENT_SERVER_HANDLE, 26 | ws::Win32::System::RemoteDesktop::WTS_CURRENT_SESSION, 27 | name.as_mut_ptr(), 28 | ) 29 | }; 30 | 31 | if wtshandle.is_null() { 32 | let err = io::Error::last_os_error(); 33 | return Err(super::Error::VirtualChannelOpenStaticChannelFailed(err)); 34 | } 35 | 36 | let mut filehandleptr: *mut ws::Win32::Foundation::HANDLE = ptr::null_mut(); 37 | let filehandleptrptr: *mut *mut ws::Win32::Foundation::HANDLE = &mut filehandleptr; 38 | let mut len = 0; 39 | 40 | common::trace!("VirtualChannelQuery"); 41 | let ret = unsafe { 42 | (self.query)( 43 | wtshandle, 44 | ws::Win32::System::RemoteDesktop::WTSVirtualFileHandle, 45 | filehandleptrptr.cast(), 46 | &mut len, 47 | ) 48 | }; 49 | if ret == ws::Win32::Foundation::FALSE { 50 | let err = io::Error::last_os_error(); 51 | return Err(super::Error::VirtualChannelQueryFailed(err)); 52 | } 53 | if filehandleptr.is_null() { 54 | let err = io::Error::last_os_error(); 55 | return Err(super::Error::VirtualChannelQueryFailed(err)); 56 | } 57 | 58 | let filehandle = unsafe { filehandleptr.read() }; 59 | 60 | common::trace!("filehandle = {filehandle:?}"); 61 | 62 | let mut dfilehandle: ws::Win32::Foundation::HANDLE = ptr::null_mut(); 63 | 64 | common::trace!("DuplicateHandle"); 65 | let ret = unsafe { 66 | ws::Win32::Foundation::DuplicateHandle( 67 | ws::Win32::System::Threading::GetCurrentProcess(), 68 | filehandle, 69 | ws::Win32::System::Threading::GetCurrentProcess(), 70 | &mut dfilehandle, 71 | 0, 72 | ws::Win32::Foundation::FALSE, 73 | ws::Win32::Foundation::DUPLICATE_SAME_ACCESS, 74 | ) 75 | }; 76 | if ret == ws::Win32::Foundation::FALSE { 77 | let err = io::Error::last_os_error(); 78 | return Err(super::Error::DuplicateHandleFailed(err)); 79 | } 80 | common::trace!("duplicated filehandle = {dfilehandle:?}"); 81 | 82 | let h_event = unsafe { 83 | ws::Win32::System::Threading::CreateEventA( 84 | ptr::null(), 85 | ws::Win32::Foundation::FALSE, 86 | ws::Win32::Foundation::FALSE, 87 | ptr::null(), 88 | ) 89 | }; 90 | 91 | if h_event.is_null() { 92 | let err = io::Error::last_os_error(); 93 | return Err(super::Error::CreateEventFailed(err)); 94 | } 95 | 96 | let anonymous = ws::Win32::System::IO::OVERLAPPED_0 { 97 | Pointer: ptr::null_mut(), 98 | }; 99 | 100 | let read_overlapped = ws::Win32::System::IO::OVERLAPPED { 101 | Internal: 0, 102 | InternalHigh: 0, 103 | Anonymous: anonymous, 104 | hEvent: h_event, 105 | }; 106 | 107 | let write_overlapped = ws::Win32::System::IO::OVERLAPPED { 108 | Internal: 0, 109 | InternalHigh: 0, 110 | Anonymous: anonymous, 111 | hEvent: ptr::null_mut(), 112 | }; 113 | 114 | let read_overlapped = cell::RefCell::new(read_overlapped); 115 | let write_overlapped = cell::RefCell::new(write_overlapped); 116 | 117 | Ok(Handle { 118 | filehandle: dfilehandle, 119 | read_overlapped, 120 | write_overlapped, 121 | }) 122 | } 123 | } 124 | 125 | pub struct Handle { 126 | filehandle: ws::Win32::Foundation::HANDLE, 127 | read_overlapped: cell::RefCell, 128 | write_overlapped: cell::RefCell, 129 | } 130 | 131 | impl super::Handler for Handle { 132 | fn read(&self, data: &mut [u8]) -> Result { 133 | let to_read = os::raw::c_uint::try_from(data.len()) 134 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 135 | 136 | let mut read = 0; 137 | 138 | let mut overlapped = self.read_overlapped.borrow_mut(); 139 | 140 | let ret = unsafe { 141 | ws::Win32::Storage::FileSystem::ReadFile( 142 | self.filehandle, 143 | data.as_mut_ptr(), 144 | to_read, 145 | &mut read, 146 | &mut *overlapped, 147 | ) 148 | }; 149 | 150 | if ret == 0 { 151 | let err = unsafe { ws::Win32::Foundation::GetLastError() }; 152 | if err == ws::Win32::Foundation::ERROR_IO_PENDING { 153 | let mut read = 0; 154 | let ret = unsafe { 155 | ws::Win32::System::IO::GetOverlappedResult( 156 | self.filehandle, 157 | &*overlapped, 158 | &mut read, 159 | ws::Win32::Foundation::TRUE, 160 | ) 161 | }; 162 | if ret == ws::Win32::Foundation::FALSE { 163 | let err = io::Error::last_os_error(); 164 | Err(super::Error::VirtualChannelReadFailed(err)) 165 | } else { 166 | Ok(read as usize) 167 | } 168 | } else { 169 | let err = io::Error::last_os_error(); 170 | Err(super::Error::VirtualChannelReadFailed(err)) 171 | } 172 | } else { 173 | Ok(read as usize) 174 | } 175 | } 176 | 177 | fn write(&self, data: &[u8]) -> Result { 178 | let to_write = os::raw::c_uint::try_from(data.len()) 179 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 180 | 181 | let mut written = 0; 182 | 183 | let mut overlapped = self.write_overlapped.borrow_mut(); 184 | 185 | common::trace!("write {to_write} bytes"); 186 | 187 | let ret = unsafe { 188 | ws::Win32::Storage::FileSystem::WriteFile( 189 | self.filehandle, 190 | data.as_ptr(), 191 | to_write, 192 | &mut written, 193 | &mut *overlapped, 194 | ) 195 | }; 196 | 197 | if ret == ws::Win32::Foundation::FALSE { 198 | let err = unsafe { ws::Win32::Foundation::GetLastError() }; 199 | if err == ws::Win32::Foundation::ERROR_IO_PENDING { 200 | let mut written = 0; 201 | let ret = unsafe { 202 | ws::Win32::System::IO::GetOverlappedResult( 203 | self.filehandle, 204 | &*overlapped, 205 | &mut written, 206 | ws::Win32::Foundation::TRUE, 207 | ) 208 | }; 209 | if ret == ws::Win32::Foundation::FALSE { 210 | let err = io::Error::last_os_error(); 211 | Err(super::Error::VirtualChannelReadFailed(err)) 212 | } else { 213 | Ok(written as usize) 214 | } 215 | } else { 216 | let err = io::Error::last_os_error(); 217 | Err(super::Error::VirtualChannelWriteFailed(err)) 218 | } 219 | } else { 220 | Ok(written as usize) 221 | } 222 | } 223 | } 224 | 225 | // Because of the *mut content (handle but also in OVERLAPPED 226 | // structure) Rust does not derive Send and Sync. Since we know how 227 | // those data will be used (especially in terms of concurrency) we 228 | // assume to unsafely implement Send and Sync. 229 | unsafe impl Send for Handle {} 230 | unsafe impl Sync for Handle {} 231 | -------------------------------------------------------------------------------- /backend/src/svc/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi, fmt, io, os}; 2 | use windows_sys as ws; 3 | 4 | mod high; 5 | #[cfg(target_os = "windows")] 6 | mod low; 7 | 8 | pub enum Error { 9 | LibraryNotFound, 10 | LibraryLoading(libloading::Error), 11 | #[cfg(target_os = "windows")] 12 | WsaStartupFailed(i32), 13 | Io(io::Error), 14 | VirtualChannelOpenStaticChannelFailed(io::Error), 15 | VirtualChannelReadFailed(io::Error), 16 | VirtualChannelWriteFailed(io::Error), 17 | #[cfg(target_os = "windows")] 18 | VirtualChannelQueryFailed(io::Error), 19 | #[cfg(target_os = "windows")] 20 | DuplicateHandleFailed(io::Error), 21 | #[cfg(target_os = "windows")] 22 | CreateEventFailed(io::Error), 23 | InvalidChannelName, 24 | } 25 | 26 | impl fmt::Display for Error { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 28 | match self { 29 | Self::LibraryNotFound => write!(f, "library not found"), 30 | Self::LibraryLoading(e) => write!(f, "library loading error: {e}"), 31 | #[cfg(target_os = "windows")] 32 | Self::WsaStartupFailed(e) => write!(f, "WSAStartup failed with error code {e}"), 33 | Self::Io(e) => write!(f, "I/O error: {e}"), 34 | Self::VirtualChannelOpenStaticChannelFailed(err) => { 35 | write!(f, "virtual channel open failed (last_error = {err})") 36 | } 37 | Self::VirtualChannelReadFailed(err) => { 38 | write!(f, "virtual channel read failed (last error = {err})") 39 | } 40 | Self::VirtualChannelWriteFailed(err) => { 41 | write!(f, "virtual channel write failed (last error = {err})") 42 | } 43 | #[cfg(target_os = "windows")] 44 | Self::VirtualChannelQueryFailed(err) => { 45 | write!(f, "virtual channel query failed (last error = {err})") 46 | } 47 | #[cfg(target_os = "windows")] 48 | Self::DuplicateHandleFailed(err) => { 49 | write!(f, "duplicate handle failed (last error = {err})") 50 | } 51 | #[cfg(target_os = "windows")] 52 | Self::CreateEventFailed(err) => { 53 | write!(f, "create event failed (last error = {err})") 54 | } 55 | Self::InvalidChannelName => { 56 | write!(f, "invalid channel name") 57 | } 58 | } 59 | } 60 | } 61 | 62 | impl From for Error { 63 | fn from(e: libloading::Error) -> Self { 64 | Self::LibraryLoading(e) 65 | } 66 | } 67 | 68 | impl From for Error { 69 | fn from(e: io::Error) -> Self { 70 | Self::Io(e) 71 | } 72 | } 73 | 74 | #[derive(Clone, Copy, Debug)] 75 | enum Instance { 76 | Citrix, 77 | Horizon, 78 | Xrdp, 79 | #[cfg(target_os = "windows")] 80 | Windows, 81 | } 82 | 83 | pub struct SymbolNames { 84 | open: &'static str, 85 | read: &'static str, 86 | write: &'static str, 87 | query: &'static str, 88 | } 89 | 90 | impl From for SymbolNames { 91 | fn from(instance: Instance) -> Self { 92 | match instance { 93 | Instance::Citrix => Self { 94 | open: "WFVirtualChannelOpen", 95 | read: "WFVirtualChannelRead", 96 | write: "WFVirtualChannelWrite", 97 | query: "WFVirtualChannelQuery", 98 | }, 99 | Instance::Horizon => Self { 100 | open: "VDP_VirtualChannelOpen", 101 | read: "VDP_VirtualChannelRead", 102 | write: "VDP_VirtualChannelWrite", 103 | query: "VDP_VirtualChannelQuery", 104 | }, 105 | Instance::Xrdp => Self { 106 | open: "WTSVirtualChannelOpen", 107 | read: "WTSVirtualChannelRead", 108 | write: "WTSVirtualChannelWrite", 109 | query: "WTSVirtualChannelQuery", 110 | }, 111 | #[cfg(target_os = "windows")] 112 | Instance::Windows => Self { 113 | open: "WTSVirtualChannelOpen", 114 | read: "WTSVirtualChannelRead", 115 | write: "WTSVirtualChannelWrite", 116 | query: "WTSVirtualChannelQuery", 117 | }, 118 | } 119 | } 120 | } 121 | 122 | pub struct Implementation { 123 | instance: Instance, 124 | lib: libloading::Library, 125 | } 126 | 127 | impl Implementation { 128 | pub(crate) fn load() -> Result { 129 | unsafe { 130 | let file = libloading::library_filename("wfapi64"); 131 | common::debug!( 132 | "trying to load Citrix library from {}", 133 | file.to_string_lossy() 134 | ); 135 | if let Ok(lib) = libloading::Library::new(file) { 136 | common::info!("Citrix library loaded"); 137 | return Ok(Self { 138 | instance: Instance::Citrix, 139 | lib, 140 | }); 141 | } 142 | 143 | let file = libloading::library_filename("vdp_rdpvcbridge"); 144 | common::debug!( 145 | "trying to load Horizon library from {}", 146 | file.to_string_lossy() 147 | ); 148 | if let Ok(lib) = libloading::Library::new(file) { 149 | common::info!("Horizon library loaded"); 150 | return Ok(Self { 151 | instance: Instance::Horizon, 152 | lib, 153 | }); 154 | } 155 | 156 | #[cfg(target_os = "windows")] 157 | { 158 | let file = libloading::library_filename("wtsapi32"); 159 | common::debug!("trying to load WTS library from {}", file.to_string_lossy()); 160 | if let Ok(lib) = libloading::Library::new(file) { 161 | common::info!("WTS library loaded"); 162 | return Ok(Self { 163 | instance: Instance::Windows, 164 | lib, 165 | }); 166 | } 167 | } 168 | 169 | let file = libloading::library_filename("xrdpapi"); 170 | common::debug!( 171 | "trying to load XRDP library from {}", 172 | file.to_string_lossy() 173 | ); 174 | if let Ok(lib) = libloading::Library::new(file) { 175 | common::info!("XRDP library loaded"); 176 | return Ok(Self { 177 | instance: Instance::Xrdp, 178 | lib, 179 | }); 180 | } 181 | 182 | Err(Error::LibraryNotFound) 183 | } 184 | } 185 | } 186 | 187 | type VirtualChannelOpen = unsafe extern "system" fn( 188 | hserver: ws::Win32::Foundation::HANDLE, 189 | sessionid: os::raw::c_uint, 190 | pvirtualname: *mut os::raw::c_char, 191 | ) -> ws::Win32::Foundation::HANDLE; 192 | 193 | type VirtualChannelRead = unsafe extern "system" fn( 194 | hchannelhandle: ws::Win32::Foundation::HANDLE, 195 | timeout: os::raw::c_ulong, 196 | buffer: *mut os::raw::c_uchar, 197 | buffersize: os::raw::c_ulong, 198 | pbytesread: *mut os::raw::c_ulong, 199 | ) -> os::raw::c_int; 200 | 201 | type VirtualChannelWrite = unsafe extern "system" fn( 202 | hchannelhandle: ws::Win32::Foundation::HANDLE, 203 | buffer: *const os::raw::c_uchar, 204 | length: os::raw::c_ulong, 205 | pbyteswritten: *mut os::raw::c_ulong, 206 | ) -> os::raw::c_uint; 207 | 208 | type VirtualChannelQuery = unsafe extern "system" fn( 209 | hchannelhandle: ws::Win32::Foundation::HANDLE, 210 | wtsvirtualclass: ws::Win32::System::RemoteDesktop::WTS_VIRTUAL_CLASS, 211 | ppbuffer: *mut *mut os::raw::c_void, 212 | pbytesreturned: *mut os::raw::c_ulong, 213 | ) -> ws::Win32::Foundation::BOOL; 214 | 215 | pub enum Svc<'a> { 216 | High { 217 | svc: high::Svc<'a>, 218 | }, 219 | #[cfg(target_os = "windows")] 220 | Low { 221 | svc: low::Svc<'a>, 222 | }, 223 | } 224 | 225 | impl<'a> Svc<'a> { 226 | pub(crate) fn load(implem: &'a Implementation) -> Result { 227 | let symbol_names = SymbolNames::from(implem.instance); 228 | match implem.instance { 229 | Instance::Citrix => { 230 | let svc = high::Svc::load(&implem.lib, &symbol_names)?; 231 | Ok(Self::High { svc }) 232 | } 233 | Instance::Horizon => { 234 | let svc = high::Svc::load(&implem.lib, &symbol_names)?; 235 | Ok(Self::High { svc }) 236 | } 237 | Instance::Xrdp => { 238 | #[cfg(feature = "log")] 239 | { 240 | common::debug!("initiate XRDP logging"); 241 | 242 | let log_init = unsafe { 243 | implem.lib.get:: *mut os::raw::c_void>( 247 | "log_config_init_for_console".as_bytes() 248 | )? 249 | }; 250 | let log_start = unsafe { 251 | implem 252 | .lib 253 | .get::("log_start_from_param".as_bytes())? 254 | }; 255 | 256 | let lc = log_init(4, std::ptr::null_mut()); 257 | 258 | if !lc.is_null() { 259 | log_start(lc); 260 | } 261 | } 262 | 263 | let svc = high::Svc::load(&implem.lib, &symbol_names)?; 264 | Ok(Self::High { svc }) 265 | } 266 | #[cfg(target_os = "windows")] 267 | Instance::Windows => { 268 | let svc = low::Svc::load(&implem.lib, &symbol_names)?; 269 | Ok(Self::Low { svc }) 270 | } 271 | } 272 | } 273 | 274 | pub(crate) fn open(&'a self, name: &ffi::CStr) -> Result, Error> { 275 | let mut cname: [ffi::c_char; 8] = [0; 8]; 276 | for (i, b) in name.to_bytes_with_nul().iter().enumerate() { 277 | cname[i] = i8::try_from(*b).map_err(|_| Error::InvalidChannelName)?; 278 | } 279 | 280 | match self { 281 | Self::High { svc } => Ok(Handle::from(svc.open(cname)?)), 282 | #[cfg(target_os = "windows")] 283 | Self::Low { svc } => Ok(Handle::from(svc.open(cname)?)), 284 | } 285 | } 286 | } 287 | 288 | pub enum Handle<'a> { 289 | High { 290 | handle: high::Handle<'a>, 291 | }, 292 | #[cfg(target_os = "windows")] 293 | Low { 294 | handle: low::Handle, 295 | }, 296 | } 297 | 298 | impl<'a> From> for Handle<'a> { 299 | fn from(handle: high::Handle<'a>) -> Self { 300 | Self::High { handle } 301 | } 302 | } 303 | 304 | #[cfg(target_os = "windows")] 305 | impl From for Handle<'_> { 306 | fn from(handle: low::Handle) -> Self { 307 | Self::Low { handle } 308 | } 309 | } 310 | 311 | impl Handler for Handle<'_> { 312 | fn read(&self, data: &mut [u8]) -> Result { 313 | match self { 314 | Self::High { handle } => handle.read(data), 315 | #[cfg(target_os = "windows")] 316 | Self::Low { handle } => handle.read(data), 317 | } 318 | } 319 | 320 | fn write(&self, data: &[u8]) -> Result { 321 | match self { 322 | Self::High { handle } => handle.write(data), 323 | #[cfg(target_os = "windows")] 324 | Self::Low { handle } => handle.write(data), 325 | } 326 | } 327 | } 328 | 329 | pub trait Handler { 330 | fn read(&self, data: &mut [u8]) -> Result; 331 | fn write(&self, data: &[u8]) -> Result; 332 | } 333 | -------------------------------------------------------------------------------- /big_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbus-seclab/soxy/77151617c290f44a533c974594db09c37a3ca556/big_picture.png -------------------------------------------------------------------------------- /common/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /common/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.9.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 10 | 11 | [[package]] 12 | name = "cc" 13 | version = "1.2.24" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" 16 | dependencies = [ 17 | "shlex", 18 | ] 19 | 20 | [[package]] 21 | name = "clipboard-win" 22 | version = "3.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" 25 | dependencies = [ 26 | "lazy-bytes-cast", 27 | "winapi", 28 | ] 29 | 30 | [[package]] 31 | name = "common" 32 | version = "2.2.1" 33 | dependencies = [ 34 | "copyrs", 35 | "crossbeam-channel", 36 | "log", 37 | "network-interface", 38 | "simplelog", 39 | ] 40 | 41 | [[package]] 42 | name = "copyrs" 43 | version = "0.5.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "acd5edd7860b40dd97afc096b00a04d118e25be6207cd9ca09052022a861f800" 46 | dependencies = [ 47 | "clipboard-win", 48 | "x11-clipboard", 49 | ] 50 | 51 | [[package]] 52 | name = "crossbeam-channel" 53 | version = "0.5.15" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 56 | dependencies = [ 57 | "crossbeam-utils", 58 | ] 59 | 60 | [[package]] 61 | name = "crossbeam-utils" 62 | version = "0.8.21" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 65 | 66 | [[package]] 67 | name = "deranged" 68 | version = "0.4.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 71 | dependencies = [ 72 | "powerfmt", 73 | ] 74 | 75 | [[package]] 76 | name = "errno" 77 | version = "0.3.12" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 80 | dependencies = [ 81 | "libc", 82 | "windows-sys", 83 | ] 84 | 85 | [[package]] 86 | name = "gethostname" 87 | version = "0.4.3" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" 90 | dependencies = [ 91 | "libc", 92 | "windows-targets 0.48.5", 93 | ] 94 | 95 | [[package]] 96 | name = "itoa" 97 | version = "1.0.15" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 100 | 101 | [[package]] 102 | name = "lazy-bytes-cast" 103 | version = "5.0.1" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" 106 | 107 | [[package]] 108 | name = "libc" 109 | version = "0.2.172" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 112 | 113 | [[package]] 114 | name = "linux-raw-sys" 115 | version = "0.4.15" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 118 | 119 | [[package]] 120 | name = "log" 121 | version = "0.4.27" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 124 | 125 | [[package]] 126 | name = "network-interface" 127 | version = "2.0.1" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "c3329f515506e4a2de3aa6e07027a6758e22e0f0e8eaf64fa47261cec2282602" 130 | dependencies = [ 131 | "cc", 132 | "libc", 133 | "thiserror", 134 | "winapi", 135 | ] 136 | 137 | [[package]] 138 | name = "num-conv" 139 | version = "0.1.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 142 | 143 | [[package]] 144 | name = "num_threads" 145 | version = "0.1.7" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 148 | dependencies = [ 149 | "libc", 150 | ] 151 | 152 | [[package]] 153 | name = "powerfmt" 154 | version = "0.2.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 157 | 158 | [[package]] 159 | name = "proc-macro2" 160 | version = "1.0.95" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 163 | dependencies = [ 164 | "unicode-ident", 165 | ] 166 | 167 | [[package]] 168 | name = "quote" 169 | version = "1.0.40" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 172 | dependencies = [ 173 | "proc-macro2", 174 | ] 175 | 176 | [[package]] 177 | name = "rustix" 178 | version = "0.38.44" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 181 | dependencies = [ 182 | "bitflags", 183 | "errno", 184 | "libc", 185 | "linux-raw-sys", 186 | "windows-sys", 187 | ] 188 | 189 | [[package]] 190 | name = "serde" 191 | version = "1.0.219" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 194 | dependencies = [ 195 | "serde_derive", 196 | ] 197 | 198 | [[package]] 199 | name = "serde_derive" 200 | version = "1.0.219" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 203 | dependencies = [ 204 | "proc-macro2", 205 | "quote", 206 | "syn", 207 | ] 208 | 209 | [[package]] 210 | name = "shlex" 211 | version = "1.3.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 214 | 215 | [[package]] 216 | name = "simplelog" 217 | version = "0.12.2" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" 220 | dependencies = [ 221 | "log", 222 | "termcolor", 223 | "time", 224 | ] 225 | 226 | [[package]] 227 | name = "syn" 228 | version = "2.0.101" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 231 | dependencies = [ 232 | "proc-macro2", 233 | "quote", 234 | "unicode-ident", 235 | ] 236 | 237 | [[package]] 238 | name = "termcolor" 239 | version = "1.4.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 242 | dependencies = [ 243 | "winapi-util", 244 | ] 245 | 246 | [[package]] 247 | name = "thiserror" 248 | version = "1.0.69" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 251 | dependencies = [ 252 | "thiserror-impl", 253 | ] 254 | 255 | [[package]] 256 | name = "thiserror-impl" 257 | version = "1.0.69" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 260 | dependencies = [ 261 | "proc-macro2", 262 | "quote", 263 | "syn", 264 | ] 265 | 266 | [[package]] 267 | name = "time" 268 | version = "0.3.41" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 271 | dependencies = [ 272 | "deranged", 273 | "itoa", 274 | "libc", 275 | "num-conv", 276 | "num_threads", 277 | "powerfmt", 278 | "serde", 279 | "time-core", 280 | "time-macros", 281 | ] 282 | 283 | [[package]] 284 | name = "time-core" 285 | version = "0.1.4" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 288 | 289 | [[package]] 290 | name = "time-macros" 291 | version = "0.2.22" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 294 | dependencies = [ 295 | "num-conv", 296 | "time-core", 297 | ] 298 | 299 | [[package]] 300 | name = "unicode-ident" 301 | version = "1.0.18" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 304 | 305 | [[package]] 306 | name = "winapi" 307 | version = "0.3.9" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 310 | dependencies = [ 311 | "winapi-i686-pc-windows-gnu", 312 | "winapi-x86_64-pc-windows-gnu", 313 | ] 314 | 315 | [[package]] 316 | name = "winapi-i686-pc-windows-gnu" 317 | version = "0.4.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 320 | 321 | [[package]] 322 | name = "winapi-util" 323 | version = "0.1.9" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 326 | dependencies = [ 327 | "windows-sys", 328 | ] 329 | 330 | [[package]] 331 | name = "winapi-x86_64-pc-windows-gnu" 332 | version = "0.4.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 335 | 336 | [[package]] 337 | name = "windows-sys" 338 | version = "0.59.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 341 | dependencies = [ 342 | "windows-targets 0.52.6", 343 | ] 344 | 345 | [[package]] 346 | name = "windows-targets" 347 | version = "0.48.5" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 350 | dependencies = [ 351 | "windows_aarch64_gnullvm 0.48.5", 352 | "windows_aarch64_msvc 0.48.5", 353 | "windows_i686_gnu 0.48.5", 354 | "windows_i686_msvc 0.48.5", 355 | "windows_x86_64_gnu 0.48.5", 356 | "windows_x86_64_gnullvm 0.48.5", 357 | "windows_x86_64_msvc 0.48.5", 358 | ] 359 | 360 | [[package]] 361 | name = "windows-targets" 362 | version = "0.52.6" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 365 | dependencies = [ 366 | "windows_aarch64_gnullvm 0.52.6", 367 | "windows_aarch64_msvc 0.52.6", 368 | "windows_i686_gnu 0.52.6", 369 | "windows_i686_gnullvm", 370 | "windows_i686_msvc 0.52.6", 371 | "windows_x86_64_gnu 0.52.6", 372 | "windows_x86_64_gnullvm 0.52.6", 373 | "windows_x86_64_msvc 0.52.6", 374 | ] 375 | 376 | [[package]] 377 | name = "windows_aarch64_gnullvm" 378 | version = "0.48.5" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 381 | 382 | [[package]] 383 | name = "windows_aarch64_gnullvm" 384 | version = "0.52.6" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 387 | 388 | [[package]] 389 | name = "windows_aarch64_msvc" 390 | version = "0.48.5" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 393 | 394 | [[package]] 395 | name = "windows_aarch64_msvc" 396 | version = "0.52.6" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 399 | 400 | [[package]] 401 | name = "windows_i686_gnu" 402 | version = "0.48.5" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 405 | 406 | [[package]] 407 | name = "windows_i686_gnu" 408 | version = "0.52.6" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 411 | 412 | [[package]] 413 | name = "windows_i686_gnullvm" 414 | version = "0.52.6" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 417 | 418 | [[package]] 419 | name = "windows_i686_msvc" 420 | version = "0.48.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 423 | 424 | [[package]] 425 | name = "windows_i686_msvc" 426 | version = "0.52.6" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 429 | 430 | [[package]] 431 | name = "windows_x86_64_gnu" 432 | version = "0.48.5" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 435 | 436 | [[package]] 437 | name = "windows_x86_64_gnu" 438 | version = "0.52.6" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 441 | 442 | [[package]] 443 | name = "windows_x86_64_gnullvm" 444 | version = "0.48.5" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 447 | 448 | [[package]] 449 | name = "windows_x86_64_gnullvm" 450 | version = "0.52.6" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 453 | 454 | [[package]] 455 | name = "windows_x86_64_msvc" 456 | version = "0.48.5" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 459 | 460 | [[package]] 461 | name = "windows_x86_64_msvc" 462 | version = "0.52.6" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 465 | 466 | [[package]] 467 | name = "x11-clipboard" 468 | version = "0.9.3" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" 471 | dependencies = [ 472 | "libc", 473 | "x11rb", 474 | ] 475 | 476 | [[package]] 477 | name = "x11rb" 478 | version = "0.13.1" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" 481 | dependencies = [ 482 | "gethostname", 483 | "rustix", 484 | "x11rb-protocol", 485 | ] 486 | 487 | [[package]] 488 | name = "x11rb-protocol" 489 | version = "0.13.1" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" 492 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | version = "2.3.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | copyrs = { version = "0", default-features = false } 8 | crossbeam-channel = "0" 9 | log = { version = "0", optional = true } 10 | network-interface = "2" 11 | simplelog = { version = "0", optional = true } 12 | 13 | [lints.clippy] 14 | pedantic = { level = "deny", priority = -1 } 15 | must_use_candidate = "allow" 16 | enum-glob-use = "allow" 17 | missing-errors-doc = "allow" 18 | 19 | [features] 20 | log = [ "dep:log", "dep:simplelog" ] 21 | backend = [ "copyrs/x11" ] 22 | frontend = [ ] 23 | -------------------------------------------------------------------------------- /common/src/api.rs: -------------------------------------------------------------------------------- 1 | use crate::service; 2 | #[cfg(feature = "frontend")] 3 | use std::sync; 4 | use std::{fmt, io}; 5 | 6 | pub const CHUNK_LENGTH: usize = 1600; // this is the max value 7 | 8 | #[derive(Debug)] 9 | pub enum Error { 10 | Io(io::Error), 11 | InvalidChunkType(Option), 12 | InvalidChunkSize(usize), 13 | PipelineBroken, 14 | } 15 | 16 | impl fmt::Display for Error { 17 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 18 | match self { 19 | Self::Io(e) => write!(fmt, "I/O error: {e}"), 20 | Self::InvalidChunkType(b) => { 21 | if let Some(b) = b { 22 | write!(fmt, "invalid chunk type: 0x{b:x}") 23 | } else { 24 | write!(fmt, "missing chunk type") 25 | } 26 | } 27 | Self::InvalidChunkSize(s) => { 28 | write!(fmt, "invalid chunk size: 0x{s:x}") 29 | } 30 | Self::PipelineBroken => write!(fmt, "broken pipeline"), 31 | } 32 | } 33 | } 34 | 35 | impl From for Error { 36 | fn from(e: io::Error) -> Self { 37 | Self::Io(e) 38 | } 39 | } 40 | 41 | impl From for Error { 42 | fn from(_e: crossbeam_channel::RecvError) -> Self { 43 | Self::PipelineBroken 44 | } 45 | } 46 | 47 | impl From> for Error { 48 | fn from(_e: crossbeam_channel::SendError) -> Self { 49 | Self::PipelineBroken 50 | } 51 | } 52 | 53 | #[derive(Debug, PartialEq, Eq)] 54 | pub enum ChunkType { 55 | Start, 56 | Data, 57 | End, 58 | } 59 | 60 | impl ChunkType { 61 | const fn serialized(self) -> u8 { 62 | match self { 63 | Self::Start => ID_START, 64 | Self::Data => ID_DATA, 65 | Self::End => ID_END, 66 | } 67 | } 68 | } 69 | 70 | impl fmt::Display for ChunkType { 71 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 72 | match self { 73 | Self::Start => write!(fmt, "Start"), 74 | Self::Data => write!(fmt, "Data"), 75 | Self::End => write!(fmt, "End"), 76 | } 77 | } 78 | } 79 | 80 | const ID_START: u8 = 0x00; 81 | const ID_DATA: u8 = 0x01; 82 | const ID_END: u8 = 0x02; 83 | 84 | pub type ClientId = u32; 85 | 86 | #[cfg(feature = "frontend")] 87 | static CLIENT_ID_COUNTER: sync::atomic::AtomicU32 = sync::atomic::AtomicU32::new(0); 88 | 89 | #[cfg(feature = "frontend")] 90 | pub(crate) fn new_client_id() -> ClientId { 91 | CLIENT_ID_COUNTER.fetch_add(1, sync::atomic::Ordering::Relaxed) 92 | } 93 | 94 | pub struct Chunk(Vec); 95 | 96 | const SERIALIZE_OVERHEAD: usize = 4 + 1 + 2; 97 | 98 | impl Chunk { 99 | fn new( 100 | chunk_type: ChunkType, 101 | client_id: ClientId, 102 | data: Option<&[u8]>, 103 | ) -> Result { 104 | let mut content = Vec::with_capacity(CHUNK_LENGTH); 105 | content.extend_from_slice(&client_id.to_le_bytes()); 106 | content.push(chunk_type.serialized()); 107 | if let Some(data) = data { 108 | let payload_len = data.len(); 109 | if payload_len > (CHUNK_LENGTH - SERIALIZE_OVERHEAD) { 110 | return Err(io::Error::new( 111 | io::ErrorKind::InvalidData, 112 | "payload is too large!", 113 | )); 114 | } 115 | let payload_len = u16::try_from(payload_len) 116 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 117 | content.extend_from_slice(&u16::to_le_bytes(payload_len)); 118 | content.extend_from_slice(data); 119 | } else { 120 | content.extend_from_slice(&0u16.to_le_bytes()); 121 | } 122 | Ok(Self(content)) 123 | } 124 | 125 | pub fn start(client_id: ClientId, service: &service::Service) -> Result { 126 | Self::new(ChunkType::Start, client_id, Some(service.name().as_bytes())) 127 | } 128 | 129 | pub fn data(client_id: ClientId, data: &[u8]) -> Result { 130 | Self::new(ChunkType::Data, client_id, Some(data)) 131 | } 132 | 133 | #[allow(clippy::missing_panics_doc)] 134 | pub fn end(client_id: ClientId) -> Self { 135 | Self::new(ChunkType::End, client_id, None).expect("infaillible") 136 | } 137 | 138 | pub fn client_id(&self) -> ClientId { 139 | let bytes = [self.0[0], self.0[1], self.0[2], self.0[3]]; 140 | u32::from_le_bytes(bytes) 141 | } 142 | 143 | pub fn chunk_type(&self) -> Result { 144 | match self.0.get(4) { 145 | Some(&ID_START) => Ok(ChunkType::Start), 146 | Some(&ID_DATA) => Ok(ChunkType::Data), 147 | Some(&ID_END) => Ok(ChunkType::End), 148 | b => Err(Error::InvalidChunkType(b.copied())), 149 | } 150 | } 151 | 152 | fn payload_len(&self) -> u16 { 153 | let data_len_bytes = [self.0[5], self.0[6]]; 154 | u16::from_le_bytes(data_len_bytes) 155 | } 156 | 157 | pub fn can_deserialize_from(data: &[u8]) -> Option { 158 | let len = data.len(); 159 | if len < SERIALIZE_OVERHEAD { 160 | return None; 161 | } 162 | let payload_len_bytes = [data[5], data[6]]; 163 | let payload_len = u16::from_le_bytes(payload_len_bytes); 164 | let expected_len = SERIALIZE_OVERHEAD + payload_len as usize; 165 | if len < expected_len { 166 | return None; 167 | } 168 | Some(expected_len) 169 | } 170 | 171 | pub fn deserialize_from(data: &[u8]) -> Result { 172 | let content = Vec::from(data); 173 | Self::deserialize(content) 174 | } 175 | 176 | pub fn deserialize(content: Vec) -> Result { 177 | let len = content.len(); 178 | if !(SERIALIZE_OVERHEAD..=CHUNK_LENGTH).contains(&len) { 179 | return Err(Error::InvalidChunkSize(len)); 180 | } 181 | let res = Self(content); 182 | if SERIALIZE_OVERHEAD + res.payload_len() as usize == len { 183 | Ok(res) 184 | } else { 185 | Err(Error::InvalidChunkSize(len)) 186 | } 187 | } 188 | 189 | pub const fn serialized_overhead() -> usize { 190 | SERIALIZE_OVERHEAD 191 | } 192 | 193 | pub const fn max_payload_length() -> usize { 194 | CHUNK_LENGTH - SERIALIZE_OVERHEAD 195 | } 196 | 197 | pub fn payload(&self) -> &[u8] { 198 | let len = usize::from(self.payload_len()); 199 | &self.0[SERIALIZE_OVERHEAD..(SERIALIZE_OVERHEAD + len)] 200 | } 201 | 202 | pub fn serialized(self) -> Vec { 203 | self.0 204 | } 205 | } 206 | 207 | impl fmt::Display for Chunk { 208 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 209 | write!( 210 | fmt, 211 | "client {:x} chunk_type = {} data = {} byte(s)", 212 | self.client_id(), 213 | self.chunk_type().map_err(|_| fmt::Error)?, 214 | self.payload_len() 215 | ) 216 | } 217 | } 218 | 219 | pub enum ChunkControl { 220 | Chunk(Chunk), 221 | Shutdown, 222 | } 223 | -------------------------------------------------------------------------------- /common/src/clipboard/backend.rs: -------------------------------------------------------------------------------- 1 | use super::protocol; 2 | use crate::service; 3 | use copyrs::Clipboard; 4 | use std::{borrow, io}; 5 | 6 | pub(crate) fn handler(mut stream: service::RdpStream<'_>) -> Result<(), io::Error> { 7 | crate::debug!("starting"); 8 | 9 | loop { 10 | let cmd = protocol::Command::receive(&mut stream)?; 11 | 12 | match cmd { 13 | protocol::Command::Read => { 14 | crate::debug!("read"); 15 | 16 | match copyrs::clipboard() { 17 | Err(e) => { 18 | crate::error!("failed to get clipboard: {e}"); 19 | protocol::Response::Failed.send(&mut stream)?; 20 | } 21 | Ok(clipboard) => match clipboard.get_content() { 22 | Err(e) => { 23 | crate::error!("failed to get clipboard content: {e}"); 24 | protocol::Response::Failed.send(&mut stream)?; 25 | } 26 | Ok(content) => match content.kind { 27 | copyrs::ClipboardContentKind::Image => { 28 | crate::error!("clipboard contrent is an image, not text"); 29 | protocol::Response::Failed.send(&mut stream)?; 30 | } 31 | copyrs::ClipboardContentKind::Text => { 32 | protocol::Response::Text(content.data).send(&mut stream)?; 33 | } 34 | }, 35 | }, 36 | } 37 | } 38 | 39 | protocol::Command::WriteText(value) => { 40 | crate::debug!("write_text {value:?}"); 41 | 42 | match copyrs::clipboard() { 43 | Err(e) => { 44 | crate::error!("failed to get clipboard: {e}"); 45 | protocol::Response::Failed.send(&mut stream)?; 46 | } 47 | Ok(mut clipboard) => { 48 | let value = borrow::Cow::Borrowed(value.as_slice()); 49 | 50 | match clipboard.set_content(value, copyrs::ClipboardContentKind::Text) { 51 | Err(e) => { 52 | crate::error!("failed to set clipboard: {e}"); 53 | protocol::Response::Failed.send(&mut stream)?; 54 | } 55 | Ok(()) => { 56 | protocol::Response::WriteDone.send(&mut stream)?; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /common/src/clipboard/frontend.rs: -------------------------------------------------------------------------------- 1 | use super::protocol; 2 | use crate::service; 3 | use std::{ 4 | io::{self, BufRead, Write}, 5 | net, thread, 6 | }; 7 | 8 | pub(crate) fn tcp_handler<'a>( 9 | _server: &service::TcpFrontendServer, 10 | _scope: &'a thread::Scope<'a, '_>, 11 | stream: net::TcpStream, 12 | channel: &'a service::Channel, 13 | ) -> Result<(), io::Error> { 14 | let lstream = stream.try_clone()?; 15 | let mut client_read = io::BufReader::new(lstream); 16 | 17 | let mut client_write = io::BufWriter::new(stream); 18 | 19 | let mut rdp = channel.connect(&super::SERVICE)?; 20 | 21 | let mut line = String::new(); 22 | 23 | loop { 24 | let _ = client_read.read_line(&mut line)?; 25 | 26 | let cline = line 27 | .strip_suffix("\n") 28 | .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "interrupted"))?; 29 | 30 | let cline = if cline.ends_with('\r') { 31 | cline.strip_suffix('\r').unwrap() 32 | } else { 33 | cline 34 | }; 35 | 36 | let (command, args) = cline 37 | .split_once(' ') 38 | .map(|(command, args)| (command, args.to_string())) 39 | .unwrap_or((cline, String::new())); 40 | let command = command.to_uppercase(); 41 | 42 | crate::debug!("{cline:?}"); 43 | crate::trace!("COMMAND = {command:?}"); 44 | crate::trace!("ARGS = {args:?}"); 45 | 46 | match command.as_str() { 47 | "READ" | "GET" => { 48 | protocol::Command::Read.send(&mut rdp)?; 49 | match protocol::Response::receive(&mut rdp)? { 50 | protocol::Response::Text(value) => { 51 | let value = String::from_utf8_lossy(&value); 52 | writeln!(client_write, "ok {value:?}")?; 53 | } 54 | protocol::Response::Failed => { 55 | writeln!(client_write, "KO")?; 56 | } 57 | protocol::Response::WriteDone => unreachable!(), 58 | } 59 | } 60 | "WRITE" | "PUT" => { 61 | protocol::Command::WriteText(args.into_bytes()).send(&mut rdp)?; 62 | match protocol::Response::receive(&mut rdp)? { 63 | protocol::Response::WriteDone => { 64 | writeln!(client_write, "ok")?; 65 | } 66 | protocol::Response::Failed => { 67 | writeln!(client_write, "KO")?; 68 | } 69 | protocol::Response::Text(_) => unreachable!(), 70 | } 71 | } 72 | "EXIT" | "QUIT" => { 73 | let _ = rdp.disconnect(); 74 | let lstream = client_read.into_inner(); 75 | let _ = lstream.shutdown(net::Shutdown::Both); 76 | return Ok(()); 77 | } 78 | _ => writeln!(client_write, "invalid command")?, 79 | } 80 | client_write.flush()?; 81 | 82 | line.clear(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /common/src/clipboard/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::service; 2 | 3 | #[cfg(feature = "backend")] 4 | mod backend; 5 | #[cfg(feature = "frontend")] 6 | mod frontend; 7 | mod protocol; 8 | 9 | pub(crate) static SERVICE: service::Service = service::Service { 10 | name: "clipboard", 11 | #[cfg(feature = "frontend")] 12 | tcp_frontend: Some(service::TcpFrontend { 13 | default_port: 3032, 14 | handler: frontend::tcp_handler, 15 | }), 16 | #[cfg(feature = "backend")] 17 | backend: service::Backend { 18 | handler: backend::handler, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /common/src/clipboard/protocol.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | const ID_READ: u8 = 0x0; 4 | const ID_WRITE_TEXT: u8 = 0x1; 5 | 6 | pub enum Command { 7 | Read, 8 | WriteText(Vec), 9 | } 10 | 11 | impl Command { 12 | #[cfg(feature = "frontend")] 13 | pub(crate) fn send(&self, stream: &mut W) -> Result<(), io::Error> 14 | where 15 | W: io::Write, 16 | { 17 | match self { 18 | Self::Read => { 19 | let mut buf = [0u8; 1]; 20 | buf[0] = ID_READ; 21 | stream.write_all(&buf)?; 22 | } 23 | Self::WriteText(d) => { 24 | let mut buf = [0u8; 1]; 25 | buf[0] = ID_WRITE_TEXT; 26 | stream.write_all(&buf)?; 27 | 28 | let len = d.len(); 29 | stream.write_all(&len.to_le_bytes())?; 30 | stream.write_all(d)?; 31 | } 32 | } 33 | stream.flush() 34 | } 35 | 36 | #[cfg(feature = "backend")] 37 | pub(crate) fn receive(stream: &mut R) -> Result 38 | where 39 | R: io::Read, 40 | { 41 | let mut buf = [0u8; 1]; 42 | stream.read_exact(&mut buf)?; 43 | 44 | match buf[0] { 45 | ID_READ => Ok(Self::Read), 46 | ID_WRITE_TEXT => { 47 | let mut buf = [0u8; 8]; 48 | stream.read_exact(&mut buf)?; 49 | let len = u64::from_le_bytes(buf); 50 | let len = usize::try_from(len) 51 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 52 | 53 | let mut buf = vec![0u8; len]; 54 | stream.read_exact(&mut buf)?; 55 | 56 | Ok(Self::WriteText(Vec::from(buf))) 57 | } 58 | _ => Err(io::Error::new( 59 | io::ErrorKind::InvalidData, 60 | "invalid command", 61 | )), 62 | } 63 | } 64 | } 65 | 66 | const ID_TEXT: u8 = 0x0; 67 | const ID_FAILED: u8 = 0x1; 68 | const ID_WRITE_DONE: u8 = 0x2; 69 | 70 | pub enum Response { 71 | Text(Vec), 72 | Failed, 73 | WriteDone, 74 | } 75 | 76 | impl Response { 77 | #[cfg(feature = "backend")] 78 | pub(crate) fn send(&self, stream: &mut W) -> Result<(), io::Error> 79 | where 80 | W: io::Write, 81 | { 82 | match self { 83 | Self::Text(s) => { 84 | let mut buf = [0u8; 1]; 85 | buf[0] = ID_TEXT; 86 | stream.write_all(&buf)?; 87 | 88 | let len = s.len(); 89 | stream.write_all(&len.to_le_bytes())?; 90 | stream.write_all(s)?; 91 | } 92 | Self::Failed => { 93 | let mut buf = [0u8; 1]; 94 | buf[0] = ID_FAILED; 95 | stream.write_all(&buf)?; 96 | } 97 | Self::WriteDone => { 98 | let mut buf = [0u8; 1]; 99 | buf[0] = ID_WRITE_DONE; 100 | stream.write_all(&buf)?; 101 | } 102 | } 103 | stream.flush() 104 | } 105 | 106 | #[cfg(feature = "frontend")] 107 | pub(crate) fn receive(stream: &mut R) -> Result 108 | where 109 | R: io::Read, 110 | { 111 | let mut buf = [0u8; 1]; 112 | stream.read_exact(&mut buf)?; 113 | 114 | match buf[0] { 115 | ID_TEXT => { 116 | let mut buf = [0u8; 8]; 117 | stream.read_exact(&mut buf)?; 118 | let len = u64::from_le_bytes(buf); 119 | let len = usize::try_from(len) 120 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 121 | 122 | let mut buf = vec![0u8; len]; 123 | stream.read_exact(&mut buf)?; 124 | 125 | Ok(Self::Text(Vec::from(buf))) 126 | } 127 | ID_FAILED => Ok(Self::Failed), 128 | ID_WRITE_DONE => Ok(Self::WriteDone), 129 | _ => Err(io::Error::new( 130 | io::ErrorKind::InvalidData, 131 | "invalid response", 132 | )), 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /common/src/command/backend.rs: -------------------------------------------------------------------------------- 1 | use crate::service; 2 | use std::{io, process, thread}; 3 | 4 | pub(crate) fn backend_handler(rdp_stream: service::RdpStream<'_>) -> Result<(), io::Error> { 5 | let client_id = rdp_stream.client_id(); 6 | 7 | #[cfg(target_os = "windows")] 8 | let cmd = "cmd.exe"; 9 | #[cfg(not(target_os = "windows"))] 10 | let cmd = "sh"; 11 | 12 | #[cfg(target_os = "windows")] 13 | let args: [String; 0] = []; 14 | #[cfg(not(target_os = "windows"))] 15 | let args = ["-i"]; 16 | 17 | crate::debug!("starting {cmd:?}"); 18 | 19 | thread::scope(|scope| { 20 | let child = process::Command::new(cmd) 21 | .args(args) 22 | .stdin(process::Stdio::piped()) 23 | .stdout(process::Stdio::piped()) 24 | .stderr(process::Stdio::piped()) 25 | .spawn()?; 26 | 27 | let mut stdin = child 28 | .stdin 29 | .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no stdin"))?; 30 | let mut stdout = child 31 | .stdout 32 | .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no stdout"))?; 33 | let mut stderr = child 34 | .stderr 35 | .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no stderr"))?; 36 | 37 | let (mut rdp_stream_read, mut rdp_stream_write_out) = rdp_stream.split(); 38 | let mut rdp_stream_write_err = rdp_stream_write_out.clone(); 39 | 40 | thread::Builder::new() 41 | .name(format!( 42 | "{} {} {client_id:x} stdout", 43 | service::Kind::Backend, 44 | super::SERVICE, 45 | )) 46 | .spawn_scoped(scope, move || { 47 | if let Err(e) = service::stream_copy(&mut stdout, &mut rdp_stream_write_out) { 48 | crate::debug!("error: {e}"); 49 | } else { 50 | crate::debug!("stopped"); 51 | } 52 | }) 53 | .unwrap(); 54 | 55 | thread::Builder::new() 56 | .name(format!( 57 | "{} {} {client_id:x} stderr", 58 | service::Kind::Backend, 59 | super::SERVICE, 60 | )) 61 | .spawn_scoped(scope, move || { 62 | if let Err(e) = service::stream_copy(&mut stderr, &mut rdp_stream_write_err) { 63 | crate::debug!("error: {e}"); 64 | } else { 65 | crate::debug!("stopped"); 66 | } 67 | }) 68 | .unwrap(); 69 | 70 | if let Err(e) = service::stream_copy(&mut rdp_stream_read, &mut stdin) { 71 | crate::debug!("error: {e}"); 72 | } else { 73 | crate::debug!("stopped"); 74 | } 75 | rdp_stream_read.disconnect(); 76 | 77 | Ok(()) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /common/src/command/frontend.rs: -------------------------------------------------------------------------------- 1 | use crate::service; 2 | use std::{io, net, thread}; 3 | 4 | pub(crate) fn tcp_frontend_handler( 5 | _server: &service::TcpFrontendServer, 6 | _scope: &thread::Scope, 7 | client: net::TcpStream, 8 | channel: &service::Channel, 9 | ) -> Result<(), io::Error> { 10 | let client_rdp = channel.connect(&super::SERVICE)?; 11 | service::double_stream_copy(service::Kind::Frontend, &super::SERVICE, client_rdp, client) 12 | } 13 | -------------------------------------------------------------------------------- /common/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::service; 2 | 3 | #[cfg(feature = "backend")] 4 | mod backend; 5 | #[cfg(feature = "frontend")] 6 | mod frontend; 7 | 8 | pub(crate) static SERVICE: service::Service = service::Service { 9 | name: "command", 10 | #[cfg(feature = "frontend")] 11 | tcp_frontend: Some(service::TcpFrontend { 12 | default_port: 3031, 13 | handler: frontend::tcp_frontend_handler, 14 | }), 15 | #[cfg(feature = "backend")] 16 | backend: service::Backend { 17 | handler: backend::backend_handler, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /common/src/ftp/backend.rs: -------------------------------------------------------------------------------- 1 | use super::protocol; 2 | use crate::service; 3 | use std::{ 4 | fs, 5 | io::{self, Write}, 6 | path, 7 | }; 8 | 9 | fn cmd_cwd(stream: &mut service::RdpStream<'_>, path: String) -> Result<(), io::Error> { 10 | crate::info!("change directory {path:?}"); 11 | 12 | let path = path::PathBuf::from(path); 13 | if path.exists() { 14 | protocol::DataReply::CwdOk.send(stream)?; 15 | } else { 16 | protocol::DataReply::Ko.send(stream)?; 17 | } 18 | 19 | Ok(()) 20 | } 21 | 22 | fn cmd_dele(stream: &mut service::RdpStream<'_>, path: String) -> Result<(), io::Error> { 23 | crate::info!("delete {path:?}"); 24 | 25 | let path = path::PathBuf::from(path); 26 | if path.exists() { 27 | if let Err(e) = fs::remove_file(path) { 28 | crate::error!("failed to delete: {e}"); 29 | protocol::DataReply::Ko.send(stream)?; 30 | } else { 31 | protocol::DataReply::DeleteOk.send(stream)?; 32 | } 33 | } else { 34 | protocol::DataReply::Ko.send(stream)?; 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | fn cmd_list(stream: &mut service::RdpStream<'_>, path: String) -> Result<(), io::Error> { 41 | crate::info!("list {path:?}"); 42 | 43 | let path = path::PathBuf::from(path); 44 | if path.exists() { 45 | if let Ok(dir) = path.read_dir() { 46 | dir.into_iter().try_for_each(|entry| { 47 | if let Ok(entry) = entry { 48 | if let Ok(file_type) = entry.file_type() { 49 | if file_type.is_dir() { 50 | write!(stream, "d")?; 51 | } else if file_type.is_file() { 52 | write!(stream, "-")?; 53 | } else { 54 | write!(stream, "l")?; 55 | } 56 | let _ = write!(stream, "rwxrwxrwx 1 ftp ftp "); 57 | if let Ok(metadata) = entry.metadata() { 58 | write!(stream, "{}", metadata.len())?; 59 | } else { 60 | write!(stream, "0")?; 61 | } 62 | write!(stream, " {}\r\n", entry.file_name().into_string().unwrap())?; 63 | } 64 | } 65 | Ok::<(), io::Error>(()) 66 | })?; 67 | } 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | fn cmd_nlst(stream: &mut service::RdpStream<'_>, path: String) -> Result<(), io::Error> { 74 | crate::info!("name list {path:?}"); 75 | 76 | let path = path::PathBuf::from(path); 77 | if path.exists() { 78 | if let Ok(dir) = path.read_dir() { 79 | dir.into_iter().try_for_each(|entry| { 80 | if let Ok(entry) = entry { 81 | write!(stream, "{}\r\n", entry.file_name().into_string().unwrap())?; 82 | } 83 | Ok::<(), io::Error>(()) 84 | })?; 85 | } 86 | } 87 | Ok(()) 88 | } 89 | 90 | fn cmd_retr(stream: &mut service::RdpStream<'_>, path: String) -> Result<(), io::Error> { 91 | crate::info!("downloading {path:?}"); 92 | 93 | let path = path::PathBuf::from(path); 94 | if path.exists() && path.is_file() { 95 | let file = fs::File::options().read(true).write(false).open(path)?; 96 | let mut file = io::BufReader::new(file); 97 | 98 | if let Err(e) = service::stream_copy(&mut file, stream) { 99 | crate::debug!("error: {e}"); 100 | } else { 101 | crate::debug!("stopped"); 102 | } 103 | } 104 | 105 | Ok(()) 106 | } 107 | 108 | fn cmd_size(stream: &mut service::RdpStream<'_>, path: String) -> Result<(), io::Error> { 109 | crate::info!("size {path:?}"); 110 | 111 | let path = path::PathBuf::from(path); 112 | if path.exists() { 113 | if let Ok(metadata) = path.metadata() { 114 | let size = metadata.len(); 115 | protocol::DataReply::SizeOk(size).send(stream)?; 116 | } else { 117 | protocol::DataReply::Ko.send(stream)?; 118 | } 119 | } else { 120 | protocol::DataReply::Ko.send(stream)?; 121 | } 122 | 123 | Ok(()) 124 | } 125 | 126 | fn cmd_stor(stream: &mut service::RdpStream<'_>, path: String) -> Result<(), io::Error> { 127 | crate::info!("uploading {path:?}"); 128 | 129 | let path = path::PathBuf::from(path); 130 | let file = fs::File::options() 131 | .create(true) 132 | .truncate(true) 133 | .write(true) 134 | .open(path)?; 135 | let mut file = io::BufWriter::new(file); 136 | 137 | if let Err(e) = service::stream_copy(stream, &mut file) { 138 | crate::debug!("error: {e}"); 139 | } else { 140 | crate::debug!("stopped"); 141 | } 142 | 143 | Ok(()) 144 | } 145 | 146 | pub(crate) fn handler(mut stream: service::RdpStream<'_>) -> Result<(), io::Error> { 147 | crate::debug!("starting"); 148 | 149 | let cmd = protocol::DataCommand::receive(&mut stream)?; 150 | 151 | match cmd { 152 | protocol::DataCommand::Cwd(path) => { 153 | #[cfg(target_os = "windows")] 154 | let path = "C:".to_string() + path.as_str(); 155 | 156 | cmd_cwd(&mut stream, path)?; 157 | } 158 | protocol::DataCommand::Dele(path) => { 159 | #[cfg(target_os = "windows")] 160 | let path = "C:".to_string() + path.as_str(); 161 | 162 | cmd_dele(&mut stream, path)?; 163 | } 164 | protocol::DataCommand::List(path) => { 165 | #[cfg(target_os = "windows")] 166 | let path = "C:".to_string() + path.as_str(); 167 | 168 | cmd_list(&mut stream, path)?; 169 | } 170 | protocol::DataCommand::NLst(path) => { 171 | #[cfg(target_os = "windows")] 172 | let path = "C:".to_string() + path.as_str(); 173 | 174 | cmd_nlst(&mut stream, path)?; 175 | } 176 | protocol::DataCommand::Retr(path) => { 177 | #[cfg(target_os = "windows")] 178 | let path = "C:".to_string() + path.as_str(); 179 | 180 | cmd_retr(&mut stream, path)?; 181 | } 182 | protocol::DataCommand::Size(path) => { 183 | #[cfg(target_os = "windows")] 184 | let path = "C:".to_string() + path.as_str(); 185 | 186 | cmd_size(&mut stream, path)?; 187 | } 188 | protocol::DataCommand::Stor(path) => { 189 | #[cfg(target_os = "windows")] 190 | let path = "C:".to_string() + path.as_str(); 191 | 192 | cmd_stor(&mut stream, path)?; 193 | } 194 | } 195 | 196 | stream.disconnect() 197 | } 198 | -------------------------------------------------------------------------------- /common/src/ftp/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::service; 2 | 3 | #[cfg(feature = "backend")] 4 | mod backend; 5 | #[cfg(feature = "frontend")] 6 | mod frontend; 7 | mod protocol; 8 | 9 | pub(crate) static SERVICE: service::Service = service::Service { 10 | name: "ftp", 11 | #[cfg(feature = "frontend")] 12 | tcp_frontend: Some(service::TcpFrontend { 13 | default_port: 2021, 14 | handler: frontend::tcp_handler, 15 | }), 16 | #[cfg(feature = "backend")] 17 | backend: service::Backend { 18 | handler: backend::handler, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /common/src/ftp/protocol.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, io}; 2 | 3 | const ID_CWD: u8 = 0x0; 4 | const ID_DELE: u8 = 0x1; 5 | const ID_LIST: u8 = 0x2; 6 | const ID_NLST: u8 = 0x3; 7 | const ID_RETR: u8 = 0x4; 8 | const ID_SIZE: u8 = 0x5; 9 | const ID_STOR: u8 = 0x6; 10 | 11 | #[derive(Debug)] 12 | pub enum DataCommand { 13 | Cwd(String), 14 | Dele(String), 15 | List(String), 16 | NLst(String), 17 | Retr(String), 18 | Size(String), 19 | Stor(String), 20 | } 21 | 22 | impl DataCommand { 23 | #[cfg(feature = "frontend")] 24 | pub(crate) const fn is_ftp_control(&self) -> bool { 25 | match self { 26 | Self::Dele(_) | Self::Cwd(_) | Self::Size(_) => true, 27 | Self::List(_) | Self::NLst(_) | Self::Retr(_) | Self::Stor(_) => false, 28 | } 29 | } 30 | 31 | #[cfg(feature = "frontend")] 32 | pub(crate) const fn is_upload(&self) -> bool { 33 | matches!(self, Self::Stor(_)) 34 | } 35 | 36 | #[cfg(feature = "frontend")] 37 | pub(crate) fn send(&self, stream: &mut W) -> Result<(), io::Error> 38 | where 39 | W: io::Write, 40 | { 41 | let (code, value) = match self { 42 | Self::Cwd(s) => (ID_CWD, s), 43 | Self::Dele(s) => (ID_DELE, s), 44 | Self::List(s) => (ID_LIST, s), 45 | Self::NLst(s) => (ID_NLST, s), 46 | Self::Retr(s) => (ID_RETR, s), 47 | Self::Size(s) => (ID_SIZE, s), 48 | Self::Stor(s) => (ID_STOR, s), 49 | }; 50 | 51 | let buf = [code; 1]; 52 | stream.write_all(&buf)?; 53 | let len = value.len() as u64; 54 | stream.write_all(&len.to_le_bytes())?; 55 | stream.write_all(value.as_bytes())?; 56 | stream.flush() 57 | } 58 | 59 | #[cfg(feature = "backend")] 60 | pub(crate) fn receive(stream: &mut R) -> Result 61 | where 62 | R: io::Read, 63 | { 64 | let mut buf = [0u8; 1]; 65 | stream.read_exact(&mut buf)?; 66 | let code = buf[0]; 67 | 68 | let mut buf = [0u8; 8]; 69 | stream.read_exact(&mut buf)?; 70 | let len = u64::from_le_bytes(buf); 71 | 72 | let len = usize::try_from(len) 73 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 74 | let mut buf = vec![0u8; len]; 75 | stream.read_exact(&mut buf)?; 76 | let value = String::from_utf8_lossy(&buf).to_string(); 77 | 78 | match code { 79 | ID_CWD => Ok(Self::Cwd(value)), 80 | ID_DELE => Ok(Self::Dele(value)), 81 | ID_LIST => Ok(Self::List(value)), 82 | ID_NLST => Ok(Self::NLst(value)), 83 | ID_RETR => Ok(Self::Retr(value)), 84 | ID_SIZE => Ok(Self::Size(value)), 85 | ID_STOR => Ok(Self::Stor(value)), 86 | v => unimplemented!("unsupported ftp data command {v}"), 87 | } 88 | } 89 | } 90 | 91 | impl fmt::Display for DataCommand { 92 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 93 | match self { 94 | Self::Cwd(path) => write!(f, "change directory {path:?}"), 95 | Self::Dele(path) => write!(f, "delete {path:?}"), 96 | Self::List(path) => write!(f, "list {path:?}"), 97 | Self::NLst(path) => write!(f, "nlst {path:?}"), 98 | Self::Retr(path) => write!(f, "download {path:?}"), 99 | Self::Size(path) => write!(f, "size {path:?}"), 100 | Self::Stor(path) => write!(f, "upload {path:?}"), 101 | } 102 | } 103 | } 104 | 105 | #[cfg(feature = "frontend")] 106 | const ID_DATA_TRANSFER_OK: u8 = 0x0; 107 | const ID_CWD_OK: u8 = 0x1; 108 | const ID_SIZE_OK: u8 = 0x2; 109 | const ID_DELETE_OK: u8 = 0x3; 110 | const ID_KO: u8 = 0x4; 111 | 112 | #[derive(Debug)] 113 | pub enum DataReply { 114 | #[cfg(feature = "frontend")] 115 | DataTransferOk, 116 | CwdOk, 117 | SizeOk(u64), 118 | DeleteOk, 119 | Ko, 120 | } 121 | 122 | impl DataReply { 123 | #[cfg(feature = "frontend")] 124 | pub(crate) const fn is_ok(&self) -> bool { 125 | match self { 126 | Self::DataTransferOk | Self::CwdOk | Self::SizeOk(_) | Self::DeleteOk => true, 127 | Self::Ko => false, 128 | } 129 | } 130 | 131 | #[cfg(feature = "backend")] 132 | pub(crate) fn send(&self, stream: &mut W) -> Result<(), io::Error> 133 | where 134 | W: io::Write, 135 | { 136 | let (code, value) = match self { 137 | #[cfg(feature = "frontend")] 138 | Self::DataTransferOk => (ID_DATA_TRANSFER_OK, None), 139 | Self::CwdOk => (ID_CWD_OK, None), 140 | Self::SizeOk(size) => (ID_SIZE_OK, Some(size)), 141 | Self::DeleteOk => (ID_DELETE_OK, None), 142 | Self::Ko => (ID_KO, None), 143 | }; 144 | 145 | let mut buf = [0u8; 1]; 146 | buf[0] = code; 147 | stream.write_all(&buf)?; 148 | if let Some(value) = value { 149 | stream.write_all(&value.to_le_bytes())?; 150 | } 151 | stream.flush() 152 | } 153 | 154 | #[cfg(feature = "frontend")] 155 | pub(crate) fn receive(stream: &mut R) -> Result 156 | where 157 | R: io::Read, 158 | { 159 | let mut buf = [0u8; 1]; 160 | stream.read_exact(&mut buf)?; 161 | let code = buf[0]; 162 | 163 | match code { 164 | #[cfg(feature = "frontend")] 165 | ID_DATA_TRANSFER_OK => Ok(Self::DataTransferOk), 166 | ID_CWD_OK => Ok(Self::CwdOk), 167 | ID_SIZE_OK => { 168 | let mut buf = [0u8; 8]; 169 | stream.read_exact(&mut buf)?; 170 | let size = u64::from_le_bytes(buf); 171 | Ok(Self::SizeOk(size)) 172 | } 173 | ID_DELETE_OK => Ok(Self::DeleteOk), 174 | ID_KO => Ok(Self::Ko), 175 | v => unimplemented!("unsupported ftp data reply {v}"), 176 | } 177 | } 178 | } 179 | 180 | impl fmt::Display for DataReply { 181 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 182 | match self { 183 | #[cfg(feature = "frontend")] 184 | Self::DataTransferOk => write!(f, "data transfer ok"), 185 | Self::CwdOk => write!(f, "change directory ok"), 186 | Self::SizeOk(size) => write!(f, "size ok ({size})"), 187 | Self::DeleteOk => write!(f, "delete ok"), 188 | Self::Ko => write!(f, "KO"), 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::ffi; 2 | #[cfg(feature = "log")] 3 | use std::fs; 4 | 5 | pub mod api; 6 | pub mod service; 7 | 8 | mod clipboard; 9 | mod command; 10 | mod ftp; 11 | mod socks5; 12 | mod stage0; 13 | 14 | mod log; 15 | #[cfg(feature = "backend")] 16 | mod util; 17 | 18 | pub const VIRTUAL_CHANNEL_NAME: &ffi::CStr = 19 | unsafe { ffi::CStr::from_bytes_with_nul_unchecked(b"SOXY\x00") }; 20 | 21 | pub enum Level { 22 | Off, 23 | Error, 24 | Warn, 25 | Info, 26 | Debug, 27 | Trace, 28 | } 29 | 30 | impl<'a> TryFrom<&'a str> for Level { 31 | type Error = String; 32 | 33 | fn try_from(s: &'a str) -> Result>::Error> { 34 | match s.to_uppercase().as_ref() { 35 | "OFF" => Ok(Self::Off), 36 | "ERROR" => Ok(Self::Error), 37 | "WARN" | "WARNING" => Ok(Self::Warn), 38 | "INFO" => Ok(Self::Info), 39 | "DEBUG" => Ok(Self::Debug), 40 | "TRACE" => Ok(Self::Trace), 41 | _ => Err("invalid log level".into()), 42 | } 43 | } 44 | } 45 | 46 | #[cfg(not(feature = "log"))] 47 | pub const fn init_logs(_level: Level, _file: Option<&String>) {} 48 | 49 | #[cfg(feature = "log")] 50 | impl Into for Level { 51 | fn into(self) -> simplelog::LevelFilter { 52 | match self { 53 | Self::Off => simplelog::LevelFilter::Off, 54 | Self::Error => simplelog::LevelFilter::Error, 55 | Self::Warn => simplelog::LevelFilter::Warn, 56 | Self::Info => simplelog::LevelFilter::Info, 57 | Self::Debug => simplelog::LevelFilter::Debug, 58 | Self::Trace => simplelog::LevelFilter::Trace, 59 | } 60 | } 61 | } 62 | 63 | #[cfg(feature = "log")] 64 | pub fn init_logs(level: Level, file: Option<&String>) { 65 | let level_filter = level.into(); 66 | 67 | let config = simplelog::ConfigBuilder::new() 68 | .set_level_padding(simplelog::LevelPadding::Right) 69 | .set_target_level(simplelog::LevelFilter::Off) 70 | .set_thread_level(simplelog::LevelFilter::Error) 71 | .set_thread_mode(simplelog::ThreadLogMode::Names) 72 | .set_time_format_rfc2822() 73 | .build(); 74 | 75 | let mut loggers: Vec> = vec![simplelog::TermLogger::new( 76 | level_filter, 77 | config.clone(), 78 | simplelog::TerminalMode::Mixed, 79 | simplelog::ColorChoice::Auto, 80 | )]; 81 | 82 | if let Some(file) = file { 83 | if let Ok(file) = fs::File::options() 84 | .create(true) 85 | .append(false) 86 | .truncate(true) 87 | .write(true) 88 | .open(file) 89 | { 90 | loggers.push(simplelog::WriteLogger::new(level_filter, config, file)); 91 | } 92 | } 93 | 94 | let _ = simplelog::CombinedLogger::init(loggers); 95 | } 96 | -------------------------------------------------------------------------------- /common/src/log.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "log"))] 2 | mod inner { 3 | #[macro_export] 4 | macro_rules! trace { 5 | ($($args:tt)*) => { 6 | let _ = format_args!($($args)*); 7 | }; 8 | () => {}; 9 | } 10 | 11 | #[macro_export] 12 | macro_rules! debug { 13 | ($($args:tt)*) => { 14 | let _ = format_args!($($args)*); 15 | }; 16 | () => {}; 17 | } 18 | 19 | #[macro_export] 20 | macro_rules! info { 21 | ($($args:tt)*) => { 22 | let _ = format_args!($($args)*); 23 | }; 24 | () => {}; 25 | } 26 | 27 | #[macro_export] 28 | macro_rules! warn { 29 | ($($args:tt)*) => { 30 | let _ = format_args!($($args)*); 31 | }; 32 | () => {}; 33 | } 34 | 35 | #[macro_export] 36 | macro_rules! error { 37 | ($($args:tt)*) => { 38 | let _ = format_args!($($args)*); 39 | }; 40 | () => {}; 41 | } 42 | } 43 | 44 | #[cfg(feature = "log")] 45 | mod inner { 46 | #[macro_export] 47 | macro_rules! trace { 48 | ($($arg:tt)*) => { log::trace!($($arg)*) } 49 | } 50 | 51 | #[macro_export] 52 | macro_rules! debug { 53 | ($($arg:tt)*) => { log::debug!($($arg)*) } 54 | } 55 | 56 | #[macro_export] 57 | macro_rules! info { 58 | ($($arg:tt)*) => { log::info!($($arg)*) } 59 | } 60 | 61 | #[macro_export] 62 | macro_rules! warn { 63 | ($($arg:tt)*) => { log::warn!($($arg)*) } 64 | } 65 | 66 | #[macro_export] 67 | macro_rules! error { 68 | ($($arg:tt)*) => { log::error!($($arg)*) } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /common/src/socks5/backend.rs: -------------------------------------------------------------------------------- 1 | use super::protocol; 2 | use crate::{service, util}; 3 | use std::{ 4 | io::{self, Write}, 5 | net, 6 | }; 7 | 8 | const SERVICE_KIND: service::Kind = service::Kind::Backend; 9 | 10 | fn encode_addr(addr: &net::SocketAddr) -> Result, io::Error> { 11 | let mut data = Vec::with_capacity(192); 12 | 13 | match addr { 14 | net::SocketAddr::V4(ipv4) => { 15 | data.write_all(&[0x01; 1])?; 16 | data.write_all(&ipv4.ip().octets())?; 17 | } 18 | net::SocketAddr::V6(ipv6) => { 19 | data.write_all(&[0x04; 1])?; 20 | data.write_all(&ipv6.ip().octets())?; 21 | } 22 | } 23 | data.write_all(&addr.port().to_be_bytes())?; 24 | 25 | Ok(data) 26 | } 27 | 28 | fn command_connect(mut stream: service::RdpStream<'_>, to_tcp: &str) -> Result<(), io::Error> { 29 | crate::info!("connecting to {to_tcp:#?}"); 30 | 31 | match net::TcpStream::connect(to_tcp) { 32 | Err(e) => { 33 | crate::error!("failed to connect to {to_tcp:#?}: {e}"); 34 | match e.kind() { 35 | io::ErrorKind::ConnectionAborted | io::ErrorKind::TimedOut => { 36 | protocol::Response::HostUnreachable.send(&mut stream) 37 | } 38 | io::ErrorKind::ConnectionRefused => { 39 | protocol::Response::ConnectionRefused.send(&mut stream) 40 | } 41 | _ => { 42 | crate::error!("failed to connect to {to_tcp:#?}: {e}"); 43 | protocol::Response::NetworkUnreachable.send(&mut stream) 44 | } 45 | } 46 | } 47 | Ok(server) => { 48 | crate::debug!("connected to {to_tcp:#?}"); 49 | 50 | let data = encode_addr(&server.local_addr()?)?; 51 | protocol::Response::Ok(data).send(&mut stream)?; 52 | 53 | crate::debug!("starting stream copy"); 54 | 55 | service::double_stream_copy(SERVICE_KIND, &super::SERVICE, stream, server) 56 | } 57 | } 58 | } 59 | 60 | fn command_bind(mut stream: service::RdpStream<'_>) -> Result<(), io::Error> { 61 | match util::find_best_address() { 62 | Err(e) => { 63 | crate::error!("failed to enumerate network interfaces: {e}"); 64 | protocol::Response::NetworkUnreachable.send(&mut stream) 65 | } 66 | Ok(util::BestAddress { cidr4, cidr6 }) => { 67 | match cidr4 68 | .map(|(ip, _)| net::IpAddr::from(ip)) 69 | .or(cidr6.map(|(ip, _)| net::IpAddr::from(ip))) 70 | { 71 | None => { 72 | crate::error!("failed to find a suitable network interfaces"); 73 | protocol::Response::NetworkUnreachable.send(&mut stream) 74 | } 75 | Some(ip) => { 76 | let from_tcp = net::SocketAddr::new(ip, 0); 77 | 78 | crate::info!("binding to {from_tcp}"); 79 | 80 | match net::TcpListener::bind(from_tcp) { 81 | Err(e) => { 82 | crate::error!("failed to bind to {from_tcp:#?}: {e}"); 83 | protocol::Response::BindFailed.send(&mut stream) 84 | } 85 | Ok(server) => { 86 | let data = encode_addr(&server.local_addr()?)?; 87 | protocol::Response::Ok(data).send(&mut stream)?; 88 | 89 | match server.accept() { 90 | Err(e) => { 91 | crate::error!("failed to accept on {from_tcp:#?}: {e}"); 92 | protocol::Response::BindFailed.send(&mut stream) 93 | } 94 | Ok((client, client_addr)) => { 95 | let data = encode_addr(&client_addr)?; 96 | protocol::Response::Ok(data).send(&mut stream)?; 97 | 98 | crate::debug!("starting stream copy"); 99 | 100 | service::double_stream_copy( 101 | SERVICE_KIND, 102 | &super::SERVICE, 103 | stream, 104 | client, 105 | ) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | pub(crate) fn handler(mut stream: service::RdpStream<'_>) -> Result<(), io::Error> { 117 | crate::debug!("starting"); 118 | 119 | let cmd = protocol::Command::receive(&mut stream)?; 120 | 121 | match cmd { 122 | protocol::Command::Connect(to_tcp) => command_connect(stream, &to_tcp), 123 | protocol::Command::Bind => command_bind(stream), 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /common/src/socks5/frontend.rs: -------------------------------------------------------------------------------- 1 | use super::protocol; 2 | use crate::service; 3 | use std::{ 4 | fmt, 5 | io::{self, Read, Write}, 6 | net, thread, 7 | }; 8 | 9 | const SERVICE_KIND: service::Kind = service::Kind::Frontend; 10 | 11 | #[derive(Debug)] 12 | enum Error { 13 | UnsupportedVersion(u8), 14 | UnsupportedAuthentication(u8), 15 | Io(io::Error), 16 | UnsupportedCommand(u8), 17 | AddressTypeNotSupported(u8), 18 | } 19 | 20 | impl From for Error { 21 | fn from(e: io::Error) -> Self { 22 | Self::Io(e) 23 | } 24 | } 25 | 26 | impl From for Error { 27 | fn from(e: protocol::Error) -> Self { 28 | match e { 29 | protocol::Error::Io(e) => Self::Io(e), 30 | protocol::Error::UnsupportedVersion(v) => Self::UnsupportedVersion(v), 31 | protocol::Error::UnsupportedCommand(c) => Self::UnsupportedCommand(c), 32 | protocol::Error::AddressTypeNotSupported(t) => Self::AddressTypeNotSupported(t), 33 | } 34 | } 35 | } 36 | 37 | impl fmt::Display for Error { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 39 | match self { 40 | Self::UnsupportedVersion(v) => write!(f, "unsupported version {v}"), 41 | Self::UnsupportedAuthentication(v) => { 42 | write!(f, "unsupported authentication {v}") 43 | } 44 | Self::UnsupportedCommand(v) => write!(f, "unsupported command {v}"), 45 | Self::AddressTypeNotSupported(v) => write!(f, "address type not supported {v}"), 46 | Self::Io(e) => write!(f, "I/O error: {e}"), 47 | } 48 | } 49 | } 50 | 51 | fn handshake(stream: &mut net::TcpStream) -> Result { 52 | // client greeting 53 | let mut buf = [0; 2]; 54 | stream.read_exact(&mut buf)?; 55 | 56 | // client version ? 57 | if buf[0] != protocol::VERSION { 58 | return Err(Error::UnsupportedVersion(buf[0])); 59 | } 60 | 61 | let nb_auth = buf[1]; 62 | 63 | // client proposed authentication methods 64 | let mut buf = vec![0; nb_auth as usize]; 65 | stream.read_exact(&mut buf)?; 66 | 67 | // server supports only 0x0 NO AUTHENTICATION 68 | if !buf.into_iter().any(|b| b == protocol::AUTHENTICATION_NONE) { 69 | return Err(Error::UnsupportedAuthentication( 70 | protocol::AUTHENTICATION_NONE, 71 | )); 72 | } 73 | 74 | // server proposes NO AUTHENTICATION 75 | let buf = [protocol::VERSION, protocol::AUTHENTICATION_NONE]; 76 | stream.write_all(&buf)?; 77 | stream.flush()?; 78 | 79 | Ok(protocol::Command::read(stream)?) 80 | } 81 | 82 | fn command_connect( 83 | mut stream: net::TcpStream, 84 | mut client_rdp: service::RdpStream<'_>, 85 | ) -> Result<(), io::Error> { 86 | let resp = protocol::Response::receive(&mut client_rdp)?; 87 | resp.answer_to_client(&mut stream)?; 88 | 89 | if !resp.is_ok() { 90 | let _ = stream.shutdown(net::Shutdown::Both); 91 | return Ok(()); 92 | } 93 | 94 | service::double_stream_copy(SERVICE_KIND, &super::SERVICE, client_rdp, stream) 95 | } 96 | 97 | fn command_bind( 98 | mut stream: net::TcpStream, 99 | mut client_rdp: service::RdpStream<'_>, 100 | ) -> Result<(), io::Error> { 101 | // for the bind operation on the backend 102 | let resp = protocol::Response::receive(&mut client_rdp)?; 103 | resp.answer_to_client(&mut stream)?; 104 | 105 | if !resp.is_ok() { 106 | let _ = stream.shutdown(net::Shutdown::Both); 107 | return Ok(()); 108 | } 109 | 110 | // waiting for the connection of a client to the bounded port on the backend 111 | let resp = protocol::Response::receive(&mut client_rdp)?; 112 | resp.answer_to_client(&mut stream)?; 113 | 114 | if !resp.is_ok() { 115 | let _ = stream.shutdown(net::Shutdown::Both); 116 | return Ok(()); 117 | } 118 | 119 | service::double_stream_copy(SERVICE_KIND, &super::SERVICE, client_rdp, stream) 120 | } 121 | 122 | pub(crate) fn tcp_handler( 123 | _server: &service::TcpFrontendServer, 124 | _scope: &thread::Scope, 125 | mut stream: net::TcpStream, 126 | channel: &service::Channel, 127 | ) -> Result<(), io::Error> { 128 | match handshake(&mut stream) { 129 | Err(e) => match e { 130 | Error::Io(e) => Err(e), 131 | Error::UnsupportedVersion(_) => { 132 | let buf = [protocol::VERSION, 0xFF]; 133 | stream.write_all(&buf)?; 134 | stream.flush()?; 135 | Ok(()) 136 | } 137 | Error::UnsupportedAuthentication(_) => { 138 | let buf = [protocol::VERSION, 0xFF]; 139 | stream.write_all(&buf)?; 140 | stream.flush()?; 141 | Ok(()) 142 | } 143 | Error::UnsupportedCommand(_) => { 144 | let buf = [ 145 | protocol::VERSION, 146 | 0x07, 147 | 0x00, 148 | 0x01, 149 | 0x00, 150 | 0x00, 151 | 0x00, 152 | 0x00, 153 | 0x00, 154 | 0x00, 155 | ]; 156 | stream.write_all(&buf)?; 157 | stream.flush()?; 158 | Ok(()) 159 | } 160 | Error::AddressTypeNotSupported(_) => { 161 | let buf = [ 162 | protocol::VERSION, 163 | 0x08, 164 | 0x00, 165 | 0x01, 166 | 0x00, 167 | 0x00, 168 | 0x00, 169 | 0x00, 170 | 0x00, 171 | 0x00, 172 | ]; 173 | stream.write_all(&buf)?; 174 | stream.flush()?; 175 | Ok(()) 176 | } 177 | }, 178 | Ok(command) => { 179 | let mut client_rdp = channel.connect(&super::SERVICE)?; 180 | 181 | command.send(&mut client_rdp)?; 182 | 183 | match command { 184 | protocol::Command::Connect(_) => command_connect(stream, client_rdp), 185 | protocol::Command::Bind => command_bind(stream, client_rdp), 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /common/src/socks5/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::service; 2 | 3 | #[cfg(feature = "backend")] 4 | mod backend; 5 | #[cfg(feature = "frontend")] 6 | mod frontend; 7 | mod protocol; 8 | 9 | pub(crate) static SERVICE: service::Service = service::Service { 10 | name: "socks5", 11 | #[cfg(feature = "frontend")] 12 | tcp_frontend: Some(service::TcpFrontend { 13 | default_port: 1080, 14 | handler: frontend::tcp_handler, 15 | }), 16 | #[cfg(feature = "backend")] 17 | backend: service::Backend { 18 | handler: backend::handler, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /common/src/socks5/protocol.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | #[cfg(feature = "frontend")] 3 | use std::net; 4 | 5 | #[cfg(feature = "frontend")] 6 | pub const VERSION: u8 = 0x05; 7 | #[cfg(feature = "frontend")] 8 | pub const AUTHENTICATION_NONE: u8 = 0x00; 9 | 10 | const ID_CMD_CONNECT: u8 = 0x01; 11 | const ID_CMD_BIND: u8 = 0x02; 12 | 13 | #[cfg(feature = "frontend")] 14 | pub enum Error { 15 | Io(io::Error), 16 | UnsupportedVersion(u8), 17 | UnsupportedCommand(u8), 18 | AddressTypeNotSupported(u8), 19 | } 20 | 21 | #[cfg(feature = "frontend")] 22 | impl From for Error { 23 | fn from(e: io::Error) -> Self { 24 | Self::Io(e) 25 | } 26 | } 27 | 28 | #[derive(Debug)] 29 | pub enum Command { 30 | Connect(String), 31 | Bind, 32 | } 33 | 34 | impl Command { 35 | #[cfg(feature = "frontend")] 36 | pub(crate) fn read(reader: &mut R) -> Result 37 | where 38 | R: io::Read, 39 | { 40 | let mut buf = [0; 4]; 41 | reader.read_exact(&mut buf)?; 42 | 43 | if buf[0] != VERSION { 44 | let ret = buf[0]; 45 | //let buf = [buf[0], 0x07, 0x00]; 46 | //self.stream.write_all(&buf)?; 47 | //self.stream.flush()?; 48 | return Err(Error::UnsupportedVersion(ret)); 49 | } 50 | 51 | // server reserved byte must be 0 52 | /* 53 | if buf[2] != 0x00 { 54 | todo!("invalid reserved field value (!= 0)") 55 | } 56 | */ 57 | 58 | let dest: String = match buf[3] { 59 | // ipv4 60 | 0x01 => { 61 | let mut buf = [0x0; 4]; 62 | reader.read_exact(&mut buf)?; 63 | let ip = u32::from_be_bytes(buf); 64 | let ip = net::Ipv4Addr::from_bits(ip); 65 | let ip = net::IpAddr::V4(ip); 66 | 67 | let mut buf = [0x0; 2]; 68 | reader.read_exact(&mut buf)?; 69 | let port = u16::from_be_bytes(buf); 70 | 71 | crate::info!("connect to {ip}:{port}"); 72 | 73 | format!("{ip}:{port}") 74 | } 75 | // domain name 76 | 0x03 => { 77 | let mut len = [0x0; 1]; 78 | reader.read_exact(&mut len)?; 79 | let mut buf = vec![0x0; len[0] as usize]; 80 | reader.read_exact(&mut buf)?; 81 | let name = String::from_utf8_lossy(&buf).to_string(); 82 | 83 | let mut buf = [0x0; 2]; 84 | reader.read_exact(&mut buf)?; 85 | let port = u16::from_be_bytes(buf); 86 | 87 | crate::info!("connect to {name}:{port}",); 88 | 89 | format!("{name}:{port}") 90 | } 91 | // ipv6 92 | 0x04 => { 93 | let mut buf = [0x0; 16]; 94 | reader.read_exact(&mut buf)?; 95 | let ip = u128::from_be_bytes(buf); 96 | let ip = net::Ipv6Addr::from_bits(ip); 97 | let ip = net::IpAddr::V6(ip); 98 | 99 | let mut buf = [0x0; 2]; 100 | reader.read_exact(&mut buf)?; 101 | let port = u16::from_be_bytes(buf); 102 | 103 | crate::info!("connect to {ip}:{port}",); 104 | 105 | format!("{ip}:{port}") 106 | } 107 | t => return Err(Error::AddressTypeNotSupported(t)), 108 | }; 109 | 110 | crate::trace!("READ {buf:?}"); 111 | 112 | match buf[1] { 113 | // CONNECT 114 | 0x01 => Ok(Self::Connect(dest)), 115 | 116 | // BIND 117 | 0x02 => Ok(Self::Bind), 118 | 119 | c => Err(Error::UnsupportedCommand(c)), 120 | } 121 | } 122 | 123 | #[cfg(feature = "frontend")] 124 | pub(crate) fn send(&self, stream: &mut W) -> Result<(), io::Error> 125 | where 126 | W: io::Write, 127 | { 128 | match self { 129 | Self::Connect(to_tcp) => { 130 | let buf = [ID_CMD_CONNECT; 1]; 131 | stream.write_all(&buf)?; 132 | 133 | let len = u32::try_from(to_tcp.len()) 134 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 135 | stream.write_all(&len.to_le_bytes())?; 136 | 137 | stream.write_all(to_tcp.as_bytes())?; 138 | } 139 | Self::Bind => { 140 | let buf = [ID_CMD_BIND; 1]; 141 | stream.write_all(&buf)?; 142 | } 143 | } 144 | stream.flush() 145 | } 146 | 147 | #[cfg(feature = "backend")] 148 | pub(crate) fn receive(stream: &mut R) -> Result 149 | where 150 | R: io::Read, 151 | { 152 | let mut buf = [0u8; 1]; 153 | stream.read_exact(&mut buf)?; 154 | 155 | match buf[0] { 156 | ID_CMD_CONNECT => { 157 | let mut buf = [0u8; 4]; 158 | stream.read_exact(&mut buf)?; 159 | let len = u32::from_le_bytes(buf); 160 | 161 | let mut buf = vec![0u8; len as usize]; 162 | stream.read_exact(&mut buf)?; 163 | let to_tcp = String::from_utf8_lossy(&buf).to_string(); 164 | 165 | Ok(Self::Connect(to_tcp)) 166 | } 167 | ID_CMD_BIND => Ok(Self::Bind), 168 | v => unimplemented!("unsupported socks command {v}"), 169 | } 170 | } 171 | } 172 | 173 | const ID_RESP_OK: u8 = 0x00; 174 | const ID_RESP_NETWORK_UNREACHABLE: u8 = 0x01; 175 | const ID_RESP_HOST_UNREACHABLE: u8 = 0x02; 176 | const ID_RESP_CONNECTION_REFUSED: u8 = 0x03; 177 | const ID_RESP_BIND_FAILED: u8 = 0x04; 178 | 179 | #[derive(Debug)] 180 | pub enum Response { 181 | Ok(Vec), 182 | NetworkUnreachable, 183 | HostUnreachable, 184 | ConnectionRefused, 185 | BindFailed, 186 | } 187 | 188 | #[cfg(feature = "frontend")] 189 | const RSP_OK: u8 = 0x00; 190 | #[cfg(feature = "frontend")] 191 | const RSP_GENERAL_SOCKS_SERVER_FAILURE: u8 = 0x01; 192 | //const RSP_CONNECTION_NOT_ALLOWED: u8 = 0x02; 193 | #[cfg(feature = "frontend")] 194 | const RSP_NETWORK_UNREACHABLE: u8 = 0x03; 195 | #[cfg(feature = "frontend")] 196 | const RSP_HOST_UNREACHABLE: u8 = 0x04; 197 | #[cfg(feature = "frontend")] 198 | const RSP_CONNECTION_REFUSED: u8 = 0x05; 199 | //const RSP_TTL_EXPIRED: u8 = 0x06; 200 | //const RSP_COMMAND_NOT_SUPPORTED: u8 = 0x07; 201 | //const RSP_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08; 202 | 203 | impl Response { 204 | #[cfg(feature = "frontend")] 205 | pub const fn is_ok(&self) -> bool { 206 | matches!(self, Self::Ok(_)) 207 | } 208 | 209 | #[cfg(feature = "frontend")] 210 | pub(crate) fn answer_to_client(&self, writer: &mut W) -> Result<(), io::Error> 211 | where 212 | W: io::Write, 213 | { 214 | match self { 215 | Self::NetworkUnreachable => { 216 | let buf = [ 217 | VERSION, 218 | RSP_NETWORK_UNREACHABLE, 219 | 0x00, 220 | 0x01, 221 | 0x00, 222 | 0x00, 223 | 0x00, 224 | 0x00, 225 | 0x00, 226 | 0x00, 227 | ]; 228 | writer.write_all(&buf)?; 229 | } 230 | Self::HostUnreachable => { 231 | let buf = [ 232 | VERSION, 233 | RSP_HOST_UNREACHABLE, 234 | 0x00, 235 | 0x01, 236 | 0x00, 237 | 0x00, 238 | 0x00, 239 | 0x00, 240 | 0x00, 241 | 0x00, 242 | ]; 243 | writer.write_all(&buf)?; 244 | } 245 | Self::ConnectionRefused => { 246 | let buf = [ 247 | VERSION, 248 | RSP_CONNECTION_REFUSED, 249 | 0x00, 250 | 0x01, 251 | 0x00, 252 | 0x00, 253 | 0x00, 254 | 0x00, 255 | 0x00, 256 | 0x00, 257 | ]; 258 | writer.write_all(&buf)?; 259 | } 260 | Self::BindFailed => { 261 | let buf = [ 262 | VERSION, 263 | RSP_GENERAL_SOCKS_SERVER_FAILURE, 264 | 0x00, 265 | 0x01, 266 | 0x00, 267 | 0x00, 268 | 0x00, 269 | 0x00, 270 | 0x00, 271 | 0x00, 272 | ]; 273 | writer.write_all(&buf)?; 274 | } 275 | Self::Ok(data) => { 276 | writer.write_all(&[VERSION, RSP_OK, 0x00])?; 277 | writer.write_all(data)?; 278 | } 279 | } 280 | writer.flush() 281 | } 282 | 283 | #[cfg(feature = "backend")] 284 | pub(crate) fn send(&self, stream: &mut W) -> Result<(), io::Error> 285 | where 286 | W: io::Write, 287 | { 288 | let (id, data) = match self { 289 | Self::Ok(data) => (ID_RESP_OK, Some(data)), 290 | Self::NetworkUnreachable => (ID_RESP_NETWORK_UNREACHABLE, None), 291 | Self::HostUnreachable => (ID_RESP_HOST_UNREACHABLE, None), 292 | Self::ConnectionRefused => (ID_RESP_CONNECTION_REFUSED, None), 293 | Self::BindFailed => (ID_RESP_BIND_FAILED, None), 294 | }; 295 | let buf = [id; 1]; 296 | stream.write_all(&buf)?; 297 | if let Some(data) = data { 298 | let len = u32::try_from(data.len()) 299 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 300 | stream.write_all(&len.to_le_bytes())?; 301 | stream.write_all(data)?; 302 | } 303 | stream.flush() 304 | } 305 | 306 | #[cfg(feature = "frontend")] 307 | pub(crate) fn receive(stream: &mut R) -> Result 308 | where 309 | R: io::Read, 310 | { 311 | let mut buf = [0u8; 1]; 312 | stream.read_exact(&mut buf)?; 313 | let code = buf[0]; 314 | 315 | match code { 316 | ID_RESP_OK => { 317 | let mut buf = [0u8; 4]; 318 | stream.read_exact(&mut buf)?; 319 | let len = u32::from_le_bytes(buf); 320 | 321 | let mut data = vec![0u8; len as usize]; 322 | stream.read_exact(&mut data)?; 323 | 324 | Ok(Self::Ok(data)) 325 | } 326 | ID_RESP_NETWORK_UNREACHABLE => Ok(Self::NetworkUnreachable), 327 | ID_RESP_HOST_UNREACHABLE => Ok(Self::HostUnreachable), 328 | ID_RESP_CONNECTION_REFUSED => Ok(Self::ConnectionRefused), 329 | ID_RESP_BIND_FAILED => Ok(Self::BindFailed), 330 | v => unimplemented!("unsupported socks response {v}"), 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /common/src/stage0/backend.rs: -------------------------------------------------------------------------------- 1 | use crate::{api, service}; 2 | use std::io::{self, Read}; 3 | 4 | pub(crate) fn handler(mut stream: service::RdpStream<'_>) -> Result<(), io::Error> { 5 | crate::debug!("starting"); 6 | 7 | crate::warn!("unexpected {} connection", super::SERVICE); 8 | 9 | let mut buf = [0; api::CHUNK_LENGTH]; 10 | let mut total = 0; 11 | 12 | loop { 13 | let read = stream.read(&mut buf)?; 14 | 15 | if read == 0 { 16 | break; 17 | } 18 | 19 | crate::trace!("{read} bytes read"); 20 | 21 | total += read; 22 | } 23 | 24 | crate::debug!("total read {total} bytes"); 25 | 26 | stream.disconnect() 27 | } 28 | -------------------------------------------------------------------------------- /common/src/stage0/frontend.rs: -------------------------------------------------------------------------------- 1 | use crate::{api, service}; 2 | use std::{ 3 | fs, 4 | io::{self, BufRead, Read, Write}, 5 | net, thread, 6 | }; 7 | 8 | pub(crate) fn tcp_handler( 9 | _server: &service::TcpFrontendServer, 10 | _scope: &thread::Scope, 11 | stream: net::TcpStream, 12 | channel: &service::Channel, 13 | ) -> Result<(), io::Error> { 14 | let lstream = stream.try_clone()?; 15 | let mut client_read = io::BufReader::new(lstream); 16 | 17 | let mut client_write = io::BufWriter::new(stream); 18 | 19 | let mut rdp = channel.connect(&super::SERVICE)?; 20 | 21 | let mut line = String::new(); 22 | 23 | let _ = client_read.read_line(&mut line)?; 24 | 25 | let cline = line 26 | .strip_suffix("\n") 27 | .ok_or(io::Error::new(io::ErrorKind::BrokenPipe, "interrupted"))?; 28 | 29 | let cline = if cline.ends_with('\r') { 30 | cline.strip_suffix('\r').unwrap() 31 | } else { 32 | cline 33 | }; 34 | 35 | let (command, args) = cline 36 | .split_once(' ') 37 | .map(|(command, args)| (command, args.to_string())) 38 | .unwrap_or((cline, String::new())); 39 | let command = command.to_uppercase(); 40 | 41 | crate::debug!("{cline:?}"); 42 | crate::trace!("COMMAND = {command:?}"); 43 | crate::trace!("ARGS = {args:?}"); 44 | 45 | match command.as_str() { 46 | "CAT" | "PUSH" | "PUT" | "SEND" | "UPLOAD" => { 47 | match fs::File::options().read(true).open(args) { 48 | Err(e) => { 49 | writeln!(client_write, "failed to open file for reading: {e}")?; 50 | } 51 | Ok(mut file) => { 52 | let mut buf = [0; api::CHUNK_LENGTH]; 53 | 54 | let mut total = 0; 55 | 56 | loop { 57 | let read = file.read(&mut buf)?; 58 | 59 | if read == 0 { 60 | break; 61 | } 62 | 63 | crate::trace!("{read} bytes read"); 64 | 65 | rdp.write_all(&buf[0..read])?; 66 | 67 | total += read; 68 | } 69 | 70 | writeln!(client_write, "file sent ({total} bytes)")?; 71 | } 72 | } 73 | } 74 | _ => writeln!(client_write, "invalid command")?, 75 | } 76 | 77 | client_write.flush()?; 78 | 79 | let _ = rdp.disconnect(); 80 | let lstream = client_read.into_inner(); 81 | let _ = lstream.shutdown(net::Shutdown::Both); 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /common/src/stage0/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::service; 2 | 3 | #[cfg(feature = "backend")] 4 | mod backend; 5 | #[cfg(feature = "frontend")] 6 | mod frontend; 7 | 8 | pub(crate) static SERVICE: service::Service = service::Service { 9 | name: "stage0", 10 | #[cfg(feature = "frontend")] 11 | tcp_frontend: Some(service::TcpFrontend { 12 | default_port: 1081, 13 | handler: frontend::tcp_handler, 14 | }), 15 | #[cfg(feature = "backend")] 16 | backend: service::Backend { 17 | handler: backend::handler, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /common/src/util.rs: -------------------------------------------------------------------------------- 1 | use network_interface::NetworkInterfaceConfig; 2 | use std::net; 3 | 4 | pub(crate) struct BestAddress { 5 | pub cidr4: Option<(net::Ipv4Addr, u8)>, 6 | pub cidr6: Option<(net::Ipv6Addr, u8)>, 7 | } 8 | 9 | pub(crate) fn find_best_address() -> Result { 10 | let interfaces = network_interface::NetworkInterface::show()?; 11 | 12 | let mut best_cidr4 = None; 13 | let mut best_cidr6 = None; 14 | 15 | for interface in interfaces { 16 | for addr in interface.addr { 17 | if addr.ip().is_loopback() || addr.ip().is_multicast() || addr.ip().is_unspecified() { 18 | continue; 19 | } 20 | 21 | if let Some(mask) = addr.netmask() { 22 | match addr.ip() { 23 | net::IpAddr::V4(ip) => match best_cidr4 { 24 | None => match mask { 25 | net::IpAddr::V4(mask) => { 26 | best_cidr4 = Some(( 27 | ip, 28 | u8::try_from(u32::from_be_bytes(mask.octets()).count_ones()) 29 | .expect("too large v4 mask"), 30 | )); 31 | } 32 | net::IpAddr::V6(_) => unreachable!(), 33 | }, 34 | Some((_, best_mask)) => { 35 | let mask_nb_ones = match mask { 36 | net::IpAddr::V4(mask) => { 37 | u8::try_from(u32::from_be_bytes(mask.octets()).count_ones()) 38 | .expect("too large v4 mask") 39 | } 40 | net::IpAddr::V6(_) => unreachable!(), 41 | }; 42 | 43 | if mask_nb_ones < best_mask { 44 | best_cidr4 = Some((ip, mask_nb_ones)); 45 | } 46 | } 47 | }, 48 | 49 | net::IpAddr::V6(ip) => match best_cidr6 { 50 | None => match mask { 51 | net::IpAddr::V6(mask) => { 52 | best_cidr6 = Some(( 53 | ip, 54 | u8::try_from(u128::from_be_bytes(mask.octets()).count_ones()) 55 | .expect("too large v6 mask"), 56 | )); 57 | } 58 | net::IpAddr::V4(_) => unreachable!(), 59 | }, 60 | Some((_, best_mask)) => { 61 | let mask_nb_ones = match mask { 62 | net::IpAddr::V6(mask) => { 63 | u8::try_from(u128::from_be_bytes(mask.octets()).count_ones()) 64 | .expect("too large v6 mask") 65 | } 66 | net::IpAddr::V4(_) => unreachable!(), 67 | }; 68 | 69 | if mask_nb_ones < best_mask { 70 | best_cidr6 = Some((ip, mask_nb_ones)); 71 | } 72 | } 73 | }, 74 | } 75 | } 76 | } 77 | } 78 | 79 | Ok(BestAddress { 80 | cidr4: best_cidr4, 81 | cidr6: best_cidr6, 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "frontend" 3 | version = "2.3.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | common = { path = "../common", features = [ "frontend" ] } 8 | crossbeam-channel = "0" 9 | dirs = "6" 10 | log = { version = "0", optional = true } 11 | serde = { version = "1", default-features = false, features = [ "derive", "std" ] } 12 | toml = { version = "0", default-features = false, features = [ "display", "parse" ] } 13 | 14 | [target.'cfg(windows)'.dependencies] 15 | windows-sys = { version = "0", features = [ 16 | "Win32_Networking_WinSock", 17 | "Win32_System_Console", 18 | "Win32_System_Ole", 19 | ] } 20 | winreg = "0" 21 | 22 | [build-dependencies] 23 | bindgen = "0" 24 | 25 | [lints.clippy] 26 | pedantic = { level = "deny", priority = -1 } 27 | must_use_candidate = "allow" 28 | enum-glob-use = "allow" 29 | missing-errors-doc = "allow" 30 | 31 | [lib] 32 | crate-type = [ "cdylib", "lib" ] 33 | name = "soxy" 34 | 35 | [profile.release] 36 | opt-level = 3 37 | debug = false 38 | strip = true 39 | lto = true 40 | codegen-units = 1 41 | 42 | [features] 43 | log = [ "common/log", "dep:log" ] 44 | -------------------------------------------------------------------------------- /frontend/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | let rdp_bindings = bindgen::Builder::default() 6 | .header("src/svc/rdp/headers.h") 7 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 8 | .default_visibility(bindgen::FieldVisibilityKind::PublicCrate) 9 | .derive_debug(false) 10 | .derive_default(true) 11 | .generate() 12 | .expect("unable to generate RDP bindings"); 13 | 14 | let citrix_bindings = bindgen::Builder::default() 15 | .header("src/svc/citrix/headers.h") 16 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 17 | .default_visibility(bindgen::FieldVisibilityKind::PublicCrate) 18 | .derive_debug(false) 19 | .derive_default(true) 20 | .generate() 21 | .expect("unable to generate Citrix bindings"); 22 | 23 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 24 | 25 | rdp_bindings 26 | .write_to_file(out_path.join("rdp_headers.rs")) 27 | .expect("could not write RDP bindings"); 28 | 29 | citrix_bindings 30 | .write_to_file(out_path.join("citrix_headers.rs")) 31 | .expect("could not write Citrix bindings"); 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/config.rs: -------------------------------------------------------------------------------- 1 | use common::service; 2 | use std::{ 3 | env, fmt, fs, 4 | io::{self, Read, Write}, 5 | string, 6 | }; 7 | 8 | pub enum Error { 9 | Deserialization(toml::de::Error), 10 | Io(io::Error), 11 | Serialization(toml::ser::Error), 12 | UnknownService(String), 13 | } 14 | 15 | impl From for Error { 16 | fn from(e: toml::de::Error) -> Self { 17 | Self::Deserialization(e) 18 | } 19 | } 20 | 21 | impl From for Error { 22 | fn from(e: io::Error) -> Self { 23 | Self::Io(e) 24 | } 25 | } 26 | 27 | impl From for Error { 28 | fn from(e: toml::ser::Error) -> Self { 29 | Self::Serialization(e) 30 | } 31 | } 32 | 33 | impl fmt::Display for Error { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 35 | match self { 36 | Self::Deserialization(e) => write!(f, "deserialization error: {e}"), 37 | Self::Io(e) => write!(f, "I/O error: {e}"), 38 | Self::Serialization(e) => write!(f, "serialization error: {e}"), 39 | Self::UnknownService(s) => write!(f, "unknown service {s:?}"), 40 | } 41 | } 42 | } 43 | 44 | fn default_log_level() -> String { 45 | #[cfg(debug_assertions)] 46 | { 47 | "DEBUG".into() 48 | } 49 | #[cfg(not(debug_assertions))] 50 | { 51 | "INFO".into() 52 | } 53 | } 54 | 55 | static LOG_FILE: &str = "soxy.log"; 56 | 57 | fn default_log_file() -> Option { 58 | let mut path = env::temp_dir(); 59 | path.push(LOG_FILE); 60 | path.to_str().map(string::ToString::to_string) 61 | } 62 | 63 | const fn default_true() -> bool { 64 | true 65 | } 66 | 67 | #[derive(serde::Deserialize, serde::Serialize)] 68 | pub(crate) struct Log { 69 | #[serde(default = "default_log_level")] 70 | level: String, 71 | #[serde(default = "default_log_file")] 72 | file: Option, 73 | } 74 | 75 | impl Default for Log { 76 | fn default() -> Self { 77 | Self { 78 | level: default_log_level(), 79 | file: None, 80 | } 81 | } 82 | } 83 | 84 | #[derive(serde::Deserialize, serde::Serialize)] 85 | pub(crate) struct Service { 86 | pub name: String, 87 | #[serde(default = "default_true")] 88 | pub enabled: bool, 89 | #[serde(default)] 90 | pub ip: Option, 91 | #[serde(default)] 92 | pub port: Option, 93 | } 94 | 95 | fn default_services() -> Vec { 96 | service::SERVICES 97 | .iter() 98 | .map(|s| Service { 99 | name: s.name().to_string(), 100 | enabled: true, 101 | ip: None, 102 | port: s.tcp_frontend().map(service::TcpFrontend::default_port), 103 | }) 104 | .collect() 105 | } 106 | 107 | static CONFIG_FILE_NAME: &str = "soxy.toml"; 108 | 109 | #[derive(serde::Deserialize, serde::Serialize)] 110 | pub(crate) struct Config { 111 | pub ip: String, 112 | #[serde(default)] 113 | pub log: Log, 114 | #[serde(default = "default_services")] 115 | pub services: Vec, 116 | } 117 | 118 | impl Default for Config { 119 | fn default() -> Self { 120 | Self { 121 | ip: "127.0.0.1".into(), 122 | log: Log::default(), 123 | services: default_services(), 124 | } 125 | } 126 | } 127 | 128 | impl Config { 129 | pub fn read() -> Result, Error> { 130 | let mut path = dirs::config_dir() 131 | .ok_or_else(|| Error::Io(io::Error::other("missing configuration directory")))?; 132 | 133 | path.push(CONFIG_FILE_NAME); 134 | 135 | common::debug!("try to read configuration file at {:?}", path.display()); 136 | 137 | if !path.exists() { 138 | return Ok(None); 139 | } 140 | 141 | common::debug!("reading configuration file"); 142 | 143 | let mut file = fs::File::options().read(true).write(false).open(path)?; 144 | 145 | let mut data = String::new(); 146 | file.read_to_string(&mut data)?; 147 | 148 | Ok(Some(Config::parse(&data)?)) 149 | } 150 | 151 | pub fn save(&self) -> Result<(), Error> { 152 | let mut path = dirs::config_dir() 153 | .ok_or_else(|| Error::Io(io::Error::other("missing configuration directory")))?; 154 | 155 | path.push(CONFIG_FILE_NAME); 156 | 157 | common::debug!("try to write configuration file at {:?}", path.display()); 158 | 159 | let mut file = fs::File::options() 160 | .read(false) 161 | .write(true) 162 | .create(true) 163 | .truncate(true) 164 | .open(path)?; 165 | 166 | Ok(write!(file, "{}", self.to_string(true)?)?) 167 | } 168 | 169 | pub fn log_level(&self) -> common::Level { 170 | common::Level::try_from(self.log.level.as_str()).unwrap_or(common::Level::Info) 171 | } 172 | 173 | pub fn log_file(&self) -> Option<&String> { 174 | self.log.file.as_ref() 175 | } 176 | 177 | fn parse(config: &str) -> Result { 178 | Ok(toml::from_str(config)?) 179 | } 180 | 181 | fn to_string(&self, pretty: bool) -> Result { 182 | if pretty { 183 | toml::to_string_pretty(self) 184 | } else { 185 | toml::to_string(self) 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /frontend/src/control.rs: -------------------------------------------------------------------------------- 1 | use crate::svc; 2 | use common::api; 3 | use std::{mem, sync, thread}; 4 | 5 | const TO_SVC_CHANNEL_SIZE: usize = 256; 6 | const FRONTEND_CHANNEL_SIZE: usize = 1; 7 | 8 | #[derive(Clone)] 9 | pub struct Control { 10 | state: sync::Arc>, 11 | frontend_input: crossbeam_channel::Receiver, 12 | frontend_output: crossbeam_channel::Sender, 13 | svc_input: crossbeam_channel::Receiver, 14 | svc_received_data: Vec, 15 | svc_output: crossbeam_channel::Sender, 16 | } 17 | 18 | impl Control { 19 | pub(crate) fn new() -> ( 20 | Self, 21 | crossbeam_channel::Sender, 22 | crossbeam_channel::Receiver, 23 | crossbeam_channel::Sender, 24 | crossbeam_channel::Receiver, 25 | ) { 26 | let (from_svc_sender, from_svc_receiver) = crossbeam_channel::bounded(1); 27 | let (to_svc_sender, to_svc_receiver) = crossbeam_channel::bounded(TO_SVC_CHANNEL_SIZE); 28 | let (from_frontend_sender, from_frontend_receiver) = 29 | crossbeam_channel::bounded(FRONTEND_CHANNEL_SIZE); 30 | let (to_frontend_sender, to_frontend_receiver) = 31 | crossbeam_channel::bounded(FRONTEND_CHANNEL_SIZE); 32 | 33 | ( 34 | Self { 35 | state: sync::Arc::new(sync::RwLock::new(svc::State::Disconnected)), 36 | frontend_input: from_frontend_receiver, 37 | frontend_output: to_frontend_sender, 38 | svc_input: from_svc_receiver, 39 | svc_received_data: Vec::with_capacity(2 * common::api::CHUNK_LENGTH), 40 | svc_output: to_svc_sender, 41 | }, 42 | from_frontend_sender, 43 | to_frontend_receiver, 44 | from_svc_sender, 45 | to_svc_receiver, 46 | ) 47 | } 48 | 49 | fn control_from_svc(&mut self) -> Result<(), crate::Error> { 50 | loop { 51 | match self.svc_input.recv()? { 52 | svc::Response::ChangeState(new_state) => { 53 | let mut state = self.state.write().unwrap(); 54 | common::info!("change state from \"{state:?}\" to \"{new_state:?}\""); 55 | *state = new_state.clone(); 56 | match new_state { 57 | svc::State::Initialized => (), 58 | svc::State::Connected(name) => { 59 | common::info!("connected to {name:?}"); 60 | self.svc_output.send(svc::Command::Open)?; 61 | } 62 | svc::State::Disconnected | svc::State::Terminated => { 63 | self.frontend_output.send(api::ChunkControl::Shutdown)?; 64 | self.svc_output.send(svc::Command::Open)?; 65 | } 66 | } 67 | } 68 | svc::Response::ReceivedData(mut data) => { 69 | common::trace!("svc -> frontend: {} bytes", data.len()); 70 | 71 | if self.svc_received_data.is_empty() { 72 | loop { 73 | match api::Chunk::can_deserialize_from(&data) { 74 | None => { 75 | self.svc_received_data.append(&mut data); 76 | break; 77 | } 78 | Some(len) => { 79 | if len == data.len() { 80 | // exactly one chunk 81 | let chunk = api::Chunk::deserialize(data)?; 82 | self.frontend_output 83 | .send(api::ChunkControl::Chunk(chunk))?; 84 | break; 85 | } 86 | 87 | // at least one chunk, maybe more 88 | // tmp contains the tail, i.e. what will 89 | // not be deserialized 90 | let mut tmp = data.split_off(len); 91 | // tmp contains data to deserialize, 92 | // remaining data are back in data 93 | mem::swap(&mut tmp, &mut data); 94 | let chunk = api::Chunk::deserialize(tmp)?; 95 | self.frontend_output.send(api::ChunkControl::Chunk(chunk))?; 96 | } 97 | } 98 | } 99 | } else { 100 | self.svc_received_data.append(&mut data); 101 | loop { 102 | match api::Chunk::can_deserialize_from(&self.svc_received_data) { 103 | None => break, 104 | Some(len) => { 105 | // tmp contains the tail, i.e. what will 106 | // not be deserialized 107 | let mut tmp = self.svc_received_data.split_off(len); 108 | // tmp contains data to deserialize, 109 | // remaining data are back in 110 | // self.svc_received_data 111 | mem::swap(&mut tmp, &mut self.svc_received_data); 112 | 113 | let chunk = api::Chunk::deserialize(tmp)?; 114 | self.frontend_output.send(api::ChunkControl::Chunk(chunk))?; 115 | } 116 | } 117 | } 118 | } 119 | } 120 | svc::Response::WriteCancelled => { 121 | common::error!("svc: write cancelled"); 122 | self.svc_output.send(svc::Command::Close)?; 123 | self.frontend_output.send(api::ChunkControl::Shutdown)?; 124 | self.svc_output.send(svc::Command::Open)?; 125 | } 126 | } 127 | } 128 | } 129 | 130 | fn control_to_svc(&self) -> Result<(), crate::Error> { 131 | loop { 132 | match self.frontend_input.recv()? { 133 | api::ChunkControl::Shutdown => { 134 | self.svc_output.send(svc::Command::Close)?; 135 | } 136 | api::ChunkControl::Chunk(chunk) => { 137 | self.svc_output.send(svc::Command::SendChunk(chunk))?; 138 | } 139 | } 140 | } 141 | } 142 | 143 | pub(crate) fn start(mut self) { 144 | let myself = self.clone(); 145 | thread::spawn(move || { 146 | if let Err(e) = myself.control_to_svc() { 147 | common::error!("control to svc error: {e}"); 148 | } 149 | common::debug!("control to svc terminated"); 150 | }); 151 | thread::spawn(move || { 152 | if let Err(e) = self.control_from_svc() { 153 | common::error!("control from svc error: {e}"); 154 | } 155 | common::debug!("control from svc terminated"); 156 | }); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /frontend/src/lib.rs: -------------------------------------------------------------------------------- 1 | use common::{api, service}; 2 | use std::{fmt, io, net, str::FromStr, sync, thread}; 3 | 4 | mod config; 5 | mod control; 6 | mod svc; 7 | #[cfg(target_os = "windows")] 8 | mod windows; 9 | 10 | pub enum Error { 11 | Api(api::Error), 12 | Config(config::Error), 13 | Io(io::Error), 14 | PipelineBroken, 15 | Svc(svc::Error), 16 | } 17 | 18 | impl fmt::Display for Error { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 20 | match self { 21 | Self::Api(e) => write!(f, "API error: {e}"), 22 | Self::Config(e) => write!(f, "configuration error: {e}"), 23 | Self::Io(e) => write!(f, "I/O error: {e}"), 24 | Self::PipelineBroken => write!(f, "broken pipeline"), 25 | Self::Svc(e) => write!(f, "virtual channel error: {e}"), 26 | } 27 | } 28 | } 29 | 30 | impl From for Error { 31 | fn from(e: api::Error) -> Self { 32 | Self::Api(e) 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(e: config::Error) -> Self { 38 | Self::Config(e) 39 | } 40 | } 41 | 42 | impl From for Error { 43 | fn from(e: io::Error) -> Self { 44 | Self::Io(e) 45 | } 46 | } 47 | 48 | impl From for Error { 49 | fn from(_e: crossbeam_channel::RecvError) -> Self { 50 | Self::PipelineBroken 51 | } 52 | } 53 | 54 | impl From> for Error { 55 | fn from(_e: crossbeam_channel::SendError) -> Self { 56 | Self::PipelineBroken 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(e: svc::Error) -> Self { 62 | Self::Svc(e) 63 | } 64 | } 65 | 66 | pub(crate) static SVC_TO_CONTROL: sync::OnceLock> = 67 | sync::OnceLock::new(); 68 | 69 | fn svc_commander(control: &crossbeam_channel::Receiver) -> Result<(), Error> { 70 | loop { 71 | match control.recv()? { 72 | svc::Command::Open => { 73 | if let Some(svc) = svc::SVC.write().unwrap().as_mut() { 74 | if let Err(e) = svc.open() { 75 | common::error!("SVC open failed: {e}"); 76 | } 77 | } else { 78 | common::error!("SVC not initialized"); 79 | } 80 | } 81 | svc::Command::SendChunk(chunk) => { 82 | if let Some(svc) = svc::SVC.read().unwrap().as_ref() { 83 | if let Err(e) = svc.write(chunk.serialized()) { 84 | common::error!("SVC write failed: {e}"); 85 | } 86 | } else { 87 | common::error!("SVC not initialized"); 88 | } 89 | } 90 | svc::Command::Close => { 91 | if let Some(svc) = svc::SVC.write().unwrap().as_mut() { 92 | if let Err(e) = svc.close() { 93 | common::error!("SVC close failed: {e}"); 94 | } 95 | } else { 96 | common::error!("SVC not initialized"); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | #[allow(clippy::missing_panics_doc)] 104 | pub fn init( 105 | frontend_channel: service::Channel, 106 | backend_to_frontend: crossbeam_channel::Receiver, 107 | ) -> Result<(), Error> { 108 | let config = match config::Config::read()? { 109 | None => { 110 | let config = config::Config::default(); 111 | config.save()?; 112 | config 113 | } 114 | Some(config) => config, 115 | }; 116 | 117 | common::init_logs(config.log_level(), config.log_file()); 118 | 119 | common::debug!("initializing frontend"); 120 | 121 | let servers = config.services.into_iter().filter(|s| s.enabled).try_fold( 122 | vec![], 123 | |mut servers, service| { 124 | let ip = net::IpAddr::from_str(&service.ip.unwrap_or(config.ip.clone())) 125 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; 126 | let port = service.port; 127 | let service = service::lookup(service.name.as_str()) 128 | .ok_or(Error::Config(config::Error::UnknownService(service.name)))?; 129 | let port = port 130 | .or(service 131 | .tcp_frontend() 132 | .map(service::TcpFrontend::default_port)) 133 | .ok_or(Error::Config(config::Error::UnknownService( 134 | service.name().to_string(), 135 | )))?; 136 | 137 | let sockaddr = net::SocketAddr::new(ip, port); 138 | let server = common::service::TcpFrontendServer::bind(service, sockaddr)?; 139 | 140 | servers.push(server); 141 | 142 | Ok::<_, Error>(servers) 143 | }, 144 | )?; 145 | 146 | thread::Builder::new() 147 | .name("frontend".into()) 148 | .spawn(move || { 149 | thread::scope(|scope| { 150 | for server in &servers { 151 | thread::Builder::new() 152 | .name(server.service().name().to_string()) 153 | .spawn_scoped(scope, || { 154 | if let Err(e) = server.start(&frontend_channel) { 155 | common::error!("{} error: {e}", server.service().name()); 156 | } else { 157 | common::debug!("{} terminated", server.service().name()); 158 | } 159 | }) 160 | .unwrap(); 161 | } 162 | 163 | if let Err(e) = 164 | frontend_channel.start(service::Kind::Frontend, &backend_to_frontend) 165 | { 166 | common::error!("frontend error: {e}"); 167 | } else { 168 | common::debug!("frontend terminated"); 169 | } 170 | }); 171 | }) 172 | .unwrap(); 173 | 174 | Ok(()) 175 | } 176 | 177 | pub(crate) fn start() { 178 | if SVC_TO_CONTROL.get().is_some() { 179 | return; 180 | } 181 | 182 | common::debug!("initializing frontend"); 183 | 184 | let ( 185 | control, 186 | frontend_to_svc_send, 187 | svc_to_frontend_receive, 188 | control_to_svc_send, 189 | control_to_svc_receive, 190 | ) = control::Control::new(); 191 | 192 | SVC_TO_CONTROL.get_or_init(|| control_to_svc_send); 193 | 194 | thread::Builder::new() 195 | .name("svc_commander".into()) 196 | .spawn(move || { 197 | if let Err(e) = svc_commander(&control_to_svc_receive) { 198 | common::error!("svc_commander error: {e}"); 199 | } 200 | common::debug!("svc_commander terminated"); 201 | }) 202 | .unwrap(); 203 | 204 | let services = service::Channel::new(frontend_to_svc_send); 205 | 206 | if let Err(e) = init(services, svc_to_frontend_receive) { 207 | common::error!("init error: {e}"); 208 | } 209 | 210 | thread::Builder::new() 211 | .name("control".into()) 212 | .spawn(|| control.start()) 213 | .unwrap(); 214 | } 215 | -------------------------------------------------------------------------------- /frontend/src/svc/citrix/headers.h: -------------------------------------------------------------------------------- 1 | typedef unsigned short IU16; 2 | typedef IU16 USHORT; 3 | 4 | 5 | typedef void *PVOID; 6 | typedef void *LPVOID; 7 | 8 | typedef unsigned char IU8; 9 | typedef IU8 UCHAR; 10 | typedef UCHAR *PUCHAR; 11 | 12 | typedef IU8 BYTE; 13 | typedef BYTE *LPBYTE; 14 | 15 | #if defined(_WIN32) 16 | typedef unsigned int IU32; 17 | #else 18 | typedef unsigned int IU32; 19 | #endif 20 | 21 | typedef IU32 ULONG; 22 | 23 | #define TYPEDEF_NATURAL int 24 | 25 | typedef unsigned TYPEDEF_NATURAL UINT; 26 | 27 | #define TYPEDEF_16BITS short 28 | 29 | typedef unsigned TYPEDEF_16BITS UINT16; 30 | typedef unsigned TYPEDEF_16BITS *PUINT16; 31 | 32 | #define TYPEDEF_32BITS int 33 | 34 | typedef unsigned TYPEDEF_32BITS UINT32; 35 | 36 | typedef UINT32 DWORD; 37 | 38 | 39 | 40 | 41 | typedef PVOID HND; 42 | 43 | 44 | 45 | 46 | 47 | 48 | const int DLL_MODULE_NAME_MAX_SIZE = 14; 49 | 50 | typedef int (PDLLPROCEDURE)(PVOID, PVOID, PUINT16); 51 | 52 | typedef struct _DLLLINK { 53 | USHORT unused1; 54 | USHORT DllSize; 55 | USHORT ProcCount; 56 | PVOID pProcedures; 57 | PVOID pData; 58 | PUCHAR unused2; 59 | BYTE ModuleName[DLL_MODULE_NAME_MAX_SIZE]; 60 | LPVOID pLibMgrCallTable; 61 | USHORT ModuleDate; 62 | USHORT ModuleTime; 63 | ULONG ModuleSize; 64 | struct _DLLLINK * pNext; 65 | ULONG DllFlags; 66 | const char *DllLoad; 67 | HND LibraryHandle; 68 | } DLLLINK, *PDLLLINK; 69 | 70 | typedef struct _VD { 71 | ULONG ChannelMask; 72 | PDLLLINK pWdLink; 73 | int LastError; 74 | PVOID pPrivate; 75 | } VD, *PVD; 76 | 77 | typedef UINT32 (*PLIBPROCEDURE)(void); 78 | 79 | typedef struct _VDOPEN { 80 | PDLLLINK pVdLink; 81 | PDLLLINK pWdLink; 82 | ULONG ChannelMask; 83 | PLIBPROCEDURE reserved2; 84 | PLIBPROCEDURE pfnStatusMsgProc; 85 | HND hICAEng; 86 | } VDOPEN, *PVDOPEN; 87 | 88 | typedef struct _DLLCLOSE { 89 | int NotUsed; 90 | } DLLCLOSE, *PDLLCLOSE; 91 | 92 | typedef struct _DLLINFO { 93 | LPBYTE pBuffer; 94 | USHORT ByteCount; 95 | } DLLINFO, *PDLLINFO; 96 | 97 | typedef struct _DLLPOLL { 98 | ULONG CurrentTime; 99 | } DLLPOLL, * PDLLPOLL; 100 | 101 | 102 | 103 | 104 | typedef enum _VDINFOCLASS { 105 | #ifndef unix 106 | VdLastError, 107 | #endif /* unix */ 108 | VdKillFocus, 109 | VdSetFocus, 110 | #ifndef unix 111 | VdMousePosition, 112 | #endif /* unix */ 113 | VdThinWireCache, 114 | VdWinCEClipboardCheck, 115 | VdDisableModule, 116 | VdFlush, 117 | VdInitWindow, 118 | VdDestroyWindow, 119 | #ifndef unix 120 | VdPaint, 121 | #endif /* unix */ 122 | VdThinwireStack, 123 | VdRealizePaletteFG, 124 | VdRealizePaletteBG, 125 | VdInactivate, 126 | #ifndef unix 127 | VdGetSecurityAccess, 128 | VdSetSecurityAccess, 129 | VdGetAudioSecurityAccess, 130 | VdSetAudioSecurityAccess, 131 | #endif /* unix */ 132 | VdGetPDASecurityAccess, 133 | VdSetPDASecurityAccess, 134 | #ifndef unix 135 | VdGetTWNSecurityAccess, 136 | VdSetTWNSecurityAccess, 137 | #endif /* unix */ 138 | VdSendLogoff, 139 | VdCCShutdown, 140 | 141 | VdSeamlessHostCommand, 142 | VdSeamlessQueryInformation, 143 | 144 | VdWindowSwitch, 145 | VdSetCursor, 146 | VdSetCursorPos, 147 | 148 | VdEnableState, 149 | 150 | VdIcaControlCommand, 151 | 152 | #ifndef unix 153 | VdVirtualChannel, 154 | VdWorkArea, 155 | #endif /* unix */ 156 | VdSupportHighThroughput, 157 | #ifndef unix 158 | VdRenderingMode, 159 | #endif /* unix */ 160 | VdPauseResume, 161 | 162 | VdRedrawNotify, 163 | VdDisplayCaps, 164 | VdICOSeamlessFunctions, 165 | VdPnP, 166 | 167 | VdEuemStartupTimes, 168 | 169 | VdEuemTwCallback, 170 | 171 | VdResizeHotBitmapCache, 172 | VdSetMonitorLayout, 173 | VdGUSBGainFocus, 174 | VdGUSBLoseFocus, 175 | 176 | VdRtpConnectionEstablished, 177 | VdRtpFinalHandshakeReady, 178 | VdDimRequest, 179 | VdGBufferValidateConnection, 180 | VdCTXIMESendCommand, 181 | VdMTCommand, 182 | VdTransportDisconnect, 183 | VdTransportReconnect, 184 | VdTransportSwitch, 185 | VdCamMetrics, 186 | VdCamStatus, 187 | VdNoiseSuppressionLevel, 188 | VdEuemQueryLastRoundtripMs, 189 | VdCsiMetrics 190 | } VDINFOCLASS; 191 | 192 | 193 | 194 | 195 | 196 | typedef struct _VDQUERYINFORMATION { 197 | VDINFOCLASS VdInformationClass; 198 | LPVOID pVdInformation; 199 | int VdInformationLength; 200 | int VdReturnLength; 201 | } VDQUERYINFORMATION, *PVDQUERYINFORMATION; 202 | 203 | 204 | typedef struct _VDSETINFORMATION { 205 | VDINFOCLASS VdInformationClass; 206 | LPVOID pVdInformation; 207 | int VdInformationLength; 208 | } VDSETINFORMATION, * PVDSETINFORMATION; 209 | 210 | 211 | 212 | 213 | 214 | const USHORT VDxCOUNT = 8; 215 | 216 | 217 | 218 | 219 | 220 | const USHORT FLUSH_IMMEDIATELY = 0; 221 | 222 | typedef struct _MEMORY_SECTION { 223 | UINT length; 224 | LPBYTE pSection; 225 | } MEMORY_SECTION, *LPMEMORY_SECTION; 226 | 227 | typedef enum _ICA_TRANSPORT_RELIABILITY_ { 228 | IcaTransportReliable = 0, 229 | IcaTransportUnreliable, 230 | IcaTransportReliableBasicFec, 231 | IcaTransportCount 232 | } ICA_TRANSPORT_RELIABILITY, *PICA_TRANSPORT_RELIABILITY; 233 | 234 | typedef int (*PQUEUEVIRTUALWRITEPROC) (LPVOID, USHORT, LPMEMORY_SECTION, USHORT, USHORT); 235 | typedef int (*PQUEUEVIRTUALWRITEPROCQOS) (LPVOID, USHORT, LPMEMORY_SECTION, USHORT, USHORT, ICA_TRANSPORT_RELIABILITY eReliability, UINT32 *Ticket); 236 | 237 | typedef int (*PVDWRITEPROCEDURE)(LPVOID, USHORT, LPBYTE, USHORT); 238 | typedef int (*PVDWRITEPROCEDUREQOS)(LPVOID, USHORT, LPBYTE, USHORT, DWORD, PVOID); 239 | typedef void (*PCBNOTIFTRANSUPDATETOVDCAM)(void); 240 | 241 | typedef struct _OUTBUF { 242 | struct _OUTBUF * pLink; 243 | LPBYTE pMemory; 244 | LPBYTE pBuffer; 245 | USHORT MaxByteCount; 246 | USHORT ByteCount; 247 | ICA_TRANSPORT_RELIABILITY eReliability; 248 | UINT32 *pTicket; 249 | } OUTBUF, *POUTBUF; 250 | 251 | typedef enum _SETINFOCLASS { 252 | CallbackComplete 253 | } SETINFOCLASS, *PSETINFOCLASS; 254 | 255 | typedef enum _QUERYINFOCLASS { 256 | QueryHostVersion, 257 | OpenVirtualChannel 258 | } QUERYINFOCLASS, *PQUERYINFOCLASS; 259 | 260 | typedef int (*POUTBUFALLOCPROC)(LPVOID, POUTBUF *); 261 | typedef void (*POUTBUFFREEPROC)(LPVOID, POUTBUF); 262 | typedef int (*PPROCESSINPUTPROC)(LPVOID, LPBYTE, USHORT, int); 263 | typedef int (*PSETINFOPROC)(LPVOID, SETINFOCLASS, LPBYTE, USHORT); 264 | typedef void (*PIOHOOKPROC)(LPBYTE, USHORT); 265 | 266 | typedef int (*PQUERYINFOPROC)(LPVOID, QUERYINFOCLASS, LPBYTE, USHORT); 267 | typedef int (*POUTBUFRESERVEPROC)(LPVOID, USHORT); 268 | typedef int (*POUTBUFAPPENDPROC)(LPVOID, LPBYTE, USHORT); 269 | typedef int (*POUTBUFWRITEPROC)(LPVOID); 270 | typedef int (*PAPPENDVDHEADERPROC)(LPVOID, USHORT, USHORT); 271 | 272 | typedef struct _VDWRITEHOOK { 273 | USHORT Type; 274 | LPVOID pVdData; 275 | union { 276 | PVDWRITEPROCEDURE pProc; 277 | PVDWRITEPROCEDUREQOS pProcQos; 278 | }; 279 | LPVOID pWdData; 280 | POUTBUFRESERVEPROC pOutBufReserveProc; 281 | POUTBUFAPPENDPROC pOutBufAppendProc; 282 | POUTBUFWRITEPROC pOutBufWriteProc; 283 | PAPPENDVDHEADERPROC pAppendVdHeaderProc; 284 | USHORT MaximumWriteSize; 285 | union { 286 | PQUEUEVIRTUALWRITEPROC pQueueVirtualWriteProc; 287 | PQUEUEVIRTUALWRITEPROCQOS pQueueVirtualWriteProcQos; 288 | }; 289 | } VDWRITEHOOK, *PVDWRITEHOOK; 290 | 291 | typedef struct _WD * PWD; 292 | 293 | const unsigned WDxQUERYINFORMATION = 6; 294 | const unsigned WDxSETINFORMATION = 7; 295 | const unsigned WDxCOUNT = 8; 296 | 297 | typedef struct _OPENVIRTUALCHANNEL { 298 | LPVOID pVCName; 299 | USHORT Channel; 300 | } OPENVIRTUALCHANNEL, *POPENVIRTUALCHANNEL; 301 | 302 | typedef enum _WDINFOCLASS { 303 | WdClientData, 304 | WdStatistics, 305 | WdLastError, 306 | WdConnect, 307 | WdDisconnect, 308 | WdKillFocus, 309 | WdSetFocus, 310 | WdEnablePassThrough, 311 | WdDisablePassThrough, 312 | WdVdAddress, 313 | WdVirtualWriteHook, 314 | WdAddReadHook, 315 | WdRemoveReadHook, 316 | WdAddWriteHook, 317 | WdRemoveWriteHook, 318 | WdModemStatus, 319 | WdXferBufferSize, 320 | WdCharCode, 321 | WdScanCode, 322 | WdMouseInfo, 323 | WdInitWindow, 324 | WdDestroyWindow, 325 | WdRedraw, 326 | WdThinwireStack, 327 | WdHostVersion, 328 | WdRealizePaletteFG, 329 | WdRealizePaletteBG, 330 | WdInactivate, 331 | WdSetProductID, 332 | WdGetTerminateInfo, 333 | WdRaiseSoftkey, 334 | WdLowerSoftkey, 335 | WdIOStatus, 336 | WdOpenVirtualChannel, 337 | WdCache, 338 | WdGetInfoData, 339 | WdWindowSwitch, 340 | #if defined(UNICODESUPPORT) || defined(USE_EUKS) 341 | WdUnicodeCode, 342 | #endif 343 | #ifdef PACKET_KEYSYM_SUPPORT 344 | WdKeysymCode, 345 | #endif 346 | #if defined(_WIN32) 347 | WdSetNetworkEvent, 348 | #endif 349 | WdPassThruLogoff, 350 | WdClientIdInfo, 351 | WdPartialDisconnect, 352 | WdDesktopInfo, 353 | WdSeamlessHostCommand, 354 | WdSeamlessQueryInformation, 355 | #if defined(UNICODESUPPORT) || defined(USE_EUKS) 356 | WdZlRegisterUnicodeHook, 357 | WdZLUnRegisterUnicodeHook, 358 | #endif 359 | WdZLRegisterScanCodeHook, 360 | WdZlUnregisterScanCodeHook, 361 | WdIcmQueryStatus, 362 | WdIcmSendPingRequest, 363 | WdSetCursor, 364 | WdFullScreenMode, 365 | WdFullScreenPaint, 366 | WdSeamlessInfo, 367 | WdCodePage, 368 | WdIcaControlCommand, 369 | WdReconnectInfo, 370 | WdServerSupportBWControl4Printing, 371 | WdVirtualChannel, 372 | WdGetLatencyInformation, 373 | WdKeyboardInput, 374 | WdMouseInput, 375 | WdCredentialPassing, 376 | WdRenderingMode, 377 | WdPauseResume, 378 | WdQueryMMWindowInfo, 379 | WdGetICAWindowInfo, 380 | WdICOSeamlessFunctions, 381 | #ifdef USE_EUKS 382 | WdEUKSVersion, 383 | #endif 384 | WdSetC2HPriority, 385 | WdPnP, 386 | WdEuemEndSLCD, 387 | WdEuemStartupTimes, 388 | WdEuemTwCallback, 389 | WdSessionIsReconnected, 390 | WdUserActivity, 391 | #ifdef WINCE 392 | WdEuemApplicationName, 393 | #endif 394 | WdLicensedFeatures, 395 | WdResizeHotBitmapCache, 396 | WdLockDisplay, 397 | WdRtpSetupInformation, 398 | WdRtpInitClientHandshake, 399 | WdRtpSetup, 400 | WdQueryVCNumbersForVD, 401 | WDCheckOutTicket, 402 | WDCheckInTicket, 403 | WdMarshallVdInfo, 404 | WdVirtualWriteHookQos, 405 | WdQueryEdt, 406 | WdQueryMaxUnreliablePayload, 407 | WdSubscribeDesktopInfoChange, 408 | WdUnsubscribeDesktopInfoChange, 409 | WdSendMTCommand, 410 | WdUpdateMonitorLayout, 411 | WdSubscribeMonitorLayoutChange, 412 | WdUnsubscribeMonitorLayoutChange, 413 | } WDINFOCLASS; 414 | 415 | #define MAX_ERRORMESSAGE 288 416 | 417 | typedef struct _VDLASTERROR { 418 | int Error; 419 | char Message[MAX_ERRORMESSAGE]; 420 | } VDLASTERROR, * PVDLASTERROR; 421 | 422 | typedef struct _WDQUERYINFORMATION { 423 | WDINFOCLASS WdInformationClass; 424 | LPVOID pWdInformation; 425 | USHORT WdInformationLength; 426 | USHORT WdReturnLength; 427 | } WDQUERYINFORMATION, *PWDQUERYINFORMATION; 428 | 429 | typedef struct _WDSETINFORMATION { 430 | WDINFOCLASS WdInformationClass; 431 | LPVOID pWdInformation; 432 | USHORT WdInformationLength; 433 | } WDSETINFORMATION, * PWDSETINFORMATION; 434 | 435 | 436 | 437 | 438 | 439 | #pragma pack(1) 440 | 441 | 442 | 443 | 444 | typedef enum _MODULECLASS { 445 | Module_UserInterface, 446 | Module_UserInterfaceExt, 447 | Module_WinstationDriver, 448 | Module_VirtualDriver, 449 | Module_ProtocolDriver, 450 | Module_TransportDriver, 451 | Module_NameResolver, 452 | Module_NameEnumerator, 453 | Module_Scripting, 454 | Module_SubDriver, 455 | ModuleClass_Maximum 456 | } MODULECLASS; 457 | 458 | typedef struct _MODULE_C2H { 459 | USHORT ByteCount; 460 | BYTE ModuleCount; 461 | BYTE ModuleClass; 462 | BYTE VersionL; 463 | BYTE VersionH; 464 | BYTE ModuleName[13]; 465 | BYTE HostModuleName[9]; 466 | USHORT ModuleDate; 467 | USHORT ModuleTime; 468 | ULONG ModuleSize; 469 | } MODULE_C2H, *PMODULE_C2H; 470 | 471 | typedef enum _VIRTUALFLOWCLASS { 472 | VirtualFlow_None, 473 | VirtualFlow_Ack, 474 | VirtualFlow_Delay, 475 | VirtualFlow_Cdm 476 | } VIRTUALFLOWCLASS; 477 | 478 | typedef struct _VDFLOWACK { 479 | USHORT MaxWindowSize; 480 | USHORT WindowSize; 481 | } VDFLOWACK, *PVDFLOWACK; 482 | 483 | typedef struct _VDFLOWDELAY { 484 | ULONG DelayTime; 485 | } VDFLOWDELAY, *PVDFLOWDELAY; 486 | 487 | typedef struct _VDFLOWCDM { 488 | USHORT MaxWindowSize; 489 | USHORT MaxByteCount; 490 | } VDFLOWCDM, *PVDFLOWCDM; 491 | 492 | typedef struct _VDFLOW { 493 | BYTE BandwidthQuota; 494 | BYTE Flow; 495 | BYTE Pad1[2]; 496 | union _VDFLOWU { 497 | VDFLOWACK Ack; 498 | VDFLOWDELAY Delay; 499 | VDFLOWCDM Cdm; 500 | } VDFLOWU ; 501 | } VDFLOW, *PVDFLOW; 502 | 503 | typedef struct _VD_C2H { 504 | MODULE_C2H Header; 505 | ULONG ChannelMask; 506 | VDFLOW Flow; 507 | } VD_C2H, *PVD_C2H; 508 | 509 | typedef struct { 510 | VD_C2H Header; 511 | } SOXY_C2H, *PSOXY_C2H; 512 | 513 | 514 | 515 | #pragma pack() 516 | 517 | 518 | 519 | 520 | 521 | 522 | const int CLIENT_STATUS_SUCCESS = 0; 523 | const int CLIENT_STATUS_ERROR_RETRY = 30; 524 | 525 | const int CLIENT_ERROR = 1000; 526 | const int CLIENT_ERROR_BUFFER_TOO_SMALL = 1004; 527 | const int CLIENT_ERROR_NULL_MEM_POINTER = 1011; 528 | const int CLIENT_ERROR_NO_OUTBUF = 1016; 529 | -------------------------------------------------------------------------------- /frontend/src/svc/citrix/headers.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(unused)] 5 | #![allow(clippy::upper_case_acronyms)] 6 | #![allow(clippy::identity_op)] 7 | 8 | include!(concat!(env!("OUT_DIR"), "/citrix_headers.rs")); 9 | -------------------------------------------------------------------------------- /frontend/src/svc/citrix/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_safety_doc)] 2 | #![allow(clippy::missing_panics_doc)] 3 | #![allow(clippy::cast_possible_wrap)] 4 | #![allow(non_snake_case)] 5 | 6 | use common::api; 7 | use std::{ffi, fmt, mem, ptr, slice, sync}; 8 | 9 | mod headers; 10 | mod vd; 11 | 12 | pub enum Error { 13 | NullPointers, 14 | InvalidMaximumWriteSize(u16), 15 | NotReady, 16 | Disconnected, 17 | VirtualChannel(i32), 18 | } 19 | 20 | impl fmt::Display for Error { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 22 | match self { 23 | Self::NullPointers => write!(f, "null pointers"), 24 | Self::InvalidMaximumWriteSize(s) => write!(f, "invalid maximum write is: {s}"), 25 | Self::NotReady => write!(f, "not ready"), 26 | Self::Disconnected => write!(f, "disconnected"), 27 | Self::VirtualChannel(e) => write!(f, "virtual channel error: {e}"), 28 | } 29 | } 30 | } 31 | 32 | static HANDLE: sync::RwLock> = sync::RwLock::new(None); 33 | 34 | struct Handle { 35 | pwd_data: headers::PWD, 36 | channel_num: headers::USHORT, 37 | queue_virtual_write: unsafe extern "C" fn( 38 | headers::LPVOID, 39 | headers::USHORT, 40 | headers::LPMEMORY_SECTION, 41 | headers::USHORT, 42 | headers::USHORT, 43 | ) -> ffi::c_int, 44 | write_last_miss: sync::RwLock>>, 45 | write_queue_send: crossbeam_channel::Sender>, 46 | write_queue_receive: crossbeam_channel::Receiver>, 47 | } 48 | 49 | unsafe impl Send for Handle {} 50 | unsafe impl Sync for Handle {} 51 | 52 | impl Handle { 53 | fn new( 54 | pwd_data: headers::PWD, 55 | channel_num: headers::USHORT, 56 | queue_virtual_write: unsafe extern "C" fn( 57 | headers::LPVOID, 58 | headers::USHORT, 59 | headers::LPMEMORY_SECTION, 60 | headers::USHORT, 61 | headers::USHORT, 62 | ) -> ffi::c_int, 63 | ) -> Self { 64 | let (write_queue_send, write_queue_receive) = 65 | crossbeam_channel::bounded(super::MAX_CHUNKS_IN_FLIGHT); 66 | Self { 67 | pwd_data, 68 | channel_num, 69 | queue_virtual_write, 70 | write_last_miss: sync::RwLock::new(None), 71 | write_queue_send, 72 | write_queue_receive, 73 | } 74 | } 75 | 76 | fn queue(&self, data: Vec) { 77 | self.write_queue_send.send(data).ok(); 78 | } 79 | } 80 | 81 | pub fn DriverOpen(vd: &mut headers::VD, vd_open: &mut headers::VDOPEN) -> Result<(), ffi::c_int> { 82 | let mut handle = HANDLE.write().unwrap(); 83 | 84 | if handle.is_some() { 85 | return Ok(()); 86 | } 87 | 88 | let mut wdovc = headers::OPENVIRTUALCHANNEL { 89 | pVCName: ptr::from_ref(common::VIRTUAL_CHANNEL_NAME) 90 | .cast_mut() 91 | .cast(), 92 | ..Default::default() 93 | }; 94 | 95 | let mut query_info = headers::WDQUERYINFORMATION { 96 | WdInformationClass: headers::_WDINFOCLASS_WdOpenVirtualChannel, 97 | pWdInformation: ptr::from_mut(&mut wdovc).cast(), 98 | WdInformationLength: u16::try_from(mem::size_of::()) 99 | .expect("value too large"), 100 | ..Default::default() 101 | }; 102 | 103 | vd::WdQueryInformation(vd, &mut query_info)?; 104 | 105 | let mask = u32::wrapping_shl(1, u32::from(wdovc.Channel)); 106 | 107 | vd_open.ChannelMask = mask; 108 | 109 | #[allow(clippy::used_underscore_items)] 110 | let mut vdwh = headers::VDWRITEHOOK { 111 | Type: wdovc.Channel, 112 | pVdData: ptr::from_mut(vd).cast(), 113 | __bindgen_anon_1: headers::_VDWRITEHOOK__bindgen_ty_1 { 114 | pProc: Some(ICADataArrival), 115 | }, 116 | ..Default::default() 117 | }; 118 | 119 | let mut set_info = headers::WDSETINFORMATION { 120 | WdInformationClass: headers::_WDINFOCLASS_WdVirtualWriteHook, 121 | pWdInformation: ptr::from_mut(&mut vdwh).cast(), 122 | WdInformationLength: u16::try_from(mem::size_of::()) 123 | .expect("value too large"), 124 | }; 125 | 126 | vd::WdSetInformation(vd, &mut set_info)?; 127 | 128 | common::debug!("maximum_write_size = {}", vdwh.MaximumWriteSize); 129 | 130 | if usize::from(vdwh.MaximumWriteSize) < (api::CHUNK_LENGTH - 1) { 131 | return Err(headers::CLIENT_ERROR_BUFFER_TOO_SMALL); 132 | } 133 | 134 | unsafe { vdwh.__bindgen_anon_2.pQueueVirtualWriteProc.as_ref() }.map_or( 135 | Err(headers::CLIENT_ERROR_NULL_MEM_POINTER), 136 | |queue_virtual_write| { 137 | let _ = handle.replace(Handle::new( 138 | vdwh.pWdData.cast(), 139 | wdovc.Channel, 140 | *queue_virtual_write, 141 | )); 142 | Ok(()) 143 | }, 144 | ) 145 | } 146 | 147 | #[allow(clippy::unnecessary_wraps)] 148 | pub fn DriverClose( 149 | _vd: &mut headers::VD, 150 | _dll_close: &mut headers::DLLCLOSE, 151 | ) -> Result<(), ffi::c_int> { 152 | let _ = HANDLE.write().unwrap().take(); 153 | Ok(()) 154 | } 155 | 156 | pub fn DriverInfo(vd: &headers::VD, dll_info: &mut headers::DLLINFO) -> Result<(), ffi::c_int> { 157 | let byte_count = u16::try_from(mem::size_of::()).expect("value too large"); 158 | 159 | if dll_info.ByteCount < byte_count { 160 | common::debug!("buffer too small: {} < {}", dll_info.ByteCount, byte_count); 161 | dll_info.ByteCount = byte_count; 162 | return Err(headers::CLIENT_ERROR_BUFFER_TOO_SMALL); 163 | } 164 | 165 | let soxy_c2h = dll_info.pBuffer.cast::(); 166 | 167 | match unsafe { soxy_c2h.as_mut() } { 168 | None => { 169 | common::error!("pBuffer is null!"); 170 | Err(headers::CLIENT_ERROR) 171 | } 172 | Some(soxy_c2h) => { 173 | let vd_c2h = &mut soxy_c2h.Header; 174 | 175 | vd_c2h.ChannelMask = vd.ChannelMask; 176 | 177 | let module_c2h = &mut vd_c2h.Header; 178 | module_c2h.ByteCount = byte_count; 179 | module_c2h.ModuleClass = 180 | u8::try_from(headers::_MODULECLASS_Module_VirtualDriver).expect("value too large"); 181 | module_c2h.VersionL = 1; 182 | module_c2h.VersionH = 1; 183 | 184 | for (i, b) in common::VIRTUAL_CHANNEL_NAME 185 | .to_bytes_with_nul() 186 | .iter() 187 | .enumerate() 188 | { 189 | module_c2h.HostModuleName[i] = *b; 190 | } 191 | 192 | let flow = &mut vd_c2h.Flow; 193 | flow.BandwidthQuota = 0; 194 | flow.Flow = 195 | u8::try_from(headers::_VIRTUALFLOWCLASS_VirtualFlow_None).expect("value too large"); 196 | 197 | dll_info.ByteCount = byte_count; 198 | 199 | Ok(()) 200 | } 201 | } 202 | } 203 | 204 | // To avoid saturating completely the Citrix queue (which is 205 | // half-duplex) during an upload from the frontend to the backend we 206 | // send at most MAX_CHUNK_BATCH_SEND chunks per poll request 207 | const MAX_CHUNK_BATCH_SEND: usize = 8; 208 | 209 | pub fn DriverPoll( 210 | _vd: &mut headers::VD, 211 | _dll_poll: &mut headers::DLLPOLL, 212 | ) -> Result<(), ffi::c_int> { 213 | let binding = HANDLE.read().unwrap(); 214 | let handle = binding.as_ref().ok_or(headers::CLIENT_ERROR)?; 215 | 216 | let mut mem = headers::MEMORY_SECTION::default(); 217 | 218 | let mut next = handle 219 | .write_last_miss 220 | .write() 221 | .unwrap() 222 | .take() 223 | .or_else(|| handle.write_queue_receive.try_recv().ok()); 224 | 225 | let mut batch_send = 0; 226 | 227 | loop { 228 | match next { 229 | None => { 230 | return Ok(()); 231 | } 232 | Some(mut data) => { 233 | common::trace!("write data ({} bytes)", data.len()); 234 | 235 | let len = u32::try_from(data.len()).expect("write error: data too large ({e})"); 236 | 237 | mem.length = len; 238 | mem.pSection = data.as_mut_ptr(); 239 | 240 | let rc = unsafe { 241 | (handle.queue_virtual_write)( 242 | handle.pwd_data.cast(), 243 | handle.channel_num, 244 | ptr::from_mut(&mut mem).cast(), 245 | 1, 246 | 0, 247 | ) 248 | }; 249 | 250 | match rc { 251 | headers::CLIENT_STATUS_SUCCESS => { 252 | batch_send += 1; 253 | 254 | if batch_send < MAX_CHUNK_BATCH_SEND { 255 | next = handle.write_queue_receive.try_recv().ok(); 256 | } else if handle.write_queue_receive.is_empty() { 257 | return Ok(()); 258 | } else { 259 | return Err(headers::CLIENT_STATUS_ERROR_RETRY); 260 | } 261 | } 262 | headers::CLIENT_ERROR_NO_OUTBUF => { 263 | common::debug!("no more space, request a retry"); 264 | handle.write_last_miss.write().unwrap().replace(data); 265 | return Err(headers::CLIENT_STATUS_ERROR_RETRY); 266 | } 267 | _ => { 268 | return Err(headers::CLIENT_ERROR); 269 | } 270 | } 271 | } 272 | } 273 | } 274 | } 275 | 276 | pub fn DriverQueryInformation( 277 | _vd: &mut headers::VD, 278 | _vd_query_info: &mut headers::VDQUERYINFORMATION, 279 | ) -> Result<(), ffi::c_int> { 280 | todo!() 281 | } 282 | 283 | #[allow(clippy::unnecessary_wraps)] 284 | pub fn DriverSetInformation( 285 | _vd: &mut headers::VD, 286 | _vd_set_info: &mut headers::VDSETINFORMATION, 287 | ) -> Result<(), ffi::c_int> { 288 | Ok(()) 289 | } 290 | 291 | /* 292 | pub fn DriverGetLastError( 293 | _vd: &mut headers::VD, 294 | _vd_last_error: &mut headers::VDLASTERROR, 295 | ) -> Result<(), ffi::c_int> { 296 | todo!() 297 | } 298 | */ 299 | 300 | extern "C" fn ICADataArrival( 301 | _pVd: headers::PVOID, 302 | _uChan: headers::USHORT, 303 | pBuf: headers::LPBYTE, 304 | Length: headers::USHORT, 305 | ) -> ffi::c_int { 306 | common::trace!("ICADataArrival"); 307 | 308 | if let Some(from_rdp) = crate::SVC_TO_CONTROL.get() { 309 | assert!( 310 | Length as usize 311 | <= (api::Chunk::serialized_overhead() + api::Chunk::max_payload_length()) 312 | ); 313 | 314 | let data = unsafe { slice::from_raw_parts(pBuf.cast::(), Length as usize) }; 315 | let data = Vec::from(data); 316 | 317 | from_rdp 318 | .send(super::Response::ReceivedData(data)) 319 | .expect("internal error: failed to send RDP message"); 320 | } 321 | 322 | headers::CLIENT_STATUS_SUCCESS 323 | } 324 | 325 | #[derive(Default)] 326 | pub struct Svc {} 327 | 328 | unsafe impl Sync for Svc {} 329 | unsafe impl Send for Svc {} 330 | 331 | impl super::SvcImplementation for Svc { 332 | #[allow(clippy::too_many_lines)] 333 | fn open(&mut self) -> Result<(), super::Error> { 334 | if HANDLE.read().unwrap().is_none() { 335 | Err(super::Error::Citrix(Error::Disconnected)) 336 | } else { 337 | Ok(()) 338 | } 339 | } 340 | 341 | fn write(&self, data: Vec) -> Result<(), super::Error> { 342 | HANDLE.read().unwrap().as_ref().map_or( 343 | Err(super::Error::Citrix(Error::Disconnected)), 344 | |handle| { 345 | handle.queue(data); 346 | Ok(()) 347 | }, 348 | ) 349 | } 350 | 351 | fn close(&mut self) -> Result<(), super::Error> { 352 | let _ = HANDLE.write().unwrap().take(); 353 | Ok(()) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /frontend/src/svc/citrix/vd.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi, mem, ptr, slice, sync}; 2 | 3 | use super::headers; 4 | 5 | struct VirtualDriver { 6 | data: headers::VD, 7 | procedures: [headers::PDLLPROCEDURE; headers::VDxCOUNT as usize], 8 | } 9 | 10 | unsafe impl Send for VirtualDriver {} 11 | unsafe impl Sync for VirtualDriver {} 12 | 13 | impl Default for VirtualDriver { 14 | fn default() -> Self { 15 | let data = headers::VD::default(); 16 | let procedures = unsafe { 17 | [ 18 | mem::transmute::< 19 | Option i32>, 20 | headers::PDLLPROCEDURE, 21 | >(Some(Load)), 22 | mem::transmute::< 23 | Option< 24 | unsafe extern "C" fn( 25 | headers::PVD, 26 | headers::PDLLLINK, 27 | headers::PUINT16, 28 | ) -> i32, 29 | >, 30 | headers::PDLLPROCEDURE, 31 | >(Some(VdUnload)), 32 | mem::transmute::< 33 | Option< 34 | unsafe extern "C" fn( 35 | headers::PVD, 36 | headers::PVDOPEN, 37 | headers::PUINT16, 38 | ) -> i32, 39 | >, 40 | headers::PDLLPROCEDURE, 41 | >(Some(VdOpen)), 42 | mem::transmute::< 43 | Option< 44 | unsafe extern "C" fn( 45 | headers::PVD, 46 | headers::PDLLCLOSE, 47 | headers::PUINT16, 48 | ) -> i32, 49 | >, 50 | headers::PDLLPROCEDURE, 51 | >(Some(VdClose)), 52 | mem::transmute::< 53 | Option< 54 | unsafe extern "C" fn( 55 | headers::PVD, 56 | headers::PDLLINFO, 57 | headers::PUINT16, 58 | ) -> i32, 59 | >, 60 | headers::PDLLPROCEDURE, 61 | >(Some(VdInfo)), 62 | mem::transmute::< 63 | Option< 64 | unsafe extern "C" fn( 65 | headers::PVD, 66 | headers::PDLLPOLL, 67 | headers::PUINT16, 68 | ) -> i32, 69 | >, 70 | headers::PDLLPROCEDURE, 71 | >(Some(VdPoll)), 72 | mem::transmute::< 73 | Option< 74 | unsafe extern "C" fn( 75 | headers::PVD, 76 | headers::PVDQUERYINFORMATION, 77 | headers::PUINT16, 78 | ) -> i32, 79 | >, 80 | headers::PDLLPROCEDURE, 81 | >(Some(VdQueryInformation)), 82 | mem::transmute::< 83 | Option< 84 | unsafe extern "C" fn( 85 | headers::PVD, 86 | headers::PVDSETINFORMATION, 87 | headers::PUINT16, 88 | ) -> i32, 89 | >, 90 | headers::PDLLPROCEDURE, 91 | >(Some(VdSetInformation)), 92 | ] 93 | }; 94 | 95 | Self { data, procedures } 96 | } 97 | } 98 | 99 | static VD: sync::OnceLock = sync::OnceLock::new(); 100 | 101 | #[unsafe(no_mangle)] 102 | extern "C" fn Load(pLink: headers::PDLLLINK) -> ffi::c_int { 103 | common::debug!("Load"); 104 | 105 | crate::start(); 106 | 107 | let vd = VD.get_or_init(VirtualDriver::default); 108 | 109 | let svc = super::Svc::default(); 110 | let svc = super::super::Svc::Citrix(svc); 111 | let _ = super::super::SVC.write().unwrap().replace(svc); 112 | 113 | match unsafe { pLink.as_mut() } { 114 | None => { 115 | common::error!("pLink is null!"); 116 | headers::CLIENT_ERROR 117 | } 118 | Some(pLink) => { 119 | pLink.ProcCount = u16::try_from(vd.procedures.len()).expect("value too large"); 120 | pLink.pProcedures = ptr::from_ref(&vd.procedures).cast_mut().cast(); 121 | pLink.pData = ptr::from_ref(&vd.data).cast_mut().cast(); 122 | 123 | headers::CLIENT_STATUS_SUCCESS 124 | } 125 | } 126 | } 127 | 128 | extern "C" fn VdUnload( 129 | _pVd: headers::PVD, 130 | pLink: headers::PDLLLINK, 131 | _puiSize: headers::PUINT16, 132 | ) -> ffi::c_int { 133 | common::debug!("VdUnload"); 134 | 135 | if let Some(pLink) = unsafe { pLink.as_mut() } { 136 | pLink.ProcCount = 0; 137 | pLink.pProcedures = ptr::null_mut(); 138 | pLink.pData = ptr::null_mut(); 139 | } 140 | 141 | let _ = super::super::SVC.write().unwrap().take(); 142 | 143 | headers::CLIENT_STATUS_SUCCESS 144 | } 145 | 146 | extern "C" fn VdOpen( 147 | pVd: headers::PVD, 148 | pVdOpen: headers::PVDOPEN, 149 | puiSize: headers::PUINT16, 150 | ) -> ffi::c_int { 151 | common::debug!("VdOpen"); 152 | 153 | match unsafe { (pVd.as_mut(), pVdOpen.as_mut(), puiSize.as_mut()) } { 154 | (None, _, _) | (_, None, _) | (_, _, None) => headers::CLIENT_ERROR, 155 | (Some(pVd), Some(pVdOpen), Some(puiSize)) => { 156 | pVd.ChannelMask = 0; 157 | pVd.pWdLink = pVdOpen.pWdLink; 158 | pVd.LastError = 0; 159 | pVd.pPrivate = ptr::null_mut(); 160 | 161 | if let Err(e) = super::DriverOpen(pVd, pVdOpen) { 162 | common::error!("DriverOpen failed: {e}"); 163 | return e; 164 | } 165 | 166 | *puiSize = u16::try_from(mem::size_of::()).expect("value too large"); 167 | 168 | pVd.ChannelMask = pVdOpen.ChannelMask; 169 | 170 | headers::CLIENT_STATUS_SUCCESS 171 | } 172 | } 173 | } 174 | 175 | extern "C" fn VdClose( 176 | pVd: headers::PVD, 177 | pDllClose: headers::PDLLCLOSE, 178 | puiSize: headers::PUINT16, 179 | ) -> ffi::c_int { 180 | common::debug!("VdClose"); 181 | 182 | match unsafe { (pVd.as_mut(), pDllClose.as_mut(), puiSize.as_mut()) } { 183 | (None, _, _) | (_, None, _) | (_, _, None) => headers::CLIENT_ERROR, 184 | (Some(pVd), Some(pDllClose), Some(puiSize)) => { 185 | if let Err(e) = super::DriverClose(pVd, pDllClose) { 186 | common::error!("DriverClose failed: {e}"); 187 | return e; 188 | } 189 | 190 | *puiSize = u16::try_from(mem::size_of::()).expect("value too large"); 191 | 192 | headers::CLIENT_STATUS_SUCCESS 193 | } 194 | } 195 | } 196 | 197 | extern "C" fn VdInfo( 198 | pVd: headers::PVD, 199 | pDllInfo: headers::PDLLINFO, 200 | puiSize: headers::PUINT16, 201 | ) -> ffi::c_int { 202 | common::debug!("VdInfo"); 203 | 204 | match unsafe { (pVd.as_mut(), pDllInfo.as_mut(), puiSize.as_mut()) } { 205 | (None, _, _) | (_, None, _) | (_, _, None) => headers::CLIENT_ERROR, 206 | (Some(pVd), Some(pDllInfo), Some(puiSize)) => { 207 | if let Err(e) = super::DriverInfo(pVd, pDllInfo) { 208 | if e != headers::CLIENT_ERROR_BUFFER_TOO_SMALL { 209 | common::error!("DriverInfo failed: {e}"); 210 | } 211 | return e; 212 | } 213 | 214 | *puiSize = u16::try_from(mem::size_of::()).expect("value too large"); 215 | 216 | headers::CLIENT_STATUS_SUCCESS 217 | } 218 | } 219 | } 220 | 221 | extern "C" fn VdPoll( 222 | pVd: headers::PVD, 223 | pDllPoll: headers::PDLLPOLL, 224 | puiSize: headers::PUINT16, 225 | ) -> ffi::c_int { 226 | common::trace!("VdPoll"); 227 | 228 | match unsafe { (pVd.as_mut(), pDllPoll.as_mut(), puiSize.as_mut()) } { 229 | (None, _, _) | (_, None, _) | (_, _, None) => headers::CLIENT_ERROR, 230 | (Some(pVd), Some(pDllPoll), Some(puiSize)) => { 231 | if let Err(e) = super::DriverPoll(pVd, pDllPoll) { 232 | if e != headers::CLIENT_STATUS_ERROR_RETRY { 233 | common::error!("DriverPoll failed: {e}"); 234 | } 235 | return e; 236 | } 237 | 238 | *puiSize = u16::try_from(mem::size_of::()).expect("value too large"); 239 | 240 | headers::CLIENT_STATUS_SUCCESS 241 | } 242 | } 243 | } 244 | 245 | extern "C" fn VdQueryInformation( 246 | pVd: headers::PVD, 247 | pVdQueryInformation: headers::PVDQUERYINFORMATION, 248 | puiSize: headers::PUINT16, 249 | ) -> ffi::c_int { 250 | common::debug!("VdQueryInformation"); 251 | 252 | match unsafe { (pVd.as_mut(), pVdQueryInformation.as_mut(), puiSize.as_mut()) } { 253 | (None, _, _) | (_, None, _) | (_, _, None) => headers::CLIENT_ERROR, 254 | (Some(pVd), Some(pVdQueryInformation), Some(puiSize)) => { 255 | if let Err(e) = super::DriverQueryInformation(pVd, pVdQueryInformation) { 256 | common::error!("DriverQueryInformation failed: {e}"); 257 | return e; 258 | } 259 | 260 | *puiSize = u16::try_from(mem::size_of::()) 261 | .expect("value too large"); 262 | 263 | headers::CLIENT_STATUS_SUCCESS 264 | } 265 | } 266 | } 267 | 268 | extern "C" fn VdSetInformation( 269 | pVd: headers::PVD, 270 | pVdSetInformation: headers::PVDSETINFORMATION, 271 | puiSize: headers::PUINT16, 272 | ) -> ffi::c_int { 273 | common::debug!("VdSetInformation"); 274 | 275 | match unsafe { (pVd.as_mut(), pVdSetInformation.as_mut(), puiSize.as_mut()) } { 276 | (None, _, _) | (_, None, _) | (_, _, None) => headers::CLIENT_ERROR, 277 | (Some(pVd), Some(pVdSetInformation), Some(_puiSize)) => { 278 | if let Err(e) = super::DriverSetInformation(pVd, pVdSetInformation) { 279 | common::error!("DriverSetInformation failed: {e}"); 280 | return e; 281 | } 282 | 283 | unsafe { 284 | *puiSize = u16::try_from(mem::size_of::()) 285 | .expect("value too large"); 286 | } 287 | 288 | headers::CLIENT_STATUS_SUCCESS 289 | } 290 | } 291 | } 292 | 293 | pub fn WdQueryInformation( 294 | vd: &mut headers::VD, 295 | query_info: &mut headers::WDQUERYINFORMATION, 296 | ) -> Result<(), ffi::c_int> { 297 | common::debug!("WdQueryInformation"); 298 | 299 | match unsafe { vd.pWdLink.as_ref() } { 300 | None => Err(headers::CLIENT_ERROR_NULL_MEM_POINTER), 301 | Some(pLink) => { 302 | let pProcedures = pLink.pProcedures as *const headers::PDLLPROCEDURE; 303 | let procs: &[headers::PDLLPROCEDURE] = 304 | unsafe { slice::from_raw_parts(pProcedures, headers::WDxCOUNT as usize) }; 305 | match procs[headers::WDxQUERYINFORMATION as usize].as_ref() { 306 | None => Err(headers::CLIENT_ERROR_NULL_MEM_POINTER), 307 | Some(proc) => { 308 | let mut ui_size = u16::try_from(mem::size_of::()) 309 | .expect("value too large"); 310 | 311 | let ret = unsafe { 312 | proc(pLink.pData, ptr::from_mut(query_info).cast(), &mut ui_size) 313 | }; 314 | 315 | if ret != headers::CLIENT_STATUS_SUCCESS { 316 | return Err(ret); 317 | } 318 | 319 | Ok(()) 320 | } 321 | } 322 | } 323 | } 324 | } 325 | 326 | pub fn WdSetInformation( 327 | vd: &mut headers::VD, 328 | set_info: &mut headers::WDSETINFORMATION, 329 | ) -> Result<(), ffi::c_int> { 330 | common::debug!("WdSetInformation"); 331 | 332 | match unsafe { vd.pWdLink.as_ref() } { 333 | None => Err(headers::CLIENT_ERROR_NULL_MEM_POINTER), 334 | Some(pLink) => { 335 | let pProcedures = pLink.pProcedures as *const headers::PDLLPROCEDURE; 336 | let procs: &[headers::PDLLPROCEDURE] = 337 | unsafe { slice::from_raw_parts(pProcedures, headers::WDxCOUNT as usize) }; 338 | match procs[headers::WDxSETINFORMATION as usize].as_ref() { 339 | None => Err(headers::CLIENT_ERROR_NULL_MEM_POINTER), 340 | Some(proc) => { 341 | let mut ui_size = u16::try_from(mem::size_of::()) 342 | .expect("value too large"); 343 | 344 | let ret = 345 | unsafe { proc(pLink.pData, ptr::from_mut(set_info).cast(), &mut ui_size) }; 346 | 347 | if ret != headers::CLIENT_STATUS_SUCCESS { 348 | return Err(ret); 349 | } 350 | 351 | Ok(()) 352 | } 353 | } 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /frontend/src/svc/mod.rs: -------------------------------------------------------------------------------- 1 | use common::api; 2 | use std::{fmt, sync}; 3 | 4 | mod citrix; 5 | mod rdp; 6 | mod semaphore; 7 | 8 | #[derive(Clone, Debug)] 9 | pub enum State { 10 | Initialized, 11 | Connected(Option), 12 | Disconnected, 13 | Terminated, 14 | } 15 | 16 | pub enum Command { 17 | Open, 18 | SendChunk(api::Chunk), 19 | Close, 20 | } 21 | 22 | pub enum Response { 23 | ChangeState(State), 24 | ReceivedData(Vec), 25 | WriteCancelled, 26 | } 27 | 28 | pub enum Error { 29 | Citrix(citrix::Error), 30 | Rdp(rdp::Error), 31 | } 32 | 33 | impl fmt::Display for Error { 34 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 35 | match self { 36 | Self::Citrix(e) => write!(f, "Citrix error: {e}"), 37 | Self::Rdp(e) => write!(f, "RDP error: {e}"), 38 | } 39 | } 40 | } 41 | 42 | trait SvcImplementation { 43 | fn open(&mut self) -> Result<(), Error>; 44 | fn write(&self, data: Vec) -> Result<(), Error>; 45 | fn close(&mut self) -> Result<(), Error>; 46 | } 47 | 48 | pub enum Svc { 49 | Citrix(citrix::Svc), 50 | Rdp(rdp::Svc), 51 | } 52 | 53 | impl Svc { 54 | pub fn open(&mut self) -> Result<(), Error> { 55 | match self { 56 | Self::Citrix(svc) => svc.open(), 57 | Self::Rdp(svc) => svc.open(), 58 | } 59 | } 60 | 61 | pub fn write(&self, data: Vec) -> Result<(), Error> { 62 | match self { 63 | Self::Citrix(svc) => svc.write(data), 64 | Self::Rdp(svc) => svc.write(data), 65 | } 66 | } 67 | 68 | pub fn close(&mut self) -> Result<(), Error> { 69 | match self { 70 | Self::Citrix(svc) => svc.close(), 71 | Self::Rdp(svc) => svc.close(), 72 | } 73 | } 74 | } 75 | 76 | pub static SVC: sync::RwLock> = sync::RwLock::new(None); 77 | 78 | const MAX_CHUNKS_IN_FLIGHT: usize = 64; 79 | -------------------------------------------------------------------------------- /frontend/src/svc/rdp/headers.h: -------------------------------------------------------------------------------- 1 | #if defined(_WIN32) 2 | #else 3 | #ifndef USE_WIN_DWORD_RANGE 4 | #define USE_WIN_DWORD_RANGE 5 | #endif 6 | #endif 7 | 8 | typedef void VOID; 9 | typedef VOID *PVOID; 10 | typedef VOID *LPVOID; 11 | 12 | typedef unsigned long ULONG; 13 | typedef ULONG *PULONG; 14 | 15 | typedef unsigned int UINT; 16 | typedef unsigned int UINT32; 17 | 18 | typedef int INT; 19 | 20 | typedef char CHAR; 21 | typedef CHAR *PCHAR; 22 | 23 | typedef int BOOL; 24 | 25 | const BOOL TRUE = 1; 26 | const BOOL FALSE = 0; 27 | 28 | #ifndef USE_WIN_DWORD_RANGE 29 | #ifdef __APPLE__ 30 | #include 31 | typedef uint32_t DWORD; 32 | #else 33 | typedef unsigned long DWORD; 34 | #endif 35 | #else 36 | typedef unsigned int DWORD; 37 | #endif 38 | 39 | typedef DWORD *LPDWORD; 40 | 41 | 42 | /* 43 | * Reference: http://msdn.microsoft.com/en-us/library/windows/desktop/aa383564(v=vs.85).aspx 44 | */ 45 | 46 | #define CHANNEL_NAME_LEN 7 47 | 48 | typedef struct _CHANNEL_DEF { 49 | char name[CHANNEL_NAME_LEN+1]; 50 | ULONG options; 51 | } CHANNEL_DEF, *PCHANNEL_DEF, **PPCHANNEL_DEF; 52 | 53 | typedef VOID (*VirtualChannelInitEvent) (LPVOID pInitHandle, 54 | UINT event, 55 | LPVOID pData, 56 | UINT dataLength 57 | ); 58 | 59 | typedef VirtualChannelInitEvent PCHANNEL_INIT_EVENT_FN; 60 | 61 | typedef UINT (*VirtualChannelInit) (LPVOID *ppInitHandle, 62 | PCHANNEL_DEF pChannel, 63 | INT channelCount, 64 | ULONG versionRequested, 65 | PCHANNEL_INIT_EVENT_FN pChannelInitEventProc 66 | ); 67 | 68 | typedef VirtualChannelInit PVIRTUALCHANNELINIT; 69 | 70 | typedef VOID (*VirtualChannelOpenEvent) (DWORD openHandle, 71 | UINT event, 72 | LPVOID pData, 73 | UINT32 dataLength, 74 | UINT32 totalLength, 75 | UINT32 dataFlags 76 | ); 77 | 78 | typedef VirtualChannelOpenEvent PCHANNEL_OPEN_EVENT_FN; 79 | 80 | typedef UINT (*VirtualChannelOpen) (LPVOID pInitHandle, 81 | LPDWORD pOpenHandle, 82 | PCHAR pChannelName, 83 | PCHANNEL_OPEN_EVENT_FN pChannelOpenEventProc 84 | ); 85 | 86 | typedef VirtualChannelOpen PVIRTUALCHANNELOPEN; 87 | 88 | typedef UINT (*VirtualChannelClose) (DWORD openHandle); 89 | 90 | typedef VirtualChannelClose PVIRTUALCHANNELCLOSE; 91 | 92 | typedef UINT (*VirtualChannelWrite) (DWORD openHandle, 93 | LPVOID pData, 94 | ULONG dataLength, 95 | LPVOID pUserData 96 | ); 97 | 98 | typedef VirtualChannelWrite PVIRTUALCHANNELWRITE; 99 | 100 | typedef struct _CHANNEL_ENTRY_POINTS { 101 | DWORD cbSize; 102 | DWORD protocolVersion; 103 | PVIRTUALCHANNELINIT pVirtualChannelInit; 104 | PVIRTUALCHANNELOPEN pVirtualChannelOpen; 105 | PVIRTUALCHANNELCLOSE pVirtualChannelClose; 106 | PVIRTUALCHANNELWRITE pVirtualChannelWrite; 107 | } CHANNEL_ENTRY_POINTS, *PCHANNEL_ENTRY_POINTS; 108 | 109 | typedef BOOL (*VirtualChannelEntryMSDN) (PCHANNEL_ENTRY_POINTS pEntryPoints); 110 | 111 | typedef VirtualChannelEntryMSDN PVIRTUALCHANNELENTRY; 112 | 113 | 114 | typedef VOID (*VirtualChannelInitEventEx) (LPVOID lpUserParam, 115 | LPVOID pInitHandle, 116 | UINT event, 117 | LPVOID pData, 118 | UINT dataLength 119 | ); 120 | 121 | typedef VirtualChannelInitEventEx PCHANNEL_INIT_EVENT_EX_FN; 122 | 123 | typedef UINT (*VirtualChannelInitEx) (LPVOID lpUserParam, 124 | LPVOID clientContext, 125 | LPVOID pInitHandle, 126 | PCHANNEL_DEF pChannel, 127 | INT channelCount, 128 | ULONG versionRequested, 129 | PCHANNEL_INIT_EVENT_EX_FN pChannelInitEventProcEx 130 | ); 131 | 132 | typedef VirtualChannelInitEx PVIRTUALCHANNELINITEX; 133 | 134 | typedef VOID (*VirtualChannelOpenEventEx) (LPVOID lpUserParam, 135 | DWORD openHandle, 136 | UINT event, 137 | LPVOID pData, 138 | UINT32 dataLength, 139 | UINT32 totalLength, 140 | UINT32 dataFlags 141 | ); 142 | 143 | typedef VirtualChannelOpenEventEx PCHANNEL_OPEN_EVENT_EX_FN; 144 | 145 | typedef UINT (*VirtualChannelOpenEx) (LPVOID pInitHandle, 146 | LPDWORD pOpenHandle, 147 | PCHAR pChannelName, 148 | PCHANNEL_OPEN_EVENT_EX_FN pChannelOpenEventProcEx 149 | ); 150 | 151 | typedef VirtualChannelOpenEx PVIRTUALCHANNELOPENEX; 152 | 153 | typedef UINT (*VirtualChannelCloseEx) (LPVOID pInitHandle, 154 | DWORD openHandle); 155 | 156 | typedef VirtualChannelCloseEx PVIRTUALCHANNELCLOSEEX; 157 | 158 | typedef UINT (*VirtualChannelWriteEx) (LPVOID pInitHandle, 159 | DWORD openHandle, 160 | LPVOID pData, 161 | ULONG dataLength, 162 | LPVOID pUserData 163 | ); 164 | 165 | typedef VirtualChannelWriteEx PVIRTUALCHANNELWRITEEX; 166 | 167 | typedef struct _CHANNEL_ENTRY_POINTS_EX { 168 | DWORD cbSize; 169 | DWORD protocolVersion; 170 | PVIRTUALCHANNELINITEX pVirtualChannelInitEx; 171 | PVIRTUALCHANNELOPENEX pVirtualChannelOpenEx; 172 | PVIRTUALCHANNELCLOSEEX pVirtualChannelCloseEx; 173 | PVIRTUALCHANNELWRITEEX pVirtualChannelWriteEx; 174 | } CHANNEL_ENTRY_POINTS_EX, *PCHANNEL_ENTRY_POINTS_EX; 175 | 176 | typedef BOOL (*VirtualChannelEntryExMSDN) (PCHANNEL_ENTRY_POINTS_EX pEntryPointsEx, 177 | PVOID pInitHandle); 178 | 179 | typedef VirtualChannelEntryExMSDN PVIRTUALCHANNELENTRYEX; 180 | 181 | /* 182 | * MS compatible SVC plugin interface 183 | * Reference: http://msdn.microsoft.com/en-us/library/aa383580.aspx 184 | */ 185 | 186 | #define CHANNEL_RC_OK 0 187 | #define CHANNEL_RC_ALREADY_INITIALIZED 1 188 | #define CHANNEL_RC_NOT_INITIALIZED 2 189 | #define CHANNEL_RC_ALREADY_CONNECTED 3 190 | #define CHANNEL_RC_NOT_CONNECTED 4 191 | #define CHANNEL_RC_TOO_MANY_CHANNELS 5 192 | #define CHANNEL_RC_BAD_CHANNEL 6 193 | #define CHANNEL_RC_BAD_CHANNEL_HANDLE 7 194 | #define CHANNEL_RC_NO_BUFFER 8 195 | #define CHANNEL_RC_BAD_INIT_HANDLE 9 196 | #define CHANNEL_RC_NOT_OPEN 10 197 | #define CHANNEL_RC_BAD_PROC 11 198 | #define CHANNEL_RC_NO_MEMORY 12 199 | #define CHANNEL_RC_UNKNOWN_CHANNEL_NAME 13 200 | #define CHANNEL_RC_ALREADY_OPEN 14 201 | #define CHANNEL_RC_NOT_IN_VIRTUALCHANNELENTRY 15 202 | #define CHANNEL_RC_NULL_DATA 16 203 | #define CHANNEL_RC_ZERO_LENGTH 17 204 | 205 | #define VIRTUAL_CHANNEL_VERSION_WIN2000 1 206 | 207 | #define CHANNEL_MAX_COUNT 30 208 | 209 | 210 | //#define CHANNEL_CHUNK_LENGTH 1600 211 | 212 | 213 | /* 214 | * Static Virtual Channel Events 215 | */ 216 | 217 | enum RDP_SVC_CHANNEL_EVENT 218 | { 219 | CHANNEL_EVENT_INITIALIZED = 0, 220 | CHANNEL_EVENT_CONNECTED = 1, 221 | CHANNEL_EVENT_V1_CONNECTED = 2, 222 | CHANNEL_EVENT_DISCONNECTED = 3, 223 | CHANNEL_EVENT_TERMINATED = 4, 224 | CHANNEL_EVENT_DATA_RECEIVED = 10, 225 | CHANNEL_EVENT_WRITE_COMPLETE = 11, 226 | CHANNEL_EVENT_WRITE_CANCELLED = 12, 227 | CHANNEL_EVENT_USER = 1000 228 | }; 229 | 230 | /* 231 | * Static Virtual Channel Flags 232 | */ 233 | 234 | enum RDP_SVC_CHANNEL_FLAG 235 | { 236 | CHANNEL_FLAG_MIDDLE = 0, 237 | CHANNEL_FLAG_FIRST = 0x01, 238 | CHANNEL_FLAG_LAST = 0x02, 239 | CHANNEL_FLAG_ONLY = (CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST), 240 | CHANNEL_FLAG_SHOW_PROTOCOL = 0x10, 241 | CHANNEL_FLAG_SUSPEND = 0x20, 242 | CHANNEL_FLAG_RESUME = 0x40, 243 | CHANNEL_FLAG_FAIL = 0x100 244 | }; 245 | -------------------------------------------------------------------------------- /frontend/src/svc/rdp/headers.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(unused)] 5 | #![allow(clippy::upper_case_acronyms)] 6 | #![allow(clippy::identity_op)] 7 | 8 | include!(concat!(env!("OUT_DIR"), "/rdp_headers.rs")); 9 | -------------------------------------------------------------------------------- /frontend/src/svc/semaphore.rs: -------------------------------------------------------------------------------- 1 | //! Simple semaphores built on top of `std::sync` primitives, no external dependency 2 | 3 | use std::sync::{Arc, Condvar, Mutex}; 4 | 5 | #[derive(Clone)] 6 | pub struct Semaphore(Arc<(Mutex, Condvar)>); 7 | 8 | impl Semaphore { 9 | pub fn new(count: usize) -> Self { 10 | Self(Arc::new((Mutex::new(count), Condvar::new()))) 11 | } 12 | 13 | pub(crate) fn reset(&self, value: usize) { 14 | let (lock, _cv) = &*self.0; 15 | let mut counter = lock.lock().expect("acquire lock"); 16 | *counter = value; 17 | } 18 | 19 | pub(crate) fn acquire(&self) { 20 | let (lock, cv) = &*self.0; 21 | let mut counter = lock.lock().expect("acquire lock"); 22 | while *counter == 0 { 23 | counter = cv 24 | .wait_while(counter, |counter| *counter == 0) 25 | .expect("condvar wait"); 26 | } 27 | *counter = counter.checked_sub(1).expect("semaphore counter decrement"); 28 | } 29 | 30 | pub(crate) fn release(&self) { 31 | let (lock, cv) = &*self.0; 32 | let mut counter = lock.lock().expect("acquire lock"); 33 | *counter = counter.checked_add(1).expect("semaphore counter increment"); 34 | cv.notify_one(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/windows.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use windows_sys as ws; 3 | 4 | const ENTRY_NAME: &str = env!("CARGO_CRATE_NAME"); 5 | 6 | const CITRIX_MACHINE_MODULES_PATH: &str = 7 | "SOFTWARE\\WOW6432Node\\Citrix\\ICA Client\\Engine\\Configuration\\Advanced\\Modules"; 8 | 9 | const CITRIX_MODULES_ICA_PATH: &str = "ICA 3.0"; 10 | 11 | const CITRIX_ICA_VDEX_PATH: &str = "VirtualDriverEx"; 12 | 13 | const CITRIX_ENTRY_DRIVER_NAME: &str = "DriverName"; 14 | const CITRIX_ENTRY_DRIVER_NAME_WIN16: &str = "DriverNameWin16"; 15 | const CITRIX_ENTRY_DRIVER_NAME_WIN32: &str = "DriverNameWin32"; 16 | 17 | fn citrix_register() -> Result<(), String> { 18 | let hk = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); 19 | let path = CITRIX_MACHINE_MODULES_PATH; 20 | 21 | let (modules, _disp) = hk 22 | .create_subkey(path) 23 | .map_err(|e| format!("failed to create citrix modules path: {e}"))?; 24 | 25 | let (ica, _disp) = modules 26 | .create_subkey(CITRIX_MODULES_ICA_PATH) 27 | .map_err(|e| format!("failed to create citrix modules virtual driver path: {e}"))?; 28 | 29 | let vdex: String = ica.get_value(CITRIX_ICA_VDEX_PATH).unwrap_or(String::new()); 30 | let mut vdex: Vec<&str> = if vdex.trim().is_empty() { 31 | vec![] 32 | } else { 33 | vdex.split(',').map(str::trim).collect() 34 | }; 35 | vdex.push(ENTRY_NAME); 36 | let vdex = vdex.join(","); 37 | ica.set_value(CITRIX_ICA_VDEX_PATH, &vdex) 38 | .map_err(|e| format!("failed to set name: {e}"))?; 39 | 40 | let (entry, _disp) = modules 41 | .create_subkey(ENTRY_NAME) 42 | .map_err(|e| format!("failed to create citrix modules entry path: {e}"))?; 43 | entry 44 | .set_value(CITRIX_ENTRY_DRIVER_NAME, &format!("{ENTRY_NAME}.dll")) 45 | .map_err(|e| format!("failed to set name: {e}"))?; 46 | entry 47 | .set_value(CITRIX_ENTRY_DRIVER_NAME_WIN16, &format!("{ENTRY_NAME}.dll")) 48 | .map_err(|e| format!("failed to set name: {e}"))?; 49 | entry 50 | .set_value(CITRIX_ENTRY_DRIVER_NAME_WIN32, &format!("{ENTRY_NAME}.dll")) 51 | .map_err(|e| format!("failed to set name: {e}"))?; 52 | 53 | Ok(()) 54 | } 55 | 56 | fn citrix_unregister() { 57 | let hk = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); 58 | let path = CITRIX_MACHINE_MODULES_PATH; 59 | 60 | if let Ok(modules) = hk.open_subkey_with_flags(path, winreg::enums::KEY_ALL_ACCESS) { 61 | if let Ok(ica) = 62 | modules.open_subkey_with_flags(CITRIX_MODULES_ICA_PATH, winreg::enums::KEY_ALL_ACCESS) 63 | { 64 | if let Ok(vdex) = ica.get_value::(CITRIX_ICA_VDEX_PATH) { 65 | let vdex = vdex.trim(); 66 | let vdex: Vec<&str> = if vdex.is_empty() { 67 | vec![] 68 | } else { 69 | vdex.split(',') 70 | .map(str::trim) 71 | .filter(|s| s != &ENTRY_NAME) 72 | .collect() 73 | }; 74 | let vdex = vdex.join(","); 75 | let _ = ica.set_value(CITRIX_ICA_VDEX_PATH, &vdex); 76 | } 77 | } 78 | 79 | let _ = modules.delete_subkey_all(ENTRY_NAME); 80 | } 81 | } 82 | 83 | const RDP_ADDINS_PATH: &str = "Software\\Microsoft\\Terminal Server Client\\Default\\AddIns"; 84 | 85 | fn rdp_register() -> Result<(), String> { 86 | let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 87 | 88 | let (addins, _disp) = hkcu 89 | .create_subkey(RDP_ADDINS_PATH) 90 | .map_err(|e| format!("failed to create addins: {e}"))?; 91 | 92 | let (entry, _disp) = addins 93 | .create_subkey(ENTRY_NAME) 94 | .map_err(|e| format!("failed to create entry: {e}"))?; 95 | 96 | entry 97 | .set_value("View Enabled", &1u32) 98 | .map_err(|e| format!("failed to set view enabled: {e}"))?; 99 | 100 | let mut dll = env::current_dir().unwrap(); 101 | dll.push(format!("{ENTRY_NAME}.dll")); 102 | entry 103 | .set_value("Name", &dll.as_os_str()) 104 | .map_err(|e| format!("failed to set name: {e}"))?; 105 | 106 | Ok(()) 107 | } 108 | 109 | fn rdp_unregister() { 110 | let hkcu = winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER); 111 | if let Ok(addins) = hkcu.open_subkey_with_flags(RDP_ADDINS_PATH, winreg::enums::KEY_ALL_ACCESS) 112 | { 113 | let _ = addins.delete_subkey_all(ENTRY_NAME); 114 | } 115 | } 116 | 117 | #[unsafe(no_mangle)] 118 | #[allow(non_snake_case, unused_variables)] 119 | extern "system" fn DllRegisterServer() -> ws::core::HRESULT { 120 | unsafe { ws::Win32::System::Console::AllocConsole() }; 121 | 122 | #[cfg(debug_assertions)] 123 | common::init_logs(common::Level::Debug, None); 124 | #[cfg(not(debug_assertions))] 125 | common::init_logs(common::Level::Info, None); 126 | 127 | let mut is_ok = true; 128 | 129 | if let Err(e) = rdp_register() { 130 | common::error!("RDP register error: {e}"); 131 | is_ok = false; 132 | } else { 133 | common::info!("RDP registered"); 134 | } 135 | 136 | if let Err(e) = citrix_register() { 137 | common::error!("Citrix register error: {e}"); 138 | is_ok = false; 139 | } else { 140 | common::info!("Citrix registered"); 141 | } 142 | 143 | if !is_ok { 144 | return ws::Win32::System::Ole::SELFREG_E_CLASS; 145 | } 146 | 147 | ws::Win32::Foundation::S_OK 148 | } 149 | 150 | #[unsafe(no_mangle)] 151 | #[allow(non_snake_case, unused_variables)] 152 | extern "system" fn DllUnregisterServer() -> ws::core::HRESULT { 153 | unsafe { ws::Win32::System::Console::AllocConsole() }; 154 | 155 | #[cfg(debug_assertions)] 156 | common::init_logs(common::Level::Debug, None); 157 | #[cfg(not(debug_assertions))] 158 | common::init_logs(common::Level::Info, None); 159 | 160 | rdp_unregister(); 161 | 162 | common::info!("RDP unregistered"); 163 | 164 | citrix_unregister(); 165 | 166 | common::info!("Citrix unregistered"); 167 | 168 | ws::Win32::Foundation::S_OK 169 | } 170 | -------------------------------------------------------------------------------- /standalone/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /standalone/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "standalone" 3 | version = "2.3.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | frontend = { path = "../frontend" } 8 | common = { path = "../common", features = [ "backend", "frontend" ] } 9 | crossbeam-channel = "0" 10 | log = { version = "0", optional = true } 11 | 12 | [lints.clippy] 13 | pedantic = { level = "deny", priority = -1 } 14 | must_use_candidate = "allow" 15 | enum-glob-use = "allow" 16 | missing-errors-doc = "allow" 17 | 18 | [profile.release] 19 | opt-level = 3 20 | debug = false 21 | strip = true 22 | lto = true 23 | panic = "abort" 24 | 25 | [[bin]] 26 | name = "soxy_standalone" 27 | path = "src/bin/standalone.rs" 28 | 29 | [features] 30 | log = [ "dep:log", "common/log", "frontend/log" ] 31 | -------------------------------------------------------------------------------- /standalone/src/bin/standalone.rs: -------------------------------------------------------------------------------- 1 | use common::service; 2 | 3 | const CHANNEL_SIZE: usize = 256; 4 | 5 | #[allow(clippy::too_many_lines)] 6 | fn main() { 7 | let (frontend_to_backend_send, frontend_to_backend_receive) = 8 | crossbeam_channel::bounded(CHANNEL_SIZE); 9 | let (backend_to_frontend_send, backend_to_frontend_receive) = 10 | crossbeam_channel::bounded(CHANNEL_SIZE); 11 | 12 | let backend_channel = service::Channel::new(backend_to_frontend_send); 13 | let frontend_channel = service::Channel::new(frontend_to_backend_send); 14 | 15 | if let Err(e) = soxy::init(frontend_channel, backend_to_frontend_receive) { 16 | common::error!("error: {e}"); 17 | return; 18 | } 19 | 20 | if let Err(e) = backend_channel.start(service::Kind::Backend, &frontend_to_backend_receive) { 21 | common::error!("error: {e}"); 22 | } else { 23 | common::debug!("terminated"); 24 | } 25 | } 26 | --------------------------------------------------------------------------------