Created
May 12, 2020 16:18
-
-
Save cybiere/abe5caa3a7504bfd733eb2e5eb829fb1 to your computer and use it in GitHub Desktop.
This is a basic python SOCK5 server which forwards the traffic through a SSH tunnel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
#This is https://github.com/rushter 's python socks server | |
#Tweaked by https://github.com/cybiere to forward to a SSH server | |
#via Fabric https://github.com/fabric/fabric | |
#to act as ssh -D | |
import logging | |
import select | |
import socket | |
import struct | |
from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler | |
import fabric | |
logging.basicConfig(level=logging.INFO) | |
SOCKS_VERSION = 5 | |
class ThreadingTCPServer(ThreadingMixIn, TCPServer): | |
pass | |
#SPECIFY THE TARGET HOST HERE | |
gateway = fabric.Connection(host='10.0.1.101', user='user', connect_kwargs={"password":"resu"}) | |
gateway.open() | |
class SocksProxy(StreamRequestHandler): | |
def handle(self): | |
logging.info('Accepting connection from %s:%s' % self.client_address) | |
# greeting header | |
# read and unpack 2 bytes from a client | |
header = self.connection.recv(2) | |
version, nmethods = struct.unpack("!BB", header) | |
# socks 5 | |
assert version == SOCKS_VERSION | |
assert nmethods > 0 | |
# get available methods | |
methods = self.get_available_methods(nmethods) | |
# accept only NO AUTH auth | |
if 0 not in set(methods): | |
# close connection | |
self.server.close_request(self.request) | |
return | |
# send welcome message | |
self.connection.sendall(struct.pack("!BB", SOCKS_VERSION, 0)) | |
# request | |
version, cmd, _, address_type = struct.unpack("!BBBB", self.connection.recv(4)) | |
assert version == SOCKS_VERSION | |
if address_type == 1: # IPv4 | |
address = socket.inet_ntoa(self.connection.recv(4)) | |
elif address_type == 3: # Domain name | |
domain_length = ord(self.connection.recv(1)[0]) | |
address = self.connection.recv(domain_length) | |
port = struct.unpack('!H', self.connection.recv(2))[0] | |
# reply | |
try: | |
if cmd == 1: # CONNECT | |
remote = gateway.transport.open_channel( | |
kind="direct-tcpip", | |
dest_addr=(address,port), | |
src_addr=("",0) | |
) | |
#remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
#remote.connect((address, port)) | |
# bind_address = remote.getsockname() | |
logging.info('Connected to %s %s' % (address, port)) | |
else: | |
self.server.close_request(self.request) | |
#addr = struct.unpack("!I", socket.inet_aton(bind_address[0]))[0] | |
#port = bind_address[1] | |
addr = struct.unpack("!I", socket.inet_aton(address))[0] | |
port = int(port) | |
reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, address_type, | |
addr, port) | |
except Exception as err: | |
logging.error(err) | |
# return connection refused error | |
reply = self.generate_failed_reply(address_type, 5) | |
self.connection.sendall(reply) | |
# establish data exchange | |
if reply[1] == 0 and cmd == 1: | |
self.exchange_loop(self.connection, remote) | |
self.server.close_request(self.request) | |
def get_available_methods(self, n): | |
methods = [] | |
for i in range(n): | |
methods.append(ord(self.connection.recv(1))) | |
return methods | |
def generate_failed_reply(self, address_type, error_number): | |
return struct.pack("!BBBBIH", SOCKS_VERSION, error_number, 0, address_type, 0, 0) | |
def exchange_loop(self, client, remote): | |
while True: | |
# wait until client or remote is available for read | |
r, w, e = select.select([client, remote], [], []) | |
if client in r: | |
data = client.recv(4096) | |
if remote.send(data) <= 0: | |
break | |
if remote in r: | |
data = remote.recv(4096) | |
if client.send(data) <= 0: | |
break | |
if __name__ == '__main__': | |
with ThreadingTCPServer(('127.0.0.1', 1080), SocksProxy) as server: | |
server.serve_forever() | |
gateway.close() |
Thank you guys made some tweak to fit my need sshsocks and sshremoteport forwarder to local machine, may also be useful to someone.
#!/usr/bin/env python3
#This is https://github.com/rushter 's python socks server
#Tweaked by https://github.com/cybiere to forward to a SSH server
#via Fabric https://github.com/fabric/fabric
#to act as ssh -D
from multiprocessing import Process, Lock
from sshtunnel import open_tunnel
from time import sleep
from sshtunnel import SSHTunnelForwarder
import os
import json
import logging
import select
import socket
import struct
from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler
from fabric import Connection as fabricConnection
from random import randint
import traceback
from socket import inet_pton, AF_INET6, inet_aton
from struct import unpack
#https://gist.github.com/cybiere/abe5caa3a7504bfd733eb2e5eb829fb1
#https://pythonrepo.com/repo/pahaz-sshtunnel-python-devops-tools
#logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.WARNING)
#logging.basicConfig(level=logging.DEBUG)
SOCKS_VERSION = 5
gateway = {}
def ip6_to_integer(ip6):
ip6 = inet_pton(AF_INET6, ip6)
a, b = unpack(">QQ", ip6)
return (a << 64) | b
def ip2long(ip_addr):
ip_packed = inet_aton(ip_addr)
ip = unpack("!L", ip_packed)[0]
return ip
class ThreadingTCPServer(ThreadingMixIn, TCPServer):
pass
class sshDsocks(fabricConnection):
def __init__(
self,
host,
user=None,
port=None,
config=None,
gateway=None,
forward_agent=None,
connect_timeout=None,
connect_kwargs=None,
inline_ssh_env=None,
):
self.host = host
self.user = user
self.port = port
self.connect_kwargs = connect_kwargs
fabricConnection.__init__(self,host,user,port,config,gateway,forward_agent,connect_timeout,connect_kwargs,inline_ssh_env )
def open(self):
"""
Initiate an SSH connection to the host/port this object is bound to.
This may include activating the configured gateway connection, if one
is set.
Also saves a handle to the now-set Transport object for easier access.
Various connect-time settings (and/or their corresponding :ref:`SSH
config options <ssh-config>`) are utilized here in the call to
`SSHClient.connect <paramiko.client.SSHClient.connect>`. (For details,
see :doc:`the configuration docs </concepts/configuration>`.)
.. versionadded:: 2.0
"""
# Short-circuit
if self.is_connected:
return
err = "Refusing to be ambiguous: connect() kwarg '{}' was given both via regular arg and via connect_kwargs!" # noqa
# These may not be given, period
for key in """
hostname
port
username
""".split():
if key in self.connect_kwargs:
raise ValueError(err.format(key))
# These may be given one way or the other, but not both
if (
"timeout" in self.connect_kwargs
and self.connect_timeout is not None
):
raise ValueError(err.format("timeout"))
# No conflicts -> merge 'em together
kwargs = dict(
self.connect_kwargs,
username=self.user,
hostname=self.host,
port=self.port,
)
if self.gateway:
kwargs["sock"] = self.open_gateway()
if self.connect_timeout:
kwargs["timeout"] = self.connect_timeout
# Strip out empty defaults for less noisy debugging
# if "key_filename" in kwargs and not kwargs["key_filename"]:
# del kwargs["key_filename"]
del kwargs["key_filename"]
# print(kwargs)
# print(kwargs)
# Actually connect!
self.client.connect(**kwargs)
self.transport = self.client.get_transport()
class SocksProxy(StreamRequestHandler):
reconnecting = False
# lock_reconnect = Lock()
def connectGateway(self,Force=False):
global gateway
if(gateway is not None):
if not Force and hasattr( gateway, "is_connected" ) and gateway.is_connected:
logging.info("Still Connected to {}".format(gateway.host))
return
else:
if hasattr(gateway, 'close') and callable(getattr(gateway, 'close')):
gateway.close()
elif Force:
if hasattr(gateway, 'close') and callable(getattr(gateway, 'close')):
gateway.close()
ssh_host = gateway.host
user = gateway.user
port = gateway.port
connect_kwargs = gateway.connect_kwargs
gateway = sshDsocks(host=ssh_host, user=user,port=port,connect_kwargs=connect_kwargs)
gateway.open()
return gateway
def reconnectGateway(self,Force=False):
global gateway
retry = 30
# we don't want more than one request attempting to open ssh tunnel when the connection fail maybe mutex lock but this works.
# self.lock_reconnect.acquire()
while not self.reconnecting and retry^0:
try:
self.reconnecting = True
logging.debug("Reconnection Attempt {} To {}".format(retry,gateway.host))
gateway = self.connectGateway(Force)
self.reconnecting = False
logging.debug("Reconnected {}".format(gateway.host))
break
except Exception as e:
# logging.error(traceback.print_exc())
retry = retry - 1
sleep(randint(3,20))
logging.error(e)
# self.lock_reconnect.release()
return gateway
def handle(self):
logging.info('Accepting connection from %s:%s' % self.client_address)
# greeting header
# read and unpack 2 bytes from a client
header = self.connection.recv(2)
if not isinstance(header, (bytes, bytearray)) or not len(header) == 2:
self.server.close_request(self.request)
return
version, nmethods = struct.unpack("!BB", header)
# socks 5
assert version == SOCKS_VERSION
assert nmethods > 0
# get available methods
methods = self.get_available_methods(nmethods)
# accept only NO AUTH auth
if 0 not in set(methods):
# close connection
self.server.close_request(self.request)
return
data = None
try:
# send welcome message
self.connection.sendall(struct.pack("!BB", SOCKS_VERSION, 0))
# request data
data = self.connection.recv(4)
except (BrokenPipeError, ConnectionResetError):
self.server.close_request(self.request)
return
if not isinstance(data, (bytes, bytearray)) and not len(data) == 4:
self.server.close_request(self.request)
return
version, cmd, _, address_type = struct.unpack("!BBBB",data )
assert version == SOCKS_VERSION
if address_type == 1: # IPv4
address = socket.inet_ntoa(self.connection.recv(4))
elif address_type == 3:
domain_length = ord(self.connection.recv(1))
address = self.connection.recv(domain_length)
if isinstance(address, (bytes, bytearray)):
address = address.decode("utf-8")
_address = address
try:
address = socket.gethostbyname(address)
address_type = 1
except Exception as e:
# Could not resolve the domain to ip maybe we can pack the domain for now let's close the connection or connection is down or domain not reachable
address = _address
self.server.close_request(self.request)
return
elif address_type == 4:
# IPV6 read 16 bytes
address = ip6_to_integer(self.connection.recv(16))
# address = str(address)
else:
raise Exception("Unknow Address type {} ".format(address_type))
port = struct.unpack('!H', self.connection.recv(2))[0]
# reply
try:
if cmd == 1: # CONNECT
remote = None
if hasattr( gateway, "is_connected" ) and gateway.is_connected:
remote = gateway.transport.open_channel(kind="direct-tcpip",dest_addr=(address,port),src_addr=("",0))
else:
self.reconnectGateway(True)
remote = gateway.transport.open_channel(kind="direct-tcpip",dest_addr=(address,port),src_addr=("",0))
#remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#remote.connect((address, port))
# bind_address = remote.getsockname()
logging.info('Connected to %s %s' % (address, port))
else:
self.server.close_request(self.request)
#addr = struct.unpack("!I", socket.inet_aton(bind_address[0]))[0]
# port = bind_address[1]
addr = struct.unpack("!I", socket.inet_aton(address))[0]
port = int(port)
reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, address_type,
addr, port)
except Exception as err:
logging.error(err)
# logging.error(traceback.print_exc())
if 'open_channel' in str(err):
self.reconnectGateway(True)
else:
self.reconnectGateway()
# return connection refused error
reply = self.generate_failed_reply(address_type, 5)
try:
self.connection.sendall(reply)
except (ConnectionResetError, BrokenPipeError):
self.server.close_request(self.request)
return
# establish data exchange
if reply[1] == 0 and cmd == 1:
self.exchange_loop(self.connection, remote)
self.server.close_request(self.request)
def get_available_methods(self, n):
methods = []
for i in range(n):
methods.append(ord(self.connection.recv(1)))
return methods
def generate_failed_reply(self, address_type, error_number):
return struct.pack("!BBBBIH", SOCKS_VERSION, error_number, 0, address_type, 0, 0)
def exchange_loop(self, client, remote):
while True:
# wait until client or remote is available for read
r, w, e = select.select([client, remote], [], [])
if client in r:
try:
data = client.recv(4096)
if remote.send(data) <= 0:
break
except (ConnectionResetError, BrokenPipeError) as e:
break
if remote in r:
data = remote.recv(4096)
if client.send(data) <= 0:
break
class sshPortForwarder(Process):
def __init__(self, ssh_config ):
self.ssh_config = ssh_config
Process.__init__(self)
def run(self):
if self.ssh_config.get('forwadports'):
self.remote_ssh_port_forward(self.ssh_config)
elif self.ssh_config.get('socksport'):
self.sock_ssh_proxy(self.ssh_config)
def sock_ssh_proxy(self,rmservice):
global gateway
local_host = "127.0.0.1"
remote_port = rmservice.get('socksport')
local_port = rmservice.get('socksport')
ssh_host = rmservice.get('host')
ssh_port = rmservice.get('port') if rmservice.get('port') else 22
user = rmservice.get('user')
password = rmservice.get('password')
gateway = sshDsocks(host=ssh_host, user=user,port=ssh_port, connect_kwargs={"password":password},)
gateway.open()
logging.info('Ssh Proxy -D {}:{} Proxied From {}'.format(local_host, local_port, ssh_host ) )
with ThreadingTCPServer(('127.0.0.1', local_port),SocksProxy ) as server:
server.serve_forever()
gateway.close()
def remote_ssh_port_forward(self,rmservice):
remote_host = ["127.0.0.1"] * len(rmservice.get('forwadports'))
remote_port = rmservice.get('forwadports')
local_port = rmservice.get('forwadports')
ssh_host = rmservice.get('host')
ssh_port = rmservice.get('port') if rmservice.get('port') else 22
user = rmservice.get('user')
password = rmservice.get('password')
remote_port_tupple = list(zip(remote_host,rmservice.get('forwadports')))
logging.info('Ssh Tunnel From {}:{} Forwarding to {}:{}'.format(remote_host[0], '|'.join(map(str, remote_port)), ssh_host,'|'.join(map(str, remote_port)) ))
with open_tunnel(
(ssh_host, 22),
ssh_username=user,
ssh_password=password,
remote_bind_addresses=remote_port_tupple,
local_bind_addresses=remote_port_tupple
) as server:
# print(server.local_bind_port)
while True:
# press Ctrl-C for stopping
sleep(1)
def save_ssh_conig_file(configdata):
root_dir = os.path.dirname(os.path.abspath(__file__))
sshconfig_path = os.path.join(root_dir, 'config', 'sshforwarder.json')
with open(sshconfig_path, 'w') as f:
json.dump(configdata, f)
def load_sshconfig():
root_dir = os.path.dirname(os.path.abspath(__file__))
sshconfig_path = os.path.join(root_dir, 'config', 'sshforwarder.json')
with open(sshconfig_path, 'r') as f:
return json.load(f)
if __name__ == "__main__":
# config file sample
remote_service_forward_list = [{"name": "ETHNODE", "host":"host", "port": 22, "user": "user", "password": "password", "forwadports":[8545,8090,3001]},{"name": "socksproxyserver", "host":"host", "port": 22, "user": "user", "password": "password", "socksport":1080}]
remote_service_forward_list = load_sshconfig()
for sshonfig in remote_service_forward_list:
servicdeamon = sshPortForwarder(sshonfig)
servicdeamon.start()
How can I add basic authentication to this?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Change to
elif address_type == 3:
domain_length = ord(self.connection.recv(1))
address = self.connection.recv(domain_length)
if isinstance(address, (bytes, bytearray)):
address = address.decode("utf-8")
address = socket.gethostbyname(address)
address_type = 1