1
0
mirror of https://github.com/moparisthebest/sslh synced 2024-11-13 12:45:05 -05:00

Probes made resilient to packets that are too short, or

contain NULLs.
This commit is contained in:
Yves Rutschle 2013-09-28 21:39:00 +02:00
commit fb0760dd72
11 changed files with 183 additions and 123 deletions

View File

@ -1,4 +1,10 @@
vNEXT:
Probes made more resilient, to incoming data
containing NULLs. Also made them behave properly
when receiving too short packets to probe on the
first incoming packet.
(Ondrej Kuzník)
Fixed bugs related to getpeername that would cause
sslh to quit erroneously (getpeername can return
actual errors if connections are dropped before
@ -14,7 +20,7 @@ v1.15: 27JUL2013
would happen.
Fixed bug in sslh-select: if socket dropped while
defered_data was present, sslh-select would crash.
deferred_data was present, sslh-select would crash.
Increased FD_SETSIZE for Cygwin, as the default 64
is too low for even moderate load.

View File

@ -80,7 +80,7 @@ distclean: clean
rm -f tags cscope.*
clean:
rm -f sslh-fork sslh-select echosrv $(MAN) *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info
rm -f sslh-fork sslh-select echosrv version.h $(MAN) *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info
tags:
ctags --globals -T *.[ch]

View File

@ -8,6 +8,7 @@
#include <stdarg.h>
#include "common.h"
#include "probe.h"
/* Added to make the code compilable under CYGWIN
* */
@ -125,22 +126,22 @@ int bind_peer(int fd, int fd_from)
/* Connect to first address that works and returns a file descriptor, or -1 if
* none work.
* If transparent proxying is on, use fd_from peer address on external address
* of new file descriptor.
* cnx_name points to the name of the service (for logging) */
int connect_addr(struct addrinfo *addr, int fd_from, const char* cnx_name)
* of new file descriptor. */
int connect_addr(struct connection *cnx, int fd_from)
{
struct addrinfo *a;
char buf[NI_MAXHOST];
int fd, res;
for (a = addr; a; a = a->ai_next) {
for (a = cnx->proto->saddr; a; a = a->ai_next) {
if (verbose)
fprintf(stderr, "connecting to %s family %d len %d\n",
sprintaddr(buf, sizeof(buf), a),
a->ai_addr->sa_family, a->ai_addrlen);
fd = socket(a->ai_family, SOCK_STREAM, 0);
if (fd == -1) {
log_message(LOG_ERR, "forward to %s failed:socket: %s\n", cnx_name, strerror(errno));
log_message(LOG_ERR, "forward to %s failed:socket: %s\n",
cnx->proto->description, strerror(errno));
} else {
if (transparent) {
res = bind_peer(fd, fd_from);
@ -149,7 +150,7 @@ int connect_addr(struct addrinfo *addr, int fd_from, const char* cnx_name)
res = connect(fd, a->ai_addr, a->ai_addrlen);
if (res == -1) {
log_message(LOG_ERR, "forward to %s failed:connect: %s\n",
cnx_name, strerror(errno));
cnx->proto->description, strerror(errno));
} else {
return fd;
}
@ -161,12 +162,20 @@ int connect_addr(struct addrinfo *addr, int fd_from, const char* cnx_name)
/* Store some data to write to the queue later */
int defer_write(struct queue *q, void* data, int data_size)
{
char *p;
if (verbose)
fprintf(stderr, "**** writing defered on fd %d\n", q->fd);
q->defered_data = malloc(data_size);
q->begin_defered_data = q->defered_data;
q->defered_data_size = data_size;
memcpy(q->defered_data, data, data_size);
fprintf(stderr, "**** writing deferred on fd %d\n", q->fd);
p = realloc(q->begin_deferred_data, q->deferred_data_size + data_size);
if (!p) {
perror("realloc");
exit(1);
}
q->deferred_data = q->begin_deferred_data = p;
p += q->deferred_data_size;
q->deferred_data_size += data_size;
memcpy(p, data, data_size);
return 0;
}
@ -175,27 +184,27 @@ int defer_write(struct queue *q, void* data, int data_size)
* Upon success, the number of bytes written is returned.
* Upon failure, -1 returned (e.g. connexion closed)
* */
int flush_defered(struct queue *q)
int flush_deferred(struct queue *q)
{
int n;
if (verbose)
fprintf(stderr, "flushing defered data to fd %d\n", q->fd);
fprintf(stderr, "flushing deferred data to fd %d\n", q->fd);
n = write(q->fd, q->defered_data, q->defered_data_size);
n = write(q->fd, q->deferred_data, q->deferred_data_size);
if (n == -1)
return n;
if (n == q->defered_data_size) {
if (n == q->deferred_data_size) {
/* All has been written -- release the memory */
free(q->begin_defered_data);
q->begin_defered_data = NULL;
q->defered_data = NULL;
q->defered_data_size = 0;
free(q->begin_deferred_data);
q->begin_deferred_data = NULL;
q->deferred_data = NULL;
q->deferred_data_size = 0;
} else {
/* There is data left */
q->defered_data += n;
q->defered_data_size -= n;
q->deferred_data += n;
q->deferred_data_size -= n;
}
return n;
@ -207,20 +216,21 @@ void init_cnx(struct connection *cnx)
memset(cnx, 0, sizeof(*cnx));
cnx->q[0].fd = -1;
cnx->q[1].fd = -1;
cnx->proto = get_first_protocol();
}
void dump_connection(struct connection *cnx)
{
printf("state: %d\n", cnx->state);
printf("fd %d, %d defered\n", cnx->q[0].fd, cnx->q[0].defered_data_size);
printf("fd %d, %d defered\n", cnx->q[1].fd, cnx->q[1].defered_data_size);
printf("fd %d, %d deferred\n", cnx->q[0].fd, cnx->q[0].deferred_data_size);
printf("fd %d, %d deferred\n", cnx->q[1].fd, cnx->q[1].deferred_data_size);
}
/*
* moves data from one fd to other
*
* retuns number of bytes copied if success
* returns number of bytes copied if success
* returns 0 (FD_CNXCLOSED) if incoming socket closed
* returns FD_NODATA if no data was available
* returns FD_STALLED if data was read, could not be written, and has been
@ -255,7 +265,7 @@ int fd2fd(struct queue *target_q, struct queue *from_q)
size_w = write(target, buffer, size_r);
/* process -1 when we know how to deal with it */
if ((size_w == -1)) {
if (size_w == -1) {
switch (errno) {
case EAGAIN:
/* write blocked: Defer data */
@ -469,7 +479,7 @@ void setup_signals(void)
res = sigaction(SIGCHLD, &action, NULL);
CHECK_RES_DIE(res, "sigaction");
/* Set SIGTERM to exit. For some reason if it's not set explicitely,
/* Set SIGTERM to exit. For some reason if it's not set explicitly,
* coverage information is lost when killing the process */
memset(&action, 0, sizeof(action));
action.sa_handler = exit;
@ -499,7 +509,7 @@ void setup_syslog(const char* bin_name) {
log_message(LOG_INFO, "%s %s started\n", server_type, VERSION);
}
/* We don't want to run as root -- drop priviledges if required */
/* We don't want to run as root -- drop privileges if required */
void drop_privileges(const char* user_name)
{
int res;

View File

@ -58,17 +58,18 @@ enum connection_state {
#define PROT_SHIFT 1000 /* protocol options will be 1000, 1001, etc */
/* A 'queue' is composed of a file descriptor (which can be read from or
* written to), and a queue for defered write data */
* written to), and a queue for deferred write data */
struct queue {
int fd;
void *begin_defered_data;
void *defered_data;
int defered_data_size;
void *begin_deferred_data;
void *deferred_data;
int deferred_data_size;
};
struct connection {
enum connection_state state;
time_t probe_timeout;
struct proto *proto;
/* q[0]: queue for external connection (client);
* q[1]: queue for internal connection (httpd or sshd);
@ -83,11 +84,10 @@ struct connection {
/* common.c */
void init_cnx(struct connection *cnx);
int connect_addr(struct addrinfo *addr, int fd_from, const char* cnx_name);
int connect_addr(struct connection *cnx, int fd_from);
int fd2fd(struct queue *target, struct queue *from);
char* sprintaddr(char* buf, size_t size, struct addrinfo *a);
void resolve_name(struct addrinfo **out, char* fullname);
struct proto* probe_client_protocol(struct connection *cnx);
void log_connection(struct connection *cnx);
int check_access_rights(int in_socket, const char* service);
void setup_signals(void);
@ -101,7 +101,7 @@ int resolve_split_name(struct addrinfo **out, const char* hostname, const char*
int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list);
int defer_write(struct queue *q, void* data, int data_size);
int flush_defered(struct queue *q);
int flush_deferred(struct queue *q);
extern int probing_timeout, verbose, inetd, foreground,
background, transparent, numeric;

View File

@ -1,6 +1,6 @@
#! /bin/bash
if [ ${#} -eq 1 ] && [ "x$1" == "x-r" ]; then
if [ ${#} -eq 1 ] && [ "x$1" = "x-r" ]; then
# release text only
QUIET=1
else

98
probe.c
View File

@ -125,10 +125,10 @@ void hexdump(const char *mem, unsigned int len)
/* Is the buffer the beginning of an SSH connection? */
static int is_ssh_protocol(const char *p, int len, struct proto *proto)
{
if (!strncmp(p, "SSH-", 4)) {
return 1;
}
return 0;
if (len < 4)
return PROBE_AGAIN;
return !strncmp(p, "SSH-", 4);
}
/* Is the buffer the beginning of an OpenVPN connection?
@ -143,8 +143,12 @@ static int is_ssh_protocol(const char *p, int len, struct proto *proto)
*/
static int is_openvpn_protocol (const char*p,int len, struct proto *proto)
{
int packet_len = ntohs(*(uint16_t*)p);
int packet_len;
if (len < 2)
return PROBE_AGAIN;
packet_len = ntohs(*(uint16_t*)p);
return packet_len == len - 2;
}
@ -153,6 +157,9 @@ static int is_openvpn_protocol (const char*p,int len, struct proto *proto)
* */
static int is_tinc_protocol( const char *p, int len, struct proto *proto)
{
if (len < 2)
return PROBE_AGAIN;
return !strncmp(p, "0 ", 2);
}
@ -162,37 +169,51 @@ static int is_tinc_protocol( const char *p, int len, struct proto *proto)
* */
static int is_xmpp_protocol( const char *p, int len, struct proto *proto)
{
return strstr(p, "jabber") ? 1 : 0;
if (len < 6)
return PROBE_AGAIN;
return memmem(p, len, "jabber", 6) ? 1 : 0;
}
static int probe_http_method(const char *p, const char *opt)
static int probe_http_method(const char *p, int len, const char *opt)
{
return !strcmp(p, opt);
if (len < strlen(opt))
return PROBE_AGAIN;
return !strncmp(p, opt, len);
}
/* Is the buffer the beginning of an HTTP connection? */
static int is_http_protocol(const char *p, int len, struct proto *proto)
{
int res;
/* If it's got HTTP in the request (HTTP/1.1) then it's HTTP */
if (strstr(p, "HTTP"))
return 1;
if (memmem(p, len, "HTTP", 4))
return PROBE_MATCH;
#define PROBE_HTTP_METHOD(opt) if ((res = probe_http_method(p, len, opt)) != PROBE_NEXT) return res
/* Otherwise it could be HTTP/1.0 without version: check if it's got an
* HTTP method (RFC2616 5.1.1) */
probe_http_method(p, "OPTIONS");
probe_http_method(p, "GET");
probe_http_method(p, "HEAD");
probe_http_method(p, "POST");
probe_http_method(p, "PUT");
probe_http_method(p, "DELETE");
probe_http_method(p, "TRACE");
probe_http_method(p, "CONNECT");
PROBE_HTTP_METHOD("OPTIONS");
PROBE_HTTP_METHOD("GET");
PROBE_HTTP_METHOD("HEAD");
PROBE_HTTP_METHOD("POST");
PROBE_HTTP_METHOD("PUT");
PROBE_HTTP_METHOD("DELETE");
PROBE_HTTP_METHOD("TRACE");
PROBE_HTTP_METHOD("CONNECT");
return 0;
#undef PROBE_HTTP_METHOD
return PROBE_NEXT;
}
static int is_tls_protocol(const char *p, int len, struct proto *proto)
{
if (len < 3)
return PROBE_AGAIN;
/* TLS packet starts with a record "Hello" (0x16), followed by version
* (0x03 0x00-0x03) (RFC6101 A.1)
* This means we reject SSLv2 and lower, which is actually a good thing (RFC6176)
@ -202,25 +223,22 @@ static int is_tls_protocol(const char *p, int len, struct proto *proto)
static int regex_probe(const char *p, int len, struct proto *proto)
{
regex_t** probe_list = (regex_t**)(proto->data);
int i=0;
regex_t **probe = proto->data;
regmatch_t pos = { 0, len };
while (probe_list[i]) {
if (!regexec(probe_list[i], p, 0, NULL, 0)) {
return 1;
}
i++;
}
return 0;
for (; *probe && regexec(*probe, p, 0, &pos, REG_STARTEND); probe++)
/* try them all */;
return (probe != NULL);
}
/*
* Read the beginning of data coming from the client connection and check if
* it's a known protocol. Then leave the data on the defered
* write buffer of the connection and returns a pointer to the protocol
* structure
* it's a known protocol.
* Return PROBE_AGAIN if not enough data, or PROBE_MATCH if it succeeded in
* which case cnx->proto is set to the appropriate protocol.
*/
struct proto* probe_client_protocol(struct connection *cnx)
int probe_client_protocol(struct connection *cnx)
{
char buffer[BUFSIZ];
struct proto *p;
@ -233,16 +251,19 @@ struct proto* probe_client_protocol(struct connection *cnx)
* function does not have to deal with a specific failure condition (the
* connection will just fail later normally). */
if (n > 0) {
int res = PROBE_NEXT;
defer_write(&cnx->q[1], buffer, n);
for (p = protocols; p; p = p->next) {
for (p = cnx->proto; p && res == PROBE_NEXT; p = p->next) {
if (! p->probe) continue;
if (verbose) fprintf(stderr, "probing for %s\n", p->description);
if (p->probe(buffer, n, p)) {
if (verbose) fprintf(stderr, "probe %s successful\n", p->description);
return p;
}
cnx->proto = p;
res = p->probe(cnx->q[1].begin_deferred_data, cnx->q[1].deferred_data_size, p);
}
if (res != PROBE_NEXT)
return res;
}
if (verbose)
@ -252,7 +273,8 @@ struct proto* probe_client_protocol(struct connection *cnx)
/* If none worked, return the first one affected (that's completely
* arbitrary) */
return protocols;
cnx->proto = protocols;
return PROBE_MATCH;
}
/* Returns the structure for specified protocol or NULL if not found */

10
probe.h
View File

@ -5,6 +5,12 @@
#include "common.h"
typedef enum {
PROBE_NEXT, /* Enough data, probe failed -- it's some other protocol */
PROBE_MATCH, /* Enough data, probe successful -- it's the current protocol */
PROBE_AGAIN, /* Not enough data for this probe, try again with more data */
} probe_result;
struct proto;
typedef int T_PROBE(const char*, int, struct proto*);
@ -39,11 +45,11 @@ void set_protocol_list(struct proto*);
/* probe_client_protocol
*
* Read the beginning of data coming from the client connection and check if
* it's a known protocol. Then leave the data on the defered
* it's a known protocol. Then leave the data on the deferred
* write buffer of the connection and returns a pointer to the protocol
* structure
*/
struct proto* probe_client_protocol(struct connection *cnx);
int probe_client_protocol(struct connection *cnx);
/* set the protocol to connect to in case of timeout */
void set_ontimeout(const char* name);

View File

@ -69,47 +69,49 @@ void start_shoveler(int in_socket)
{
fd_set fds;
struct timeval tv;
struct addrinfo *saddr;
int res;
int res = PROBE_AGAIN;
int out_socket;
struct connection cnx;
struct proto *prot;
init_cnx(&cnx);
cnx.q[0].fd = in_socket;
FD_ZERO(&fds);
FD_SET(in_socket, &fds);
memset(&tv, 0, sizeof(tv));
tv.tv_sec = probing_timeout;
res = select(in_socket + 1, &fds, NULL, NULL, &tv);
if (res == -1)
perror("select");
cnx.q[0].fd = in_socket;
while (res == PROBE_AGAIN) {
/* POSIX does not guarantee that tv will be updated, but the client can
* only postpone the inevitable for so long */
res = select(in_socket + 1, &fds, NULL, NULL, &tv);
if (res == -1)
perror("select");
if (FD_ISSET(in_socket, &fds)) {
/* Received data: figure out what protocol it is */
prot = probe_client_protocol(&cnx);
} else {
/* Timed out: it's necessarily SSH */
prot = timeout_protocol();
if (FD_ISSET(in_socket, &fds)) {
/* Received data: figure out what protocol it is */
res = probe_client_protocol(&cnx);
} else {
/* Timed out: it's necessarily SSH */
cnx.proto = timeout_protocol();
break;
}
}
saddr = prot->saddr;
if (prot->service &&
check_access_rights(in_socket, prot->service)) {
if (cnx.proto->service &&
check_access_rights(in_socket, cnx.proto->service)) {
exit(0);
}
/* Connect the target socket */
out_socket = connect_addr(saddr, in_socket, prot->description);
out_socket = connect_addr(&cnx, in_socket);
CHECK_RES_DIE(out_socket, "connect");
cnx.q[1].fd = out_socket;
log_connection(&cnx);
flush_defered(&cnx.q[1]);
flush_deferred(&cnx.q[1]);
shovel(&cnx);

View File

@ -275,7 +275,7 @@ static int config_parse(char *filename, struct addrinfo **listen, struct proto *
config_lookup_bool(&config, "numeric", &numeric);
config_lookup_bool(&config, "transparent", &transparent);
if (config_lookup_int(&config, "timeout", &timeout) == CONFIG_TRUE) {
if (config_lookup_int(&config, "timeout", (int *)&timeout) == CONFIG_TRUE) {
probing_timeout = timeout;
}

View File

@ -64,8 +64,8 @@ int tidy_connection(struct connection *cnx, fd_set *fds, fd_set *fds2)
close(cnx->q[i].fd);
FD_CLR(cnx->q[i].fd, fds);
FD_CLR(cnx->q[i].fd, fds2);
if (cnx->q[i].defered_data)
free(cnx->q[i].defered_data);
if (cnx->q[i].deferred_data)
free(cnx->q[i].deferred_data);
}
}
init_cnx(cnx);
@ -129,18 +129,16 @@ int accept_new_connection(int listen_socket, struct connection *cnx[], int* cnx_
/* Connect queue 1 of connection to SSL; returns new file descriptor */
int connect_queue(struct connection *cnx, struct addrinfo *addr,
const char* cnx_name,
fd_set *fds_r, fd_set *fds_w)
int connect_queue(struct connection *cnx, fd_set *fds_r, fd_set *fds_w)
{
struct queue *q = &cnx->q[1];
q->fd = connect_addr(addr, cnx->q[0].fd, cnx_name);
q->fd = connect_addr(cnx, cnx->q[0].fd);
if ((q->fd != -1) && fd_is_in_range(q->fd)) {
log_connection(cnx);
set_nonblock(q->fd);
flush_defered(q);
if (q->defered_data) {
flush_deferred(q);
if (q->deferred_data) {
FD_SET(q->fd, fds_w);
} else {
FD_SET(q->fd, fds_r);
@ -190,13 +188,13 @@ int is_fd_active(int fd, fd_set* set)
}
/* Main loop: the idea is as follow:
* - fds_r and fds_w contain the file descritors to monitor in read and write
* - fds_r and fds_w contain the file descriptors to monitor in read and write
* - When a file descriptor goes off, process it: read from it, write the data
* to its corresponding pair.
* - When a file descriptor blocks when writing, remove the read fd from fds_r,
* move the data to a defered buffer, and add the write fd to fds_w. Defered
* move the data to a deferred buffer, and add the write fd to fds_w. Defered
* buffer is allocated dynamically.
* - When we can write to a file descriptor that has defered data, we try to
* - When we can write to a file descriptor that has deferred data, we try to
* write as much as we can. Once all data is written, remove the fd from fds_w
* and add its corresponding pair to fds_r, free the buffer.
*
@ -210,7 +208,6 @@ void main_loop(int listen_sockets[], int num_addr_listen)
struct timeval tv;
int max_fd, in_socket, i, j, res;
struct connection *cnx;
struct proto *prot;
int num_cnx; /* Number of connections in *cnx */
int num_probing = 0; /* Number of connections currently probing
* We use this to know if we need to time out of
@ -268,16 +265,16 @@ void main_loop(int listen_sockets[], int num_addr_listen)
if (cnx[i].q[0].fd != -1) {
for (j = 0; j < 2; j++) {
if (is_fd_active(cnx[i].q[j].fd, &writefds)) {
res = flush_defered(&cnx[i].q[j]);
res = flush_deferred(&cnx[i].q[j]);
if ((res == -1) && ((errno == EPIPE) || (errno == ECONNRESET))) {
if (cnx[i].state == ST_PROBING) num_probing--;
tidy_connection(&cnx[i], &fds_r, &fds_w);
if (verbose)
fprintf(stderr, "closed slot %d\n", i);
} else {
/* If no defered data is left, stop monitoring the fd
/* If no deferred data is left, stop monitoring the fd
* for write, and restart monitoring the other one for reads*/
if (!cnx[i].q[j].defered_data_size) {
if (!cnx[i].q[j].deferred_data_size) {
FD_CLR(cnx[i].q[j].fd, &fds_w);
FD_SET(cnx[i].q[1-j].fd, &fds_r);
}
@ -303,27 +300,27 @@ void main_loop(int listen_sockets[], int num_addr_listen)
dump_connection(&cnx[i]);
exit(1);
}
num_probing--;
cnx[i].state = ST_SHOVELING;
/* If timed out it's SSH, otherwise the client sent
* data so probe the protocol */
if ((cnx[i].probe_timeout < time(NULL))) {
prot = timeout_protocol();
cnx[i].proto = timeout_protocol();
} else {
prot = probe_client_protocol(&cnx[i]);
res = probe_client_protocol(&cnx[i]);
if (res == PROBE_AGAIN)
continue;
}
num_probing--;
cnx[i].state = ST_SHOVELING;
/* libwrap check if required for this protocol */
if (prot->service &&
check_access_rights(in_socket, prot->service)) {
if (cnx[i].proto->service &&
check_access_rights(in_socket, cnx[i].proto->service)) {
tidy_connection(&cnx[i], &fds_r, &fds_w);
res = -1;
} else {
res = connect_queue(&cnx[i],
prot->saddr,
prot->description,
&fds_r, &fds_w);
res = connect_queue(&cnx[i], &fds_r, &fds_w);
}
if (res >= max_fd)

21
t
View File

@ -18,6 +18,7 @@ my $pidfile = "/tmp/sslh_test.pid";
my $SSL_CNX = 1;
my $SSH_SHY_CNX = 1;
my $SSH_BOLD_CNX = 1;
my $SSH_PROBE_AGAIN = 1;
my $SSL_MIX_SSH = 1;
my $SSH_MIX_SSL = 1;
my $BIG_MSG = 0; # This test is unreliable
@ -58,11 +59,11 @@ for my $binary (@binaries) {
my $cmd = "./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile";
warn "$cmd\n";
#exec $cmd;
exec "valgrind --leak-check=full ./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address -ssl $ssl_address -P $pidfile";
exec "valgrind --leak-check=full ./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address -ssl $ssl_address -P $pidfile";
exit 0;
}
warn "spawned $sslh_pid\n";
sleep 1; # valgrind can be heavy -- wait 5 seconds
sleep 5; # valgrind can be heavy -- wait 5 seconds
my $test_data = "hello world\n";
@ -108,6 +109,22 @@ for my $binary (@binaries) {
}
}
# Test: PROBE_AGAIN, incomplete first frame
if ($SSH_PROBE_AGAIN) {
print "***Test: incomplete SSH first frame\n";
my $cnx_h = new IO::Socket::INET(PeerHost => "localhost:$sslh_port");
warn "$!\n" unless $cnx_h;
if (defined $cnx_h) {
my $td = "SSH-2.0 testsuite\t$test_data";
print $cnx_h substr $td, 0, 2;
sleep 1;
print $cnx_h substr $td, 2;
my $data = <$cnx_h>;
is($data, "ssh: $td", "Incomplete first SSH frame");
}
}
# Test: One SSL half-started then one SSH
if ($SSL_MIX_SSH) {
print "***Test: One SSL half-started then one SSH\n";