Skip to content

Commit 7845cbb

Browse files
[SOCKS4] Packet parser & builder (#1047)
* Initial commit * Parse socks4 packet * Lint check * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix lint issues * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add handler test skeleton * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 8e15dc4 commit 7845cbb

File tree

8 files changed

+258
-0
lines changed

8 files changed

+258
-0
lines changed

proxy/socks/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
from .client import SocksClientConnection
12+
from .packet import Socks4Packet
13+
from .handler import SocksProtocolHandler
14+
from .operations import Socks4Operations, socks4Operations
15+
16+
17+
__all__ = [
18+
'Socks4Packet',
19+
'socks4Operations',
20+
'Socks4Operations',
21+
'SocksProtocolHandler',
22+
'SocksClientConnection',
23+
]

proxy/socks/client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
from ..core.connection import TcpClientConnection
12+
13+
14+
class SocksClientConnection(TcpClientConnection):
15+
pass

proxy/socks/handler.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
from typing import Any, Optional
12+
13+
from .client import SocksClientConnection
14+
from ..core.base import BaseTcpServerHandler
15+
16+
17+
class SocksProtocolHandler(BaseTcpServerHandler[SocksClientConnection]):
18+
"""Reference https://www.openssh.com/txt/socks4.protocol"""
19+
20+
def __init__(self, *args: Any, **kwargs: Any) -> None:
21+
super().__init__(*args, **kwargs)
22+
23+
@staticmethod
24+
def create(**kwargs: Any) -> SocksClientConnection:
25+
return SocksClientConnection(**kwargs)
26+
27+
def handle_data(self, data: memoryview) -> Optional[bool]:
28+
return super().handle_data(data)

proxy/socks/operations.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
from typing import NamedTuple
12+
13+
14+
Socks4Operations = NamedTuple(
15+
'Socks4Operations', [
16+
('CONNECT', int),
17+
('BIND', int),
18+
],
19+
)
20+
21+
socks4Operations = Socks4Operations(1, 2)

proxy/socks/packet.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import struct
12+
from typing import Optional
13+
14+
15+
NULL = b'\x00'
16+
17+
18+
class Socks4Packet:
19+
"""SOCKS4 and SOCKS4a protocol parser.
20+
21+
FIXME: Currently doesn't buffer during parsing and expects
22+
packet to arrive within a single socket receive event.
23+
"""
24+
25+
def __init__(self) -> None:
26+
# 1 byte, must be equal to 4
27+
self.vn: Optional[int] = None
28+
# 1 byte
29+
self.cd: Optional[int] = None
30+
# 2 bytes
31+
self.dstport: Optional[int] = None
32+
# 4 bytes
33+
self.dstip: Optional[bytes] = None
34+
# Variable bytes, NULL terminated
35+
self.userid: Optional[bytes] = None
36+
37+
def parse(self, raw: memoryview) -> None:
38+
cursor = 0
39+
# Parse vn
40+
if self.vn is None:
41+
assert int(raw[cursor]) == 4
42+
self.vn = 4
43+
cursor += 1
44+
# Parse cd
45+
self.cd = raw[cursor]
46+
cursor += 1
47+
# Parse dstport
48+
self.dstport = struct.unpack('!H', raw[cursor:cursor+2])[0]
49+
cursor += 2
50+
# Parse dstip
51+
self.dstip = struct.unpack('!4s', raw[cursor:cursor+4])[0]
52+
cursor += 4
53+
# Parse userid
54+
ulen = len(raw) - cursor - 1
55+
self.userid = struct.unpack(
56+
'!%ds' % ulen, raw[cursor:cursor+ulen],
57+
)[0]
58+
cursor += ulen
59+
# Assert null terminated
60+
assert raw[cursor] == NULL[0]
61+
62+
def pack(self) -> bytes:
63+
user_id = self.userid or b''
64+
return struct.pack(
65+
'!bbH4s%ds' % len(user_id),
66+
self.vn, self.cd,
67+
self.dstport, self.dstip,
68+
user_id,
69+
) + NULL

tests/socks/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""

tests/socks/test_handler.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import pytest
12+
13+
from pytest_mock import MockerFixture
14+
15+
from proxy.socks import SocksProtocolHandler, SocksClientConnection
16+
from proxy.common.flag import FlagParser
17+
from ..test_assertions import Assertions
18+
19+
20+
class TestHttpProtocolHandlerWithoutServerMock(Assertions):
21+
22+
@pytest.fixture(autouse=True) # type: ignore[misc]
23+
def _setUp(self, mocker: MockerFixture) -> None:
24+
self.mock_fromfd = mocker.patch('socket.fromfd')
25+
self.mock_selector = mocker.patch('selectors.DefaultSelector')
26+
27+
self.fileno = 10
28+
self._addr = ('127.0.0.1', 54382)
29+
self._conn = self.mock_fromfd.return_value
30+
31+
self.flags = FlagParser.initialize(threaded=True)
32+
33+
self.handler = SocksProtocolHandler(
34+
SocksClientConnection(conn=self._conn, addr=self._addr),
35+
flags=self.flags,
36+
)
37+
self.handler.initialize()
38+
39+
def test(self) -> None:
40+
self.assertEqual(1, 1)

tests/socks/test_packet.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import re
12+
import socket
13+
import binascii
14+
15+
import unittest
16+
17+
from proxy.socks import Socks4Packet, socks4Operations
18+
19+
20+
def unhexlify(raw: str) -> bytes:
21+
return binascii.unhexlify(re.sub(r'\s', '', raw))
22+
23+
24+
# Examples taken from https://en.wikipedia.org/wiki/SOCKS
25+
CLIENT_CONNECT_REQ = unhexlify('04 01 00 50 42 66 07 63 46 72 65 64 00')
26+
SERVER_CONNECT_OK = unhexlify('00 5A')
27+
28+
29+
class TestSocks4Packet(unittest.TestCase):
30+
31+
def test_pack(self) -> None:
32+
pkt = Socks4Packet()
33+
pkt.vn = 4
34+
pkt.cd = socks4Operations.CONNECT
35+
pkt.dstport = 80
36+
pkt.dstip = socket.inet_aton('66.102.7.99')
37+
pkt.userid = b'Fred'
38+
self.assertEqual(
39+
pkt.pack(),
40+
CLIENT_CONNECT_REQ,
41+
)
42+
43+
def test_parse(self) -> None:
44+
wiki = memoryview(CLIENT_CONNECT_REQ)
45+
pkt = Socks4Packet()
46+
pkt.parse(wiki)
47+
self.assertEqual(pkt.vn, 4)
48+
self.assertEqual(pkt.cd, socks4Operations.CONNECT)
49+
self.assertEqual(pkt.dstport, 80)
50+
assert pkt.dstip
51+
self.assertEqual(socket.inet_ntoa(pkt.dstip), '66.102.7.99')
52+
self.assertEqual(pkt.userid, b'Fred')

0 commit comments

Comments
 (0)