Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion protocols/docs/NOW-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
TOC is generated in [Obsidian](obsidian.md) via
[TOC plugin](https://github.com/hipstersmoothie/obsidian-plugin-toc)
-->
# NOW-PROTO 1.6
# NOW-PROTO 1.7
- [Messages](#messages)
- [Transport](#transport)
- [Message Syntax](#message-syntax)
Expand Down Expand Up @@ -46,6 +46,7 @@ TOC is generated in [Obsidian](obsidian.md) via
- [NOW_EXEC_BATCH_MSG](#now_exec_batch_msg)
- [NOW_EXEC_WINPS_MSG](#now_exec_winps_msg)
- [NOW_EXEC_PWSH_MSG](#now_exec_pwsh_msg)
- [Elevated Execution](#elevated-execution)
- [RDM Messages](#rdm-messages)
- [NOW_RDM_MSG](#now_rdm_msg)
- [NOW_RDM_CAPABILITIES_MSG](#now_rdm_capabilities_msg)
Expand Down Expand Up @@ -328,6 +329,7 @@ increment major version; Protocol implementations with different major version a
| NOW_CAP_EXEC_STYLE_PWSH<br>0x0020 | PowerShell 7 (.ps1) execution style. |
| NOW_CAP_EXEC_UNICODE_CONSOLE<br>0x0040 | Host supports encoding control flags (RAW_ENCODING, UNICODE_CONSOLE, and ENCODING_UTF8). |
| NOW_CAP_EXEC_IO_REDIRECTION<br>0x1000 | Set if host implements exec session IO redirection. |
| NOW_CAP_EXEC_ELEVATE_SHELL<br>0x0080 | Set if host can elevate an exec session using the platform shell. Elevation may prompt the interactive user for consent, and IO redirection is unavailable for elevated sessions. See [Elevated Execution](#elevated-execution). |

<!-- TODO: add AppleScript command -->

Expand Down Expand Up @@ -956,6 +958,7 @@ packet-beta
| Flag | Meaning |
|----------------------------------------|---------------------------|
| NOW_EXEC_FLAG_RUN_DIRECTORY_SET<br>0x0001 | `directory` field contains non-default value. |
| NOW_EXEC_FLAG_RUN_ELEVATED<br>0x0002 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). |

**sessionId (4 bytes)**: A 32-bit unsigned integer containing a unique remote execution session id.

Expand Down Expand Up @@ -993,6 +996,7 @@ packet-beta
| NOW_EXEC_FLAG_PROCESS_PARAMETERS_SET<br>0x0001 | `parameters` field contains non-default value. |
| NOW_EXEC_FLAG_PROCESS_DIRECTORY_SET<br>0x0002 | `directory` field contains non-default value.|
| NOW_EXEC_FLAG_PROCESS_ENCODING_UTF8<br>0x0004 | Enables OEM-to-UTF-8 transcoding for stdin, stdout, and stderr. Without this flag, data streams are passed through as raw bytes without encoding conversion. |
| NOW_EXEC_FLAG_PROCESS_ELEVATED<br>0x0008 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). |
| NOW_EXEC_FLAG_PROCESS_IO_REDIRECTION<br>0x1000 | Enable stdio (stdout, stderr, stdin) redirection. |
| NOW_EXEC_FLAG_PROCESS_DETACHED<br>0x8000 | Detached mode: the process is started without tracking execution or sending back output. |

Expand Down Expand Up @@ -1035,6 +1039,7 @@ packet-beta
|----------------------------------------|---------------------------|
| NOW_EXEC_FLAG_SHELL_SHELL_SET<br>0x0001 | `shell` field contains non-default value. |
| NOW_EXEC_FLAG_SHELL_DIRECTORY_SET<br>0x0002 | `directory` field contains non-default value. |
| NOW_EXEC_FLAG_SHELL_ELEVATED<br>0x0008 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). |
| NOW_EXEC_FLAG_SHELL_IO_REDIRECTION<br>0x1000 | Enable stdio (stdout, stderr, stdin) redirection. |
| NOW_EXEC_FLAG_SHELL_DETACHED<br>0x8000 | Detached mode: the shell is started without tracking execution or sending back output. |

Expand Down Expand Up @@ -1077,6 +1082,8 @@ packet-beta
| NOW_EXEC_FLAG_BATCH_DIRECTORY_SET<br>0x0001 | `directory` field contains non-default value. |
| NOW_EXEC_FLAG_BATCH_RAW_ENCODING<br>0x0002 | Disables the default OEM-to-UTF-8 transcoding: data streams are passed through as raw bytes without any encoding conversion. |
| NOW_EXEC_FLAG_BATCH_UNICODE_CONSOLE<br>0x0004 | Enables Unicode console: agent injects `@chcp 65001 > nul` and writes the script in BOM-less UTF-8. Implies UTF-8 stdout/stderr streams. |
| NOW_EXEC_FLAG_BATCH_ELEVATED<br>0x0008 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). |
| NOW_EXEC_FLAG_BATCH_NO_EXIT<br>0x0010 | Keeps the command interpreter running after the batch file completes (`cmd /K` rather than `/C`). MUST be ignored when the session redirects stdio, where the hidden interpreter would never exit. |
| NOW_EXEC_FLAG_BATCH_IO_REDIRECTION<br>0x1000 | Enable stdio (stdout, stderr, stdin) redirection. |
| NOW_EXEC_FLAG_BATCH_DETACHED<br>0x8000 | Detached mode: the batch is started without tracking execution or sending back output. |

Expand Down Expand Up @@ -1132,6 +1139,7 @@ packet-beta
| NOW_EXEC_FLAG_PS_DIRECTORY_SET<br>0x0100 | `directory` field contains non-default value and specifies command working directory |
| NOW_EXEC_FLAG_PS_RAW_ENCODING<br>0x0200 | Disables the default OEM-to-UTF-8 transcoding: data streams are passed through as raw bytes without any encoding conversion. |
| NOW_EXEC_FLAG_PS_UNICODE_CONSOLE<br>0x0400 | Enables Unicode console: agent injects `$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()` at script start. Implies stdin/stdout/stderr streams are UTF-8. |
| NOW_EXEC_FLAG_PS_ELEVATED<br>0x0800 | Execute the command with elevated privileges. The elevation mechanism is selected by the server and advertised in `execCapset`; see [Elevated Execution](#elevated-execution). |
| NOW_EXEC_FLAG_PS_IO_REDIRECTION<br>0x1000 | Enable stdio (stdout, stderr, stdin) redirection. |
| NOW_EXEC_FLAG_PS_SERVER_MODE<br>0x2000 | Run PowerShell in server mode. |
| NOW_EXEC_FLAG_PS_DETACHED<br>0x8000 | Detached mode: PowerShell is started without tracking execution or sending back output. |
Expand Down Expand Up @@ -1196,6 +1204,50 @@ packet-beta

**configurationName (variable)**: A NOW_VARSTR structure, same as with NOW_EXEC_WINPS_MSG.

#### Elevated Execution

Any exec message may request elevated execution by setting its `NOW_EXEC_FLAG_*_ELEVATED` flag.
The flag expresses *intent* only: it never names a mechanism, and the client does not choose one.

`ELEVATED` raises the privileges of the identity that would otherwise run the command, which is the
session user. It does not select a different identity, and it carries no credentials. Running a
command as another user is a separate concern and would be expressed by a separate field.

**Requesting elevation.** A client SHOULD NOT set an `ELEVATED` flag unless the server advertised
at least one `NOW_CAP_EXEC_ELEVATE_*` capability. A server that receives an `ELEVATED` flag without
having advertised any elevation capability MUST fail the session with `NOW_CODE_NOT_IMPLEMENTED`.
A server predating this version ignores the flag entirely and executes without elevation, so a
client MUST NOT treat the absence of a failure as evidence that elevation took place: capability
negotiation is the only reliable signal, and a client is expected to refuse the request locally
when no elevation capability was advertised.
A server MUST NOT execute a command without elevation after being asked to elevate it: silently
downgrading the request denies the client any way to detect that privileges were not granted.

**Mechanism selection.** The server selects the mechanism and advertises it in `execCapset`. Only
`NOW_CAP_EXEC_ELEVATE_SHELL` is defined at present. A mechanism with different observable
properties, such as one that raises no consent prompt or preserves stdio redirection, is expected to
be advertised as an additional capability rather than by changing the meaning of an existing one, so
a client always learns what to expect before it sends a request.

**Semantics under NOW_CAP_EXEC_ELEVATE_SHELL.** The server elevates through the platform shell,
which on Windows raises a consent prompt on the interactive user's desktop:

- A client SHOULD omit `NOW_EXEC_FLAG_*_IO_REDIRECTION` when requesting elevation, and MUST NOT
assume the server honours it. The server MUST NOT open stdio channels for the session unless it
advertises a capability stating that elevated sessions keep their stdio.
- Unless the request uses `NOW_EXEC_RUN_MSG`, which is never tracked, or sets
`NOW_EXEC_FLAG_*_DETACHED`, which promises no tracking, the server SHOULD still track the
session and report the real exit code in `NOW_EXEC_RESULT_MSG`, so a client can distinguish
success from failure without stdio.
- `NOW_EXEC_ABORT_MSG` and `NOW_EXEC_CANCEL_REQ_MSG` MAY fail with `NOW_CODE_NOT_IMPLEMENTED`,
because the elevated child runs at a higher integrity level than the server's session process.
- Elevation MAY require interactive consent, so the request MAY fail when no user is present to
grant it, or when the user declines.

**Failure reporting.** A server that cannot grant elevation MUST report it distinctly rather than
succeeding: the account has no elevated token available, the user declined consent, or elevation is
not implemented for the requested exec style.

### RDM Messages

#### NOW_RDM_MSG
Expand Down Expand Up @@ -1484,3 +1536,7 @@ packet-beta
- Add `NOW_EXEC_FLAG_PROCESS_ENCODING_UTF8` flag for process exec commands.
- Add `NOW_EXEC_FLAG_*_UNICODE_CONSOLE` flags for batch (cmd), winps, and pwsh exec commands.
- Add `NOW_CAP_EXEC_UNICODE_CONSOLE` capability flag.
- 1.7
- Add `NOW_EXEC_FLAG_*_ELEVATED` flags for run, process, shell, batch, winps, and pwsh exec messages.
- Add `NOW_CAP_EXEC_ELEVATE_SHELL` capability flag.
- Add `NOW_EXEC_FLAG_BATCH_NO_EXIT` flag for batch exec commands.
6 changes: 6 additions & 0 deletions protocols/dotnet/Devolutions.NowClient/src/AExecParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public StartedHandler OnStarted
}
}

/// <summary>
/// Whether the caller asked for elevated execution. Inspected by <see cref="NowClient"/>
/// so a request cannot be sent to a server that never advertised an elevation capability.
/// </summary>
internal bool IsElevated { get; set; }

internal ExecSession ToExecSession(uint sessionId, ChannelWriter<IClientCommand> commandWriter)
{
return new ExecSession(
Expand Down
31 changes: 31 additions & 0 deletions protocols/dotnet/Devolutions.NowClient/src/ExecBatchParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ public ExecBatchParams UnicodeConsole(bool enable = true)
return this;
}

/// <summary>
/// Keep the command interpreter open after the batch file completes (`cmd /K` rather than
/// `/C`). Ignored by the server when the session redirects stdio.
/// </summary>
public ExecBatchParams NoExit(bool enable = true)
{
_noExit = enable;
return this;
Comment on lines +61 to +64

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concern is real but I'd rather not fix it this way, because the remedy would make NoExit the only version-gated exec flag in the library while its exact siblings stay ungated.

State of the code today:

  • NowClient version-gates exactly one thing: the whole RDM extension (MIN_RDM_ENABLED_VERSION = 1.3, checked in EnsureRdmCapabilities).
  • It version-gates zero exec flags. DETACHED arrived in 1.4 with the identical property you describe — send it to a 1.3 server and the session is tracked anyway, silently contradicting the caller. RAW_ENCODING and UNICODE_CONSOLE arrived in 1.6; the latter got a capability bit, NOW_CAP_EXEC_UNICODE_CONSOLE, rather than a version check.

So the two coherent options are "the library gates no exec flags and callers gate" (status quo — RDM gates fire-and-forget on Version >= 1.4 and Unicode console on the capset bit) or "the library gates all of them, consistently". Gating just the newest flag is the one option that leaves the codebase harder to
reason about.

There's also a severity gap worth naming. Elevation silently not happening is a trust and correctness failure: the operator believes a script ran as administrator when it did not, and nothing in the UI or the logs says otherwise - that is the exact bug this PR exists to close, which is why it gets a hard refusal client-side. NoExit silently not applying means a console window closes instead of staying open: immediately visible to the person watching the remote desktop, no incorrect belief about privileges, no correctness impact. The enforcement asymmetry is proportionate to the consequence asymmetry.

Elevation also had a mechanism that a version can never express (shell vs token elevation differ in whether stdio survives), which is why it needed capability negotiation rather than a version compare. NoExit has no such dimension.

Concretely, my preference: leave the library as is, and have RDM gate the Keep open checkbox the same way it already gates Fire and forget. That is in the RDM-side plan for this work. If you'd rather have library-level enforcement, I think it deserves its own PR that covers DETACHED and the encoding flags too, so the rule is uniform.

}

/// <summary>
/// Execute with elevated privileges. Requires the host to advertise an elevation
/// capability; under shell-based elevation the session has no IO redirection.
/// </summary>
public ExecBatchParams Elevated(bool enable = true)
Comment thread
thenextman marked this conversation as resolved.
{
IsElevated = enable;
return this;
}
Comment thread
thenextman marked this conversation as resolved.

internal NowMsgExecBatch ToNowMessage(uint sessionId)
{
var builder = new NowMsgExecBatch.Builder(sessionId, command);
Expand Down Expand Up @@ -83,6 +103,16 @@ internal NowMsgExecBatch ToNowMessage(uint sessionId)
builder.EnableUnicodeConsole();
}

if (IsElevated)
{
builder.EnableElevated();
}

if (_noExit)
{
builder.EnableNoExit();
}

return builder.Build();
}

Expand All @@ -91,5 +121,6 @@ internal NowMsgExecBatch ToNowMessage(uint sessionId)
private bool _detached = false;
private bool _rawEncoding = false;
private bool _unicodeConsole = false;
private bool _noExit = false;
}
}
17 changes: 16 additions & 1 deletion protocols/dotnet/Devolutions.NowClient/src/ExecProcessParams.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Devolutions.NowProto.Messages;
using Devolutions.NowProto.Messages;

namespace Devolutions.NowClient
{
Expand Down Expand Up @@ -54,6 +54,16 @@ public ExecProcessParams EncodingUtf8(bool enable = true)
return this;
}

/// <summary>
/// Execute with elevated privileges. Requires the host to advertise an elevation
/// capability; under shell-based elevation the session has no IO redirection.
/// </summary>
public ExecProcessParams Elevated(bool enable = true)
Comment thread
thenextman marked this conversation as resolved.
{
IsElevated = enable;
return this;
}

internal NowMsgExecProcess ToNowMessage(uint sessionId)
{
var builder = new NowMsgExecProcess.Builder(sessionId, filename);
Expand Down Expand Up @@ -83,6 +93,11 @@ internal NowMsgExecProcess ToNowMessage(uint sessionId)
builder.EnableEncodingUtf8();
}

if (IsElevated)
{
builder.EnableElevated();
}

return builder.Build();
}

Expand Down
15 changes: 15 additions & 0 deletions protocols/dotnet/Devolutions.NowClient/src/ExecPwshParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ public ExecPwshParams UnicodeConsole(bool enable = true)
return this;
}

/// <summary>
/// Execute with elevated privileges. Requires the host to advertise an elevation
/// capability; under shell-based elevation the session has no IO redirection.
/// </summary>
public ExecPwshParams Elevated(bool enable = true)
Comment thread
thenextman marked this conversation as resolved.
{
IsElevated = enable;
return this;
}

internal NowMsgExecPwsh ToNowMessage(uint sessionId)
{
var builder = _serverMode
Expand Down Expand Up @@ -193,6 +203,11 @@ internal NowMsgExecPwsh ToNowMessage(uint sessionId)
builder.EnableUnicodeConsole();
}

if (IsElevated)
{
builder.EnableElevated();
}

return builder.Build();
}

Expand Down
15 changes: 15 additions & 0 deletions protocols/dotnet/Devolutions.NowClient/src/ExecRunParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ public ExecRunParams Directory(string directory)
return this;
}

/// <summary>
/// Execute with elevated privileges. Requires the host to advertise an elevation
/// capability; under shell-based elevation the session has no IO redirection.
/// </summary>
public ExecRunParams Elevated(bool enable = true)
Comment thread
thenextman marked this conversation as resolved.
{
IsElevated = enable;
return this;
}

internal NowMsgExecRun ToNowMessage(uint sessionId)
{
var builder = new NowMsgExecRun.Builder(sessionId, command);
Expand All @@ -27,6 +37,11 @@ internal NowMsgExecRun ToNowMessage(uint sessionId)
builder.Directory(_directory);
}

if (IsElevated)
{
builder.EnableElevated();
}

return builder.Build();
}

