|
|
@@ -0,0 +1,190 @@
|
|
|
+package sub
|
|
|
+
|
|
|
+import (
|
|
|
+ "net/url"
|
|
|
+ "reflect"
|
|
|
+ "strings"
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
+)
|
|
|
+
|
|
|
+func TestGenTuicLinkBasic(t *testing.T) {
|
|
|
+ inbound := &model.Inbound{
|
|
|
+ Listen: "198.51.100.1",
|
|
|
+ Port: 8443,
|
|
|
+ Protocol: model.TUIC,
|
|
|
+ Remark: "tuic-test",
|
|
|
+ Settings: `{"server":{"certificate":"/path/cert","private_key":"/path/key"},"clients":[{"uuid":"11111111-1111-1111-1111-111111111111","password":"testpassword","email":"user@test"}]}`,
|
|
|
+ }
|
|
|
+
|
|
|
+ s := &SubService{}
|
|
|
+ link := s.genTuicLink(inbound, "user@test")
|
|
|
+
|
|
|
+ u, err := url.Parse(link)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("link does not parse: %v\ngot: %s", err, link)
|
|
|
+ }
|
|
|
+ if u.Scheme != "tuic" {
|
|
|
+ t.Fatalf("scheme = %q, want tuic", u.Scheme)
|
|
|
+ }
|
|
|
+ if u.Host != "198.51.100.1:8443" {
|
|
|
+ t.Fatalf("host = %q, want 198.51.100.1:8443", u.Host)
|
|
|
+ }
|
|
|
+ if u.User.Username() != "11111111-1111-1111-1111-111111111111" {
|
|
|
+ t.Fatalf("username = %q, want uuid", u.User.Username())
|
|
|
+ }
|
|
|
+ pass, ok := u.User.Password()
|
|
|
+ if !ok || pass != "testpassword" {
|
|
|
+ t.Fatalf("password = %q, want testpassword", pass)
|
|
|
+ }
|
|
|
+ q := u.Query()
|
|
|
+ if q.Get("congestion_control") != "bbr" {
|
|
|
+ t.Fatalf("congestion_control = %q, want bbr", q.Get("congestion_control"))
|
|
|
+ }
|
|
|
+ if q.Get("allow_insecure") != "0" {
|
|
|
+ t.Fatalf("allow_insecure = %q, want 0", q.Get("allow_insecure"))
|
|
|
+ }
|
|
|
+ if q.Get("alpn") != "h3,spdy/3.1" {
|
|
|
+ t.Fatalf("alpn = %q, want h3,spdy/3.1", q.Get("alpn"))
|
|
|
+ }
|
|
|
+ if q.Get("udp_relay_mode") != "native" {
|
|
|
+ t.Fatalf("udp_relay_mode = %q, want native", q.Get("udp_relay_mode"))
|
|
|
+ }
|
|
|
+ if u.Fragment != "tuic-test-user@test" {
|
|
|
+ t.Fatalf("fragment = %q, want tuic-test-user@test", u.Fragment)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestGenTuicLinkExplicitParams(t *testing.T) {
|
|
|
+ inbound := &model.Inbound{
|
|
|
+ Listen: "198.51.100.1",
|
|
|
+ Port: 8443,
|
|
|
+ Protocol: model.TUIC,
|
|
|
+ Remark: "tuic-test",
|
|
|
+ Settings: `{"server":{"certificate":"/path/cert","private_key":"/path/key","congestion_control":"cubic","alpn":["h3"],"sni":"tuic.example.com","udp_relay_mode":"quic"},"clients":[{"uuid":"11111111-1111-1111-1111-111111111111","password":"testpassword","email":"user@test"}]}`,
|
|
|
+ }
|
|
|
+
|
|
|
+ s := &SubService{}
|
|
|
+ link := s.genTuicLink(inbound, "user@test")
|
|
|
+
|
|
|
+ u, err := url.Parse(link)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("link does not parse: %v\ngot: %s", err, link)
|
|
|
+ }
|
|
|
+ q := u.Query()
|
|
|
+ if q.Get("congestion_control") != "cubic" {
|
|
|
+ t.Fatalf("congestion_control = %q, want cubic", q.Get("congestion_control"))
|
|
|
+ }
|
|
|
+ if q.Get("alpn") != "h3" {
|
|
|
+ t.Fatalf("alpn = %q, want h3", q.Get("alpn"))
|
|
|
+ }
|
|
|
+ if q.Get("sni") != "tuic.example.com" {
|
|
|
+ t.Fatalf("sni = %q, want tuic.example.com", q.Get("sni"))
|
|
|
+ }
|
|
|
+ if q.Get("udp_relay_mode") != "quic" {
|
|
|
+ t.Fatalf("udp_relay_mode = %q, want quic", q.Get("udp_relay_mode"))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestGenTuicLinkExternalProxyFanOut(t *testing.T) {
|
|
|
+ inbound := &model.Inbound{
|
|
|
+ Listen: "0.0.0.0",
|
|
|
+ Port: 8443,
|
|
|
+ Protocol: model.TUIC,
|
|
|
+ Remark: "tuic-base",
|
|
|
+ Settings: `{"server":{"certificate":"/path/cert","private_key":"/path/key"},"clients":[{"uuid":"11111111-1111-1111-1111-111111111111","password":"testpassword","email":"user@test"}]}`,
|
|
|
+ StreamSettings: `{"externalProxy":[{"dest":"host1.example.com","port":9443,"remark":"US"},{"dest":"host2.example.com","port":10443,"remark":"EU","allowInsecure":true}]}`,
|
|
|
+ }
|
|
|
+
|
|
|
+ s := &SubService{}
|
|
|
+ links := s.genTuicLink(inbound, "user@test")
|
|
|
+ lines := strings.Split(strings.TrimSpace(links), "\n")
|
|
|
+ if len(lines) != 2 {
|
|
|
+ t.Fatalf("expected 2 links for externalProxy fan-out, got %d:\n%s", len(lines), links)
|
|
|
+ }
|
|
|
+
|
|
|
+ u1, err := url.Parse(lines[0])
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("first link does not parse: %v", err)
|
|
|
+ }
|
|
|
+ if u1.Host != "host1.example.com:9443" {
|
|
|
+ t.Fatalf("first host = %q, want host1.example.com:9443", u1.Host)
|
|
|
+ }
|
|
|
+ if !strings.Contains(u1.Fragment, "US") {
|
|
|
+ t.Fatalf("first fragment = %q, want to contain US", u1.Fragment)
|
|
|
+ }
|
|
|
+ if u1.Query().Get("allow_insecure") != "0" {
|
|
|
+ t.Fatalf("first allow_insecure = %q, want 0", u1.Query().Get("allow_insecure"))
|
|
|
+ }
|
|
|
+ if u1.Query().Has("allowInsecure") {
|
|
|
+ t.Fatalf("first link should not have allowInsecure")
|
|
|
+ }
|
|
|
+
|
|
|
+ u2, err := url.Parse(lines[1])
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("second link does not parse: %v", err)
|
|
|
+ }
|
|
|
+ if u2.Host != "host2.example.com:10443" {
|
|
|
+ t.Fatalf("second host = %q, want host2.example.com:10443", u2.Host)
|
|
|
+ }
|
|
|
+ if !strings.Contains(u2.Fragment, "EU") {
|
|
|
+ t.Fatalf("second fragment = %q, want to contain EU", u2.Fragment)
|
|
|
+ }
|
|
|
+ if u2.Query().Get("allow_insecure") != "1" {
|
|
|
+ t.Fatalf("second allow_insecure = %q, want 1", u2.Query().Get("allow_insecure"))
|
|
|
+ }
|
|
|
+ if u2.Query().Has("allowInsecure") {
|
|
|
+ t.Fatalf("second link should not have allowInsecure")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestBuildTuicProxy_ExternalProxyOverrides(t *testing.T) {
|
|
|
+ svc := &SubClashService{SubService: &SubService{address: "sub.example.com"}}
|
|
|
+ inbound := &model.Inbound{
|
|
|
+ Listen: "198.51.100.1",
|
|
|
+ Port: 8443,
|
|
|
+ Protocol: model.TUIC,
|
|
|
+ Remark: "tuic-base",
|
|
|
+ Settings: `{"server":{"certificate":"/path/cert","private_key":"/path/key","sni":"base.example.com","alpn":["h3"]},"clients":[{"uuid":"11111111-1111-1111-1111-111111111111","password":"testpassword","email":"user@test"}]}`,
|
|
|
+ }
|
|
|
+ client := model.Client{Email: "user@test"}
|
|
|
+
|
|
|
+ // 1. Without overrides from ep
|
|
|
+ baseProxy := svc.buildTuicProxy(svc.SubService, inbound, client, nil)
|
|
|
+ if baseProxy == nil {
|
|
|
+ t.Fatal("baseProxy is nil")
|
|
|
+ }
|
|
|
+ if baseProxy["sni"] != "base.example.com" {
|
|
|
+ t.Fatalf("base sni = %v, want base.example.com", baseProxy["sni"])
|
|
|
+ }
|
|
|
+ if !reflect.DeepEqual(baseProxy["alpn"], []string{"h3"}) {
|
|
|
+ t.Fatalf("base alpn = %v, want [h3]", baseProxy["alpn"])
|
|
|
+ }
|
|
|
+ if _, ok := baseProxy["skip-cert-verify"]; ok {
|
|
|
+ t.Fatalf("base skip-cert-verify should not be set")
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. With overrides from ep
|
|
|
+ ep := map[string]any{
|
|
|
+ "dest": "custom.example.com",
|
|
|
+ "port": float64(9443),
|
|
|
+ "remark": "custom-node",
|
|
|
+ "sni": "custom.sni.com",
|
|
|
+ "alpn": "h3,spdy/3.1",
|
|
|
+ "allowInsecure": true,
|
|
|
+ }
|
|
|
+ proxy := svc.buildTuicProxy(svc.SubService, inbound, client, ep)
|
|
|
+ if proxy == nil {
|
|
|
+ t.Fatal("proxy is nil")
|
|
|
+ }
|
|
|
+ if proxy["sni"] != "custom.sni.com" {
|
|
|
+ t.Fatalf("sni = %v, want custom.sni.com", proxy["sni"])
|
|
|
+ }
|
|
|
+ if !reflect.DeepEqual(proxy["alpn"], []string{"h3", "spdy/3.1"}) {
|
|
|
+ t.Fatalf("alpn = %v, want [h3 spdy/3.1]", proxy["alpn"])
|
|
|
+ }
|
|
|
+ if proxy["skip-cert-verify"] != true {
|
|
|
+ t.Fatalf("skip-cert-verify = %v, want true", proxy["skip-cert-verify"])
|
|
|
+ }
|
|
|
+}
|