Paramiko server port forward with openssh client -N option - python

I am attempting to build a Paramiko server that just forwards ports. I adapted the code from the demo server code
#!/usr/bin/env python
import base64
from binascii import hexlify
import os
import socket
import sys
import threading
import traceback
import paramiko
from paramiko.py3compat import b, u, decodebytes
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
host_key = paramiko.RSAKey(filename="test_rsa.key")
logger.info("Read key: " + u(hexlify(host_key.get_fingerprint())))
class Server(paramiko.ServerInterface):
def __init__(self):
self.event = threading.Event()
def check_auth_publickey(self, username, key):
logger.info("Auth attempt with key: " + u(hexlify(key.get_fingerprint())))
try:
with open("client_rsa.pub.stripped", "rb") as f:
good_key = f.read()
good_pub_key = paramiko.RSAKey(data=decodebytes(good_key))
except:
logger.exception("failed to read public key")
return paramiko.AUTH_FAILED
if (username == "robey") and (key == good_pub_key):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
def get_allowed_auths(self, username):
return "publickey"
def check_channel_request(self, kind, chanid):
logger.info("inside channel request")
return paramiko.OPEN_SUCCEEDED
def check_channel_direct_tcpip_request(self, chanid, origin, destination):
return paramiko.OPEN_SUCCEEDED
def check_channel_shell_request(self, channel):
self.event.set()
return True
if __name__ == "__main__":
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("", 2200))
sock.listen(100)
logger.info("Listening for connection ...")
client, addr = sock.accept()
logger.info("Got a connection!")
with paramiko.Transport(client) as t:
t.load_server_moduli()
t.add_server_key(host_key)
server = Server()
t.start_server(server=server)
# wait for auth
chan = t.accept(20)
if chan is None:
logger.info("*** No channel.")
sys.exit(1)
logger.info("Authenticated!")
# prompt for more information
chan.send("Username: ")
f = chan.makefile("rU")
username = f.readline().strip("\r\n")
logger.info("received username: " + username)
chan.close()
And I am using this command to connect successfully:
ssh -i client_rsa.key -p 2200 -L 9999:localhost:4000 -T robey#localhost
However, when I attempt to use the -N option for the ssh client, ie:
ssh -i client_rsa.key -p 2200 -L 9999:localhost:4000 -T -N robey#localhost
the Paramiko server hangs after authenticating the client, never reaching the check_channel_request function. Here are the logs from the run:
INFO:__main__:Read key: 689f8799e649f931b116b19227dbb2a3
INFO:__main__:Listening for connection ...
INFO:__main__:Got a connection!
INFO:paramiko.transport:Connected (version 2.0, client OpenSSH_7.2p2)
INFO:paramiko.transport:Auth rejected (none).
INFO:__main__:Auth attempt with key: cdbb2439816b22a59ee036be3a953e51
INFO:paramiko.transport:Auth rejected (publickey).
INFO:__main__:Auth attempt with key: 11c470c88233719a2499f03336589618
INFO:paramiko.transport:Auth granted (publickey).
Is there anyway to get the Paramiko server to be able to handle this situation?

Figured this out. The reason nothing was happening is that the tunnel forwarding is not opened until you try to use it. It turns out my tunnel wasn't being created even without the -N option. So the answer is to make sure to use the local port after creating the SSH connection.

Related

Paramiko. Reverse Forward Tunnel Question- Help Appreciated

