Skip to content

Instantly share code, notes, and snippets.

@cybiere
Created May 12, 2020 16:18
Show Gist options
  • Save cybiere/abe5caa3a7504bfd733eb2e5eb829fb1 to your computer and use it in GitHub Desktop.
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
#!/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()
@cybiere
Copy link
Author

cybiere commented Jun 5, 2020

You can use proxychains to make most programs go through the socks proxy. If you only have one pivot to the bastion, you can simply run the ssh command with the -D option to use openssh client built-in socks proxy, instead of this script

Copy link

ghost commented Jun 5, 2020

I can of course do that, however I am looking at a POC for an app that can do this in code, so that many different servers can be consumed in one web app. Think all the Spark resources that are running, being pulled into a central location (web app), and then that Spark master changed so the user can consume those resources for a different environment.

@weirdobeardo48
Copy link

Hi,
Thanks for your script, it works great with firefox (without querying dns over socks), but chrome doesn't,
it show exception at line 57: domain_length = ord(self.connection.recv(1)[0])
TypeError: ord() expected string of length 1, but int found
So basically, querying dns over socks proxy doesn't work, would you please take a look? Thanks a lot!

@ifeanyipossibilities
Copy link

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

@ifeanyipossibilities
Copy link

ifeanyipossibilities commented Jan 17, 2022

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()
		
	
	

@AliAkhtari78
Copy link

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