Skip to content
Draft
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
**/target
**/node_modules
.git/
.dockerignore
Dockerfile
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ dist/
# Downloaded build dependencies
tun2socks.exe
wintun.dll
PROTOCOL.md
TECHNICAL_SPEC.md

# E2E test artifacts
tests/e2e/test-results/
tests/e2e/node_modules/
# Dev artifacts
devolutions-agent-linux
117 changes: 117 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# =============================================================================
# Devolutions Gateway — Source build for Coolify
# =============================================================================
# Multi-stage build:
# 1. rust-builder — compile the gateway binary from source
# 2. official-image — extract libxmf and PowerShell module from official image
# 3. runtime — assemble the final image
#
# Both the gateway binary AND the webapp are built from THIS repo's source.
# The webapp must be pre-built locally (pnpm build:gateway) because some
# dependencies (@devolutions/icons) require private registry authentication.
# The libxmf.so and PowerShell module come from the official published image.
# =============================================================================

# Global ARG — must be before any FROM to be usable in FROM lines
ARG GATEWAY_VERSION=latest

# ---------------------------------------------------------------------------
# Stage 1: Rust builder
# ---------------------------------------------------------------------------
FROM rust:1.90-bookworm AS rust-builder

WORKDIR /src

# Install build dependencies (cmake required by quiche/BoringSSL, go required by quiche)
RUN apt-get update && apt-get install -y --no-install-recommends \
cmake \
golang-go \
nasm \
&& rm -rf /var/lib/apt/lists/*

# Copy manifests first for better layer caching
COPY Cargo.toml Cargo.lock rust-toolchain.toml ./
COPY crates crates
COPY devolutions-gateway devolutions-gateway
COPY devolutions-agent devolutions-agent
COPY devolutions-session devolutions-session
COPY jetsocat jetsocat
COPY testsuite testsuite
COPY tools tools
COPY fuzz fuzz

# Build only the gateway binary in release mode
RUN cargo build --release --package devolutions-gateway \
&& cp target/release/devolutions-gateway /usr/local/bin/devolutions-gateway

# ---------------------------------------------------------------------------
# Stage 2: Extract libxmf + PowerShell module from the official image
# ---------------------------------------------------------------------------
FROM devolutions/devolutions-gateway:${GATEWAY_VERSION} AS official-image

# ---------------------------------------------------------------------------
# Stage 3: Runtime
# ---------------------------------------------------------------------------
FROM debian:bookworm-slim

LABEL maintainer="Devolutions Inc."
LABEL description="Devolutions Gateway — built from source with QUIC agent tunnel"

# Install PowerShell and runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends wget ca-certificates openssl curl \
&& ARCH=$(dpkg --print-architecture) \
&& if [ "$ARCH" = "arm64" ]; then \
PWSH_VERSION=7.4.6 \
&& wget -q "https://github.com/PowerShell/PowerShell/releases/download/v${PWSH_VERSION}/powershell-${PWSH_VERSION}-linux-arm64.tar.gz" \
&& mkdir -p /opt/microsoft/powershell/7 \
&& tar -xzf "powershell-${PWSH_VERSION}-linux-arm64.tar.gz" -C /opt/microsoft/powershell/7 \
&& chmod +x /opt/microsoft/powershell/7/pwsh \
&& ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh \
&& rm "powershell-${PWSH_VERSION}-linux-arm64.tar.gz"; \
else \
wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \
&& dpkg -i packages-microsoft-prod.deb \
&& rm packages-microsoft-prod.deb \
&& apt-get update \
&& apt-get install -y --no-install-recommends powershell; \
fi \
&& rm -rf /var/lib/apt/lists/*

ENV XDG_CACHE_HOME="/tmp/.cache"
ENV XDG_DATA_HOME="/tmp/.local/share"
ENV POWERSHELL_TELEMETRY_OPTOUT="1"

ENV DGATEWAY_CONFIG_PATH="/tmp/devolutions-gateway"
RUN mkdir -p "$DGATEWAY_CONFIG_PATH"

WORKDIR /opt/devolutions/gateway

ENV DGATEWAY_EXECUTABLE_PATH="/opt/devolutions/gateway/devolutions-gateway"
ENV DGATEWAY_LIB_XMF_PATH="/opt/devolutions/gateway/libxmf.so"
ENV DGATEWAY_WEBAPP_PATH="/opt/devolutions/gateway/webapp"

# Gateway binary — built from THIS repo's source code
COPY --from=rust-builder /usr/local/bin/devolutions-gateway $DGATEWAY_EXECUTABLE_PATH

# Webapp — pre-built locally (pnpm build:gateway), output in webapp/dist/gateway-ui/
COPY webapp/dist/gateway-ui/ /opt/devolutions/gateway/webapp/client/

# libxmf — from official image (native library, not built from source)
COPY --from=official-image /opt/devolutions/gateway/libxmf.so $DGATEWAY_LIB_XMF_PATH

# PowerShell module — from official image (includes pre-compiled .NET DLLs)
COPY --from=official-image /opt/microsoft/powershell/7/Modules/DevolutionsGateway /opt/microsoft/powershell/7/Modules/DevolutionsGateway

# Entrypoint script from this repo's source
COPY package/Linux/entrypoint.ps1 /usr/local/bin/entrypoint.ps1
RUN chmod +x /usr/local/bin/entrypoint.ps1

EXPOSE 7171
EXPOSE 8181
EXPOSE 4433/udp

HEALTHCHECK --interval=30s --timeout=10s --retries=5 --start-period=15s \
CMD curl -sf http://localhost:7171/jet/health || exit 1

ENTRYPOINT ["pwsh", "-File", "/usr/local/bin/entrypoint.ps1"]
57 changes: 57 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# documentation: https://docs.devolutions.net/gateway/standalone/
# slogan: Protocol-aware relay server with QUIC agent tunnel for private network access (RDP, SSH, VNC, Telnet, ARD)
# tags: devolutions,gateway,rdp,ssh,vnc,telnet,remote-access,relay,quic,agent-tunnel
# port: 7171

services:
gateway:
image: irvingou/devolutions-gateway:quic-tunnel-v6
# To build from source instead of using the pre-built image, comment out
# "image:" above and uncomment the line below:
# build: .
environment:
# -- Coolify Magic Variables --
# SERVICE_FQDN_GATEWAY_7171 tells Traefik to route traffic to port 7171 inside the container.
- SERVICE_FQDN_GATEWAY_7171
# Auto-generated credentials — visible in Coolify's environment variables UI.
- WEB_APP_USERNAME=${SERVICE_USER_GATEWAY}
- WEB_APP_PASSWORD=${SERVICE_PASSWORD_GATEWAY}

# -- Gateway Standalone Configuration --
# Enable the built-in web application (admin UI + web-based remote access).
- WEB_APP_ENABLED=true
# Internal scheme is HTTP; Coolify's Traefik reverse proxy terminates TLS.
- WEB_SCHEME=http
# Tell the gateway that clients reach it over HTTPS (via Traefik).
- EXTERNAL_WEB_SCHEME=https
# Session recording storage path inside the container.
- RECORDING_PATH=/recordings
# Logging verbosity: Default, Debug, Tls, All, Quiet
- VERBOSITY_PROFILE=${VERBOSITY_PROFILE:-Debug}

# -- QUIC Agent Tunnel --
# Enable QUIC listener for agent-based private network routing.
- AGENT_TUNNEL_ENABLED=true
# QUIC listener port (UDP). Agents connect to this port.
- AGENT_TUNNEL_PORT=${AGENT_TUNNEL_PORT:-4433}

volumes:
# Persist session recordings across redeployments.
- gateway-recordings:/recordings
# Persist gateway configuration (provisioner keys, config files, agent certs).
- gateway-config:/tmp/devolutions-gateway

healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:7171/jet/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 15s

ports:
# QUIC agent tunnel — UDP, must bypass Traefik, exposed directly on host
- "${AGENT_TUNNEL_PORT:-4433}:${AGENT_TUNNEL_PORT:-4433}/udp"

volumes:
gateway-recordings:
gateway-config:
13 changes: 13 additions & 0 deletions package/AgentWindowsManaged/Actions/AgentActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,18 @@ internal static class AgentActions
When = When.Before
};

private static readonly ElevatedManagedAction enrollAgentTunnel = new(
new Id($"CA.{nameof(enrollAgentTunnel)}"),
CustomActions.EnrollAgentTunnel,
Return.check,
When.Before, Step.StartServices,
Condition.NOT_BeingRemoved,
Sequence.InstallExecuteSequence)
{
Execute = Execute.deferred,
Impersonate = false,
};

private static readonly ElevatedManagedAction registerExplorerCommand = new(
CustomActions.RegisterExplorerCommand
)
Expand Down Expand Up @@ -329,6 +341,7 @@ private static string UseProperties(IEnumerable<IWixProperty> properties)
getInstallDirFromRegistry,
setArpInstallLocation,
configureFeatures,
enrollAgentTunnel,
createProgramDataDirectory,
setProgramDataDirectoryPermissions,
createProgramDataPedmDirectories,
Expand Down
94 changes: 94 additions & 0 deletions package/AgentWindowsManaged/Actions/CustomActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,100 @@ static ActionResult ToggleAgentFeature(Session session, string feature, bool ena
}
}

[CustomAction]
public static ActionResult EnrollAgentTunnel(Session session)
{
string enrollmentString = session.Property(AgentProperties.AgentTunnelEnrollmentString);
string subnetsRaw = session.Property(AgentProperties.AgentTunnelAdvertiseSubnets);

if (string.IsNullOrWhiteSpace(enrollmentString))
{
session.Log("Agent tunnel enrollment string not provided, skipping tunnel setup");
return ActionResult.Success;
}

try
{
// Parse enrollment string to extract gateway URL, token, and name.
// Format: dgw-enroll:v1:<base64 JSON payload>
const string prefix = "dgw-enroll:v1:";
if (!enrollmentString.StartsWith(prefix))
{
session.Log("Invalid enrollment string prefix");
return ActionResult.Failure;
}

string base64 = enrollmentString.Substring(prefix.Length);
byte[] decoded = Convert.FromBase64String(base64.Replace('-', '+').Replace('_', '/'));
string json = System.Text.Encoding.UTF8.GetString(decoded);

var payload = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
string apiBaseUrl = payload["api_base_url"]?.ToString();
string enrollmentToken = payload["enrollment_token"]?.ToString();
string agentName = payload.ContainsKey("name") && payload["name"] != null
? payload["name"].ToString()
: Environment.MachineName;

if (string.IsNullOrEmpty(agentName))
{
agentName = Environment.MachineName;
}

// Build CLI arguments for: devolutions-agent.exe enroll <url> <token> <name> <config> [subnets]
string configPath = Path.Combine(ProgramDataDirectory, "agent.json");
string installDir = session.Property(AgentProperties.InstallDir);
string exePath = Path.Combine(installDir, Includes.EXECUTABLE_NAME);

string subnetsArg = string.IsNullOrWhiteSpace(subnetsRaw) ? "" : subnetsRaw.Trim();

string arguments = $"enroll \"{apiBaseUrl}\" \"{enrollmentToken}\" \"{agentName}\" \"{configPath}\"";
if (!string.IsNullOrEmpty(subnetsArg))
{
arguments += $" \"{subnetsArg}\"";
}

session.Log($"Running enrollment: {exePath} {arguments.Replace(enrollmentToken, "***")}");

ProcessStartInfo startInfo = new ProcessStartInfo(exePath, arguments)
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = ProgramDataDirectory,
};

using Process process = Process.Start(startInfo);
string stdout = process.StandardOutput.ReadToEnd();
string stderr = process.StandardError.ReadToEnd();
process.WaitForExit(60_000); // 60 second timeout

if (!string.IsNullOrEmpty(stdout))
{
session.Log($"enrollment stdout: {stdout}");
}

if (!string.IsNullOrEmpty(stderr))
{
session.Log($"enrollment stderr: {stderr}");
}

if (process.ExitCode != 0)
{
session.Log($"Enrollment failed with exit code {process.ExitCode}");
return ActionResult.Failure;
}

session.Log("Agent tunnel enrollment completed successfully");
return ActionResult.Success;
}
catch (Exception e)
{
session.Log($"Agent tunnel enrollment failed: {e}");
return ActionResult.Failure;
}
}

[CustomAction]
public static ActionResult ConfigureFeatures(Session session)
{
Expand Down
Loading
Loading