@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.
Files changed (116) hide show
  1. package/README.md +43 -2
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/sandbox/credential-mask-files.d.ts +72 -0
  7. package/dist/sandbox/credential-mask-files.d.ts.map +1 -0
  8. package/dist/sandbox/credential-mask-files.js +146 -0
  9. package/dist/sandbox/credential-mask-files.js.map +1 -0
  10. package/dist/sandbox/credential-sentinel.d.ts +72 -0
  11. package/dist/sandbox/credential-sentinel.d.ts.map +1 -0
  12. package/dist/sandbox/credential-sentinel.js +130 -0
  13. package/dist/sandbox/credential-sentinel.js.map +1 -0
  14. package/dist/sandbox/domain-pattern.d.ts +36 -0
  15. package/dist/sandbox/domain-pattern.d.ts.map +1 -0
  16. package/dist/sandbox/domain-pattern.js +60 -0
  17. package/dist/sandbox/domain-pattern.js.map +1 -0
  18. package/dist/sandbox/http-proxy.d.ts +32 -1
  19. package/dist/sandbox/http-proxy.d.ts.map +1 -1
  20. package/dist/sandbox/http-proxy.js +10 -2
  21. package/dist/sandbox/http-proxy.js.map +1 -1
  22. package/dist/sandbox/linux-sandbox-utils.d.ts +15 -0
  23. package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
  24. package/dist/sandbox/linux-sandbox-utils.js +106 -47
  25. package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
  26. package/dist/sandbox/listen-in-range.d.ts +12 -0
  27. package/dist/sandbox/listen-in-range.d.ts.map +1 -0
  28. package/dist/sandbox/listen-in-range.js +43 -0
  29. package/dist/sandbox/listen-in-range.js.map +1 -0
  30. package/dist/sandbox/macos-sandbox-utils.d.ts +11 -0
  31. package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
  32. package/dist/sandbox/macos-sandbox-utils.js +69 -14
  33. package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
  34. package/dist/sandbox/mitm-ca.d.ts +18 -2
  35. package/dist/sandbox/mitm-ca.d.ts.map +1 -1
  36. package/dist/sandbox/mitm-ca.js +49 -9
  37. package/dist/sandbox/mitm-ca.js.map +1 -1
  38. package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
  39. package/dist/sandbox/mitm-leaf.js +24 -6
  40. package/dist/sandbox/mitm-leaf.js.map +1 -1
  41. package/dist/sandbox/mux-proxy.d.ts +59 -0
  42. package/dist/sandbox/mux-proxy.d.ts.map +1 -0
  43. package/dist/sandbox/mux-proxy.js +159 -0
  44. package/dist/sandbox/mux-proxy.js.map +1 -0
  45. package/dist/sandbox/request-filter.d.ts +11 -1
  46. package/dist/sandbox/request-filter.d.ts.map +1 -1
  47. package/dist/sandbox/request-filter.js.map +1 -1
  48. package/dist/sandbox/sandbox-config.d.ts +254 -25
  49. package/dist/sandbox/sandbox-config.d.ts.map +1 -1
  50. package/dist/sandbox/sandbox-config.js +200 -13
  51. package/dist/sandbox/sandbox-config.js.map +1 -1
  52. package/dist/sandbox/sandbox-manager.d.ts +4 -0
  53. package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
  54. package/dist/sandbox/sandbox-manager.js +601 -194
  55. package/dist/sandbox/sandbox-manager.js.map +1 -1
  56. package/dist/sandbox/sandbox-schemas.d.ts +15 -0
  57. package/dist/sandbox/sandbox-schemas.d.ts.map +1 -1
  58. package/dist/sandbox/sandbox-utils.d.ts +42 -6
  59. package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
  60. package/dist/sandbox/sandbox-utils.js +115 -27
  61. package/dist/sandbox/sandbox-utils.js.map +1 -1
  62. package/dist/sandbox/socks-proxy.d.ts +11 -5
  63. package/dist/sandbox/socks-proxy.d.ts.map +1 -1
  64. package/dist/sandbox/socks-proxy.js +13 -81
  65. package/dist/sandbox/socks-proxy.js.map +1 -1
  66. package/dist/sandbox/tls-terminate-proxy.d.ts +2 -2
  67. package/dist/sandbox/tls-terminate-proxy.d.ts.map +1 -1
  68. package/dist/sandbox/tls-terminate-proxy.js +31 -5
  69. package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
  70. package/dist/sandbox/windows-sandbox-utils.d.ts +347 -11
  71. package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
  72. package/dist/sandbox/windows-sandbox-utils.js +437 -45
  73. package/dist/sandbox/windows-sandbox-utils.js.map +1 -1
  74. package/package.json +7 -4
  75. package/vendor/seccomp/build.ts +8 -30
  76. package/vendor/srt-win/build.ts +21 -0
  77. package/dist/vendor/seccomp/Dockerfile.build +0 -6
  78. package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
  79. package/dist/vendor/seccomp/build.ts +0 -91
  80. package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
  81. package/dist/vendor/seccomp-src/apply-seccomp.c +0 -280
  82. package/dist/vendor/seccomp-src/seccomp-unix-block.c +0 -148
  83. package/dist/vendor/srt-win/Cargo.lock +0 -361
  84. package/dist/vendor/srt-win/Cargo.toml +0 -46
  85. package/dist/vendor/srt-win/ci/cleanup.ps1 +0 -49
  86. package/dist/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
  87. package/dist/vendor/srt-win/ci/smoke.ps1 +0 -307
  88. package/dist/vendor/srt-win/src/job.rs +0 -102
  89. package/dist/vendor/srt-win/src/launch.rs +0 -732
  90. package/dist/vendor/srt-win/src/lib.rs +0 -20
  91. package/dist/vendor/srt-win/src/main.rs +0 -669
  92. package/dist/vendor/srt-win/src/self_protect.rs +0 -169
  93. package/dist/vendor/srt-win/src/sid.rs +0 -296
  94. package/dist/vendor/srt-win/src/token.rs +0 -341
  95. package/dist/vendor/srt-win/src/util.rs +0 -42
  96. package/dist/vendor/srt-win/src/wfp.rs +0 -992
  97. package/dist/vendor/srt-win/src/winsta.rs +0 -209
  98. package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
  99. package/vendor/seccomp-src/apply-seccomp.c +0 -280
  100. package/vendor/seccomp-src/seccomp-unix-block.c +0 -148
  101. package/vendor/srt-win/Cargo.lock +0 -361
  102. package/vendor/srt-win/Cargo.toml +0 -46
  103. package/vendor/srt-win/ci/cleanup.ps1 +0 -49
  104. package/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
  105. package/vendor/srt-win/ci/smoke.ps1 +0 -307
  106. package/vendor/srt-win/src/job.rs +0 -102
  107. package/vendor/srt-win/src/launch.rs +0 -732
  108. package/vendor/srt-win/src/lib.rs +0 -20
  109. package/vendor/srt-win/src/main.rs +0 -669
  110. package/vendor/srt-win/src/self_protect.rs +0 -169
  111. package/vendor/srt-win/src/sid.rs +0 -296
  112. package/vendor/srt-win/src/token.rs +0 -341
  113. package/vendor/srt-win/src/util.rs +0 -42
  114. package/vendor/srt-win/src/wfp.rs +0 -992
  115. package/vendor/srt-win/src/winsta.rs +0 -209
  116. 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
- }