So I've been working with the Paramiko Libary. I have a client and two servers Server A & Server B. The client connects to Server A, and then requests a reverse-forwarded tunnel to Server B, there is a lot more functionality to write into it but my problem at the moment is very fundamental and likely has a very simply answer I'm just somehow overlooking or not understanding.
What I am trying to do at this point is have Server A send some information to Server B every time it connects to it, which due to a timer on the client should after a connection is closed be each minute. (reconnecting each time)
I want to have Server A send, Server B some information every time it connects to it. Now my question relates to how I'd achieve that.
My first thought was to have the client send a command to Server A after the reverse tunnel is connected, I suspect and here my understanding may be wrong, thus why I'm checking here. The command (which is a string) will be forwarded by Server A to Server B, whilst I am looking for the response to that command to be sent Server B.
The other option as I see it is to have Server A push the data to Server B. But I don't know how to check for when a reverse-forwarded-tunnel is created, I could do it for any connection but then that seems inefficient, as the client will get some data, then the data will once again be sent as the reverse forward tunnel is created. (Again likely overlooking something simple here)
So I'm curious given my code's present state what could I do, could change that would let me check for when a reverse-forward-tunnel is made to Server B so I can send the data I want to send to it.
Thank you for taking the time to try and help me here, and yes I understand hardcoding in passwords etc is a bad idea for application security.
The code below is the client & server code (which again need some work but are getting there.)
client Code
import getpass
import os
import socket
import select
import sys
import threading
import paramiko
from paramiko import Channel
import schedule
import time
import string
import random
from optparse import OptionParser
IP = '127.0.0.1'
USER = 'user'
PASSWORD = 'CrabRave'
PORT = 900
REMOTE_PORT = 443
REMOTE_IP = ... ###Remote IP will go here.
def handler(chan, host, port):
sock = socket.socket()
try:
sock.connect((IP, PORT))
except Exception as e:
Timer()
def ssh_client(IP, PORT, USER, PASSWORD):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(IP, PORT, USER, PASSWORD)
ssh_session = client.get_transport().open_session()
def reverse_forward_tunnel(PORT, REMOTE_IP, REMOTE_PORT, transport):
transport.request_port_forward("", PORT)
while True:
chan = transport.accept(1000)
if chan is None:
continue
thr = threading.Thread(
target=handler, args=(chan, REMOTE_IP, REMOTE_PORT))
thr.setDaemon(True)
thr.start()
def Timer():
if Channel.is_active():
schedule.every(1).seconds.do(Timer)
else: schedule.every(1).minutes.do(main)
def main():
client = ssh_client
try:
ssh_client(IP, PORT, USER, PASSWORD)
except Exception as E:
Timer()
try:
reverse_forward_tunnel(PORT, REMOTE_IP, REMOTE_PORT, client.get_transport())
except KeyboardInterrupt:
Timer()
try: Timer()
except Exception as E:
Timer
if __name__ == '__main__':
main()
Server Code
from ABC import abstractmethod
from sys import platform
from Shell import Shell
from cmd import Cmd
from src.server_base import ServerBase
from src.ssh_server_interface import SshServerInterface
from src.shell import Shell
from src.ssh_server import SshServer
import base64
import os
import socket
import sys
import paramiko
import threading
import string
import random
my_key = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(100))
class Shell(Cmd):
use_rawinput=False
promt='My Shell> '
def __init__(self, stdin=None, stdout=None):
super(Shell, self).__init__(completkey='tab', stdin=stdin, stdout =stdout)
def print(self, value):
if self.stdout and not self.stdout.closed:
self.stdout.write(value)
self.stdout.flush()
def printline(self, value):
self.print(value + '\r\n')
def emptyline(self):
self.print('\r\n')
class ServerBase(ABC):
def __init__(self):
self._is_running = threading.Event()
self._socket = None
self.client_shell = None
self._listen_thread = None
def start(self, address='127.0.0.1', port=900, timeout=1):
if not self._is_running.is_set():
self._is_running.set()
self._socket(socket.AF_INET, socket.SOCK_STREAM)
if platform == "linux" or platform == "linux2":
self._socket.setsocketopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, True)
self._socket.settimeout(timeout)
self._socket.bind((address, port))
self._listen_thread = threading.Thread(target=self._listen)
self._listen_thread.start()
def stop(self):
if self._is_running.is_set():
self._is_running.clear()
self._listen_thread.join()
self._socket.close()
def listen(self):
while self._is_running.is_set():
try:
self._socket.listen()
client, addr = self._socket.accept()
self.connection_function(client)
except socket.timeout:
pass
#abstractmethod
def connection_function(self, client):
pass
class SshServerInterface(paramiko.ServerInterface):
def check_channel_request(self, kind, chanid):
if kind == "session":
return paramiko.OPEN_SUCCEEDED
def check_auth_password(self, username: str, password: str) -> int:
if (username == "user") and (password == "CrabRave"):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
def check_channel_pty_request(self, channel: Channel, term: bytes, width: int, height: int, pixelwidth: int, pixelheight: int, modes: bytes):
return True
def check_channel_shell_request(self, channel: Channel) -> bool:
return True
def check_channel_env_request(self, channel: Channel, name: bytes, value: bytes) -> bool:
return True
def check_port_forward_request(self, address: str, port: int) -> int:
return port
class SshServer(ServerBase):
def __init__(self, host_key_file, host_key_file_password=None):
super(SshServer, self).__init__()
self._host_key = paramiko.RSAKey.from_private_key_file(StringIO.StringIO(my_key))
def connection_function(self, client):
try:
session = paramiko.Transport(client)
session.add_server_key(self._host_key)
server = SshServerInterface()
try:
session.start_server(server=server)
except paramiko.SSHException:
return
channel = session.accept()
stdio = channel.makefile('rwU')
self.client = Shell(stdio, stdio)
self.client_shell.cmdloop()
session.close()
except:
pass
if __name__ == '__main__':
server = SshServer(my_key)
server.start()

