feat: add v2rayN.Bridge CLI for external tool integration

Add a lightweight command-line interface project (v2rayN.Bridge) that
exposes v2rayN's ServiceLib configuration parsing and generation
capabilities to external scripts and tools.

Features:
- Parse share links to JSON (`to-json` command)
- Batch-parse subscription content (`parse-sub` command)
- Convert server profiles back to share URIs (`to-uri` command)
- Self-contained single-file binary deployment

Includes:
- Program.cs with CLI command handling
- Project file targeting .NET 10.0 with ServiceLib reference
- Comprehensive README with build instructions and Python integration example
- Solution file updates adding the new project and x64/x86 platform configurations
This commit is contained in:
Amir Hossein Samiee 2026-05-11 01:42:54 +03:30
parent eee43288a4
commit d350775eb3
4 changed files with 331 additions and 2 deletions

View file

@ -0,0 +1,104 @@
using System.Text.Json;
using ServiceLib.Handler;
using ServiceLib.Handler.Fmt;
using ServiceLib.Models;
if (args.Length < 2)
{
Console.WriteLine("Usage: v2rayN.Bridge <command> <argument>");
Console.WriteLine("Commands: to-uri, parse-sub, to-json");
return;
}
string command = args[0];
string argument = args[1];
switch (command)
{
case "to-json":
ToJson(argument);
break;
case "parse-sub":
ParseSub(argument);
break;
case "to-uri":
ToUri(argument);
break;
default:
Console.WriteLine("Unknown command");
break;
}
static void ToJson(string shareLink)
{
string msg;
var item = FmtHandler.ResolveConfig(shareLink, out msg);
if (item != null)
{
var json = JsonSerializer.Serialize(
item,
new JsonSerializerOptions { WriteIndented = true }
);
Console.WriteLine(json);
}
else
{
Console.WriteLine($"ERROR: {msg}");
}
}
static void ParseSub(string configText)
{
// For parsing subscription content or config text
// Split by newlines and parse each line as a potential link
string msg;
var lines = configText.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
var items = new List<ProfileItem>();
foreach (var line in lines)
{
var trimmed = line.Trim();
if (!string.IsNullOrEmpty(trimmed))
{
var item = FmtHandler.ResolveConfig(trimmed, out msg);
if (item != null)
{
items.Add(item);
}
}
}
var json = JsonSerializer.Serialize(
items,
new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}
);
Console.WriteLine(json);
}
static void ToUri(string jsonItem)
{
try
{
var options = new JsonSerializerOptions();
var item = JsonSerializer.Deserialize<ProfileItem>(jsonItem, options);
if (item != null)
{
var uri = FmtHandler.GetShareUri(item);
Console.WriteLine(uri ?? "Failed to generate URI");
}
else
{
Console.WriteLine("ERROR: Invalid ProfileItem JSON");
}
}
catch (Exception ex)
{
Console.WriteLine($"ERROR: {ex.Message}");
}
}

View file

