Skip to content

Commit 79cbaa6

Browse files
Add files via upload
1 parent 6d48166 commit 79cbaa6

3 files changed

Lines changed: 298 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Deduplicating Configurations
2+
3+
When working with large lists of configurations, especially from multiple subscription links, it's common to encounter duplicates. These are profiles that are functionally identical (same server, port, and user ID) but may have different tags or remarks. Running tests on these duplicates is inefficient and clutters your results.
4+
5+
The library provides a simple yet powerful utility function, `deduplicate_configs`, to clean up your list.
6+
7+
## The Deduplication Strategy
8+
9+
The function intelligently identifies duplicates by focusing on the **core properties** of a configuration, while **intentionally ignoring the `tag`**.
10+
11+
- **Unique Key**: For each configuration, a unique key is generated based on properties like `protocol`, `address`, `port`, and `id`/`password`.
12+
- **First-Come, First-Served**: The function keeps the *first* occurrence of each unique configuration it encounters and discards all subsequent duplicates.
13+
14+
This ensures that your final list is clean and ready for efficient testing.
15+
16+
## Practical Example
17+
18+
This minimal example demonstrates how to use `deduplicate_configs` to clean a list of `ConfigParams` objects.
19+
20+
```python
21+
# examples/09_deduplicate_configs.py
22+
23+
import os
24+
import sys
25+
26+
# Add project root to path to find our library
27+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
28+
29+
from python_v2ray.config_parser import load_configs, deduplicate_configs
30+
31+
def main():
32+
"""
33+
* A minimal example to demonstrate how to remove duplicate configurations
34+
* from a list, ignoring their tags (# remarks).
35+
"""
36+
37+
# --- 1. Define a list with duplicate configurations ---
38+
# Note: The first two VLESS configs are identical except for their tag.
39+
raw_uris = [
40+
"vless://abcdef@example.com:443?type=ws#VLESS-Config-1",
41+
"vless://abcdef@example.com:443?type=ws#VLESS-Config-2-DUPLICATE",
42+
"trojan://password@anotherexample.com:443#Trojan-Config-UNIQUE",
43+
]
44+
45+
print("--- Initial List of Raw URIs ---")
46+
for uri in raw_uris:
47+
print(f"- {uri}")
48+
49+
# --- 2. Load the configs into ConfigParams objects ---
50+
parsed_configs = load_configs(source=raw_uris)
51+
print(f"\n* Initially parsed {len(parsed_configs)} configurations.")
52+
53+
# --- 3. Apply the deduplication function ---
54+
print("\n--- Applying Deduplication (Ignoring Tags) ---")
55+
unique_configs = deduplicate_configs(parsed_configs)
56+
57+
# --- 4. Display the final, clean list ---
58+
print(f"\n* Found {len(unique_configs)} unique configurations.")
59+
print("Tags of final unique configs:", [p.tag for p in unique_configs])
60+
61+
if __name__ == "__main__":
62+
main()
63+
```
64+
65+
### Expected Output:
66+
```
67+
--- Initial List of Raw URIs ---
68+
- vless://abcdef@example.com:443?type=ws#VLESS-Config-1
69+
- vless://abcdef@example.com:443?type=ws#VLESS-Config-2-DUPLICATE
70+
- trojan://password@anotherexample.com:443#Trojan-Config-UNIQUE
71+
72+
* Initially parsed 3 valid configurations.
73+
74+
--- Applying Deduplication (Ignoring Tags) ---
75+
76+
* Found 2 unique configurations.
77+
Tags of final unique configs: ['VLESS-Config-1', 'Trojan-Config-UNIQUE']
78+
```
79+
80+
## API Reference
81+
82+
### `deduplicate_configs()`
83+
84+
```python
85+
def deduplicate_configs(configs: List[ConfigParams]) -> List[ConfigParams]:
86+
```
87+
88+
- **`configs`**: A list of `ConfigParams` objects to be cleaned.
89+
- **Returns**: A new list containing only the unique `ConfigParams` objects.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Loading Configs from Different Sources
2+
3+
Manually managing individual configuration links can be tedious. The library provides a powerful and flexible universal loader, `load_configs`, to import profiles from various sources, including remote subscription links, local files, and Python lists.
4+
5+
This function acts as the primary entry point for getting configurations into the system before testing or processing.
6+
7+
## Key Features
8+
9+
- **Subscription Link Support**: Directly fetches and decodes configurations from remote subscription URLs.
10+
- **Intelligent Decoding**: Automatically handles both **Base64-encoded** and **plain-text** subscription content.
11+
- **Multiple Sources**: Accepts a URL, a local file path, or a Python list of URIs as input.
12+
- **Robust Parsing**: Each raw URI is then processed by the internal `parse_uri` function to create a standardized `ConfigParams` object.
13+
14+
## Using the `load_configs` Function
15+
16+
The behavior of the function is controlled by two parameters: `source` and `is_subscription`.
17+
18+
### Scenario 1: Loading from a Subscription URL
19+
20+
This is the most common use case. Provide the URL and set the `is_subscription` flag to `True`.
21+
22+
```python
23+
from python_v2ray.config_parser import load_configs
24+
25+
sub_url = "https://your.subscription.link/path"
26+
27+
# The flag tells the function to fetch the URL and decode its content
28+
parsed_configs = load_configs(source=sub_url, is_subscription=True)
29+
30+
print(f"Loaded {len(parsed_configs)} configs from the subscription.")
31+
```
32+
33+
### Scenario 2: Loading from a Local File
34+
35+
You can load configurations from a local text file. The loader handles two types of files automatically.
36+
37+
#### a) File with one URI per line
38+
39+
```python
40+
# --- contents of my_configs.txt ---
41+
# vless://...
42+
# trojan://...
43+
44+
from pathlib import Path
45+
from python_v2ray.config_parser import load_configs
46+
47+
file_path = Path("my_configs.txt")
48+
49+
# No flag is needed; this is the default behavior for files
50+
parsed_configs = load_configs(source=file_path)
51+
```
52+
53+
#### b) File containing a single subscription link
54+
55+
```python
56+
# --- contents of my_sub.txt ---
57+
# https://another.subscription.link/path
58+
59+
from pathlib import Path
60+
from python_v2ray.config_parser import load_configs
61+
62+
file_path = Path("my_sub.txt")
63+
64+
# The flag tells the function to read the URL from the file and then fetch it
65+
parsed_configs = load_configs(source=file_path, is_subscription=True)
66+
```
67+
68+
### Scenario 3: Loading from a Python List
69+
70+
If you already have your configuration URIs in a Python list, you can pass it directly.
71+
72+
```python
73+
from python_v2ray.config_parser import load_configs
74+
75+
my_uri_list = [
76+
"vless://abcdef@example.com:443?type=ws#Config-1",
77+
"trojan://password@anotherexample.com:443#Config-2",
78+
]
79+
80+
# This is the most direct way to parse a list of configs
81+
parsed_configs = load_configs(source=my_uri_list)
82+
```
83+
84+
## API Reference
85+
86+
### `load_configs()`
87+
88+
```python
89+
def load_configs(
90+
source: Union[str, List[str], Path],
91+
is_subscription: bool = False
92+
) -> List[ConfigParams]:
93+
```
94+
95+
- **`source`**: The input source. Can be:
96+
- A `str` representing a URL or a single URI.
97+
- A `list` of URI strings.
98+
- A `pathlib.Path` object pointing to a local file.
99+
- **`is_subscription`** (optional): A boolean flag. Set to `True` if the `source` (or the content of the source file) is a subscription link that needs to be fetched and decoded. Defaults to `False`.
100+
- **Returns**: A list of `ConfigParams` objects. **This function always returns a list**, even if it's empty.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Proxy Chaining ("WARP on Any")
2+
3+
This is one of the most powerful features of the library, allowing you to route the traffic of any Xray-compatible configuration through a final exit-point proxy. This is commonly used to enhance privacy or bypass complex network restrictions by creating a "WARP on Any" setup, where all your traffic exits through a Cloudflare WARP (WireGuard) endpoint.
4+
5+
The implementation is seamless: once you provide a WARP configuration, the `XrayConfigBuilder` automatically configures all other outbounds to use it as their `dialerProxy`.
6+
7+
## How It Works
8+
9+
Instead of connecting directly to the internet, a primary outbound (like VLESS or Trojan) first routes its traffic through the specified WARP outbound. The traffic flow looks like this:
10+
11+
`Your App` -> `Local SOCKS Inbound` -> `Primary Outbound (e.g., VLESS)` -> `WARP Outbound (WireGuard)` -> `Internet`
12+
13+
This entire chain is managed within a single, merged Xray instance for maximum efficiency.
14+
15+
## Practical Example: Testing Connectivity through WARP
16+
17+
This example demonstrates how to test the connectivity (ping) of several configurations that are all forced to route their traffic through a single WARP profile.
18+
19+
```python
20+
# examples/07_warp_tester.py
21+
22+
import os
23+
import sys
24+
from pathlib import Path
25+
26+
# Add project root to path
27+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
28+
29+
from python_v2ray.downloader import BinaryDownloader
30+
from python_v2ray.tester import ConnectionTester
31+
from python_v2ray.config_parser import parse_uri
32+
33+
def main():
34+
project_root = Path(__file__).parent.parent
35+
36+
# Ensure all necessary binaries are ready
37+
downloader = BinaryDownloader(project_root)
38+
downloader.ensure_all()
39+
40+
# --- 1. Define your WARP config (must be a WireGuard URI) ---
41+
warp_uri = "wireguard://YOUR_PRIVATE_KEY@engage.cloudflareclient.com:2408?address=172.16.0.2/32&publicKey=...#WARP-Profile"
42+
43+
# --- 2. Define the primary configs to test THROUGH WARP ---
44+
test_uris = [
45+
"vless://YOUR_UUID@your.domain.com:443?type=ws#VLESS-through-WARP",
46+
"trojan://YOUR_PASSWORD@another.domain.com:443#Trojan-through-WARP",
47+
# You can even chain another WireGuard config through the main WARP config
48+
# "wireguard://ANOTHER_PRIVATE_KEY@...#WG-on-WARP"
49+
]
50+
51+
# --- 3. Parse all configurations ---
52+
parsed_warp_config = parse_uri(warp_uri)
53+
parsed_test_configs = [p for p in (parse_uri(uri) for uri in test_uris) if p]
54+
55+
# --- Input Validation ---
56+
if not parsed_warp_config or "YOUR_PRIVATE_KEY" in warp_uri:
57+
print("! Please provide a valid WARP WireGuard URI.")
58+
return
59+
if not parsed_test_configs:
60+
print("! No valid primary configurations found to test.")
61+
return
62+
63+
tester = ConnectionTester(
64+
vendor_path=str(project_root / "vendor"),
65+
core_engine_path=str(project_root / "core_engine")
66+
)
67+
68+
# --- 4. Run the test, passing the WARP config to the `warp_config` parameter ---
69+
print(f"\n--- Running Connectivity Test with WARP-on-Any enabled ---")
70+
results = tester.test_uris(
71+
parsed_params=parsed_test_configs,
72+
warp_config=parsed_warp_config,
73+
timeout=30 # Allow a longer timeout for chained connections
74+
)
75+
76+
# --- 5. Print the results ---
77+
if results:
78+
print("\n" + "="*20 + " WARP ON ANY PING TEST RESULTS " + "="*20)
79+
sorted_results = sorted(results, key=lambda x: x.get('ping_ms', 9999))
80+
for result in sorted_results:
81+
tag = result.get('tag', 'N/A')
82+
ping = result.get('ping_ms', -1)
83+
status = result.get('status', 'error')
84+
85+
if status == 'success':
86+
print(f"* Tag: {tag:<30} | Ping: {ping:>4} ms | Status: SUCCESS")
87+
else:
88+
print(f"! Tag: {tag:<30} | Ping: ---- ms | Status: FAILED ({status})")
89+
else:
90+
print("! No results were received from the tester.")
91+
92+
if __name__ == "__main__":
93+
main()
94+
```
95+
96+
## API Reference: The `warp_config` Parameter
97+
98+
To enable proxy chaining, a new optional parameter has been added to all primary testing methods:
99+
100+
- `test_uris(..., warp_config: Optional[ConfigParams] = None)`
101+
- `test_speed(..., warp_config: Optional[ConfigParams] = None)`
102+
- `test_upload(..., warp_config: Optional[ConfigParams] = None)`
103+
104+
Simply pass a parsed `ConfigParams` object of a valid WireGuard configuration to this parameter, and the `ConnectionTester` will handle the rest.
105+
106+
## Important Considerations
107+
108+
- The `warp_config` **must** be a valid `ConfigParams` object generated from a WireGuard URI.
109+
- This chaining feature is handled by Xray's `dialerProxy`. Therefore, it only applies to protocols that are managed within the merged Xray instance (VLESS, VMess, Trojan, etc.). It does **not** apply to protocols like Hysteria that are launched in a separate, independent process by the Go test engine.

0 commit comments

Comments
 (0)