feat: isolate simultaneous RDP fixture profiles
This commit is contained in:
@@ -904,6 +904,12 @@ response”. If the public browser hostname has an AAAA record, `rdp-access up
|
|||||||
--bind 0.0.0.0` must own a tracked IPv6-to-IPv4 forward and expose its
|
--bind 0.0.0.0` must own a tracked IPv6-to-IPv4 forward and expose its
|
||||||
`ipv6_forward` status; this prevents a browser selecting IPv6 for the WebSocket
|
`ipv6_forward` status; this prevents a browser selecting IPv6 for the WebSocket
|
||||||
from silently taking a different, refused path.
|
from silently taking a different, refused path.
|
||||||
|
The helper preserves the single-fixture `default` profile for compatibility;
|
||||||
|
simultaneous already-running VM gateways use a unique `RDP_ACCESS_PROFILE` (or
|
||||||
|
`--profile`) with separate runtime/project state and HTTP/tunnel ports, while
|
||||||
|
`RVBOX_TEST_VBOX_HOST`, the documented VM/snapshot identity variables, and
|
||||||
|
`RDP_ACCESS_VRDE_PORT` select the actual target. Profile isolation does not
|
||||||
|
override the native controller's exclusive lease for prepare/reset operations.
|
||||||
|
|
||||||
For this provisioned lane, the approved host-only credential-file location is
|
For this provisioned lane, the approved host-only credential-file location is
|
||||||
`/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password`. It must
|
`/home/cabbage/.local/share/rvbox-secrets/rvbox-win10-test.password`. It must
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ down/reset lifecycle; it is not an alternative to the native test controller.
|
|||||||
In public-bind mode the helper also owns a tracked IPv6-to-IPv4 `socat` forward
|
In public-bind mode the helper also owns a tracked IPv6-to-IPv4 `socat` forward
|
||||||
when the browser hostname has an AAAA record; check `ipv6_forward=active` in
|
when the browser hostname has an AAAA record; check `ipv6_forward=active` in
|
||||||
`rdp-access status` before diagnosing a browser-side “Waiting for response”.
|
`rdp-access status` before diagnosing a browser-side “Waiting for response”.
|
||||||
|
When more than one already-running VM needs browser access, set matching
|
||||||
|
`RVBOX_TEST_VBOX_*`/`RDP_ACCESS_VRDE_PORT` values and a unique
|
||||||
|
`RDP_ACCESS_PROFILE` plus HTTP/tunnel ports; the helper README has the
|
||||||
|
copy/paste example and explains the native-host lease boundary.
|
||||||
|
|
||||||
## Snapshots and reset contract
|
## Snapshots and reset contract
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -210,7 +210,9 @@ self-signed HTTPS Guacamole lifecycle; it must be started only after the native
|
|||||||
fixture controller has prepared and leased the VM, and stopped before reset.
|
fixture controller has prepared and leased the VM, and stopped before reset.
|
||||||
For a public hostname with an AAAA record, its `up --bind 0.0.0.0` mode also
|
For a public hostname with an AAAA record, its `up --bind 0.0.0.0` mode also
|
||||||
tracks an IPv6-to-IPv4 forward; verify `ipv6_forward=active` before debugging
|
tracks an IPv6-to-IPv4 forward; verify `ipv6_forward=active` before debugging
|
||||||
a browser stuck at “Waiting for response”.
|
a browser stuck at “Waiting for response”. If several already-running VMs need
|
||||||
|
browser access at once, use the documented `RDP_ACCESS_PROFILE` and matching
|
||||||
|
host/VRDE/HTTP/tunnel-port overrides in the helper README.
|
||||||
|
|
||||||
The Linux production Compose asset has a separate, loopback-only smoke lane. It
|
The Linux production Compose asset has a separate, loopback-only smoke lane. It
|
||||||
uses a disposable self-signed key only under the ignored `.test-runs` tree,
|
uses a disposable self-signed key only under the ignored `.test-runs` tree,
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ All fixture-specific values have embedded, working defaults: the
|
|||||||
`rvboxtest`, the browser listener (`127.0.0.1:5002`), and the private tunnel
|
`rvboxtest`, the browser listener (`127.0.0.1:5002`), and the private tunnel
|
||||||
port (`54001`). They can be overridden without editing tracked files through
|
port (`54001`). They can be overridden without editing tracked files through
|
||||||
`RVBOX_TEST_VBOX_HOST`, `RDP_ACCESS_VRDE_HOST`, `RDP_ACCESS_VRDE_PORT`,
|
`RVBOX_TEST_VBOX_HOST`, `RDP_ACCESS_VRDE_HOST`, `RDP_ACCESS_VRDE_PORT`,
|
||||||
|
`RDP_ACCESS_PROFILE`,
|
||||||
`RDP_ACCESS_WEB_USER`, `RDP_ACCESS_RDP_USER`, `RDP_ACCESS_BIND`,
|
`RDP_ACCESS_WEB_USER`, `RDP_ACCESS_RDP_USER`, `RDP_ACCESS_BIND`,
|
||||||
`RDP_ACCESS_HTTP_PORT`, `RDP_ACCESS_TUNNEL_PORT`, and
|
`RDP_ACCESS_HTTP_PORT`, `RDP_ACCESS_TUNNEL_PORT`, and
|
||||||
`RDP_ACCESS_PUBLIC_HOST`, `RDP_ACCESS_IPV6_BIND`, and
|
`RDP_ACCESS_PUBLIC_HOST`, `RDP_ACCESS_IPV6_BIND`, and
|
||||||
@@ -134,6 +135,36 @@ turn the diagnostic server into a remote target.
|
|||||||
Set `RDP_ACCESS_GUACD_LOG_LEVEL=debug` temporarily when collecting detailed
|
Set `RDP_ACCESS_GUACD_LOG_LEVEL=debug` temporarily when collecting detailed
|
||||||
guacd/RDP negotiation diagnostics; the default is `info`.
|
guacd/RDP negotiation diagnostics; the default is `info`.
|
||||||
|
|
||||||
|
The VM selector and the gateway profile are separate. Set the matching
|
||||||
|
`RVBOX_TEST_VBOX_*` identity variables (and `RDP_ACCESS_VRDE_PORT`) for the
|
||||||
|
VM you want; the complete identity set is listed in
|
||||||
|
[`docs/testing-vm.md`](../../docs/testing-vm.md). For two already-running VMs,
|
||||||
|
give the second gateway a distinct profile and listener ports, for example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
RDP_ACCESS_PROFILE=vm-b \
|
||||||
|
RVBOX_TEST_VBOX_HOST=helium-remote-b \
|
||||||
|
RDP_ACCESS_VRDE_PORT=3391 \
|
||||||
|
RDP_ACCESS_HTTP_PORT=5003 \
|
||||||
|
RDP_ACCESS_TUNNEL_PORT=54002 \
|
||||||
|
test/rdp-access/rdp-access up --bind 0.0.0.0 --public-host vm-b.example.net
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `helium-remote-b`, `3391`, and the public hostname with the second
|
||||||
|
fixture's recorded values, and export its matching VM/snapshot UUID variables
|
||||||
|
before running `up` or `status`. A non-`default` profile stores its verifier,
|
||||||
|
certificate, tunnel state, and logs under `.runtime/<profile>/` and uses the
|
||||||
|
Compose project `rvbox-rdp-access-<profile>`. Keep the HTTP port, public
|
||||||
|
hostname, and profile unique so IPv4/IPv6 listeners and browser sessions do
|
||||||
|
not collide. Inspect, repair, stop, or clean that instance by repeating the
|
||||||
|
same `RDP_ACCESS_PROFILE` and endpoint variables on `status`, `repair`,
|
||||||
|
`down`, or `clean`.
|
||||||
|
|
||||||
|
Profile isolation does not bypass the native controller's exclusive lease:
|
||||||
|
`test-host prepare`/`reset` must still be coordinated when VMs share one
|
||||||
|
fixture host and staging root. For VMs that are already running, the profile
|
||||||
|
and matching identity/VRDE variables are sufficient to select the endpoint.
|
||||||
|
|
||||||
When `--bind 0.0.0.0` is used, `up` also starts a tracked `socat` listener on
|
When `--bind 0.0.0.0` is used, `up` also starts a tracked `socat` listener on
|
||||||
`[::]:$RDP_ACCESS_HTTP_PORT` and forwards it to the IPv4 gateway listener. This
|
`[::]:$RDP_ACCESS_HTTP_PORT` and forwards it to the IPv4 gateway listener. This
|
||||||
matters when the public hostname has an AAAA record: some browsers choose IPv6
|
matters when the public hostname has an AAAA record: some browsers choose IPv6
|
||||||
|
|||||||
+41
-13
@@ -5,11 +5,11 @@ set -eu
|
|||||||
|
|
||||||
helper_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
helper_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
repo_root=$(CDPATH= cd -- "$helper_dir/../.." && pwd)
|
repo_root=$(CDPATH= cd -- "$helper_dir/../.." && pwd)
|
||||||
runtime_dir=$helper_dir/.runtime
|
runtime_root=$helper_dir/.runtime
|
||||||
compose_file=$helper_dir/compose.yaml
|
compose_file=$helper_dir/compose.yaml
|
||||||
mapping_template=$helper_dir/user-mapping.xml.in
|
mapping_template=$helper_dir/user-mapping.xml.in
|
||||||
project=rvbox-rdp-access
|
|
||||||
|
|
||||||
|
: "${RDP_ACCESS_PROFILE:=default}"
|
||||||
: "${RDP_ACCESS_BIND:=127.0.0.1}"
|
: "${RDP_ACCESS_BIND:=127.0.0.1}"
|
||||||
: "${RDP_ACCESS_HTTP_PORT:=5002}"
|
: "${RDP_ACCESS_HTTP_PORT:=5002}"
|
||||||
: "${RDP_ACCESS_TUNNEL_PORT:=54001}"
|
: "${RDP_ACCESS_TUNNEL_PORT:=54001}"
|
||||||
@@ -38,6 +38,8 @@ Actions:
|
|||||||
the exact unused Guacamole/nginx images
|
the exact unused Guacamole/nginx images
|
||||||
|
|
||||||
up options:
|
up options:
|
||||||
|
--profile NAME isolated VM/gateway profile (default default; use one
|
||||||
|
distinct profile per simultaneous VM gateway)
|
||||||
--bind ADDRESS listener address (default 127.0.0.1; use 0.0.0.0 only
|
--bind ADDRESS listener address (default 127.0.0.1; use 0.0.0.0 only
|
||||||
for a temporary, deliberately public endpoint)
|
for a temporary, deliberately public endpoint)
|
||||||
--http-port PORT HTTPS listener port (default 5002)
|
--http-port PORT HTTPS listener port (default 5002)
|
||||||
@@ -47,7 +49,7 @@ up options:
|
|||||||
--reset-auth discard the saved password hash and prompt again
|
--reset-auth discard the saved password hash and prompt again
|
||||||
--web-password-stdin read the password once from stdin instead of prompting
|
--web-password-stdin read the password once from stdin instead of prompting
|
||||||
|
|
||||||
Environment equivalents: RDP_ACCESS_BIND, RDP_ACCESS_HTTP_PORT,
|
Environment equivalents: RDP_ACCESS_PROFILE, RDP_ACCESS_BIND, RDP_ACCESS_HTTP_PORT,
|
||||||
RDP_ACCESS_TUNNEL_PORT, RDP_ACCESS_PUBLIC_HOST, RDP_ACCESS_WEB_USER,
|
RDP_ACCESS_TUNNEL_PORT, RDP_ACCESS_PUBLIC_HOST, RDP_ACCESS_WEB_USER,
|
||||||
RDP_ACCESS_RDP_USER, RVBOX_TEST_VBOX_HOST, RDP_ACCESS_VRDE_HOST, and
|
RDP_ACCESS_RDP_USER, RVBOX_TEST_VBOX_HOST, RDP_ACCESS_VRDE_HOST, and
|
||||||
RDP_ACCESS_VRDE_PORT. In public mode, RDP_ACCESS_IPV6_BIND and
|
RDP_ACCESS_VRDE_PORT. In public mode, RDP_ACCESS_IPV6_BIND and
|
||||||
@@ -63,6 +65,16 @@ safe_name() {
|
|||||||
case $2 in ''|*[!A-Za-z0-9.-]*) fail "$1 contains unsupported characters" ;; esac
|
case $2 in ''|*[!A-Za-z0-9.-]*) fail "$1 contains unsupported characters" ;; esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
safe_profile() {
|
||||||
|
case $2 in
|
||||||
|
[a-z0-9]* ) ;;
|
||||||
|
* ) fail "$1 must start with a lowercase alphanumeric" ;;
|
||||||
|
esac
|
||||||
|
case $2 in
|
||||||
|
*[!a-z0-9-]*) fail "$1 must contain only lowercase letters, digits, and hyphens" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
safe_port() {
|
safe_port() {
|
||||||
case $2 in ''|*[!0-9]*) fail "$1 must be a port number" ;; esac
|
case $2 in ''|*[!0-9]*) fail "$1 must be a port number" ;; esac
|
||||||
[ "$2" -ge 1024 ] && [ "$2" -le 65535 ] || fail "$1 must be between 1024 and 65535"
|
[ "$2" -ge 1024 ] && [ "$2" -le 65535 ] || fail "$1 must be between 1024 and 65535"
|
||||||
@@ -102,15 +114,6 @@ wait_for_gateway() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
socket_path=$runtime_dir/ssh-control.socket
|
|
||||||
session_file=$runtime_dir/session.env
|
|
||||||
cert_name_file=$runtime_dir/cert-name
|
|
||||||
tunnel_stop_file=$runtime_dir/tunnel.stop
|
|
||||||
tunnel_pid_file=$runtime_dir/tunnel-watchdog.pid
|
|
||||||
tunnel_log_file=$runtime_dir/tunnel.log
|
|
||||||
ipv6_pid_file=$runtime_dir/ipv6-forward.pid
|
|
||||||
ipv6_log_file=$runtime_dir/ipv6-forward.log
|
|
||||||
|
|
||||||
stop_tunnel() {
|
stop_tunnel() {
|
||||||
mkdir -p "$runtime_dir"
|
mkdir -p "$runtime_dir"
|
||||||
: >"$tunnel_stop_file"
|
: >"$tunnel_stop_file"
|
||||||
@@ -429,6 +432,7 @@ password_stdin=false
|
|||||||
remove_images=false
|
remove_images=false
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
|
--profile) [ "$#" -ge 2 ] || fail "--profile needs a value"; RDP_ACCESS_PROFILE=$2; shift 2 ;;
|
||||||
--bind) [ "$#" -ge 2 ] || fail "--bind needs a value"; RDP_ACCESS_BIND=$2; shift 2 ;;
|
--bind) [ "$#" -ge 2 ] || fail "--bind needs a value"; RDP_ACCESS_BIND=$2; shift 2 ;;
|
||||||
--http-port) [ "$#" -ge 2 ] || fail "--http-port needs a value"; RDP_ACCESS_HTTP_PORT=$2; shift 2 ;;
|
--http-port) [ "$#" -ge 2 ] || fail "--http-port needs a value"; RDP_ACCESS_HTTP_PORT=$2; shift 2 ;;
|
||||||
--tunnel-port) [ "$#" -ge 2 ] || fail "--tunnel-port needs a value"; RDP_ACCESS_TUNNEL_PORT=$2; shift 2 ;;
|
--tunnel-port) [ "$#" -ge 2 ] || fail "--tunnel-port needs a value"; RDP_ACCESS_TUNNEL_PORT=$2; shift 2 ;;
|
||||||
@@ -442,6 +446,26 @@ while [ "$#" -gt 0 ]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
safe_profile RDP_ACCESS_PROFILE "$RDP_ACCESS_PROFILE"
|
||||||
|
profile=$RDP_ACCESS_PROFILE
|
||||||
|
if [ "$profile" = default ]; then
|
||||||
|
runtime_dir=$runtime_root
|
||||||
|
project=rvbox-rdp-access
|
||||||
|
else
|
||||||
|
runtime_dir=$runtime_root/$profile
|
||||||
|
project=rvbox-rdp-access-$profile
|
||||||
|
fi
|
||||||
|
[ ! -L "$runtime_root" ] || fail "runtime root must not be a symlink"
|
||||||
|
[ ! -L "$runtime_dir" ] || fail "runtime directory must not be a symlink"
|
||||||
|
socket_path=$runtime_dir/ssh-control.socket
|
||||||
|
session_file=$runtime_dir/session.env
|
||||||
|
cert_name_file=$runtime_dir/cert-name
|
||||||
|
tunnel_stop_file=$runtime_dir/tunnel.stop
|
||||||
|
tunnel_pid_file=$runtime_dir/tunnel-watchdog.pid
|
||||||
|
tunnel_log_file=$runtime_dir/tunnel.log
|
||||||
|
ipv6_pid_file=$runtime_dir/ipv6-forward.pid
|
||||||
|
ipv6_log_file=$runtime_dir/ipv6-forward.log
|
||||||
|
|
||||||
safe_bind "$RDP_ACCESS_BIND"
|
safe_bind "$RDP_ACCESS_BIND"
|
||||||
safe_port RDP_ACCESS_HTTP_PORT "$RDP_ACCESS_HTTP_PORT"
|
safe_port RDP_ACCESS_HTTP_PORT "$RDP_ACCESS_HTTP_PORT"
|
||||||
safe_port RDP_ACCESS_TUNNEL_PORT "$RDP_ACCESS_TUNNEL_PORT"
|
safe_port RDP_ACCESS_TUNNEL_PORT "$RDP_ACCESS_TUNNEL_PORT"
|
||||||
@@ -520,6 +544,7 @@ case $action in
|
|||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
[ "$#" -eq 0 ] || { usage >&2; fail "status accepts no options"; }
|
[ "$#" -eq 0 ] || { usage >&2; fail "status accepts no options"; }
|
||||||
|
printf 'profile=%s project=%s runtime=%s\n' "$profile" "$project" "$runtime_dir"
|
||||||
"$repo_root/scripts/windows/test-host" status || true
|
"$repo_root/scripts/windows/test-host" status || true
|
||||||
if [ -f "$session_file" ]; then sed -n '1p' "$session_file"; fi
|
if [ -f "$session_file" ]; then sed -n '1p' "$session_file"; fi
|
||||||
compose ps
|
compose ps
|
||||||
@@ -568,7 +593,10 @@ case $action in
|
|||||||
stop_ipv6_forward
|
stop_ipv6_forward
|
||||||
stop_tunnel
|
stop_tunnel
|
||||||
compose down --remove-orphans || true
|
compose down --remove-orphans || true
|
||||||
case $runtime_dir in "$helper_dir"/.runtime) rm -rf "$runtime_dir" ;; *) fail "unsafe runtime path" ;; esac
|
case "$runtime_dir" in
|
||||||
|
"$runtime_root"|"$runtime_root/$profile") rm -rf "$runtime_dir" ;;
|
||||||
|
*) fail "unsafe runtime path" ;;
|
||||||
|
esac
|
||||||
if [ "$remove_images" = true ]; then
|
if [ "$remove_images" = true ]; then
|
||||||
docker image rm guacamole/guacamole:1.6.0 guacamole/guacd:1.6.0 nginx:1.27-alpine >/dev/null 2>&1 || true
|
docker image rm guacamole/guacamole:1.6.0 guacamole/guacd:1.6.0 nginx:1.27-alpine >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -49,6 +49,18 @@ func TestNativeFixtureAssets_HP_HARNESS_20(t *testing.T) {
|
|||||||
t.Fatalf("native test-host is missing compressed transfer contract %q", required)
|
t.Fatalf("native test-host is missing compressed transfer contract %q", required)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rdpAccess := read("test/rdp-access/rdp-access")
|
||||||
|
for _, required := range []string{"RDP_ACCESS_PROFILE", "--profile", "rvbox-rdp-access-$profile", "runtime_root", "ipv6_forward_status", "compose restart guacd", "compose down --remove-orphans"} {
|
||||||
|
if !strings.Contains(rdpAccess, required) {
|
||||||
|
t.Fatalf("RDP helper is missing isolated lifecycle contract %q", required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rdpReadme := read("test/rdp-access/README.md")
|
||||||
|
for _, required := range []string{"Current fixture quick start", "RDP_ACCESS_PROFILE=vm-b", "Partial stack or changed endpoint", "Inspect, repair, stop, or clean that instance"} {
|
||||||
|
if !strings.Contains(rdpReadme, required) {
|
||||||
|
t.Fatalf("RDP helper documentation is missing %q", required)
|
||||||
|
}
|
||||||
|
}
|
||||||
compose := read("test/linux-server/compose.yaml")
|
compose := read("test/linux-server/compose.yaml")
|
||||||
for _, required := range []string{"../../bin/rvbox-server", "nginx:1.27-alpine", "rvbox.native.run_id", "RVBOX_NATIVE_RUNTIME_DIR", "certgen", "networks: [native]"} {
|
for _, required := range []string{"../../bin/rvbox-server", "nginx:1.27-alpine", "rvbox.native.run_id", "RVBOX_NATIVE_RUNTIME_DIR", "certgen", "networks: [native]"} {
|
||||||
if !strings.Contains(compose, required) {
|
if !strings.Contains(compose, required) {
|
||||||
|
|||||||
Reference in New Issue
Block a user