tunnelgo-client 2.20.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,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: tunnelgo-client
3
+ Version: 2.20.0
4
+ Summary: TunnelGo 内网穿透客户端 — 通过 pip 一键安装,支持 double-fork 守护进程模式
5
+ Author: TunnelGo Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ctz168/tunnelgo
8
+ Project-URL: Repository, https://github.com/ctz168/tunnelgo
9
+ Project-URL: Issues, https://github.com/ctz168/tunnelgo/issues
10
+ Keywords: tunnel,ngrok,nat-traversal,reverse-proxy,daemon
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Internet :: Proxy Servers
24
+ Classifier: Topic :: System :: Networking
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+
28
+ # TunnelGo Client (Python)
29
+
30
+ TunnelGo 内网穿透客户端的 Python 封装包。通过 `pip` 一键安装,自动下载对应平台的 Go 二进制文件,支持 double-fork 守护进程模式。
31
+
32
+ ## 安装
33
+
34
+ ```bash
35
+ pip install tunnelgo-client
36
+ ```
37
+
38
+ 安装后会自动下载对应平台(Linux/macOS/Windows,amd64/arm64)的 Go 二进制文件到 `~/.tunnelgo/bin/`。
39
+
40
+ ## 使用
41
+
42
+ ```bash
43
+ # 基本模式
44
+ tunnelgo-client --key YOUR_TOKEN --port 8080
45
+
46
+ # 子域名模式(推荐)
47
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp
48
+
49
+ # 守护进程模式(Python 原生 double-fork,脱离终端)
50
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --daemon
51
+
52
+ # 停止守护进程
53
+ tunnelgo-client --stop
54
+
55
+ # 子域名 + TCP 转发
56
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp --tcp-ports 22,3306
57
+
58
+ # 禁用 P2P 直连
59
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --no-p2p
60
+
61
+ # 强制更新客户端二进制
62
+ tunnelgo-client --update
63
+ ```
64
+
65
+ ## Double-Fork 守护进程模式
66
+
67
+ `--daemon` 参数启动 Python 原生的 double-fork 守护进程模式:
68
+
69
+ 1. 第一次 fork → 创建中间子进程
70
+ 2. 中间子进程调用 `setsid()` 成为新会话组长
71
+ 3. 第二次 fork → 创建孙进程(实际守护进程)
72
+ 4. 中间子进程退出
73
+ 5. 孙进程被 init (PID 1) 收养,完全脱离终端
74
+
75
+ 守护进程的日志输出到 `/tmp/tunnelgo-client.log`(Windows 为 `%TEMP%\tunnelgo-client.log`)。
76
+
77
+ ## 参数
78
+
79
+ | 参数 | 说明 | 默认值 |
80
+ |------|------|--------|
81
+ | `-k, --key` | 认证令牌(必填) | - |
82
+ | `-p, --port` | 本地服务端口 | 8080 |
83
+ | `-s, --server` | 服务器地址 | aicq.online:6639 |
84
+ | `--host` | 本地服务地址 | localhost |
85
+ | `--p2p-port` | P2P 监听端口 | 与 --port 相同 |
86
+ | `--no-p2p` | 禁用 P2P 直连 | false |
87
+ | `--tcp-ports` | TCP 转发端口,逗号分隔 | - |
88
+ | `--tcp-default` | 设为非TLS同端口默认路由 | false |
89
+ | `--http-port` | HTTP 独立端口模式 | false |
90
+ | `--subdomain` | 子域名前缀 | - |
91
+ | `--daemon` | 以守护进程模式运行 (double-fork) | false |
92
+ | `--stop` | 停止守护进程 | false |
93
+ | `--update` | 强制更新客户端二进制 | false |
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,70 @@
1
+ # TunnelGo Client (Python)
2
+
3
+ TunnelGo 内网穿透客户端的 Python 封装包。通过 `pip` 一键安装,自动下载对应平台的 Go 二进制文件,支持 double-fork 守护进程模式。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install tunnelgo-client
9
+ ```
10
+
11
+ 安装后会自动下载对应平台(Linux/macOS/Windows,amd64/arm64)的 Go 二进制文件到 `~/.tunnelgo/bin/`。
12
+
13
+ ## 使用
14
+
15
+ ```bash
16
+ # 基本模式
17
+ tunnelgo-client --key YOUR_TOKEN --port 8080
18
+
19
+ # 子域名模式(推荐)
20
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp
21
+
22
+ # 守护进程模式(Python 原生 double-fork,脱离终端)
23
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --daemon
24
+
25
+ # 停止守护进程
26
+ tunnelgo-client --stop
27
+
28
+ # 子域名 + TCP 转发
29
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp --tcp-ports 22,3306
30
+
31
+ # 禁用 P2P 直连
32
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --no-p2p
33
+
34
+ # 强制更新客户端二进制
35
+ tunnelgo-client --update
36
+ ```
37
+
38
+ ## Double-Fork 守护进程模式
39
+
40
+ `--daemon` 参数启动 Python 原生的 double-fork 守护进程模式:
41
+
42
+ 1. 第一次 fork → 创建中间子进程
43
+ 2. 中间子进程调用 `setsid()` 成为新会话组长
44
+ 3. 第二次 fork → 创建孙进程(实际守护进程)
45
+ 4. 中间子进程退出
46
+ 5. 孙进程被 init (PID 1) 收养,完全脱离终端
47
+
48
+ 守护进程的日志输出到 `/tmp/tunnelgo-client.log`(Windows 为 `%TEMP%\tunnelgo-client.log`)。
49
+
50
+ ## 参数
51
+
52
+ | 参数 | 说明 | 默认值 |
53
+ |------|------|--------|
54
+ | `-k, --key` | 认证令牌(必填) | - |
55
+ | `-p, --port` | 本地服务端口 | 8080 |
56
+ | `-s, --server` | 服务器地址 | aicq.online:6639 |
57
+ | `--host` | 本地服务地址 | localhost |
58
+ | `--p2p-port` | P2P 监听端口 | 与 --port 相同 |
59
+ | `--no-p2p` | 禁用 P2P 直连 | false |
60
+ | `--tcp-ports` | TCP 转发端口,逗号分隔 | - |
61
+ | `--tcp-default` | 设为非TLS同端口默认路由 | false |
62
+ | `--http-port` | HTTP 独立端口模式 | false |
63
+ | `--subdomain` | 子域名前缀 | - |
64
+ | `--daemon` | 以守护进程模式运行 (double-fork) | false |
65
+ | `--stop` | 停止守护进程 | false |
66
+ | `--update` | 强制更新客户端二进制 | false |
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tunnelgo-client"
7
+ version = "2.20.0"
8
+ description = "TunnelGo 内网穿透客户端 — 通过 pip 一键安装,支持 double-fork 守护进程模式"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ {name = "TunnelGo Contributors"},
14
+ ]
15
+ keywords = ["tunnel", "ngrok", "nat-traversal", "reverse-proxy", "daemon"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: System Administrators",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Topic :: Internet :: Proxy Servers",
30
+ "Topic :: System :: Networking",
31
+ ]
32
+
33
+ [project.scripts]
34
+ tunnelgo-client = "tunnelgo_client.cli:main"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/ctz168/tunnelgo"
38
+ Repository = "https://github.com/ctz168/tunnelgo"
39
+ Issues = "https://github.com/ctz168/tunnelgo/issues"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """
2
+ TunnelGo Client — 内网穿透客户端 Python 封装
3
+
4
+ 通过 pip 安装后可直接使用 tunnelgo-client 命令。
5
+ 自动从 GitHub Releases 下载对应平台的 Go 二进制文件。
6
+ 支持 double-fork 守护进程模式(--daemon)。
7
+ """
8
+
9
+ __version__ = "2.20.0"
10
+ __author__ = "TunnelGo Contributors"
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m tunnelgo_client`."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,130 @@
1
+ """
2
+ CLI entry point for the tunnelgo-client Python package.
3
+
4
+ Provides the same interface as the Go binary, plus automatic
5
+ binary download and Python-native double-fork daemon mode.
6
+ """
7
+
8
+ import argparse
9
+ import os
10
+ import subprocess
11
+ import sys
12
+
13
+ from . import __version__
14
+ from .downloader import ensure_binary
15
+ from .daemon import double_fork_daemon, stop_daemon
16
+
17
+
18
+ def main():
19
+ """Main CLI entry point."""
20
+ parser = argparse.ArgumentParser(
21
+ prog="tunnelgo-client",
22
+ description=f"TunnelGo 客户端 v{__version__} (Python 封装,自动下载 Go 二进制)",
23
+ formatter_class=argparse.RawDescriptionHelpFormatter,
24
+ epilog="""
25
+ 示例:
26
+ tunnelgo-client --key YOUR_TOKEN --port 8080
27
+ tunnelgo-client -k YOUR_TOKEN -p 3000 -s aicq.online:6639
28
+ tunnelgo-client -k YOUR_TOKEN -p 80 --no-p2p
29
+ tunnelgo-client -k TOKEN -p 80 --daemon 后台守护进程运行 (double-fork)
30
+ tunnelgo-client --stop 停止守护进程
31
+
32
+ P2P 模式 (默认启用):
33
+ 优先级: IPv6 直连 > UPnP IPv4 > 服务器中转
34
+ """,
35
+ )
36
+
37
+ parser.add_argument("-k", "--key", default="", help="认证令牌 (必须)")
38
+ parser.add_argument("-p", "--port", type=int, default=8080, help="本地服务端口 (默认: 8080)")
39
+ parser.add_argument("-s", "--server", default="aicq.online:6639", help="服务器地址 (默认: aicq.online:6639)")
40
+ parser.add_argument("--host", default="localhost", help="本地服务地址 (默认: localhost)")
41
+ parser.add_argument("--p2p-port", type=int, default=0, help="P2P 监听端口 (默认: 与 --port 相同)")
42
+ parser.add_argument("--no-p2p", action="store_true", help="禁用 P2P 直连,强制使用服务器中转模式")
43
+ parser.add_argument("--tcp-ports", default="", help="TCP 转发端口,逗号分隔 (如: 22,3306)")
44
+ parser.add_argument("--http-port", action="store_true", help="启用 HTTP 独立端口模式")
45
+ parser.add_argument("--subdomain", default="", help="子域名前缀 (如: myapp)")
46
+ parser.add_argument("--tcp-default", action="store_true", help="设为非TLS同端口默认路由")
47
+ parser.add_argument("--daemon", action="store_true", help="以守护进程模式运行 (double-fork,脱离终端)")
48
+ parser.add_argument("--stop", action="store_true", help="停止守护进程 (通过 PID 文件)")
49
+ parser.add_argument("--update", action="store_true", help="强制更新到最新版本的客户端二进制")
50
+ parser.add_argument("--version", action="version", version=f"tunnelgo-client v{__version__} (Python)")
51
+
52
+ args = parser.parse_args()
53
+
54
+ # Handle --stop
55
+ if args.stop:
56
+ stop_daemon()
57
+ return
58
+
59
+ # Validate required arguments
60
+ if not args.key:
61
+ parser.error("必须提供认证令牌 (--key)")
62
+
63
+ # Ensure the Go binary is available
64
+ try:
65
+ binary_path = ensure_binary()
66
+ except RuntimeError as e:
67
+ print(f"错误: {e}", file=sys.stderr)
68
+ sys.exit(1)
69
+
70
+ # Handle --update
71
+ if args.update:
72
+ from .downloader import download_binary
73
+ try:
74
+ binary_path = download_binary(force=True)
75
+ print(f" 客户端已更新: {binary_path}")
76
+ except RuntimeError as e:
77
+ print(f"更新失败: {e}", file=sys.stderr)
78
+ sys.exit(1)
79
+ return
80
+
81
+ # Build Go binary arguments
82
+ go_args = []
83
+ go_args.extend(["-k", args.key])
84
+ go_args.extend(["-p", str(args.port)])
85
+ go_args.extend(["-s", args.server])
86
+ go_args.extend(["--host", args.host])
87
+
88
+ if args.p2p_port:
89
+ go_args.extend(["--p2p-port", str(args.p2p_port)])
90
+
91
+ if args.no_p2p:
92
+ go_args.append("--no-p2p")
93
+
94
+ if args.tcp_ports:
95
+ go_args.extend(["--tcp-ports", args.tcp_ports])
96
+
97
+ if args.http_port:
98
+ go_args.append("--http-port")
99
+
100
+ if args.subdomain:
101
+ go_args.extend(["--subdomain", args.subdomain])
102
+
103
+ if args.tcp_default:
104
+ go_args.append("--tcp-default")
105
+
106
+ # Handle --daemon: use Python-native double-fork
107
+ if args.daemon:
108
+ # Do NOT pass --daemon to Go binary — Python handles the double-fork
109
+ # The Go binary will run in the foreground inside the daemon process
110
+ double_fork_daemon(binary_path, go_args)
111
+ return
112
+
113
+ # Normal mode: run the Go binary in the foreground
114
+ try:
115
+ result = subprocess.run(
116
+ [binary_path] + go_args,
117
+ stdin=sys.stdin,
118
+ stdout=sys.stdout,
119
+ stderr=sys.stderr,
120
+ )
121
+ sys.exit(result.returncode)
122
+ except KeyboardInterrupt:
123
+ sys.exit(0)
124
+ except FileNotFoundError:
125
+ print(f"错误: 找不到客户端二进制: {binary_path}", file=sys.stderr)
126
+ sys.exit(1)
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
@@ -0,0 +1,212 @@
1
+ """
2
+ Double-fork daemon mode for TunnelGo client.
3
+
4
+ Implements the classic Unix double-fork pattern in Python:
5
+ 1. First fork → child_1 (intermediate)
6
+ 2. Child_1: setsid() to become session leader
7
+ 3. Second fork → child_2 (grandchild, the actual daemon)
8
+ 4. Child_1 exits
9
+ 5. Grandchild: redirect stdio, write PID file, run the client
10
+
11
+ This ensures the daemon is fully detached from the terminal
12
+ and cannot accidentally acquire a controlling terminal.
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import signal
18
+ import subprocess
19
+ import time
20
+
21
+
22
+ def pid_file_path():
23
+ """Return the path to the PID file."""
24
+ return "/tmp/tunnelgo-client.pid"
25
+
26
+
27
+ def log_file_path():
28
+ """Return the path to the daemon log file."""
29
+ if sys.platform == "win32":
30
+ return os.path.join(os.environ.get("TEMP", os.environ.get("TMP", "C:\\Temp")), "tunnelgo-client.log")
31
+ return "/tmp/tunnelgo-client.log"
32
+
33
+
34
+ def write_pid_file():
35
+ """Write the current PID to the PID file."""
36
+ with open(pid_file_path(), "w") as f:
37
+ f.write(str(os.getpid()))
38
+
39
+
40
+ def remove_pid_file():
41
+ """Remove the PID file."""
42
+ try:
43
+ os.remove(pid_file_path())
44
+ except FileNotFoundError:
45
+ pass
46
+
47
+
48
+ def stop_daemon():
49
+ """Stop the daemon process via PID file."""
50
+ try:
51
+ with open(pid_file_path(), "r") as f:
52
+ pid = int(f.read().strip())
53
+ except (FileNotFoundError, ValueError):
54
+ print("守护进程未运行 (PID 文件不存在)", file=sys.stderr)
55
+ sys.exit(1)
56
+
57
+ try:
58
+ os.kill(pid, signal.SIGTERM)
59
+ except ProcessLookupError:
60
+ print(f"进程 {pid} 不存在", file=sys.stderr)
61
+ except PermissionError:
62
+ print(f"无权限终止进程 {pid}", file=sys.stderr)
63
+ sys.exit(1)
64
+ except OSError as e:
65
+ print(f"终止进程 {pid} 失败: {e}", file=sys.stderr)
66
+ sys.exit(1)
67
+
68
+ remove_pid_file()
69
+ print(f" 守护进程已停止 (PID: {pid})")
70
+
71
+
72
+ def double_fork_daemon(binary_path, args):
73
+ """
74
+ Start the TunnelGo client as a double-fork daemon.
75
+
76
+ This implements the classic Unix double-fork pattern:
77
+ 1. Fork → child (intermediate process)
78
+ 2. Child: setsid() to become session leader
79
+ 3. Fork again → grandchild (actual daemon)
80
+ 4. Child exits
81
+ 5. Grandchild: redirect stdio, write PID, exec the Go binary
82
+
83
+ On Windows, this falls back to a simple detached subprocess.
84
+
85
+ Args:
86
+ binary_path: Path to the Go client binary.
87
+ args: List of command-line arguments to pass to the binary.
88
+ """
89
+ if sys.platform == "win32":
90
+ _daemon_windows(binary_path, args)
91
+ return
92
+
93
+ # Unix: true double-fork
94
+ _daemon_unix(binary_path, args)
95
+
96
+
97
+ def _daemon_unix(binary_path, args):
98
+ """Double-fork daemon implementation for Unix systems."""
99
+ # Build the full command
100
+ cmd = [binary_path] + args
101
+
102
+ # First fork
103
+ try:
104
+ pid = os.fork()
105
+ except OSError as e:
106
+ print(f"守护进程错误: 第一次 fork 失败: {e}", file=sys.stderr)
107
+ sys.exit(1)
108
+
109
+ if pid > 0:
110
+ # Parent process: wait briefly for daemon to start, then return
111
+ # The child will fork again and the grandchild will be the actual daemon
112
+ os.waitpid(pid, 0) # Wait for intermediate child to exit
113
+
114
+ # Wait for PID file to appear
115
+ for _ in range(20):
116
+ try:
117
+ with open(pid_file_path(), "r") as f:
118
+ daemon_pid = int(f.read().strip())
119
+ if daemon_pid > 0:
120
+ print(f" 守护进程已启动 (PID: {daemon_pid})")
121
+ break
122
+ except (FileNotFoundError, ValueError):
123
+ pass
124
+ time.sleep(0.1)
125
+ else:
126
+ print(" 守护进程已启动 (PID 文件尚未写入,请查看日志)")
127
+
128
+ print(f" 日志文件: {log_file_path()}")
129
+ print(" 停止命令: tunnelgo-client --stop")
130
+ return
131
+
132
+ # First fork child (intermediate): create new session, then fork again
133
+ os.setsid() # Become session leader, detach from controlling terminal
134
+
135
+ # Second fork
136
+ try:
137
+ pid2 = os.fork()
138
+ except OSError as e:
139
+ print(f"守护进程错误: 第二次 fork 失败: {e}", file=sys.stderr)
140
+ os._exit(1)
141
+
142
+ if pid2 > 0:
143
+ # Intermediate child exits — grandchild is adopted by init (PID 1)
144
+ os._exit(0)
145
+
146
+ # Grandchild: the actual daemon process
147
+ # Redirect standard file descriptors
148
+ _redirect_stdio()
149
+
150
+ # Write PID file
151
+ write_pid_file()
152
+
153
+ # Execute the Go binary (replaces the Python process)
154
+ try:
155
+ os.execv(binary_path, cmd)
156
+ except OSError as e:
157
+ print(f"守护进程错误: 无法执行 {binary_path}: {e}", file=sys.stderr)
158
+ remove_pid_file()
159
+ os._exit(1)
160
+
161
+
162
+ def _daemon_windows(binary_path, args):
163
+ """Daemon implementation for Windows (detached subprocess)."""
164
+ cmd = [binary_path] + args
165
+
166
+ log_file = log_file_path()
167
+ try:
168
+ lf = open(log_file, "a")
169
+ except IOError:
170
+ lf = subprocess.DEVNULL
171
+
172
+ try:
173
+ proc = subprocess.Popen(
174
+ cmd,
175
+ stdin=subprocess.DEVNULL,
176
+ stdout=lf if lf != subprocess.DEVNULL else subprocess.DEVNULL,
177
+ stderr=subprocess.STDOUT if lf != subprocess.DEVNULL else subprocess.DEVNULL,
178
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
179
+ close_fds=True,
180
+ )
181
+ print(f" 守护进程已启动 (PID: {proc.pid})")
182
+ print(f" 日志文件: {log_file}")
183
+ print(" 停止命令: tunnelgo-client --stop")
184
+ except Exception as e:
185
+ print(f"守护进程错误: 启动失败: {e}", file=sys.stderr)
186
+ sys.exit(1)
187
+ finally:
188
+ if lf != subprocess.DEVNULL:
189
+ lf.close()
190
+
191
+
192
+ def _redirect_stdio():
193
+ """Redirect stdin/stdout/stderr for the daemon process."""
194
+ # stdin → /dev/null
195
+ devnull = os.open(os.devnull, os.O_RDWR)
196
+ os.dup2(devnull, 0) # stdin
197
+
198
+ # stdout/stderr → log file
199
+ log_path = log_file_path()
200
+ try:
201
+ log_fd = os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
202
+ except OSError:
203
+ log_fd = devnull
204
+
205
+ os.dup2(log_fd, 1) # stdout
206
+ os.dup2(log_fd, 2) # stderr
207
+
208
+ # Close original fds
209
+ if devnull > 2:
210
+ os.close(devnull)
211
+ if log_fd > 2 and log_fd != devnull:
212
+ os.close(log_fd)
@@ -0,0 +1,177 @@
1
+ """
2
+ Binary downloader for TunnelGo client.
3
+
4
+ Downloads the platform-appropriate Go binary from GitHub Releases
5
+ and caches it in ~/.tunnelgo/bin/.
6
+ """
7
+
8
+ import hashlib
9
+ import io
10
+ import json
11
+ import os
12
+ import platform
13
+ import stat
14
+ import sys
15
+ import urllib.request
16
+ import urllib.error
17
+ import zipfile
18
+ import tarfile
19
+
20
+ # GitHub repo info
21
+ GITHUB_OWNER = "ctz168"
22
+ GITHUB_REPO = "tunnelgo"
23
+
24
+ # Cache directory
25
+ CACHE_DIR = os.path.expanduser("~/.tunnelgo/bin")
26
+ VERSION_FILE = os.path.join(CACHE_DIR, ".installed_version")
27
+
28
+ # Platform mapping: (system, machine) → binary name suffix
29
+ PLATFORM_MAP = {
30
+ ("Linux", "x86_64"): "tunnelgo-client-linux-amd64",
31
+ ("Linux", "aarch64"): "tunnelgo-client-linux-arm64",
32
+ ("Linux", "armv7l"): "tunnelgo-client-linux-arm64",
33
+ ("Darwin", "x86_64"): "tunnelgo-client-darwin-amd64",
34
+ ("Darwin", "arm64"): "tunnelgo-client-darwin-arm64",
35
+ ("Windows", "AMD64"): "tunnelgo-client-windows-amd64.exe",
36
+ }
37
+
38
+
39
+ def get_platform_binary_name():
40
+ """Return the binary filename for the current platform."""
41
+ system = platform.system()
42
+ machine = platform.machine()
43
+
44
+ # Normalize machine names
45
+ machine_lower = machine.lower()
46
+ if machine_lower in ("x86_64", "amd64"):
47
+ machine_key = "x86_64" if system != "Windows" else "AMD64"
48
+ elif machine_lower in ("aarch64", "arm64", "armv7l"):
49
+ machine_key = machine
50
+ else:
51
+ machine_key = machine
52
+
53
+ key = (system, machine_key)
54
+ if key in PLATFORM_MAP:
55
+ return PLATFORM_MAP[key]
56
+
57
+ # Fallback: try common combinations
58
+ for k, v in PLATFORM_MAP.items():
59
+ if k[0] == system:
60
+ return v
61
+
62
+ raise RuntimeError(
63
+ f"Unsupported platform: {system} {machine}. "
64
+ f"Please download the binary manually from "
65
+ f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases"
66
+ )
67
+
68
+
69
+ def get_latest_version():
70
+ """Fetch the latest release version from GitHub API."""
71
+ api_url = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest"
72
+ try:
73
+ req = urllib.request.Request(api_url, headers={"User-Agent": "tunnelgo-client-python"})
74
+ with urllib.request.urlopen(req, timeout=10) as resp:
75
+ data = json.loads(resp.read().decode())
76
+ return data.get("tag_name", "").lstrip("v")
77
+ except Exception:
78
+ return None
79
+
80
+
81
+ def get_installed_version():
82
+ """Read the currently installed version from the version file."""
83
+ try:
84
+ with open(VERSION_FILE, "r") as f:
85
+ return f.read().strip()
86
+ except (FileNotFoundError, IOError):
87
+ return None
88
+
89
+
90
+ def get_binary_path():
91
+ """Return the full path to the cached binary."""
92
+ binary_name = get_platform_binary_name()
93
+ # Normalize: strip .exe for the local copy on non-Windows
94
+ if platform.system() != "Windows" and binary_name.endswith(".exe"):
95
+ binary_name = binary_name[:-4]
96
+ return os.path.join(CACHE_DIR, binary_name)
97
+
98
+
99
+ def download_binary(version=None, force=False):
100
+ """
101
+ Download the TunnelGo client binary from GitHub Releases.
102
+
103
+ Args:
104
+ version: Specific version to download (e.g. "2.20.0"). If None, uses latest.
105
+ force: Force re-download even if binary exists.
106
+
107
+ Returns:
108
+ Path to the downloaded binary.
109
+ """
110
+ binary_path = get_binary_path()
111
+
112
+ # Check if binary already exists and is up-to-date
113
+ if not force and os.path.isfile(binary_path) and os.access(binary_path, os.X_OK):
114
+ installed = get_installed_version()
115
+ if installed and (version is None or installed == version):
116
+ return binary_path
117
+
118
+ # Determine version
119
+ if version is None:
120
+ version = get_latest_version()
121
+ if version is None:
122
+ version = "latest"
123
+
124
+ # Build download URL
125
+ binary_name = get_platform_binary_name()
126
+ if version == "latest":
127
+ download_url = (
128
+ f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest/download/{binary_name}"
129
+ )
130
+ else:
131
+ download_url = (
132
+ f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases/download/v{version}/{binary_name}"
133
+ )
134
+
135
+ # Create cache directory
136
+ os.makedirs(CACHE_DIR, exist_ok=True)
137
+
138
+ # Download
139
+ print(f" 正在下载 TunnelGo 客户端 v{version} ({binary_name})...")
140
+ try:
141
+ req = urllib.request.Request(download_url, headers={"User-Agent": "tunnelgo-client-python"})
142
+ with urllib.request.urlopen(req, timeout=60) as resp:
143
+ data = resp.read()
144
+ except urllib.error.HTTPError as e:
145
+ raise RuntimeError(
146
+ f"下载失败 (HTTP {e.code}): {download_url}\n"
147
+ f"请手动从 https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases 下载"
148
+ )
149
+ except Exception as e:
150
+ raise RuntimeError(f"下载失败: {e}")
151
+
152
+ # Write binary
153
+ with open(binary_path, "wb") as f:
154
+ f.write(data)
155
+
156
+ # Make executable
157
+ os.chmod(binary_path, os.stat(binary_path).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
158
+
159
+ # Write version file
160
+ if version != "latest":
161
+ with open(VERSION_FILE, "w") as f:
162
+ f.write(version)
163
+
164
+ size_mb = len(data) / (1024 * 1024)
165
+ print(f" 下载完成 ({size_mb:.1f} MB) → {binary_path}")
166
+
167
+ return binary_path
168
+
169
+
170
+ def ensure_binary(version=None):
171
+ """
172
+ Ensure the binary is available, downloading if necessary.
173
+
174
+ Returns:
175
+ Path to the binary.
176
+ """
177
+ return download_binary(version=version, force=False)
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: tunnelgo-client
3
+ Version: 2.20.0
4
+ Summary: TunnelGo 内网穿透客户端 — 通过 pip 一键安装,支持 double-fork 守护进程模式
5
+ Author: TunnelGo Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ctz168/tunnelgo
8
+ Project-URL: Repository, https://github.com/ctz168/tunnelgo
9
+ Project-URL: Issues, https://github.com/ctz168/tunnelgo/issues
10
+ Keywords: tunnel,ngrok,nat-traversal,reverse-proxy,daemon
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Internet :: Proxy Servers
24
+ Classifier: Topic :: System :: Networking
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+
28
+ # TunnelGo Client (Python)
29
+
30
+ TunnelGo 内网穿透客户端的 Python 封装包。通过 `pip` 一键安装,自动下载对应平台的 Go 二进制文件,支持 double-fork 守护进程模式。
31
+
32
+ ## 安装
33
+
34
+ ```bash
35
+ pip install tunnelgo-client
36
+ ```
37
+
38
+ 安装后会自动下载对应平台(Linux/macOS/Windows,amd64/arm64)的 Go 二进制文件到 `~/.tunnelgo/bin/`。
39
+
40
+ ## 使用
41
+
42
+ ```bash
43
+ # 基本模式
44
+ tunnelgo-client --key YOUR_TOKEN --port 8080
45
+
46
+ # 子域名模式(推荐)
47
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp
48
+
49
+ # 守护进程模式(Python 原生 double-fork,脱离终端)
50
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --daemon
51
+
52
+ # 停止守护进程
53
+ tunnelgo-client --stop
54
+
55
+ # 子域名 + TCP 转发
56
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --subdomain myapp --tcp-ports 22,3306
57
+
58
+ # 禁用 P2P 直连
59
+ tunnelgo-client -k YOUR_TOKEN -p 8080 --no-p2p
60
+
61
+ # 强制更新客户端二进制
62
+ tunnelgo-client --update
63
+ ```
64
+
65
+ ## Double-Fork 守护进程模式
66
+
67
+ `--daemon` 参数启动 Python 原生的 double-fork 守护进程模式:
68
+
69
+ 1. 第一次 fork → 创建中间子进程
70
+ 2. 中间子进程调用 `setsid()` 成为新会话组长
71
+ 3. 第二次 fork → 创建孙进程(实际守护进程)
72
+ 4. 中间子进程退出
73
+ 5. 孙进程被 init (PID 1) 收养,完全脱离终端
74
+
75
+ 守护进程的日志输出到 `/tmp/tunnelgo-client.log`(Windows 为 `%TEMP%\tunnelgo-client.log`)。
76
+
77
+ ## 参数
78
+
79
+ | 参数 | 说明 | 默认值 |
80
+ |------|------|--------|
81
+ | `-k, --key` | 认证令牌(必填) | - |
82
+ | `-p, --port` | 本地服务端口 | 8080 |
83
+ | `-s, --server` | 服务器地址 | aicq.online:6639 |
84
+ | `--host` | 本地服务地址 | localhost |
85
+ | `--p2p-port` | P2P 监听端口 | 与 --port 相同 |
86
+ | `--no-p2p` | 禁用 P2P 直连 | false |
87
+ | `--tcp-ports` | TCP 转发端口,逗号分隔 | - |
88
+ | `--tcp-default` | 设为非TLS同端口默认路由 | false |
89
+ | `--http-port` | HTTP 独立端口模式 | false |
90
+ | `--subdomain` | 子域名前缀 | - |
91
+ | `--daemon` | 以守护进程模式运行 (double-fork) | false |
92
+ | `--stop` | 停止守护进程 | false |
93
+ | `--update` | 强制更新客户端二进制 | false |
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/tunnelgo_client/__init__.py
4
+ src/tunnelgo_client/__main__.py
5
+ src/tunnelgo_client/cli.py
6
+ src/tunnelgo_client/daemon.py
7
+ src/tunnelgo_client/downloader.py
8
+ src/tunnelgo_client.egg-info/PKG-INFO
9
+ src/tunnelgo_client.egg-info/SOURCES.txt
10
+ src/tunnelgo_client.egg-info/dependency_links.txt
11
+ src/tunnelgo_client.egg-info/entry_points.txt
12
+ src/tunnelgo_client.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tunnelgo-client = tunnelgo_client.cli:main
@@ -0,0 +1 @@
1
+ tunnelgo_client