Skip to main content
The FreeRDP RDP proxy is a man-in-the-middle component that sits between an RDP client and a real RDP server. It terminates the client-side RDP connection, establishes a new connection to the target server on behalf of the client, and forwards traffic in both directions.

What the proxy does

  • Accepts RDP connections from clients (the “front” side)
  • Connects to a configured target RDP server (the “back” side)
  • Forwards graphics, input, and channel data between the two sides
  • Exposes hook and filter callbacks that loadable modules can use to inspect or modify traffic

Use cases

Use caseHow
Security inspection / DLPFilter keyboard input, channel data, or clipboard content via a module
Session recordingHook frame or channel events in a module and write to disk
Load balancing / routingUse ServerFetchTargetAddr filter to direct clients to different backends
Protocol translationIntercept and rewrite specific channels
Credential redirectionHandle ServerPeerLogon to capture or replace credentials

Building

cmake -DWITH_PROXY=ON ..
This produces the freerdp-proxy binary and the libfreerdp-proxy library.

Running the proxy

freerdp-proxy <config.ini>
Dump a default configuration file to start from:
freerdp-proxy --dump-config proxy.ini

Configuration file

The proxy is configured with an INI file. All sections and keys are described below.
[Server]
; Address the proxy listens on (optional, defaults to all interfaces)
Host=0.0.0.0
; Port the proxy listens on
Port=3389

[Target]
; When true the proxy always connects to TargetHost:TargetPort.
; When false a module must supply the target via ServerFetchTargetAddr.
FixedTarget=true
Host=rdp-server.example.com
Port=3389
; Optional credentials to use when connecting to the target
;User=domain\user
;Password=secret
;Domain=EXAMPLE
; Minimum TLS security level for the back-end connection (0–5, OpenSSL scale)
TlsSecLevel=1

[Channels]
; Enable or disable individual virtual channels
GFX=true
DisplayControl=true
Clipboard=false
AudioInput=true
AudioOutput=true
DeviceRedirection=false
VideoRedirection=true
CameraRedirection=true
RemoteApp=false
; Comma-separated list of additional channels to pass through
Passthrough=
; Comma-separated list of channels to intercept (module receives data)
Intercept=
; When true, Passthrough list acts as a blocklist instead of an allowlist
PassthroughIsBlacklist=false

[Input]
Keyboard=true
Mouse=true
Multitouch=true

[Security]
; Front-end (client → proxy) security
ServerTlsSecurity=true
ServerRdpSecurity=true
ServerNlaSecurity=false
; Back-end (proxy → target) security
ClientTlsSecurity=true
ClientNlaSecurity=true
ClientRdpSecurity=true
ClientAllowFallbackToTls=true

[Certificates]
; PEM files used for the front-end TLS handshake
CertificateFile=/path/to/server.crt
PrivateKeyFile=/path/to/server.key
; Alternatively supply PEM content inline (base64 encoded)
;CertificateContent=<base64>
;PrivateKeyContent=<base64>

[Codecs]
RFX=true
NSC=true

[Plugins]
; Comma-separated list of shared library filenames to load as modules
Modules=my_module.so
; Comma-separated list of module names that must load successfully
Required=my_module
When FixedTarget=false a loaded module must set target_address and target_port in the proxyFetchTargetEventInfo struct inside its ServerFetchTargetAddr filter callback, otherwise the connection is aborted.

Proxy server API

Embed the proxy in an application using the public C API in include/freerdp/server/proxy/proxy_server.h:
#include <freerdp/server/proxy/proxy_server.h>
#include <freerdp/server/proxy/proxy_config.h>

// Load configuration from file
proxyConfig* config = pf_server_config_load_file("/etc/freerdp/proxy.ini");

// Create proxy server instance
proxyServer* server = pf_server_new(config);

// Optionally register built-in modules before starting
pf_server_add_module(server, my_module_entry_point, userdata);

// Start listening (non-blocking)
pf_server_start(server);

// Block in the main loop until stopped
pf_server_run(server);

// Cleanup
pf_server_stop(server);
pf_server_free(server);
pf_server_config_free(config);

Additional config helpers

// Dump a default config file to disk
BOOL pf_server_config_dump(const char* file);

// Load config from an in-memory INI string
proxyConfig* pf_server_config_load_buffer(const char* buffer);

// Load config from an already-parsed wIniFile*
proxyConfig* server_config_load_ini(wIniFile* ini);

// Read an arbitrary key from the config INI
const char* pf_config_get(const proxyConfig* config, const char* section, const char* key);

