@sysid/sandbox-runtime-improved 0.0.56-sysid.1 → 0.0.61-sysid.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sandbox/credential-mask-files.d.ts +72 -0
- package/dist/sandbox/credential-mask-files.d.ts.map +1 -0
- package/dist/sandbox/credential-mask-files.js +146 -0
- package/dist/sandbox/credential-mask-files.js.map +1 -0
- package/dist/sandbox/credential-sentinel.d.ts +72 -0
- package/dist/sandbox/credential-sentinel.d.ts.map +1 -0
- package/dist/sandbox/credential-sentinel.js +130 -0
- package/dist/sandbox/credential-sentinel.js.map +1 -0
- package/dist/sandbox/domain-pattern.d.ts +36 -0
- package/dist/sandbox/domain-pattern.d.ts.map +1 -0
- package/dist/sandbox/domain-pattern.js +60 -0
- package/dist/sandbox/domain-pattern.js.map +1 -0
- package/dist/sandbox/http-proxy.d.ts +32 -1
- package/dist/sandbox/http-proxy.d.ts.map +1 -1
- package/dist/sandbox/http-proxy.js +10 -2
- package/dist/sandbox/http-proxy.js.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.d.ts +15 -0
- package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.js +106 -47
- package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
- package/dist/sandbox/listen-in-range.d.ts +12 -0
- package/dist/sandbox/listen-in-range.d.ts.map +1 -0
- package/dist/sandbox/listen-in-range.js +43 -0
- package/dist/sandbox/listen-in-range.js.map +1 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts +11 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/macos-sandbox-utils.js +69 -14
- package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
- package/dist/sandbox/mitm-ca.d.ts +18 -2
- package/dist/sandbox/mitm-ca.d.ts.map +1 -1
- package/dist/sandbox/mitm-ca.js +49 -9
- package/dist/sandbox/mitm-ca.js.map +1 -1
- package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
- package/dist/sandbox/mitm-leaf.js +24 -6
- package/dist/sandbox/mitm-leaf.js.map +1 -1
- package/dist/sandbox/mux-proxy.d.ts +59 -0
- package/dist/sandbox/mux-proxy.d.ts.map +1 -0
- package/dist/sandbox/mux-proxy.js +159 -0
- package/dist/sandbox/mux-proxy.js.map +1 -0
- package/dist/sandbox/request-filter.d.ts +11 -1
- package/dist/sandbox/request-filter.d.ts.map +1 -1
- package/dist/sandbox/request-filter.js.map +1 -1
- package/dist/sandbox/sandbox-config.d.ts +254 -25
- package/dist/sandbox/sandbox-config.d.ts.map +1 -1
- package/dist/sandbox/sandbox-config.js +200 -13
- package/dist/sandbox/sandbox-config.js.map +1 -1
- package/dist/sandbox/sandbox-manager.d.ts +4 -0
- package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox/sandbox-manager.js +601 -194
- package/dist/sandbox/sandbox-manager.js.map +1 -1
- package/dist/sandbox/sandbox-schemas.d.ts +15 -0
- package/dist/sandbox/sandbox-schemas.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.d.ts +42 -6
- package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.js +115 -27
- package/dist/sandbox/sandbox-utils.js.map +1 -1
- package/dist/sandbox/socks-proxy.d.ts +11 -5
- package/dist/sandbox/socks-proxy.d.ts.map +1 -1
- package/dist/sandbox/socks-proxy.js +13 -81
- package/dist/sandbox/socks-proxy.js.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.d.ts +2 -2
- package/dist/sandbox/tls-terminate-proxy.d.ts.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.js +31 -5
- package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.d.ts +347 -11
- package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.js +437 -45
- package/dist/sandbox/windows-sandbox-utils.js.map +1 -1
- package/package.json +7 -4
- package/vendor/seccomp/build.ts +8 -30
- package/vendor/srt-win/build.ts +21 -0
- package/dist/vendor/seccomp/Dockerfile.build +0 -6
- package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/dist/vendor/seccomp/build.ts +0 -91
- package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
- package/dist/vendor/seccomp-src/apply-seccomp.c +0 -280
- package/dist/vendor/seccomp-src/seccomp-unix-block.c +0 -148
- package/dist/vendor/srt-win/Cargo.lock +0 -361
- package/dist/vendor/srt-win/Cargo.toml +0 -46
- package/dist/vendor/srt-win/ci/cleanup.ps1 +0 -49
- package/dist/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
- package/dist/vendor/srt-win/ci/smoke.ps1 +0 -307
- package/dist/vendor/srt-win/src/job.rs +0 -102
- package/dist/vendor/srt-win/src/launch.rs +0 -732
- package/dist/vendor/srt-win/src/lib.rs +0 -20
- package/dist/vendor/srt-win/src/main.rs +0 -669
- package/dist/vendor/srt-win/src/self_protect.rs +0 -169
- package/dist/vendor/srt-win/src/sid.rs +0 -296
- package/dist/vendor/srt-win/src/token.rs +0 -341
- package/dist/vendor/srt-win/src/util.rs +0 -42
- package/dist/vendor/srt-win/src/wfp.rs +0 -992
- package/dist/vendor/srt-win/src/winsta.rs +0 -209
- package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
- package/vendor/seccomp-src/apply-seccomp.c +0 -280
- package/vendor/seccomp-src/seccomp-unix-block.c +0 -148
- package/vendor/srt-win/Cargo.lock +0 -361
- package/vendor/srt-win/Cargo.toml +0 -46
- package/vendor/srt-win/ci/cleanup.ps1 +0 -49
- package/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
- package/vendor/srt-win/ci/smoke.ps1 +0 -307
- package/vendor/srt-win/src/job.rs +0 -102
- package/vendor/srt-win/src/launch.rs +0 -732
- package/vendor/srt-win/src/lib.rs +0 -20
- package/vendor/srt-win/src/main.rs +0 -669
- package/vendor/srt-win/src/self_protect.rs +0 -169
- package/vendor/srt-win/src/sid.rs +0 -296
- package/vendor/srt-win/src/token.rs +0 -341
- package/vendor/srt-win/src/util.rs +0 -42
- package/vendor/srt-win/src/wfp.rs +0 -992
- package/vendor/srt-win/src/winsta.rs +0 -209
- package/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
|
@@ -1,992 +0,0 @@
|
|
|
1
|
-
//! Windows Filtering Platform (WFP) filter management and local-group
|
|
2
|
-
//! provisioning for the sandbox-runtime Windows network fence.
|
|
3
|
-
//!
|
|
4
|
-
//! ## Design
|
|
5
|
-
//!
|
|
6
|
-
//! At install time we create a local group (default
|
|
7
|
-
//! `sandbox-runtime-net`), add target users, and persist **one
|
|
8
|
-
//! machine-wide** filter set — four filters at each of
|
|
9
|
-
//! `FWPM_LAYER_ALE_AUTH_CONNECT_V4` and `_V6` (8 total), all under one
|
|
10
|
-
//! persistent sublayer. None of the filters reference a user SID, so
|
|
11
|
-
//! enterprises install once per machine; adding a user to the group
|
|
12
|
-
//! is the only per-user step.
|
|
13
|
-
//!
|
|
14
|
-
//! WFP's `ALE_USER_ID` condition with `FWP_MATCH_EQUAL` evaluates the
|
|
15
|
-
//! supplied security descriptor via `AccessCheck` against the
|
|
16
|
-
//! connecting token: the filter *matches* iff the check grants
|
|
17
|
-
//! access. We use that to discriminate three token states with
|
|
18
|
-
//! respect to `<group_sid>` — **enabled**, **deny-only**, **absent**:
|
|
19
|
-
//!
|
|
20
|
-
//! 0. **PERMIT non-member** (weight 0xF) — SD
|
|
21
|
-
//! `O:LSG:LSD:(D;;CC;;;<group_sid>)(A;;CC;;;WD)`. The DENY ACE on the
|
|
22
|
-
//! group hits any token where the group is present (enabled
|
|
23
|
-
//! *or* deny-only — `SE_GROUP_USE_FOR_DENY_ONLY` SIDs match DENY
|
|
24
|
-
//! ACEs); only tokens with the group *absent* fall through to
|
|
25
|
-
//! ALLOW-Everyone and match. Lets services, SYSTEM, and users
|
|
26
|
-
//! who haven't been added to the group through untouched.
|
|
27
|
-
//!
|
|
28
|
-
//! 1. **PERMIT group-enabled** (weight 0xE) — SD
|
|
29
|
-
//! `O:LSG:LSD:(A;;CC;;;<group_sid>)`. Matches tokens with the group
|
|
30
|
-
//! *enabled* (broker, ordinary processes of a member user).
|
|
31
|
-
//! Tokens with the group deny-only do **not** match: deny-only
|
|
32
|
-
//! SIDs are ignored by ALLOW ACEs.
|
|
33
|
-
//!
|
|
34
|
-
//! 2. **PERMIT loopback** (weight 0xD) — `IP_REMOTE_ADDRESS` is
|
|
35
|
-
//! `127.0.0.0/8` (v4) / `::1` (v6) **and** `IP_REMOTE_PORT` is
|
|
36
|
-
//! in `[low, high]` (default 60080–60089). No user condition.
|
|
37
|
-
//! The sandboxed child reaches the host proxies — which on
|
|
38
|
-
//! Windows bind inside this range — but not arbitrary
|
|
39
|
-
//! loopback listeners. (Linux/macOS restrict the child to
|
|
40
|
-
//! exactly the two proxy ports; this range is the closest
|
|
41
|
-
//! Windows analogue without per-`initialize()` admin.)
|
|
42
|
-
//!
|
|
43
|
-
//! 3. **BLOCK** (weight 0x1) — SD `O:LSG:LSD:(A;;CC;;;WD)`
|
|
44
|
-
//! (ALLOW-Everyone). Matches every token; catches the sandboxed
|
|
45
|
-
//! child for everything off-loopback. The Everyone ACE is
|
|
46
|
-
//! belt-and-braces — a no-condition BLOCK would behave the same
|
|
47
|
-
//! — but keeping an `ALE_USER_ID` condition on every filter
|
|
48
|
-
//! makes enumeration uniform.
|
|
49
|
-
//!
|
|
50
|
-
//! Filters carry a small JSON tag in `providerData` (`{tool, kind,
|
|
51
|
-
//! port_range?}`) so install/uninstall/status can locate them by
|
|
52
|
-
//! enumeration. There
|
|
53
|
-
//! is no marker file: `wfp status` enumerates the live engine; `group
|
|
54
|
-
//! status` queries SAM and the current token directly.
|
|
55
|
-
|
|
56
|
-
// The WFP structs are large and partially-initialised; the
|
|
57
|
-
// `..Default::default()` struct-update form clippy suggests is
|
|
58
|
-
// significantly less readable here than field-by-field assignment.
|
|
59
|
-
#![allow(clippy::field_reassign_with_default)]
|
|
60
|
-
|
|
61
|
-
use anyhow::{anyhow, Context, Result};
|
|
62
|
-
use serde::{Deserialize, Serialize};
|
|
63
|
-
use std::ffi::c_void;
|
|
64
|
-
use windows::core::{GUID, PCWSTR, PWSTR};
|
|
65
|
-
use windows::Win32::Foundation::{
|
|
66
|
-
LocalFree, ERROR_MEMBER_IN_ALIAS, HANDLE, HLOCAL,
|
|
67
|
-
};
|
|
68
|
-
use windows::Win32::NetworkManagement::NetManagement::{
|
|
69
|
-
NetLocalGroupAdd, NetLocalGroupAddMembers, NetLocalGroupDel,
|
|
70
|
-
NERR_GroupExists, NERR_GroupNotFound, LOCALGROUP_INFO_1,
|
|
71
|
-
LOCALGROUP_MEMBERS_INFO_0,
|
|
72
|
-
};
|
|
73
|
-
use windows::Win32::NetworkManagement::WindowsFilteringPlatform::{
|
|
74
|
-
FwpmEngineClose0, FwpmEngineOpen0, FwpmFilterAdd0,
|
|
75
|
-
FwpmFilterCreateEnumHandle0, FwpmFilterDeleteByKey0,
|
|
76
|
-
FwpmFilterDestroyEnumHandle0, FwpmFilterEnum0, FwpmFreeMemory0,
|
|
77
|
-
FwpmSubLayerAdd0, FwpmSubLayerDeleteByKey0, FwpmTransactionAbort0,
|
|
78
|
-
FwpmTransactionBegin0, FwpmTransactionCommit0, FWPM_ACTION0,
|
|
79
|
-
FWPM_ACTION0_0, FWPM_CONDITION_ALE_USER_ID,
|
|
80
|
-
FWPM_CONDITION_IP_REMOTE_ADDRESS, FWPM_CONDITION_IP_REMOTE_PORT,
|
|
81
|
-
FWPM_DISPLAY_DATA0, FWPM_FILTER0, FWPM_FILTER_CONDITION0,
|
|
82
|
-
FWPM_FILTER_ENUM_TEMPLATE0, FWPM_FILTER_FLAG_PERSISTENT,
|
|
83
|
-
FWPM_LAYER_ALE_AUTH_CONNECT_V4, FWPM_LAYER_ALE_AUTH_CONNECT_V6,
|
|
84
|
-
FWPM_SUBLAYER0, FWPM_SUBLAYER_FLAG_PERSISTENT, FWP_ACTION_BLOCK,
|
|
85
|
-
FWP_ACTION_PERMIT, FWP_ACTION_TYPE, FWP_BYTE_ARRAY16,
|
|
86
|
-
FWP_BYTE_ARRAY16_TYPE, FWP_BYTE_BLOB, FWP_CONDITION_VALUE0,
|
|
87
|
-
FWP_CONDITION_VALUE0_0, FWP_FILTER_ENUM_OVERLAPPING, FWP_MATCH_EQUAL,
|
|
88
|
-
FWP_MATCH_RANGE, FWP_RANGE0, FWP_RANGE_TYPE,
|
|
89
|
-
FWP_SECURITY_DESCRIPTOR_TYPE, FWP_UINT16, FWP_UINT64,
|
|
90
|
-
FWP_V4_ADDR_AND_MASK, FWP_V4_ADDR_MASK, FWP_VALUE0, FWP_VALUE0_0,
|
|
91
|
-
};
|
|
92
|
-
use windows::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW;
|
|
93
|
-
use windows::Win32::Security::{
|
|
94
|
-
GetSecurityDescriptorLength, PSECURITY_DESCRIPTOR,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
use crate::sid;
|
|
98
|
-
use crate::util::{pcwstr, wstr};
|
|
99
|
-
|
|
100
|
-
const GROUP_COMMENT: &str = "sandbox-runtime network sandbox membership";
|
|
101
|
-
|
|
102
|
-
/// Default sublayer GUID. Stable so uninstall can find filters from a
|
|
103
|
-
/// previous install. Overridable via `--sublayer-guid` so an
|
|
104
|
-
/// enterprise that provisions WFP via its own tooling can point us at
|
|
105
|
-
/// theirs. {2c5d0ad6-5f3b-4d4e-9b8f-1a3e7c9d0b21}
|
|
106
|
-
pub const DEFAULT_SUBLAYER_GUID: GUID =
|
|
107
|
-
GUID::from_u128(0x2c5d0ad6_5f3b_4d4e_9b8f_1a3e7c9d0b21);
|
|
108
|
-
|
|
109
|
-
/// Default loopback port range for filter 2. The JS http/socks
|
|
110
|
-
/// proxies bind inside this range on Windows so the sandboxed child
|
|
111
|
-
/// can reach them. Ten ports leaves headroom for http + socks +
|
|
112
|
-
/// future listeners and for `EADDRINUSE` retries. Overridable via
|
|
113
|
-
/// `--proxy-port-range`.
|
|
114
|
-
pub const DEFAULT_PROXY_PORT_RANGE: (u16, u16) = (60080, 60089);
|
|
115
|
-
|
|
116
|
-
/// Sanity cap on `--proxy-port-range` width (`high - low`). The
|
|
117
|
-
/// range exists to *narrow* loopback exposure relative to the
|
|
118
|
-
/// previous all-of-127/8 design; an unbounded range would defeat
|
|
119
|
-
/// that.
|
|
120
|
-
pub const MAX_PROXY_PORT_RANGE_WIDTH: u16 = 64;
|
|
121
|
-
|
|
122
|
-
// WFP error codes we treat as benign idempotency outcomes.
|
|
123
|
-
const FWP_E_ALREADY_EXISTS: u32 = 0x80320009;
|
|
124
|
-
const FWP_E_FILTER_NOT_FOUND: u32 = 0x80320003;
|
|
125
|
-
const FWP_E_SUBLAYER_NOT_FOUND: u32 = 0x80320007;
|
|
126
|
-
const FWP_E_IN_USE: u32 = 0x8032000A;
|
|
127
|
-
|
|
128
|
-
const SDDL_REVISION_1: u32 = 1;
|
|
129
|
-
|
|
130
|
-
// ────────────────────── small RAII helpers ──────────────────────
|
|
131
|
-
|
|
132
|
-
/// Heap SD owned by us; freed via `LocalFree`.
|
|
133
|
-
struct OwnedSd {
|
|
134
|
-
ptr: PSECURITY_DESCRIPTOR,
|
|
135
|
-
len: u32,
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
impl OwnedSd {
|
|
139
|
-
fn from_sddl(sddl: &str) -> Result<Self> {
|
|
140
|
-
let w = wstr(sddl);
|
|
141
|
-
let mut psd = PSECURITY_DESCRIPTOR::default();
|
|
142
|
-
let mut sz: u32 = 0;
|
|
143
|
-
unsafe {
|
|
144
|
-
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
|
145
|
-
pcwstr(&w),
|
|
146
|
-
SDDL_REVISION_1,
|
|
147
|
-
&mut psd,
|
|
148
|
-
Some(&mut sz),
|
|
149
|
-
)
|
|
150
|
-
.map_err(|e| {
|
|
151
|
-
anyhow!(
|
|
152
|
-
"ConvertStringSecurityDescriptorToSecurityDescriptorW({sddl}): {e}"
|
|
153
|
-
)
|
|
154
|
-
})?;
|
|
155
|
-
if sz == 0 {
|
|
156
|
-
sz = GetSecurityDescriptorLength(psd);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
Ok(Self { ptr: psd, len: sz })
|
|
160
|
-
}
|
|
161
|
-
fn byte_blob(&self) -> FWP_BYTE_BLOB {
|
|
162
|
-
FWP_BYTE_BLOB {
|
|
163
|
-
size: self.len,
|
|
164
|
-
data: self.ptr.0 as *mut u8,
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
impl Drop for OwnedSd {
|
|
170
|
-
fn drop(&mut self) {
|
|
171
|
-
if !self.ptr.0.is_null() {
|
|
172
|
-
unsafe {
|
|
173
|
-
let _ = LocalFree(Some(HLOCAL(self.ptr.0)));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/// WFP engine handle; closed on drop.
|
|
180
|
-
struct EngineHandle(HANDLE);
|
|
181
|
-
|
|
182
|
-
impl EngineHandle {
|
|
183
|
-
fn open() -> Result<Self> {
|
|
184
|
-
let mut h = HANDLE::default();
|
|
185
|
-
// RPC_C_AUTHN_DEFAULT
|
|
186
|
-
let rc = unsafe {
|
|
187
|
-
FwpmEngineOpen0(PCWSTR::null(), 0xFFFF_FFFF, None, None, &mut h)
|
|
188
|
-
};
|
|
189
|
-
if rc != 0 {
|
|
190
|
-
return Err(anyhow!("FwpmEngineOpen0 failed: 0x{rc:08x}"));
|
|
191
|
-
}
|
|
192
|
-
Ok(Self(h))
|
|
193
|
-
}
|
|
194
|
-
fn h(&self) -> HANDLE {
|
|
195
|
-
self.0
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
impl Drop for EngineHandle {
|
|
200
|
-
fn drop(&mut self) {
|
|
201
|
-
if !self.0.is_invalid() {
|
|
202
|
-
unsafe {
|
|
203
|
-
let _ = FwpmEngineClose0(self.0);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ────────────────────── condition builders ──────────────────────
|
|
210
|
-
|
|
211
|
-
fn fwp_uint64(slot: &mut u64) -> FWP_VALUE0 {
|
|
212
|
-
FWP_VALUE0 {
|
|
213
|
-
r#type: FWP_UINT64,
|
|
214
|
-
Anonymous: FWP_VALUE0_0 {
|
|
215
|
-
uint64: slot as *mut u64,
|
|
216
|
-
},
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
fn cond_sd(field_key: GUID, blob: &mut FWP_BYTE_BLOB) -> FWPM_FILTER_CONDITION0 {
|
|
221
|
-
FWPM_FILTER_CONDITION0 {
|
|
222
|
-
fieldKey: field_key,
|
|
223
|
-
matchType: FWP_MATCH_EQUAL,
|
|
224
|
-
conditionValue: FWP_CONDITION_VALUE0 {
|
|
225
|
-
r#type: FWP_SECURITY_DESCRIPTOR_TYPE,
|
|
226
|
-
Anonymous: FWP_CONDITION_VALUE0_0 {
|
|
227
|
-
sd: blob as *mut _,
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
fn cond_v4_subnet(
|
|
234
|
-
field_key: GUID,
|
|
235
|
-
am: &mut FWP_V4_ADDR_AND_MASK,
|
|
236
|
-
) -> FWPM_FILTER_CONDITION0 {
|
|
237
|
-
FWPM_FILTER_CONDITION0 {
|
|
238
|
-
fieldKey: field_key,
|
|
239
|
-
matchType: FWP_MATCH_EQUAL,
|
|
240
|
-
conditionValue: FWP_CONDITION_VALUE0 {
|
|
241
|
-
r#type: FWP_V4_ADDR_MASK,
|
|
242
|
-
Anonymous: FWP_CONDITION_VALUE0_0 {
|
|
243
|
-
v4AddrMask: am as *mut _,
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
fn cond_v6_addr(
|
|
250
|
-
field_key: GUID,
|
|
251
|
-
addr: &mut FWP_BYTE_ARRAY16,
|
|
252
|
-
) -> FWPM_FILTER_CONDITION0 {
|
|
253
|
-
FWPM_FILTER_CONDITION0 {
|
|
254
|
-
fieldKey: field_key,
|
|
255
|
-
matchType: FWP_MATCH_EQUAL,
|
|
256
|
-
conditionValue: FWP_CONDITION_VALUE0 {
|
|
257
|
-
r#type: FWP_BYTE_ARRAY16_TYPE,
|
|
258
|
-
Anonymous: FWP_CONDITION_VALUE0_0 {
|
|
259
|
-
byteArray16: addr as *mut _,
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
fn fwp_uint16(v: u16) -> FWP_VALUE0 {
|
|
266
|
-
FWP_VALUE0 {
|
|
267
|
-
r#type: FWP_UINT16,
|
|
268
|
-
Anonymous: FWP_VALUE0_0 { uint16: v },
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
fn cond_port_range(
|
|
273
|
-
field_key: GUID,
|
|
274
|
-
range: &mut FWP_RANGE0,
|
|
275
|
-
) -> FWPM_FILTER_CONDITION0 {
|
|
276
|
-
FWPM_FILTER_CONDITION0 {
|
|
277
|
-
fieldKey: field_key,
|
|
278
|
-
matchType: FWP_MATCH_RANGE,
|
|
279
|
-
conditionValue: FWP_CONDITION_VALUE0 {
|
|
280
|
-
r#type: FWP_RANGE_TYPE,
|
|
281
|
-
Anonymous: FWP_CONDITION_VALUE0_0 {
|
|
282
|
-
rangeValue: range as *mut _,
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ────────────────────── filter tagging ──────────────────────
|
|
289
|
-
|
|
290
|
-
/// JSON payload stored in each filter's `providerData` so we can
|
|
291
|
-
/// identify our own filters during enumerate/uninstall without fixed
|
|
292
|
-
/// filter GUIDs. The optional `port_range` mirrors the
|
|
293
|
-
/// `IP_REMOTE_PORT` range condition on `permit-loopback` filters so
|
|
294
|
-
/// `wfp status` can report it without unsafe condition-walking.
|
|
295
|
-
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
|
296
|
-
pub struct FilterTag {
|
|
297
|
-
/// Discriminator: `"srt-win"`. Anything else means the filter
|
|
298
|
-
/// belongs to some other tool that happens to share our sublayer.
|
|
299
|
-
pub tool: String,
|
|
300
|
-
/// One of `permit-nonmember`, `permit-group`, `permit-loopback`,
|
|
301
|
-
/// `block`.
|
|
302
|
-
pub kind: String,
|
|
303
|
-
/// `[low, high]` for `permit-loopback`; `None` otherwise (and on
|
|
304
|
-
/// pre-port-range installs).
|
|
305
|
-
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
306
|
-
pub port_range: Option<[u16; 2]>,
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
impl FilterTag {
|
|
310
|
-
fn new(kind: &str) -> Self {
|
|
311
|
-
Self {
|
|
312
|
-
tool: "srt-win".into(),
|
|
313
|
-
kind: kind.into(),
|
|
314
|
-
port_range: None,
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
fn loopback(range: (u16, u16)) -> Self {
|
|
318
|
-
Self {
|
|
319
|
-
tool: "srt-win".into(),
|
|
320
|
-
kind: "permit-loopback".into(),
|
|
321
|
-
port_range: Some([range.0, range.1]),
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
fn to_blob_bytes(&self) -> Vec<u8> {
|
|
325
|
-
serde_json::to_vec(self).expect("FilterTag is always serialisable")
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ────────────────────── local group management ──────────────────────
|
|
330
|
-
|
|
331
|
-
/// Create the local group if it doesn't exist and add `user_sid` to
|
|
332
|
-
/// it. Idempotent.
|
|
333
|
-
pub fn ensure_group(name: &str, user_sid: &str) -> Result<()> {
|
|
334
|
-
unsafe {
|
|
335
|
-
let mut name_w = wstr(name);
|
|
336
|
-
let mut comment_w = wstr(GROUP_COMMENT);
|
|
337
|
-
let info = LOCALGROUP_INFO_1 {
|
|
338
|
-
lgrpi1_name: PWSTR(name_w.as_mut_ptr()),
|
|
339
|
-
lgrpi1_comment: PWSTR(comment_w.as_mut_ptr()),
|
|
340
|
-
};
|
|
341
|
-
let rc = NetLocalGroupAdd(
|
|
342
|
-
PCWSTR::null(),
|
|
343
|
-
1,
|
|
344
|
-
&info as *const _ as *const u8,
|
|
345
|
-
None,
|
|
346
|
-
);
|
|
347
|
-
// SAM returns ERROR_ALIAS_EXISTS (1379) for an existing local
|
|
348
|
-
// group; some paths return NERR_GroupExists (2223). Either is
|
|
349
|
-
// fine for idempotency.
|
|
350
|
-
const ERROR_ALIAS_EXISTS: u32 = 1379;
|
|
351
|
-
if rc != 0 && rc != NERR_GroupExists && rc != ERROR_ALIAS_EXISTS {
|
|
352
|
-
return Err(anyhow!("NetLocalGroupAdd({name}): {rc}"));
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
let psid = sid::LocalPsid::from_string(user_sid)?;
|
|
356
|
-
unsafe {
|
|
357
|
-
let name_w = wstr(name);
|
|
358
|
-
let info = LOCALGROUP_MEMBERS_INFO_0 {
|
|
359
|
-
lgrmi0_sid: psid.as_psid(),
|
|
360
|
-
};
|
|
361
|
-
let rc = NetLocalGroupAddMembers(
|
|
362
|
-
PCWSTR::null(),
|
|
363
|
-
pcwstr(&name_w),
|
|
364
|
-
0,
|
|
365
|
-
&info as *const _ as *const u8,
|
|
366
|
-
1,
|
|
367
|
-
);
|
|
368
|
-
if rc != 0 && rc != ERROR_MEMBER_IN_ALIAS.0 {
|
|
369
|
-
return Err(anyhow!(
|
|
370
|
-
"NetLocalGroupAddMembers({name}, {user_sid}): {rc}"
|
|
371
|
-
));
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
Ok(())
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/// Delete the local group. Idempotent on `NERR_GroupNotFound`.
|
|
378
|
-
pub fn delete_group(name: &str) -> Result<()> {
|
|
379
|
-
unsafe {
|
|
380
|
-
let name_w = wstr(name);
|
|
381
|
-
let rc = NetLocalGroupDel(PCWSTR::null(), pcwstr(&name_w));
|
|
382
|
-
// 2220 (NERR_GroupNotFound) and 1376 (ERROR_NO_SUCH_ALIAS) both
|
|
383
|
-
// mean "already gone" depending on Windows version.
|
|
384
|
-
const ERROR_NO_SUCH_ALIAS: u32 = 1376;
|
|
385
|
-
if rc != 0 && rc != NERR_GroupNotFound && rc != ERROR_NO_SUCH_ALIAS {
|
|
386
|
-
return Err(anyhow!("NetLocalGroupDel({name}): {rc}"));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
Ok(())
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ────────────────────── filter enumeration ──────────────────────
|
|
393
|
-
|
|
394
|
-
const ALE_LAYERS: [(GUID, &str); 2] = [
|
|
395
|
-
(FWPM_LAYER_ALE_AUTH_CONNECT_V4, "ale_auth_connect_v4"),
|
|
396
|
-
(FWPM_LAYER_ALE_AUTH_CONNECT_V6, "ale_auth_connect_v6"),
|
|
397
|
-
];
|
|
398
|
-
|
|
399
|
-
/// Walk every filter at the two ALE connect layers under
|
|
400
|
-
/// `sublayer` that carries a parseable `srt-win` providerData tag,
|
|
401
|
-
/// invoking `f(layer_name, filterKey, &tag)` for each. Owns the
|
|
402
|
-
/// enum-handle and per-batch `FwpmFreeMemory0` lifecycle so callers
|
|
403
|
-
/// don't duplicate the unsafe FFI walk.
|
|
404
|
-
///
|
|
405
|
-
/// Errors are propagated (with the enum handle destroyed first) —
|
|
406
|
-
/// don't swallow them: inside `install_filters`' txn, a missed enum
|
|
407
|
-
/// error would skip stale-filter cleanup and the fresh set would be
|
|
408
|
-
/// added on top, growing the filter count every install.
|
|
409
|
-
fn for_each_tagged_filter(
|
|
410
|
-
engine: &EngineHandle,
|
|
411
|
-
sublayer: &GUID,
|
|
412
|
-
mut f: impl FnMut(&'static str, GUID, &FilterTag),
|
|
413
|
-
) -> Result<()> {
|
|
414
|
-
for (layer, layer_name) in ALE_LAYERS {
|
|
415
|
-
let mut tmpl = FWPM_FILTER_ENUM_TEMPLATE0::default();
|
|
416
|
-
tmpl.layerKey = layer;
|
|
417
|
-
tmpl.enumType = FWP_FILTER_ENUM_OVERLAPPING;
|
|
418
|
-
tmpl.actionMask = 0xFFFF_FFFF;
|
|
419
|
-
let mut h = HANDLE::default();
|
|
420
|
-
let rc = unsafe {
|
|
421
|
-
FwpmFilterCreateEnumHandle0(engine.h(), Some(&tmpl), &mut h)
|
|
422
|
-
};
|
|
423
|
-
if rc != 0 {
|
|
424
|
-
return Err(anyhow!(
|
|
425
|
-
"FwpmFilterCreateEnumHandle0({layer_name}): 0x{rc:08x}"
|
|
426
|
-
));
|
|
427
|
-
}
|
|
428
|
-
loop {
|
|
429
|
-
let mut entries: *mut *mut FWPM_FILTER0 = std::ptr::null_mut();
|
|
430
|
-
let mut n: u32 = 0;
|
|
431
|
-
let rc = unsafe {
|
|
432
|
-
FwpmFilterEnum0(engine.h(), h, 256, &mut entries, &mut n)
|
|
433
|
-
};
|
|
434
|
-
if rc != 0 {
|
|
435
|
-
unsafe {
|
|
436
|
-
let _ = FwpmFilterDestroyEnumHandle0(engine.h(), h);
|
|
437
|
-
}
|
|
438
|
-
return Err(anyhow!(
|
|
439
|
-
"FwpmFilterEnum0({layer_name}): 0x{rc:08x}"
|
|
440
|
-
));
|
|
441
|
-
}
|
|
442
|
-
if n == 0 {
|
|
443
|
-
if !entries.is_null() {
|
|
444
|
-
unsafe {
|
|
445
|
-
FwpmFreeMemory0(&mut (entries as *mut c_void));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
let slice =
|
|
451
|
-
unsafe { std::slice::from_raw_parts(entries, n as usize) };
|
|
452
|
-
for &fp in slice {
|
|
453
|
-
if fp.is_null() {
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
let flt = unsafe { &*fp };
|
|
457
|
-
if &flt.subLayerKey != sublayer {
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
if flt.providerData.size == 0
|
|
461
|
-
|| flt.providerData.data.is_null()
|
|
462
|
-
{
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
let bytes = unsafe {
|
|
466
|
-
std::slice::from_raw_parts(
|
|
467
|
-
flt.providerData.data,
|
|
468
|
-
flt.providerData.size as usize,
|
|
469
|
-
)
|
|
470
|
-
};
|
|
471
|
-
if let Ok(tag) = serde_json::from_slice::<FilterTag>(bytes)
|
|
472
|
-
&& tag.tool == "srt-win"
|
|
473
|
-
{
|
|
474
|
-
// `tag` is owned (parsed from bytes); the
|
|
475
|
-
// `flt`/`bytes` borrows are released before
|
|
476
|
-
// FwpmFreeMemory0 below, so no FFI lifetime
|
|
477
|
-
// hazard for the closure.
|
|
478
|
-
f(layer_name, flt.filterKey, &tag);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
unsafe {
|
|
482
|
-
FwpmFreeMemory0(&mut (entries as *mut c_void));
|
|
483
|
-
}
|
|
484
|
-
if (n as usize) < 256 {
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
unsafe {
|
|
489
|
-
let _ = FwpmFilterDestroyEnumHandle0(engine.h(), h);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
Ok(())
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/// Delete every srt-win-tagged filter under `sublayer`. Returns the
|
|
496
|
-
/// number deleted. Does not delete the sublayer itself.
|
|
497
|
-
fn delete_tagged_filters(
|
|
498
|
-
engine: &EngineHandle,
|
|
499
|
-
sublayer: &GUID,
|
|
500
|
-
) -> Result<usize> {
|
|
501
|
-
// Collect across both layers, then delete. Deletion is by global
|
|
502
|
-
// filterKey GUID inside one txn, so per-layer ordering is not
|
|
503
|
-
// load-bearing.
|
|
504
|
-
let mut to_delete: Vec<GUID> = Vec::new();
|
|
505
|
-
for_each_tagged_filter(engine, sublayer, |_, key, _| {
|
|
506
|
-
to_delete.push(key);
|
|
507
|
-
})?;
|
|
508
|
-
let mut deleted = 0usize;
|
|
509
|
-
for key in to_delete {
|
|
510
|
-
let rc = unsafe { FwpmFilterDeleteByKey0(engine.h(), &key) };
|
|
511
|
-
if rc == 0 {
|
|
512
|
-
deleted += 1;
|
|
513
|
-
} else if rc != FWP_E_FILTER_NOT_FOUND {
|
|
514
|
-
return Err(anyhow!(
|
|
515
|
-
"FwpmFilterDeleteByKey0({key:?}): 0x{rc:08x}"
|
|
516
|
-
));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
Ok(deleted)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// ────────────────────── install / uninstall ──────────────────────
|
|
523
|
-
|
|
524
|
-
#[allow(clippy::too_many_arguments)]
|
|
525
|
-
fn add_filter(
|
|
526
|
-
engine: HANDLE,
|
|
527
|
-
sublayer: &GUID,
|
|
528
|
-
layer: GUID,
|
|
529
|
-
name: &str,
|
|
530
|
-
weight: u64,
|
|
531
|
-
action: FWP_ACTION_TYPE,
|
|
532
|
-
conditions: &mut [FWPM_FILTER_CONDITION0],
|
|
533
|
-
tag_bytes: &mut [u8],
|
|
534
|
-
) -> Result<()> {
|
|
535
|
-
let mut name_w = wstr(name);
|
|
536
|
-
let mut desc_w = wstr("sandbox-runtime WFP filter");
|
|
537
|
-
let mut weight_slot = weight;
|
|
538
|
-
let mut filter = FWPM_FILTER0::default();
|
|
539
|
-
// filterKey left zeroed → WFP assigns a fresh GUID. We identify
|
|
540
|
-
// our filters via providerData, not by fixed key.
|
|
541
|
-
filter.displayData = FWPM_DISPLAY_DATA0 {
|
|
542
|
-
name: PWSTR(name_w.as_mut_ptr()),
|
|
543
|
-
description: PWSTR(desc_w.as_mut_ptr()),
|
|
544
|
-
};
|
|
545
|
-
filter.flags = FWPM_FILTER_FLAG_PERSISTENT;
|
|
546
|
-
filter.layerKey = layer;
|
|
547
|
-
filter.subLayerKey = *sublayer;
|
|
548
|
-
filter.weight = fwp_uint64(&mut weight_slot);
|
|
549
|
-
filter.numFilterConditions = conditions.len() as u32;
|
|
550
|
-
filter.filterCondition = if conditions.is_empty() {
|
|
551
|
-
std::ptr::null_mut()
|
|
552
|
-
} else {
|
|
553
|
-
conditions.as_mut_ptr()
|
|
554
|
-
};
|
|
555
|
-
filter.action = FWPM_ACTION0 {
|
|
556
|
-
r#type: action,
|
|
557
|
-
Anonymous: FWPM_ACTION0_0 {
|
|
558
|
-
filterType: GUID::zeroed(),
|
|
559
|
-
},
|
|
560
|
-
};
|
|
561
|
-
filter.providerData = FWP_BYTE_BLOB {
|
|
562
|
-
size: tag_bytes.len() as u32,
|
|
563
|
-
data: tag_bytes.as_mut_ptr(),
|
|
564
|
-
};
|
|
565
|
-
let rc = unsafe { FwpmFilterAdd0(engine, &filter, None, None) };
|
|
566
|
-
if rc != 0 && rc != FWP_E_ALREADY_EXISTS {
|
|
567
|
-
return Err(anyhow!("FwpmFilterAdd0({name}): 0x{rc:08x}"));
|
|
568
|
-
}
|
|
569
|
-
Ok(())
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// SDDL builders for the three ALE_USER_ID security descriptors.
|
|
573
|
-
//
|
|
574
|
-
// All carry `O:LS G:LS` (owner + primary group = LocalService).
|
|
575
|
-
// WFP's kernel-side ALE_USER_ID match doesn't require the primary
|
|
576
|
-
// group to be set, but user-mode `AccessCheck` — which
|
|
577
|
-
// `tests/sd_access_check_matrix.rs` uses to prove these SDs do
|
|
578
|
-
// what we claim — returns ERROR_INVALID_SECURITY_DESCR for an SD
|
|
579
|
-
// with no `G:`. The group's value is irrelevant to DACL
|
|
580
|
-
// evaluation; LS is just a stable, always-present principal.
|
|
581
|
-
|
|
582
|
-
/// SDDL for filter 0 — DENY `<group_sid>` then ALLOW Everyone.
|
|
583
|
-
/// `AccessCheck` against this SD grants iff the token does **not**
|
|
584
|
-
/// carry the group at all (deny-only counts as carrying it). DENY
|
|
585
|
-
/// before ALLOW is the canonical ACE order.
|
|
586
|
-
pub fn sddl_nonmember(group_sid: &str) -> String {
|
|
587
|
-
format!("O:LSG:LSD:(D;;CC;;;{group_sid})(A;;CC;;;WD)")
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/// SDDL for filter 1 — ALLOW `<group_sid>`. Grants iff the group is
|
|
591
|
-
/// **enabled** in the token; deny-only SIDs are ignored by ALLOW
|
|
592
|
-
/// ACEs.
|
|
593
|
-
pub fn sddl_group(group_sid: &str) -> String {
|
|
594
|
-
format!("O:LSG:LSD:(A;;CC;;;{group_sid})")
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/// SDDL for filter 3 — ALLOW Everyone. Grants for every token.
|
|
598
|
-
pub const SDDL_EVERYONE: &str = "O:LSG:LSD:(A;;CC;;;WD)";
|
|
599
|
-
|
|
600
|
-
/// Install (or refresh) the eight machine-wide filters under
|
|
601
|
-
/// `sublayer`, keyed only on `group_sid`. Filter 2's loopback
|
|
602
|
-
/// permit is restricted to `port_range` (inclusive). Idempotent:
|
|
603
|
-
/// any existing srt-win-tagged filters are deleted first, then a
|
|
604
|
-
/// fresh set is added, all inside one WFP transaction.
|
|
605
|
-
pub fn install_filters(
|
|
606
|
-
sublayer: &GUID,
|
|
607
|
-
group_sid: &str,
|
|
608
|
-
port_range: (u16, u16),
|
|
609
|
-
) -> Result<()> {
|
|
610
|
-
debug_assert!(port_range.0 <= port_range.1);
|
|
611
|
-
let sd_nonmember = OwnedSd::from_sddl(&sddl_nonmember(group_sid))
|
|
612
|
-
.context("build non-member SD")?;
|
|
613
|
-
let sd_group = OwnedSd::from_sddl(&sddl_group(group_sid))
|
|
614
|
-
.context("build group SD")?;
|
|
615
|
-
let sd_everyone =
|
|
616
|
-
OwnedSd::from_sddl(SDDL_EVERYONE).context("build Everyone SD")?;
|
|
617
|
-
|
|
618
|
-
let engine = EngineHandle::open()?;
|
|
619
|
-
let rc = unsafe { FwpmTransactionBegin0(engine.h(), 0) };
|
|
620
|
-
if rc != 0 {
|
|
621
|
-
return Err(anyhow!("FwpmTransactionBegin0: 0x{rc:08x}"));
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
let result: Result<()> = (|| {
|
|
625
|
-
// Sublayer (idempotent). Display name identifies the owning
|
|
626
|
-
// tool, not the group.
|
|
627
|
-
let mut sl_name = wstr("srt-win");
|
|
628
|
-
let mut sl_desc =
|
|
629
|
-
wstr("sandbox-runtime WFP sublayer (deny-only-group fence)");
|
|
630
|
-
let sl = FWPM_SUBLAYER0 {
|
|
631
|
-
subLayerKey: *sublayer,
|
|
632
|
-
displayData: FWPM_DISPLAY_DATA0 {
|
|
633
|
-
name: PWSTR(sl_name.as_mut_ptr()),
|
|
634
|
-
description: PWSTR(sl_desc.as_mut_ptr()),
|
|
635
|
-
},
|
|
636
|
-
flags: FWPM_SUBLAYER_FLAG_PERSISTENT,
|
|
637
|
-
providerKey: std::ptr::null_mut(),
|
|
638
|
-
providerData: FWP_BYTE_BLOB {
|
|
639
|
-
size: 0,
|
|
640
|
-
data: std::ptr::null_mut(),
|
|
641
|
-
},
|
|
642
|
-
weight: 0x8000,
|
|
643
|
-
};
|
|
644
|
-
let rc = unsafe { FwpmSubLayerAdd0(engine.h(), &sl, None) };
|
|
645
|
-
if rc != 0 && rc != FWP_E_ALREADY_EXISTS {
|
|
646
|
-
return Err(anyhow!("FwpmSubLayerAdd0: 0x{rc:08x}"));
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Idempotency: drop any stale filter set before re-adding.
|
|
650
|
-
// (Inside the transaction so a crash leaves the previous
|
|
651
|
-
// state intact.)
|
|
652
|
-
let _ = delete_tagged_filters(&engine, sublayer)?;
|
|
653
|
-
|
|
654
|
-
// Weights — kept below 2^60 so we stay in WFP's "manual
|
|
655
|
-
// weight" class (top 4 bits are auto-classifier).
|
|
656
|
-
const W_NONMEMBER: u64 = 0x0F00_0000_0000_0000;
|
|
657
|
-
const W_GROUP: u64 = 0x0E00_0000_0000_0000;
|
|
658
|
-
const W_LOOPBACK: u64 = 0x0D00_0000_0000_0000;
|
|
659
|
-
const W_BLOCK: u64 = 0x0100_0000_0000_0000;
|
|
660
|
-
|
|
661
|
-
let mut sd_nonmember_blob = sd_nonmember.byte_blob();
|
|
662
|
-
let mut sd_group_blob = sd_group.byte_blob();
|
|
663
|
-
let mut sd_everyone_blob = sd_everyone.byte_blob();
|
|
664
|
-
|
|
665
|
-
// 127.0.0.0/8
|
|
666
|
-
let mut v4_loop = FWP_V4_ADDR_AND_MASK {
|
|
667
|
-
addr: 0x7F00_0000,
|
|
668
|
-
mask: 0xFF00_0000,
|
|
669
|
-
};
|
|
670
|
-
// ::1
|
|
671
|
-
let mut v6_loop = FWP_BYTE_ARRAY16 {
|
|
672
|
-
byteArray16: [0; 16],
|
|
673
|
-
};
|
|
674
|
-
v6_loop.byteArray16[15] = 1;
|
|
675
|
-
// remote port ∈ [low, high]
|
|
676
|
-
let mut port_range_slot = FWP_RANGE0 {
|
|
677
|
-
valueLow: fwp_uint16(port_range.0),
|
|
678
|
-
valueHigh: fwp_uint16(port_range.1),
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
let mut tag_nm = FilterTag::new("permit-nonmember").to_blob_bytes();
|
|
682
|
-
let mut tag_gp = FilterTag::new("permit-group").to_blob_bytes();
|
|
683
|
-
let mut tag_lb = FilterTag::loopback(port_range).to_blob_bytes();
|
|
684
|
-
let mut tag_bk = FilterTag::new("block").to_blob_bytes();
|
|
685
|
-
|
|
686
|
-
for (layer, label) in [
|
|
687
|
-
(FWPM_LAYER_ALE_AUTH_CONNECT_V4, "v4"),
|
|
688
|
-
(FWPM_LAYER_ALE_AUTH_CONNECT_V6, "v6"),
|
|
689
|
-
] {
|
|
690
|
-
// 0 — PERMIT non-member.
|
|
691
|
-
let mut c0 = [cond_sd(
|
|
692
|
-
FWPM_CONDITION_ALE_USER_ID,
|
|
693
|
-
&mut sd_nonmember_blob,
|
|
694
|
-
)];
|
|
695
|
-
add_filter(
|
|
696
|
-
engine.h(),
|
|
697
|
-
sublayer,
|
|
698
|
-
layer,
|
|
699
|
-
&format!("srt-win-{label}-permit-nonmember"),
|
|
700
|
-
W_NONMEMBER,
|
|
701
|
-
FWP_ACTION_PERMIT,
|
|
702
|
-
&mut c0,
|
|
703
|
-
&mut tag_nm,
|
|
704
|
-
)?;
|
|
705
|
-
|
|
706
|
-
// 1 — PERMIT group-enabled.
|
|
707
|
-
let mut c1 =
|
|
708
|
-
[cond_sd(FWPM_CONDITION_ALE_USER_ID, &mut sd_group_blob)];
|
|
709
|
-
add_filter(
|
|
710
|
-
engine.h(),
|
|
711
|
-
sublayer,
|
|
712
|
-
layer,
|
|
713
|
-
&format!("srt-win-{label}-permit-group"),
|
|
714
|
-
W_GROUP,
|
|
715
|
-
FWP_ACTION_PERMIT,
|
|
716
|
-
&mut c1,
|
|
717
|
-
&mut tag_gp,
|
|
718
|
-
)?;
|
|
719
|
-
|
|
720
|
-
// 2 — PERMIT loopback ∩ port-range (no user condition).
|
|
721
|
-
// Two conditions on different fieldKeys → ANDed by WFP.
|
|
722
|
-
let addr_cond = if label == "v4" {
|
|
723
|
-
cond_v4_subnet(
|
|
724
|
-
FWPM_CONDITION_IP_REMOTE_ADDRESS,
|
|
725
|
-
&mut v4_loop,
|
|
726
|
-
)
|
|
727
|
-
} else {
|
|
728
|
-
cond_v6_addr(FWPM_CONDITION_IP_REMOTE_ADDRESS, &mut v6_loop)
|
|
729
|
-
};
|
|
730
|
-
let mut c2 = [
|
|
731
|
-
addr_cond,
|
|
732
|
-
cond_port_range(
|
|
733
|
-
FWPM_CONDITION_IP_REMOTE_PORT,
|
|
734
|
-
&mut port_range_slot,
|
|
735
|
-
),
|
|
736
|
-
];
|
|
737
|
-
add_filter(
|
|
738
|
-
engine.h(),
|
|
739
|
-
sublayer,
|
|
740
|
-
layer,
|
|
741
|
-
&format!("srt-win-{label}-permit-loopback"),
|
|
742
|
-
W_LOOPBACK,
|
|
743
|
-
FWP_ACTION_PERMIT,
|
|
744
|
-
&mut c2,
|
|
745
|
-
&mut tag_lb,
|
|
746
|
-
)?;
|
|
747
|
-
|
|
748
|
-
// 3 — BLOCK Everyone.
|
|
749
|
-
let mut c3 = [cond_sd(
|
|
750
|
-
FWPM_CONDITION_ALE_USER_ID,
|
|
751
|
-
&mut sd_everyone_blob,
|
|
752
|
-
)];
|
|
753
|
-
add_filter(
|
|
754
|
-
engine.h(),
|
|
755
|
-
sublayer,
|
|
756
|
-
layer,
|
|
757
|
-
&format!("srt-win-{label}-block"),
|
|
758
|
-
W_BLOCK,
|
|
759
|
-
FWP_ACTION_BLOCK,
|
|
760
|
-
&mut c3,
|
|
761
|
-
&mut tag_bk,
|
|
762
|
-
)?;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
Ok(())
|
|
766
|
-
})();
|
|
767
|
-
|
|
768
|
-
if let Err(e) = result {
|
|
769
|
-
unsafe {
|
|
770
|
-
let _ = FwpmTransactionAbort0(engine.h());
|
|
771
|
-
}
|
|
772
|
-
return Err(e);
|
|
773
|
-
}
|
|
774
|
-
let rc = unsafe { FwpmTransactionCommit0(engine.h()) };
|
|
775
|
-
if rc != 0 {
|
|
776
|
-
return Err(anyhow!("FwpmTransactionCommit0: 0x{rc:08x}"));
|
|
777
|
-
}
|
|
778
|
-
Ok(())
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/// Remove every srt-win-tagged filter under `sublayer`, then attempt
|
|
782
|
-
/// to delete the sublayer itself (best-effort; `FWP_E_IN_USE` means
|
|
783
|
-
/// foreign filters are still under it).
|
|
784
|
-
pub fn uninstall_filters(sublayer: &GUID) -> Result<usize> {
|
|
785
|
-
let engine = EngineHandle::open()?;
|
|
786
|
-
let rc = unsafe { FwpmTransactionBegin0(engine.h(), 0) };
|
|
787
|
-
if rc != 0 {
|
|
788
|
-
return Err(anyhow!("FwpmTransactionBegin0: 0x{rc:08x}"));
|
|
789
|
-
}
|
|
790
|
-
let n = match delete_tagged_filters(&engine, sublayer) {
|
|
791
|
-
Ok(n) => n,
|
|
792
|
-
Err(e) => {
|
|
793
|
-
unsafe {
|
|
794
|
-
let _ = FwpmTransactionAbort0(engine.h());
|
|
795
|
-
}
|
|
796
|
-
return Err(e);
|
|
797
|
-
}
|
|
798
|
-
};
|
|
799
|
-
let rc = unsafe { FwpmSubLayerDeleteByKey0(engine.h(), sublayer) };
|
|
800
|
-
if rc != 0
|
|
801
|
-
&& rc != FWP_E_SUBLAYER_NOT_FOUND
|
|
802
|
-
&& rc != FWP_E_FILTER_NOT_FOUND
|
|
803
|
-
&& rc != FWP_E_IN_USE
|
|
804
|
-
{
|
|
805
|
-
unsafe {
|
|
806
|
-
let _ = FwpmTransactionAbort0(engine.h());
|
|
807
|
-
}
|
|
808
|
-
return Err(anyhow!("FwpmSubLayerDeleteByKey0: 0x{rc:08x}"));
|
|
809
|
-
}
|
|
810
|
-
let rc = unsafe { FwpmTransactionCommit0(engine.h()) };
|
|
811
|
-
if rc != 0 {
|
|
812
|
-
return Err(anyhow!("FwpmTransactionCommit0: 0x{rc:08x}"));
|
|
813
|
-
}
|
|
814
|
-
Ok(n)
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/// Status of the WFP fence under `sublayer`. `installed` iff at
|
|
818
|
-
/// least one `permit-group` and one `block` srt-win filter exist.
|
|
819
|
-
/// (We don't insist on the exact count so enterprise tooling that
|
|
820
|
-
/// adds extras under the same sublayer doesn't break detection.)
|
|
821
|
-
/// `port_range` is read from the first `permit-loopback` tag;
|
|
822
|
-
/// `None` when no loopback filter is present or it predates the
|
|
823
|
-
/// port-range design.
|
|
824
|
-
#[derive(Debug, Serialize)]
|
|
825
|
-
pub struct WfpStatus {
|
|
826
|
-
pub state: &'static str,
|
|
827
|
-
pub filters: usize,
|
|
828
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
829
|
-
pub port_range: Option<[u16; 2]>,
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
pub fn filter_status(sublayer: &GUID) -> Result<WfpStatus> {
|
|
833
|
-
let engine = EngineHandle::open()?;
|
|
834
|
-
let mut filters = 0usize;
|
|
835
|
-
let mut have_permit_group = false;
|
|
836
|
-
let mut have_block = false;
|
|
837
|
-
let mut port_range: Option<[u16; 2]> = None;
|
|
838
|
-
for_each_tagged_filter(&engine, sublayer, |_, _, tag| {
|
|
839
|
-
filters += 1;
|
|
840
|
-
match tag.kind.as_str() {
|
|
841
|
-
"permit-group" => have_permit_group = true,
|
|
842
|
-
"block" => have_block = true,
|
|
843
|
-
"permit-loopback" if port_range.is_none() => {
|
|
844
|
-
port_range = tag.port_range;
|
|
845
|
-
}
|
|
846
|
-
_ => {}
|
|
847
|
-
}
|
|
848
|
-
})?;
|
|
849
|
-
let state = if have_permit_group && have_block {
|
|
850
|
-
"installed"
|
|
851
|
-
} else {
|
|
852
|
-
"absent"
|
|
853
|
-
};
|
|
854
|
-
Ok(WfpStatus { state, filters, port_range })
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
/// Parse a `--proxy-port-range LOW-HIGH` argument. Both ends are
|
|
858
|
-
/// inclusive. Validates `low <= high` and width `<=
|
|
859
|
-
/// MAX_PROXY_PORT_RANGE_WIDTH`.
|
|
860
|
-
pub fn parse_port_range(s: &str) -> Result<(u16, u16)> {
|
|
861
|
-
let (lo_s, hi_s) = s
|
|
862
|
-
.split_once('-')
|
|
863
|
-
.ok_or_else(|| anyhow!("expected LOW-HIGH (e.g. 60080-60089)"))?;
|
|
864
|
-
let lo: u16 = lo_s
|
|
865
|
-
.trim()
|
|
866
|
-
.parse()
|
|
867
|
-
.map_err(|_| anyhow!("invalid low port '{lo_s}'"))?;
|
|
868
|
-
let hi: u16 = hi_s
|
|
869
|
-
.trim()
|
|
870
|
-
.parse()
|
|
871
|
-
.map_err(|_| anyhow!("invalid high port '{hi_s}'"))?;
|
|
872
|
-
if lo == 0 {
|
|
873
|
-
// Port 0 is "any" at bind time and never appears as a
|
|
874
|
-
// remote port, so it's a dead slot in the range.
|
|
875
|
-
return Err(anyhow!("low port must be >= 1"));
|
|
876
|
-
}
|
|
877
|
-
if lo > hi {
|
|
878
|
-
return Err(anyhow!("low ({lo}) > high ({hi})"));
|
|
879
|
-
}
|
|
880
|
-
if hi - lo > MAX_PROXY_PORT_RANGE_WIDTH {
|
|
881
|
-
return Err(anyhow!(
|
|
882
|
-
"range too wide ({} ports); max width {}",
|
|
883
|
-
hi - lo + 1,
|
|
884
|
-
MAX_PROXY_PORT_RANGE_WIDTH + 1
|
|
885
|
-
));
|
|
886
|
-
}
|
|
887
|
-
Ok((lo, hi))
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/// Parse a `--sublayer-guid` argument. Accepts braced or unbraced
|
|
891
|
-
/// canonical form. `GUID::try_from` only takes the unbraced form and
|
|
892
|
-
/// returns an unhelpful error on failure, so strip braces and
|
|
893
|
-
/// pre-validate the shape for a friendlier message.
|
|
894
|
-
pub fn parse_guid(s: &str) -> Result<GUID> {
|
|
895
|
-
let t = s.trim().trim_start_matches('{').trim_end_matches('}');
|
|
896
|
-
// 8-4-4-4-12 hex with hyphens, exactly 36 chars.
|
|
897
|
-
let ok = t.len() == 36
|
|
898
|
-
&& t.bytes().enumerate().all(|(i, b)| match i {
|
|
899
|
-
8 | 13 | 18 | 23 => b == b'-',
|
|
900
|
-
_ => b.is_ascii_hexdigit(),
|
|
901
|
-
});
|
|
902
|
-
if !ok {
|
|
903
|
-
return Err(anyhow!(
|
|
904
|
-
"invalid GUID '{s}': expected xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
905
|
-
));
|
|
906
|
-
}
|
|
907
|
-
GUID::try_from(t).map_err(|e| anyhow!("invalid GUID '{s}': {e}"))
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
#[cfg(test)]
|
|
911
|
-
mod tests {
|
|
912
|
-
use super::*;
|
|
913
|
-
|
|
914
|
-
/// All three SDDL templates used by `install_filters` must
|
|
915
|
-
/// parse for a representative SID. Catches template typos
|
|
916
|
-
/// without needing a live WFP engine.
|
|
917
|
-
#[test]
|
|
918
|
-
fn sddl_templates_parse() {
|
|
919
|
-
let g = "S-1-5-32-545";
|
|
920
|
-
for sddl in [sddl_nonmember(g), sddl_group(g), SDDL_EVERYONE.into()] {
|
|
921
|
-
let sd = OwnedSd::from_sddl(&sddl).expect("sddl");
|
|
922
|
-
assert!(!sd.ptr.0.is_null());
|
|
923
|
-
assert!(sd.len > 0);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
#[test]
|
|
928
|
-
fn sddl_rejects_garbage() {
|
|
929
|
-
assert!(OwnedSd::from_sddl("O:LSG:LSD:(A;;CC;;;NOT-A-SID)").is_err());
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
#[test]
|
|
933
|
-
fn filter_tag_round_trip() {
|
|
934
|
-
let t = FilterTag::new("block");
|
|
935
|
-
let bytes = t.to_blob_bytes();
|
|
936
|
-
let back: FilterTag = serde_json::from_slice(&bytes).unwrap();
|
|
937
|
-
assert_eq!(t, back);
|
|
938
|
-
// port_range omitted from JSON when None (back-compat with
|
|
939
|
-
// pre-range tags).
|
|
940
|
-
assert!(!std::str::from_utf8(&bytes).unwrap().contains("port_range"));
|
|
941
|
-
|
|
942
|
-
let lb = FilterTag::loopback((60080, 60089));
|
|
943
|
-
let lb_bytes = lb.to_blob_bytes();
|
|
944
|
-
let lb_back: FilterTag = serde_json::from_slice(&lb_bytes).unwrap();
|
|
945
|
-
assert_eq!(lb, lb_back);
|
|
946
|
-
assert_eq!(lb_back.port_range, Some([60080, 60089]));
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
#[test]
|
|
950
|
-
fn filter_tag_parses_legacy() {
|
|
951
|
-
// A pre-port-range tag (no port_range key) must still parse.
|
|
952
|
-
let legacy = br#"{"tool":"srt-win","kind":"permit-loopback"}"#;
|
|
953
|
-
let t: FilterTag = serde_json::from_slice(legacy).unwrap();
|
|
954
|
-
assert_eq!(t.kind, "permit-loopback");
|
|
955
|
-
assert_eq!(t.port_range, None);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
#[test]
|
|
959
|
-
fn parse_port_range_ok() {
|
|
960
|
-
assert_eq!(parse_port_range("60080-60089").unwrap(), (60080, 60089));
|
|
961
|
-
assert_eq!(parse_port_range(" 1 - 1 ").unwrap(), (1, 1));
|
|
962
|
-
assert_eq!(
|
|
963
|
-
parse_port_range("1-65").unwrap(),
|
|
964
|
-
(1, 1 + MAX_PROXY_PORT_RANGE_WIDTH)
|
|
965
|
-
);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
#[test]
|
|
969
|
-
fn parse_port_range_rejects() {
|
|
970
|
-
assert!(parse_port_range("60089-60080").is_err()); // low>high
|
|
971
|
-
assert!(parse_port_range("1-1000").is_err()); // too wide
|
|
972
|
-
assert!(parse_port_range("60080").is_err()); // no dash
|
|
973
|
-
assert!(parse_port_range("a-b").is_err()); // not u16
|
|
974
|
-
assert!(parse_port_range("0-65536").is_err()); // overflow
|
|
975
|
-
assert!(parse_port_range("0-9").is_err()); // port 0
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
#[test]
|
|
979
|
-
fn parse_guid_accepts_both_forms() {
|
|
980
|
-
let g1 =
|
|
981
|
-
parse_guid("2c5d0ad6-5f3b-4d4e-9b8f-1a3e7c9d0b21").unwrap();
|
|
982
|
-
let g2 =
|
|
983
|
-
parse_guid("{2c5d0ad6-5f3b-4d4e-9b8f-1a3e7c9d0b21}").unwrap();
|
|
984
|
-
assert_eq!(g1, g2);
|
|
985
|
-
assert_eq!(g1, DEFAULT_SUBLAYER_GUID);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
#[test]
|
|
989
|
-
fn parse_guid_rejects_garbage() {
|
|
990
|
-
assert!(parse_guid("not-a-guid").is_err());
|
|
991
|
-
}
|
|
992
|
-
}
|