Commit dd2dac97 authored by Yury's avatar Yury

split socks.py to submodule, BIND method, unittest for hello, authenticate

parent 1a8fd063
Pipeline #114 passed with stages
in 9 minutes and 44 seconds
stages:
# - unittests
- unittests
- build
#tests:
# stage: unittests
# tags:
# - alpine
# script:
# - apk update
# - apk add --update --no-cache --virtual .build-deps alpine-sdk python3-dev musl-dev postgresql-dev libffi-dev
# - pip3 install -U setuptools pip
# - pip3 install --no-cache-dir -r requirements.txt
# - python3 -m unittest discover tests *_test.py
tests:
stage: unittests
tags:
- alpine
script:
- apk update
- apk add --update --no-cache --virtual .build-deps alpine-sdk python3-dev musl-dev postgresql-dev libffi-dev
- pip3 install -U setuptools pip
- pip3 install --no-cache-dir -r requirements.txt
- python3 -m unittest discover tests *_test.py
pypi_build:
stage: build
......
import logging
from ipaddress import ip_network
from . import protocol
from . import auth
from . import config
from . import dispatcher
from . import engine
from . import socks
from . import auth
from . import config
from ipaddress import ip_network
from . import state
logging.basicConfig(level=logging.INFO)
def validate_config():
if hasattr(config, 'ALLOWED_DESTINATIONS'):
if hasattr(config, "ALLOWED_DESTINATIONS"):
ips = []
for ip in config.ALLOWED_DESTINATIONS:
ips.append(ip_network(ip))
config.ALLOWED_DESTINATIONS = ips
validate_config()
def start_server(host='0.0.0.0', port=8080):
def start_server(host="0.0.0.0", port=8080):
server = engine.Server(host, port)
server.start()
\ No newline at end of file
server.start()
import logging
class Authenticator:
method = 2
......@@ -16,9 +16,8 @@ class Authenticator:
login = data[2:2 + data[1]] # index 1 specifies length of login
password = data[3 + data[1]:3 + data[1] + data[2 + data[1]]] # index 2+data[1] specifies length of password
except IndexError:
print(f'INDEX ERROR {data}')
logging.error(f'INDEX ERROR in {self.__class__}. Data: {data} ')
return False
if self.users.get(login.decode()) == password.decode():
return True
return False
from .core import Authenticator
\ No newline at end of file
from .core import Authenticator
......@@ -10,4 +10,4 @@ class Authenticator:
pass
async def authenticate(self, data):
return True
\ No newline at end of file
return True
AUTHENTICATION_METHODS = ['saturn.auth.dict']
AUTHENTICATION_METHODS = ["saturn.auth.dict"]
SATURN_AUTH_DICT = {
'USER_TEST': 'Test_password'
"USER_TEST": "Test_password"
}
ALLOWED_DESTINATIONS = ['0.0.0.0/0']
\ No newline at end of file
ALLOWED_DESTINATIONS = ["0.0.0.0/0"]
from saturn import state
from saturn.socks import SocksTcpRequest, SocksHello, SocksAuthenticate
from saturn.socks import SocksHello, SocksAuthenticate
from saturn.socks.request import SocksRequest
class Dispatcher:
......@@ -14,19 +15,16 @@ class Dispatcher:
async def handle(self, data):
result = None
# if (not isinstance(self.state, state.Connected) or not isinstance(self.state, state.Authenticated)) and \
# data[0] == 5 and len(data) == data[1] + 2:
# self.state = state.NotAuthenticated()
if isinstance(self.state, state.Connected):
self.client_transport.write(data)
elif isinstance(self.state, state.NotAuthenticated):
result = SocksHello(self, data).reply()
elif isinstance(self.state, state.WaitingAuthenticationData) and data[0] == 1:
elif isinstance(self.state, state.WaitingAuthenticationData):
result = await SocksAuthenticate(self, data).authenticate()
elif isinstance(self.state, state.Authenticated) and data[0] == 5 and len(data) >=10:
request = SocksTcpRequest.parse(self, data)
elif isinstance(self.state, state.Authenticated):
request = SocksRequest.parse(self, data)
result = await request.go()
return result
def reply(self, data):
self.server_transport.write(data)
\ No newline at end of file
self.server_transport.write(data)
import asyncio
from saturn import protocol, config
class Server:
def __init__(self, host, port, tcp=True, udp=False):
def __init__(self, host, port, tcp=True, udp=False, custom_auth=None):
self.host = host
self.port = port
self.tcp = tcp
self.udp = udp
self.auth_methods = []
self.auth_config = config.AUTHENTICATION_METHODS if custom_auth is None else custom_auth
def init_auth_methods(self):
for method in config.AUTHENTICATION_METHODS:
m = __import__(method, globals=globals(), fromlist=[''])
self.auth_methods.append(m.Authenticator(**getattr(config, method.upper().replace('.', '_'), {})))
for method in self.auth_config:
m = __import__(method, globals=globals(), fromlist=[""])
self.auth_methods.append(m.Authenticator(**getattr(config, method.upper().replace(".", "_"), {})))
if not self.auth_methods:
raise Exception("Server have no auth methods. Please fill in AUTHENTICATION_METHODS")
@property
def server_auth_methods(self):
......@@ -33,3 +36,8 @@ class Server:
if self.tcp:
loop.create_task(protocol.Socks5TcpServer(self, loop).start_server(self.host, self.port))
loop.run_forever()
if __name__ == "__main__":
server = Server("0.0.0.0", 8081)
server.start()
from .client_tcp import TcpClient
from .server_tcp import TcpServer
from .socks_tcp import Socks5TcpServer, main
from .server_tcp import TcpServer
\ No newline at end of file
import asyncio
from saturn import socks
from ipaddress import IPv4Address, IPv6Address
from ipaddress import IPv4Address
class TcpServer(asyncio.Protocol):
def __init__(self, dispatcher, loop, *args, **kwargs):
......@@ -9,19 +10,17 @@ class TcpServer(asyncio.Protocol):
self.dispatcher = dispatcher
def connection_made(self, transport):
from saturn.socks import reply
self.transport = transport
addr = IPv4Address(self.transport.get_extra_info('peername')[0])
port = self.transport.get_extra_info('peername')[1]
print(addr, port)
self.dispatcher.server_transport.write(bytes(socks.SocksTcpReply(self.dispatcher,
5, 0, 0, 1, int(addr), int(port))))
self.dispatcher.server_transport.write(reply.Success(addr, port))
def data_received(self, data: bytes) -> None:
print('ooh data')
self.dispatcher.client_transport.write(data)
async def start_server(self, host='0.0.0.0', port=8080):
server = await self.loop.create_server(
lambda: self, host, port)
lambda: TcpServer(self.dispatcher, self.loop), host, port)
async with server:
await server.serve_forever()
\ No newline at end of file
await server.serve_forever()
import asyncio
from saturn.dispatcher import Dispatcher
......@@ -25,8 +26,6 @@ class Socks5TcpServer(asyncio.Protocol):
if reply:
self.transport.write(bytes(reply))
else:
# if not isinstance(dispatcher.state, state.Connected):
# print('fail',dispatcher.server_transport.get_extra_info('peername'), dispatcher, dispatcher.state, data)
return
async def start_server(self, host='0.0.0.0', port=8080):
......@@ -35,13 +34,14 @@ class Socks5TcpServer(asyncio.Protocol):
async with server:
await server.serve_forever()
async def main(server, loop):
server = await loop.create_server(
lambda: Socks5TcpServer(server, loop), host='0.0.0.0', port=8082)
async with server:
await server.serve_forever()
if __name__ == '__main__':
loop = asyncio.new_event_loop()
loop.run_until_complete(main(loop))
# loop.run_until_complete(Socks5TcpServer(loop).start_server())
\ No newline at end of file
from ipaddress import IPv6Address, IPv4Address, AddressValueError
from saturn import state
from saturn.protocol.client_tcp import TcpClient
from saturn import protocol, config
import socket
import random
from ipaddress import ip_network
import socket
from ipaddress import IPv4Address
class SocksPacket:
def __init__(self, data):
# assert data[0] == 5
self.ver = 5
from saturn import protocol
from saturn import state
from saturn.protocol.client_tcp import TcpClient
class SocksHello(SocksPacket):
def __init__(self, dispatcher, data):
def __init__(self, dispatcher, data):
super().__init__(data)
self.dispatcher = dispatcher
self.nmethods = data[1]
......@@ -22,7 +17,8 @@ class SocksHello(SocksPacket):
def reply(self):
for m in self.dispatcher.server.server_auth_methods:
if m in self.methods:
self.dispatcher.state = state.WaitingAuthenticationData(method=m) if not m == 0 else state.Authenticated()
self.dispatcher.state = state.WaitingAuthenticationData(
method=m) if not m == 0 else state.Authenticated()
return self.ver.to_bytes(1, byteorder='big') + int.to_bytes(m, 1, byteorder='big')
return self.ver.to_bytes(1, byteorder='big') + int.to_bytes(255, 1, byteorder='big')
......@@ -40,72 +36,6 @@ class SocksAuthenticate:
return int(1).to_bytes(1, byteorder='big') + int(10).to_bytes(1, byteorder='big')
class SocksTcpRequest:
@staticmethod
def parse(dispatcher, data):
assert data[0] == 5
if data[1] == 1:
return SocksRequestConnect(dispatcher, data)
elif data[1] == 2:
return SocksRequestBind(dispatcher, data)
elif data[1] == 3:
return SocksRequestUdpAssociate(dispatcher, data)
return
class SocksRequest:
def __init__(self,dispatcher, data):
self.dispatcher = dispatcher
self.ver = data[0]
self.cmd = data[1]
self.rsv = data[2]
self.atyp = data[3]
if self.atyp == 1:
self.dst_addr = IPv4Address(data[4:-2])
elif self.atyp == 3:
self.dst_addr = data[5:5 + data[4]].decode()
elif self.atyp == 4:
self.dst_addr = IPv6Address(data[4:-2])
self.dst_port = int.from_bytes(data[-2:], byteorder='big')
async def go(self):
pass
class SocksRequestConnect(SocksRequest):
async def go(self):
assert not isinstance(self.dispatcher.state, state.Connected)
on_connect = self.dispatcher.loop.create_future()
allowed_to = False
for addr in getattr(config, 'ALLOWED_DESTINATIONS', [ip_network('0.0.0.0/0')]):
if self.dst_addr in ip_network(addr):
allowed_to = True
break
if not allowed_to:
return SocksTcpReply(self.dispatcher, 5, 2, 0, 1, int(IPv4Address('0.0.0.0')), 0)
try:
self.dispatcher.client_transport, self.client_protocol = await self.dispatcher.loop.create_connection(
lambda: TcpClient(self.dispatcher, on_connect),
str(self.dst_addr), self.dst_port)
except OSError as e:
if e.errno == 110:
return SocksTcpReply(self.dispatcher, 5, 3, 0, 1, int(IPv4Address('0.0.0.0')), 0)
if e.errno == 111:
return SocksTcpReply(self.dispatcher, 5, 5, 0, 1, int(IPv4Address('0.0.0.0')), 0)
if e.errno == 113 or e.errno == 101:
return SocksTcpReply(self.dispatcher, 5, 4, 0, 1, int(IPv4Address('0.0.0.0')), 0)
if e.errno == 22:
return SocksTcpReply(self.dispatcher, 5, 8, 0, 1, int(IPv4Address('0.0.0.0')), 0)
print('ERROR ',e.errno, e)
return SocksTcpReply(self.dispatcher, 5, 1, 0, 1, int(IPv4Address('0.0.0.0')), 0)
self.dispatcher.connected = True
await on_connect
self.dispatcher.state = state.Connected()
return SocksTcpReply(self.dispatcher, 5, 0, 0, 1, int(IPv4Address(socket.gethostbyname(socket.gethostname()))),
8081)
class SocksRequestBind(SocksRequest):
def __init__(self, dispatcher, data):
......@@ -122,31 +52,14 @@ class SocksRequestBind(SocksRequest):
print(e.errno, e)
try:
port = random.randrange(30000, 65535)
self.dispatcher.loop.create_task(protocol.TcpServer(self, self.dispatcher.loop).start_server(self.host, port))
self.dispatcher.loop.create_task(
protocol.TcpServer(self, self.dispatcher.loop).start_server(self.host, port))
except OSError as e:
print(e.errno, e)
return SocksTcpReply(self.dispatcher, 5, 0, 0, 1, int(IPv4Address(socket.gethostbyname(socket.gethostname()))), port)
return SocksTcpReply(self.dispatcher, 5, 0, 0, 1, int(IPv4Address(socket.gethostbyname(socket.gethostname()))),
port)
class SocksRequestUdpAssociate(SocksRequest):
async def go(self):
print('wooops2')
class SocksTcpReply:
def __init__(self, dispatcher, ver, rep, rsv, atyp, bind_addr, bind_port):
self.dispatcher = dispatcher
self.ver: int = ver
self.rep: int = rep
self.rsv: int = rsv
self.atyp: int = atyp
self.bind_addr: int = bind_addr
self.bind_port: int = bind_port
def __bytes__(self):
return self.ver.to_bytes(1, byteorder='big') + \
self.rep.to_bytes(1, byteorder='big') + \
self.rsv.to_bytes(1, byteorder='big') + \
self.atyp.to_bytes(1, byteorder='big') + \
self.bind_addr.to_bytes(4, byteorder='big') + \
self.bind_port.to_bytes(2, byteorder='big')
from . import reply
from . import request
from .authenticate import SocksAuthenticate
from .hello import SocksHello
from saturn import state
class SocksAuthenticate:
def __init__(self, dispatcher, data):
self.data = data
self.dispatcher = dispatcher
self.server = dispatcher.server
async def authenticate(self):
if await self.server.auth(self.dispatcher.state.method, self.data):
self.dispatcher.state = state.Authenticated()
return int(1).to_bytes(1, byteorder='big') + int(0).to_bytes(1, byteorder='big')
return int(1).to_bytes(1, byteorder='big') + int(10).to_bytes(1, byteorder='big')
from saturn import state
class SocksHello:
def __init__(self, dispatcher, data):
self.ver = 5
self.dispatcher = dispatcher
self.nmethods = data[1]
self.methods = [x for x in data[2:2 + self.nmethods]]
def reply(self):
for m in self.dispatcher.server.server_auth_methods:
if m in self.methods:
self.dispatcher.state = state.WaitingAuthenticationData(method=m) if not m == 0 \
else state.Authenticated()
return self.ver.to_bytes(1, byteorder='big') + int.to_bytes(m, 1, byteorder='big')
return self.ver.to_bytes(1, byteorder='big') + int.to_bytes(255, 1, byteorder='big')
from ipaddress import IPv6Address, IPv4Address
class SocksReply:
rep = 0
def __init__(self, bind_addr=IPv4Address("0.0.0.0"), bind_port=0):
self.ver = 5
self.rsv = 0
if isinstance(bind_addr, IPv4Address):
self.atyp = 1
elif isinstance(bind_addr, IPv6Address):
self.atyp = 4
else:
self.atyp = 3
self.bind_addr = bind_addr
self.bind_port = bind_port
def __bytes__(self):
return self.ver.to_bytes(1, byteorder='big') + \
self.rep.to_bytes(1, byteorder='big') + \
self.rsv.to_bytes(1, byteorder='big') + \
self.atyp.to_bytes(1, byteorder='big') + \
int(self.bind_addr).to_bytes(4, byteorder='big') + \
self.bind_port.to_bytes(2, byteorder='big')
class Success(SocksReply):
pass
class Failure(SocksReply):
rep = 1
class ConnectionNotAllowed(SocksReply):
rep = 2
class NetworkUnreachable(SocksReply):
rep = 3
class HostUnreachable(SocksReply):
rep = 4
class ConnectionRefused(SocksReply):
rep = 5
class TTLExpired(SocksReply):
rep = 6
class CommandNotSupported(SocksReply):
rep = 7
class AddressTypeNotSupported(SocksReply):
rep = 8
from .base import SocksRequest
from .connect import SocksRequestConnect
from .bind import SocksRequestBind
\ No newline at end of file
from ipaddress import IPv4Address, IPv6Address
class SocksRequest:
action_id: int
def __init__(self, dispatcher, data):
self.dispatcher = dispatcher
self.ver = data[0]
self.cmd = data[1]
self.rsv = data[2]
self.atyp = data[3]
if self.atyp == 1:
self.dst_addr = IPv4Address(data[4:-2])
elif self.atyp == 3:
self.dst_addr = data[5:5 + data[4]].decode()
elif self.atyp == 4:
self.dst_addr = IPv6Address(data[4:-2])
self.dst_port = int.from_bytes(data[-2:], byteorder='big')
async def go(self):
pass
@classmethod
def parse(cls, dispatcher, data):
assert data[0] == 5
for sub in cls.__subclasses__():
if sub.action_id == data[1]:
return sub(dispatcher, data)
import random
import socket
from ipaddress import IPv4Address
from saturn.protocol.server_tcp import TcpServer
from saturn.socks import reply
from .base import SocksRequest
class SocksRequestBind(SocksRequest):
action_id = 2
async def go(self):
# Check if CONNECTED
port = random.randrange(30000, 65535)
new_server = TcpServer(self.dispatcher, self.dispatcher.loop).start_server(port)
return reply.Success(IPv4Address(socket.gethostbyname(socket.gethostname())), port)
import logging
import socket
from ipaddress import IPv4Address
from ipaddress import ip_network
from saturn import state, config
from saturn.protocol import TcpClient
from saturn.socks import reply
from .base import SocksRequest
class SocksRequestConnect(SocksRequest):
action_id = 1
async def go(self):
assert not isinstance(self.dispatcher.state, state.Connected)
on_connect = self.dispatcher.loop.create_future()
allowed_to = False
for addr in getattr(config, 'ALLOWED_DESTINATIONS', [ip_network('0.0.0.0/0')]):
if self.dst_addr in ip_network(addr):
allowed_to = True
break
if not allowed_to:
return reply.ConnectionNotAllowed()
try:
self.dispatcher.client_transport, self.client_protocol = await self.dispatcher.loop.create_connection(
lambda: TcpClient(self.dispatcher, on_connect),
str(self.dst_addr), self.dst_port)
except OSError as e:
if e.errno == 110:
return reply.NetworkUnreachable()
if e.errno == 111:
return reply.ConnectionRefused()
if e.errno == 113 or e.errno == 101:
return reply.HostUnreachable()
if e.errno == 22:
return reply.AddressTypeNotSupported()
logging.error(f'TCP Client got {e.errno}: {e} while trying to connect to {self.dst_addr}')
return reply.Failure()
self.dispatcher.connected = True
await on_connect
self.dispatcher.state = state.Connected()
return reply.Success(IPv4Address(socket.gethostbyname(socket.gethostname())), 8081)
from .base import SocksRequest
class UdpAssociate(SocksRequest):
action_id = 3
import unittest
import saturn
import asyncio
class SocksTests(unittest.TestCase):
def test_hello_none_auth(self):
server = saturn.engine.Server('127.0.0.1', 1, custom_auth=["saturn.auth.none"])
server.init_auth_methods()
dispatcher = saturn.dispatcher.Dispatcher(server, None, None, None)
hello = saturn.socks.SocksHello(dispatcher, b'\x05\x02\x00\x02').reply()
self.assertEqual(b"\x05\x00", bytes(hello))
hello = saturn.socks.SocksHello(dispatcher, b'\x05\x01\x02').reply()
self.assertEqual(bytes(hello), b"\x05\xff")
def test_hello_only_password(self):
server = saturn.engine.Server('127.0.0.1', 1, custom_auth=["saturn.auth.dict"])
server.init_auth_methods()
dispatcher = saturn.dispatcher.Dispatcher(server, None, None, None)
hello = saturn.socks.SocksHello(dispatcher, b'\x05\x01\x00').reply()
self.assertEqual(bytes(hello), b"\x05\xff")
hello = saturn.socks.SocksHello(dispatcher, b'\x05\x01\x02').reply()
self.assertEqual(bytes(hello), b"\x05\x02")
def test_hello_unknown(self):
server = saturn.engine.Server('127.0.0.1', 1, custom_auth=["saturn.auth.dict"])
server.init_auth_methods()
dispatcher = saturn.dispatcher.Dispatcher(server, None, None, None)
hello = saturn.socks.SocksHello(dispatcher, b'\x05\x01\x05').reply()
self.assertEqual(bytes(hello), b"\x05\xff")
def test_no_auth_methods(self):
server = saturn.engine.Server('127.0.0.1', 1, custom_auth=[])
self.assertRaises(Exception, server.init_auth_methods)
def test_SocksAuthenticate(self):
server = saturn.engine.Server('127.0.0.1', 1, custom_auth=["saturn.auth.dict"])
server.init_auth_methods()
dispatcher = saturn.dispatcher.Dispatcher(server, None, None, None)
dispatcher.state = saturn.state.WaitingAuthenticationData(2)
login = 'USER_TEST'
password = 'Test_password'
req = b"\x05" + len(login).to_bytes(1, "big") + login.encode() + len(password).to_bytes(1, "big") + password.encode()
self.assertEqual(b"\x01\x00", asyncio.run(saturn.socks.SocksAuthenticate(dispatcher, req).authenticate()))
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment