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
41 changes: 41 additions & 0 deletions packages/pam/handlers/rdp/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,44 @@ type Bridge struct {
handle uint64
cleanup func()
}

// EventType discriminates the variants in Event.
type EventType uint8

const (
EventTypeKeyboard EventType = 1
EventTypeUnicode EventType = 2
EventTypeMouse EventType = 3
EventTypeTargetFrame EventType = 4
)

// Action identifies the RDP framing of a TargetFrame event.
type Action uint8

const (
ActionX224 Action = 0
ActionFastPath Action = 1
)

// Fields are reused across variants; switch on Type.
type Event struct {
Type EventType
ElapsedNs uint64
Scancode uint8
CodePoint uint16
X uint16
Y uint16
Flags uint32
WheelDelta int32
Action Action
Payload []byte
}

// PollResult discriminates PollEvent outcomes.
type PollResult uint8

const (
PollOK PollResult = 0
PollTimeout PollResult = 1
PollEnded PollResult = 2
)
85 changes: 81 additions & 4 deletions packages/pam/handlers/rdp/bridge_cgo_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package rdp
/*
#cgo CFLAGS: -I${SRCDIR}/native/include

#include <stdlib.h>
#include "rdp_bridge.h"
*/
import "C"
Expand All @@ -14,6 +15,8 @@ import (
"errors"
"fmt"
"net"
"time"
"unsafe"
)

func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) error {
Expand All @@ -36,6 +39,26 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er
}
defer bridge.Close()

// Drain bridge tap events into the session logger. The Rust side closes
// the events channel when the session ends, so the goroutine exits via
// PollEnded without needing an explicit shutdown signal.
drainCtx, cancelDrain := context.WithCancel(ctx)
drainDone := make(chan struct{})
go func() {
defer close(drainDone)
drainBridgeEvents(drainCtx, bridge, p.config.SessionLogger, p.config.SessionID, p.config.SessionStartedAt)
}()
defer func() {
cancelDrain()
// Wait briefly for the drain loop to exit so a cancelled session
// can't race the Bridge.Close below. PollEvent's timeout caps how
// long this can take.
select {
case <-drainDone:
case <-time.After(2 * pollTimeout):
}
}()

waitErr := make(chan error, 1)
go func() { waitErr <- bridge.Wait() }()

Expand Down Expand Up @@ -89,8 +112,62 @@ func (b *Bridge) Close() error {
return nil
}

// IsSupported reports whether this build has a real RDP bridge. Used
// by the gateway to decide whether to advertise RDP in the capabilities
// response: a stub-build gateway that advertises support would route
// RDP sessions only to fail them at connect time.
// True when the real bridge is compiled in (vs the stub).
func IsSupported() bool { return true }

// PollEvent drains one tap event with the given timeout. The returned Event
// is only meaningful when result == PollOK. PollEvent is not safe to call
// concurrently for the same Bridge; serialize calls in a single goroutine.
func (b *Bridge) PollEvent(timeout time.Duration) (PollResult, Event, error) {
timeoutMs := timeout.Milliseconds()
if timeoutMs < 0 {
timeoutMs = 0
}
if timeoutMs > int64(^C.uint32_t(0)) {
timeoutMs = int64(^C.uint32_t(0))
}

var raw C.struct_RdpEvent
rc := C.rdp_bridge_poll_event(C.uint64_t(b.handle), &raw, C.uint32_t(timeoutMs))

switch rc {
case C.RDP_POLL_OK:
// fall through to event materialization below
case C.RDP_POLL_TIMEOUT:
return PollTimeout, Event{}, nil
case C.RDP_POLL_ENDED:
return PollEnded, Event{}, nil
case C.RDP_POLL_INVALID_HANDLE:
return PollEnded, Event{}, ErrInvalidHandle
default:
return PollEnded, Event{}, fmt.Errorf("rdp bridge: poll returned unexpected status %d", int32(rc))
}

ev := Event{
Type: EventType(uint8(raw.event_type)),
ElapsedNs: uint64(raw.elapsed_ns),
Flags: uint32(raw.flags),
WheelDelta: int32(raw.wheel_delta),
Action: Action(uint8(raw.action)),
}
switch ev.Type {
case EventTypeKeyboard:
ev.Scancode = uint8(raw.value_a)
case EventTypeUnicode:
ev.CodePoint = uint16(raw.value_a)
case EventTypeMouse:
ev.X = uint16(raw.value_a)
ev.Y = uint16(raw.value_b)
case EventTypeTargetFrame:
// Always free the libc-malloc'd buffer Rust handed us, even if
// the copy below is empty -- ownership transfer is unconditional.
if raw.payload_ptr != nil {
defer C.free(unsafe.Pointer(raw.payload_ptr))
if raw.payload_len > 0 {
ev.Payload = C.GoBytes(unsafe.Pointer(raw.payload_ptr), C.int(raw.payload_len))
}
}
}

return PollOK, ev, nil
}
15 changes: 2 additions & 13 deletions packages/pam/handlers/rdp/bridge_cgo_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,8 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username,
return &Bridge{handle: uint64(handle)}, nil
}

