proxy-tuner 0.1.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.
- proxy_tuner-0.1.0/PKG-INFO +122 -0
- proxy_tuner-0.1.0/README.md +88 -0
- proxy_tuner-0.1.0/pyproject.toml +72 -0
- proxy_tuner-0.1.0/setup.cfg +4 -0
- proxy_tuner-0.1.0/src/proxy_tuner/__init__.py +3 -0
- proxy_tuner-0.1.0/src/proxy_tuner/cli.py +109 -0
- proxy_tuner-0.1.0/src/proxy_tuner/cli_config.py +152 -0
- proxy_tuner-0.1.0/src/proxy_tuner/cli_logs.py +58 -0
- proxy_tuner-0.1.0/src/proxy_tuner/cli_monitor.py +84 -0
- proxy_tuner-0.1.0/src/proxy_tuner/cli_outbound.py +146 -0
- proxy_tuner-0.1.0/src/proxy_tuner/cli_rule.py +251 -0
- proxy_tuner-0.1.0/src/proxy_tuner/cli_start.py +277 -0
- proxy_tuner-0.1.0/src/proxy_tuner/cli_stats.py +71 -0
- proxy_tuner-0.1.0/src/proxy_tuner/completions.py +85 -0
- proxy_tuner-0.1.0/src/proxy_tuner/config.py +391 -0
- proxy_tuner-0.1.0/src/proxy_tuner/dns.py +258 -0
- proxy_tuner-0.1.0/src/proxy_tuner/doctor.py +207 -0
- proxy_tuner-0.1.0/src/proxy_tuner/firewall.py +172 -0
- proxy_tuner-0.1.0/src/proxy_tuner/forwarder.py +449 -0
- proxy_tuner-0.1.0/src/proxy_tuner/http_proxy.py +142 -0
- proxy_tuner-0.1.0/src/proxy_tuner/logging.py +96 -0
- proxy_tuner-0.1.0/src/proxy_tuner/outbounds.py +186 -0
- proxy_tuner-0.1.0/src/proxy_tuner/platform/__init__.py +40 -0
- proxy_tuner-0.1.0/src/proxy_tuner/platform/base.py +54 -0
- proxy_tuner-0.1.0/src/proxy_tuner/platform/linux.py +196 -0
- proxy_tuner-0.1.0/src/proxy_tuner/platform/macos.py +46 -0
- proxy_tuner-0.1.0/src/proxy_tuner/platform/windows.py +244 -0
- proxy_tuner-0.1.0/src/proxy_tuner/pool.py +157 -0
- proxy_tuner-0.1.0/src/proxy_tuner/rules.py +255 -0
- proxy_tuner-0.1.0/src/proxy_tuner/setup_wizard.py +157 -0
- proxy_tuner-0.1.0/src/proxy_tuner/socks5.py +241 -0
- proxy_tuner-0.1.0/src/proxy_tuner/tun.py +240 -0
- proxy_tuner-0.1.0/src/proxy_tuner.egg-info/PKG-INFO +122 -0
- proxy_tuner-0.1.0/src/proxy_tuner.egg-info/SOURCES.txt +47 -0
- proxy_tuner-0.1.0/src/proxy_tuner.egg-info/dependency_links.txt +1 -0
- proxy_tuner-0.1.0/src/proxy_tuner.egg-info/entry_points.txt +2 -0
- proxy_tuner-0.1.0/src/proxy_tuner.egg-info/requires.txt +11 -0
- proxy_tuner-0.1.0/src/proxy_tuner.egg-info/top_level.txt +1 -0
- proxy_tuner-0.1.0/tests/test_cli.py +297 -0
- proxy_tuner-0.1.0/tests/test_completions.py +56 -0
- proxy_tuner-0.1.0/tests/test_config.py +419 -0
- proxy_tuner-0.1.0/tests/test_dns.py +126 -0
- proxy_tuner-0.1.0/tests/test_firewall.py +95 -0
- proxy_tuner-0.1.0/tests/test_forwarder.py +226 -0
- proxy_tuner-0.1.0/tests/test_http_proxy.py +40 -0
- proxy_tuner-0.1.0/tests/test_pool.py +122 -0
- proxy_tuner-0.1.0/tests/test_rules.py +628 -0
- proxy_tuner-0.1.0/tests/test_socks5.py +83 -0
- proxy_tuner-0.1.0/tests/test_tun.py +173 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proxy-tuner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cross-platform CLI tool for routing traffic through multiple proxy outbounds with flexible rule-based routing by process, domain, IP, and port
|
|
5
|
+
Author-email: HoomanJ <Hooman.Jalalpoor@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/HoomanJ/proxy-tuner
|
|
8
|
+
Project-URL: Documentation, https://github.com/HoomanJ/proxy-tuner/tree/master/docs
|
|
9
|
+
Project-URL: Issues, https://github.com/HoomanJ/proxy-tuner/issues
|
|
10
|
+
Keywords: proxy,tunnel,vpn,routing,socks5,http-proxy
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: System :: Networking
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: click>=8.0
|
|
26
|
+
Requires-Dist: rich>=13.0
|
|
27
|
+
Provides-Extra: linux
|
|
28
|
+
Requires-Dist: pyroute2>=0.7; extra == "linux"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
34
|
+
|
|
35
|
+
# ProxyTuner
|
|
36
|
+
|
|
37
|
+
> **⚠ Warning: This software is in early development (alpha). Do not use in production or sensitive workspaces.**
|
|
38
|
+
> It may contain bugs, insecure defaults, or incomplete security measures.
|
|
39
|
+
> Use only for personal testing and experimentation.
|
|
40
|
+
|
|
41
|
+
A cross-platform (Windows & Linux) CLI tool for tuning and routing network traffic through multiple proxy outbounds with flexible rule-based routing.
|
|
42
|
+
|
|
43
|
+
## What It Does
|
|
44
|
+
|
|
45
|
+
ProxyTuner lets you:
|
|
46
|
+
|
|
47
|
+
- **Manage proxy outbounds** — add/remove SOCKS5 or HTTP proxies as named outbounds
|
|
48
|
+
- **Define routing rules** — direct traffic to specific outbounds based on:
|
|
49
|
+
- Process name (e.g., `firefox`, `chrome`, `curl`)
|
|
50
|
+
- Domain / hostname patterns (regex)
|
|
51
|
+
- IP addresses and CIDR ranges
|
|
52
|
+
- URL patterns (regex)
|
|
53
|
+
- Port ranges
|
|
54
|
+
- **Start/stop tuning** — activate or deactivate traffic redirection at any time
|
|
55
|
+
- **Per-platform optimization** — uses native OS mechanisms:
|
|
56
|
+
- **Linux**: nftables/iptables TPROXY + TUN interface
|
|
57
|
+
- **Windows**: WinDivert packet interception
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Install
|
|
63
|
+
pip install proxy-tuner
|
|
64
|
+
|
|
65
|
+
# Add a SOCKS5 proxy outbound
|
|
66
|
+
proxy-tuner outbound add my-vpn --type socks5 --host 127.0.0.1 --port 1080
|
|
67
|
+
|
|
68
|
+
# Add an HTTP proxy outbound
|
|
69
|
+
proxy-tuner outbound add my-http --type http --host 127.0.0.1 --port 8080
|
|
70
|
+
|
|
71
|
+
# Add a routing rule: send Firefox traffic through my-vpn
|
|
72
|
+
proxy-tuner rule add firefox-vpn --process firefox --outbound my-vpn
|
|
73
|
+
|
|
74
|
+
# Add a rule: send *.cn domains through a specific proxy
|
|
75
|
+
proxy-tuner rule add china-traffic --domain "*.cn" --outbound my-http
|
|
76
|
+
|
|
77
|
+
# Start tuning
|
|
78
|
+
proxy-tuner start
|
|
79
|
+
|
|
80
|
+
# Stop tuning
|
|
81
|
+
proxy-tuner stop
|
|
82
|
+
|
|
83
|
+
# Show status
|
|
84
|
+
proxy-tuner status
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## How It Works
|
|
88
|
+
|
|
89
|
+
ProxyTuner creates a local transparent proxy that intercepts traffic based on your rules and forwards it through the configured outbound proxies.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Application traffic
|
|
93
|
+
│
|
|
94
|
+
▼
|
|
95
|
+
┌──────────────────┐
|
|
96
|
+
│ ProxyTuner │
|
|
97
|
+
│ (rule engine) │
|
|
98
|
+
└──────────────────┘
|
|
99
|
+
│
|
|
100
|
+
┌────┴────┐
|
|
101
|
+
▼ ▼
|
|
102
|
+
Outbound1 Outbound2
|
|
103
|
+
(SOCKS5) (HTTP)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
On Linux, traffic is redirected to ProxyTuner via nftables/iptables with per-process (`--uid-owner`) matching. On Windows, WinDivert captures packets from specific processes.
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
- Python 3.10+
|
|
111
|
+
- Linux: `nftables` or `iptables` (root/sudo required)
|
|
112
|
+
- Windows: [WinDivert](https://reqrypt.org/windivert.html) driver (admin required)
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
- [Architecture](docs/architecture.md) — technical design and internals
|
|
117
|
+
- [Configuration](docs/configuration.md) — config file reference
|
|
118
|
+
- [Usage Guide](docs/usage.md) — CLI commands and examples
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# ProxyTuner
|
|
2
|
+
|
|
3
|
+
> **⚠ Warning: This software is in early development (alpha). Do not use in production or sensitive workspaces.**
|
|
4
|
+
> It may contain bugs, insecure defaults, or incomplete security measures.
|
|
5
|
+
> Use only for personal testing and experimentation.
|
|
6
|
+
|
|
7
|
+
A cross-platform (Windows & Linux) CLI tool for tuning and routing network traffic through multiple proxy outbounds with flexible rule-based routing.
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
ProxyTuner lets you:
|
|
12
|
+
|
|
13
|
+
- **Manage proxy outbounds** — add/remove SOCKS5 or HTTP proxies as named outbounds
|
|
14
|
+
- **Define routing rules** — direct traffic to specific outbounds based on:
|
|
15
|
+
- Process name (e.g., `firefox`, `chrome`, `curl`)
|
|
16
|
+
- Domain / hostname patterns (regex)
|
|
17
|
+
- IP addresses and CIDR ranges
|
|
18
|
+
- URL patterns (regex)
|
|
19
|
+
- Port ranges
|
|
20
|
+
- **Start/stop tuning** — activate or deactivate traffic redirection at any time
|
|
21
|
+
- **Per-platform optimization** — uses native OS mechanisms:
|
|
22
|
+
- **Linux**: nftables/iptables TPROXY + TUN interface
|
|
23
|
+
- **Windows**: WinDivert packet interception
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Install
|
|
29
|
+
pip install proxy-tuner
|
|
30
|
+
|
|
31
|
+
# Add a SOCKS5 proxy outbound
|
|
32
|
+
proxy-tuner outbound add my-vpn --type socks5 --host 127.0.0.1 --port 1080
|
|
33
|
+
|
|
34
|
+
# Add an HTTP proxy outbound
|
|
35
|
+
proxy-tuner outbound add my-http --type http --host 127.0.0.1 --port 8080
|
|
36
|
+
|
|
37
|
+
# Add a routing rule: send Firefox traffic through my-vpn
|
|
38
|
+
proxy-tuner rule add firefox-vpn --process firefox --outbound my-vpn
|
|
39
|
+
|
|
40
|
+
# Add a rule: send *.cn domains through a specific proxy
|
|
41
|
+
proxy-tuner rule add china-traffic --domain "*.cn" --outbound my-http
|
|
42
|
+
|
|
43
|
+
# Start tuning
|
|
44
|
+
proxy-tuner start
|
|
45
|
+
|
|
46
|
+
# Stop tuning
|
|
47
|
+
proxy-tuner stop
|
|
48
|
+
|
|
49
|
+
# Show status
|
|
50
|
+
proxy-tuner status
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## How It Works
|
|
54
|
+
|
|
55
|
+
ProxyTuner creates a local transparent proxy that intercepts traffic based on your rules and forwards it through the configured outbound proxies.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Application traffic
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
┌──────────────────┐
|
|
62
|
+
│ ProxyTuner │
|
|
63
|
+
│ (rule engine) │
|
|
64
|
+
└──────────────────┘
|
|
65
|
+
│
|
|
66
|
+
┌────┴────┐
|
|
67
|
+
▼ ▼
|
|
68
|
+
Outbound1 Outbound2
|
|
69
|
+
(SOCKS5) (HTTP)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
On Linux, traffic is redirected to ProxyTuner via nftables/iptables with per-process (`--uid-owner`) matching. On Windows, WinDivert captures packets from specific processes.
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
- Python 3.10+
|
|
77
|
+
- Linux: `nftables` or `iptables` (root/sudo required)
|
|
78
|
+
- Windows: [WinDivert](https://reqrypt.org/windivert.html) driver (admin required)
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
- [Architecture](docs/architecture.md) — technical design and internals
|
|
83
|
+
- [Configuration](docs/configuration.md) — config file reference
|
|
84
|
+
- [Usage Guide](docs/usage.md) — CLI commands and examples
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "proxy-tuner"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cross-platform CLI tool for routing traffic through multiple proxy outbounds with flexible rule-based routing by process, domain, IP, and port"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "HoomanJ", email = "Hooman.Jalalpoor@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["proxy", "tunnel", "vpn", "routing", "socks5", "http-proxy"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Operating System :: Microsoft :: Windows",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: System :: Networking",
|
|
28
|
+
"Topic :: System :: Monitoring",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"click>=8.0",
|
|
32
|
+
"rich>=13.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
linux = [
|
|
37
|
+
"pyroute2>=0.7",
|
|
38
|
+
]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=7.0",
|
|
41
|
+
"pytest-asyncio>=0.21",
|
|
42
|
+
"ruff>=0.1.0",
|
|
43
|
+
"mypy>=1.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
proxy-tuner = "proxy_tuner.cli:main"
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Repository = "https://github.com/HoomanJ/proxy-tuner"
|
|
51
|
+
Documentation = "https://github.com/HoomanJ/proxy-tuner/tree/master/docs"
|
|
52
|
+
Issues = "https://github.com/HoomanJ/proxy-tuner/issues"
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py310"
|
|
59
|
+
line-length = 100
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM"]
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
python_version = "3.10"
|
|
66
|
+
strict = true
|
|
67
|
+
warn_return_any = true
|
|
68
|
+
warn_unused_configs = true
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Main CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from proxy_tuner import __version__
|
|
11
|
+
from proxy_tuner.config import ConfigManager
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
@click.version_option(version=__version__, prog_name="proxy-tuner")
|
|
18
|
+
@click.option(
|
|
19
|
+
"--config",
|
|
20
|
+
"config_path",
|
|
21
|
+
type=click.Path(path_type=Path),
|
|
22
|
+
default=None,
|
|
23
|
+
help="Path to config file (default: platform default)",
|
|
24
|
+
)
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def main(ctx: click.Context, config_path: Path | None) -> None:
|
|
27
|
+
"""ProxyTuner — cross-platform proxy routing CLI.
|
|
28
|
+
|
|
29
|
+
Route network traffic through multiple proxy outbounds with
|
|
30
|
+
flexible rule-based routing.
|
|
31
|
+
"""
|
|
32
|
+
ctx.ensure_object(dict)
|
|
33
|
+
ctx.obj["config_manager"] = ConfigManager(config_path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Register subcommands
|
|
37
|
+
from proxy_tuner.cli_config import config_group # noqa: E402
|
|
38
|
+
from proxy_tuner.cli_logs import logs # noqa: E402
|
|
39
|
+
from proxy_tuner.cli_monitor import monitor # noqa: E402
|
|
40
|
+
from proxy_tuner.cli_outbound import outbound_group # noqa: E402
|
|
41
|
+
from proxy_tuner.cli_rule import rule_group # noqa: E402
|
|
42
|
+
from proxy_tuner.cli_start import start, status, stop # noqa: E402
|
|
43
|
+
from proxy_tuner.cli_stats import stats # noqa: E402
|
|
44
|
+
from proxy_tuner.completions import completions_group # noqa: E402
|
|
45
|
+
from proxy_tuner.doctor import doctor # noqa: E402
|
|
46
|
+
from proxy_tuner.setup_wizard import setup_wizard # noqa: E402
|
|
47
|
+
|
|
48
|
+
main.add_command(outbound_group)
|
|
49
|
+
main.add_command(rule_group)
|
|
50
|
+
main.add_command(config_group)
|
|
51
|
+
main.add_command(completions_group)
|
|
52
|
+
main.add_command(setup_wizard)
|
|
53
|
+
main.add_command(doctor)
|
|
54
|
+
main.add_command(logs)
|
|
55
|
+
main.add_command(stats)
|
|
56
|
+
main.add_command(monitor)
|
|
57
|
+
main.add_command(start)
|
|
58
|
+
main.add_command(stop)
|
|
59
|
+
main.add_command(status)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@main.command("version")
|
|
63
|
+
def version_cmd() -> None:
|
|
64
|
+
"""Show version information."""
|
|
65
|
+
from rich.console import Console
|
|
66
|
+
|
|
67
|
+
Console().print(f"proxy-tuner {__version__}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@main.command("reload")
|
|
71
|
+
@click.pass_context
|
|
72
|
+
def reload(ctx: click.Context) -> None:
|
|
73
|
+
"""Reload configuration (hot-reload while running)."""
|
|
74
|
+
import os
|
|
75
|
+
import signal
|
|
76
|
+
|
|
77
|
+
manager: ConfigManager = ctx.obj["config_manager"]
|
|
78
|
+
|
|
79
|
+
from proxy_tuner.cli_start import _read_pid
|
|
80
|
+
|
|
81
|
+
pid = _read_pid(manager)
|
|
82
|
+
if pid is None:
|
|
83
|
+
console.print("[yellow]ProxyTuner is not running.[/yellow]")
|
|
84
|
+
raise click.Abort()
|
|
85
|
+
|
|
86
|
+
# Validate new config
|
|
87
|
+
try:
|
|
88
|
+
config = manager.load()
|
|
89
|
+
except Exception as e:
|
|
90
|
+
console.print(f"[red]Config error:[/red] {e}")
|
|
91
|
+
raise click.Abort() from e
|
|
92
|
+
|
|
93
|
+
errors = config.validate_references()
|
|
94
|
+
if errors:
|
|
95
|
+
console.print("[red]Config validation errors:[/red]")
|
|
96
|
+
for err in errors:
|
|
97
|
+
console.print(f" • {err}")
|
|
98
|
+
raise click.Abort()
|
|
99
|
+
|
|
100
|
+
# Send SIGHUP to trigger reload
|
|
101
|
+
try:
|
|
102
|
+
os.kill(pid, signal.SIGHUP)
|
|
103
|
+
console.print(f"[green]✓[/green] Sent reload signal to PID {pid}")
|
|
104
|
+
except OSError as e:
|
|
105
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Config management CLI subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
|
|
9
|
+
from proxy_tuner.config import ConfigManager
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group("config")
|
|
15
|
+
def config_group() -> None:
|
|
16
|
+
"""Manage configuration."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@config_group.command("show")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def show_config(ctx: click.Context) -> None:
|
|
22
|
+
"""Display the current configuration."""
|
|
23
|
+
import json
|
|
24
|
+
|
|
25
|
+
manager: ConfigManager = ctx.obj["config_manager"]
|
|
26
|
+
config = manager.get()
|
|
27
|
+
|
|
28
|
+
from proxy_tuner.config import _serialize_config
|
|
29
|
+
|
|
30
|
+
output = json.dumps(_serialize_config(config), indent=2, ensure_ascii=False)
|
|
31
|
+
console.print(Syntax(output, "json", theme="monokai"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@config_group.command("path")
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def config_path(ctx: click.Context) -> None:
|
|
37
|
+
"""Show the config file path."""
|
|
38
|
+
manager: ConfigManager = ctx.obj["config_manager"]
|
|
39
|
+
console.print(str(manager.path))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@config_group.command("edit")
|
|
43
|
+
@click.pass_context
|
|
44
|
+
def edit_config(ctx: click.Context) -> None:
|
|
45
|
+
"""Open the config file in the default editor."""
|
|
46
|
+
import os
|
|
47
|
+
import subprocess
|
|
48
|
+
|
|
49
|
+
manager: ConfigManager = ctx.obj["config_manager"]
|
|
50
|
+
|
|
51
|
+
# Ensure file exists
|
|
52
|
+
if not manager.path.exists():
|
|
53
|
+
manager.save(manager.get())
|
|
54
|
+
|
|
55
|
+
editor = os.environ.get("EDITOR", "vi")
|
|
56
|
+
try:
|
|
57
|
+
subprocess.run([editor, str(manager.path)], check=True)
|
|
58
|
+
console.print(f"[green]✓[/green] Config edited at {manager.path}")
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
msg = f"[red]Error:[/red] Editor '{editor}' not found."
|
|
61
|
+
console.print(msg + " Set $EDITOR or use 'config show'.")
|
|
62
|
+
except subprocess.CalledProcessError as e:
|
|
63
|
+
console.print(f"[red]Error:[/red] Editor exited with code {e.returncode}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@config_group.command("validate")
|
|
67
|
+
@click.pass_context
|
|
68
|
+
def validate_config(ctx: click.Context) -> None:
|
|
69
|
+
"""Validate the configuration file."""
|
|
70
|
+
manager: ConfigManager = ctx.obj["config_manager"]
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
config = manager.load()
|
|
74
|
+
except Exception as e:
|
|
75
|
+
console.print(f"[red]Parse error:[/red] {e}")
|
|
76
|
+
raise click.Abort() from e
|
|
77
|
+
|
|
78
|
+
errors = config.validate_references()
|
|
79
|
+
if errors:
|
|
80
|
+
console.print("[red]Validation errors:[/red]")
|
|
81
|
+
for err in errors:
|
|
82
|
+
console.print(f" • {err}")
|
|
83
|
+
raise click.Abort()
|
|
84
|
+
|
|
85
|
+
console.print("[green]✓[/green] Configuration is valid")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@config_group.command("init")
|
|
89
|
+
@click.pass_context
|
|
90
|
+
def init_config(ctx: click.Context) -> None:
|
|
91
|
+
"""Create a default config file."""
|
|
92
|
+
from proxy_tuner.config import Config
|
|
93
|
+
|
|
94
|
+
manager: ConfigManager = ctx.obj["config_manager"]
|
|
95
|
+
|
|
96
|
+
if manager.path.exists():
|
|
97
|
+
console.print(f"[yellow]Config already exists at {manager.path}[/yellow]")
|
|
98
|
+
if not click.confirm("Overwrite?"):
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
manager.save(Config())
|
|
102
|
+
console.print(f"[green]✓[/green] Created default config at {manager.path}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@config_group.command("set")
|
|
106
|
+
@click.argument("key")
|
|
107
|
+
@click.argument("value")
|
|
108
|
+
@click.pass_context
|
|
109
|
+
def set_config(ctx: click.Context, key: str, value: str) -> None:
|
|
110
|
+
"""Set a configuration value.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
proxy-tuner config set settings.listen_port 9090
|
|
114
|
+
proxy-tuner config set settings.log_level debug
|
|
115
|
+
proxy-tuner config set settings.dns_server 1.1.1.1
|
|
116
|
+
"""
|
|
117
|
+
manager: ConfigManager = ctx.obj["config_manager"]
|
|
118
|
+
config = manager.get()
|
|
119
|
+
|
|
120
|
+
# Parse the key path
|
|
121
|
+
parts = key.split(".")
|
|
122
|
+
if len(parts) < 2:
|
|
123
|
+
msg = "[red]Error:[/red] Key must be in format 'section.field'"
|
|
124
|
+
console.print(f"{msg} (e.g., settings.listen_port)")
|
|
125
|
+
raise click.Abort()
|
|
126
|
+
|
|
127
|
+
section = parts[0]
|
|
128
|
+
field_name = parts[1]
|
|
129
|
+
|
|
130
|
+
# Type coercion
|
|
131
|
+
if value.lower() == "true":
|
|
132
|
+
coerced: object = True
|
|
133
|
+
elif value.lower() == "false":
|
|
134
|
+
coerced = False
|
|
135
|
+
elif value.lower() == "null" or value.lower() == "none":
|
|
136
|
+
coerced = None
|
|
137
|
+
else:
|
|
138
|
+
try:
|
|
139
|
+
coerced = int(value)
|
|
140
|
+
except ValueError:
|
|
141
|
+
try:
|
|
142
|
+
coerced = float(value)
|
|
143
|
+
except ValueError:
|
|
144
|
+
coerced = value
|
|
145
|
+
|
|
146
|
+
if section == "settings" and hasattr(config.settings, field_name):
|
|
147
|
+
setattr(config.settings, field_name, coerced)
|
|
148
|
+
manager.save(config)
|
|
149
|
+
console.print(f"[green]✓[/green] Set {key} = {coerced}")
|
|
150
|
+
else:
|
|
151
|
+
console.print(f"[red]Error:[/red] Unknown setting '{key}'. Valid sections: settings")
|
|
152
|
+
raise click.Abort()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Logs viewing CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("logs")
|
|
12
|
+
@click.option("-n", "--lines", default=50, type=int, help="Number of lines to show")
|
|
13
|
+
@click.option("-f", "--follow", is_flag=True, help="Follow log output (tail -f)")
|
|
14
|
+
@click.pass_context
|
|
15
|
+
def logs(ctx: click.Context, lines: int, follow: bool) -> None:
|
|
16
|
+
"""View ProxyTuner logs."""
|
|
17
|
+
|
|
18
|
+
from proxy_tuner.config import get_config_dir
|
|
19
|
+
|
|
20
|
+
log_file = get_config_dir() / "proxy-tuner.log"
|
|
21
|
+
|
|
22
|
+
if follow:
|
|
23
|
+
# Tail -f mode
|
|
24
|
+
if not log_file.exists():
|
|
25
|
+
console.print(f"[yellow]Log file not found: {log_file}[/yellow]")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
console.print(f"[dim]Following {log_file} (Ctrl+C to stop)[/dim]")
|
|
29
|
+
try:
|
|
30
|
+
with open(log_file) as f:
|
|
31
|
+
# Seek to end minus some bytes
|
|
32
|
+
f.seek(0, 2)
|
|
33
|
+
size = f.tell()
|
|
34
|
+
f.seek(max(0, size - 4096))
|
|
35
|
+
f.readline() # Skip partial line
|
|
36
|
+
|
|
37
|
+
while True:
|
|
38
|
+
line = f.readline()
|
|
39
|
+
if line:
|
|
40
|
+
console.print(line.rstrip())
|
|
41
|
+
else:
|
|
42
|
+
import time
|
|
43
|
+
time.sleep(0.1)
|
|
44
|
+
except KeyboardInterrupt:
|
|
45
|
+
pass
|
|
46
|
+
else:
|
|
47
|
+
# Show last N lines
|
|
48
|
+
if not log_file.exists():
|
|
49
|
+
console.print(f"[yellow]No log file found at {log_file}[/yellow]")
|
|
50
|
+
msg = "[dim]Logs are written when running with "
|
|
51
|
+
console.print(msg + "--log-file or in daemon mode[/dim]")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
with open(log_file) as f:
|
|
55
|
+
all_lines = f.readlines()
|
|
56
|
+
recent = all_lines[-lines:]
|
|
57
|
+
for line in recent:
|
|
58
|
+
console.print(line.rstrip())
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Monitor command — live view of connections and activity."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.live import Live
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _format_bytes(n: int) -> str:
|
|
16
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
17
|
+
if abs(n) < 1024:
|
|
18
|
+
return f"{n:.1f} {unit}"
|
|
19
|
+
n /= 1024
|
|
20
|
+
return f"{n:.1f} TB"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command("monitor")
|
|
24
|
+
@click.option("--interval", "-i", default=2, type=int, help="Refresh interval in seconds")
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def monitor(ctx: click.Context, interval: int) -> None:
|
|
27
|
+
"""Live monitoring dashboard."""
|
|
28
|
+
import json
|
|
29
|
+
|
|
30
|
+
from proxy_tuner.config import get_config_dir
|
|
31
|
+
|
|
32
|
+
stats_file = get_config_dir() / "stats.json"
|
|
33
|
+
pid_file = get_config_dir() / "proxytuner.pid"
|
|
34
|
+
|
|
35
|
+
def _build_table() -> Table:
|
|
36
|
+
table = Table(title="ProxyTuner Monitor", show_header=True)
|
|
37
|
+
table.add_column("Metric", style="bold")
|
|
38
|
+
table.add_column("Value", justify="right")
|
|
39
|
+
|
|
40
|
+
# Status
|
|
41
|
+
running = False
|
|
42
|
+
if pid_file.exists():
|
|
43
|
+
try:
|
|
44
|
+
import os
|
|
45
|
+
pid = int(pid_file.read_text().strip())
|
|
46
|
+
os.kill(pid, 0)
|
|
47
|
+
running = True
|
|
48
|
+
except (ValueError, OSError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
status = "[green]Running[/green]" if running else "[red]Stopped[/red]"
|
|
52
|
+
table.add_row("Status", status)
|
|
53
|
+
|
|
54
|
+
# Load stats
|
|
55
|
+
data = {}
|
|
56
|
+
if stats_file.exists():
|
|
57
|
+
try:
|
|
58
|
+
with open(stats_file) as f:
|
|
59
|
+
data = json.load(f)
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
# Aggregate stats
|
|
64
|
+
total_conns = sum(s.get("connections", 0) for s in data.values())
|
|
65
|
+
total_sent = sum(s.get("bytes_sent", 0) for s in data.values())
|
|
66
|
+
total_recv = sum(s.get("bytes_received", 0) for s in data.values())
|
|
67
|
+
total_errors = sum(s.get("errors", 0) for s in data.values())
|
|
68
|
+
|
|
69
|
+
table.add_row("Total connections", str(total_conns))
|
|
70
|
+
table.add_row("Bytes sent", _format_bytes(total_sent))
|
|
71
|
+
table.add_row("Bytes received", _format_bytes(total_recv))
|
|
72
|
+
table.add_row("Errors", str(total_errors))
|
|
73
|
+
table.add_row("Last update", time.strftime("%H:%M:%S"))
|
|
74
|
+
|
|
75
|
+
return table
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
refresh = 1 // max(interval, 1)
|
|
79
|
+
with Live(_build_table(), refresh_per_second=refresh, console=console) as live:
|
|
80
|
+
while True:
|
|
81
|
+
time.sleep(interval)
|
|
82
|
+
live.update(_build_table())
|
|
83
|
+
except KeyboardInterrupt:
|
|
84
|
+
pass
|