mscdex / ssh2

SSH2 client and server modules written in pure JavaScript for node.js
MIT License
5.52k stars 664 forks source link

Socksv5 for multihop with NETCONF subsystem #405

Closed lynchs61 closed 8 years ago

lynchs61 commented 8 years ago

I'm trying to set up a socksv5 connection through a bastion host to open up a NETCONF session on the destination. Is this possible? Looking at the socksv5 example from the README, it looks like the NETCONF session would have to be opened after stream.pipe(clientSocket).pipe(stream), but I think the only event available there is 'close'. Any advice on how to do this?

mscdex commented 8 years ago

I'm not sure I understand what exactly you're trying to do. The socksv5 example just forwards incoming socks client connections to the requested address and port on/from some remote ssh server. The socks client could be a netconf client I suppose. However there is also a netconf ssh subsystem which is a different thing that is a part of the actual remote ssh server.

lynchs61 commented 8 years ago

Ok. The situation is that I have devices that I need to connect to and open up a NETCONF session to send RPC's. I will often need to connect to the devices through a bastion host. I've implemented this idea using connection hopping and it works fine. But I want to give the users the ability to choose socksv5 to perform the same task. Is this possible? If so can you give me an idea of how to accomplish this?

Thank you for the quick response.

mscdex commented 8 years ago

I'm not familiar with netconf. Perhaps you would need to use ssh.subsys('netconf') in the socksv5 example instead of ssh.forwardOut()? It might be helpful to see how you're currently doing the connection hopping.

lynchs61 commented 8 years ago

Yes I was thinking that the subsystem should be started instead of forwardOut. But that didn't make sense to me because then I couldn't figure out how to pipe the clientSocket to the stream.

Here is a minimal example of how I did it with connection-hopping.

var conn1 = new Client();
var conn2 = new Client();

conn1.on('ready', function() {
    conn1.exec("nc <host> 22", function(err, stream) {
        conn2.connect(
            {
                sock: stream,
                username: "<host_username>",
                password: "<host_password>"
            }
        )
    })
});

conn1.connect(
    {
        host: "<bastian_host_address>",
        port: 22,
        username: "<bastian_host_username>",
        password: "<bastian_host_password>"
    }
);

// Example RPC for a Juniper router
var rpc = "<rpc><get-chassis-inventory></get-chassis-inventory></rpc>";

conn2.on('ready', function() {
    conn2.subsys("netconf", function (err, stream) {
        stream.on('data', function (data) {
            // Process response from device
            //...
        });

        stream.write(rpc);
    })
});
mscdex commented 8 years ago

I'm assuming that conn.subsys() should be conn2.subsys()?

mscdex commented 8 years ago

So you're still wanting to do connection hopping but just substituting the host passed to nc with the address (and port?) requested by the socks client?

lynchs61 commented 8 years ago

Yes I fixed the typo: conn -> conn2

lynchs61 commented 8 years ago

And yes I guess that the best to think of it... Substituting the host passed to nc with the address and port requested by the socks client.

The reason for this, and the reason to give the users the ability to specify socks instead of connection hopping, is because what I'm working on can potentially be using the bastion host to connect to many devices. With connection hopping the bastion host will open sessions for every device but with socks all the device connections will go through the single socks session.

I'm not quite seeing however, based on the examples, how I would pass the socks address and port to nc.

mscdex commented 8 years ago

AFAIK it should just be as simple as replacing the conn.forwardOut() with your conn1.exec("nc <host> 22", ...), using info.dstAddr and info.dstPort for the host and port values passed to nc.

lynchs61 commented 8 years ago

I've come up with a way to do this that works except for one problem.

The server is at a relatively high level in the code as is basically the same as your example.

server = socks.createServer(function(info, accept, deny) {
    conn_bastion = new Client();
    conn_bastion.on('ready', function() {
        conn_bastion.forwardOut(
            info.srcAddr,
            info.srcPort,
            info.dstAddr,
            info.dstPort,
            function(err, stream) {
                if (err) {
                    conn_bastion.end();
                    return deny();
                }

                var clientSocket;
                if (clientSocket = accept(true)) {
                    stream.pipe(clientSocket).pipe(stream).on('close', function() {

                    });

                } else {
                    conn_bastion.end();
                }
            });
    }).on('error', function(err) {
        deny();
    }).connect(
        {
            host: bastion_host_info.address,
            port: 22,
            username: bastion_host_info.username,
            password: bastion_host_info.password
        }
    );
}).listen(1080, 'localhost', function() {

}).useAuth(socks.auth.None());

The plan is to use that server for any client to connect to and tunnel through to the device. In my case there can be many clients that need to use the same socks tunnel to get to the device. The client is as follows.

// CLIENT
var conn_device = new Client();
var client = socks.connect(
    {
        host: device.host,
        port: device.port,
        proxyHost: '127.0.0.1',
        proxyPort: 1080,
        auths: [socks.auth.None()]
    },
    function(socket) {
        conn_device.on('ready', function() {
            conn_device.subsys("netconf", function(err, stream) {
                stream.on('data', function(data) {
                    // Parse NETCONF response
                });

                stream.on("close", function (code, signal) {
                    conn.end();
                });

                stream.write(rpc);
            });
        });

        conn_device.connect(
            {
                sock: socket,
                username: device.username,
                password: device.password

            }
        )
    }
)

The problem I'm seeing is that every client creates a new connection to the bastion host. I would expect that there would only be one server that would just be listening for client connections and tunnel them through to the device.

The way I have it set up, each new client will make a new connection to the bastion host instead of using the existing connection to tunnel through to the device. This defeats the purpose since I could just use connection hopping if I wanted a new connection through the bastion host for each device.

Do you have any advice on how I can do this with only one connection to the bastion host and use that to tunnel through for each device?

mscdex commented 8 years ago

Just move the conn_bastion creation and setup outside of the client connection handler and instead just call conn_bastion.forwardOut().

lynchs61 commented 8 years ago

Seems so obvious now you mention it :) This works perfectly! Thanks for your help.