FreeRDP
sshagent_main.c
1 
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 {
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 } SSHAGENT_PLUGIN;
92 
98 static int connect_to_sshagent(const char* udspath)
99 {
100  WINPR_ASSERT(udspath);
101 
102  int agent_fd = socket(AF_UNIX, SOCK_STREAM, 0);
103 
104  if (agent_fd == -1)
105  {
106  WLog_ERR(TAG, "Can't open Unix domain socket!");
107  return -1;
108  }
109 
110  struct sockaddr_un addr = { 0 };
111 
112  addr.sun_family = AF_UNIX;
113 
114  strncpy(addr.sun_path, udspath, sizeof(addr.sun_path) - 1);
115 
116  int rc = connect(agent_fd, (struct sockaddr*)&addr, sizeof(addr));
117 
118  if (rc != 0)
119  {
120  WLog_ERR(TAG, "Can't connect to Unix domain socket \"%s\"!", udspath);
121  close(agent_fd);
122  return -1;
123  }
124 
125  return agent_fd;
126 }
127 
134 static DWORD WINAPI sshagent_read_thread(LPVOID data)
135 {
136  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)data;
137  WINPR_ASSERT(callback);
138 
139  BYTE buffer[4096] = { 0 };
140  int going = 1;
141  UINT status = CHANNEL_RC_OK;
142 
143  while (going)
144  {
145  const ssize_t bytes_read = read(callback->agent_fd, buffer, sizeof(buffer));
146 
147  if (bytes_read == 0)
148  {
149  /* Socket closed cleanly at other end */
150  going = 0;
151  }
152  else if (bytes_read < 0)
153  {
154  if (errno != EINTR)
155  {
156  WLog_ERR(TAG, "Error reading from sshagent, errno=%d", errno);
157  status = ERROR_READ_FAULT;
158  going = 0;
159  }
160  }
161  else if ((size_t)bytes_read > ULONG_MAX)
162  {
163  status = ERROR_READ_FAULT;
164  going = 0;
165  }
166  else
167  {
168  /* Something read: forward to virtual channel */
169  IWTSVirtualChannel* channel = callback->generic.channel;
170  status = channel->Write(channel, (ULONG)bytes_read, buffer, NULL);
171 
172  if (status != CHANNEL_RC_OK)
173  {
174  going = 0;
175  }
176  }
177  }
178 
179  close(callback->agent_fd);
180 
181  if (status != CHANNEL_RC_OK)
182  setChannelError(callback->rdpcontext, status, "sshagent_read_thread reported an error");
183 
184  ExitThread(status);
185  return status;
186 }
187 
193 static UINT sshagent_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data)
194 {
195  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
196  WINPR_ASSERT(callback);
197 
198  BYTE* pBuffer = Stream_Pointer(data);
199  size_t cbSize = Stream_GetRemainingLength(data);
200  BYTE* pos = pBuffer;
201  /* Forward what we have received to the ssh agent */
202  size_t bytes_to_write = cbSize;
203  errno = 0;
204 
205  while (bytes_to_write > 0)
206  {
207  const ssize_t bytes_written = write(callback->agent_fd, pos, bytes_to_write);
208 
209  if (bytes_written < 0)
210  {
211  if (errno != EINTR)
212  {
213  WLog_ERR(TAG, "Error writing to sshagent, errno=%d", errno);
214  return ERROR_WRITE_FAULT;
215  }
216  }
217  else
218  {
219  bytes_to_write -= WINPR_ASSERTING_INT_CAST(size_t, bytes_written);
220  pos += bytes_written;
221  }
222  }
223 
224  /* Consume stream */
225  Stream_Seek(data, cbSize);
226  return CHANNEL_RC_OK;
227 }
228 
234 static UINT sshagent_on_close(IWTSVirtualChannelCallback* pChannelCallback)
235 {
236  SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
237  WINPR_ASSERT(callback);
238 
239  /* Call shutdown() to wake up the read() in sshagent_read_thread(). */
240  shutdown(callback->agent_fd, SHUT_RDWR);
241  EnterCriticalSection(&callback->lock);
242 
243  if (WaitForSingleObject(callback->thread, INFINITE) == WAIT_FAILED)
244  {
245  UINT error = GetLastError();
246  WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error);
247  return error;
248  }
249 
250  (void)CloseHandle(callback->thread);
251  LeaveCriticalSection(&callback->lock);
252  DeleteCriticalSection(&callback->lock);
253  free(callback);
254  return CHANNEL_RC_OK;
255 }
256 
262 static UINT sshagent_on_new_channel_connection(IWTSListenerCallback* pListenerCallback,
263  IWTSVirtualChannel* pChannel, BYTE* Data,
264  BOOL* pbAccept,
265  IWTSVirtualChannelCallback** ppCallback)
266 {
267  SSHAGENT_LISTENER_CALLBACK* listener_callback = (SSHAGENT_LISTENER_CALLBACK*)pListenerCallback;
268  WINPR_UNUSED(Data);
269  WINPR_UNUSED(pbAccept);
270 
271  SSHAGENT_CHANNEL_CALLBACK* callback =
272  (SSHAGENT_CHANNEL_CALLBACK*)calloc(1, sizeof(SSHAGENT_CHANNEL_CALLBACK));
273 
274  if (!callback)
275  {
276  WLog_ERR(TAG, "calloc failed!");
277  return CHANNEL_RC_NO_MEMORY;
278  }
279 
280  /* Now open a connection to the local ssh-agent. Do this for each
281  * connection to the plugin in case we mess up the agent session. */
282  callback->agent_fd = connect_to_sshagent(listener_callback->agent_uds_path);
283 
284  if (callback->agent_fd == -1)
285  {
286  free(callback);
287  return CHANNEL_RC_INITIALIZATION_ERROR;
288  }
289 
290  InitializeCriticalSection(&callback->lock);
291 
292  GENERIC_CHANNEL_CALLBACK* generic = &callback->generic;
293  generic->iface.OnDataReceived = sshagent_on_data_received;
294  generic->iface.OnClose = sshagent_on_close;
295  generic->plugin = listener_callback->plugin;
296  generic->channel_mgr = listener_callback->channel_mgr;
297  generic->channel = pChannel;
298  callback->rdpcontext = listener_callback->rdpcontext;
299  callback->thread = CreateThread(NULL, 0, sshagent_read_thread, (void*)callback, 0, NULL);
300 
301  if (!callback->thread)
302  {
303  WLog_ERR(TAG, "CreateThread failed!");
304  DeleteCriticalSection(&callback->lock);
305  free(callback);
306  return CHANNEL_RC_INITIALIZATION_ERROR;
307  }
308 
309  *ppCallback = (IWTSVirtualChannelCallback*)callback;
310  return CHANNEL_RC_OK;
311 }
312 
318 static UINT sshagent_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr)
319 {
320  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
321  WINPR_ASSERT(sshagent);
322  WINPR_ASSERT(pChannelMgr);
323 
324  sshagent->listener_callback =
325  (SSHAGENT_LISTENER_CALLBACK*)calloc(1, sizeof(SSHAGENT_LISTENER_CALLBACK));
326 
327  if (!sshagent->listener_callback)
328  {
329  WLog_ERR(TAG, "calloc failed!");
330  return CHANNEL_RC_NO_MEMORY;
331  }
332 
333  sshagent->listener_callback->rdpcontext = sshagent->rdpcontext;
334  sshagent->listener_callback->iface.OnNewChannelConnection = sshagent_on_new_channel_connection;
335  sshagent->listener_callback->plugin = pPlugin;
336  sshagent->listener_callback->channel_mgr = pChannelMgr;
337  sshagent->listener_callback->agent_uds_path = getenv("SSH_AUTH_SOCK");
338 
339  if (sshagent->listener_callback->agent_uds_path == NULL)
340  {
341  WLog_ERR(TAG, "Environment variable $SSH_AUTH_SOCK undefined!");
342  free(sshagent->listener_callback);
343  sshagent->listener_callback = NULL;
344  return CHANNEL_RC_INITIALIZATION_ERROR;
345  }
346 
347  return pChannelMgr->CreateListener(pChannelMgr, "SSHAGENT", 0,
348  (IWTSListenerCallback*)sshagent->listener_callback, NULL);
349 }
350 
356 static UINT sshagent_plugin_terminated(IWTSPlugin* pPlugin)
357 {
358  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
359  free(sshagent);
360  return CHANNEL_RC_OK;
361 }
362 
368 FREERDP_ENTRY_POINT(UINT VCAPITYPE sshagent_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints))
369 {
370  UINT status = CHANNEL_RC_OK;
371 
372  WINPR_ASSERT(pEntryPoints);
373 
374  SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "sshagent");
375 
376  if (!sshagent)
377  {
378  sshagent = (SSHAGENT_PLUGIN*)calloc(1, sizeof(SSHAGENT_PLUGIN));
379 
380  if (!sshagent)
381  {
382  WLog_ERR(TAG, "calloc failed!");
383  return CHANNEL_RC_NO_MEMORY;
384  }
385 
386  sshagent->iface.Initialize = sshagent_plugin_initialize;
387  sshagent->iface.Connected = NULL;
388  sshagent->iface.Disconnected = NULL;
389  sshagent->iface.Terminated = sshagent_plugin_terminated;
390  sshagent->rdpcontext = pEntryPoints->GetRdpContext(pEntryPoints);
391  status = pEntryPoints->RegisterPlugin(pEntryPoints, "sshagent", &sshagent->iface);
392  }
393 
394  return status;
395 }