// Clone configuration
BOOL pf_config_clone(proxyConfig** dst, const proxyConfig* config);

Module API

Modules extend the proxy at runtime. A module is a shared library (.so / .dll) that exports a single entry point:
// Entry point signature — export this symbol from your module
BOOL proxy_module_entry_point(proxyPluginsManager* manager, void* userdata);
Inside the entry point, register a proxyPlugin struct:
#include <freerdp/server/proxy/proxy_modules_api.h>

static BOOL my_keyboard_filter(proxyPlugin* plugin, proxyData* pdata, void* param)
{
    proxyKeyboardEventInfo* event = (proxyKeyboardEventInfo*)param;
    // return FALSE to drop the key event, TRUE to forward it
    if (event->rdp_scan_code == RDP_SCANCODE_KEY_X)
        return FALSE;
    return TRUE;
}

static BOOL my_server_post_connect(proxyPlugin* plugin, proxyData* pdata, void* param)
{
    // param is freerdp_peer*
    freerdp_peer* peer = (freerdp_peer*)param;
    // inspect or log connection details
    return TRUE;
}

static BOOL my_unload(proxyPlugin* plugin)
{
    // free plugin->custom if allocated
    return TRUE;
}

BOOL proxy_module_entry_point(proxyPluginsManager* manager, void* userdata)
{
    proxyPlugin plugin = { 0 };

    plugin.name        = "my_module";
    plugin.description = "Example module";
    plugin.PluginUnload = my_unload;

    // Hook: called after the server peer connects
    plugin.ServerPostConnect = my_server_post_connect;

    // Filter: return FALSE to drop keyboard events
    plugin.KeyboardEvent = my_keyboard_filter;

    return manager->RegisterPlugin(manager, &plugin);
}

Hook types

Hooks are called for lifecycle events. Returning FALSE aborts the connection.
Hook fieldcustom typeWhen called
ClientInitConnectrdpContext*Before client connection is initialised
ClientPreConnectrdpContext*Before client sends ClientInfo PDU
ClientPostConnectrdpContext*After client connection is fully established
ClientPostDisconnectrdpContext*After client disconnects
ClientX509CertificaterdpContext*Server certificate verification
ClientLoginFailurerdpContext*Back-end login failed
ClientEndPaintrdpContext*After each paint cycle
ClientRedirectrdpContext*Server sent a redirect PDU
ClientLoadChannelsrdpContext*Channel list is available
ServerPostConnectfreerdp_peer*After the front-end peer connects
ServerPeerActivatefreerdp_peer*Peer becomes active
ServerChannelsInitfreerdp_peer*Channels are initialised
ServerChannelsFreefreerdp_peer*Channels are being freed
ServerSessionEndfreerdp_peer*Session is ending
ServerSessionInitializefreerdp_peer*Session is initialising
ServerSessionStartedfreerdp_peer*Session has started

Filter types

Filters are called for data events. Returning FALSE drops (does not forward) the event.
Filter fieldParameter typeDescription
KeyboardEventproxyKeyboardEventInfo*Keyboard key event
UnicodeEventproxyUnicodeEventInfo*Unicode keyboard event
MouseEventproxyMouseEventInfo*Mouse event
MouseExEventproxyMouseExEventInfo*Extended mouse event
ClientChannelDataproxyChannelDataEventInfo*Data from client on a passthrough channel
ServerChannelDataproxyChannelDataEventInfo*Data from server on a passthrough channel
ChannelCreateproxyChannelDataEventInfo*Static channel creation
DynamicChannelCreateproxyChannelDataEventInfo*Dynamic channel creation
ServerFetchTargetAddrproxyFetchTargetEventInfo*Override the target server address
ServerPeerLogonproxyServerPeerLogon*Client authentication completed
DynChannelToInterceptproxyChannelToInterceptData*Decide whether to intercept a DVC
DynChannelInterceptproxyDynChannelInterceptData*Receive intercepted DVC data
StaticChannelToInterceptproxyChannelToInterceptData*Decide whether to intercept a static channel

Per-session plugin data

Modules can store and retrieve session-scoped data using the plugin manager:
// Store data for this session
plugin->mgr->SetPluginData(plugin->mgr, "my_module", pdata, my_session_data);

// Retrieve it later (e.g., in a filter callback)
void* data = plugin->mgr->GetPluginData(plugin->mgr, "my_module", pdata);

// Forcibly abort the session from a callback
plugin->mgr->AbortConnect(plugin->mgr, pdata);
Build the example modules in server/proxy/modules/ as a reference. They demonstrate logging, traffic inspection, and target address overriding.