Expand Down
15 changes: 15 additions & 0 deletions protocols/dotnet/Devolutions.NowClient/src/ExecShellParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ public ExecShellParams Detached(bool enable = true)
return this;
}

/// <summary>
/// Execute with elevated privileges. Requires the host to advertise an elevation
/// capability; under shell-based elevation the session has no IO redirection.
/// </summary>
public ExecShellParams Elevated(bool enable = true)
Comment thread
thenextman marked this conversation as resolved.
{
IsElevated = enable;
return this;
}

internal NowMsgExecShell ToNowMessage(uint sessionId)
{
var builder = new NowMsgExecShell.Builder(sessionId, command);
Expand All @@ -70,6 +80,11 @@ internal NowMsgExecShell ToNowMessage(uint sessionId)
builder.EnableDetached();
}

if (IsElevated)
{
builder.EnableElevated();
}

return builder.Build();
}

Expand Down
15 changes: 15 additions & 0 deletions protocols/dotnet/Devolutions.NowClient/src/ExecWinPsParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ public ExecWinPsParams UnicodeConsole(bool enable = true)
return this;
}

/// <summary>
/// Execute with elevated privileges. Requires the host to advertise an elevation
/// capability; under shell-based elevation the session has no IO redirection.
/// </summary>
public ExecWinPsParams Elevated(bool enable = true)
Comment thread
thenextman marked this conversation as resolved.
{
IsElevated = enable;
return this;
}

internal NowMsgExecWinPs ToNowMessage(uint sessionId)
{
var builder = _serverMode
Expand Down Expand Up @@ -194,6 +204,11 @@ internal NowMsgExecWinPs ToNowMessage(uint sessionId)
builder.EnableUnicodeConsole();
}

if (IsElevated)
{
builder.EnableElevated();
}

return builder.Build();
}

Expand Down
Loading