How to make paramiko SSH server execute cammands?

I'm trying to write an SSH server and everything is fine but the problem seems that I cant make client to execute commands on the server as normal and can't find correct way to do it since there is no mention of it in the documentation and can't see a demo example of how to make server to accept connections so I'm completely lost in this area. code is:
#!/bin/python3
import paramiko
import socket
class Ctx(paramiko.server.ServerInterface):
def get_allowed_auths(self, username): return "password,publickey"
def check_auth_publickey(self, key): return paramiko.AUTH_SUCCESSFUL
def check_channel_request(self, kind, channelID): return paramiko.OPEN_SUCCEEDED
def check_channel_shell_request(self, channel): return True
def check_channel_pty_request(self, c, t, w, h, p, ph, m): return True
def get_banner(self): return ("This is MY SSH Server\n\r", "EN")
def check_channel_exec_request(self, channel, command):
print(command) # Print command
self.event.set() # I dont know why this is used.
return True # return True to accept command exec request
def check_auth_password(self, username, password):
if password == "1999": return paramiko.AUTH_SUCCESSFUL
else: return paramiko.AUTH_FAILED
paramiko.util.log_to_file("demo_server.log") # setup log file
host_key = paramiko.RSAKey(filename="./rsa") # setup rsa key file that will be used during authnitication
ctx = Ctx() # create ServerInterface context object
sock = socket.socket() # Create socket object
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("127.0.0.1", 5555)) # bind socket to specific Port
sock.listen(100) # Listen for TCP connections
print("***************** Listening for connection **************************")
client, addr = sock.accept() # accept TCP socket connection
print("[+]***************** Listeing for SSH connections ***************** ")
server = paramiko.Transport(client)
server.add_server_key(host_key) # Setup key
server.start_server(server=ctx) # SSH start_server
channel = server.accept(30) # Accept Auth requests
if channel is None:
print("[+] ***************** No Auth request Was made. ***************** ")
exit(1)
channel.send("[+]***************** Welcome ***************** \n\r")
while True: # This is supposed to be used to listen to commands
channel.event.wait(5) # but I'm not sure what it does actually
As you can see from the output of your print statement in check_channel_exec_request, you are receiving a command name. You just need to execute the command and send the output to the client. One implementation of that might look like:
def check_channel_exec_request(self, channel, command):
try:
res = subprocess.run(command, shell=True, stdout=subprocess.PIPE)
channel.send(res.stdout)
channel.send_exit_status(res.returncode)
except Exception as err:
print('exception: {}'.format(err))
channel.send('An error occurred: {}\r\n'.format(err))
channel.send_exit_status(255)
finally:
self.event.set()
return True
This uses subprocess.run(...) to execute the command and then sends
the output to the client. There are several limitations to this
implementation...
It's not interactive (the output isn't returned to the client until
after the command is complete).
It doesn't handle command output on stderr
...but hopefully it's enough to get you started.
Another problem with your code is with your treatment of
client.event. This is a Python Event object, used for signaling between threads. When you write:
channel.event.wait(5)
You are saying "wait up to 5 seconds for the Event to be set". An
Event becomes set by something calling event.set(), which you can
see we are doing in check_channel_exec_request.
The way you're using this doesn't make sense, by writing:
while true:
channel.event.wait(5)
You have an infinite loop. You want something that will wait for the
command to execute and then close the channel, so maybe something
like:
channel.event.wait(30)
channel.close()
This means "wait up to 30 seconds for the command to complete, and
even if it doesn't, close the channel".
With these two changes, your code will accept a single command and
exit. If you want the server to keep running so that you can connect
to it multiple times, you will need to implement some sort of loop in
the main section of your code.
Here's the code with all the changes I suggested:
#!/bin/python3
import paramiko
import socket
import subprocess
import time
class Ctx(paramiko.server.ServerInterface):
def get_allowed_auths(self, username):
return "password,publickey"
def check_auth_publickey(self, key):
return paramiko.AUTH_SUCCESSFUL
def check_channel_request(self, kind, channelID):
return paramiko.OPEN_SUCCEEDED
def check_channel_shell_request(self, channel):
return True
def check_channel_pty_request(self, c, t, w, h, p, ph, m):
return True
def get_banner(self):
return ("This is MY SSH Server\n\r", "EN")
def check_channel_exec_request(self, channel, command):
try:
res = subprocess.run(command, shell=True, stdout=subprocess.PIPE)
channel.send(res.stdout)
channel.send_exit_status(res.returncode)
except Exception as err:
print('exception: {}'.format(err))
channel.send('An error occurred: {}\r\n'.format(err))
channel.send_exit_status(255)
finally:
self.event.set()
return True
def check_auth_password(self, username, password):
if password == "1999": return paramiko.AUTH_SUCCESSFUL
else: return paramiko.AUTH_FAILED
paramiko.util.log_to_file("demo_server.log") # setup log file
host_key = paramiko.RSAKey(
filename="./test_rsa.key"
) # setup rsa key file that will be used during authnitication
ctx = Ctx() # create ServerInterface context object
sock = socket.socket() # Create socket object
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("127.0.0.1", 5555)) # bind socket to specific Port
sock.listen(100) # Listen for TCP connections
print("***************** Listening for connection **************************")
client, addr = sock.accept() # accept TCP socket connection
print("[+]***************** Listening for SSH connections ***************** ")
server = paramiko.Transport(client)
server.add_server_key(host_key) # Setup key
server.start_server(server=ctx) # SSH start_server
channel = server.accept(30) # Accept Auth requests
if channel is None:
print("[+] ***************** No Auth request Was made. ***************** ")
exit(1)
channel.send("[+]***************** Welcome ***************** \n\r")
# wait for command execution to complete (or timeout)
channel.event.wait(30) # but I'm not sure what it does actually
channel.close()
Update 1
Just to be clear, this doesn't get you an interactive session. This lets you run a command like this:
$ ssh -p 5555 localhost date
This is MY SSH Server
lars#localhost's password:
[+]***************** Welcome *****************
Sun Aug 15 09:35:53 AM EDT 2021
Connection to localhost closed by remote host.
If you want to enable an interactive session, check_channel_exec_request is not what you want.
does this mean that I have to open a new channel for each command, Is this is how it's supposed to be done with SSH or I can just use the wait in loop so that only one channel for all upcoming commands.
Using this model, with check_channel_exec_request, you would need a new connection for each command. The main section of your code would look like:
while True:
print("***************** Listening for connection **************************")
client, addr = sock.accept() # accept TCP socket connection
print("[+]***************** Listening for SSH connections ***************** ")
server = paramiko.Transport(client)
[...]
This isn't the only way of handling things, of course, and if you look
around you can find a number of examples of Paramiko-based services
that might help out. For example, ShuSSH shows a non-trivial Paramiko server implementation.
I know you already did an "early acceptance" of an answer, but you might take a look at the following, which is based on this answer on SO that has been modified as follows:
Uses threading to support concurrent SSH requests.
Recognizes an "exit" command to terminate the program since the code to handle ctrl-C for termination is less than ideal. Set constant SUPPORT_EXIT = False to remove this support.
The program just currently logs the command and echoes it back to the user.
Example of use:
ssh localhost -p 5555 some-command
The code:
#!/usr/bin/env python
import logging
import socket
import sys
import threading
from queue import Queue
import paramiko
logging.basicConfig()
paramiko.util.log_to_file('demo_server.log', level='INFO')
logger = paramiko.util.get_logger("paramiko")
host_key = paramiko.RSAKey(filename='./rsa')
SUPPORT_EXIT = True
# input queue of requests:
in_q = Queue()
class Server(paramiko.ServerInterface):
def __init__(self):
self.event = threading.Event()
def check_channel_request(self, kind, chanid):
if kind == 'session':
return paramiko.OPEN_SUCCEEDED
def check_auth_password(self, username, password):
if password == '9999':
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
def get_allowed_auths(self, username):
return 'publickey,password'
def check_channel_exec_request(self, channel, command):
# This is the command we need to parse
# Here we just log it and echo it back to the user:
command = command.decode() # convert to string from bytes:
logger.info('Command = %s', command)
channel.send(command + '\n')
if SUPPORT_EXIT and command == 'exit':
# Place None in in_q to signify time to exit:
in_q.put(None)
self.event.set()
return True
def run_server(client):
t = paramiko.Transport(client)
t.set_gss_host(socket.getfqdn(""))
t.load_server_moduli()
t.add_server_key(host_key)
server = Server()
t.start_server(server=server)
# Wait 30 seconds for a command
server.event.wait(30)
t.close()
def accept(sock):
while True:
try:
client, _ = sock.accept()
except Exception as exc:
logger.error(exc)
else:
in_q.put(client)
def listener():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 5555))
sock.listen(100)
threading.Thread(target=accept, args=(sock,), daemon=True).start()
while True:
try:
client = in_q.get()
if SUPPORT_EXIT and client is None: # exit command issued
break
threading.Thread(target=run_server, args=(client,), daemon=True).start()
except KeyboardInterrupt:
sys.exit(0)
if __name__ == '__main__':
listener()
My previous answer is fine for executing a single command. This new version supports the following variations:
ssh ip-address -p 5555 -T - Creates an interactive session. For now each input line is just echoed back and logged until 'quit\n' is entered.
ssh ip-address -p 5555 some-command - Executes the single command some-command, but for now that consists of just echoing back the command and logging it.
ssh ip-address -p 5555 exit - shuts down the server if SUPPORT_EXIT = True is set in the source.
#!/usr/bin/env python
import logging
import socket
import sys
import threading
from queue import Queue
import paramiko
logging.basicConfig()
paramiko.util.log_to_file('demo_server.log', level='INFO')
logger = paramiko.util.get_logger("paramiko")
host_key = paramiko.RSAKey(filename='./rsa')
SUPPORT_EXIT = True
# input queue of requests:
in_q = Queue()
def my_processor(stdin, stdout, event):
stdout.write('This is MY SSH Server:\n\n')
for command in stdin:
if command == 'quit\n':
break
# Just log the command and send it back:
logger.info('Command = %s', command)
stdout.write(command)
# signal termination
event.set()
class Server(paramiko.ServerInterface):
def __init__(self):
self.event = threading.Event()
def check_channel_request(self, kind, chanid):
if kind == 'session':
return paramiko.OPEN_SUCCEEDED
def check_auth_password(self, username, password):
if password == '9999':
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
def get_allowed_auths(self, username):
return 'publickey,password'
def check_channel_exec_request(self, channel, command):
# This is the command we need to parse
command = command.decode() # convert to string from bytes:
if SUPPORT_EXIT and command == 'exit':
# Place None in in_q to signify time to exit:
in_q.put(None)
# We just log it and echo it back to the user:
logger.info('Command = %s', command)
channel.send(command + '\n')
self.event.set() # Command execution complete
# Show command successfully "wired up" to stdin, stdout and stderr:
# Return False if invalid command:
return True
def check_channel_shell_request(self, channel):
""" No command specified, interactive session implied """
stdout = channel.makefile('w')
stdin = channel.makefile('r')
threading.Thread(target=my_processor, args=(stdin, stdout, self.event), daemon=True).start()
# Show command successfully "wired up" to stdin, stdout and stderr:
return True
def run_server(client):
t = paramiko.Transport(client)
t.set_gss_host(socket.getfqdn(""))
t.load_server_moduli()
t.add_server_key(host_key)
server = Server()
t.start_server(server=server)
# wait for termination:
server.event.wait()
t.close()
def accept(sock):
while True:
try:
client, _ = sock.accept()
except Exception as exc:
logger.error(exc)
else:
in_q.put(client)
def listener():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 5555))
sock.listen(100)
threading.Thread(target=accept, args=(sock,), daemon=True).start()
while True:
try:
client = in_q.get()
if SUPPORT_EXIT and client is None: # exit command issued
break
threading.Thread(target=run_server, args=(client,), daemon=True).start()
except KeyboardInterrupt:
sys.exit(0)
if __name__ == '__main__':
listener()

