linksocks 3.0.13__cp39-cp39-macosx_13_0_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of linksocks might be problematic. Click here for more details.
- linksocks/__init__.py +9 -0
- linksocks/_base.py +230 -0
- linksocks/_cli.py +291 -0
- linksocks/_client.py +291 -0
- linksocks/_logging_config.py +182 -0
- linksocks/_server.py +355 -0
- linksocks/_utils.py +144 -0
- linksocks-3.0.13.dist-info/METADATA +493 -0
- linksocks-3.0.13.dist-info/RECORD +21 -0
- linksocks-3.0.13.dist-info/WHEEL +5 -0
- linksocks-3.0.13.dist-info/entry_points.txt +2 -0
- linksocks-3.0.13.dist-info/top_level.txt +2 -0
- linksockslib/__init__.py +0 -0
- linksockslib/_linksockslib.cpython-39-darwin.h +1101 -0
- linksockslib/_linksockslib.cpython-39-darwin.so +0 -0
- linksockslib/build.py +761 -0
- linksockslib/go.py +4023 -0
- linksockslib/linksocks.py +5756 -0
- linksockslib/linksockslib.c +14300 -0
- linksockslib/linksockslib.go +8238 -0
- linksockslib/linksockslib_go.h +1101 -0
linksocks/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""linksocks: SOCKS5 over WebSocket proxy library."""
|
|
2
|
+
|
|
3
|
+
__version__ = "3.0.13"
|
|
4
|
+
|
|
5
|
+
from ._server import Server
|
|
6
|
+
from ._client import Client
|
|
7
|
+
from ._base import ReverseTokenResult, set_log_level
|
|
8
|
+
|
|
9
|
+
__all__ = ["Server", "Client", "ReverseTokenResult", "set_log_level"]
|
linksocks/_base.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes and utilities for linksocks.
|
|
3
|
+
|
|
4
|
+
This module contains shared functionality used by Server and Client classes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import asyncio
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from datetime import timedelta
|
|
16
|
+
from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Union, List
|
|
17
|
+
|
|
18
|
+
# Underlying Go bindings module (generated)
|
|
19
|
+
from linksockslib import linksocks # type: ignore
|
|
20
|
+
|
|
21
|
+
_logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Type aliases
|
|
24
|
+
DurationLike = Union[int, float, timedelta, str]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _snake_to_camel(name: str) -> str:
|
|
28
|
+
"""Convert snake_case to CamelCase."""
|
|
29
|
+
parts = name.split("_")
|
|
30
|
+
return "".join(p.capitalize() for p in parts if p)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _camel_to_snake(name: str) -> str:
|
|
34
|
+
"""Convert CamelCase to snake_case."""
|
|
35
|
+
out: List[str] = []
|
|
36
|
+
for ch in name:
|
|
37
|
+
if ch.isupper() and out:
|
|
38
|
+
out.append("_")
|
|
39
|
+
out.append(ch.lower())
|
|
40
|
+
return "".join(out)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _to_duration(value: Optional[DurationLike]) -> Any:
|
|
44
|
+
"""Convert seconds/str/timedelta to Go time.Duration via bindings.
|
|
45
|
+
|
|
46
|
+
- None -> 0
|
|
47
|
+
- int/float -> seconds (supports fractions)
|
|
48
|
+
- timedelta -> total seconds
|
|
49
|
+
- str -> parsed by Go (e.g., "1.5s", "300ms")
|
|
50
|
+
"""
|
|
51
|
+
if value is None:
|
|
52
|
+
return 0
|
|
53
|
+
if isinstance(value, timedelta):
|
|
54
|
+
seconds = value.total_seconds()
|
|
55
|
+
return seconds * linksocks.Second()
|
|
56
|
+
if isinstance(value, (int, float)):
|
|
57
|
+
return value * linksocks.Second()
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
try:
|
|
60
|
+
return linksocks.ParseDuration(value)
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
raise ValueError(f"Invalid duration string: {value}") from exc
|
|
63
|
+
raise TypeError(f"Unsupported duration type: {type(value)!r}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Shared Go->Python log dispatcher
|
|
67
|
+
_def_level_map = {
|
|
68
|
+
"trace": logging.DEBUG,
|
|
69
|
+
"debug": logging.DEBUG,
|
|
70
|
+
"info": logging.INFO,
|
|
71
|
+
"warn": logging.WARNING,
|
|
72
|
+
"warning": logging.WARNING,
|
|
73
|
+
"error": logging.ERROR,
|
|
74
|
+
"fatal": logging.CRITICAL,
|
|
75
|
+
"panic": logging.CRITICAL,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _emit_go_log(py_logger: logging.Logger, line: str) -> None:
|
|
80
|
+
"""Process a Go log line and emit it to the Python logger."""
|
|
81
|
+
try:
|
|
82
|
+
obj = json.loads(line)
|
|
83
|
+
except Exception:
|
|
84
|
+
py_logger.info(line)
|
|
85
|
+
return
|
|
86
|
+
level = str(obj.get("level", "")).lower()
|
|
87
|
+
message = obj.get("message") or obj.get("msg") or ""
|
|
88
|
+
extras: Dict[str, Any] = {}
|
|
89
|
+
for k, v in obj.items():
|
|
90
|
+
if k in ("level", "time", "message", "msg"):
|
|
91
|
+
continue
|
|
92
|
+
extras[k] = v
|
|
93
|
+
py_logger.log(_def_level_map.get(level, logging.INFO), message, extra={"go": extras})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Global registry for logger instances
|
|
97
|
+
_logger_registry: Dict[str, logging.Logger] = {}
|
|
98
|
+
|
|
99
|
+
# Event-driven log monitoring system
|
|
100
|
+
_log_listeners: List[Callable[[List], None]] = []
|
|
101
|
+
_listener_thread: Optional[threading.Thread] = None
|
|
102
|
+
_listener_active: bool = False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _start_log_listener() -> None:
|
|
106
|
+
"""Start background thread to drain Go log buffer and forward to Python loggers."""
|
|
107
|
+
global _listener_thread, _listener_active
|
|
108
|
+
if _listener_active and _listener_thread and _listener_thread.is_alive():
|
|
109
|
+
return
|
|
110
|
+
_listener_active = True
|
|
111
|
+
|
|
112
|
+
def _run() -> None:
|
|
113
|
+
# Drain loop: wait for entries with timeout to allow graceful shutdown
|
|
114
|
+
while _listener_active:
|
|
115
|
+
try:
|
|
116
|
+
entries = linksocks.WaitForLogEntries(2000) # wait up to 2s
|
|
117
|
+
except Exception:
|
|
118
|
+
# Backoff on unexpected errors to avoid busy loop
|
|
119
|
+
time.sleep(0.2)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if not entries:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Iterate returned entries; handle both attr and dict styles
|
|
126
|
+
for entry in entries:
|
|
127
|
+
try:
|
|
128
|
+
logger_id = getattr(entry, "LoggerID", None)
|
|
129
|
+
if logger_id is None and isinstance(entry, dict):
|
|
130
|
+
logger_id = entry.get("LoggerID")
|
|
131
|
+
|
|
132
|
+
message = getattr(entry, "Message", None)
|
|
133
|
+
if message is None and isinstance(entry, dict):
|
|
134
|
+
message = entry.get("Message")
|
|
135
|
+
|
|
136
|
+
if not message:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
py_logger = _logger_registry.get(str(logger_id)) or _logger
|
|
140
|
+
_emit_go_log(py_logger, str(message))
|
|
141
|
+
except Exception:
|
|
142
|
+
# Never let logging path crash the listener
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
_listener_thread = threading.Thread(target=_run, name="linksocks-go-log-listener", daemon=True)
|
|
146
|
+
_listener_thread.start()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _stop_log_listener() -> None:
|
|
150
|
+
"""Stop the background log listener thread."""
|
|
151
|
+
global _listener_active
|
|
152
|
+
_listener_active = False
|
|
153
|
+
try:
|
|
154
|
+
# Unblock WaitForLogEntries callers
|
|
155
|
+
linksocks.CancelLogWaiters()
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class BufferZerologLogger:
|
|
161
|
+
"""Buffer-based logger system for Go bindings."""
|
|
162
|
+
|
|
163
|
+
def __init__(self, py_logger: logging.Logger, logger_id: str):
|
|
164
|
+
self.py_logger = py_logger
|
|
165
|
+
self.logger_id = logger_id
|
|
166
|
+
# Ensure background listener is running
|
|
167
|
+
_start_log_listener()
|
|
168
|
+
|
|
169
|
+
# Prefer Go logger with explicit ID so we can map entries back
|
|
170
|
+
try:
|
|
171
|
+
# Newer binding that tags entries with our provided ID
|
|
172
|
+
self.go_logger = linksocks.NewLoggerWithID(self.logger_id)
|
|
173
|
+
except Exception:
|
|
174
|
+
# Fallback to older API; if present, still try callback path
|
|
175
|
+
try:
|
|
176
|
+
def log_callback(line: str) -> None:
|
|
177
|
+
_emit_go_log(py_logger, line)
|
|
178
|
+
|
|
179
|
+
self.go_logger = linksocks.NewLogger(log_callback)
|
|
180
|
+
except Exception:
|
|
181
|
+
# As a last resort, create a default Go logger
|
|
182
|
+
self.go_logger = linksocks.NewLoggerWithID(self.logger_id) # may still raise; surface to caller
|
|
183
|
+
_logger_registry[logger_id] = py_logger
|
|
184
|
+
|
|
185
|
+
def cleanup(self):
|
|
186
|
+
"""Clean up logger resources."""
|
|
187
|
+
if self.logger_id in _logger_registry:
|
|
188
|
+
del _logger_registry[self.logger_id]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass
|
|
192
|
+
class ReverseTokenResult:
|
|
193
|
+
"""Result of adding a reverse token."""
|
|
194
|
+
token: str
|
|
195
|
+
port: int
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class _SnakePassthrough:
|
|
199
|
+
"""Mixin to map snake_case attribute access to underlying CamelCase.
|
|
200
|
+
|
|
201
|
+
Only used when an explicit Pythonic method/attribute is not defined.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __getattr__(self, name: str) -> Any:
|
|
205
|
+
raw = super().__getattribute__("_raw") # type: ignore[attr-defined]
|
|
206
|
+
camel = _snake_to_camel(name)
|
|
207
|
+
try:
|
|
208
|
+
return getattr(raw, camel)
|
|
209
|
+
except AttributeError:
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
def __dir__(self) -> List[str]:
|
|
213
|
+
# Expose snake_case versions of underlying CamelCase for IDEs
|
|
214
|
+
names = set(super().__dir__())
|
|
215
|
+
try:
|
|
216
|
+
raw = super().__getattribute__("_raw") # type: ignore[attr-defined]
|
|
217
|
+
for attr in dir(raw):
|
|
218
|
+
if not attr or attr.startswith("_"):
|
|
219
|
+
continue
|
|
220
|
+
names.add(_camel_to_snake(attr))
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
return sorted(names)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def set_log_level(level: Union[int, str]) -> None:
|
|
227
|
+
"""Set the global log level for linksocks."""
|
|
228
|
+
if isinstance(level, str):
|
|
229
|
+
level = getattr(logging, level.upper())
|
|
230
|
+
_logger.setLevel(level)
|
linksocks/_cli.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for linksocks.
|
|
3
|
+
|
|
4
|
+
This module provides all CLI commands for the linksocks SOCKS5 over WebSocket proxy tool.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import platform
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from ._logging_config import init_logging
|
|
15
|
+
from ._utils import get_env_or_flag, parse_socks_proxy, validate_required_token
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def cli():
|
|
20
|
+
"""SOCKS5 over WebSocket proxy tool."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.command()
|
|
25
|
+
def version():
|
|
26
|
+
"""Print version and platform information."""
|
|
27
|
+
try:
|
|
28
|
+
from linksocks import __version__
|
|
29
|
+
version_str = __version__
|
|
30
|
+
except ImportError:
|
|
31
|
+
version_str = "unknown"
|
|
32
|
+
|
|
33
|
+
platform_str = platform.platform()
|
|
34
|
+
click.echo(f"linksocks version {version_str} {platform_str}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.command()
|
|
38
|
+
@click.option("--token", "-t", help="Authentication token (env: LINKSOCKS_TOKEN)")
|
|
39
|
+
@click.option("--url", "-u", default="ws://localhost:8765", help="WebSocket server address")
|
|
40
|
+
@click.option("--reverse", "-r", is_flag=True, default=False, help="Use reverse SOCKS5 proxy")
|
|
41
|
+
@click.option("--connector-token", "-c", default=None, help="Connector token for reverse proxy (env: LINKSOCKS_CONNECTOR_TOKEN)")
|
|
42
|
+
@click.option("--socks-host", "-s", default="127.0.0.1", help="SOCKS5 server listen address")
|
|
43
|
+
@click.option("--socks-port", "-p", default=9870, help="SOCKS5 server listen port")
|
|
44
|
+
@click.option("--socks-username", "-n", help="SOCKS5 authentication username")
|
|
45
|
+
@click.option("--socks-password", "-w", help="SOCKS5 authentication password (env: LINKSOCKS_SOCKS_PASSWORD)")
|
|
46
|
+
@click.option("--socks-no-wait", "-i", is_flag=True, default=False, help="Start SOCKS server immediately")
|
|
47
|
+
@click.option("--no-reconnect", "-R", is_flag=True, default=False, help="Stop when server disconnects")
|
|
48
|
+
@click.option("--debug", "-d", count=True, help="Debug logging (-d for debug, -dd for trace)")
|
|
49
|
+
@click.option("--threads", "-T", default=1, help="Number of threads for data transfer")
|
|
50
|
+
@click.option("--upstream-proxy", "-x", help="Upstream SOCKS5 proxy (socks5://[user:pass@]host[:port])")
|
|
51
|
+
@click.option("--fast-open", "-f", is_flag=True, default=False, help="Assume connection success and allow data transfer immediately")
|
|
52
|
+
@click.option("--no-env-proxy", "-E", is_flag=True, default=False, help="Ignore proxy env vars for WebSocket connection")
|
|
53
|
+
def client(
|
|
54
|
+
token: Optional[str],
|
|
55
|
+
url: str,
|
|
56
|
+
reverse: bool,
|
|
57
|
+
connector_token: Optional[str],
|
|
58
|
+
socks_host: str,
|
|
59
|
+
socks_port: int,
|
|
60
|
+
socks_username: Optional[str],
|
|
61
|
+
socks_password: Optional[str],
|
|
62
|
+
socks_no_wait: bool,
|
|
63
|
+
no_reconnect: bool,
|
|
64
|
+
debug: int,
|
|
65
|
+
threads: int,
|
|
66
|
+
upstream_proxy: Optional[str],
|
|
67
|
+
fast_open: bool,
|
|
68
|
+
no_env_proxy: bool,
|
|
69
|
+
):
|
|
70
|
+
"""Start SOCKS5 over WebSocket proxy client."""
|
|
71
|
+
from ._client import Client
|
|
72
|
+
|
|
73
|
+
async def main():
|
|
74
|
+
# Get values from flags or environment
|
|
75
|
+
actual_token = get_env_or_flag(token, "LINKSOCKS_TOKEN")
|
|
76
|
+
actual_connector_token = get_env_or_flag(connector_token, "LINKSOCKS_CONNECTOR_TOKEN")
|
|
77
|
+
actual_socks_password = get_env_or_flag(socks_password, "LINKSOCKS_SOCKS_PASSWORD")
|
|
78
|
+
|
|
79
|
+
# Validate required token
|
|
80
|
+
try:
|
|
81
|
+
validated_token = validate_required_token(actual_token)
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
raise click.ClickException(str(e)) from e
|
|
84
|
+
|
|
85
|
+
# Setup logging
|
|
86
|
+
if debug == 0:
|
|
87
|
+
log_level = logging.INFO
|
|
88
|
+
elif debug == 1:
|
|
89
|
+
log_level = logging.DEBUG
|
|
90
|
+
else:
|
|
91
|
+
log_level = 5 # TRACE level
|
|
92
|
+
|
|
93
|
+
init_logging(level=log_level)
|
|
94
|
+
|
|
95
|
+
# Parse upstream proxy
|
|
96
|
+
if upstream_proxy:
|
|
97
|
+
try:
|
|
98
|
+
upstream_host, upstream_username, upstream_password = parse_socks_proxy(upstream_proxy)
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
raise click.ClickException(f"Invalid upstream proxy: {e}") from e
|
|
101
|
+
else:
|
|
102
|
+
upstream_host = upstream_username = upstream_password = None
|
|
103
|
+
|
|
104
|
+
# Create client
|
|
105
|
+
client = Client(
|
|
106
|
+
ws_url=url,
|
|
107
|
+
token=validated_token,
|
|
108
|
+
reverse=reverse,
|
|
109
|
+
socks_host=socks_host,
|
|
110
|
+
socks_port=socks_port,
|
|
111
|
+
socks_username=socks_username,
|
|
112
|
+
socks_password=actual_socks_password,
|
|
113
|
+
socks_wait_server=not socks_no_wait,
|
|
114
|
+
reconnect=not no_reconnect,
|
|
115
|
+
upstream_proxy=upstream_host,
|
|
116
|
+
upstream_username=upstream_username,
|
|
117
|
+
upstream_password=upstream_password,
|
|
118
|
+
threads=threads,
|
|
119
|
+
fast_open=fast_open,
|
|
120
|
+
no_env_proxy=no_env_proxy,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Add connector for reverse mode
|
|
124
|
+
if actual_connector_token and reverse:
|
|
125
|
+
await client.async_add_connector(actual_connector_token)
|
|
126
|
+
await asyncio.sleep(0.2)
|
|
127
|
+
|
|
128
|
+
# Run client
|
|
129
|
+
async with client:
|
|
130
|
+
await asyncio.Future()
|
|
131
|
+
|
|
132
|
+
asyncio.run(main())
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@click.command()
|
|
136
|
+
@click.option("--ws-host", "-H", default="0.0.0.0", help="WebSocket server listen address")
|
|
137
|
+
@click.option("--ws-port", "-P", default=8765, help="WebSocket server listen port")
|
|
138
|
+
@click.option("--token", "-t", default=None, help="Auth token, auto-generated if not provided (env: LINKSOCKS_TOKEN)")
|
|
139
|
+
@click.option("--connector-token", "-c", default=None, help="Connector token for reverse proxy (env: LINKSOCKS_CONNECTOR_TOKEN)")
|
|
140
|
+
@click.option("--connector-autonomy", "-a", is_flag=True, default=False, help="Allow clients to manage connector tokens")
|
|
141
|
+
@click.option("--buffer-size", "-b", default=4096, help="Buffer size for data transfer")
|
|
142
|
+
@click.option("--reverse", "-r", is_flag=True, default=False, help="Use reverse SOCKS5 proxy")
|
|
143
|
+
@click.option("--socks-host", "-s", default="127.0.0.1", help="SOCKS5 server listen address for reverse proxy")
|
|
144
|
+
@click.option("--socks-port", "-p", default=9870, help="SOCKS5 server listen port for reverse proxy")
|
|
145
|
+
@click.option("--socks-username", "-n", help="SOCKS5 username for authentication")
|
|
146
|
+
@click.option("--socks-password", "-w", help="SOCKS5 password for authentication (env: LINKSOCKS_SOCKS_PASSWORD)")
|
|
147
|
+
@click.option("--socks-nowait", "-i", is_flag=True, default=False, help="Start SOCKS server immediately")
|
|
148
|
+
@click.option("--debug", "-d", count=True, help="Debug logging (-d for debug, -dd for trace)")
|
|
149
|
+
@click.option("--api-key", "-k", help="Enable HTTP API with specified key")
|
|
150
|
+
@click.option("--upstream-proxy", "-x", help="Upstream SOCKS5 proxy (socks5://[user:pass@]host[:port])")
|
|
151
|
+
@click.option("--fast-open", "-f", is_flag=True, default=False, help="Assume connection success and allow data transfer immediately")
|
|
152
|
+
def server(
|
|
153
|
+
ws_host: str,
|
|
154
|
+
ws_port: int,
|
|
155
|
+
token: Optional[str],
|
|
156
|
+
connector_token: Optional[str],
|
|
157
|
+
connector_autonomy: bool,
|
|
158
|
+
buffer_size: int,
|
|
159
|
+
reverse: bool,
|
|
160
|
+
socks_host: str,
|
|
161
|
+
socks_port: int,
|
|
162
|
+
socks_username: Optional[str],
|
|
163
|
+
socks_password: Optional[str],
|
|
164
|
+
socks_nowait: bool,
|
|
165
|
+
debug: int,
|
|
166
|
+
api_key: Optional[str],
|
|
167
|
+
upstream_proxy: Optional[str],
|
|
168
|
+
fast_open: bool,
|
|
169
|
+
):
|
|
170
|
+
"""Start SOCKS5 over WebSocket proxy server."""
|
|
171
|
+
from ._server import Server
|
|
172
|
+
|
|
173
|
+
async def main():
|
|
174
|
+
# Get values from flags or environment
|
|
175
|
+
actual_token = get_env_or_flag(token, "LINKSOCKS_TOKEN")
|
|
176
|
+
actual_connector_token = get_env_or_flag(connector_token, "LINKSOCKS_CONNECTOR_TOKEN")
|
|
177
|
+
actual_socks_password = get_env_or_flag(socks_password, "LINKSOCKS_SOCKS_PASSWORD")
|
|
178
|
+
|
|
179
|
+
# Setup logging
|
|
180
|
+
if debug == 0:
|
|
181
|
+
log_level = logging.INFO
|
|
182
|
+
elif debug == 1:
|
|
183
|
+
log_level = logging.DEBUG
|
|
184
|
+
else:
|
|
185
|
+
log_level = 5 # TRACE level
|
|
186
|
+
|
|
187
|
+
init_logging(level=log_level)
|
|
188
|
+
|
|
189
|
+
# Parse upstream proxy
|
|
190
|
+
if upstream_proxy:
|
|
191
|
+
try:
|
|
192
|
+
upstream_host, upstream_username, upstream_password = parse_socks_proxy(upstream_proxy)
|
|
193
|
+
except ValueError as e:
|
|
194
|
+
raise click.ClickException(f"Invalid upstream proxy: {e}") from e
|
|
195
|
+
else:
|
|
196
|
+
upstream_host = upstream_username = upstream_password = None
|
|
197
|
+
|
|
198
|
+
# Create server
|
|
199
|
+
server = Server(
|
|
200
|
+
ws_host=ws_host,
|
|
201
|
+
ws_port=ws_port,
|
|
202
|
+
socks_host=socks_host,
|
|
203
|
+
socks_wait_client=not socks_nowait,
|
|
204
|
+
upstream_proxy=upstream_host,
|
|
205
|
+
upstream_username=upstream_username,
|
|
206
|
+
upstream_password=upstream_password,
|
|
207
|
+
buffer_size=buffer_size,
|
|
208
|
+
api_key=api_key,
|
|
209
|
+
fast_open=fast_open,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Configure tokens if no API key
|
|
213
|
+
if not api_key:
|
|
214
|
+
if reverse:
|
|
215
|
+
result = await server.async_add_reverse_token(
|
|
216
|
+
token=actual_token,
|
|
217
|
+
port=socks_port,
|
|
218
|
+
username=socks_username,
|
|
219
|
+
password=actual_socks_password,
|
|
220
|
+
allow_manage_connector=connector_autonomy,
|
|
221
|
+
)
|
|
222
|
+
use_token = result.token
|
|
223
|
+
|
|
224
|
+
if not connector_autonomy:
|
|
225
|
+
use_connector_token = await server.async_add_connector_token(
|
|
226
|
+
actual_connector_token, use_token
|
|
227
|
+
)
|
|
228
|
+
await asyncio.sleep(0.2)
|
|
229
|
+
|
|
230
|
+
server.log.info("Configuration:")
|
|
231
|
+
server.log.info(" Mode: reverse proxy (SOCKS5 on server -> client -> network)")
|
|
232
|
+
server.log.info(f" Token: {use_token}")
|
|
233
|
+
server.log.info(f" SOCKS5 port: {result.port}")
|
|
234
|
+
if socks_username and actual_socks_password:
|
|
235
|
+
server.log.info(f" SOCKS5 username: {socks_username}")
|
|
236
|
+
if not connector_autonomy:
|
|
237
|
+
server.log.info(f" Connector Token: {use_connector_token}")
|
|
238
|
+
if connector_autonomy:
|
|
239
|
+
server.log.info(" Connector autonomy: enabled")
|
|
240
|
+
else:
|
|
241
|
+
use_token = await server.async_add_forward_token(actual_token)
|
|
242
|
+
await asyncio.sleep(0.2)
|
|
243
|
+
server.log.info("Configuration:")
|
|
244
|
+
server.log.info(" Mode: forward proxy (SOCKS5 on client -> server -> network)")
|
|
245
|
+
server.log.info(f" Token: {use_token}")
|
|
246
|
+
|
|
247
|
+
# Run server
|
|
248
|
+
async with server:
|
|
249
|
+
await asyncio.Future()
|
|
250
|
+
|
|
251
|
+
asyncio.run(main())
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def create_provider_command():
|
|
255
|
+
"""Create provider command as alias for client -r."""
|
|
256
|
+
def provider_wrapper(*args, **kwargs):
|
|
257
|
+
kwargs['reverse'] = True
|
|
258
|
+
return client(*args, **kwargs)
|
|
259
|
+
|
|
260
|
+
return click.Command(
|
|
261
|
+
name="provider",
|
|
262
|
+
callback=provider_wrapper,
|
|
263
|
+
params=client.params.copy(),
|
|
264
|
+
help="Start reverse SOCKS5 proxy client (alias for 'client -r')"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def create_connector_command():
|
|
269
|
+
"""Create connector command as alias for client."""
|
|
270
|
+
return click.Command(
|
|
271
|
+
name="connector",
|
|
272
|
+
callback=client,
|
|
273
|
+
params=client.params.copy(),
|
|
274
|
+
help="Start SOCKS5 proxy client (alias for 'client')"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# Create command aliases
|
|
279
|
+
provider_cmd = create_provider_command()
|
|
280
|
+
connector_cmd = create_connector_command()
|
|
281
|
+
|
|
282
|
+
# Register commands
|
|
283
|
+
cli.add_command(client)
|
|
284
|
+
cli.add_command(connector_cmd)
|
|
285
|
+
cli.add_command(provider_cmd)
|
|
286
|
+
cli.add_command(server)
|
|
287
|
+
cli.add_command(version)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
cli()
|