Run perltidy on debug tools
[transportpce.git] / debug_tools / netconf_TCP_SSH_hijackingproxy.pl
1 #!/usr/bin/env perl
2 ##############################################################################
3 #Copyright (c) 2017 Orange, Inc. and others.  All rights reserved.
4 #
5 # This program and the accompanying materials are made available under the
6 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
7 # and is available at http://www.eclipse.org/legal/epl-v10.html
8 ##############################################################################
9 #
10 # debian dependencies: apt-get install libnet-openssh-perl libio-pty-perl
11 #
12
13 use strict;
14 use warnings;
15
16 #use diagnostics;  #uncomment this line for more details when encountering warnings
17 use Net::OpenSSH;
18 use FileHandle;
19 use Getopt::Long qw(:config no_ignore_case bundling);
20
21 use IO::Socket;
22 use Net::hostent;
23
24 my (
25     $host,        $help,         $usage,         $proxy_port,
26     $login,       $password,     $kidpid,        $ssh_subsocket,
27     $simpleproxy, $pid,          $ssh_handle,    $client,
28     $server,      $capabilities, $hello_message, $verbose
29 );
30
31 GetOptions(
32     "h|help"           => \$help,
33     "p|port=i"         => \$proxy_port,
34     "s|simpleproxy"    => \$simpleproxy,
35     "v|verbose"        => \$verbose,
36     "C|capabilities=s" => \$capabilities
37 );
38 $usage = "
39 USAGE: netconf_TCP_SSH_hijackproxy.pl [-h|--help] [-p|--port <port_number>] [-s|--simpleproxy] [-v|--verbose] [-C|--capabilities <custom_hello_file.xml>] <[login[:password]@]host[:port]> [login] [password]
40
41 Netconf SSH to TCP proxy to debug netconf exchanges.
42 It listens to connections in clear TCP to the given port. When a TCP connection demand is received,
43 it establishes a netconf SSH encrypted connection to the host in argument. Netconf rpcs and replies
44 are then proxified between both ends.
45 By default, exchanges are altered according to the rules specified inside this script and easily
46 modifiable. This behaviour can be disabled with the '-s' option.
47 For more convenience, the server hello handshake can also alternatively be replaced by the content
48 of an external file instead of writing specific rules.
49
50 OPTIONS :
51
52         -h or --help             print this help
53         -p or --port             use the given port number for listening TCP clients, default=9000
54         -s or --simpleproxy      simple proxy mode, do not alter any exchanges
55         -v or --verbose          display exchanges to STDOUT
56         -C or --capabilities     do not relay the real server hello message to the client
57                                  but replace it by the one provided in the following file
58
59
60 ";
61
62 if ($help) {
63     print $usage;
64     exit(0);
65 }
66
67 unless ( @ARGV >= 1 ) {
68     print $usage;
69     exit(0);
70 }
71
72 ( $host, $login, $password ) = @ARGV;
73
74 #netconf default port is no 22 but 830
75 if ( $host !~ /:[0-9]+$/ ) { $host .= ':830'; }
76
77 if ( !defined($proxy_port) ) { $proxy_port = 9000; }
78
79 my $connection_string = $host;
80 if ($password) {
81     $connection_string = $login . ":" . $password . "@" . $connection_string;
82 }
83 elsif ($login) {
84     $connection_string = $login . "@" . $connection_string;
85 }
86
87 #retrieving hello custom file if any
88 if ( ( !defined($simpleproxy) ) && ( defined($capabilities) ) ) {
89     open( CAPABILITIES, '<', $capabilities )
90       or die("can not open $capabilities");
91     while (<CAPABILITIES>) {
92         $hello_message .= $_;
93     }
94     chop $hello_message;    # removing EOF
95     $hello_message .= "]]>]]>";
96     close(CAPABILITIES);
97 }
98
99 # the following regex are used to modify some part of the server messages relayed to the client
100 # you can adapt it to your needs, some examples have been commented.
101 my %regex_hash = (
102
103 # replace oo-device v1.2 by v1.2.1
104 #   'module=org-openroadm-device&amp;revision=2016-10-14.*<\/capability>'=>'s/&amp;revision=2016-10-14/&amp;revision=2017-02-06/',
105 #   '<schema><identifier>org-openroadm-device<\/identifier><version>2016-10-14'=>'s@<schema><identifier>org-openroadm-device</identifier><version>2016-10-14@<schema><identifier>org-openroadm-device</identifier><version>2017-02-06@',
106 # remove all deviations found
107 #   '&amp;deviations=.*<\/capability>'=>'s@&amp;deviations=.*</capability>@</capability>@',
108 # add the ietf-netconf capability to the hello handshake - without it, ODL netconf mountpoints can not work
109 #    '<\/capabilities>'=>'s@</capabilities>@\n<capability>urn:ietf:params:xml:ns:yang:ietf-netconf?module=ietf-netconf&amp;revision=2011-06-01</capability>\n</capabilities>@',
110 # add the right notifications capabilities to the hello handshake + provide another solution for the ietf-netconf capability
111     '<\/capabilities>' =>
112 's@</capabilities>@\n<capability>urn:ietf:params:xml:ns:netmod:notification?module=nc-notifications&amp;revision=2008-07-14</capability>\n<capability>urn:ietf:params:xml:ns:netconf:notification:1.0?module=notifications&amp;revision=2008-07-14</capability>\n<capability>urn:ietf:params:xml:ns:netconf:base:1.0?module=ietf-netconf&amp;revision=2011-06-01</capability>\n</capabilities>@'
113 );
114
115 if ( defined($simpleproxy) ) { %regex_hash = (); }
116
117 my %compiled_regex_hash;
118 foreach my $keyword ( keys %regex_hash ) {
119     eval( '$compiled_regex_hash{$keyword}= qr/' . $keyword . '/;' );
120 }
121
122 $server = IO::Socket::INET->new(
123     Proto     => 'tcp',
124     LocalPort => $proxy_port,
125     Listen    => SOMAXCONN,
126     Reuse     => 1
127 );
128 die "can't setup server" unless $server;
129 print STDERR "[Proxy server $0 accepting clients: Ctrl-C to stop]\n";
130
131 while ( $client = $server->accept() ) {
132     $client->autoflush(1);
133     my $hostinfo = gethostbyaddr( $client->peeraddr );
134     printf STDERR "[Incoming connection from %s]\n",
135       $hostinfo->name || $client->peerhost;
136
137     print STDERR "[relaying to " . $connection_string . "]\n";
138
139     $ssh_handle = Net::OpenSSH->new(
140         $connection_string,
141         master_opts         => [ -o => 'StrictHostKeyChecking=no' ],
142         timeout             => 500,
143         kill_ssh_on_timeout => 500
144     );
145
146     #netconf requires a specific socket
147     ( $ssh_subsocket, $pid ) =
148       $ssh_handle->open2socket( { ssh_opts => '-s' }, 'netconf' );
149     die "can't establish connection: exiting\n" unless defined($ssh_subsocket);
150
151     print STDERR "[Connected]\n";
152
153     # split the program into two processes, identical twins
154     die "can't fork: $!" unless defined( $kidpid = fork() );
155
156     $| = 1;
157
158 # the if{} block runs only in the parent process (server output relayed to the client)
159     if ( !$kidpid ) {
160
161         # copy the socket to standard output
162         my $buf;
163
164         if ( defined($hello_message) ) {
165
166             #retrieve the server hello but do not relay it
167             while ( my $nread = sysread( $ssh_subsocket, $buf, 400 ) ) {
168                 $ssh_subsocket->flush();
169                 if ( $buf =~ /]]>]]>/ ) { last }
170             }
171
172             #send a custom hello message instead
173             print $client $hello_message;
174             if ( defined($verbose) ) { print STDOUT $hello_message; }
175         }
176
177 #while (<$ssh_subsocket>) {
178 #buffer seems not totally flushed when using the usual syntax above (nor when using autoflush)
179         while ( my $nread = sysread( $ssh_subsocket, $buf, 400 ) ) {
180             foreach my $keyword ( keys %regex_hash ) {
181                 if ( $buf =~ $compiled_regex_hash{$keyword} ) {
182                     print STDERR 'found regex '
183                       . $keyword
184                       . ": replacing '\n"
185                       . $buf
186                       . "\n' by '\n";
187                     eval( '$buf =~ ' . $regex_hash{$keyword} . ';' );
188                     print STDERR $buf . "\n'\n";
189                 }
190             }
191             print $client $buf;
192             $ssh_subsocket->flush();
193             if ( defined($verbose) ) { print STDOUT $buf; }
194
195         }
196
197         kill( "TERM", $kidpid );    # send SIGTERM to child
198     }
199
200 # the else{} block runs only in the child process (client input relayed to the server)
201     else {
202
203         $ssh_subsocket->autoflush(1);
204         sleep 1;    # wait needed for ensuring STDOUT buffer is not melt
205         my $buf;
206
207         #while (defined (my $buf = <$client>)) {
208         #usual syntax above used in verbose mode results into flush problems
209         while ( my $nread = sysread( $client, $buf, 400 ) ) {
210             print $ssh_subsocket $buf;
211             $client->flush();
212             if ( defined($verbose) ) { print STDOUT $buf; }
213         }
214         continue { }
215
216         close $client;
217
218     }
219
220     $| = 0;
221
222     sleep 2;
223     kill( "TERM", $kidpid );    # send SIGTERM to child
224
225 }
226
227 exit;