| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- package sub
- import (
- "encoding/json"
- "testing"
- "github.com/mhsanaei/3x-ui/v3/database/model"
- )
- func hasDirectOutOutbound(svc *SubJsonService) bool {
- for _, raw := range svc.defaultOutbounds {
- var outbound map[string]any
- if err := json.Unmarshal(raw, &outbound); err != nil {
- continue
- }
- if outbound["tag"] == "direct_out" {
- return true
- }
- }
- return false
- }
- func outboundSettings(t *testing.T, raw []byte) map[string]any {
- t.Helper()
- var parsed map[string]any
- if err := json.Unmarshal(raw, &parsed); err != nil {
- t.Fatalf("failed to unmarshal outbound: %v", err)
- }
- settings, _ := parsed["settings"].(map[string]any)
- if settings == nil {
- t.Fatal("outbound has no settings")
- }
- return settings
- }
- func TestSubJsonServiceInjectsGlobalFinalMask(t *testing.T) {
- finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello","length":"100-200","delay":"10-20"}}],"udp":[{"type":"noise","settings":{"noise":[{"type":"base64","packet":"SGVsbG8="}]}}],"quicParams":{"congestion":"bbr"}}`
- svc := NewSubJsonService("", "", finalMask, nil)
- if hasDirectOutOutbound(svc) {
- t.Fatal("direct_out outbound must never be emitted")
- }
- stream := svc.streamData(`{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`)
- if _, ok := stream["sockopt"]; ok {
- t.Fatal("legacy direct_out dialerProxy sockopt must never be set")
- }
- finalmask, _ := stream["finalmask"].(map[string]any)
- if finalmask == nil {
- t.Fatal("streamSettings is missing finalmask")
- }
- tcp, _ := finalmask["tcp"].([]any)
- if len(tcp) != 1 {
- t.Fatalf("tcp masks len = %d, want 1", len(tcp))
- }
- if first, _ := tcp[0].(map[string]any); first["type"] != "fragment" {
- t.Fatalf("tcp[0] type = %v, want fragment", first["type"])
- }
- udp, _ := finalmask["udp"].([]any)
- if len(udp) != 1 {
- t.Fatalf("udp masks len = %d, want 1", len(udp))
- }
- quic, _ := finalmask["quicParams"].(map[string]any)
- if quic == nil || quic["congestion"] != "bbr" {
- t.Fatalf("quicParams missing/wrong: %#v", finalmask["quicParams"])
- }
- }
- func TestSubJsonServiceMergesWithExistingFinalMask(t *testing.T) {
- finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello"}}]}`
- svc := NewSubJsonService("", "", finalMask, nil)
- stream := svc.streamData(`{
- "network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}},
- "finalmask":{"tcp":[{"type":"sudoku"}]}
- }`)
- finalmask, _ := stream["finalmask"].(map[string]any)
- tcp, _ := finalmask["tcp"].([]any)
- if len(tcp) != 2 {
- t.Fatalf("tcp masks len = %d, want 2 (existing + global)", len(tcp))
- }
- a, _ := tcp[0].(map[string]any)
- b, _ := tcp[1].(map[string]any)
- if a["type"] != "sudoku" || b["type"] != "fragment" {
- t.Fatalf("tcp masks = %#v, want existing sudoku then global fragment", tcp)
- }
- }
- func TestSubJsonServiceNoFinalMaskWhenEmpty(t *testing.T) {
- svc := NewSubJsonService("", "", "", nil)
- stream := svc.streamData(`{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`)
- if _, ok := stream["finalmask"]; ok {
- t.Fatal("no finalmask should be emitted when subJsonFinalMask is empty")
- }
- if _, ok := stream["sockopt"]; ok {
- t.Fatal("legacy direct_out sockopt must never be set")
- }
- }
- func TestSubJsonServiceVlessFlattened(t *testing.T) {
- inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`}
- client := model.Client{ID: "uuid-1", Flow: "xtls-rprx-vision"}
- settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genVless(inbound, nil, client))
- if _, ok := settings["vnext"]; ok {
- t.Fatal("vless outbound must not use vnext")
- }
- if settings["address"] != "1.2.3.4" || settings["id"] != "uuid-1" || settings["encryption"] != "none" || settings["flow"] != "xtls-rprx-vision" {
- t.Fatalf("flat vless settings wrong: %#v", settings)
- }
- }
- func TestSubJsonServiceVmessFlattened(t *testing.T) {
- inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VMESS, Settings: `{}`}
- client := model.Client{ID: "uuid-2"}
- settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genVnext(inbound, nil, client))
- if _, ok := settings["vnext"]; ok {
- t.Fatal("vmess outbound must not use vnext")
- }
- if settings["id"] != "uuid-2" || settings["security"] != "auto" {
- t.Fatalf("flat vmess settings wrong: %#v", settings)
- }
- }
- func TestSubJsonServiceServerFlattened(t *testing.T) {
- trojan := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.Trojan, Settings: `{}`}
- client := model.Client{Password: "p4ss"}
- settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genServer(trojan, nil, client))
- if _, ok := settings["servers"]; ok {
- t.Fatal("trojan outbound must not use servers array")
- }
- if settings["password"] != "p4ss" || settings["address"] != "1.2.3.4" {
- t.Fatalf("flat trojan settings wrong: %#v", settings)
- }
- ss := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.Shadowsocks, Settings: `{"method":"aes-256-gcm"}`}
- ssSettings := outboundSettings(t, NewSubJsonService("", "", "", nil).genServer(ss, nil, client))
- if ssSettings["method"] != "aes-256-gcm" {
- t.Fatalf("flat shadowsocks must carry method: %#v", ssSettings)
- }
- }
|