/** * @example sshtunnel.c * An example of an RFB client tunneled through SSH by using libssh2. * This is based on https://www.libssh2.org/examples/direct_tcpip.html * with the following changes: * - the listening is split out into a separate thread function * - the listener gets closed immediately once a connection was accepted * - the listening port is chosen by the OS, SO_REUSEADDR removed * - global variables moved into SshData helper structure * - added name resolution for the ssh host */ #include #include #include #include #include #include #ifdef LIBVNCSERVER_HAVE_SYS_TYPES_H #include #endif #ifdef LIBVNCSERVER_HAVE_SYS_SOCKET_H #include #endif #ifdef LIBVNCSERVER_HAVE_UNISTD_H #include #endif /* The one global bool that's global so we can set it via a signal handler... */ int maintain_connection = 1; typedef struct { rfbClient *client; LIBSSH2_SESSION *session; #ifdef LIBVNCSERVER_HAVE_LIBPTHREAD pthread_t thread; #elif defined(LIBVNCSERVER_HAVE_WIN32THREADS) uintptr_t thread; #endif int ssh_sock; int local_listensock; int local_listenport; const char *remote_desthost; int remote_destport; } SshData; THREAD_ROUTINE_RETURN_TYPE ssh_proxy_loop(void *arg) { SshData *data = arg; int rc, i; struct sockaddr_in sin; socklen_t sinlen; LIBSSH2_CHANNEL *channel = NULL; const char *shost; int sport; fd_set fds; struct timeval tv; ssize_t len, wr; char buf[16384]; int proxy_sock = RFB_INVALID_SOCKET; proxy_sock = accept(data->local_listensock, (struct sockaddr *)&sin, &sinlen); if(proxy_sock == RFB_INVALID_SOCKET) { fprintf(stderr, "ssh_proxy_loop: accept: %s\n", strerror(errno)); goto shutdown; } /* Close listener once a connection got accepted */ rfbCloseSocket(data->local_listensock); shost = inet_ntoa(sin.sin_addr); sport = ntohs(sin.sin_port); printf("ssh_proxy_loop: forwarding connection from %s:%d here to remote %s:%d\n", shost, sport, data->remote_desthost, data->remote_destport); channel = libssh2_channel_direct_tcpip_ex(data->session, data->remote_desthost, data->remote_destport, shost, sport); if(!channel) { fprintf(stderr, "ssh_proxy_loop: Could not open the direct-tcpip channel!\n" "(Note that this can be a problem at the server!" " Please review the server logs.)\n"); goto shutdown; } /* Must use non-blocking IO hereafter due to the current libssh2 API */ libssh2_session_set_blocking(data->session, 0); while(1) { FD_ZERO(&fds); FD_SET(proxy_sock, &fds); tv.tv_sec = 0; tv.tv_usec = 100000; rc = select(proxy_sock + 1, &fds, NULL, NULL, &tv); if(-1 == rc) { fprintf(stderr, "ssh_proxy_loop: select: %s\n", strerror(errno)); goto shutdown; } if(rc && FD_ISSET(proxy_sock, &fds)) { len = recv(proxy_sock, buf, sizeof(buf), 0); if(len < 0) { fprintf(stderr, "read: %s\n", strerror(errno)); goto shutdown; } else if(0 == len) { fprintf(stderr, "ssh_proxy_loop: the client at %s:%d disconnected!\n", shost, sport); goto shutdown; } wr = 0; while(wr < len) { i = libssh2_channel_write(channel, buf + wr, len - wr); if(LIBSSH2_ERROR_EAGAIN == i) { continue; } if(i < 0) { fprintf(stderr, "ssh_proxy_loop: libssh2_channel_write: %d\n", i); goto shutdown; } wr += i; } } while(1) { len = libssh2_channel_read(channel, buf, sizeof(buf)); if(LIBSSH2_ERROR_EAGAIN == len) break; else if(len < 0) { fprintf(stderr, "ssh_proxy_loop: libssh2_channel_read: %d\n", (int)len); goto shutdown; } wr = 0; while(wr < len) { i = send(proxy_sock, buf + wr, len - wr, 0); if(i <= 0) { fprintf(stderr, "ssh_proxy_loop: write: %s\n", strerror(errno)); goto shutdown; } wr += i; } if(libssh2_channel_eof(channel)) { fprintf(stderr, "ssh_proxy_loop: the server at %s:%d disconnected!\n", data->remote_desthost, data->remote_destport); goto shutdown; } } } shutdown: printf("ssh_proxy_loop: shutting down\n"); rfbCloseSocket(proxy_sock); if(channel) libssh2_channel_free(channel); libssh2_session_disconnect(data->session, "Client disconnecting normally"); libssh2_session_free(data->session); rfbCloseSocket(data->ssh_sock); return THREAD_ROUTINE_RETURN_VALUE; } /** Decide whether or not the SSH tunnel setup should continue based on the current host and its fingerprint. Business logic is up to the implementer in a real app, i.e. compare keys, ask user etc... @return -1 if tunnel setup should be aborted 0 if tunnel setup should continue */ int ssh_fingerprint_check(const char *fingerprint, size_t fingerprint_len, const char *host, rfbClient *client) { size_t i; fprintf(stderr, "ssh_fingerprint_check: host %s has ", host); for(i = 0; i < fingerprint_len; i++) printf("%02X ", (unsigned char)fingerprint[i]); printf("\n"); return 0; } /** Creates an SSH tunnel and a local proxy and returns the port the proxy is listening on. @return A pointer to an SshData structure or NULL on error. */ SshData* ssh_tunnel_open(const char *ssh_host, const char *ssh_user, const char *ssh_password, const char *ssh_pub_key_path, const char *ssh_priv_key_path, const char *ssh_priv_key_password, const char *rfb_host, int rfb_port, rfbClient *client) { int rc, i; struct sockaddr_in sin; socklen_t sinlen; const char *fingerprint; char *userauthlist; struct addrinfo hints, *res; SshData *data; /* Sanity checks */ if(!ssh_host || !ssh_user || !rfb_host) /* these must be set */ return NULL; data = calloc(1, sizeof(SshData)); data->client = client; data->remote_desthost = rfb_host; /* resolved by the server */ data->remote_destport = rfb_port; /* Connect to SSH server */ data->ssh_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if(data->ssh_sock == RFB_INVALID_SOCKET) { fprintf(stderr, "ssh_tunnel_open: socket: %s\n", strerror(errno)); goto error; } memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; if ((rc = getaddrinfo(ssh_host, NULL, &hints, &res)) == 0) { sin.sin_family = AF_INET; sin.sin_addr.s_addr = (((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr); freeaddrinfo(res); } else { fprintf(stderr, "ssh_tunnel_open: getaddrinfo: %s\n", gai_strerror(rc)); goto error; } sin.sin_port = htons(22); if(connect(data->ssh_sock, (struct sockaddr*)(&sin), sizeof(struct sockaddr_in)) != 0) { fprintf(stderr, "ssh_tunnel_open: failed to connect to SSH server!\n"); goto error; } /* Create a session instance */ data->session = libssh2_session_init(); if(!data->session) { fprintf(stderr, "ssh_tunnel_open: could not initialize SSH session!\n"); goto error; } /* ... start it up. This will trade welcome banners, exchange keys, * and setup crypto, compression, and MAC layers */ rc = libssh2_session_handshake(data->session, data->ssh_sock); if(rc) { fprintf(stderr, "ssh_tunnel_open: error when starting up SSH session: %d\n", rc); goto error; } /* At this point we havn't yet authenticated. The first thing to do * is check the hostkey's fingerprint against our known hosts Your app * may have it hard coded, may go to a file, may present it to the * user, that's your call */ fingerprint = libssh2_hostkey_hash(data->session, LIBSSH2_HOSTKEY_HASH_SHA256); if(ssh_fingerprint_check(fingerprint, 32, ssh_host, data->client) == -1) { fprintf(stderr, "ssh_tunnel_open: fingerprint check indicated tunnel setup stop\n"); goto error; } /* check what authentication methods are available */ userauthlist = libssh2_userauth_list(data->session, ssh_user, strlen(ssh_user)); printf("ssh_tunnel_open: authentication methods: %s\n", userauthlist); if(ssh_password && strstr(userauthlist, "password")) { if(libssh2_userauth_password(data->session, ssh_user, ssh_password)) { fprintf(stderr, "ssh_tunnel_open: authentication by password failed.\n"); goto error; } } else if(ssh_priv_key_path && ssh_priv_key_password && strstr(userauthlist, "publickey")) { if(libssh2_userauth_publickey_fromfile(data->session, ssh_user, ssh_pub_key_path, ssh_priv_key_path, ssh_priv_key_password)) { fprintf(stderr, "ssh_tunnel_open: authentication by public key failed!\n"); goto error; } } else { fprintf(stderr, "ssh_tunnel_open: no supported authentication methods found!\n"); goto error; } /* Create and bind the local listening socket */ data->local_listensock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if(data->local_listensock == RFB_INVALID_SOCKET) { fprintf(stderr, "ssh_tunnel_open: socket: %s\n", strerror(errno)); return NULL; } sin.sin_family = AF_INET; sin.sin_port = htons(0); /* let the OS choose the port */ sin.sin_addr.s_addr = inet_addr("127.0.0.1"); if(INADDR_NONE == sin.sin_addr.s_addr) { fprintf(stderr, "ssh_tunnel_open: inet_addr: %s\n", strerror(errno)); goto error; } sinlen = sizeof(sin); if(-1 == bind(data->local_listensock, (struct sockaddr *)&sin, sinlen)) { fprintf(stderr, "bind: %s\n", strerror(errno)); goto error; } if(-1 == listen(data->local_listensock, 1)) { fprintf(stderr, "listen: %s\n", strerror(errno)); goto error; } /* get info back from OS */ if (getsockname(data->local_listensock, (struct sockaddr *)&sin, &sinlen ) == -1){ fprintf(stderr, "ssh_tunnel_open: getsockname: %s\n", strerror(errno)); goto error; } data->local_listenport = ntohs(sin.sin_port); printf("ssh_tunnel_open: waiting for TCP connection on %s:%d...\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port)); /* Create the proxy thread */ #if defined(LIBVNCSERVER_HAVE_LIBPTHREAD) if (pthread_create(&data->thread, NULL, ssh_proxy_loop, data) != 0) { #elif defined(LIBVNCSERVER_HAVE_WIN32THREADS) if(data->thread = _beginthread(proxy_loop, 0, data) == 0); #endif fprintf(stderr, "ssh_tunnel_open: proxy thread creation failed\n"); goto error; } return data; error: if (data->session) { libssh2_session_disconnect(data->session, "Error in SSH tunnel setup"); libssh2_session_free(data->session); } rfbCloseSocket(data->local_listensock); rfbCloseSocket(data->ssh_sock); free(data); return NULL; } void ssh_tunnel_close(SshData *data) { if(!data) return; /* the proxy thread does the internal cleanup as it can be ended due to external reasons */ THREAD_JOIN(data->thread); free(data); printf("ssh_tunnel_close: done\n"); } void intHandler(int dummy) { maintain_connection = 0; } int main(int argc, char *argv[]) { rfbClient *client = rfbGetClient(8,3,4); /* Get args and create SSH tunnel */ int rc = libssh2_init(0); if(rc) { fprintf(stderr, "libssh2 initialization failed (%d)\n", rc); return EXIT_FAILURE; } SshData *data; if (argc == 6) { /* SSH tunnel w/ password */ data = ssh_tunnel_open(argv[1], argv[2], argv[3], NULL, NULL, NULL, argv[4], atoi(argv[5]), client); } else if (argc == 8) { /* SSH tunnel w/ privkey */ data = ssh_tunnel_open(argv[1], argv[2], NULL, argv[3], argv[4], argv[5], argv[6], atoi(argv[7]), client); } else { fprintf(stderr, "Usage (w/ password): %s \n" "Usage (w/ privkey): %s \n", argv[0], argv[0]); return(EXIT_FAILURE); } /* The actual VNC connection setup. */ client->serverHost = strdup("127.0.0.1"); if(data) // might be NULL if ssh setup failed client->serverPort = data->local_listenport; rfbClientSetClientData(client, (void*)42, data); if (!data || !rfbInitClient(client,NULL,NULL)) return EXIT_FAILURE; printf("Successfully connected to %s:%d - hit Ctrl-C to disconnect\n", client->serverHost, client->serverPort); signal(SIGINT, intHandler); while (maintain_connection) { int n = WaitForMessage(client,50); if(n < 0) break; if(n) if(!HandleRFBServerMessage(client)) break; } /* Disconnect client inside tunnel */ if(client && client->sock != RFB_INVALID_SOCKET) rfbCloseSocket(client->sock); /* Close the tunnel and clean up */ ssh_tunnel_close(rfbClientGetClientData(client, (void*)42)); /* free client */ rfbClientCleanup(client); /* Teardown libssh2 */ libssh2_exit(); return EXIT_SUCCESS; }