How can I create a Python SSL Client/Server pair where only the server authenticates the client

I have been trying to get a simple Python SSL example working for a day now with no luck. I want to create an SSL server and SSL client. The server should authenticate the client. The Python docs are pretty light on examples for the SSL module, and in general I can't find many working examples. The code I am working with is as follows;
Client:
import socket
import ssl
class SSLClient:
def __init__(self, server_host, server_port, client_cert, client_key):
self.server_host = server_host
self.server_port = server_port
self._context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
self._context.load_cert_chain(client_cert, client_key)
self._sock = None
self._ssock = None
def __del__(self):
self.close()
def connect(self):
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._ssock = self._context.wrap_socket(
self._sock, server_hostname=self.server_host
)
self._ssock.connect((self.server_host, self.server_port))
def send(self, msg):
self._ssock.send(msg.encode())
def close(self):
self._ssock.close()
Server:
import socket
import ssl
from threading import Thread
class SSLServer:
def __init__(self, host, port, cafile, chunk_size=1024):
self.host = host
self.port = port
self.chunk_size = chunk_size
self._context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
self._context.load_verify_locations(cafile)
self._ssock = None
def __del__(self):
self.close()
def connect(self):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
sock.bind((self.host, self.port))
sock.listen(5)
with self._context.wrap_socket(sock, server_side=True) as self._ssock:
conn, _ = self._ssock.accept()
while True:
data = conn.recv(self.chunk_size).decode()
print(data)
if data is None:
break
def close(self):
self._ssock.close()
class SSLServerThread(Thread):
def __init__(self, server):
super().__init__()
self._server = server
self.daemon = True
def run(self):
self._server.connect()
def stop(self):
self._server.close()
Test script:
import client, server
from os import path
from time import sleep
server_host = "localhost"
server_port = 11234
client_cert = path.join(path.dirname(__file__), "client.crt")
client_key = path.join(path.dirname(__file__), "client.key")
s = server.SSLServer(server_host, server_port, client_cert)
s_thread = server.SSLServerThread(s)
s_thread.start()
sleep(2)
c = client.SSLClient(server_host, server_port, client_cert, client_key)
c.connect()
c.send("This is a test message!")
c.close()
s.close()
I generated my client certificate and key using the following command:
openssl req -newkey rsa:2048 \
-x509 \
-sha256 \
-days 3650 \
-nodes \
-out client.crt \
-keyout client.key \
-subj "/C=UK/ST=Scotland/L=Glasgow/O=Company A/OU=Testing/CN=MyName"
The test script seems to start the server and allow the client to connect, but I am getting a BrokenPipeError when I try to send the test message.
Annoyingly I have been getting various different error messages as I go, so it's likely a combination of things. This is a simple example I created to try and get something working. On my more complex example I get "NO_SHARED_CIPHERS" when the client attempts to connect to the server. Annoyingly I can't see why this simple example seems to get further than the more complex one (i.e. the connection seems to be established successfully) even though they are set up almost identically.
I have uploaded a repo at git#github.com:stevengillies87/python-ssl-client-auth-example.git if anyone would like to test it.
I realised the first bug came from copy pasting and example and not realising how it differed from my code in its setup. It used socket.socket() to create the socket whereas my example used socket.create_connection(), which also connects the socket. This was the reason I was getting a BrokenPipeError. Now both my simple example and the actual code I am writing both have a consistent NO_SHARED_CIPHER error. I added a line to the source code to connect the client after the socket has been wrapped.
So, as expected it was a combination of things.
Before I added the SSL layer to my code it worked with TCP sockets. I was using socket.create_connection() in the client to create and connect a socket in one call. When I added SSL I continued to do this but because I was attempting to connect to an SSL server via a TCP socket I was getting a NO_SHARED_CIPHER error.
The solution to this problem was to only create the TCP socket with sock = socket.socket(), wrap it with ssock = ssl_context.wrap_context(sock) and then call connect on the SSL layer, ssock.connect((host, port)).
However, I was still getting a handshaking error on connection. I found this link, https://www.electricmonk.nl/log/2018/06/02/ssl-tls-client-certificate-verification-with-python-v3-4-sslcontext/, which provided a detailed example of how to create mutually authenticating SSL client/server. Crucially, the author pointed out that hostname used for server authentication must match the "common name" entered when creating the server.crt and server.key files. Previously I had just been using the same host that I was connecting to, "localhost" in this case. They also noted that the ssl_context verify mode should be set to verify_mode = ssl.CERT_REQUIRED for client auth.
Once the example worked I set about removing the client auth of the server. This was done by changing the client SSL context from ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) to ssl.SSLContext(). The client now does not require the server.crt file to connect successfully.
Frustratingly I still need to create server cert/key files and load them into the server using ssl_context.load_cert_chain(), even though I do not need the server to be authenticated. If I try to remove this step from the server I get a NO_SHARED_CIPHER error again. If anyone knows how I can avoid this then please let me know, or explain why it is necessary.
Working code below, and updated at the github link in the question.
Client:
import socket
import ssl
class SSLClient:
def __init__(
self, server_host, server_port, sni_hostname, client_cert, client_key,
):
self.server_host = server_host
self.server_port = server_port
self.sni_hostname = sni_hostname
self._context = ssl.SSLContext()
self._context.load_cert_chain(client_cert, client_key)
self._sock = None
self._ssock = None
def __del__(self):
self.close()
def connect(self):
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._ssock = self._context.wrap_socket(self._sock,)
self._ssock.connect((self.server_host, self.server_port))
def send(self, msg):
self._ssock.send(msg.encode())
def close(self):
self._ssock.close()
Server:
import socket
import ssl
from threading import Thread
class SSLServer:
def __init__(
self, host, port, server_cert, server_key, client_cert, chunk_size=1024
):
self.host = host
self.port = port
self.chunk_size = chunk_size
self._context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
self._context.verify_mode = ssl.CERT_REQUIRED
self._context.load_cert_chain(server_cert, server_key)
self._context.load_verify_locations(client_cert)
def connect(self):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
sock.bind((self.host, self.port))
sock.listen(5)
while True:
conn, _ = sock.accept()
with self._context.wrap_socket(conn, server_side=True) as sconn:
self._recv(sconn)
def _recv(self, sock):
while True:
data = sock.recv(self.chunk_size)
if data:
print(data.decode())
else:
break
class SSLServerThread(Thread):
def __init__(self, server):
super().__init__()
self._server = server
self.daemon = True
def run(self):
self._server.connect()
Test:
import client, server
from os import path
from time import sleep
server_host = "127.0.0.1"
server_port = 35689
server_sni_hostname = "www.company-b.com"
client_cert = path.join(path.dirname(__file__), "client.crt")
client_key = path.join(path.dirname(__file__), "client.key")
server_cert = path.join(path.dirname(__file__), "server.crt")
server_key = path.join(path.dirname(__file__), "server.key")
s = server.SSLServer(server_host, server_port, server_cert, server_key, client_cert)
s_thread = server.SSLServerThread(s)
s_thread.start()
sleep(2)
c = client.SSLClient(
server_host, server_port, server_sni_hostname, client_cert, client_key
)
c.connect()
c.send("This is a test message!")
c.close()

