Coverage Report

Created: 2026-10-15 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/FreeRDP/channels/sshagent/client/sshagent_main.c
Line
Count
Source
1
/**
2
 * FreeRDP: A Remote Desktop Protocol Implementation
3
 * SSH Agent Virtual Channel Extension
4
 *
5
 * Copyright 2013 Christian Hofstaedtler
6
 * Copyright 2015 Thincast Technologies GmbH
7
 * Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
8
 * Copyright 2017 Ben Cohen
9
 *
10
 * Licensed under the Apache License, Version 2.0 (the "License");
11
 * you may not use this file except in compliance with the License.
12
 * You may obtain a copy of the License at
13
 *
14
 *     http://www.apache.org/licenses/LICENSE-2.0
15
 *
16
 * Unless required by applicable law or agreed to in writing, software
17
 * distributed under the License is distributed on an "AS IS" BASIS,
18
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
 * See the License for the specific language governing permissions and
20
 * limitations under the License.
21
 */
22
23
/*
24
 * sshagent_main.c: DVC plugin to forward queries from RDP to the ssh-agent
25
 *
26
 * This relays data to and from an ssh-agent program equivalent running on the
27
 * RDP server to an ssh-agent running locally.  Unlike the normal ssh-agent,
28
 * which sends data over an SSH channel, the data is send over an RDP dynamic
29
 * virtual channel.
30
 *
31
 * protocol specification:
32
 *     Forward data verbatim over RDP dynamic virtual channel named "sshagent"
33
 *     between a ssh client on the xrdp server and the real ssh-agent where
34
 *     the RDP client is running.  Each connection by a separate client to
35
 *     xrdp-ssh-agent gets a separate DVC invocation.
36
 */
37
38
#include <freerdp/config.h>
39
40
#include <stdio.h>
41
#include <stdlib.h>
42
#include <sys/types.h>
43
#include <sys/socket.h>
44
#include <sys/un.h>
45
#include <pwd.h>
46
#include <unistd.h>
47
#include <errno.h>
48
49
#include <winpr/crt.h>
50
#include <winpr/assert.h>
51
#include <winpr/synch.h>
52
#include <winpr/thread.h>
53
#include <winpr/stream.h>
54
55
#include "sshagent_main.h"
56
57
#include <freerdp/freerdp.h>
58
#include <freerdp/client/channels.h>
59
#include <freerdp/channels/log.h>
60
61
#define TAG CHANNELS_TAG("sshagent.client")
62
63
typedef struct
64
{
65
  IWTSListenerCallback iface;
66
67
  IWTSPlugin* plugin;
68
  IWTSVirtualChannelManager* channel_mgr;
69
70
  rdpContext* rdpcontext;
71
  const char* agent_uds_path;
72
} SSHAGENT_LISTENER_CALLBACK;
73
74
typedef struct
75
{
76
  GENERIC_CHANNEL_CALLBACK generic;
77
78
  rdpContext* rdpcontext;
79
  int agent_fd;
80
  HANDLE thread;
81
  CRITICAL_SECTION lock;
82
} SSHAGENT_CHANNEL_CALLBACK;
83
84
typedef struct
85
{
86
  IWTSPlugin iface;
87
88
  SSHAGENT_LISTENER_CALLBACK* listener_callback;
89
90
  rdpContext* rdpcontext;
91
  BOOL connected;
92
} SSHAGENT_PLUGIN;
93
94
/**
95
 * Function to open the connection to the sshagent
96
 *
97
 * @return The fd on success, otherwise -1
98
 */
99
static int connect_to_sshagent(const char* udspath)
100
0
{
101
0
  WINPR_ASSERT(udspath);
102
103
0
  int agent_fd = socket(AF_UNIX, SOCK_STREAM, 0);
104
105
0
  if (agent_fd == -1)
106
0
  {
107
0
    WLog_ERR(TAG, "Can't open Unix domain socket!");
108
0
    return -1;
109
0
  }
110
111
0
  struct sockaddr_un addr = WINPR_C_ARRAY_INIT;
112
113
0
  addr.sun_family = AF_UNIX;
114
115
0
  strncpy(addr.sun_path, udspath, sizeof(addr.sun_path) - 1);
116
117
0
  int rc = connect(agent_fd, (struct sockaddr*)&addr, sizeof(addr));
118
119
0
  if (rc != 0)
120
0
  {
121
0
    WLog_ERR(TAG, "Can't connect to Unix domain socket \"%s\"!", udspath);
122
0
    close(agent_fd);
123
0
    return -1;
124
0
  }
125
126
0
  return agent_fd;
127
0
}
128
129
/**
130
 * Entry point for thread to read from the ssh-agent socket and forward
131
 * the data to RDP
132
 *
133
 * @return 0
134
 */
135
static DWORD WINAPI sshagent_read_thread(LPVOID data)
136
0
{
137
0
  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)data;
138
0
  WINPR_ASSERT(callback);
139
140
0
  BYTE buffer[4096] = WINPR_C_ARRAY_INIT;
141
0
  int going = 1;
142
0
  UINT status = CHANNEL_RC_OK;
143
144
0
  while (going)
145
0
  {
146
0
    const ssize_t bytes_read = read(callback->agent_fd, buffer, sizeof(buffer));
147
148
0
    if (bytes_read == 0)
149
0
    {
150
      /* Socket closed cleanly at other end */
151
0
      going = 0;
152
0
    }
153
0
    else if (bytes_read < 0)
154
0
    {
155
0
      if (errno != EINTR)
156
0
      {
157
0
        WLog_ERR(TAG, "Error reading from sshagent, errno=%d", errno);
158
0
        status = ERROR_READ_FAULT;
159
0
        going = 0;
160
0
      }
161
0
    }
162
0
    else if ((size_t)bytes_read > ULONG_MAX)
163
0
    {
164
0
      status = ERROR_READ_FAULT;
165
0
      going = 0;
166
0
    }
167
0
    else
168
0
    {
169
      /* Something read: forward to virtual channel */
170
0
      IWTSVirtualChannel* channel = callback->generic.channel;
171
0
      status = channel->Write(channel, (ULONG)bytes_read, buffer, nullptr);
172
173
0
      if (status != CHANNEL_RC_OK)
174
0
      {
175
0
        going = 0;
176
0
      }
177
0
    }
178
0
  }
179
180
0
  close(callback->agent_fd);
181
182
0
  if (status != CHANNEL_RC_OK)
183
0
    setChannelError(callback->rdpcontext, status, "sshagent_read_thread reported an error");
184
185
0
  ExitThread(status);
186
0
  return status;
187
0
}
188
189
/**
190
 * Callback for data received from the RDP server; forward this to ssh-agent
191
 *
192
 * @return 0 on success, otherwise a Win32 error code
193
 */
194
static UINT sshagent_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data)
195
0
{
196
0
  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
197
0
  WINPR_ASSERT(callback);
198
199
0
  BYTE* pBuffer = Stream_Pointer(data);
200
0
  size_t cbSize = Stream_GetRemainingLength(data);
201
0
  BYTE* pos = pBuffer;
202
  /* Forward what we have received to the ssh agent */
203
0
  size_t bytes_to_write = cbSize;
204
0
  errno = 0;
205
206
0
  while (bytes_to_write > 0)
207
0
  {
208
0
    const ssize_t bytes_written = write(callback->agent_fd, pos, bytes_to_write);
209
210
0
    if (bytes_written < 0)
211
0
    {
212
0
      if (errno != EINTR)
213
0
      {
214
0
        WLog_ERR(TAG, "Error writing to sshagent, errno=%d", errno);
215
0
        return ERROR_WRITE_FAULT;
216
0
      }
217
0
    }
218
0
    else
219
0
    {
220
0
      bytes_to_write -= WINPR_ASSERTING_INT_CAST(size_t, bytes_written);
221
0
      pos += bytes_written;
222
0
    }
223
0
  }
224
225
  /* Consume stream */
226
0
  Stream_Seek(data, cbSize);
227
0
  return CHANNEL_RC_OK;
228
0
}
229
230
/**
231
 * Callback for when the virtual channel is closed
232
 *
233
 * @return 0 on success, otherwise a Win32 error code
234
 */
235
static UINT sshagent_on_close(IWTSVirtualChannelCallback* pChannelCallback)
236
0
{
237
0
  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
238
0
  WINPR_ASSERT(callback);
239
240
  /* Call shutdown() to wake up the read() in sshagent_read_thread(). */
241
0
  shutdown(callback->agent_fd, SHUT_RDWR);
242
0
  EnterCriticalSection(&callback->lock);
243
244
0
  if (WaitForSingleObject(callback->thread, INFINITE) == WAIT_FAILED)
245
0
  {
246
0
    UINT error = GetLastError();
247
0
    WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error);
248
0
    return error;
249
0
  }
250
251
0
  (void)CloseHandle(callback->thread);
252
0
  LeaveCriticalSection(&callback->lock);
253
0
  DeleteCriticalSection(&callback->lock);
254
255
0
  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)callback->generic.plugin;
256
0
  WINPR_ASSERT(sshagent);
257
0
  sshagent->connected = FALSE;
258
0
  free(callback);
259
0
  return CHANNEL_RC_OK;
260
0
}
261
262
/**
263
 * Callback for when a new virtual channel is opened
264
 *
265
 * @return 0 on success, otherwise a Win32 error code
266
 */
267
// NOLINTBEGIN(readability-non-const-parameter)
268
static UINT sshagent_on_new_channel_connection(IWTSListenerCallback* pListenerCallback,
269
                                               IWTSVirtualChannel* pChannel, BYTE* Data,
270
                                               BOOL* pbAccept,
271
                                               IWTSVirtualChannelCallback** ppCallback)
272
// NOLINTEND(readability-non-const-parameter)
273
0
{
274
0
  SSHAGENT_LISTENER_CALLBACK* listener_callback = (SSHAGENT_LISTENER_CALLBACK*)pListenerCallback;
275
0
  WINPR_UNUSED(Data);
276
0
  WINPR_UNUSED(pbAccept);
277
278
0
  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)listener_callback->plugin;
279
0
  WINPR_ASSERT(sshagent);
280
281
0
  if (sshagent->connected)
282
0
  {
283
0
    WLog_ERR(TAG, "Channel already connected, terminating.");
284
0
    return ERROR_DEVICE_ALREADY_ATTACHED;
285
0
  }
286
287
0
  SSHAGENT_CHANNEL_CALLBACK* callback =
288
0
      (SSHAGENT_CHANNEL_CALLBACK*)calloc(1, sizeof(SSHAGENT_CHANNEL_CALLBACK));
289
290
0
  if (!callback)
291
0
  {
292
0
    WLog_ERR(TAG, "calloc failed!");
293
0
    return CHANNEL_RC_NO_MEMORY;
294
0
  }
295
296
  /* Now open a connection to the local ssh-agent.  Do this for each
297
   * connection to the plugin in case we mess up the agent session. */
298
0
  callback->agent_fd = connect_to_sshagent(listener_callback->agent_uds_path);
299
300
0
  if (callback->agent_fd == -1)
301
0
  {
302
0
    free(callback);
303
0
    return CHANNEL_RC_INITIALIZATION_ERROR;
304
0
  }
305
306
0
  InitializeCriticalSection(&callback->lock);
307
308
0
  GENERIC_CHANNEL_CALLBACK* generic = &callback->generic;
309
0
  generic->iface.OnDataReceived = sshagent_on_data_received;
310
0
  generic->iface.OnClose = sshagent_on_close;
311
0
  generic->plugin = listener_callback->plugin;
312
0
  generic->channel_mgr = listener_callback->channel_mgr;
313
0
  generic->channel = pChannel;
314
0
  callback->rdpcontext = listener_callback->rdpcontext;
315
0
  callback->thread = CreateThread(nullptr, 0, sshagent_read_thread, (void*)callback, 0, nullptr);
316
317
0
  if (!callback->thread)
