feat(rdp): remote audio and a shared folder redirected as a drive
- Audio: the RDPSND channel with IronRDP's cpal playback backend (PCM; Opus left out to avoid another native library). On by default for new RDP profiles. Building now needs the ALSA headers on Linux (libasound2-dev), added to CI, release and fuzz jobs and the build docs. - Shared folder (Linux/macOS): one local folder appears on the remote desktop as a drive. IronRDP's native backend joins server paths onto the share unchecked, so it is wrapped in ConfinedDrive: create, list and rename paths with '..' components, NUL bytes, or that resolve through a symlink outside the share are refused (ACCESS_DENIED); tests cover the escapes. Verified against xrdp (with FUSE): the remote read the shared file, and writes, mkdir and rename landed in the local folder. Not verified: audio playback (the test container has no sound module), and xrdp lists the drive as 'ignored' and can't list its directories (an IronRDP/xrdp naming and listing mismatch, fixed upstream for the name).
This commit is contained in:
@@ -26,7 +26,7 @@ jobs:
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxkbcommon-dev libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev libgtk-3-dev openssh-server
|
||||
sudo apt-get install -y libxkbcommon-dev libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev libgtk-3-dev libasound2-dev openssh-server
|
||||
- run: cargo test --locked --workspace
|
||||
- run: cargo fmt --all --check
|
||||
- run: cargo clippy --locked --workspace --all-targets -- -D warnings
|
||||
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
working-directory: rust/fuzz
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
- run: sudo apt-get update && sudo apt-get install -y libasound2-dev
|
||||
- run: rustup toolchain install nightly --profile minimal
|
||||
- run: cargo +nightly install cargo-fuzz --locked
|
||||
- run: cargo +nightly fuzz run ${{ matrix.target }} -- -max_total_time=120 -rss_limit_mb=2048
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxkbcommon-dev libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev libgtk-3-dev zip
|
||||
sudo apt-get install -y libxkbcommon-dev libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev libgtk-3-dev libasound2-dev zip
|
||||
|
||||
- name: Test
|
||||
run: cargo test --locked --workspace
|
||||
|
||||
@@ -17,7 +17,7 @@ The toolchain is pinned in `rust/rust-toolchain.toml`; `rustup` installs it on f
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y build-essential pkg-config libxkbcommon-dev libgl1-mesa-dev \
|
||||
libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev libgtk-3-dev zip
|
||||
libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev libgtk-3-dev libasound2-dev zip
|
||||
```
|
||||
|
||||
- **macOS:** Xcode command-line tools (`xcode-select --install`).
|
||||
|
||||
@@ -148,6 +148,7 @@ Use Nexus Terminal when you need to:
|
||||
- Scale-to-fit or 1:1, a fixed resolution or one that follows the tab (Display Control).
|
||||
- Keyboard (scancodes for keys and shortcuts, Unicode for text), mouse and wheel; Ctrl+Alt+Del; text clipboard sharing in both directions.
|
||||
- Password storage modes and domain logon.
|
||||
- Remote audio playback, and a shared folder redirected as a drive (Linux and macOS), confined to that folder.
|
||||
|
||||
### VNC capabilities
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ Use SSH tunnelling for VNC whenever possible. If a VNC profile has no tunnel, Ne
|
||||
- **RDP**: domain (optional), password storage mode, **Certificate Policy** and **Allow servers without NLA**. The username, host and port (3389) are the profile's own.
|
||||
- **Display**: **Scale to Fit**, a fixed **Width** and **Height** (0 × 0 fits the tab) and **Resize With Tab**.
|
||||
- **Proxy**: **SOCKS5** or **HTTP CONNECT**, with an optional username and password.
|
||||
- **Devices**: **Play Remote Audio**, and on Linux and macOS a **Shared Folder** that appears on the remote desktop as a drive named Nexus. The server can read and change everything in that folder and nothing outside it: paths that try to leave it (`..`, or symlinks pointing elsewhere) are refused.
|
||||
- **SSH Tunnel**: carry RDP through SSH, as for VNC. Use either a proxy or a tunnel, not both.
|
||||
|
||||
Nexus Terminal logs on with Network Level Authentication (CredSSP, NTLM), so your password is checked before a session is set up. Servers without NLA, such as xrdp, need **Allow servers without NLA**; the password is then passed to the server's own logon. Kerberos and smart cards are not supported.
|
||||
|
||||
Generated
+280
-3
@@ -264,6 +264,28 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
version = "0.6.1"
|
||||
@@ -273,7 +295,7 @@ dependencies = [
|
||||
"android-properties",
|
||||
"bitflags 2.13.2",
|
||||
"cc",
|
||||
"jni",
|
||||
"jni 0.22.4",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -949,6 +971,12 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
@@ -1154,6 +1182,50 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.17.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni 0.21.1",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2 0.5.0",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"objc2 0.6.5",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-avf-audio",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpubits"
|
||||
version = "0.1.1"
|
||||
@@ -1362,6 +1434,12 @@ dependencies = [
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.1"
|
||||
@@ -2758,7 +2836,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"mach2",
|
||||
"mach2 0.4.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2775,6 +2853,8 @@ dependencies = [
|
||||
"ironrdp-graphics",
|
||||
"ironrdp-input",
|
||||
"ironrdp-pdu",
|
||||
"ironrdp-rdpdr",
|
||||
"ironrdp-rdpsnd",
|
||||
"ironrdp-session",
|
||||
"ironrdp-svc",
|
||||
]
|
||||
@@ -2921,6 +3001,59 @@ dependencies = [
|
||||
"x509-cert",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-rdpdr"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ece9e67ccf992eb8ce5ec0416d3950e2e199d4b1e2523f91af192205279fc970"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"ironrdp-core",
|
||||
"ironrdp-error",
|
||||
"ironrdp-pdu",
|
||||
"ironrdp-svc",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-rdpdr-native"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3e964857871c3a49cf5e642320908a0be73c923f7f037c347c33f23001113e7"
|
||||
dependencies = [
|
||||
"ironrdp-core",
|
||||
"ironrdp-pdu",
|
||||
"ironrdp-rdpdr",
|
||||
"ironrdp-svc",
|
||||
"nix 0.31.3",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-rdpsnd"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1a4f7092ae8fdc0a2f61be8e100277dc6f4ad51b85455dc9e8157979993ea2"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"ironrdp-core",
|
||||
"ironrdp-pdu",
|
||||
"ironrdp-svc",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-rdpsnd-native"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d2d36d9bb45863f9bc5225bf3bccda125fafb1b225c1f99dbeaeebb258e7dc"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"ironrdp-error",
|
||||
"ironrdp-rdpsnd",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironrdp-session"
|
||||
version = "0.11.0"
|
||||
@@ -2980,6 +3113,22 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
@@ -3239,6 +3388,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -3493,6 +3651,8 @@ dependencies = [
|
||||
"image",
|
||||
"ironrdp",
|
||||
"ironrdp-blocking",
|
||||
"ironrdp-rdpdr-native",
|
||||
"ironrdp-rdpsnd-native",
|
||||
"keyring",
|
||||
"libc",
|
||||
"parking_lot",
|
||||
@@ -3780,6 +3940,31 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-audio-toolbox"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
"objc2 0.6.5",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-avf-audio"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||
dependencies = [
|
||||
"objc2 0.6.5",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-cloud-kit"
|
||||
version = "0.2.2"
|
||||
@@ -3804,6 +3989,29 @@ dependencies = [
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"objc2 0.6.5",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-data"
|
||||
version = "0.2.2"
|
||||
@@ -3823,7 +4031,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2 0.6.2",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
"objc2 0.6.5",
|
||||
]
|
||||
|
||||
@@ -3891,6 +4101,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2 0.6.2",
|
||||
"libc",
|
||||
"objc2 0.6.5",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
@@ -5471,7 +5682,7 @@ dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"io-kit-sys",
|
||||
"mach2",
|
||||
"mach2 0.4.3",
|
||||
"nix 0.26.4",
|
||||
"scopeguard",
|
||||
"unescaper",
|
||||
@@ -6996,6 +7207,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -7032,6 +7252,21 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
@@ -7074,6 +7309,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -7086,6 +7327,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -7098,6 +7345,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -7122,6 +7375,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
@@ -7134,6 +7393,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
@@ -7146,6 +7411,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
@@ -7158,6 +7429,12 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -700,6 +700,34 @@ impl ProfileEditor {
|
||||
"Plain TLS logon, e.g. xrdp. Less secure: the session is set up before you authenticate.",
|
||||
);
|
||||
});
|
||||
section(ui, "Devices", |ui| {
|
||||
check(ui, "Play Remote Audio", &mut c.audio, "");
|
||||
if cfg!(unix) {
|
||||
label(ui, "Shared Folder");
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::button(ui, icon::FOLDER_OPEN, "Choose…").clicked()
|
||||
&& let Some(dir) = rfd::FileDialog::new().set_title("Folder to share with the remote desktop").pick_folder()
|
||||
{
|
||||
c.shared_folder = dir.to_string_lossy().into_owned();
|
||||
}
|
||||
if !c.shared_folder.is_empty() && ui.button("Stop Sharing").clicked() {
|
||||
c.shared_folder.clear();
|
||||
}
|
||||
});
|
||||
err(ui, errs, "rdp.sharedFolder");
|
||||
widgets::hint(
|
||||
ui,
|
||||
&if c.shared_folder.is_empty() {
|
||||
"Nothing is shared.".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{} appears on the remote desktop as a drive named Nexus; the server can read and change everything in it, and nothing outside it.",
|
||||
c.shared_folder
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
section(ui, "Display", |ui| {
|
||||
check(ui, "Scale to Fit", &mut c.scale_to_fit, "");
|
||||
num(ui, "Width (0 = fit tab)", &mut c.width, 0..=8192);
|
||||
|
||||
@@ -36,10 +36,14 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] }
|
||||
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
||||
webpki-roots = "1"
|
||||
ironrdp = { version = "0.17", features = ["connector", "session", "graphics", "pdu", "svc", "dvc", "displaycontrol", "input", "cliprdr"] }
|
||||
ironrdp = { version = "0.17", features = ["connector", "session", "graphics", "pdu", "svc", "dvc", "displaycontrol", "input", "cliprdr", "rdpsnd", "rdpdr"] }
|
||||
ironrdp-rdpsnd-native = { version = "0.7", default-features = false }
|
||||
ironrdp-blocking = "0.10"
|
||||
x509-cert = { version = "0.2", default-features = false, features = ["std"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
ironrdp-rdpdr-native = "0.7"
|
||||
|
||||
[dev-dependencies]
|
||||
tiny_http = "0.12"
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ impl Callbacks {
|
||||
pub mod proxy;
|
||||
pub mod raw;
|
||||
pub mod rdp;
|
||||
#[cfg(unix)]
|
||||
pub mod rdp_drive;
|
||||
pub mod serial;
|
||||
pub mod telnet;
|
||||
pub mod vnc;
|
||||
|
||||
@@ -48,6 +48,10 @@ pub struct Config {
|
||||
pub height: u16,
|
||||
/// Also allow plain TLS (graphical logon) for servers without NLA, such as xrdp.
|
||||
pub allow_tls_only: bool,
|
||||
/// Play the remote desktop's sound locally.
|
||||
pub audio: bool,
|
||||
/// A local folder to share as a drive (Unix only), and the name it appears under.
|
||||
pub shared_folder: Option<(std::path::PathBuf, String)>,
|
||||
pub dial: Dial,
|
||||
pub check_cert: CertCheck,
|
||||
pub connect_timeout: Duration,
|
||||
@@ -165,7 +169,7 @@ impl Client {
|
||||
request_data: None,
|
||||
// Also hands the credentials to TLS-only servers (xrdp), which would otherwise show their own login screen.
|
||||
autologon: !c.password.is_empty(),
|
||||
enable_audio_playback: false,
|
||||
enable_audio_playback: c.audio,
|
||||
// ponytail: no bulk compression (the bitmap codecs already compress); enabling it means
|
||||
// carrying the decompressor across reactivations.
|
||||
compression_type: None,
|
||||
@@ -189,6 +193,14 @@ impl Client {
|
||||
let mut framed = Framed::new(tcp);
|
||||
let drdynvc = DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new())));
|
||||
let mut connector = ClientConnector::new(self.connector_config(), local).with_static_channel(drdynvc);
|
||||
if self.cfg.audio {
|
||||
connector.attach_static_channel(ironrdp::rdpsnd::client::Rdpsnd::new(Box::new(ironrdp_rdpsnd_native::cpal::RdpsndBackend::new())));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
if let Some((folder, name)) = &self.cfg.shared_folder {
|
||||
let drive = super::rdp_drive::ConfinedDrive::new(folder).map_err(|e| format!("shared folder {}: {e}", folder.display()))?;
|
||||
connector.attach_static_channel(ironrdp::rdpdr::Rdpdr::new(Box::new(drive), "Nexus".into()).with_drives(Some(vec![(1, name.clone())])));
|
||||
}
|
||||
let (clip_tx, clip_rx) = mpsc::channel();
|
||||
connector.attach_static_channel(CliprdrClient::new(Box::new(TextClipboard {
|
||||
tx: clip_tx,
|
||||
@@ -570,6 +582,8 @@ mod tests {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
allow_tls_only: true,
|
||||
audio: std::env::var("NEXUS_RDP_AUDIO").is_ok(),
|
||||
shared_folder: std::env::var("NEXUS_RDP_SHARE").ok().map(|p| (p.into(), "nexus".into())),
|
||||
dial: Arc::new(move || Ok(TcpStream::connect((h2.as_str(), port))?)),
|
||||
check_cert: Arc::new(move |fp| {
|
||||
*sc.lock() = fp.to_string();
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Drive redirection for RDP, confined to one shared folder. IronRDP's native backend joins the
|
||||
//! server's paths onto the share without checking them, so a hostile server could reach
|
||||
//! `\..\..\.ssh\id_ed25519`. Every path the server sends is checked here first: no `..`
|
||||
//! components, and after resolving symlinks it must stay inside the share. Unix only (the native
|
||||
//! backend has no Windows implementation).
|
||||
|
||||
use ironrdp::core::impl_as_any;
|
||||
use ironrdp::pdu::PduResult;
|
||||
use ironrdp::rdpdr::RdpdrBackend;
|
||||
use ironrdp::rdpdr::pdu::RdpdrPdu;
|
||||
use ironrdp::rdpdr::pdu::efs::{
|
||||
ClientDriveQueryDirectoryResponse, ClientDriveSetInformationResponse, DeviceControlRequest, DeviceCreateResponse, DeviceIoResponse, FileInformationClass,
|
||||
Information, NtStatus, ServerDeviceAnnounceResponse, ServerDriveIoRequest,
|
||||
};
|
||||
use ironrdp::rdpdr::pdu::esc::{ScardCall, ScardIoCtlCode};
|
||||
use ironrdp::svc::SvcMessage;
|
||||
use ironrdp_rdpdr_native::backend::NixRdpdrBackend;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConfinedDrive {
|
||||
inner: NixRdpdrBackend,
|
||||
base: PathBuf,
|
||||
}
|
||||
|
||||
impl_as_any!(ConfinedDrive);
|
||||
|
||||
impl ConfinedDrive {
|
||||
pub fn new(folder: &Path) -> std::io::Result<ConfinedDrive> {
|
||||
let base = folder.canonicalize()?;
|
||||
if !base.is_dir() {
|
||||
return Err(std::io::Error::other(format!("{} is not a folder", base.display())));
|
||||
}
|
||||
Ok(ConfinedDrive { inner: NixRdpdrBackend::new(base.to_string_lossy().trim_end_matches('/').to_string()), base })
|
||||
}
|
||||
|
||||
/// Whether a server path (`\dir\file`, Windows separators) stays inside the share.
|
||||
fn allowed(&self, server_path: &str) -> bool {
|
||||
let rel = server_path.replace('\\', "/");
|
||||
if rel.contains('\0') {
|
||||
return false;
|
||||
}
|
||||
let mut p = self.base.clone();
|
||||
for c in rel.split('/').filter(|c| !c.is_empty() && *c != ".") {
|
||||
if c == ".." {
|
||||
return false;
|
||||
}
|
||||
p.push(c);
|
||||
}
|
||||
// The deepest part that exists must resolve (through any symlinks) inside the share; what
|
||||
// doesn't exist yet can't be a link.
|
||||
let mut probe = p.as_path();
|
||||
loop {
|
||||
if probe.symlink_metadata().is_ok() {
|
||||
return probe.canonicalize().is_ok_and(|real| real.starts_with(&self.base));
|
||||
}
|
||||
match probe.parent() {
|
||||
Some(parent) if parent.starts_with(&self.base) => probe = parent,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refuse(req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
|
||||
tracing::warn!("RDP server asked for a path outside the shared folder; refused");
|
||||
let pdu = match req {
|
||||
ServerDriveIoRequest::ServerCreateDriveRequest(r) => RdpdrPdu::DeviceCreateResponse(DeviceCreateResponse {
|
||||
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::ACCESS_DENIED),
|
||||
file_id: 0,
|
||||
information: Information::empty(),
|
||||
}),
|
||||
ServerDriveIoRequest::ServerDriveQueryDirectoryRequest(r) => RdpdrPdu::ClientDriveQueryDirectoryResponse(ClientDriveQueryDirectoryResponse {
|
||||
device_io_reply: DeviceIoResponse::new(r.device_io_request, NtStatus::NO_MORE_FILES),
|
||||
buffer: None,
|
||||
}),
|
||||
ServerDriveIoRequest::ServerDriveSetInformationRequest(r) => RdpdrPdu::ClientDriveSetInformationResponse(
|
||||
ClientDriveSetInformationResponse::new(&r, NtStatus::ACCESS_DENIED).map_err(|e| ironrdp::pdu::encode_err!(e))?,
|
||||
),
|
||||
_ => return Ok(vec![]),
|
||||
};
|
||||
Ok(vec![SvcMessage::from(pdu)])
|
||||
}
|
||||
}
|
||||
|
||||
impl RdpdrBackend for ConfinedDrive {
|
||||
fn handle_server_device_announce_response(&mut self, pdu: ServerDeviceAnnounceResponse) -> PduResult<()> {
|
||||
self.inner.handle_server_device_announce_response(pdu)
|
||||
}
|
||||
|
||||
fn handle_scard_call(&mut self, req: DeviceControlRequest<ScardIoCtlCode>, call: ScardCall) -> PduResult<()> {
|
||||
self.inner.handle_scard_call(req, call)
|
||||
}
|
||||
|
||||
fn handle_drive_io_request(&mut self, req: ServerDriveIoRequest) -> PduResult<Vec<SvcMessage>> {
|
||||
let ok = match &req {
|
||||
ServerDriveIoRequest::ServerCreateDriveRequest(r) => self.allowed(&r.path),
|
||||
ServerDriveIoRequest::ServerDriveQueryDirectoryRequest(r) => r.initial_query == 0 || self.allowed(r.path.trim_end_matches('*')),
|
||||
ServerDriveIoRequest::ServerDriveSetInformationRequest(r) => match &r.set_buffer {
|
||||
FileInformationClass::Rename(info) => self.allowed(&info.file_name),
|
||||
_ => true,
|
||||
},
|
||||
// Everything else names a file by an id that a checked create handed out.
|
||||
_ => true,
|
||||
};
|
||||
if ok { self.inner.handle_drive_io_request(req) } else { Self::refuse(req) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn server_paths_stay_inside_the_share() {
|
||||
let root = std::env::temp_dir().join(format!("nexus-drive-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let share = root.join("share");
|
||||
std::fs::create_dir_all(share.join("sub")).unwrap();
|
||||
std::fs::write(root.join("secret"), "x").unwrap();
|
||||
std::os::unix::fs::symlink(&root, share.join("escape")).unwrap();
|
||||
std::os::unix::fs::symlink(share.join("sub"), share.join("inside")).unwrap();
|
||||
let d = ConfinedDrive::new(&share).unwrap();
|
||||
for ok in ["\\", "\\sub", "\\sub\\new.txt", "\\new\\deeper\\file", "\\inside\\x", "\\.\\sub"] {
|
||||
assert!(d.allowed(ok), "{ok}");
|
||||
}
|
||||
for bad in ["\\..\\secret", "\\sub\\..\\..\\secret", "\\escape\\secret", "\\escape\\new", "\\a\0b"] {
|
||||
assert!(!d.allowed(bad), "{bad:?}");
|
||||
}
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -409,6 +409,10 @@ pub struct RdpConfig {
|
||||
/// Allow plain TLS logon for servers without Network Level Authentication (e.g. xrdp).
|
||||
#[serde(rename = "allowTLSOnly")]
|
||||
pub allow_tls_only: bool,
|
||||
/// Play remote audio locally.
|
||||
pub audio: bool,
|
||||
/// Local folder shared with the remote desktop as a drive (empty: none). Linux and macOS.
|
||||
pub shared_folder: String,
|
||||
/// Trust policy for the server's TLS certificate (pinned like an SSH host key).
|
||||
#[serde(deserialize_with = "lenient")]
|
||||
pub host_key_policy: HostKeyPolicy,
|
||||
@@ -433,6 +437,7 @@ impl RdpConfig {
|
||||
RdpConfig {
|
||||
scale_to_fit: true,
|
||||
dynamic_resolution: true,
|
||||
audio: true,
|
||||
proxy_type: "none".into(),
|
||||
ssh_port: DEFAULT_SSH_PORT.into(),
|
||||
ssh_auth_method: "publickey".into(),
|
||||
|
||||
@@ -354,6 +354,8 @@ impl Manager {
|
||||
width,
|
||||
height,
|
||||
allow_tls_only: rc.allow_tls_only,
|
||||
audio: rc.audio,
|
||||
shared_folder: (!rc.shared_folder.trim().is_empty()).then(|| (rc.shared_folder.trim().into(), "Nexus".into())),
|
||||
dial,
|
||||
check_cert,
|
||||
connect_timeout: timeout,
|
||||
|
||||
@@ -334,6 +334,10 @@ pub fn validate_rdp_config(cfg: &RdpConfig) -> Result<(), ValidationErrors> {
|
||||
}
|
||||
t => errs.add("rdp.proxyType", format!("unsupported proxy type {t:?}")),
|
||||
}
|
||||
let share = cfg.shared_folder.trim();
|
||||
if !share.is_empty() && !std::path::Path::new(share).is_dir() {
|
||||
errs.add("rdp.sharedFolder", "the shared folder must be an existing folder");
|
||||
}
|
||||
if cfg.ssh_tunnel {
|
||||
if !matches!(cfg.proxy_type.as_str(), "" | "none") {
|
||||
errs.add("rdp.proxyType", "use either a proxy or an SSH tunnel, not both");
|
||||
|
||||
Reference in New Issue
Block a user