localstream 1.0.0__tar.gz

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.
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: localstream
3
+ Version: 1.0.0
4
+ Summary: Python CLI client for slipstream-rust DNS tunnel
5
+ Author: LocalStream Team
6
+ License: MIT
7
+ Keywords: dns,tunnel,vpn,slipstream,cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Internet :: Proxy Servers
15
+ Requires-Python: >=3.13
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: requests>=2.31.0
18
+ Requires-Dist: colorama>=0.4.6
19
+
20
+ # LocalStream
21
+
22
+ A Python command-line client for [slipstream-rust](https://github.com/Mygod/slipstream-rust) DNS tunnel.
23
+
24
+ ## Features
25
+
26
+ - **VPN Mode**: System-wide traffic tunneling (requires Admin)
27
+ - **PROXY Mode**: SOCKS5 proxy on local port
28
+ - Auto-reconnect on connection drops
29
+ - Easy pip installation
30
+ - Colorful CLI interface
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.13+
35
+ - Windows OS
36
+ - Administrator privileges (for VPN Mode)
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install -e .
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```bash
47
+ LocalStream # Normal user (PROXY mode only)
48
+ # Run as Administrator for VPN mode
49
+ ```
50
+
51
+ ### Menu Options
52
+
53
+ ```
54
+ [1] Connect to server
55
+ ├── [1] VPN Mode (system-wide, requires Admin)
56
+ ├── [2] PROXY Mode (SOCKS5 only)
57
+ └── [3] Back
58
+ [2] Edit configuration
59
+ [3] Exit
60
+ ```
61
+
62
+ ### Keyboard Shortcuts
63
+
64
+ - **Ctrl+C**: Disconnect and exit
65
+ - **Ctrl+D**: Restart connection
66
+
67
+ ## Configuration
68
+
69
+ Config stored in `~/.localstream/config.json`:
70
+
71
+ ```json
72
+ {
73
+ "server_ip": "203.0.113.2",
74
+ "server_port": 53,
75
+ "local_port": 5201,
76
+ "domain": "s.example.com"
77
+ }
78
+ ```
79
+
80
+ ## Downloaded Binaries
81
+
82
+ Stored in `~/.localstream/bin/`:
83
+ - `slipstream-client.exe` - DNS tunnel client
84
+ - `tun2proxy.exe` - TUN adapter for VPN mode
85
+ - `wintun.dll` - Windows TUN driver
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,70 @@
1
+ # LocalStream
2
+
3
+ A Python command-line client for [slipstream-rust](https://github.com/Mygod/slipstream-rust) DNS tunnel.
4
+
5
+ ## Features
6
+
7
+ - **VPN Mode**: System-wide traffic tunneling (requires Admin)
8
+ - **PROXY Mode**: SOCKS5 proxy on local port
9
+ - Auto-reconnect on connection drops
10
+ - Easy pip installation
11
+ - Colorful CLI interface
12
+
13
+ ## Requirements
14
+
15
+ - Python 3.13+
16
+ - Windows OS
17
+ - Administrator privileges (for VPN Mode)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install -e .
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ LocalStream # Normal user (PROXY mode only)
29
+ # Run as Administrator for VPN mode
30
+ ```
31
+
32
+ ### Menu Options
33
+
34
+ ```
35
+ [1] Connect to server
36
+ ├── [1] VPN Mode (system-wide, requires Admin)
37
+ ├── [2] PROXY Mode (SOCKS5 only)
38
+ └── [3] Back
39
+ [2] Edit configuration
40
+ [3] Exit
41
+ ```
42
+
43
+ ### Keyboard Shortcuts
44
+
45
+ - **Ctrl+C**: Disconnect and exit
46
+ - **Ctrl+D**: Restart connection
47
+
48
+ ## Configuration
49
+
50
+ Config stored in `~/.localstream/config.json`:
51
+
52
+ ```json
53
+ {
54
+ "server_ip": "203.0.113.2",
55
+ "server_port": 53,
56
+ "local_port": 5201,
57
+ "domain": "s.example.com"
58
+ }
59
+ ```
60
+
61
+ ## Downloaded Binaries
62
+
63
+ Stored in `~/.localstream/bin/`:
64
+ - `slipstream-client.exe` - DNS tunnel client
65
+ - `tun2proxy.exe` - TUN adapter for VPN mode
66
+ - `wintun.dll` - Windows TUN driver
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from localstream.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,321 @@
1
+ import sys
2
+ import os
3
+
4
+ try:
5
+ import colorama
6
+ colorama.init()
7
+ except ImportError:
8
+ pass
9
+
10
+ from localstream.config import config_exists, load_config, save_config, get_default_config
11
+ from localstream.connection import ConnectionManager, is_admin
12
+ from localstream.downloader import client_exists, download_client, tun2proxy_exists, download_tun2proxy
13
+
14
+
15
+ CYAN = "\033[96m"
16
+ GREEN = "\033[92m"
17
+ YELLOW = "\033[93m"
18
+ RED = "\033[91m"
19
+ MAGENTA = "\033[95m"
20
+ BLUE = "\033[94m"
21
+ WHITE = "\033[97m"
22
+ DIM = "\033[2m"
23
+ BOLD = "\033[1m"
24
+ RESET = "\033[0m"
25
+
26
+
27
+ LOGO = f"""
28
+ {CYAN}╔═══════════════════════════════════════════════════════════════╗
29
+ ║ ║
30
+ ║ {MAGENTA}██╗ ██████╗ ██████╗ █████╗ ██╗ {CYAN} ║
31
+ ║ {MAGENTA}██║ ██╔═══██╗██╔════╝██╔══██╗██║ {CYAN} ║
32
+ ║ {MAGENTA}██║ ██║ ██║██║ ███████║██║ {CYAN} ║
33
+ ║ {MAGENTA}██║ ██║ ██║██║ ██╔══██║██║ {CYAN} ║
34
+ ║ {MAGENTA}███████╗╚██████╔╝╚██████╗██║ ██║███████╗{CYAN} ║
35
+ ║ {MAGENTA}╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝{CYAN} ║
36
+ ║ ║
37
+ ║ {BLUE}███████╗████████╗██████╗ ███████╗ █████╗ ███╗ ███╗{CYAN} ║
38
+ ║ {BLUE}██╔════╝╚══██╔══╝██╔══██╗██╔════╝██╔══██╗████╗ ████║{CYAN} ║
39
+ ║ {BLUE}███████╗ ██║ ██████╔╝█████╗ ███████║██╔████╔██║{CYAN} ║
40
+ ║ {BLUE}╚════██║ ██║ ██╔══██╗██╔══╝ ██╔══██║██║╚██╔╝██║{CYAN} ║
41
+ ║ {BLUE}███████║ ██║ ██║ ██║███████╗██║ ██║██║ ╚═╝ ██║{CYAN} ║
42
+ ║ {BLUE}╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝{CYAN} ║
43
+ ║ ║
44
+ ║ {DIM}SlipStream DNS Tunnel • Python CLI Client{CYAN} ║
45
+ ╚═══════════════════════════════════════════════════════════════╝{RESET}
46
+ """
47
+
48
+ MINI_LOGO = f"""
49
+ {CYAN}╔═══════════════════════════════════════════╗
50
+ ║ {MAGENTA}LocalStream{CYAN} • {DIM}SlipStream CLI Client{CYAN} ║
51
+ ╚═══════════════════════════════════════════╝{RESET}
52
+ """
53
+
54
+
55
+ def clear_screen():
56
+ os.system("cls" if os.name == "nt" else "clear")
57
+
58
+
59
+ def print_logo(mini: bool = False):
60
+ if mini:
61
+ print(MINI_LOGO)
62
+ else:
63
+ print(LOGO)
64
+
65
+
66
+ def print_box(title: str, content: list, color: str = CYAN):
67
+ max_len = max(len(line) for line in content) if content else 20
68
+ max_len = max(max_len, len(title) + 4)
69
+ width = max_len + 4
70
+
71
+ print(f"\n{color}┌{'─' * width}┐{RESET}")
72
+ print(f"{color}│{RESET} {BOLD}{title}{RESET}{' ' * (width - len(title) - 2)}{color}│{RESET}")
73
+ print(f"{color}├{'─' * width}┤{RESET}")
74
+
75
+ for line in content:
76
+ padding = width - len(line) - 2
77
+ print(f"{color}│{RESET} {line}{' ' * padding}{color}│{RESET}")
78
+
79
+ print(f"{color}└{'─' * width}┘{RESET}")
80
+
81
+
82
+ def print_config(config: dict):
83
+ server = f"{config.get('server_ip', 'N/A')}:{config.get('server_port', 53)}"
84
+ admin_status = f"{GREEN}✓ Admin{RESET}" if is_admin() else f"{YELLOW}○ User{RESET}"
85
+ content = [
86
+ f"{GREEN}●{RESET} Server : {WHITE}{server}{RESET}",
87
+ f"{GREEN}●{RESET} Domain : {WHITE}{config.get('domain', 'N/A')}{RESET}",
88
+ f"{GREEN}●{RESET} Local Port : {WHITE}{config.get('local_port', 5201)}{RESET}",
89
+ f"{DIM}─────────────────────────────────{RESET}",
90
+ f"{BLUE}●{RESET} Status : {admin_status}",
91
+ ]
92
+ print_box("Current Configuration", content, CYAN)
93
+
94
+
95
+ def print_main_menu():
96
+ content = [
97
+ f"{YELLOW}[1]{RESET} Connect to server",
98
+ f"{YELLOW}[2]{RESET} Edit configuration",
99
+ f"{YELLOW}[3]{RESET} Exit",
100
+ ]
101
+ print_box("Menu", content, BLUE)
102
+
103
+
104
+ def print_connection_menu():
105
+ vpn_status = f"{GREEN}●{RESET}" if is_admin() else f"{RED}○{RESET}"
106
+ admin_note = "" if is_admin() else f" {DIM}(requires Admin){RESET}"
107
+ content = [
108
+ f"{YELLOW}[1]{RESET} {vpn_status} VPN Mode {DIM}(system-wide){RESET}{admin_note}",
109
+ f"{YELLOW}[2]{RESET} {GREEN}●{RESET} PROXY Mode {DIM}(SOCKS5 only){RESET}",
110
+ f"{YELLOW}[3]{RESET} Back to menu",
111
+ ]
112
+ print_box("Connection Mode", content, MAGENTA)
113
+
114
+
115
+ def get_input(prompt: str, default: str = "") -> str:
116
+ if default:
117
+ result = input(f" {GREEN}▸{RESET} {prompt} {DIM}[{default}]{RESET}: ").strip()
118
+ return result if result else default
119
+ return input(f" {GREEN}▸{RESET} {prompt}: ").strip()
120
+
121
+
122
+ def get_int_input(prompt: str, default: int) -> int:
123
+ while True:
124
+ result = input(f" {GREEN}▸{RESET} {prompt} {DIM}[{default}]{RESET}: ").strip()
125
+ if not result:
126
+ return default
127
+ try:
128
+ return int(result)
129
+ except ValueError:
130
+ print(f" {RED}✗{RESET} Please enter a valid number")
131
+
132
+
133
+ def prompt_for_config() -> dict:
134
+ print(f"\n{YELLOW}━━━ Server Configuration ━━━{RESET}\n")
135
+
136
+ config = get_default_config()
137
+
138
+ while True:
139
+ server_ip = get_input("Server IP (e.g., 203.0.113.2)")
140
+ if server_ip:
141
+ config["server_ip"] = server_ip
142
+ break
143
+ print(f" {RED}✗{RESET} Server IP is required")
144
+
145
+ config["server_port"] = get_int_input("Server Port", 53)
146
+ config["local_port"] = get_int_input("Local Port", 5201)
147
+
148
+ while True:
149
+ domain = get_input("Domain (e.g., s.example.com)")
150
+ if domain:
151
+ config["domain"] = domain
152
+ break
153
+ print(f" {RED}✗{RESET} Domain is required")
154
+
155
+ return config
156
+
157
+
158
+ def show_main_menu() -> int:
159
+ print_main_menu()
160
+ print()
161
+
162
+ while True:
163
+ try:
164
+ choice = input(f" {MAGENTA}▸{RESET} Select option {DIM}[1-3]{RESET}: ").strip()
165
+ if choice in ["1", "2", "3"]:
166
+ return int(choice)
167
+ print(f" {RED}✗{RESET} Please enter 1, 2, or 3")
168
+ except (ValueError, EOFError):
169
+ return 3
170
+
171
+
172
+ def show_connection_menu() -> int:
173
+ print_connection_menu()
174
+ print()
175
+
176
+ while True:
177
+ try:
178
+ choice = input(f" {MAGENTA}▸{RESET} Select mode {DIM}[1-3]{RESET}: ").strip()
179
+ if choice in ["1", "2", "3"]:
180
+ return int(choice)
181
+ print(f" {RED}✗{RESET} Please enter 1, 2, or 3")
182
+ except (ValueError, EOFError):
183
+ return 3
184
+
185
+
186
+ def print_connecting_banner(config: dict, mode: str):
187
+ server = f"{config.get('server_ip')}:{config.get('server_port')}"
188
+ domain = config.get('domain')
189
+ local_port = config.get('local_port')
190
+ mode_display = f"{GREEN}VPN{RESET}" if mode == "vpn" else f"{BLUE}PROXY{RESET}"
191
+
192
+ print(f"\n{CYAN}╔═══════════════════════════════════════════════════════╗{RESET}")
193
+ print(f"{CYAN}║{RESET} {GREEN}●{RESET} {BOLD}Connecting in {mode_display} Mode...{RESET}{' ' * 24}{CYAN}║{RESET}")
194
+ print(f"{CYAN}╟───────────────────────────────────────────────────────╢{RESET}")
195
+ print(f"{CYAN}║{RESET} Resolver : {WHITE}{server:<38}{RESET}{CYAN}║{RESET}")
196
+ print(f"{CYAN}║{RESET} Domain : {WHITE}{domain:<38}{RESET}{CYAN}║{RESET}")
197
+ print(f"{CYAN}║{RESET} Local Port : {WHITE}{local_port:<38}{RESET}{CYAN}║{RESET}")
198
+ print(f"{CYAN}╟───────────────────────────────────────────────────────╢{RESET}")
199
+ print(f"{CYAN}║{RESET} {DIM}Press Ctrl+C to disconnect • Ctrl+D to restart{RESET} {CYAN}║{RESET}")
200
+ print(f"{CYAN}╚═══════════════════════════════════════════════════════╝{RESET}\n")
201
+
202
+
203
+ def handle_first_run():
204
+ clear_screen()
205
+ print_logo()
206
+
207
+ print(f"\n{GREEN}✓{RESET} Welcome to LocalStream!")
208
+ print(f"{DIM} First-time setup - Let's configure your connection{RESET}\n")
209
+
210
+ if not client_exists():
211
+ print(f"\n{YELLOW}⟳{RESET} Downloading slipstream-client...")
212
+ if not download_client():
213
+ print(f"\n{RED}✗{RESET} Failed to download client. Check your internet connection.")
214
+ sys.exit(1)
215
+ print(f"{GREEN}✓{RESET} Download complete!\n")
216
+
217
+ config = prompt_for_config()
218
+ save_config(config)
219
+ print(f"\n{GREEN}✓{RESET} Configuration saved!")
220
+
221
+ print_config(config)
222
+
223
+ input(f"\n {DIM}Press Enter to continue to menu...{RESET}")
224
+ handle_menu()
225
+
226
+
227
+ def handle_connection(config: dict, mode: str):
228
+ while True:
229
+ clear_screen()
230
+ print_logo(mini=True)
231
+ print_connecting_banner(config, mode)
232
+
233
+ manager = ConnectionManager()
234
+
235
+ if mode == "vpn":
236
+ result = manager.connect_vpn(config)
237
+ else:
238
+ result = manager.connect_proxy(config)
239
+
240
+ if result == "restart":
241
+ print(f"\n{YELLOW}⟳{RESET} Restarting connection...")
242
+ continue
243
+ break
244
+
245
+
246
+ def handle_menu():
247
+ while True:
248
+ clear_screen()
249
+ print_logo()
250
+
251
+ config = load_config()
252
+ print_config(config)
253
+
254
+ choice = show_main_menu()
255
+
256
+ if choice == 1:
257
+ clear_screen()
258
+ print_logo(mini=True)
259
+ print_config(config)
260
+
261
+ mode_choice = show_connection_menu()
262
+
263
+ if mode_choice == 1:
264
+ if not is_admin():
265
+ print(f"\n{RED}✗{RESET} VPN Mode requires Administrator privileges!")
266
+ print(f"{YELLOW}!{RESET} Please restart LocalStream as Administrator")
267
+ input(f"\n {DIM}Press Enter to continue...{RESET}")
268
+ continue
269
+
270
+ if not tun2proxy_exists():
271
+ print(f"\n{YELLOW}⟳{RESET} Downloading VPN components...")
272
+ if not download_tun2proxy():
273
+ print(f"\n{RED}✗{RESET} Failed to download VPN components.")
274
+ input(f"\n {DIM}Press Enter to continue...{RESET}")
275
+ continue
276
+
277
+ handle_connection(config, "vpn")
278
+ input(f"\n {DIM}Press Enter to continue...{RESET}")
279
+
280
+ elif mode_choice == 2:
281
+ if not client_exists():
282
+ print(f"\n{YELLOW}⟳{RESET} Downloading client...")
283
+ if not download_client():
284
+ print(f"\n{RED}✗{RESET} Failed to download client.")
285
+ input(f"\n {DIM}Press Enter to continue...{RESET}")
286
+ continue
287
+
288
+ handle_connection(config, "proxy")
289
+ input(f"\n {DIM}Press Enter to continue...{RESET}")
290
+
291
+ elif mode_choice == 3:
292
+ continue
293
+
294
+ elif choice == 2:
295
+ print()
296
+ config = prompt_for_config()
297
+ save_config(config)
298
+ print(f"\n{GREEN}✓{RESET} Configuration updated!")
299
+ input(f"\n {DIM}Press Enter to continue...{RESET}")
300
+
301
+ elif choice == 3:
302
+ clear_screen()
303
+ print(f"\n{CYAN}╔═══════════════════════════════════════╗{RESET}")
304
+ print(f"{CYAN}║{RESET} {GREEN}✓{RESET} Thanks for using LocalStream! {CYAN}║{RESET}")
305
+ print(f"{CYAN}╚═══════════════════════════════════════╝{RESET}\n")
306
+ sys.exit(0)
307
+
308
+
309
+ def main():
310
+ try:
311
+ if config_exists():
312
+ handle_menu()
313
+ else:
314
+ handle_first_run()
315
+ except KeyboardInterrupt:
316
+ print(f"\n\n{GREEN}✓{RESET} Goodbye!")
317
+ sys.exit(0)
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()
@@ -0,0 +1,40 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ def get_config_dir() -> Path:
7
+ config_dir = Path.home() / ".localstream"
8
+ config_dir.mkdir(parents=True, exist_ok=True)
9
+ return config_dir
10
+
11
+
12
+ def get_config_path() -> Path:
13
+ return get_config_dir() / "config.json"
14
+
15
+
16
+ def config_exists() -> bool:
17
+ return get_config_path().exists()
18
+
19
+
20
+ def load_config() -> dict:
21
+ config_path = get_config_path()
22
+ if not config_path.exists():
23
+ return {}
24
+ with open(config_path, "r", encoding="utf-8") as f:
25
+ return json.load(f)
26
+
27
+
28
+ def save_config(config: dict) -> None:
29
+ config_path = get_config_path()
30
+ with open(config_path, "w", encoding="utf-8") as f:
31
+ json.dump(config, f, indent=2)
32
+
33
+
34
+ def get_default_config() -> dict:
35
+ return {
36
+ "server_ip": "",
37
+ "server_port": 53,
38
+ "local_port": 5201,
39
+ "domain": ""
40
+ }
@@ -0,0 +1,403 @@
1
+ import subprocess
2
+ import signal
3
+ import sys
4
+ import threading
5
+ import ctypes
6
+ import re
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from localstream.downloader import get_client_path, get_tun2proxy_path, client_exists, tun2proxy_exists, download_client, download_tun2proxy
11
+
12
+
13
+ CYAN = "\033[96m"
14
+ GREEN = "\033[92m"
15
+ YELLOW = "\033[93m"
16
+ RED = "\033[91m"
17
+ BLUE = "\033[94m"
18
+ MAGENTA = "\033[95m"
19
+ WHITE = "\033[97m"
20
+ DIM = "\033[2m"
21
+ BOLD = "\033[1m"
22
+ RESET = "\033[0m"
23
+
24
+
25
+ LOG_PATTERNS = {
26
+ r"INFO.*Listening on TCP port (\d+)": lambda m: f"{GREEN}●{RESET} Listening on port {WHITE}{m.group(1)}{RESET}",
27
+ r"INFO.*Connection ready": lambda m: f"{GREEN}●{RESET} {GREEN}Connection ready!{RESET}",
28
+ r"INFO.*accepted new client": lambda m: f"{BLUE}→{RESET} New client connected",
29
+ r"INFO.*client disconnected": lambda m: f"{BLUE}←{RESET} Client disconnected",
30
+ r"WARN.*certificate pinning": lambda m: f"{YELLOW}!{RESET} {DIM}Certificate pinning disabled{RESET}",
31
+ r"ERROR|FATAL": lambda m: f"{RED}✗{RESET} {m.group(0)}",
32
+ r"INFO.*bytes.*transferred": lambda m: f"{DIM}↔{RESET} {DIM}Data transferred{RESET}",
33
+ }
34
+
35
+
36
+ def format_log_line(line: str) -> str:
37
+ line = line.strip()
38
+ if not line:
39
+ return None
40
+
41
+ for pattern, formatter in LOG_PATTERNS.items():
42
+ match = re.search(pattern, line, re.IGNORECASE)
43
+ if match:
44
+ return formatter(match)
45
+
46
+ if "INFO" in line or "DEBUG" in line:
47
+ return None
48
+
49
+ return f"{DIM}│{RESET} {line}"
50
+
51
+
52
+ def is_admin() -> bool:
53
+ try:
54
+ return ctypes.windll.shell32.IsUserAnAdmin()
55
+ except:
56
+ return False
57
+
58
+
59
+ def run_as_admin():
60
+ if sys.platform != "win32":
61
+ return False
62
+
63
+ try:
64
+ ctypes.windll.shell32.ShellExecuteW(
65
+ None, "runas", sys.executable, " ".join(sys.argv), None, 1
66
+ )
67
+ return True
68
+ except:
69
+ return False
70
+
71
+
72
+ class ConnectionManager:
73
+ def __init__(self):
74
+ self.slipstream_process = None
75
+ self.tun2proxy_process = None
76
+ self.restart_requested = False
77
+ self.user_disconnected = False
78
+ self.stop_listener = False
79
+ self._original_sigint = None
80
+ self._listener_thread = None
81
+ self._monitor_thread = None
82
+
83
+ def _setup_signal_handlers(self):
84
+ self._original_sigint = signal.signal(signal.SIGINT, self._sigint_handler)
85
+
86
+ def _restore_signal_handlers(self):
87
+ if self._original_sigint:
88
+ signal.signal(signal.SIGINT, self._original_sigint)
89
+
90
+ def _sigint_handler(self, signum, frame):
91
+ print(f"\n\n{YELLOW}⟳{RESET} Disconnecting...")
92
+ self.user_disconnected = True
93
+ self.disconnect()
94
+ raise KeyboardInterrupt()
95
+
96
+ def _keyboard_listener(self):
97
+ try:
98
+ import msvcrt
99
+ while not self.stop_listener:
100
+ if msvcrt.kbhit():
101
+ key = msvcrt.getch()
102
+ if key == b'\x04':
103
+ self.restart_requested = True
104
+ self.disconnect()
105
+ return
106
+ threading.Event().wait(0.1)
107
+ except:
108
+ pass
109
+
110
+ def _start_keyboard_listener(self):
111
+ self.stop_listener = False
112
+ self._listener_thread = threading.Thread(target=self._keyboard_listener, daemon=True)
113
+ self._listener_thread.start()
114
+
115
+ def _stop_keyboard_listener(self):
116
+ self.stop_listener = True
117
+ if self._listener_thread and self._listener_thread.is_alive():
118
+ self._listener_thread.join(timeout=0.5)
119
+
120
+ def connect_proxy(self, config: dict) -> str:
121
+ if not client_exists():
122
+ if not download_client():
123
+ print(f"{RED}✗{RESET} Cannot connect: slipstream-client not available")
124
+ return "error"
125
+
126
+ client_path = get_client_path()
127
+ server_ip = config.get("server_ip", "")
128
+ server_port = config.get("server_port", 53)
129
+ local_port = config.get("local_port", 5201)
130
+ domain = config.get("domain", "")
131
+
132
+ if not server_ip or not domain:
133
+ print(f"{RED}✗{RESET} Invalid configuration: server_ip and domain are required")
134
+ return "error"
135
+
136
+ resolver = f"{server_ip}:{server_port}"
137
+
138
+ cmd = [
139
+ str(client_path),
140
+ "--resolver", resolver,
141
+ "--domain", domain,
142
+ "--tcp-listen-port", str(local_port)
143
+ ]
144
+
145
+ self._setup_signal_handlers()
146
+ self.restart_requested = False
147
+ self.user_disconnected = False
148
+ reconnect_count = 0
149
+
150
+ try:
151
+ while True:
152
+ self.slipstream_process = subprocess.Popen(
153
+ cmd,
154
+ stdout=subprocess.PIPE,
155
+ stderr=subprocess.STDOUT,
156
+ text=True,
157
+ bufsize=1,
158
+ creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
159
+ )
160
+
161
+ self._start_keyboard_listener()
162
+
163
+ if reconnect_count == 0:
164
+ print(f"\n{CYAN}┌{'─' * 50}┐{RESET}")
165
+ print(f"{CYAN}│{RESET} {GREEN}●{RESET} {BOLD}PROXY Mode - Connection Status{RESET}{' ' * 16}{CYAN}│{RESET}")
166
+ print(f"{CYAN}└{'─' * 50}┘{RESET}\n")
167
+ else:
168
+ print(f"\n {GREEN}●{RESET} Reconnected! (attempt #{reconnect_count + 1})\n")
169
+
170
+ for line in self.slipstream_process.stdout:
171
+ if self.restart_requested or self.user_disconnected:
172
+ break
173
+ formatted = format_log_line(line)
174
+ if formatted:
175
+ print(f" {formatted}")
176
+
177
+ self.slipstream_process.wait()
178
+ self._stop_keyboard_listener()
179
+
180
+ if self.restart_requested:
181
+ return "restart"
182
+
183
+ if self.user_disconnected:
184
+ return "done"
185
+
186
+ return_code = self.slipstream_process.returncode
187
+ if return_code != 0:
188
+ reconnect_count += 1
189
+ print(f"\n {YELLOW}!{RESET} Connection dropped (code: {return_code})")
190
+ print(f" {YELLOW}⟳{RESET} Auto-reconnecting in 3 seconds...")
191
+ time.sleep(3)
192
+ continue
193
+
194
+ return "done"
195
+
196
+ except KeyboardInterrupt:
197
+ self._stop_keyboard_listener()
198
+ return "done"
199
+ except FileNotFoundError:
200
+ print(f"{RED}✗{RESET} Client not found: {client_path}")
201
+ return "error"
202
+ except Exception as e:
203
+ print(f"{RED}✗{RESET} Connection error: {e}")
204
+ return "error"
205
+ finally:
206
+ self._restore_signal_handlers()
207
+ self._stop_keyboard_listener()
208
+ self.disconnect()
209
+
210
+ def _monitor_slipstream(self, cmd):
211
+ reconnect_count = 0
212
+ while not self.user_disconnected and not self.restart_requested:
213
+ if self.slipstream_process and self.slipstream_process.poll() is not None:
214
+ return_code = self.slipstream_process.returncode
215
+ if return_code != 0 and not self.user_disconnected:
216
+ reconnect_count += 1
217
+ print(f"\n {YELLOW}!{RESET} Slipstream dropped (code: {return_code})")
218
+ print(f" {YELLOW}⟳{RESET} Auto-reconnecting...")
219
+ time.sleep(2)
220
+
221
+ try:
222
+ self.slipstream_process = subprocess.Popen(
223
+ cmd,
224
+ stdout=subprocess.PIPE,
225
+ stderr=subprocess.STDOUT,
226
+ text=True,
227
+ bufsize=1,
228
+ creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
229
+ )
230
+ time.sleep(2)
231
+ if self.slipstream_process.poll() is None:
232
+ print(f" {GREEN}●{RESET} Slipstream reconnected! (attempt #{reconnect_count + 1})")
233
+ except:
234
+ pass
235
+ time.sleep(1)
236
+
237
+ def connect_vpn(self, config: dict) -> str:
238
+ if not is_admin():
239
+ print(f"\n{RED}✗{RESET} VPN Mode requires Administrator privileges!")
240
+ print(f"{YELLOW}!{RESET} Please run LocalStream as Administrator")
241
+ return "error"
242
+
243
+ if not client_exists():
244
+ if not download_client():
245
+ print(f"{RED}✗{RESET} Cannot connect: slipstream-client not available")
246
+ return "error"
247
+
248
+ if not tun2proxy_exists():
249
+ if not download_tun2proxy():
250
+ print(f"{RED}✗{RESET} Cannot connect: tun2proxy not available")
251
+ return "error"
252
+
253
+ client_path = get_client_path()
254
+ tun2proxy_path = get_tun2proxy_path()
255
+ server_ip = config.get("server_ip", "")
256
+ server_port = config.get("server_port", 53)
257
+ local_port = config.get("local_port", 5201)
258
+ domain = config.get("domain", "")
259
+
260
+ if not server_ip or not domain:
261
+ print(f"{RED}✗{RESET} Invalid configuration: server_ip and domain are required")
262
+ return "error"
263
+
264
+ resolver = f"{server_ip}:{server_port}"
265
+
266
+ slipstream_cmd = [
267
+ str(client_path),
268
+ "--resolver", resolver,
269
+ "--domain", domain,
270
+ "--tcp-listen-port", str(local_port)
271
+ ]
272
+
273
+ tun2proxy_cmd = [
274
+ str(tun2proxy_path),
275
+ "--setup",
276
+ "--proxy", f"socks5://127.0.0.1:{local_port}",
277
+ "--bypass", server_ip,
278
+ "--dns", "virtual"
279
+ ]
280
+
281
+ self._setup_signal_handlers()
282
+ self.restart_requested = False
283
+ self.user_disconnected = False
284
+
285
+ try:
286
+ print(f"\n{CYAN}┌{'─' * 50}┐{RESET}")
287
+ print(f"{CYAN}│{RESET} {GREEN}●{RESET} {BOLD}VPN Mode - Starting Tunnel{RESET}{' ' * 21}{CYAN}│{RESET}")
288
+ print(f"{CYAN}└{'─' * 50}┘{RESET}\n")
289
+
290
+ print(f" {YELLOW}⟳{RESET} Starting slipstream-client...")
291
+ self.slipstream_process = subprocess.Popen(
292
+ slipstream_cmd,
293
+ stdout=subprocess.PIPE,
294
+ stderr=subprocess.STDOUT,
295
+ text=True,
296
+ bufsize=1,
297
+ creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
298
+ )
299
+
300
+ time.sleep(2)
301
+
302
+ if self.slipstream_process.poll() is not None:
303
+ print(f" {RED}✗{RESET} Slipstream failed to start")
304
+ return "error"
305
+
306
+ print(f" {GREEN}✓{RESET} Slipstream-client running on port {local_port}")
307
+
308
+ print(f" {YELLOW}⟳{RESET} Starting tun2proxy (VPN tunnel)...")
309
+ self.tun2proxy_process = subprocess.Popen(
310
+ tun2proxy_cmd,
311
+ stdout=subprocess.PIPE,
312
+ stderr=subprocess.STDOUT,
313
+ text=True,
314
+ bufsize=1
315
+ )
316
+
317
+ time.sleep(2)
318
+
319
+ if self.tun2proxy_process.poll() is not None:
320
+ print(f" {RED}✗{RESET} tun2proxy failed to start")
321
+ self.disconnect()
322
+ return "error"
323
+
324
+ print(f" {GREEN}✓{RESET} VPN tunnel established!")
325
+ print(f"\n{GREEN}{'═' * 52}{RESET}")
326
+ print(f"{GREEN} ✓ All traffic is now routed through the VPN!{RESET}")
327
+ print(f"{GREEN}{'═' * 52}{RESET}")
328
+ print(f"\n {DIM}Press Ctrl+C to disconnect{RESET}\n")
329
+
330
+ self._monitor_thread = threading.Thread(
331
+ target=self._monitor_slipstream,
332
+ args=(slipstream_cmd,),
333
+ daemon=True
334
+ )
335
+ self._monitor_thread.start()
336
+
337
+ self._start_keyboard_listener()
338
+
339
+ ignore_patterns = [
340
+ "brokenpipe",
341
+ "tcp connection closed",
342
+ "connection closed",
343
+ "connectionnotallowed",
344
+ "ending #",
345
+ "info tun2proxy",
346
+ ]
347
+
348
+ for line in self.tun2proxy_process.stdout:
349
+ if self.restart_requested:
350
+ break
351
+ line = line.strip()
352
+ if not line:
353
+ continue
354
+
355
+ line_lower = line.lower()
356
+ if any(pattern in line_lower for pattern in ignore_patterns):
357
+ continue
358
+
359
+ if "error" in line_lower or "fatal" in line_lower:
360
+ print(f" {RED}✗{RESET} {line}")
361
+
362
+ self.tun2proxy_process.wait()
363
+ self._stop_keyboard_listener()
364
+
365
+ if self.restart_requested:
366
+ return "restart"
367
+
368
+ return "done"
369
+
370
+ except KeyboardInterrupt:
371
+ self._stop_keyboard_listener()
372
+ return "done"
373
+ except FileNotFoundError as e:
374
+ print(f"{RED}✗{RESET} Binary not found: {e}")
375
+ return "error"
376
+ except Exception as e:
377
+ print(f"{RED}✗{RESET} Connection error: {e}")
378
+ return "error"
379
+ finally:
380
+ self._restore_signal_handlers()
381
+ self._stop_keyboard_listener()
382
+ self.disconnect()
383
+
384
+ def disconnect(self):
385
+ self.user_disconnected = True
386
+
387
+ if self.tun2proxy_process and self.tun2proxy_process.poll() is None:
388
+ self.tun2proxy_process.terminate()
389
+ try:
390
+ self.tun2proxy_process.wait(timeout=5)
391
+ except subprocess.TimeoutExpired:
392
+ self.tun2proxy_process.kill()
393
+ print(f"{GREEN}✓{RESET} VPN tunnel closed")
394
+ self.tun2proxy_process = None
395
+
396
+ if self.slipstream_process and self.slipstream_process.poll() is None:
397
+ self.slipstream_process.terminate()
398
+ try:
399
+ self.slipstream_process.wait(timeout=5)
400
+ except subprocess.TimeoutExpired:
401
+ self.slipstream_process.kill()
402
+ print(f"{GREEN}✓{RESET} Slipstream disconnected")
403
+ self.slipstream_process = None
@@ -0,0 +1,197 @@
1
+ import os
2
+ import sys
3
+ import requests
4
+ import zipfile
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ from localstream.config import get_config_dir
9
+
10
+
11
+ CYAN = "\033[96m"
12
+ GREEN = "\033[92m"
13
+ YELLOW = "\033[93m"
14
+ RED = "\033[91m"
15
+ DIM = "\033[2m"
16
+ RESET = "\033[0m"
17
+
18
+
19
+ SLIPSTREAM_URL = "https://github.com/AliRezaBeigy/slipstream-rust-deploy/releases/latest/download/slipstream-client-windows-amd64.exe"
20
+ TUN2PROXY_URL = "https://github.com/tun2proxy/tun2proxy/releases/download/v0.7.19/tun2proxy-x86_64-pc-windows-msvc.zip"
21
+ WINTUN_URL = "https://www.wintun.net/builds/wintun-0.14.1.zip"
22
+
23
+
24
+ def get_bin_dir() -> Path:
25
+ bin_dir = get_config_dir() / "bin"
26
+ bin_dir.mkdir(parents=True, exist_ok=True)
27
+ return bin_dir
28
+
29
+
30
+ def get_client_path() -> Path:
31
+ return get_bin_dir() / "slipstream-client.exe"
32
+
33
+
34
+ def get_tun2proxy_path() -> Path:
35
+ return get_bin_dir() / "tun2proxy.exe"
36
+
37
+
38
+ def get_wintun_path() -> Path:
39
+ return get_bin_dir() / "wintun.dll"
40
+
41
+
42
+ def client_exists() -> bool:
43
+ return get_client_path().exists()
44
+
45
+
46
+ def tun2proxy_exists() -> bool:
47
+ return get_tun2proxy_path().exists() and get_wintun_path().exists()
48
+
49
+
50
+ def download_file(url: str, dest_path: Path, name: str) -> bool:
51
+ print(f"\n{CYAN}╔═══════════════════════════════════════════════════════╗{RESET}")
52
+ print(f"{CYAN}║{RESET} {YELLOW}⟳{RESET} Downloading {name}...{' ' * (37 - len(name))}{CYAN}║{RESET}")
53
+ print(f"{CYAN}╚═══════════════════════════════════════════════════════╝{RESET}")
54
+ print(f"\n{DIM} URL: {url[:50]}...{RESET}")
55
+ print(f"{DIM} Destination: {dest_path}{RESET}\n")
56
+
57
+ try:
58
+ response = requests.get(url, stream=True, timeout=120)
59
+ response.raise_for_status()
60
+
61
+ total_size = int(response.headers.get("content-length", 0))
62
+ downloaded = 0
63
+
64
+ with open(dest_path, "wb") as f:
65
+ for chunk in response.iter_content(chunk_size=8192):
66
+ if chunk:
67
+ f.write(chunk)
68
+ downloaded += len(chunk)
69
+ if total_size > 0:
70
+ percent = (downloaded / total_size) * 100
71
+ bar_length = 40
72
+ filled = int(bar_length * downloaded / total_size)
73
+ bar = f"{GREEN}{'█' * filled}{DIM}{'░' * (bar_length - filled)}{RESET}"
74
+ size_mb = downloaded / (1024 * 1024)
75
+ total_mb = total_size / (1024 * 1024)
76
+ print(f"\r {bar} {percent:5.1f}% ({size_mb:.1f}/{total_mb:.1f} MB)", end="", flush=True)
77
+
78
+ print(f"\n\n{GREEN}✓{RESET} Download complete!")
79
+ return True
80
+
81
+ except requests.RequestException as e:
82
+ print(f"\n\n{RED}✗{RESET} Download failed: {e}")
83
+ if dest_path.exists():
84
+ dest_path.unlink()
85
+ return False
86
+
87
+
88
+ def download_client(force: bool = False) -> bool:
89
+ client_path = get_client_path()
90
+
91
+ if client_path.exists() and not force:
92
+ return True
93
+
94
+ return download_file(SLIPSTREAM_URL, client_path, "slipstream-client")
95
+
96
+
97
+ def download_tun2proxy(force: bool = False) -> bool:
98
+ tun2proxy_path = get_tun2proxy_path()
99
+ wintun_path = get_wintun_path()
100
+ bin_dir = get_bin_dir()
101
+
102
+ if tun2proxy_path.exists() and wintun_path.exists() and not force:
103
+ return True
104
+
105
+ if not tun2proxy_path.exists() or force:
106
+ print(f"\n{CYAN}╔═══════════════════════════════════════════════════════╗{RESET}")
107
+ print(f"{CYAN}║{RESET} {YELLOW}⟳{RESET} Downloading tun2proxy... {CYAN}║{RESET}")
108
+ print(f"{CYAN}╚═══════════════════════════════════════════════════════╝{RESET}")
109
+ print(f"\n{DIM} URL: {TUN2PROXY_URL[:50]}...{RESET}\n")
110
+
111
+ try:
112
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
113
+ tmp_path = Path(tmp.name)
114
+
115
+ response = requests.get(TUN2PROXY_URL, stream=True, timeout=120)
116
+ response.raise_for_status()
117
+
118
+ total_size = int(response.headers.get("content-length", 0))
119
+ downloaded = 0
120
+
121
+ with open(tmp_path, "wb") as f:
122
+ for chunk in response.iter_content(chunk_size=8192):
123
+ if chunk:
124
+ f.write(chunk)
125
+ downloaded += len(chunk)
126
+ if total_size > 0:
127
+ percent = (downloaded / total_size) * 100
128
+ bar_length = 40
129
+ filled = int(bar_length * downloaded / total_size)
130
+ bar = f"{GREEN}{'█' * filled}{DIM}{'░' * (bar_length - filled)}{RESET}"
131
+ size_mb = downloaded / (1024 * 1024)
132
+ total_mb = total_size / (1024 * 1024)
133
+ print(f"\r {bar} {percent:5.1f}% ({size_mb:.1f}/{total_mb:.1f} MB)", end="", flush=True)
134
+
135
+ print(f"\n\n{YELLOW}⟳{RESET} Extracting tun2proxy...")
136
+
137
+ with zipfile.ZipFile(tmp_path, 'r') as zip_ref:
138
+ for file_info in zip_ref.namelist():
139
+ if file_info.endswith("tun2proxy-bin.exe") or file_info.endswith("tun2proxy.exe"):
140
+ with zip_ref.open(file_info) as src:
141
+ with open(tun2proxy_path, "wb") as dst:
142
+ dst.write(src.read())
143
+ break
144
+ elif file_info.endswith(".exe") and "tun2proxy" in file_info:
145
+ with zip_ref.open(file_info) as src:
146
+ with open(tun2proxy_path, "wb") as dst:
147
+ dst.write(src.read())
148
+ break
149
+
150
+ tmp_path.unlink()
151
+
152
+ if not tun2proxy_path.exists():
153
+ print(f"{RED}✗{RESET} Failed to extract tun2proxy.exe from archive")
154
+ return False
155
+
156
+ print(f"{GREEN}✓{RESET} tun2proxy extracted!")
157
+
158
+ except Exception as e:
159
+ print(f"\n{RED}✗{RESET} Failed to download tun2proxy: {e}")
160
+ return False
161
+
162
+ if not wintun_path.exists() or force:
163
+ print(f"\n{YELLOW}⟳{RESET} Downloading wintun driver...")
164
+
165
+ try:
166
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
167
+ tmp_path = Path(tmp.name)
168
+
169
+ response = requests.get(WINTUN_URL, timeout=120)
170
+ response.raise_for_status()
171
+
172
+ with open(tmp_path, "wb") as f:
173
+ f.write(response.content)
174
+
175
+ print(f"{YELLOW}⟳{RESET} Extracting wintun.dll...")
176
+
177
+ with zipfile.ZipFile(tmp_path, 'r') as zip_ref:
178
+ for file_info in zip_ref.namelist():
179
+ if file_info.endswith("amd64/wintun.dll"):
180
+ with zip_ref.open(file_info) as src:
181
+ with open(wintun_path, "wb") as dst:
182
+ dst.write(src.read())
183
+ break
184
+
185
+ tmp_path.unlink()
186
+
187
+ if not wintun_path.exists():
188
+ print(f"{RED}✗{RESET} Failed to extract wintun.dll from archive")
189
+ return False
190
+
191
+ print(f"{GREEN}✓{RESET} Wintun driver extracted!")
192
+
193
+ except Exception as e:
194
+ print(f"{RED}✗{RESET} Failed to download wintun: {e}")
195
+ return False
196
+
197
+ return True
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: localstream
3
+ Version: 1.0.0
4
+ Summary: Python CLI client for slipstream-rust DNS tunnel
5
+ Author: LocalStream Team
6
+ License: MIT
7
+ Keywords: dns,tunnel,vpn,slipstream,cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Internet :: Proxy Servers
15
+ Requires-Python: >=3.13
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: requests>=2.31.0
18
+ Requires-Dist: colorama>=0.4.6
19
+
20
+ # LocalStream
21
+
22
+ A Python command-line client for [slipstream-rust](https://github.com/Mygod/slipstream-rust) DNS tunnel.
23
+
24
+ ## Features
25
+
26
+ - **VPN Mode**: System-wide traffic tunneling (requires Admin)
27
+ - **PROXY Mode**: SOCKS5 proxy on local port
28
+ - Auto-reconnect on connection drops
29
+ - Easy pip installation
30
+ - Colorful CLI interface
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.13+
35
+ - Windows OS
36
+ - Administrator privileges (for VPN Mode)
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install -e .
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```bash
47
+ LocalStream # Normal user (PROXY mode only)
48
+ # Run as Administrator for VPN mode
49
+ ```
50
+
51
+ ### Menu Options
52
+
53
+ ```
54
+ [1] Connect to server
55
+ ├── [1] VPN Mode (system-wide, requires Admin)
56
+ ├── [2] PROXY Mode (SOCKS5 only)
57
+ └── [3] Back
58
+ [2] Edit configuration
59
+ [3] Exit
60
+ ```
61
+
62
+ ### Keyboard Shortcuts
63
+
64
+ - **Ctrl+C**: Disconnect and exit
65
+ - **Ctrl+D**: Restart connection
66
+
67
+ ## Configuration
68
+
69
+ Config stored in `~/.localstream/config.json`:
70
+
71
+ ```json
72
+ {
73
+ "server_ip": "203.0.113.2",
74
+ "server_port": 53,
75
+ "local_port": 5201,
76
+ "domain": "s.example.com"
77
+ }
78
+ ```
79
+
80
+ ## Downloaded Binaries
81
+
82
+ Stored in `~/.localstream/bin/`:
83
+ - `slipstream-client.exe` - DNS tunnel client
84
+ - `tun2proxy.exe` - TUN adapter for VPN mode
85
+ - `wintun.dll` - Windows TUN driver
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ localstream/__init__.py
4
+ localstream/__main__.py
5
+ localstream/cli.py
6
+ localstream/config.py
7
+ localstream/connection.py
8
+ localstream/downloader.py
9
+ localstream.egg-info/PKG-INFO
10
+ localstream.egg-info/SOURCES.txt
11
+ localstream.egg-info/dependency_links.txt
12
+ localstream.egg-info/entry_points.txt
13
+ localstream.egg-info/requires.txt
14
+ localstream.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ LocalStream = localstream.cli:main
@@ -0,0 +1,2 @@
1
+ requests>=2.31.0
2
+ colorama>=0.4.6
@@ -0,0 +1 @@
1
+ localstream
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "localstream"
7
+ version = "1.0.0"
8
+ description = "Python CLI client for slipstream-rust DNS tunnel"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "LocalStream Team"}
14
+ ]
15
+ keywords = ["dns", "tunnel", "vpn", "slipstream", "cli"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: End Users/Desktop",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: Microsoft :: Windows",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Internet :: Proxy Servers",
24
+ ]
25
+ dependencies = [
26
+ "requests>=2.31.0",
27
+ "colorama>=0.4.6",
28
+ ]
29
+
30
+ [project.scripts]
31
+ LocalStream = "localstream.cli:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["."]
35
+ include = ["localstream*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+