Commit b0e39d2b authored by Yury's avatar Yury

stable socks5, readme, target ip limits, config validation

parent feb48777
Pipeline #112 failed with stages
in 10 minutes and 1 second
stages:
- unittests
- build
- test_install
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
......@@ -14,6 +15,8 @@ tests:
pypi_build:
stage: build
tags:
- alpine
script:
- apk update
- apk add --update --no-cache --virtual .build-deps alpine-sdk python3-dev musl-dev postgresql-dev libffi-dev
......
MIT License
Copyright (c) 2018 Yurzs
Copyright (c) 2019 Yurzs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
......
# Socks5 async proxy server
Saturn
TBD
\ No newline at end of file
Saturn is a SOCKS5 server based on asyncio protocols
## Installation
### From [pypi.org](https://pypi.org/project/saturn-proxy-server/)
```bash
pip install saturn-proxy-server
```
### From this repo
```bash
git clone https://git.best-service.online/yurzs/saturn.git
cd saturn
python3 ./setup.py install
```
## Usage
Please edit `config.py` file before starting your server.
By default proxying allowed to all hosts (`ALLOWED_DESTINATIONS = [0.0.0.0/0]`). You can specify single IP addresses
(both IPv4 and IPv6) and subnets.
```python3
import saturn
saturn.config.ALLOWED_DESTINATIONS = ["192.168.1.0/24"]
saturn.config.AUTHENTICATION_METHODS = ["saturn.auth.none"]
saturn.start_server("127.0.0.1", 8080)
```
This config will allow passwordless connections with allowed proxying for `192.168.1.0-192.169.1.255` IP range.
You can use multiple auth methods at once like `["saturn.auth.none", saturn.auth.dict"]`
## Authentication methods
Current SOCKS5 standart supports
- [x] None ["saturn.auth.none"]
- [ ] GSSAPI ["saturn.auth.gssapi"]
- [x] Login/Password (dict format) ["saturn.auth.dict"]
### Custom authentication methods
You can implement your own authentication method (Login/Password)
All you need to do is to implement `Authenticator` class with `async def authenticate(self, data)` method which will return `bool`
authentication result. Then just import your module and use it in config
`saturn.config.AUTHENTICATION_METHODS = ["your_auth_method"]`
You can see examples in `saturn.auth`
\ No newline at end of file
......@@ -2,4 +2,19 @@ from . import protocol
from . import dispatcher
from . import engine
from . import socks
from . import auth
\ No newline at end of file
from . import auth
from . import config
from ipaddress import ip_network
def validate_config():
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):
server = engine.Server(host, port)
server.start()
\ No newline at end of file
AUTHENTICATION_METHODS = ['saturn.auth.none']
AUTHENTICATION_METHODS = ['saturn.auth.dict']
SATURN_AUTH_DICT = {
'test_user': 'Test_password'
}
\ No newline at end of file
'USER_TEST': 'Test_password'
}
ALLOWED_DESTINATIONS = ['0.0.0.0/0']
\ No newline at end of file
......@@ -3,29 +3,30 @@ from saturn.socks import SocksTcpRequest, SocksHello, SocksAuthenticate
class Dispatcher:
def __init__(self, server, loop, protocol):
def __init__(self, server, loop, protocol, transport):
self.server_transport = transport
self.server = server
self.loop = loop
self.server_protocol = protocol
self.client_transport = None
self.state = state.NotAuthenticated()
self.busy = False
self.previous = None
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):
return SocksHello(self, data).reply(self.server)
result = SocksHello(self, data).reply()
elif isinstance(self.state, state.WaitingAuthenticationData) and data[0] == 1:
return await SocksAuthenticate(self, data).authenticate()
elif isinstance(self.state, state.Authenticated) and data[0] == 5:
request = SocksTcpRequest(self, data)
if request.cmd == 1:
result = bytes(await request.connect())
return result
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)
result = await request.go()
return result
def reply(self, data):
self.server_protocol.transport.write(data)
\ No newline at end of file
self.server_transport.write(data)
\ No newline at end of file
......@@ -3,8 +3,7 @@ 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):
self.host = host
self.port = port
self.tcp = tcp
......@@ -34,8 +33,3 @@ 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()
\ No newline at end of file
from .socks_tcp import Socks5TcpServer
\ No newline at end of file
from .socks_tcp import Socks5TcpServer, main
from .server_tcp import TcpServer
\ No newline at end of file
......@@ -8,16 +8,13 @@ class TcpClient(asyncio.Protocol):
def connection_made(self, transport):
self.transport = transport
print('connected')
self.on_connect.set_result(True)
def data_received(self, data):
print('got data')
self.dispatcher.reply(data)
def connection_lost(self, exc):
print('con lost')
self.dispatcher.server_protocol.transport.close()
self.dispatcher.server_transport.close()
def send(self, data):
self.transport.write(data)
import asyncio
from saturn import socks
from ipaddress import IPv4Address, IPv6Address
class TcpServer(asyncio.Protocol):
def __init__(self, dispatcher, loop, *args, **kwargs):
super().__init__(*args, **kwargs)
self.loop = loop
self.dispatcher = dispatcher
def connection_made(self, transport):
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))))
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)
async with server:
await server.serve_forever()
\ No newline at end of file
import asyncio
from saturn.dispatcher import Dispatcher
from saturn import state
class Socks5TcpServer(asyncio.Protocol):
......@@ -11,7 +11,7 @@ class Socks5TcpServer(asyncio.Protocol):
def connection_made(self, transport):
self.transport = transport
self.dispatcher = Dispatcher(self.server, self.loop, self)
self.dispatcher = Dispatcher(self.server, self.loop, self, transport)
def data_received(self, data):
asyncio.Task(self.async_data_handler(data))
......@@ -23,20 +23,25 @@ class Socks5TcpServer(asyncio.Protocol):
def async_data_handler(self, data: bytes) -> None:
reply = yield from self.dispatcher.handle(data)
if reply:
print(self.dispatcher, self.dispatcher.state, data, reply)
self.transport.write(reply)
self.transport.write(bytes(reply))
else:
if not isinstance(self.dispatcher.state, state.Connected):
print('fail', self.dispatcher, self.dispatcher.state, data)
# 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):
server = await self.loop.create_server(
lambda: self, host, port)
lambda: Socks5TcpServer(self.server, self.loop), host, port)
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(Socks5TcpServer(loop).start_server())
\ No newline at end of file
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
class SocksPacket:
def __init__(self, data):
......@@ -16,7 +19,7 @@ class SocksHello(SocksPacket):
self.nmethods = data[1]
self.methods = [x for x in data[2:2 + self.nmethods]]
def reply(self, server):
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()
......@@ -38,9 +41,22 @@ class SocksAuthenticate:
class SocksTcpRequest:
def __init__(self, dispatcher, data):
@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
assert data[0] == 5, f'EXCEPTION! : {data}'
self.ver = data[0]
self.cmd = data[1]
self.rsv = data[2]
......@@ -53,28 +69,68 @@ class SocksTcpRequest:
self.dst_addr = IPv6Address(data[4:-2])
self.dst_port = int.from_bytes(data[-2:], byteorder='big')
async def connect(self):
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 :
print(e.errno)
return SocksTcpReply(self.dispatcher, 5, 1, 0, 1, int(IPv4Address('0.0.0.0')), 1)
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()
sock_info = self.dispatcher.client_transport.get_extra_info('socket').getsockname()
return SocksTcpReply(self.dispatcher, 5, 0, 0, 1, int(IPv4Address('80.211.196.34')), 8081)
return SocksTcpReply(self.dispatcher, 5, 0, 0, 1, int(IPv4Address(socket.gethostbyname(socket.gethostname()))),
8081)
async def bind(self):
pass
async def udp_associate(self):
pass
class SocksRequestBind(SocksRequest):
def __init__(self, dispatcher, data):
assert len(data) >= 10
super().__init__(dispatcher, data)
async def go(self):
on_connect = self.dispatcher.loop.create_future()
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:
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))
except OSError as e:
print(e.errno, e)
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:
......
import socks
for _ in range(500):
s = socks.socksocket()
s.set_proxy(socks.SOCKS5, "localhost", 8081, username='test_user', password='Test_password')
s.connect(("google.com", 80))
s.sendall(b"GET / HTTP/1.1 ...")
s.sendall(b"GET / HTTP/1.1 ...")
s.close()
print(s.recv(4096))
\ 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