- proxypool: 站点级出口绑定 proxies = [条目名, "sub:订阅名"],粘性选路、 失败切换、后台健康探测;支持 Xray 分享链接 / 订阅 / 裸 ip:port 与 socks5 端点, 托管 Xray 核心(可自动下载);站点未绑定时回退 [proxy] 主/备线路 - upstream: RouteSource 抽象 + routesFor/poolReport;求解与请求共用同一出口 - admin: 代理池面板与维护动作;overview 暴露池状态
146 lines
5.0 KiB
Go
146 lines
5.0 KiB
Go
package proxypool
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseVless(t *testing.T) {
|
|
line := "vless://11111111-2222-3333-4444-555555555555@example.com:443" +
|
|
"?type=ws&security=tls&sni=sni.example.com&path=%2Fws&host=cdn.example.com&fp=chrome#node"
|
|
n, err := ParseNode(line, "http")
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if n.Kind != "xray" || n.Scheme != "vless" || n.Display != "example.com:443" {
|
|
t.Fatalf("node = %+v", n)
|
|
}
|
|
if n.Outbound["protocol"] != "vless" {
|
|
t.Fatalf("protocol = %v", n.Outbound["protocol"])
|
|
}
|
|
settings := n.Outbound["settings"].(map[string]any)
|
|
vnext := settings["vnext"].([]any)[0].(map[string]any)
|
|
if vnext["address"] != "example.com" || vnext["port"] != 443 {
|
|
t.Fatalf("vnext = %+v", vnext)
|
|
}
|
|
users := vnext["users"].([]any)[0].(map[string]any)
|
|
if users["id"] != "11111111-2222-3333-4444-555555555555" || users["encryption"] != "none" {
|
|
t.Fatalf("users = %+v", users)
|
|
}
|
|
ss := n.Outbound["streamSettings"].(map[string]any)
|
|
if ss["network"] != "ws" || ss["security"] != "tls" {
|
|
t.Fatalf("stream = %+v", ss)
|
|
}
|
|
ws := ss["wsSettings"].(map[string]any)
|
|
if ws["path"] != "/ws" {
|
|
t.Fatalf("ws = %+v", ws)
|
|
}
|
|
tls := ss["tlsSettings"].(map[string]any)
|
|
if tls["serverName"] != "sni.example.com" {
|
|
t.Fatalf("tls = %+v", tls)
|
|
}
|
|
}
|
|
|
|
func TestParseVmess(t *testing.T) {
|
|
payload := `{"v":"2","ps":"n","add":"vm.example.com","port":"443","id":"uuid-1",` +
|
|
`"aid":"0","scy":"auto","net":"ws","type":"none","host":"vm.example.com",` +
|
|
`"path":"/vm","tls":"tls","sni":"vm.example.com"}`
|
|
line := "vmess://" + base64.StdEncoding.EncodeToString([]byte(payload))
|
|
n, err := ParseNode(line, "")
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if n.Kind != "xray" || n.Scheme != "vmess" || n.Display != "vm.example.com:443" {
|
|
t.Fatalf("node = %+v", n)
|
|
}
|
|
ss := n.Outbound["streamSettings"].(map[string]any)
|
|
if ss["network"] != "ws" || ss["security"] != "tls" {
|
|
t.Fatalf("stream = %+v", ss)
|
|
}
|
|
if ss["wsSettings"].(map[string]any)["path"] != "/vm" {
|
|
t.Fatalf("ws = %+v", ss["wsSettings"])
|
|
}
|
|
}
|
|
|
|
func TestParseTrojan(t *testing.T) {
|
|
n, err := ParseNode("trojan://secret@tr.example.com:443?security=tls&sni=tr.example.com#t", "")
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if n.Scheme != "trojan" || n.Display != "tr.example.com:443" {
|
|
t.Fatalf("node = %+v", n)
|
|
}
|
|
settings := n.Outbound["settings"].(map[string]any)
|
|
srv := settings["servers"].([]any)[0].(map[string]any)
|
|
if srv["address"] != "tr.example.com" || srv["port"] != 443 || srv["password"] != "secret" {
|
|
t.Fatalf("server = %+v", srv)
|
|
}
|
|
}
|
|
|
|
func TestParseShadowsocks(t *testing.T) {
|
|
// SIP002: base64(method:password)@host:port
|
|
ui := base64.RawURLEncoding.EncodeToString([]byte("aes-256-gcm:pass:word"))
|
|
n, err := ParseNode("ss://"+ui+"@ss.example.com:8388#x", "")
|
|
if err != nil {
|
|
t.Fatalf("sip002 parse: %v", err)
|
|
}
|
|
srv := n.Outbound["settings"].(map[string]any)["servers"].([]any)[0].(map[string]any)
|
|
if srv["method"] != "aes-256-gcm" || srv["password"] != "pass:word" || srv["address"] != "ss.example.com" {
|
|
t.Fatalf("server = %+v", srv)
|
|
}
|
|
|
|
// legacy: base64(method:password@host:port)
|
|
legacy := base64.RawURLEncoding.EncodeToString([]byte("aes-256-gcm:secret@ss.example.com:8388"))
|
|
n2, err := ParseNode("ss://"+legacy, "")
|
|
if err != nil {
|
|
t.Fatalf("legacy parse: %v", err)
|
|
}
|
|
srv2 := n2.Outbound["settings"].(map[string]any)["servers"].([]any)[0].(map[string]any)
|
|
if srv2["password"] != "secret" || srv2["address"] != "ss.example.com" || srv2["port"] != 8388 {
|
|
t.Fatalf("legacy server = %+v", srv2)
|
|
}
|
|
}
|
|
|
|
func TestParsePlainEndpoints(t *testing.T) {
|
|
cases := []struct {
|
|
line, def, wantURL, wantDisplay string
|
|
}{
|
|
{"socks5://u:p@1.2.3.4:1080", "http", "socks5://u:p@1.2.3.4:1080", "1.2.3.4:1080"},
|
|
{"http://1.2.3.4:8080", "socks5", "http://1.2.3.4:8080", "1.2.3.4:8080"},
|
|
{"1.2.3.4:8080", "http", "http://1.2.3.4:8080", "1.2.3.4:8080"},
|
|
{"1.2.3.4:8080", "socks5", "socks5://1.2.3.4:8080", "1.2.3.4:8080"},
|
|
{"u:p@1.2.3.4:1080", "http", "http://u:p@1.2.3.4:1080", "1.2.3.4:1080"},
|
|
{"1.2.3.4:8080:u:p", "http", "http://u:p@1.2.3.4:8080", "1.2.3.4:8080"},
|
|
}
|
|
for _, c := range cases {
|
|
n, err := ParseNode(c.line, c.def)
|
|
if err != nil {
|
|
t.Fatalf("%q: %v", c.line, err)
|
|
}
|
|
if n.Kind != "url" || n.URL != c.wantURL || n.Display != c.wantDisplay {
|
|
t.Errorf("%q => kind=%s url=%q display=%q", c.line, n.Kind, n.URL, n.Display)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseSubscriptionBody(t *testing.T) {
|
|
plain := "vless://a@b:1\n# comment\nsocks5://x:y@1.2.3.4:5\n\n"
|
|
got := ParseSubscriptionBody([]byte(plain))
|
|
if len(got) != 2 || got[0] != "vless://a@b:1" || got[1] != "socks5://x:y@1.2.3.4:5" {
|
|
t.Fatalf("plain parse = %v", got)
|
|
}
|
|
enc := base64.StdEncoding.EncodeToString([]byte(plain))
|
|
got2 := ParseSubscriptionBody([]byte(enc))
|
|
if len(got2) != 2 || got2[0] != got[0] {
|
|
t.Fatalf("base64 parse = %v", got2)
|
|
}
|
|
}
|
|
|
|
func TestParseNodeRejectsJunk(t *testing.T) {
|
|
for _, bad := range []string{"", "hello world", "vless://no-port", "ss://@@@", "1.2.3.4", "ftp://1.2.3.4:21"} {
|
|
if _, err := ParseNode(bad, "http"); err == nil {
|
|
t.Errorf("%q should fail to parse", bad)
|
|
}
|
|
}
|
|
}
|