FreeRDP
Loading...
Searching...
No Matches
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
63typedef 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
74typedef struct
75{
77
78 rdpContext* rdpcontext;
79 int agent_fd;
80 HANDLE thread;
82} SSHAGENT_CHANNEL_CALLBACK;
83
84typedef struct
85{
86 IWTSPlugin iface;
87
88 SSHAGENT_LISTENER_CALLBACK* listener_callback;
89
90 rdpContext* rdpcontext;
91 BOOL connected;
92} SSHAGENT_PLUGIN;
93
99static int connect_to_sshagent(const char* udspath)
100{
101 WINPR_ASSERT(udspath);
102
103 int agent_fd = socket(AF_UNIX, SOCK_STREAM, 0);
104
105 if (agent_fd == -1)
106 {
107 WLog_ERR(TAG, "Can't open Unix domain socket!");
108 return -1;
109 }
110
111 struct sockaddr_un addr = WINPR_C_ARRAY_INIT;
112
113 addr.sun_family = AF_UNIX;
114
115 strncpy(addr.sun_path, udspath, sizeof(addr.sun_path) - 1);
116
117 int rc = connect(agent_fd, (struct sockaddr*)&addr, sizeof(addr));
118
119 if (rc != 0)
120 {
121 WLog_ERR(TAG, "Can't connect to Unix domain socket \"%s\"!", udspath);
122 close(agent_fd);
123 return -1;
124 }
125
126 return agent_fd;
127}
128
135static DWORD WINAPI sshagent_read_thread(LPVOID data)
136{
137 SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)data;
138 WINPR_ASSERT(callback);
139
140 BYTE buffer[4096] = WINPR_C_ARRAY_INIT;
141 int going = 1;
142 UINT status = CHANNEL_RC_OK;
143
144 while (going)
145 {
146 const ssize_t bytes_read = read(callback->agent_fd, buffer, sizeof(buffer));
147
148 if (bytes_read == 0)
149 {
150 /* Socket closed cleanly at other end */
151 going = 0;
152 }
153 else if (bytes_read < 0)
154 {
155 if (errno != EINTR)
156 {
157 WLog_ERR(TAG, "Error reading from sshagent, errno=%d", errno);
158 status = ERROR_READ_FAULT;
159 going = 0;
160 }
161 }
162 else if ((size_t)bytes_read > ULONG_MAX)
163 {
164 status = ERROR_READ_FAULT;
165 going = 0;
166 }
167 else
168 {
169 /* Something read: forward to virtual channel */
170 IWTSVirtualChannel* channel = callback->generic.channel;
171 status = channel->Write(channel, (ULONG)bytes_read, buffer, nullptr);
172
173 if (status != CHANNEL_RC_OK)
174 {
175 going = 0;
176 }
177 }
178 }
179
180 close(callback->agent_fd);
181
182 if (status != CHANNEL_RC_OK)
183 setChannelError(callback->rdpcontext, status, "sshagent_read_thread reported an error");
184
185 ExitThread(status);
186 return status;
187}
188
194static UINT sshagent_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data)
195{
196 SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
197 WINPR_ASSERT(callback);
198
199 BYTE* pBuffer = Stream_Pointer(data);
200 size_t cbSize = Stream_GetRemainingLength(data);
201 BYTE* pos = pBuffer;
202 /* Forward what we have received to the ssh agent */
203 size_t bytes_to_write = cbSize;
204 errno = 0;
205
206 while (bytes_to_write > 0)
207 {
208 const ssize_t bytes_written = write(callback->agent_fd, pos, bytes_to_write);
209
210 if (bytes_written < 0)
211 {
212 if (errno != EINTR)
213 {
214 WLog_ERR(TAG, "Error writing to sshagent, errno=%d", errno);
215 return ERROR_WRITE_FAULT;
216 }
217 }
218 else
219 {
220 bytes_to_write -= WINPR_ASSERTING_INT_CAST(size_t, bytes_written);
221 pos += bytes_written;
222 }
223 }
224
225 /* Consume stream */
226 Stream_Seek(data, cbSize);
227 return CHANNEL_RC_OK;
228}
229
235static UINT sshagent_on_close(IWTSVirtualChannelCallback* pChannelCallback)
236{
237 SSHAGENT_CHANNEL_CALLBACK* callback = (SSHAGENT_CHANNEL_CALLBACK*)pChannelCallback;
238 WINPR_ASSERT(callback);
239
240 /* Call shutdown() to wake up the read() in sshagent_read_thread(). */
241 shutdown(callback->agent_fd, SHUT_RDWR);
242 EnterCriticalSection(&callback->lock);
243
244 if (WaitForSingleObject(callback->thread, INFINITE) == WAIT_FAILED)
245 {
246 UINT error = GetLastError();
247 WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", error);
248 return error;
249 }
250
251 (void)CloseHandle(callback->thread);
252 LeaveCriticalSection(&callback->lock);
253 DeleteCriticalSection(&callback->lock);
254
255 SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)callback->generic.plugin;
256 WINPR_ASSERT(sshagent);
257 sshagent->connected = FALSE;
258 free(callback);
259 return CHANNEL_RC_OK;
260}
261
267// NOLINTBEGIN(readability-non-const-parameter)
268static 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{
274 SSHAGENT_LISTENER_CALLBACK* listener_callback = (SSHAGENT_LISTENER_CALLBACK*)pListenerCallback;
275 WINPR_UNUSED(Data);
276 WINPR_UNUSED(pbAccept);
277
278 SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)listener_callback->plugin;
279 WINPR_ASSERT(sshagent);
280
281 if (sshagent->connected)
282 {
283 WLog_ERR(TAG, "Channel already connected, terminating.");
284 return ERROR_DEVICE_ALREADY_ATTACHED;
285 }
286
287 SSHAGENT_CHANNEL_CALLBACK* callback =
288 (SSHAGENT_CHANNEL_CALLBACK*)calloc(1, sizeof(SSHAGENT_CHANNEL_CALLBACK));
289
290 if (!callback)
291 {
292 WLog_ERR(TAG, "calloc failed!");
293 return CHANNEL_RC_NO_MEMORY;
294 }
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 callback->agent_fd = connect_to_sshagent(listener_callback->agent_uds_path);
299
300 if (callback->agent_fd == -1)
301 {
302 free(callback);
303 return CHANNEL_RC_INITIALIZATION_ERROR;
304 }
305
306 InitializeCriticalSection(&callback->lock);
307
308 GENERIC_CHANNEL_CALLBACK* generic = &callback->generic;
309 generic->iface.OnDataReceived = sshagent_on_data_received;
310 generic->iface.OnClose = sshagent_on_close;
311 generic->plugin = listener_callback->plugin;
312 generic->channel_mgr = listener_callback->channel_mgr;
313 generic->channel = pChannel;
314 callback->rdpcontext = listener_callback->rdpcontext;
315 callback->thread = CreateThread(nullptr, 0, sshagent_read_thread, (void*)callback, 0, nullptr);
316
317 if (!callback->thread)
318 {
319 WLog_ERR(TAG, "CreateThread failed!");
320 DeleteCriticalSection(&callback->lock);
321 free(callback);
322 return CHANNEL_RC_INITIALIZATION_ERROR;
323 }
324
325 *ppCallback = &callback->generic.iface;
326 sshagent->connected = TRUE;
327 return CHANNEL_RC_OK;
328}
329
335static UINT sshagent_plugin_initialize(IWTSPlugin* pPlugin, IWTSVirtualChannelManager* pChannelMgr)
336{
337 SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
338 WINPR_ASSERT(sshagent);
339 WINPR_ASSERT(pChannelMgr);
340
341 sshagent->listener_callback =
342 (SSHAGENT_LISTENER_CALLBACK*)calloc(1, sizeof(SSHAGENT_LISTENER_CALLBACK));
343
344 if (!sshagent->listener_callback)
345 {
346 WLog_ERR(TAG, "calloc failed!");
347 return CHANNEL_RC_NO_MEMORY;
348 }
349
350 sshagent->listener_callback->rdpcontext = sshagent->rdpcontext;
351 sshagent->listener_callback->iface.OnNewChannelConnection = sshagent_on_new_channel_connection;
352 sshagent->listener_callback->plugin = pPlugin;
353 sshagent->listener_callback->channel_mgr = pChannelMgr;
354 // NOLINTNEXTLINE(concurrency-mt-unsafe)
355 sshagent->listener_callback->agent_uds_path = getenv("SSH_AUTH_SOCK");
356
357 if (sshagent->listener_callback->agent_uds_path == nullptr)
358 {
359 WLog_ERR(TAG, "Environment variable $SSH_AUTH_SOCK undefined!");
360 free(sshagent->listener_callback);
361 sshagent->listener_callback = nullptr;
362 return CHANNEL_RC_INITIALIZATION_ERROR;
363 }
364
365 return pChannelMgr->CreateListener(pChannelMgr, "SSHAGENT", 0,
366 (IWTSListenerCallback*)sshagent->listener_callback, nullptr);
367}
368
374static UINT sshagent_plugin_terminated(IWTSPlugin* pPlugin)
375{
376 SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pPlugin;
377 free(sshagent);
378 return CHANNEL_RC_OK;
379}
380
386FREERDP_ENTRY_POINT(UINT VCAPITYPE sshagent_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints))
387{
388 UINT status = CHANNEL_RC_OK;
389
390 WINPR_ASSERT(pEntryPoints);
391
392 SSHAGENT_PLUGIN* sshagent = (SSHAGENT_PLUGIN*)pEntryPoints->GetPlugin(pEntryPoints, "sshagent");
393
394 if (!sshagent)
395 {
396 sshagent = (SSHAGENT_PLUGIN*)calloc(1, sizeof(SSHAGENT_PLUGIN));
397
398 if (!sshagent)
399 {
400 WLog_ERR(TAG, "calloc failed!");
401 return CHANNEL_RC_NO_MEMORY;
402 }
403
404 sshagent->iface.Initialize = sshagent_plugin_initialize;
405 sshagent->iface.Connected = nullptr;
406 sshagent->iface.Disconnected = nullptr;
407 sshagent->iface.Terminated = sshagent_plugin_terminated;
408 sshagent->rdpcontext = pEntryPoints->GetRdpContext(pEntryPoints);
409 status = pEntryPoints->RegisterPlugin(pEntryPoints, "sshagent", &sshagent->iface);
410 }
411
412 return status;
413}