318
0
  {
319
0
    WLog_ERR(TAG, "CreateThread failed!");
320
0
    DeleteCriticalSection(&callback->lock);
321
0
    free(callback);
322
0
    return CHANNEL_RC_INITIALIZATION_ERROR;
323
0
  }
324
325
0
  *ppCallback = &callback->generic.iface;
326
0
  sshagent->connected = TRUE;
327
0
  return CHANNEL_RC_OK;
328
0
}
329
330
/**
331
 * Callback for when the plugin is initialised
332
 *
333
 * @return 0 on success, otherwise a Win32 error code
334
 */
335
static UINT sshagent_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr)
336
0
{
337
0
  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
338
0
  WINPR_ASSERT(sshagent);
339
0
  WINPR_ASSERT(pChannelMgr);
340
341
0
  sshagent->listener_callback =
342
0
      (SSHAGENT_LISTENER_CALLBACK*)calloc(1, sizeof(SSHAGENT_LISTENER_CALLBACK));
343
344
0
  if (!sshagent->listener_callback)
345
0
  {
346
0
    WLog_ERR(TAG, "calloc failed!");
347
0
    return CHANNEL_RC_NO_MEMORY;
348
0
  }
349
350
0
  sshagent->listener_callback->rdpcontext = sshagent->rdpcontext;
351
0
  sshagent->listener_callback->iface.OnNewChannelConnection = sshagent_on_new_channel_connection;
352
0
  sshagent->listener_callback->plugin = pPlugin;
353
0
  sshagent->listener_callback->channel_mgr = pChannelMgr;
354
  // NOLINTNEXTLINE(concurrency-mt-unsafe)
355
0
  sshagent->listener_callback->agent_uds_path = getenv("SSH_AUTH_SOCK");
356
357
0
  if (sshagent->listener_callback->agent_uds_path == nullptr)
358
0
  {
359
0
    WLog_ERR(TAG, "Environment variable $SSH_AUTH_SOCK undefined!");
360
0
    free(sshagent->listener_callback);
361
0
    sshagent->listener_callback = nullptr;
362
0
    return CHANNEL_RC_INITIALIZATION_ERROR;
363
0
  }
364
365
0
  return pChannelMgr->CreateListener(pChannelMgr, "SSHAGENT", 0,
366
0
                                     (IWTSListenerCallback*)sshagent->listener_callback, nullptr);
367
0
}
368
369
/**
370
 * Callback for when the plugin is terminated
371
 *
372
 * @return 0 on success, otherwise a Win32 error code
373
 */
374
static UINT sshagent_plugin_terminated(IWTSPlugin* pPlugin)
375
0
{
376
0
  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
377
0
  free(sshagent);
378
0
  return CHANNEL_RC_OK;
379
0
}
380
381
/**
382
 * Main entry point for sshagent DVC plugin
383
 *
384
 * @return 0 on success, otherwise a Win32 error code
385
 */
386
FREERDP_ENTRY_POINT(UINT VCAPITYPE sshagent_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints))
387
0
{
388
0
  UINT status = CHANNEL_RC_OK;
389
390
0
  WINPR_ASSERT(pEntryPoints);
391
392
0
  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "sshagent");
393
394
0
  if (!sshagent)
395
0
  {
396
0
    sshagent = (SSHAGENT_PLUGIN*)calloc(1, sizeof(SSHAGENT_PLUGIN));
397
398
0
    if (!sshagent)
399
0
    {
400
0
      WLog_ERR(TAG, "calloc failed!");
401
0
      return CHANNEL_RC_NO_MEMORY;
402
0
    }
403
404
0
    sshagent->iface.Initialize = sshagent_plugin_initialize;
405
0
    sshagent->iface.Connected = nullptr;
406
0
    sshagent->iface.Disconnected = nullptr;
407
0
    sshagent->iface.Terminated = sshagent_plugin_terminated;
408
0
    sshagent->rdpcontext = pEntryPoints->GetRdpContext(pEntryPoints);
409
0
    status = pEntryPoints->RegisterPlugin(pEntryPoints, "sshagent", &sshagent->iface);
410
0
  }
411
412
0
  return status;
413
0
}