Remote port forwarding with paramiko

How do I implement remote port forwarding with Paramiko? I have seen examples doing the local port forward but this puzzles me. I would like to implement
ssh -N -R 2210:localhost:5000 username#remotehost
with paramiko. (forward remotehost port 2210 to localhost port 5000, do not open shell as it is not permitted). I have tried
ssh=paramiko.SSHClient()
ssh.connect(remotehost, username=remoteusername, key_filename="/....")
transport = ssh.get_transport()
transport.open_channel(......)
but all combinations of dest_addr, src_addr and action (direct-tcpip, forwarded-tcpip) seem to fail to ChannelException (administrately prohibited). My command line ssh works, so I am authorised to do this, but I don't know how to do the Paramiko implementation.
Any ideas?
Hannu
Sample script showing how to do remote port forwarding over paramiko.
This script connects to the requested SSH server and sets up remote port
forwarding (the openssh -R option) from a remote port through a tunneled
connection to a destination reachable from the local machine.
Example: ssh -R 4000:internal.example.com:80 public.example.com
import socket
import select
import sys
import threading
import paramiko
def handler(chan, host, port):
sock = socket.socket()
try:
sock.connect((host, port))
except Exception as e:
print("Forwarding request to %s:%d failed: %r" % (host, port, e))
return
print(
"Connected! Tunnel open %r -> %r -> %r"
% (chan.origin_addr, chan.getpeername(), (host, port))
)
while True:
r, w, x = select.select([sock, chan], [], [])
if sock in r:
data = sock.recv(1024)
if len(data) == 0:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break
sock.send(data)
chan.close()
sock.close()
def reverse_forward_tunnel(server_port, remote_host, remote_port, transport):
transport.request_port_forward("", server_port)
while True:
chan = transport.accept(1000)
if chan is None:
continue
thr = threading.Thread(
target=handler, args=(chan, remote_host, remote_port)
)
thr.setDaemon(True)
thr.start()
def main():
"""
ssh -R 4000:internal.example.com:80 public.example.com
"""
ssh_host = 'public.example.com'
ssh_port = 22
ssh_user = 'root'
ssh_pass = 'password'
remote_bind_port = 4000 # port on server to forward
forward_host = 'internal.example.com' # dest host to forward to
forward_port = 80 # dest port to forward to
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
try:
client.connect(
ssh_host,
ssh_port,
username=ssh_user,
password=ssh_pass,
)
except Exception as e:
print("*** Failed to connect to %s:%d: %r" % (ssh_host, ssh_port, e))
sys.exit(1)
print(
"Now forwarding remote port %d to %s:%d ..."
% (remote_bind_port, forward_host, forward_port)
)
try:
reverse_forward_tunnel(
remote_bind_port, forward_host, forward_port, client.get_transport()
)
except KeyboardInterrupt:
print("C-c: Port forwarding stopped.")
sys.exit(0)
if __name__ == "__main__":
main()

