Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: Create a SSH2 Server with abitlity to treat Remote Forwarding #698

Closed
wibimaster opened this issue May 16, 2018 · 15 comments
Closed
Labels

Comments

@wibimaster
Copy link

Hi,

Sorry but I really don't find how to achieve this ; I read some Stackoverflow, some "bugfix" here, this post too : #435

For the moment, I can do this on my client-side :

ssh -R 8100:localsite.tld:80 sub.distantserver.com

All traffic from sub.distantserver.com:8100 is redirect to localsite.tld:80. (port 22, traditional SSH daemon).

My only goal is to achieve this with a Node SSH2 Server on port 21 :

ssh -R 8100:localsite.tld:80 foo@sub.distantserver.com -p 21
password: bar

When it'll work, I can do some check on the user and start some other process ;)

I'm pretty sure I'm not the fist guy who try to do this, can you share a snippet or improve the documentation ?

Many, many thanks if you can help me !

@mscdex
Copy link
Owner

mscdex commented May 16, 2018

The client connection object will emit a 'request' event. After that, it's just a matter calling forwardOut() on the client connection object which can get you a stream representing the TCP connection.

From there you just write whatever data you want to the stream. If you have a real socket (or any other stream really) that is backing this forwarded connection, you could just pipe the two together.

@wibimaster
Copy link
Author

Thanks for your answer, I'll try to precise my current state :

On the server, I have a service running in background, that "proxyfying" some HTTP or WS request on 127.0.0.1:8100.
On the client, I want to receive these requests. So, I start a remote SSH forwarding (ssh -R 8100:localsite.tld:80 sub.distantserver.com).

Now, if I want to do the same process with the Node SSH2 server, I tried something like this :

let fs = require('fs'),
  inspect = require('util').inspect,
  ssh2 = require('ssh2');

new ssh2.Server({
  hostKeys: [fs.readFileSync('/etc/ssh/ssh_host_rsa_key')]
}, client => {
  console.log('Client connected!');
  client
    .on('authentication', ctx => {
      if (
        ctx.method === 'password'
        && ctx.username === 'foo'
        && ctx.password === 'bar'
      ) {
        ctx.accept();
      } else {
        ctx.reject();
      }
    })
    .on('ready', () => {
      console.log('Client authenticated!');
      client
        .on('session', (accept, reject) => {
          let session = accept();
          session.once('exec', (accept, reject, info) => {
            console.log('Client wants to execute: ' + inspect(info.command));
            let stream = accept();
            stream.stderr.write('Oh no, the dreaded errors!\n');
            stream.write('Just kidding about the errors!\n');
            stream.exit(0);
            stream.end();
          });
        })
        .on('request', (accept, reject, name, info) => {
          console.log(info);
          if (name === 'tcpip-forward') {
            accept();
            console.log('Sending incoming tcpip forward');
            client.forwardOut(
              info.bindAddr,
              info.bindPort,
              'which IP here ?',
              'which port here ?',
              (err, stream) => {
                if (err)
                  return;
                stream.end('hello world\n');
              }
            );
          } else {
            reject();
          }
        });
    });
}).listen(21, '0.0.0.0', function() {
  console.log('Listening on port ' + this.address().port);
});

But I don't understand, with the client.forwardOut, how to just start the remote forwarding. If there is any traffic on 127.0.0.1:8100 (server side), it should be redirect to 127.0.0.1:80 (client side) :/

Actually, with this example, on the client side I have this output :

PTY allocation request failed on channel 0
shell request failed on channel 0

And on server side :

Listening on port 21
Client connected!
Client authenticated!
{ bindAddr: 'localhost', bindPort: 8100 }
Sending incoming tcpip forward

@mscdex
Copy link
Owner

mscdex commented May 17, 2018

You need to listen on the port yourself, using net.createServer(), ssh2 will not do that for you. Then whenever you get a socket connection locally on the server, call client.forwardOut() with the appropriate information and pipe between the socket and the stream as I previously described.

@wibimaster
Copy link
Author

Okay, so to reproduce an ssh -R 8100:localhost:80 distant_host, I tried what you say but I just don't understand the arguments of forwardOut :

client.on('request', (accept, reject, name, info) => {
  console.log(info);
  if (name === 'tcpip-forward') {
    accept();
    console.log('tcpip-forward accepted');

    net.createServer(function(socket) {
      socket.setEncoding('utf8');
      socket.on('end', () => {
        console.log('Socket closed');
      });
      socket.on('data', () => {
        console.log('Socket receive data');
      });
      client.forwardOut(
        info.bindAddr, // it's 127.0.0.1
        info.bindPort, // it's 8100
        /* don't know what write here, */
        /* don't know what write here, */
        (err, upstream) => {
          if (err) {
            socket.end();
            return console.error('not working: ' + err);
          }
          console.log('forward data');
          socket.pipe(upstream).pipe(socket);
        });
    }).listen(info.bindPort);// listen on 127.0.0.1:8100
    console.log('created server that listen on port ' + info.bindPort);
  } else {
    reject();
  }
});

The client info give me "127.0.0.1:8100" (the server port to forward), the net socket listen on this, so for me the source and destination is the same :/

Can you explain to me ?