// StartWithReadWriter adapts an fd-less Go byte stream (e.g. *tls.Conn
// from the gateway's mTLS-wrapped virtual connection) to the bridge,
// which needs a real file descriptor because the Rust side uses tokio's
// TcpStream::from_raw_fd and does direct async I/O on the socket.
//
// Trick: open a loopback TCP pair. Hand one end's fd to the bridge (it
// thinks it has a real client). Keep the other end in Go and shuttle
// bytes between it and rw with two io.Copy goroutines.
//
// rw (e.g. *tls.Conn) <-io.Copy-> peer <-kernel loopback-> accepted (fd -> Rust bridge)
//
// Cost: two extra in-process copies and a loopback round-trip per byte.
// Negligible vs. the TLS + CredSSP work on either side.
// Adapts an fd-less Go byte stream to the Rust bridge (which needs a real fd
// for tokio's TcpStream::from_raw_fd) by routing through a loopback TCP pair.
func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions packages/pam/handlers/rdp/bridge_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"io"
"net"
"time"
)

// Stub implementations for builds without `-tags rdp` or on platforms
Expand All @@ -29,6 +30,10 @@ func (b *Bridge) Wait() error { return ErrRdpUnavailable }
func (b *Bridge) Cancel() error { return ErrRdpUnavailable }
func (b *Bridge) Close() error { return ErrRdpUnavailable }

func (b *Bridge) PollEvent(_ time.Duration) (PollResult, Event, error) {
return PollEnded, Event{}, ErrRdpUnavailable
}

// IsSupported reports whether this build has a real RDP bridge. See the
// rdp-enabled counterpart in bridge_cgo_shared.go for details.
func IsSupported() bool { return false }
2 changes: 2 additions & 0 deletions packages/pam/handlers/rdp/native/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/pam/handlers/rdp/native/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ path = "src/lib.rs"
[dependencies]
ironrdp-acceptor = "0.8"
ironrdp-connector = "0.8"
ironrdp-core = "0.1"
ironrdp-tokio = { version = "0.8", features = ["reqwest"] }
ironrdp-pdu = "0.7"
ironrdp-tls = { version = "0.2", features = ["rustls"] }
x509-cert = { version = "0.2", features = ["std"] }
libc = "0.2"

tokio = { version = "1", features = ["full"] }
tokio-util = "0.7"
Expand Down
36 changes: 31 additions & 5 deletions packages/pam/handlers/rdp/native/include/rdp_bridge.h
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
/*
* infisical-rdp-bridge C ABI. See ffi.rs for details. Lifecycle:
* start_* -> wait -> free; cancel may be called from any thread.
* start_* transfers ownership of the client fd/socket to the bridge.
*/
/* C ABI; see ffi.rs. Lifecycle: start_* -> wait -> free. start_* takes
* ownership of the client fd/socket. cancel is thread-safe. */

#ifndef INFISICAL_RDP_BRIDGE_H
#define INFISICAL_RDP_BRIDGE_H
Expand Down Expand Up @@ -46,6 +43,35 @@ int32_t rdp_bridge_wait(uint64_t handle);
int32_t rdp_bridge_cancel(uint64_t handle);
int32_t rdp_bridge_free(uint64_t handle);

/* Poll return codes (distinct number space from the bridge status codes
* above; consumed by rdp_bridge_poll_event only). */
#define RDP_POLL_OK 0
#define RDP_POLL_TIMEOUT 1
#define RDP_POLL_ENDED 2
#define RDP_POLL_INVALID_HANDLE -1

/* Event type discriminator. */
#define RDP_EVENT_KEYBOARD 1
#define RDP_EVENT_UNICODE 2
#define RDP_EVENT_MOUSE 3
#define RDP_EVENT_TARGET_FRAME 4

/* Fields reused across variants; check event_type. For TargetFrame,
* payload_ptr is libc-malloc'd and the Go caller must C.free it. */
struct RdpEvent {
uint8_t event_type;
uint64_t elapsed_ns;
uint32_t value_a;
uint32_t value_b;
uint32_t flags;
int32_t wheel_delta;
uint8_t action;
uint8_t *payload_ptr;
uint32_t payload_len;
};

int32_t rdp_bridge_poll_event(uint64_t handle, struct RdpEvent *out, uint32_t timeout_ms);

#ifdef __cplusplus
}
#endif
Expand Down
Loading
Loading