Problem originating SSH tunnels from python

The object is to set up n number of ssh tunnels between satellite servers and a centralized registry database. I have already set up public key authentication between my servers so they just log right in without password prompts. Now what ? I've tried Paramiko. It seems decent but gets pretty complicated just to set up a basic tunnel, although code exmplaes would be aprreciated. I've tried Autossh and it dies 2 minutes after setting up a working tunnel, bizarre! Hopefully someone can help me with a simple code snippet that I can daemonize and monitor with supervisord or monit.
Here is a cutdown version of the script that Alex pointed you to.
It simply connects to 192.168.0.8 and forwards port 3389 from 192.168.0.6 to localhost
import select
import SocketServer
import sys
import paramiko
class ForwardServer(SocketServer.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
class Handler (SocketServer.BaseRequestHandler):
def handle(self):
try:
chan = self.ssh_transport.open_channel('direct-tcpip', (self.chain_host, self.chain_port), self.request.getpeername())
except Exception, e:
print('Incoming request to %s:%d failed: %s' % (self.chain_host, self.chain_port, repr(e)))
return
if chan is None:
print('Incoming request to %s:%d was rejected by the SSH server.' % (self.chain_host, self.chain_port))
return
print('Connected! Tunnel open %r -> %r -> %r' % (self.request.getpeername(), chan.getpeername(), (self.chain_host, self.chain_port)))
while True:
r, w, x = select.select([self.request, chan], [], [])
if self.request in r:
data = self.request.recv(1024)
if len(data) == 0:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break
self.request.send(data)
chan.close()
self.request.close()
print('Tunnel closed from %r' % (self.request.getpeername(),))
def main():
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
client.connect("192.168.0.8")
class SubHandler(Handler):
chain_host = "192.168.0.6"
chain_port = 3389
ssh_transport = client.get_transport()
try:
ForwardServer(('', 3389), SubHandler).serve_forever()
except KeyboardInterrupt:
sys.exit(0)
if __name__ == '__main__':
main()
Is there a special reason not to just do it with ssh, the usual
(ssh -L <localport>:localhost:<remoteport> <remotehost>)
minuet? Anyway, this script is an example of local port forwarding (AKA tunneling).

Categories

Resources