diff --git a/CMakeLists.txt b/CMakeLists.txt index caa319d6b9..3b101b71bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1218,7 +1218,7 @@ endforeach() include(FeatureSummary) #SET_FEATURE_INFO(NAME DESCRIPTION [URL [COMMENT] ]) SET_FEATURE_INFO(SBC "SBC Codec for Bluetooth A2DP stream playing" "www: http://git.kernel.org/cgit/bluetooth/sbc.git" ) -SET_FEATURE_INFO(LIBSSH "libssh is library for ssh connections and it is needed to build sshdump" "www: https://www.libssh.org/get-it/" ) +SET_FEATURE_INFO(LIBSSH "libssh is library for ssh connections and it is needed to build sshdump/ciscodump" "www: https://www.libssh.org/get-it/" ) FEATURE_SUMMARY(WHAT ALL) @@ -2374,6 +2374,32 @@ elseif (BUILD_sshdump) #message( WARNING "Cannot find libssh, cannot build sshdump" ) endif() +if(ENABLE_EXTCAP AND BUILD_ciscodump AND LIBSSH_FOUND) + set(ciscodump_LIBS + wsutil + ${GLIB2_LIBRARIES} + ${CMAKE_DL_LIBS} + ${LIBSSH_LIBRARIES} + ) + if (WIN32) + set(ciscodump_LIBS wsutil ${ciscodump_LIBS}) + endif() + set(ciscodump_FILES + extcap/ciscodump.c + extcap/extcap-base.c + extcap/ssh-base.c + pcapio.c + ) + + add_executable(ciscodump WIN32 ${ciscodump_FILES}) + set_extcap_executable_properties(ciscodump) + target_link_libraries(ciscodump ${ciscodump_LIBS}) + target_include_directories(ciscodump PUBLIC ${LIBSSH_INCLUDE_DIR}) + install(TARGETS ciscodump RUNTIME DESTINATION ${EXTCAP_DIR}) +elseif (BUILD_ciscodump) + #message( WARNING "Cannot find libssh, cannot build ciscodump" ) +endif() + if(ENABLE_EXTCAP AND BUILD_randpktdump) set(randpktdump_LIBS randpkt_core @@ -2464,6 +2490,7 @@ set(CLEAN_FILES ${dumpcap_FILES} ${androiddump_FILES} ${sshdump_FILES} + ${ciscodump_FILES} ) if (WERROR_COMMON_FLAGS) diff --git a/CMakeOptions.txt b/CMakeOptions.txt index 52cf9e6855..edd6a0df3b 100644 --- a/CMakeOptions.txt +++ b/CMakeOptions.txt @@ -16,6 +16,7 @@ option(BUILD_randpkt "Build randpkt" ON) option(BUILD_dftest "Build dftest" ON) option(BUILD_androiddump "Build androiddump" ON) option(BUILD_sshdump "Build sshdump" ON) +option(BUILD_ciscodump "Build ciscodump" ON) option(BUILD_randpktdump "Build randpktdump" ON) option(AUTOGEN_dcerpc "Autogenerate DCE RPC dissectors" OFF) option(AUTOGEN_pidl "Autogenerate pidl dissectors" OFF) diff --git a/config.nmake b/config.nmake index 44b0ee20ef..7214485ea0 100644 --- a/config.nmake +++ b/config.nmake @@ -416,7 +416,7 @@ GNUTLS_PKG=3.2.15-2.7 GPGERROR_DLL=libgpg-error-0.dll GCC_DLL=libgcc_s_sjlj-1.dll -# Optional: libssh library is required for sshdump support +# Optional: libssh library is required for sshdump and ciscodump support # # If you don't have libssh, comment this line out so that LIBSSH_DIR # isn't defined. diff --git a/configure.ac b/configure.ac index d81988298d..a1a1c7ca21 100644 --- a/configure.ac +++ b/configure.ac @@ -3016,13 +3016,16 @@ dnl sshdump check AC_MSG_CHECKING(whether to build sshdump) AC_ARG_ENABLE(sshdump, - AC_HELP_STRING( [--enable-sshdump], - [build sshdump @<:@default=yes@:>@]), - sshdump=$enableval,enable_sshdump=yes) + AC_HELP_STRING( [--enable-sshdump], + [build sshdump @<:@default=yes@:>@]), + [],[enable_sshdump=yes]) if test "x$have_extcap" != xyes; then AC_MSG_RESULT(no, extcap disabled) enable_sshdump=no +elif test "x$have_good_libssh" != xyes; then + AC_MSG_RESULT(no, libssh not available) + enable_sshdump=no elif test "x$enable_sshdump" = "xyes" ; then AC_MSG_RESULT(yes) else @@ -3030,15 +3033,8 @@ else fi if test "x$enable_sshdump" = "xyes" ; then - if test "x$have_good_libssh" = "xyes" ; then - sshdump_bin="sshdump\$(EXEEXT)" - sshdump_man="sshdump.1" - else - echo "Can't find libssh. Disabling sshdump." - enable_sshdump=no - sshdump_bin="" - sshdump_man="" - fi + sshdump_bin="sshdump\$(EXEEXT)" + sshdump_man="sshdump.1" else sshdump_bin="" sshdump_man="" @@ -3046,13 +3042,43 @@ fi AC_SUBST(sshdump_bin) AC_SUBST(sshdump_man) +dnl ciscodump check +AC_MSG_CHECKING(whether to build ciscodump) + +AC_ARG_ENABLE(ciscodump, + AC_HELP_STRING( [--enable-ciscodump], + [build ciscodump @<:@default=yes@:>@]), + [],[enable_ciscodump=yes]) + +if test "x$have_extcap" != xyes; then + AC_MSG_RESULT(no, extcap disabled) + enable_ciscodump=no +elif test "x$have_good_libssh" != xyes; then + AC_MSG_RESULT(no, libssh not available) + enable_ciscodump=no +elif test "x$enable_ciscodump" = "xyes" ; then + AC_MSG_RESULT(yes) +else + AC_MSG_RESULT(no) +fi + +if test "x$enable_ciscodump" = "xyes" ; then + ciscodump_bin="ciscodump\$(EXEEXT)" + ciscodump_man="ciscodump.1" +else + ciscodump_bin="" + ciscodump_man="" +fi +AC_SUBST(ciscodump_bin) +AC_SUBST(ciscodump_man) + dnl randpktdump check AC_MSG_CHECKING(whether to build randpktdump) AC_ARG_ENABLE(randpktdump, - AC_HELP_STRING( [--enable-randpktdump], - [build androiddump @<:@default=yes@:>@]), - randpktdump=$enableval,enable_randpktdump=yes) + AC_HELP_STRING( [--enable-randpktdump], + [build androiddump @<:@default=yes@:>@]), + randpktdump=$enableval,enable_randpktdump=yes) if test "x$have_extcap" != xyes; then AC_MSG_RESULT(no, extcap disabled) @@ -3443,6 +3469,7 @@ echo " Build dftest : $enable_dftest" echo " Build rawshark : $enable_rawshark" echo " Build androiddump : $enable_androiddump" echo " Build sshdump : $enable_sshdump" +echo " Build ciscodump : $enable_ciscodump" echo " Build randpktdump : $enable_randpktdump" echo " Build echld : $have_echld" echo "" diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index b9830b6704..d2bc082c5a 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -84,6 +84,7 @@ pod2manhtml(${CMAKE_CURRENT_SOURCE_DIR}/randpktdump 1) pod2manhtml(${CMAKE_CURRENT_SOURCE_DIR}/rawshark 1) pod2manhtml(${CMAKE_CURRENT_SOURCE_DIR}/reordercap 1) pod2manhtml(${CMAKE_CURRENT_SOURCE_DIR}/sshdump 1) +pod2manhtml(${CMAKE_CURRENT_SOURCE_DIR}/ciscodump 1) pod2manhtml(${CMAKE_CURRENT_SOURCE_DIR}/text2pcap 1) pod2manhtml(${CMAKE_CURRENT_SOURCE_DIR}/tshark 1) pod2manhtml(${CMAKE_CURRENT_BINARY_DIR}/wireshark 1) @@ -107,6 +108,7 @@ set(MAN1_INSTALL_FILES ${CMAKE_CURRENT_BINARY_DIR}/rawshark.1 ${CMAKE_CURRENT_BINARY_DIR}/reordercap.1 ${CMAKE_CURRENT_BINARY_DIR}/sshdump.1 + ${CMAKE_CURRENT_BINARY_DIR}/ciscodump.1 ${CMAKE_CURRENT_BINARY_DIR}/text2pcap.1 ${CMAKE_CURRENT_BINARY_DIR}/tshark.1 ${CMAKE_CURRENT_BINARY_DIR}/wireshark.1 @@ -134,6 +136,7 @@ set(HTML_INSTALL_FILES ${CMAKE_CURRENT_BINARY_DIR}/rawshark.html ${CMAKE_CURRENT_BINARY_DIR}/reordercap.html ${CMAKE_CURRENT_BINARY_DIR}/sshdump.html + ${CMAKE_CURRENT_BINARY_DIR}/ciscodump.html ${CMAKE_CURRENT_BINARY_DIR}/text2pcap.html ${CMAKE_CURRENT_BINARY_DIR}/tshark.html ${CMAKE_CURRENT_BINARY_DIR}/wireshark.html diff --git a/doc/ciscodump.pod b/doc/ciscodump.pod new file mode 100644 index 0000000000..ff46d3e397 --- /dev/null +++ b/doc/ciscodump.pod @@ -0,0 +1,231 @@ + +=head1 NAME + +ciscodump - Provide interfaces to capture from a remote Cisco router through SSH. + +=head1 SYNOPSIS + +B +S<[ B<--help> ]> +S<[ B<--version> ]> +S<[ B<--extcap-interfaces> ]> +S<[ B<--extcap-dlts> ]> +S<[ B<--extcap-interface>=EinterfaceE ]> +S<[ B<--extcap-config> ]> +S<[ B<--extcap-capture-filter>=Ecapture filterE ]> +S<[ B<--capture> ]> +S<[ B<--fifo>=Epath to file or pipeE ]> +S<[ B<--remote-host>=EIP addressE ]> +S<[ B<--remote-port>=ETCP portE ]> +S<[ B<--remote-username>=EusernameE ]> +S<[ B<--remote-password>=EpasswordE ]> +S<[ B<--remote-filter>=Efilter ]> +S<[ B<--sshkey>=Epublic key path ]> +S<[ B<--remote-interface>=EinterfaceE ]> + + +B +S> + +B +S=EinterfaceE> +S> + +B +S=EinterfaceE> +S> + +B +S=EinterfaceE> +S=Epath to file or pipeE> +S> +S> +S> +S> +S=Ethe router interfaceE> + +=head1 DESCRIPTION + +B is an extcap tool that relys on Cisco EPC to allow a user to run a remote capture +on a Cisco router in a SSH connection. The minimum IOS version supporting this feature is 12.4(20)T. More details can be +found here: +http://www.cisco.com/c/en/us/products/collateral/ios-nx-os-software/ios-embedded-packet-capture/datasheet_c78-502727.html + +Supported interfaces: + +=over 4 + +=item 1. cisco + +=back + +=head1 OPTIONS + +=over 4 + +=item --help + +Print program arguments. + +=item --version + +Print program version. + +=item --extcap-interfaces + +List available interfaces. + +=item --extcap-interface=EinterfaceE + +Use specified interfaces. + +=item --extcap-dlts + +List DLTs of specified interface. + +=item --extcap-config + +List configuration options of specified interface. + +=item --capture + +Start capturing from specified interface and save it in place specified by --fifo. + +=item --fifo=Epath to file or pipeE + +Save captured packet to file or send it through pipe. + +=item --remote-host=Eremote hostE + +The address of the remote host for capture. + +=item --remote-port=Eremote portE + +The SSH port of the remote host. + +=item --remote-username=EusernameE + +The username for ssh authentication. + +=item --remote-password=EpasswordE + +The password to use (if not ssh-agent and pubkey are used). WARNING: the +passwords are stored in plaintext and visible to all users on this system. It is +recommended to use keyfiles with a SSH agent. + +=item --remote-filter=EfilterE + +The remote filter on the router. This is a capture filter that follows the Cisco IOS standards (http://www.cisco.com/c/en/us/support/docs/ip/access-lists/26448-ACLsamples.html). Multiple filters can be specified using a comma between them. BEWARE: when using a filter, the default behavior is to drop all the packets except the ones that fall into the filter. + +Examples: + + permit ip host MYHOST any, permit ip any host MYHOST (capture the traffic for MYHOST) + + deny ip host MYHOST any, deny ip any host MYHOST, permit ip any any (capture all the traffic except MYHOST) + +=item --sshkey=ESSH private key pathE + +The path to a private key for authentication. + +=item --remote-interface=Eremote interfaceE + +The remote network interface to capture from. + +=item --extcap-capture-filter=Ecapture filterE + +Unused (compatibility only). + +=back + +=head1 EXAMPLES + +To see program arguments: + + ciscodump --help + +To see program version: + + ciscodump --version + +To see interfaces: + + ciscodump --extcap-interfaces + +Only one interface (cisco) is supported. + + Output: + interface {value=cisco}{display=SSH remote capture} + +To see interface DLTs: + + ciscodump --extcap-interface=cisco --extcap-dlts + + Output: + dlt {number=147}{name=cisco}{display=Remote capture dependant DLT} + +To see interface configuration options: + + ciscodump --extcap-interface=cisco --extcap-config + + Output: + ciscodump --extcap-interface=cisco --extcap-config + arg {number=0}{call=--remote-host}{display=Remote SSH server address} + {type=string}{tooltip=The remote SSH host. It can be both an IP address or a hostname} + {required=true} + arg {number=1}{call=--remote-port}{display=Remote SSH server port}{type=unsigned} + {default=22}{tooltip=The remote SSH host port (1-65535)}{range=1,65535} + arg {number=2}{call=--remote-username}{display=Remote SSH server username}{type=string} + {default=}{tooltip=The remote SSH username. If not provided, the current + user will be used} + arg {number=3}{call=--remote-password}{display=Remote SSH server password}{type=string} + {tooltip=The SSH password, used when other methods (SSH agent or key files) are unavailable.} + arg {number=4}{call=--sshkey}{display=Path to SSH private key}{type=fileselect} + {tooltip=The path on the local filesystem of the private ssh key} + arg {number=5}{call--sshkey-passphrase}{display=SSH key passphrase} + {type=string}{tooltip=Passphrase to unlock the SSH private key} + arg {number=6}{call=--remote-interface}{display=Remote interface}{type=string} + {required=true}{tooltip=The remote network interface used for capture} + arg {number=7}{call=--remote-filter}{display=Remote capture filter}{type=string} + {default=(null)}{tooltip=The remote capture filter} + arg {number=8}{call=--remote-count}{display=Packets to capture}{type=unsigned}{required=true} + {tooltip=The number of remote packets to capture.} + + +To capture: + + ciscodump --extcap-interface cisco --fifo=/tmp/cisco.pcap --capture --remote-host 192.168.1.10 + --remote-username user --remote-interface gigabit0/0 + --remote-filter "permit ip host 192.168.1.1 any, permit ip any host 192.168.1.1" + +NOTE: Packet count is mandatory, hence the capture will start after this number. + +=head1 KNOWN ISSUES + +The configuration of the capture on the routers is a multi-step process. If the SSH connection is interrupted during +it, the configuration can be in an inconsistent state. That can happen also if the capture is stopped and ciscodump +can't clean the configuration up. In this case it is necessary to log into the router and manually clean the +configuration, removing both the capture point (WIRESHARK_CAPTURE_POINT), the capture buffer (WIRESHARK_CAPTURE_BUFFER) +and the capture filter (WIRESHARK_CAPTURE_FILTER). + +Another known issues is related to the number of captured packets (--remote-count). Due to the nature of the capture +buffer, ciscodump waits for the capture to complete and then issues the command to show it. It means that if the user +specifies a number of packets above the currently captured, the show command is never shown. Not only is the count of +the maximum number of captured packets, but it is also the _exact_ number of expected packets. + +=head1 SEE ALSO + +wireshark(1), tshark(1), dumpcap(1), extcap(4), sshdump(1) + +=head1 NOTES + +B is part of the B distribution. The latest version +of B can be found at L. + +HTML versions of the Wireshark project man pages are available at: +L. + +=head1 AUTHORS + + Original Author + -------- ------ + Dario Lombardo diff --git a/extcap/Makefile.am b/extcap/Makefile.am index 4566c9d782..d4fee3baa1 100644 --- a/extcap/Makefile.am +++ b/extcap/Makefile.am @@ -34,9 +34,10 @@ EXTRA_DIST = \ extcap_PROGRAMS = \ @androiddump_bin@ \ @randpktdump_bin@ \ - @sshdump_bin@ + @sshdump_bin@ \ + @ciscodump_bin@ -EXTRA_PROGRAMS = androiddump randpktdump sshdump +EXTRA_PROGRAMS = androiddump randpktdump sshdump ciscodump if ENABLE_STATIC androiddump_LDFLAGS = -Wl,-static -all-static @@ -78,3 +79,17 @@ sshdump_LDADD = \ @GLIB_LIBS@ \ @LIBSSH_LIBS@ \ @SOCKET_LIBS@ + +if ENABLE_STATIC + ciscodump_LDFLAGS = -Wl,-static -all-static +else + ciscodump_LDFLAGS = -export-dynamic +endif + +# Libraries and plugin flags with which to link ciscodump. +ciscodump_LDADD = \ + ../wiretap/libwiretap.la \ + ../wsutil/libwsutil.la \ + @GLIB_LIBS@ \ + @LIBSSH_LIBS@ \ + @SOCKET_LIBS@ diff --git a/extcap/Makefile.common b/extcap/Makefile.common index 22ca1dc7b3..0d795b783e 100644 --- a/extcap/Makefile.common +++ b/extcap/Makefile.common @@ -37,6 +37,12 @@ sshdump_SOURCES = \ extcap-base.c \ ssh-base.c +# ciscodump specifics +ciscodump_SOURCES = \ + ciscodump.c \ + extcap-base.c \ + ssh-base.c + noinst_HEADERS = \ - extcap-base.h \ + extcap-base.h \ ssh-base.h diff --git a/extcap/Makefile.nmake b/extcap/Makefile.nmake index 3e8cfd804b..4eea2b628b 100644 --- a/extcap/Makefile.nmake +++ b/extcap/Makefile.nmake @@ -61,10 +61,21 @@ sshdump_LIBS = $(sshdump_WSLIBS) \ $(LIBSSH_LIBS) \ $(GLIB_LIBS) +ciscodump_OBJECTS = $(ciscodump_SOURCES:.c=.obj) + +ciscodump_WSLIBS = \ + ..\wiretap\wiretap-$(WTAP_VERSION).lib \ + ..\wsutil\libwsutil.lib + +ciscodump_LIBS = $(ciscodump_WSLIBS) \ + wsock32.lib user32.lib \ + $(LIBSSH_LIBS) \ + $(GLIB_LIBS) + EXECUTABLES=androiddump.exe randpktdump.exe !IFDEF LIBSSH_DIR -EXECUTABLES += sshdump.exe +EXECUTABLES += sshdump.exe ciscodump.exe !ENDIF all: $(EXECUTABLES) @@ -96,11 +107,19 @@ sshdump.exe : $(LIBS_CHECK) ..\config.h sshdump.obj extcap-base.obj ssh-base.obj !IFDEF MANIFEST_INFO_REQUIRED mt.exe -nologo -manifest "sshdump.exe.manifest" -outputresource:sshdump.exe;1 !ENDIF +ciscodump.exe : $(LIBS_CHECK) ..\config.h ciscodump.obj extcap-base.obj ssh-base.obj $(ciscodump_WSLIBS) + @echo Linking $@ + $(LINK) @<< + /OUT:ciscodump.exe $(conflags) $(conlibsdll) $(LDFLAGS) /SUBSYSTEM:WINDOWS ciscodump.obj extcap-base.obj ssh-base.obj $(ciscodump_LIBS) +<< +!IFDEF MANIFEST_INFO_REQUIRED + mt.exe -nologo -manifest "ciscodump.exe.manifest" -outputresource:ciscodump.exe;1 +!ENDIF !ENDIF clean: rm -f $(androiddump_OBJECTS) $(randpktdump_OBJECTS) $(sshdump_OBJECTS) \ - $(EXECUTABLES) *.nativecodeanalysis.xml *.pdb *.sbr \ + ${ciscodump_OBJECTS} $(EXECUTABLES) *.nativecodeanalysis.xml *.pdb *.sbr \ doxygen.cfg *.exe.manifest # "distclean" removes all files not part of the distribution. @@ -138,8 +157,8 @@ checkapi: checkapi-base checkapi-todo checkapi-base: $(PERL) ../tools/checkAPIs.pl -g deprecated-gtk -build \ - $(androiddump_SOURCES) $(randpktdump_SOURCES) $(sshdump_SOURCES) + $(androiddump_SOURCES) $(randpktdump_SOURCES) $(sshdump_SOURCES) ${ciscodump_SOURCES} checkapi-todo: $(PERL) ../tools/checkAPIs.pl -M -g deprecated-gtk-todo -build \ - $(androiddump_SOURCES) $(randpktdump_SOURCES) $(sshdump_SOURCES) + $(androiddump_SOURCES) $(randpktdump_SOURCES) $(sshdump_SOURCES) ${ciscodump_SOURCES} diff --git a/extcap/ciscodump.c b/extcap/ciscodump.c new file mode 100644 index 0000000000..132665e779 --- /dev/null +++ b/extcap/ciscodump.c @@ -0,0 +1,735 @@ +/* ciscodump.c + * ciscodump is extcap tool used to capture data using a ssh on a remote cisco router + * + * Copyright 2015, Dario Lombardo + * + * Wireshark - Network traffic analyzer + * By Gerald Combs + * Copyright 1998 Gerald Combs + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include +#include +#include +#include + +#include +#include +#include + +#ifndef STDERR_FILENO +#define STDERR_FILENO 2 +#endif + +#ifndef STDOUT_FILENO +#define STDOUT_FILENO 1 +#endif + +#define CISCODUMP_VERSION_MAJOR "1" +#define CISCODUMP_VERSION_MINOR "0" +#define CISCODUMP_VERSION_RELEASE "0" + +/* The read timeout in msec */ +#define CISCODUMP_READ_TIMEOUT 3000 + +#define CISCODUMP_EXTCAP_INTERFACE "cisco" +#define SSH_READ_BLOCK_SIZE 1024 +#define SSH_READ_TIMEOUT 10000 + +#define WIRESHARK_CAPTURE_POINT "WIRESHARK_CAPTURE_POINT" +#define WIRESHARK_CAPTURE_BUFFER "WIRESHARK_CAPTURE_BUFFER" +#define WIRESHARK_CAPTURE_ACCESSLIST "WIRESHARK_CAPTURE_ACCESSLIST" + +#define PCAP_SNAPLEN 0xffff + +#define PACKET_MAX_SIZE 65535 + +#define MINIMUM_IOS_MAJOR 12 +#define MINIMUM_IOS_MINOR 4 + +/* Status of the parser */ +enum { + CISCODUMP_PARSER_STARTING, + CISCODUMP_PARSER_IN_PACKET, + CISCODUMP_PARSER_IN_HEADER, + CISCODUMP_PARSER_END_PACKET, + CISCODUMP_PARSER_ERROR +}; + +#define verbose_print(...) { if (verbose) printf(__VA_ARGS__); } + +static gboolean verbose = TRUE; + +enum { + EXTCAP_BASE_OPTIONS_ENUM, + OPT_HELP, + OPT_VERSION, + OPT_VERBOSE, + OPT_REMOTE_HOST, + OPT_REMOTE_PORT, + OPT_REMOTE_USERNAME, + OPT_REMOTE_PASSWORD, + OPT_REMOTE_INTERFACE, + OPT_REMOTE_FILTER, + OPT_SSHKEY, + OPT_SSHKEY_PASSPHRASE, + OPT_REMOTE_COUNT +}; + +static struct option longopts[] = { + EXTCAP_BASE_OPTIONS, + { "help", no_argument, NULL, OPT_HELP}, + { "version", no_argument, NULL, OPT_VERSION}, + { "verbose", optional_argument, NULL, OPT_VERBOSE}, + SSH_BASE_OPTIONS, + { 0, 0, 0, 0} +}; + +static char* interfaces_list_to_filter(GSList* interfaces, unsigned int remote_port) +{ + GString* filter = g_string_new(NULL); + GSList* cur; + + if (interfaces) { + g_string_append_printf(filter, "deny tcp host %s any eq %u, deny tcp any eq %u host %s", + (char*)interfaces->data, remote_port, remote_port, (char*)interfaces->data); + cur = g_slist_next(interfaces); + while (cur->next != NULL) { + g_string_append_printf(filter, ", deny tcp host %s any eq %u, deny tcp any eq %u host %s", + (char*)cur->data, remote_port, remote_port, (char*)cur->data); + cur = cur->next; + } + g_string_append_printf(filter, ", permit ip any any"); + } + + return g_string_free(filter, FALSE); +} + +static char* local_interfaces_to_filter(const unsigned int remote_port) +{ + GSList* interfaces = local_interfaces_to_list(); + char* filter = interfaces_list_to_filter(interfaces, remote_port); + g_slist_free_full(interfaces, g_free); + return filter; +} + +/* Read bytes from the channel. If bytes == -1, read all data (until timeout). If outbuf != NULL, data are stored there */ +static int read_output_bytes(ssh_channel channel, int bytes, char* outbuf) +{ + char chr; + int total; + int bytes_read; + + total = (bytes > 0 ? bytes : G_MAXINT); + bytes_read = 0; + + while(ssh_channel_read_timeout(channel, &chr, 1, 0, 2000) > 0 && bytes_read < total) { + verbose_print("%c", chr); + if (chr == '^') + return EXIT_FAILURE; + if (outbuf) + outbuf[bytes_read] = chr; + bytes_read++; + } + return EXIT_SUCCESS; +} + +static void ciscodump_cleanup(ssh_session sshs, ssh_channel channel, const char* iface, const char* cfilter) +{ + if (channel) { + if (read_output_bytes(channel, -1, NULL) == EXIT_SUCCESS) { + ssh_channel_printf(channel, "monitor capture point stop %s\n", WIRESHARK_CAPTURE_POINT); + ssh_channel_printf(channel, "no monitor capture point ip cef %s %s\n", WIRESHARK_CAPTURE_POINT, iface); + ssh_channel_printf(channel, "no monitor capture buffer %s\n", WIRESHARK_CAPTURE_BUFFER); + if (cfilter) { + ssh_channel_printf(channel, "configure terminal\n"); + ssh_channel_printf(channel, "no ip access-list ex %s\n", WIRESHARK_CAPTURE_ACCESSLIST); + } + read_output_bytes(channel, -1, NULL); + } + } + ssh_cleanup(&sshs, &channel); +} + +static int wait_until_data(ssh_channel channel, const long unsigned count) +{ + long unsigned got = 0; + char output[SSH_READ_BLOCK_SIZE]; + char* output_ptr; + guint rounds = 100; + + while (got < count && rounds--) { + if (ssh_channel_printf(channel, "show monitor capture buffer %s parameters\n", WIRESHARK_CAPTURE_BUFFER) == EXIT_FAILURE) { + errmsg_print("Can't write to channel"); + return EXIT_FAILURE; + } + if (read_output_bytes(channel, SSH_READ_BLOCK_SIZE, output) == EXIT_FAILURE) + return EXIT_FAILURE; + + output_ptr = g_strstr_len(output, strlen(output), "Packets"); + if (!output_ptr) { + errmsg_print("Error in sscanf()"); + return EXIT_FAILURE; + } else { + sscanf(output_ptr, "Packets : %lu", &got); + } + } + verbose_print("All packets got: dumping\n"); + return EXIT_SUCCESS; +} + +static int parse_line(char* packet _U_, unsigned* offset, char* line, int status) +{ + char** parts; + char** part; + int value; + guint64 size; + + if (strlen(line) <= 1) { + if (status == CISCODUMP_PARSER_IN_PACKET) + return CISCODUMP_PARSER_END_PACKET; + else + return status; + } + + /* we got the packet header */ + /* The packet header is a line like: */ + /* 16:09:37.171 ITA Mar 18 2016 : IPv4 LES CEF : Gi0/1 None */ + if (g_regex_match_simple("^\\d{2}:\\d{2}:\\d{2}.\\d+ .*", line, G_REGEX_CASELESS, G_REGEX_MATCH_ANCHORED)) { + return CISCODUMP_PARSER_IN_HEADER; + } + + /* we got a line of the packet */ + /* A line looks like */ + /*
: <1st group> <2nd group> <3rd group> <4th group> */ + /* ABCDEF01: 01020304 05060708 090A0B0C 0D0E0F10 ................ */ + /* Note that any of the 4 groups are optional and that a group can be 1 to 4 bytes long */ + parts = g_regex_split_simple( + "^[\\dA-Z]{8,8}:\\s+([\\dA-Z]{2,8})\\s+([\\dA-Z]{2,8}){0,1}\\s+([\\dA-Z]{2,8}){0,1}\\s+([\\dA-Z]{2,8}){0,1}.*", + line, G_REGEX_CASELESS, G_REGEX_MATCH_ANCHORED); + + part = parts; + while(*part) { + if (strlen(*part) > 1) { + value = htonl(strtoul(*part, NULL, 16)); + size = strlen(*part) / 2; + memcpy(packet + *offset, &value, size); + *offset += size; + } + part++; + } + return CISCODUMP_PARSER_IN_PACKET; +} + +static void ssh_loop_read(ssh_channel channel, FILE* fp, const long unsigned count) +{ + char line[SSH_READ_BLOCK_SIZE]; + char chr; + unsigned offset = 0; + unsigned packet_size = 0; + char packet[PACKET_MAX_SIZE]; + time_t curtime = time(NULL); + int err; + guint64 bytes_written; + long unsigned packets = 0; + int status = CISCODUMP_PARSER_STARTING; + + do { + if (ssh_channel_read_timeout(channel, &chr, 1, FALSE, SSH_READ_TIMEOUT) == SSH_ERROR) { + errmsg_print("Error reading from channel"); + return; + } + + if (chr != '\n') { + line[offset] = chr; + offset++; + } else { + /* Parse the current line */ + line[offset] = '\0'; + status = parse_line(packet, &packet_size, line, status); + + if (status == CISCODUMP_PARSER_END_PACKET) { + /* dump the packet to the pcap file */ + libpcap_write_packet(fp, curtime, (guint32)(curtime / 1000), packet_size, packet_size, packet, &bytes_written, &err); + verbose_print("Dumped packet %lu size: %u\n", packets, packet_size); + packet_size = 0; + status = CISCODUMP_PARSER_STARTING; + packets++; + } + offset = 0; + } + + } while(packets < count); +} + +static int check_ios_version(ssh_channel channel) +{ + gchar* cmdline = "show version | include Cisco IOS\n"; + gchar version[255]; + guint major = 0; + guint minor = 0; + gchar* cur; + + memset(version, 0x0, 255); + + if (ssh_channel_write(channel, cmdline, (guint32)strlen(cmdline)) == SSH_ERROR) + return FALSE; + if (read_output_bytes(channel, (int)strlen(cmdline), NULL) == EXIT_FAILURE) + return FALSE; + if (read_output_bytes(channel, 255, version) == EXIT_FAILURE) + return FALSE; + + cur = g_strstr_len(version, strlen(version), "Version"); + if (cur) { + cur += strlen("Version "); + sscanf(cur, "%u.%u", &major, &minor); + if ((major > MINIMUM_IOS_MAJOR) || (major == MINIMUM_IOS_MAJOR && minor >= MINIMUM_IOS_MINOR)) { + verbose_print("Current IOS Version: %u.%u\n", major, minor); + if (read_output_bytes(channel, -1, NULL) == EXIT_FAILURE) + return FALSE; + return TRUE; + } + } + + errmsg_print("Invalid IOS version. Minimum version: 12.4, current: %u.%u", major, minor); + return FALSE; +} + +static ssh_channel run_capture(ssh_session sshs, const char* iface, const char* cfilter, const unsigned long int count) +{ + char* cmdline = NULL; + ssh_channel channel; + int ret = 0; + + channel = ssh_channel_new(sshs); + if (!channel) + return NULL; + + if (ssh_channel_open_session(channel) != SSH_OK) + goto error; + + if (ssh_channel_request_pty(channel) != SSH_OK) + goto error; + + if (ssh_channel_change_pty_size(channel, 80, 24) != SSH_OK) + goto error; + + if (ssh_channel_request_shell(channel) != SSH_OK) + goto error; + + if (!check_ios_version(channel)) + goto error; + + if (ssh_channel_printf(channel, "terminal length 0\n") == EXIT_FAILURE) + goto error; + + if (ssh_channel_printf(channel, "monitor capture buffer %s max-size 9500\n", WIRESHARK_CAPTURE_BUFFER) == EXIT_FAILURE) + goto error; + + if (ssh_channel_printf(channel, "monitor capture buffer %s limit packet-count %lu\n", WIRESHARK_CAPTURE_BUFFER, count) == EXIT_FAILURE) + goto error; + + if (cfilter) { + gchar* multiline_filter; + gchar* chr; + + if (ssh_channel_printf(channel, "configure terminal\n") == EXIT_FAILURE) + goto error; + + if (ssh_channel_printf(channel, "ip access-list ex %s\n", WIRESHARK_CAPTURE_ACCESSLIST) == EXIT_FAILURE) + goto error; + + multiline_filter = g_strdup(cfilter); + chr = multiline_filter; + while((chr = g_strstr_len(chr, strlen(chr), ",")) != NULL) { + chr[0] = '\n'; + verbose_print("Splitting filter into multiline\n"); + } + ret = ssh_channel_write(channel, multiline_filter, (uint32_t)strlen(multiline_filter)); + g_free(multiline_filter); + if (ret == SSH_ERROR) + goto error; + + if (ssh_channel_printf(channel, "\nend\n") == EXIT_FAILURE) + goto error; + + if (ssh_channel_printf(channel, "monitor capture buffer %s filter access-list %s\n", + WIRESHARK_CAPTURE_BUFFER, WIRESHARK_CAPTURE_ACCESSLIST) == EXIT_FAILURE) + goto error; + } + + if (ssh_channel_printf(channel, "monitor capture point ip cef %s %s both\n", WIRESHARK_CAPTURE_POINT, + iface) == EXIT_FAILURE) + goto error; + + if (ssh_channel_printf(channel, "monitor capture point associate %s %s \n", WIRESHARK_CAPTURE_POINT, + WIRESHARK_CAPTURE_BUFFER) == EXIT_FAILURE) + goto error; + + if (ssh_channel_printf(channel, "monitor capture point start %s\n", WIRESHARK_CAPTURE_POINT) == EXIT_FAILURE) + goto error; + + if (read_output_bytes(channel, -1, NULL) == EXIT_FAILURE) + goto error; + + if (wait_until_data(channel, count) == EXIT_FAILURE) + goto error; + + if (read_output_bytes(channel, -1, NULL) == EXIT_FAILURE) + goto error; + + cmdline = g_strdup_printf("show monitor capture buffer %s dump\n", WIRESHARK_CAPTURE_BUFFER); + if (ssh_channel_printf(channel, cmdline) == EXIT_FAILURE) + goto error; + + if (read_output_bytes(channel, (int)strlen(cmdline), NULL) == EXIT_FAILURE) + goto error; + + g_free(cmdline); + return channel; +error: + g_free(cmdline); + errmsg_print("Error running ssh remote command"); + read_output_bytes(channel, -1, NULL); + + ssh_channel_close(channel); + ssh_channel_free(channel); + return NULL; +} + +static int ssh_open_remote_connection(const char* hostname, const unsigned int port, const char* username, const char* password, + const char* sshkey, const char* sshkey_passphrase, const char* iface, const char* cfilter, + const unsigned long int count, const char* fifo) +{ + ssh_session sshs; + ssh_channel channel; + FILE* fp = stdout; + guint64 bytes_written = 0; + int err; + int ret = EXIT_FAILURE; + char* err_info = NULL; + + if (g_strcmp0(fifo, "-")) { + /* Open or create the output file */ + fp = fopen(fifo, "w"); + if (!fp) { + errmsg_print("Error creating output file: %s\n", g_strerror(errno)); + return EXIT_FAILURE; + } + } + + sshs = create_ssh_connection(hostname, port, username, password, sshkey, sshkey_passphrase, &err_info); + if (!sshs) { + errmsg_print("Error creating connection: %s", err_info); + goto cleanup; + } + + if (!libpcap_write_file_header(fp, 1, PCAP_SNAPLEN, FALSE, &bytes_written, &err)) { + errmsg_print("Can't write pcap file header"); + goto cleanup; + } + + channel = run_capture(sshs, iface, cfilter, count); + if (!channel) { + ret = EXIT_FAILURE; + goto cleanup; + } + + verbose_print("\n"); + + /* read from channel and write into fp */ + ssh_loop_read(channel, fp, count); + + /* clean up and exit */ + ciscodump_cleanup(sshs, channel, iface, cfilter); + + ret = EXIT_SUCCESS; +cleanup: + if (fp != stdout) + fclose(fp); + verbose_print("\n\n"); + return ret; +} + +static void help(const char* binname) +{ + printf("Help\n"); + printf(" Usage:\n"); + printf(" %s --extcap-interfaces\n", binname); + printf(" %s --extcap-interface=INTERFACE --extcap-dlts\n", binname); + printf(" %s --extcap-interface=INTERFACE --extcap-config\n", binname); + printf(" %s --extcap-interface=INTERFACE --remote-host myhost --remote-port 22222 " + "--remote-username myuser --remote-interface gigabit0/0 " + "--fifo=FILENAME --capture\n", binname); + printf("\n\n"); + printf(" --help: print this help\n"); + printf(" --version: print the version\n"); + printf(" --verbose: print more messages\n"); + printf(" --extcap-interfaces: list the interfaces\n"); + printf(" --extcap-interface : specify the interface\n"); + printf(" --extcap-dlts: list the DTLs for an interface\n"); + printf(" --extcap-config: list the additional configuration for an interface\n"); + printf(" --extcap-capture-filter : the capture filter\n"); + printf(" --capture: run the capture\n"); + printf(" --fifo : dump data to file or fifo\n"); + printf(" --remote-host : the remote SSH host\n"); + printf(" --remote-port : the remote SSH port (default: 22)\n"); + printf(" --remote-username : the remote SSH username (default: the current user)\n"); + printf(" --remote-password : the remote SSH password. If not specified, ssh-agent and ssh-key are used\n"); + printf(" --sshkey : the path of the ssh key\n"); + printf(" --sshkey-passphrase : the passphrase to unlock public ssh\n"); + printf(" --remote-interface : the remote capture interface\n"); + printf(" --remote-filter : a filter for remote capture (default: don't capture data for local interfaces IPs)\n"); +} + +static int list_config(char *interface, unsigned int remote_port) +{ + unsigned inc = 0; + char* ipfilter; + + if (!interface) { + g_fprintf(stderr, "ERROR: No interface specified.\n"); + return EXIT_FAILURE; + } + + if (g_strcmp0(interface, CISCODUMP_EXTCAP_INTERFACE)) { + errmsg_print("ERROR: interface must be %s\n", CISCODUMP_EXTCAP_INTERFACE); + return EXIT_FAILURE; + } + + ipfilter = local_interfaces_to_filter(remote_port); + + printf("arg {number=%u}{call=--remote-host}{display=Remote SSH server address}" + "{type=string}{tooltip=The remote SSH host. It can be both " + "an IP address or a hostname}{required=true}\n", inc++); + printf("arg {number=%u}{call=--remote-port}{display=Remote SSH server port}" + "{type=unsigned}{default=22}{tooltip=The remote SSH host port (1-65535)}" + "{range=1,65535}\n", inc++); + printf("arg {number=%u}{call=--remote-username}{display=Remote SSH server username}" + "{type=string}{default=%s}{tooltip=The remote SSH username. If not provided, " + "the current user will be used}\n", inc++, g_get_user_name()); + printf("arg {number=%u}{call=--remote-password}{display=Remote SSH server password}" + "{type=password}{tooltip=The SSH password, used when other methods (SSH agent " + "or key files) are unavailable.}\n", inc++); + printf("arg {number=%u}{call=--sshkey}{display=Path to SSH private key}" + "{type=fileselect}{tooltip=The path on the local filesystem of the private ssh key}\n", + inc++); + printf("arg {number=%u}{call--sshkey-passphrase}{display=SSH key passphrase}" + "{type=password}{tooltip=Passphrase to unlock the SSH private key}\n", + inc++); + printf("arg {number=%u}{call=--remote-interface}{display=Remote interface}" + "{type=string}{required=true}{tooltip=The remote network interface used for capture" + "}\n", inc++); + printf("arg {number=%u}{call=--remote-filter}{display=Remote capture filter}" + "{type=string}{tooltip=The remote capture filter}", inc++); + if (ipfilter) + printf("{default=%s}", ipfilter); + printf("\n"); + printf("arg {number=%u}{call=--remote-count}{display=Packets to capture}" + "{type=unsigned}{required=true}{tooltip=The number of remote packets to capture.}\n", + inc++); + + g_free(ipfilter); + + return EXIT_SUCCESS; +} + +int main(int argc, char **argv) +{ + int result; + int option_idx = 0; + int i; + char* remote_host = NULL; + unsigned int remote_port = 22; + char* remote_username = NULL; + char* remote_password = NULL; + char* remote_interface = NULL; + char* sshkey = NULL; + char* sshkey_passphrase = NULL; + char* remote_filter = NULL; + unsigned long int count = 0; + int ret = EXIT_SUCCESS; + extcap_parameters * extcap_conf = g_new0(extcap_parameters, 1); + +#ifdef _WIN32 + WSADATA wsaData; + + attach_parent_console(); +#endif /* _WIN32 */ + + extcap_base_set_util_info(extcap_conf, CISCODUMP_VERSION_MAJOR, CISCODUMP_VERSION_MINOR, CISCODUMP_VERSION_RELEASE, NULL); + extcap_base_register_interface(extcap_conf, CISCODUMP_EXTCAP_INTERFACE, "Cisco remote capture", 147, "Remote capture dependent DLT"); + + opterr = 0; + optind = 0; + + if (argc == 1) { + help(argv[0]); + return EXIT_FAILURE; + } + + for (i = 0; i < argc; i++) { + verbose_print("%s ", argv[i]); + } + verbose_print("\n"); + + while ((result = getopt_long(argc, argv, ":", longopts, &option_idx)) != -1) { + + switch (result) { + + case OPT_HELP: + help(argv[0]); + return EXIT_SUCCESS; + + case OPT_VERBOSE: + verbose = TRUE; + break; + + case OPT_VERSION: + printf("%s.%s.%s\n", CISCODUMP_VERSION_MAJOR, CISCODUMP_VERSION_MINOR, CISCODUMP_VERSION_RELEASE); + return EXIT_SUCCESS; + + case OPT_REMOTE_HOST: + g_free(remote_host); + remote_host = g_strdup(optarg); + break; + + case OPT_REMOTE_PORT: + remote_port = (unsigned int)strtoul(optarg, NULL, 10); + if (remote_port > 65535 || remote_port == 0) { + printf("Invalid port: %s\n", optarg); + return EXIT_FAILURE; + } + break; + + case OPT_REMOTE_USERNAME: + g_free(remote_username); + remote_username = g_strdup(optarg); + break; + + case OPT_REMOTE_PASSWORD: + g_free(remote_password); + remote_password = g_strdup(optarg); + memset(optarg, 'X', strlen(optarg)); + break; + + case OPT_SSHKEY: + g_free(sshkey); + sshkey = g_strdup(optarg); + break; + + case OPT_SSHKEY_PASSPHRASE: + g_free(sshkey_passphrase); + sshkey_passphrase = g_strdup(optarg); + memset(optarg, 'X', strlen(optarg)); + break; + + case OPT_REMOTE_INTERFACE: + g_free(remote_interface); + remote_interface = g_strdup(optarg); + break; + + case OPT_REMOTE_FILTER: + g_free(remote_filter); + remote_filter = g_strdup(optarg); + break; + + case OPT_REMOTE_COUNT: + count = strtoul(optarg, NULL, 10); + break; + + case ':': + /* missing option argument */ + errmsg_print("Option '%s' requires an argument", argv[optind - 1]); + break; + + default: + if (!extcap_base_parse_options(extcap_conf, result - EXTCAP_OPT_LIST_INTERFACES, optarg)) { + errmsg_print("Invalid option: %s", argv[optind - 1]); + return EXIT_FAILURE; + } + } + } + + if (optind != argc) { + errmsg_print("Unexpected extra option: %s", argv[optind]); + return EXIT_FAILURE; + } + + if (extcap_base_handle_interface(extcap_conf)) + return EXIT_SUCCESS; + + if (extcap_conf->show_config) + return list_config(extcap_conf->interface, remote_port); + +#ifdef _WIN32 + result = WSAStartup(MAKEWORD(1,1), &wsaData); + if (result != 0) { + if (verbose) + errmsg_print("ERROR: WSAStartup failed with error: %d", result); + return EXIT_FAILURE; + } +#endif /* _WIN32 */ + + if (extcap_conf->capture) { + if (!remote_host) { + errmsg_print("Missing parameter: --remote-host"); + return EXIT_FAILURE; + } + + if (!remote_interface) { + errmsg_print("ERROR: No interface specified (--remote-interface)"); + return EXIT_FAILURE; + } + if (count == 0) { + errmsg_print("ERROR: count of packets must be specified (--remote-count)"); + return EXIT_FAILURE; + } + + ret = ssh_open_remote_connection(remote_host, remote_port, remote_username, + remote_password, sshkey, sshkey_passphrase, remote_interface, + remote_filter, count, extcap_conf->fifo); + } else { + verbose_print("You should not come here... maybe some parameter missing?\n"); + ret = EXIT_FAILURE; + } + + extcap_base_cleanup(&extcap_conf); + return ret; +} + +#ifdef _WIN32 +int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, + LPSTR lpCmdLine, int nCmdShow) { + return main(__argc, __argv); +} +#endif + +/* + * Editor modelines - https://www.wireshark.org/tools/modelines.html + * + * Local variables: + * c-basic-offset: 4 + * tab-width: 4 + * indent-tabs-mode: t + * End: + * + * vi: set shiftwidth=4 tabstop=4 noexpandtab: + * :indentSize=4:tabSize=4:noTabs=false: + */ diff --git a/extcap/ssh-base.c b/extcap/ssh-base.c index 10fefd2577..830485df3d 100644 --- a/extcap/ssh-base.c +++ b/extcap/ssh-base.c @@ -26,6 +26,7 @@ #include #include +#include #define verbose_print(...) { if (verbose) printf(__VA_ARGS__); } @@ -132,6 +133,22 @@ failure: return NULL; } +int ssh_channel_printf(ssh_channel channel, const char* fmt, ...) +{ + gchar* buf; + va_list arg; + int ret = EXIT_SUCCESS; + + va_start(arg, fmt); + buf = g_strdup_vprintf(fmt, arg); + if (ssh_channel_write(channel, buf, (guint32)strlen(buf)) == SSH_ERROR) + ret = EXIT_FAILURE; + va_end(arg); + g_free(buf); + + return ret; +} + void ssh_cleanup(ssh_session* sshs, ssh_channel* channel) { if (*channel) { diff --git a/extcap/ssh-base.h b/extcap/ssh-base.h index 07c3e3305a..a881c0f4ae 100644 --- a/extcap/ssh-base.h +++ b/extcap/ssh-base.h @@ -27,10 +27,24 @@ #include +#define SSH_BASE_OPTIONS \ + { "remote-host", required_argument, NULL, OPT_REMOTE_HOST}, \ + { "remote-port", required_argument, NULL, OPT_REMOTE_PORT}, \ + { "remote-username", required_argument, NULL, OPT_REMOTE_USERNAME}, \ + { "remote-password", required_argument, NULL, OPT_REMOTE_PASSWORD}, \ + { "remote-interface", required_argument, NULL, OPT_REMOTE_INTERFACE}, \ + { "remote-filter", required_argument, NULL, OPT_REMOTE_FILTER}, \ + { "remote-count", required_argument, NULL, OPT_REMOTE_COUNT}, \ + { "sshkey", required_argument, NULL, OPT_SSHKEY}, \ + { "sshkey-passphrase", required_argument, NULL, OPT_SSHKEY_PASSPHRASE} + /* Create a ssh connection using all the possible authentication menthods */ ssh_session create_ssh_connection(const char* hostname, const unsigned int port, const char* username, const char* password, const char* sshkey_path, const char* sshkey_passphrase, char** err_info); +/* Write a formatted message in the channel */ +int ssh_channel_printf(ssh_channel channel, const char* fmt, ...); + /* Clean the current ssh session and channel. */ void ssh_cleanup(ssh_session* sshs, ssh_channel* channel);