@ -0,0 +1,139 @@
# v2rayN.Bridge
A lightweight CLI that exposes [v2rayN](https://github.com/2dust/v2rayN)'s battle-tested
configuration parsing, generation, and (hopefully, in the future) other features, to external scripts, tools, and languages.
## Why?
v2rayN handles an enormous variety of proxy protocols, share links, and subscription
formats. Being wildly used by many users, either directly via the original client or indirectly via its numerous forks,
the popularity makes it sort of the "standard" for dealing with this context, be it for casual use or development. The
capability to use v2rayN's native, well-known, and widely-maintainend features directly gives you a seamless experience
for an extensive range of interests.
**v2rayN.Bridge aims to wrap the exact same `ServiceLib` code that v2rayN uses
internally and makes it available through a simple stdout interface.**
## Vision
`v2rayN.Bridge` is the first step toward **an APIlike interface for v2rayN**.
Eventually it should be able to:
- Load and manipulate the local configuration database
- Trigger speed tests, ping checks, and subscription updates
- Manage routing rules, DNS settings, and core config generation
- Serve as the backend for headless deployments, web dashboards, or
crossplatform GUI alternatives
Each new command is a `case` statement added to `Program.cs`.
If you can call a `ServiceLib` method from C#, you can expose it here.
## Current Features
- Parse any v2rayN share link (VMess, VLESS, Trojan, Shadowsocks, Hysteria2, TUIC, …)
- Batchparse subscription content
- Convert a server profile back into a share URI
- Designed for piping: JSON in, JSON out
- Selfcontained singlefile binary no .NET runtime required on the target machine
## Build
```bash
# From the repository root (the parent directory of this README file)
dotnet publish v2rayN.Bridge -c Release -r <YOUR-OS> --self-contained true -p:PublishSingleFile=true
```
Replace `<YOUR-OS>` with `win-x64`, `linux-x64`, `osx-x64`, or `osx-arm64` accordingly.
## Usage
### CLI commands
```
v2rayN.Bridge <command> <argument>
```
| Command | Argument | Output |
| ----------- | ------------------------------- | ----------------------------------- |
| `to-json` | A share link (e.g. `vmess://…`) | JSON object (`ProfileItem`) |
| `parse-sub` | Subscription text (all URLs) | JSON array of `ProfileItem` objects |
| `to-uri` | JSON of a `ProfileItem` | The share link as a string |
### Example: Python integration
```python
# create test.py next to this README file
from pathlib import Path
import subprocess
import json
class Bridge:
def __init__(self, path_to_exec):
self.exec = path_to_exec
def run(self, *cmdargs) -> str:
proc = subprocess.run([self.exec, *cmdargs], capture_output=True, text=True)
if proc.returncode:
raise RuntimeError(proc.stderr)
return proc.stdout
def load_uri(self, uri: str):
json_str = self.run("to-json", uri)
return json.loads(json_str)
def dump_uri(self, config: dict):
return self.run("to-uri", json.dumps(config))
def extract_configs(self, sub_content):
return json.loads(self.run("parse-sub", sub_content))
sub_content = """
this is an example of some text content, probably
a config subscription's response, potentially
including config URIs supported by v2rayN;
such as:
ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp1MTdUM0J2cFlhYWl1VzJj@api.namasha.co:443
vmess://ew0KICAidiI6ICIyIiwNCiAgInBzIjogIiIsDQogICJhZGQiOiAiIiwNCiAgInBvcnQiOiAiMCIsDQogICJpZCI6ICIiLA0KICAiYWlkIjogIjAiLA0KICAic2N5IjogIiIsDQogICJuZXQiOiAidGNwIiwNCiAgInRscyI6ICIiLA0KICAiYWxwbiI6ICIiLA0KICAiaW5zZWN1cmUiOiAiMCINCn0=
and also pieces of plain text as well (obviously!)
"""
# individual URIs
uris = [
"vless://7f8f9f5d-d75e-4dd4-b921-c63b56d50865@185.143.234.25:2052?encryption=none&security=none&type=ws&host=tr-st3.ge9.ir&path=%2Fpath#%C9%A2%E1%B4%87%CA%80%E1%B4%8D%E1%B4%80%C9%B4%20%C2%B9%20%7C%20%F0%9F%87%A9%F0%9F%87%AA",
"trojan://J6aoK74aaYaReDimz0zvQw@dateandusage.a:8443?type=tcp&headerType=none",
]
config = {"log": {"loglevel": "debug"}, "dns": {"hosts": {"dns.google": ["8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844"], "cloudflare-dns.com": ["104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9"]}, "servers": [{"address": "119.29.29.29", "domains": ["geosite:private", "geosite:cn"], "skipFallback": True, "tag": "direct-dns-2"}, "https://cloudflare-dns.com/dns-query"], "tag": "dns-module"}, "inbounds": [{"tag": "socks", "port": 10808, "listen": "127.0.0.1", "protocol": "mixed", "sniffing": {"enabled": True, "destOverride": ["http", "tls", "fakedns+others", "fakedns", "quic"], "routeOnly": False}, "settings": {"auth": "noauth", "udp": True, "allowTransparent": False}}], "outbounds": [{"tag": "proxy", "protocol": "vless", "settings": {"vnext": [{"address": "185.143.233.200", "port": 2082, "users": [{"id": "1691eb05-2fce-4ee1-b32b-c9b4e1647992", "email": "t@t.tt", "security": "auto", "encryption": "none"} ]} ] }, "streamSettings": {"network": "ws", "wsSettings": {"path": "/", "host": "nima.hayalco.ir", "headers": {}}}, "mux": {"enabled": True, "concurrency": 8}}], "routing": {"domainStrategy": "AsIs", "rules": [{"type": "field", "inboundTag": ["api"], "outboundTag": "api"}]}, "metrics": {"tag": "api"}, "policy": {"system": {"statsOutboundUplink": True, "statsOutboundDownlink": True}}, "stats": {}}
# after build, walk through the following path and modify in
# cases of difference; based on your OS, dotnet version, etc.
exec = Path(__file__).parent.joinpath("bin", "Release", "net10.0", "win-x64", "v2rayN.Bridge.exe")
assert exec.exists()
bridge = Bridge(exec)
if __name__ == "__main__":
extracted = bridge.extract_configs(sub_content)
print("\nextracted configs from sub:", extracted)
for uri in uris:
loaded = bridge.load_uri(uri)
print("\nloaded uri:", loaded)
dumped = bridge.dump_uri(config)
print("\ndumped uri:", dumped)
```
## Contributing
1. Fork v2rayN, and add your command to `v2rayN.Bridge/Program.cs`.
2. Ensure JSON output matches the `ServiceLib` model properties (PascalCase).
3. Test with your favourite scripting language.
4. Open a PR and submit.
## License
Same as v2rayN [GPL-3.0](https://github.com/2dust/v2rayN/blob/master/LICENSE).

View file

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\ServiceLib\ServiceLib.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View file

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.5.11709.299 stable
VisualStudioVersion = 18.5.11709.299
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "v2rayN", "v2rayN\v2rayN.csproj", "{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}"
EndProject
@ -36,40 +36,114 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceLib.Tests", "Service
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceLib.UdpTest", "ServiceLib.UdpTest\ServiceLib.UdpTest.csproj", "{CE9D9298-0289-4718-2522-B236489F2912}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "v2rayN.Bridge", "v2rayN.Bridge\v2rayN.Bridge.csproj", "{008E6D04-F573-42FE-9299-6C3500F4DE7B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Debug|x64.ActiveCfg = Debug|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Debug|x64.Build.0 = Debug|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Debug|x86.ActiveCfg = Debug|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Debug|x86.Build.0 = Debug|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Release|Any CPU.Build.0 = Release|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Release|x64.ActiveCfg = Release|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Release|x64.Build.0 = Release|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Release|x86.ActiveCfg = Release|Any CPU
{6DE127CA-1763-4236-B297-D2EF9CB2EC9B}.Release|x86.Build.0 = Release|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Debug|x64.ActiveCfg = Debug|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Debug|x64.Build.0 = Debug|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Debug|x86.ActiveCfg = Debug|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Debug|x86.Build.0 = Debug|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Release|Any CPU.Build.0 = Release|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Release|x64.ActiveCfg = Release|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Release|x64.Build.0 = Release|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Release|x86.ActiveCfg = Release|Any CPU
{1B6456C4-FFAA-4298-80F6-7B689A6E9243}.Release|x86.Build.0 = Release|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Debug|x64.ActiveCfg = Debug|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Debug|x64.Build.0 = Debug|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Debug|x86.ActiveCfg = Debug|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Debug|x86.Build.0 = Debug|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Release|Any CPU.Build.0 = Release|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Release|x64.ActiveCfg = Release|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Release|x64.Build.0 = Release|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Release|x86.ActiveCfg = Release|Any CPU
{5D16541A-F971-4C17-9315-BB8955E3F984}.Release|x86.Build.0 = Release|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Debug|x64.ActiveCfg = Debug|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Debug|x64.Build.0 = Debug|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Debug|x86.ActiveCfg = Debug|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Debug|x86.Build.0 = Debug|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Release|Any CPU.Build.0 = Release|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Release|x64.ActiveCfg = Release|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Release|x64.Build.0 = Release|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Release|x86.ActiveCfg = Release|Any CPU
{47D68B1C-601C-4C69-873B-FFF0DC13EC97}.Release|x86.Build.0 = Release|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Debug|x64.ActiveCfg = Debug|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Debug|x64.Build.0 = Debug|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Debug|x86.ActiveCfg = Debug|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Debug|x86.Build.0 = Debug|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Release|Any CPU.Build.0 = Release|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Release|x64.ActiveCfg = Release|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Release|x64.Build.0 = Release|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Release|x86.ActiveCfg = Release|Any CPU
{CB3DE54F-3A26-AE02-1299-311132C32156}.Release|x86.Build.0 = Release|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|x64.ActiveCfg = Debug|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|x64.Build.0 = Debug|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|x86.ActiveCfg = Debug|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Debug|x86.Build.0 = Debug|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|Any CPU.Build.0 = Release|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|x64.ActiveCfg = Release|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|x64.Build.0 = Release|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|x86.ActiveCfg = Release|Any CPU
{E0B6C5C7-ED48-42EB-947A-877779E9F555}.Release|x86.Build.0 = Release|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Debug|x64.ActiveCfg = Debug|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Debug|x64.Build.0 = Debug|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Debug|x86.ActiveCfg = Debug|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Debug|x86.Build.0 = Debug|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Release|Any CPU.Build.0 = Release|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Release|x64.ActiveCfg = Release|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Release|x64.Build.0 = Release|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Release|x86.ActiveCfg = Release|Any CPU
{CE9D9298-0289-4718-2522-B236489F2912}.Release|x86.Build.0 = Release|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Debug|x64.ActiveCfg = Debug|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Debug|x64.Build.0 = Debug|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Debug|x86.ActiveCfg = Debug|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Debug|x86.Build.0 = Debug|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Release|Any CPU.Build.0 = Release|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Release|x64.ActiveCfg = Release|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Release|x64.Build.0 = Release|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Release|x86.ActiveCfg = Release|Any CPU
{008E6D04-F573-42FE-9299-6C3500F4DE7B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE