From 155c6ba2903ae170f60b6d193d2b039abe1c6147 Mon Sep 17 00:00:00 2001 From: Chang lue Tsen Date: Tue, 6 May 2025 18:36:35 +0900 Subject: [PATCH] feat(adapter): add V2rayN adapter and build method for proxy management --- internal/logic/subscribe/subscribeLogic.go | 8 +- pkg/adapter/adapter.go | 4 + pkg/adapter/general/uri.go | 51 +++- pkg/adapter/v2rayn/v2rayN.go | 266 +++++++++++++++++++++ 4 files changed, 315 insertions(+), 14 deletions(-) create mode 100644 pkg/adapter/v2rayn/v2rayN.go diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index 08ef50d..5e59d0c 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -117,12 +117,14 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, serverIds := tool.StringToInt64Slice(subDetails.Server) groupIds := tool.StringToInt64Slice(subDetails.ServerGroup) + logger.Debugf("[Generate Subscribe]serverIds: %v, groupIds: %v", serverIds, groupIds) + servers, err := l.svc.ServerModel.FindServerDetailByGroupIdsAndIds(l.ctx.Request.Context(), groupIds, serverIds) if err != nil { l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) } - + logger.Debugf("[Generate Subscribe]found servers: %v", len(servers)) return servers, nil } @@ -241,6 +243,9 @@ func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub SubscribeURL: subsURL, }) l.setSurfboardHeaders() + case "v2rayn": + resp = proxyManager.BuildV2rayN(userSub.UUID) + default: resp = proxyManager.BuildGeneral(userSub.UUID) } @@ -286,6 +291,7 @@ func (l *SubscribeLogic) getClientType(req *types.SubscribeRequest) string { "shadowrocket": "shadowrocket", "loon": "loon", "surfboard": "surfboard", + "v2rayn": "v2rayn", } findClient := func(s string) string { diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go index daf60ff..4ad1e38 100644 --- a/pkg/adapter/adapter.go +++ b/pkg/adapter/adapter.go @@ -10,6 +10,7 @@ import ( "github.com/perfect-panel/server/pkg/adapter/shadowrocket" "github.com/perfect-panel/server/pkg/adapter/singbox" "github.com/perfect-panel/server/pkg/adapter/surfboard" + "github.com/perfect-panel/server/pkg/adapter/v2rayn" ) type Config struct { @@ -80,3 +81,6 @@ func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte { return surfboard.BuildSurfboard(m.Adapter, siteName, user) } +func (m *Adapter) BuildV2rayN(uuid string) []byte { + return v2rayn.NewV2rayN(m.Adapter).Build(uuid) +} diff --git a/pkg/adapter/general/uri.go b/pkg/adapter/general/uri.go index 36034a2..cbaba28 100644 --- a/pkg/adapter/general/uri.go +++ b/pkg/adapter/general/uri.go @@ -69,7 +69,10 @@ func buildProxy(data proxy.Proxy, uuid string) string { } func ShadowsocksUri(data proxy.Proxy, uuid string) string { - ss := data.Option.(proxy.Shadowsocks) + ss, ok := data.Option.(proxy.Shadowsocks) + if !ok { + return "" + } // sip002 u := &url.URL{ Scheme: "ss", @@ -94,10 +97,20 @@ func VmessUri(data proxy.Proxy, uuid string) string { Port: fmt.Sprint(data.Port), ID: uuid, Aid: "0", - Net: vmess.Transport, - // Type: "?", - Host: transport.Host, - Path: transport.Path, + } + + switch vmess.Transport { + case "websocket": + s.Net = "ws" + s.Path = transport.Path + s.Host = transport.Host + case "grpc": + s.Net = "grpc" + s.Path = transport.ServiceName + case "httpupgrade": + s.Net = "http" + s.Path = transport.Path + s.Host = transport.Host } if vmess.Security == "tls" { @@ -117,23 +130,35 @@ func VlessUri(data proxy.Proxy, uuid string) string { var query = make(url.Values) setQuery(&query, "flow", vless.Flow) - setQuery(&query, "type", vless.Transport) setQuery(&query, "security", vless.Security) switch vless.Transport { - case "ws", "http", "httpupgrade": + case "websocket": + setQuery(&query, "type", "ws") + setQuery(&query, "host", transportConfig.Host) + setQuery(&query, "path", transportConfig.Path) + + case "http2", "httpupgrade": + setQuery(&query, "type", vless.Transport) setQuery(&query, "path", transportConfig.Path) setQuery(&query, "host", transportConfig.Host) case "grpc": + setQuery(&query, "type", "grpc") setQuery(&query, "serviceName", transportConfig.ServiceName) - case "meek": - setQuery(&query, "url", transportConfig.Host) } - setQuery(&query, "sni", securityConfig.SNI) - setQuery(&query, "fp", securityConfig.Fingerprint) - setQuery(&query, "pbk", securityConfig.RealityPublicKey) - setQuery(&query, "sid", securityConfig.RealityShortId) + if vless.Security == "tls" { + setQuery(&query, "sni", securityConfig.SNI) + setQuery(&query, "fp", securityConfig.Fingerprint) + } else if vless.Security == "reality" { + setQuery(&query, "pbk", securityConfig.RealityPublicKey) + setQuery(&query, "sid", securityConfig.RealityShortId) + setQuery(&query, "sni", securityConfig.SNI) + setQuery(&query, "fp", securityConfig.Fingerprint) + setQuery(&query, "servername", securityConfig.SNI) + setQuery(&query, "spx", "/") + + } u := url.URL{ Scheme: "vless", diff --git a/pkg/adapter/v2rayn/v2rayN.go b/pkg/adapter/v2rayn/v2rayN.go new file mode 100644 index 0000000..5ed4ee5 --- /dev/null +++ b/pkg/adapter/v2rayn/v2rayN.go @@ -0,0 +1,266 @@ +package v2rayn + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "github.com/perfect-panel/server/pkg/adapter/proxy" + "net" + "net/url" + "strconv" + "strings" +) + +type v2rayShareLink struct { + Ps string `json:"ps"` + Add string `json:"add"` + Port string `json:"port"` + ID string `json:"id"` + Aid string `json:"aid"` + Net string `json:"net"` + Type string `json:"type"` + Host string `json:"host"` + SNI string `json:"sni"` + Path string `json:"path"` + TLS string `json:"tls"` + Flow string `json:"flow,omitempty"` + Alpn string `json:"alpn,omitempty"` + AllowInsecure bool `json:"allowInsecure,omitempty"` + Fingerprint string `json:"fp,omitempty"` + PublicKey string `json:"pbk,omitempty"` + ShortId string `json:"sid,omitempty"` + SpiderX string `json:"spx,omitempty"` + V string `json:"v"` +} +type V2rayN struct { + proxy.Adapter +} + +func NewV2rayN(adapter proxy.Adapter) *V2rayN { + return &V2rayN{ + Adapter: adapter, + } +} +func (m *V2rayN) Build(uuid string) []byte { + uri := "" + for _, p := range m.Proxies { + switch p.Protocol { + case "shadowsocks": + uri += m.buildShadowsocks(uuid, p) + "\r\n" + case "vmess": + uri += m.buildVmess(uuid, p) + "\r\n" + case "vless": + uri += m.buildVless(uuid, p) + "\r\n" + case "trojan": + uri += m.buildTrojan(uuid, p) + "\r\n" + case "hysteria2": + uri += m.buildHysteria2(uuid, p) + "\r\n" + case "tuic": + uri += m.buildTuic(uuid, p) + "\r\n" + } + } + result := base64.StdEncoding.EncodeToString([]byte(uri)) + + return []byte(result) +} + +func (m *V2rayN) buildShadowsocks(uuid string, data proxy.Proxy) string { + ss, ok := data.Option.(proxy.Shadowsocks) + if !ok { + return "" + } + // sip002 + u := &url.URL{ + Scheme: "ss", + // 还没有写 2022 的 + User: url.User(strings.TrimSuffix(base64.URLEncoding.EncodeToString([]byte(ss.Method+":"+uuid)), "=")), + Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), + Fragment: data.Name, + } + return u.String() +} + +func (m *V2rayN) buildTrojan(uuid string, data proxy.Proxy) string { + trojan := data.Option.(proxy.Trojan) + transportConfig := trojan.TransportConfig + securityConfig := trojan.SecurityConfig + + var query = make(url.Values) + setQuery(&query, "type", trojan.Transport) + setQuery(&query, "security", trojan.Security) + + switch trojan.Transport { + case "ws", "http", "httpupgrade": + setQuery(&query, "path", transportConfig.Path) + setQuery(&query, "host", transportConfig.Host) + case "grpc": + setQuery(&query, "serviceName", transportConfig.ServiceName) + case "meek": + setQuery(&query, "url", transportConfig.Host) + } + + setQuery(&query, "sni", securityConfig.SNI) + setQuery(&query, "fp", securityConfig.Fingerprint) + setQuery(&query, "pbk", securityConfig.RealityPublicKey) + setQuery(&query, "sid", securityConfig.RealityShortId) + + if securityConfig.AllowInsecure { + setQuery(&query, "allowInsecure", "1") + } + + u := &url.URL{ + Scheme: "trojan", + User: url.User(uuid), + Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + +func (m *V2rayN) buildVmess(uuid string, data proxy.Proxy) string { + vmess := data.Option.(proxy.Vmess) + + transport := vmess.TransportConfig + + securityConfig := vmess.SecurityConfig + + var s = v2rayShareLink{ + V: "2", + Add: data.Server, + Port: fmt.Sprint(data.Port), + ID: uuid, + Aid: "0", + } + + switch vmess.Transport { + case "websocket": + s.Net = "ws" + s.Path = transport.Path + s.Host = transport.Host + case "grpc": + s.Net = "grpc" + s.Path = transport.ServiceName + case "httpupgrade": + s.Net = "http" + s.Path = transport.Path + s.Host = transport.Host + } + + if vmess.Security == "tls" { + s.TLS = "tls" + s.SNI = securityConfig.SNI + s.AllowInsecure = securityConfig.AllowInsecure + s.Fingerprint = securityConfig.Fingerprint + } + b, _ := json.Marshal(s) + return "vmess://" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(b), "=") +} + +func (m *V2rayN) buildVless(uuid string, data proxy.Proxy) string { + vless := data.Option.(proxy.Vless) + transportConfig := vless.TransportConfig + securityConfig := vless.SecurityConfig + + var query = make(url.Values) + setQuery(&query, "flow", vless.Flow) + setQuery(&query, "security", vless.Security) + + switch vless.Transport { + case "websocket": + setQuery(&query, "type", "ws") + setQuery(&query, "host", transportConfig.Host) + setQuery(&query, "path", transportConfig.Path) + + case "http2", "httpupgrade": + setQuery(&query, "type", vless.Transport) + setQuery(&query, "path", transportConfig.Path) + setQuery(&query, "host", transportConfig.Host) + case "grpc": + setQuery(&query, "type", "grpc") + setQuery(&query, "serviceName", transportConfig.ServiceName) + } + + if vless.Security == "tls" { + setQuery(&query, "sni", securityConfig.SNI) + setQuery(&query, "fp", securityConfig.Fingerprint) + } else if vless.Security == "reality" { + setQuery(&query, "pbk", securityConfig.RealityPublicKey) + setQuery(&query, "sid", securityConfig.RealityShortId) + setQuery(&query, "sni", securityConfig.SNI) + setQuery(&query, "fp", securityConfig.Fingerprint) + setQuery(&query, "servername", securityConfig.SNI) + setQuery(&query, "spx", "/") + + } + + u := url.URL{ + Scheme: "vless", + User: url.User(uuid), + Host: net.JoinHostPort(data.Server, fmt.Sprint(data.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + +func (m *V2rayN) buildHysteria2(uuid string, data proxy.Proxy) string { + hysteria2 := data.Option.(proxy.Hysteria2) + + var query = make(url.Values) + + setQuery(&query, "sni", hysteria2.SecurityConfig.SNI) + + if hysteria2.SecurityConfig.AllowInsecure { + setQuery(&query, "insecure", "1") + } + + if hp := strings.TrimSpace(hysteria2.HopPorts); hp != "" { + setQuery(&query, "mport", hp) + } + + if hysteria2.ObfsPassword != "" { + setQuery(&query, "obfs", "salamander") + setQuery(&query, "obfs-password", hysteria2.ObfsPassword) + } + + u := &url.URL{ + Scheme: "hysteria2", + User: url.User(uuid), + Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + +func (m *V2rayN) buildTuic(uuid string, data proxy.Proxy) string { + tuic := data.Option.(proxy.Tuic) + var query = make(url.Values) + + setQuery(&query, "congestion_control", "bbr") + + if tuic.SecurityConfig.SNI == "" { + setQuery(&query, "sni", tuic.SecurityConfig.SNI) + } else { + setQuery(&query, "disable_sni", "1") + } + if tuic.SecurityConfig.AllowInsecure { + setQuery(&query, "allow_insecure", "1") + } + + u := &url.URL{ + Scheme: "tuic", + User: url.User(uuid + ":" + uuid), + Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + +func setQuery(q *url.Values, k, v string) { + if v != "" { + q.Set(k, v) + } +}