If I do what I did I get an infinite loop when I call distant_server:8100 (seems logic)

Thanks, and sorry to take your time, tell me if I'm bothering you too much :x

@mscdex
Copy link
Owner

mscdex commented May 18, 2018

The 3rd and 4th arguments are the remote address and remote port respectively. You would get those from your socket, since that is the remote party connecting to your bound address and port. This is how the SSH client knows who is connecting.

@wibimaster
Copy link
Author

Hmm, the socket created by net.createServer only give me "127.0.0.1" and "8100", because this is the address it listens...

@mscdex
Copy link
Owner

mscdex commented May 18, 2018

The socket passed to your TCP server's connection callback definitely has the incoming connection's address and port accessible.

@wibimaster
Copy link
Author

My bad, there is :)
But it stay in an infinite loop, and the final client is never called...

        client.on('request', (accept, reject, name, info) => {
          if (name === 'tcpip-forward') {
            accept();
            net.createServer(function(socket) {
              socket.setEncoding('utf8');
              socket.on('end', () => {
                console.log('Socket closed');
              });
              socket.on('data', (data) => {
                console.log('Socket receive data');
                console.log(data);
              });
              client.forwardOut(
                info.bindAddr, info.bindPort,
                socket.remoteAddress, socket.remotePort,
                (err, upstream) => {
                  if (err) {
                    socket.end();
                    return console.error('not working: ' + err);
                  }
                  console.log('forward data', socket.remoteAddress, socket.remotePort, info.bindAddr, info.bindPort);
                  socket.pipe(upstream).pipe(socket);
                });
            }).listen(info.bindPort);// listen on 127.0.0.1:8100

I can see all request on server:8100, but on local client, no call on port 80 (remember ssh -R 8100:[...]:80)

Example of console.log :

Socket receive data
GET / HTTP/1.1
Host: truc.itpoc.io
Host: truc.itpoc.io
X-Forwarded-For: 2a01:cb00:362:9c00:7118:4f37:fe83:948a
X-Forwarded-Host: truc.itpoc.io
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Real-IP: 2a01:cb00:362:9c00:7118:4f37:fe83:948a
X-Real-Port: 2a01:cb00:362:9c00:7118:4f37:fe83:948a
Connection: upgrade
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=7bln8i811a73ipgfrja86j8hqg; gsScrollPos-3122=0

forward data ::ffff:127.0.0.1 34020 localhost 8080
Socket closed

@mscdex
Copy link
Owner

mscdex commented May 19, 2018

Remove your socket event handlers, especially for 'data', which is probably causing all of the data to be read before the forwardOut() completes.

@wibimaster
Copy link
Author

Wow it works ! Thanks !!!
Before closing this I'll write a full example, I think it can be fair for other people searching the same thing :)

@wibimaster
Copy link
Author

let fs = require('fs'),
  inspect = require('util').inspect,
  ssh2 = require('ssh2'),
  net = require('net');

new ssh2.Server({
  hostKeys: [fs.readFileSync('/etc/ssh/ssh_host_rsa_key')]
}, client => {
  console.log('Client connected!');
  client
    .on('authentication', ctx => {
      if (
        ctx.method === 'password'
        && ctx.username === 'foo'
        && ctx.password === 'bar'
      ) {
        ctx.accept();
      } else {
        ctx.reject();
      }
    })
    .on('ready', () => {
      console.log('Client authenticated!');
      client
        .on('session', (accept, reject) => {
          let session = accept();
          session.on('shell', function(accept, reject) {
            let stream = accept();
          });
        })
        .on('request', (accept, reject, name, info) => {
          if (name === 'tcpip-forward') {
            accept();
            net.createServer(function(socket) {
              socket.setEncoding('utf8');
              client.forwardOut(
                info.bindAddr, info.bindPort,
                socket.remoteAddress, socket.remotePort,
                (err, upstream) => {
                  if (err) {
                    socket.end();
                    return console.error('not working: ' + err);
                  }
                  upstream.pipe(socket).pipe(upstream);
                });
            }).listen(info.bindPort);
          } else {
            reject();
          }
        });
    });
}).listen(21, '0.0.0.0', function() {
  console.log('Listening on port ' + this.address().port);
});

@Sammons
Copy link

Sammons commented May 19, 2018

can this work for unix sockets? I have a project where I use ssh2 for everything but piping i/o from the remote docker.sock through a local docker.sock handle
ssh -nNT -L $(pwd)/docker.sock:/var/run/docker.sock user@ip

@mscdex
Copy link
Owner

mscdex commented May 19, 2018

@Sammons -L is the other direction, but yes, you can listen for the 'openssh.streamlocal' on the connection object as described in the documentation. It's similar to the 'tcpip' event, but for UNIX domain sockets.

@Sammons
Copy link

Sammons commented May 19, 2018

@mscdex thanks! Kudos on this lib by the way, its really fast and is making things really easy today

@mscdex
Copy link
Owner

mscdex commented May 20, 2018

@wibimaster It would probably be better to avoid automatically calling accept() for 'tcpip-forward' requests until you've actually tried to bind to the port (and address). If something else is already listening on that port, it'd be a good idea to let the client know their request is not possible by rejecting it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants