Skip to content
Open
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 packages/api/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,7 @@ type PAMSessionCredentials struct {
ServiceAccountToken string `json:"serviceAccountToken,omitempty"`
ServiceAccountName string `json:"serviceAccountName,omitempty"`
Namespace string `json:"namespace,omitempty"`
Domain string `json:"domain,omitempty"`
}

type MFASessionStatus string
Expand Down
1 change: 1 addition & 0 deletions packages/pam/handlers/rdp/bridge_cgo_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func (p *RDPProxy) HandleConnection(ctx context.Context, clientConn net.Conn) er
p.config.TargetPort,
p.config.InjectUsername,
p.config.InjectPassword,
p.config.InjectDomain,
)
if err != nil {
return fmt.Errorf("rdp proxy: start bridge: %w", err)
Expand Down
20 changes: 15 additions & 5 deletions packages/pam/handlers/rdp/bridge_cgo_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ import (

// StartWithConn hands an independent dup of conn's fd to the bridge.
// For TLS-wrapped or otherwise non-fd-backed conns, use StartWithReadWriter.
func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) {
// `domain` is empty for local accounts; set to the AD domain name for
// domain-joined NTLM CredSSP.
func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) {
dupFd, err := dupConnFD(conn)
if err != nil {
return nil, fmt.Errorf("rdp bridge: dup client fd: %w", err)
}
return startWithDupedFD(dupFd, targetHost, targetPort, username, password)
return startWithDupedFD(dupFd, targetHost, targetPort, username, password, domain)
}

// Ownership of dupFd transfers to Rust on success; we close it on failure.
func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password string) (*Bridge, error) {
func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) {
success := false
defer func() {
if !success {
Expand All @@ -46,13 +48,21 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username,
cPass := C.CString(password)
defer C.free(unsafe.Pointer(cPass))

// Empty domain -> NULL pointer; bridge treats both the same way.
var cDomain *C.char
if domain != "" {
cDomain = C.CString(domain)
defer C.free(unsafe.Pointer(cDomain))
}

var handle C.uint64_t
rc := C.rdp_bridge_start_unix_fd(
C.int(dupFd),
cHost,
C.uint16_t(targetPort),
cUser,
cPass,
cDomain,
&handle,
)
if rc != C.RDP_BRIDGE_OK {
Expand All @@ -75,7 +85,7 @@ func startWithDupedFD(dupFd int, targetHost string, targetPort uint16, username,
//
// Cost: two extra in-process copies and a loopback round-trip per byte.
// Negligible vs. the TLS + CredSSP work on either side.
func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) {
func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err)
Expand Down Expand Up @@ -110,7 +120,7 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16,
return nil, fmt.Errorf("rdp bridge: dup accepted fd: %w", err)
}

bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password)
bridge, err := startWithDupedFD(dupFd, targetHost, targetPort, username, password, domain)
if err != nil {
_ = peer.Close()
return nil, err
Expand Down
17 changes: 12 additions & 5 deletions packages/pam/handlers/rdp/bridge_cgo_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import (
"golang.org/x/sys/windows"
)

func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password string) (*Bridge, error) {
func StartWithConn(conn net.Conn, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) {
dupSocket, err := dupConnSocket(conn)
if err != nil {
return nil, fmt.Errorf("rdp bridge: dup client socket: %w", err)
}
return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password)
return startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, domain)
}

func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password string) (*Bridge, error) {
func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) {
success := false
defer func() {
if !success {
Expand All @@ -44,13 +44,20 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor
cPass := C.CString(password)
defer C.free(unsafe.Pointer(cPass))

var cDomain *C.char
if domain != "" {
cDomain = C.CString(domain)
defer C.free(unsafe.Pointer(cDomain))
}

var handle C.uint64_t
rc := C.rdp_bridge_start_windows_socket(
C.uintptr_t(dupSocket),
cHost,
C.uint16_t(targetPort),
cUser,
cPass,
cDomain,
&handle,
)
if rc != C.RDP_BRIDGE_OK {
Expand All @@ -60,7 +67,7 @@ func startWithDupedSocket(dupSocket windows.Handle, targetHost string, targetPor
return &Bridge{handle: uint64(handle)}, nil
}

func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password string) (*Bridge, error) {
func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16, username, password, domain string) (*Bridge, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, fmt.Errorf("rdp bridge: loopback listen: %w", err)
Expand Down Expand Up @@ -95,7 +102,7 @@ func StartWithReadWriter(rw io.ReadWriter, targetHost string, targetPort uint16,
return nil, fmt.Errorf("rdp bridge: dup accepted socket: %w", err)
}

bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password)
bridge, err := startWithDupedSocket(dupSocket, targetHost, targetPort, username, password, domain)
if err != nil {
_ = peer.Close()
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions packages/pam/handlers/rdp/bridge_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import (
// where the Rust bridge isn't compiled. All entry points return
// ErrRdpUnavailable.

func StartWithConn(_ net.Conn, _ string, _ uint16, _, _ string) (*Bridge, error) {
func StartWithConn(_ net.Conn, _ string, _ uint16, _, _, _ string) (*Bridge, error) {
return nil, ErrRdpUnavailable
}

func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _ string) (*Bridge, error) {
func StartWithReadWriter(_ io.ReadWriter, _ string, _ uint16, _, _, _ string) (*Bridge, error) {
return nil, ErrRdpUnavailable
}

Expand Down
5 changes: 5 additions & 0 deletions packages/pam/handlers/rdp/native/include/rdp_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ extern "C" {
#define RDP_BRIDGE_BAD_ARG -2
#define RDP_BRIDGE_RUNTIME_ERROR -3

// `domain` is optional. NULL or empty string means no domain (NTLM falls back
// to local-account auth). Set this for AD domain accounts so NTLM CredSSP
// authenticates against the target's AD binding rather than its local SAM.
#if defined(__unix__) || defined(__APPLE__)
int32_t rdp_bridge_start_unix_fd(
int client_fd,
const char *target_host,
uint16_t target_port,
const char *username,
const char *password,
const char *domain,
uint64_t *out_handle
);
#endif
Expand All @@ -38,6 +42,7 @@ int32_t rdp_bridge_start_windows_socket(
uint16_t target_port,
const char *username,
const char *password,
const char *domain,
uint64_t *out_handle
);
#endif
Expand Down
8 changes: 7 additions & 1 deletion packages/pam/handlers/rdp/native/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub struct TargetEndpoint {
pub port: u16,
pub username: String,
pub password: String,
/// Set for AD domain accounts; flows into NTLM CredSSP via connector config.
pub domain: Option<String>,
}

pub async fn run_mitm(
Expand Down Expand Up @@ -260,7 +262,11 @@ async fn run_connector_half(target: TargetEndpoint) -> Result<(ErasedStream, byt
let client_addr = target_tcp.local_addr().context("connector: local_addr")?;

let mut target_framed = ironrdp_tokio::TokioFramed::new(target_tcp);
let config = connector_config(target.username.clone(), target.password.clone());
let config = connector_config(
target.username.clone(),
target.password.clone(),
target.domain.clone(),
);
let mut connector = ClientConnector::new(config, client_addr);

let should_upgrade = ironrdp_tokio::connect_begin(&mut target_framed, &mut connector)
Expand Down
6 changes: 4 additions & 2 deletions packages/pam/handlers/rdp/native/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo};
pub const DEFAULT_WIDTH: u16 = 1920;
pub const DEFAULT_HEIGHT: u16 = 1080;

pub fn connector_config(username: String, password: String) -> Config {
pub fn connector_config(username: String, password: String, domain: Option<String>) -> Config {
Config {
desktop_size: DesktopSize {
width: DEFAULT_WIDTH,
Expand All @@ -25,7 +25,9 @@ pub fn connector_config(username: String, password: String) -> Config {
enable_credssp: true,

credentials: Credentials::UsernamePassword { username, password },
domain: None,
// Set for AD domain accounts; IronRDP forwards this in NTLM CredSSP so
// the target's LSA authenticates against AD rather than the local SAM.
domain,

// Shape-fillers: unused after CredSSP (see module doc).
client_build: 0,
Expand Down
14 changes: 11 additions & 3 deletions packages/pam/handlers/rdp/native/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fn spawn_session(
port: u16,
username: String,
password: String,
domain: Option<String>,
) -> anyhow::Result<u64> {
client_tcp.set_nonblocking(true)?;
let cancel = CancellationToken::new();
Expand All @@ -77,6 +78,7 @@ fn spawn_session(
port,
username,
password,
domain,
};
run_mitm(client, endpoint, cancel_for_thread).await
})
Expand All @@ -91,7 +93,8 @@ fn spawn_session(
/// # Safety
///
/// `client_fd` ownership transfers to the bridge on OK, stays with the
/// caller on error. Strings must be NUL-terminated valid UTF-8.
/// caller on error. Strings must be NUL-terminated valid UTF-8. `domain`
/// may be NULL or empty for non-domain sessions.
#[cfg(unix)]
#[no_mangle]
pub unsafe extern "C" fn rdp_bridge_start_unix_fd(
Expand All @@ -100,6 +103,7 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd(
target_port: u16,
username: *const c_char,
password: *const c_char,
domain: *const c_char,
out_handle: *mut u64,
) -> i32 {
if out_handle.is_null() {
Expand All @@ -117,11 +121,13 @@ pub unsafe extern "C" fn rdp_bridge_start_unix_fd(
Some(v) => v,
None => return RDP_BRIDGE_BAD_ARG,
};
// Empty domain string is treated the same as NULL: no domain.
let domain = unsafe { c_str_to_owned(domain) }.filter(|s| !s.is_empty());

use std::os::unix::io::FromRawFd;
let client_tcp = unsafe { StdTcpStream::from_raw_fd(client_fd) };

match spawn_session(client_tcp, host, target_port, username, password) {
match spawn_session(client_tcp, host, target_port, username, password, domain) {
Ok(id) => {
unsafe { *out_handle = id };
RDP_BRIDGE_OK
Expand All @@ -144,6 +150,7 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket(
target_port: u16,
username: *const c_char,
password: *const c_char,
domain: *const c_char,
out_handle: *mut u64,
) -> i32 {
if out_handle.is_null() {
Expand All @@ -161,11 +168,12 @@ pub unsafe extern "C" fn rdp_bridge_start_windows_socket(
Some(v) => v,
None => return RDP_BRIDGE_BAD_ARG,
};
let domain = unsafe { c_str_to_owned(domain) }.filter(|s| !s.is_empty());

use std::os::windows::io::{FromRawSocket, RawSocket};
let client_tcp = unsafe { StdTcpStream::from_raw_socket(client_socket as RawSocket) };

match spawn_session(client_tcp, host, target_port, username, password) {
match spawn_session(client_tcp, host, target_port, username, password, domain) {
Ok(id) => {
unsafe { *out_handle = id };
RDP_BRIDGE_OK
Expand Down
5 changes: 4 additions & 1 deletion packages/pam/handlers/rdp/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ type RDPProxyConfig struct {
TargetPort uint16
InjectUsername string
InjectPassword string
SessionID string
// Empty for local accounts; AD domain name (e.g. "CORP.EXAMPLE.COM") for
// domain-joined NTLM CredSSP. Backend session credentials populate this.
InjectDomain string
SessionID string
// Retained for API symmetry with other PAM handlers; not yet written
// through (no RDP session recording in this MVP).
SessionLogger session.SessionLogger
Expand Down
1 change: 1 addition & 0 deletions packages/pam/pam-proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ func HandlePAMProxy(ctx context.Context, conn *tls.Conn, pamConfig *GatewayPAMCo
TargetPort: uint16(credentials.Port),
InjectUsername: credentials.Username,
InjectPassword: credentials.Password,
InjectDomain: credentials.Domain,
SessionID: pamConfig.SessionId,
SessionLogger: sessionLogger,
}
Expand Down
2 changes: 2 additions & 0 deletions packages/pam/session/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type PAMCredentials struct {
ServiceAccountToken string
ServiceAccountName string
Namespace string
Domain string
PolicyRules *api.PAMPolicyRules
}

Expand Down Expand Up @@ -186,6 +187,7 @@ func (cm *CredentialsManager) GetPAMSessionCredentials(sessionId string, expiryT
ServiceAccountToken: response.Credentials.ServiceAccountToken,
ServiceAccountName: response.Credentials.ServiceAccountName,
Namespace: response.Credentials.Namespace,
Domain: response.Credentials.Domain,
PolicyRules: response.PolicyRules,
}

Expand Down
Loading