diff --git a/sub/subClashService.go b/sub/subClashService.go new file mode 100644 index 00000000..ea095919 --- /dev/null +++ b/sub/subClashService.go @@ -0,0 +1,385 @@ +package sub + +import ( + "fmt" + "strings" + + "github.com/goccy/go-json" + yaml "github.com/goccy/go-yaml" + + "github.com/mhsanaei/3x-ui/v2/database/model" + "github.com/mhsanaei/3x-ui/v2/logger" + "github.com/mhsanaei/3x-ui/v2/web/service" + "github.com/mhsanaei/3x-ui/v2/xray" +) + +type SubClashService struct { + inboundService service.InboundService + SubService *SubService +} + +type ClashConfig struct { + Proxies []map[string]any `yaml:"proxies"` + ProxyGroups []map[string]any `yaml:"proxy-groups"` + Rules []string `yaml:"rules"` +} + +func NewSubClashService(subService *SubService) *SubClashService { + return &SubClashService{SubService: subService} +} + +func (s *SubClashService) GetClash(subId string, host string) (string, string, error) { + inbounds, err := s.SubService.getInboundsBySubId(subId) + if err != nil || len(inbounds) == 0 { + return "", "", err + } + + var traffic xray.ClientTraffic + var clientTraffics []xray.ClientTraffic + var proxies []map[string]any + + for _, inbound := range inbounds { + clients, err := s.inboundService.GetClients(inbound) + if err != nil { + logger.Error("SubClashService - GetClients: Unable to get clients from inbound") + } + if clients == nil { + continue + } + if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { + listen, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings) + if err == nil { + inbound.Listen = listen + inbound.Port = port + inbound.StreamSettings = streamSettings + } + } + for _, client := range clients { + if client.Enable && client.SubID == subId { + clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email)) + proxies = append(proxies, s.getProxies(inbound, client, host)...) + } + } + } + + if len(proxies) == 0 { + return "", "", nil + } + + for index, clientTraffic := range clientTraffics { + if index == 0 { + traffic.Up = clientTraffic.Up + traffic.Down = clientTraffic.Down + traffic.Total = clientTraffic.Total + if clientTraffic.ExpiryTime > 0 { + traffic.ExpiryTime = clientTraffic.ExpiryTime + } + } else { + traffic.Up += clientTraffic.Up + traffic.Down += clientTraffic.Down + if traffic.Total == 0 || clientTraffic.Total == 0 { + traffic.Total = 0 + } else { + traffic.Total += clientTraffic.Total + } + if clientTraffic.ExpiryTime != traffic.ExpiryTime { + traffic.ExpiryTime = 0 + } + } + } + + proxyNames := make([]string, 0, len(proxies)+1) + for _, proxy := range proxies { + if name, ok := proxy["name"].(string); ok && name != "" { + proxyNames = append(proxyNames, name) + } + } + proxyNames = append(proxyNames, "DIRECT") + + config := ClashConfig{ + Proxies: proxies, + ProxyGroups: []map[string]any{{ + "name": "PROXY", + "type": "select", + "proxies": proxyNames, + }}, + Rules: []string{"MATCH,PROXY"}, + } + + finalYAML, err := yaml.Marshal(config) + if err != nil { + return "", "", err + } + + header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) + return string(finalYAML), header, nil +} + +func (s *SubClashService) getProxies(inbound *model.Inbound, client model.Client, host string) []map[string]any { + stream := s.streamData(inbound.StreamSettings) + externalProxies, ok := stream["externalProxy"].([]any) + if !ok || len(externalProxies) == 0 { + externalProxies = []any{map[string]any{ + "forceTls": "same", + "dest": host, + "port": float64(inbound.Port), + "remark": "", + }} + } + delete(stream, "externalProxy") + + proxies := make([]map[string]any, 0, len(externalProxies)) + for _, ep := range externalProxies { + extPrxy := ep.(map[string]any) + workingInbound := *inbound + workingInbound.Listen = extPrxy["dest"].(string) + workingInbound.Port = int(extPrxy["port"].(float64)) + workingStream := cloneMap(stream) + + switch extPrxy["forceTls"].(string) { + case "tls": + if workingStream["security"] != "tls" { + workingStream["security"] = "tls" + workingStream["tlsSettings"] = map[string]any{} + } + case "none": + if workingStream["security"] != "none" { + workingStream["security"] = "none" + delete(workingStream, "tlsSettings") + delete(workingStream, "realitySettings") + } + } + + proxy := s.buildProxy(&workingInbound, client, workingStream, extPrxy["remark"].(string)) + if len(proxy) > 0 { + proxies = append(proxies, proxy) + } + } + return proxies +} + +func (s *SubClashService) buildProxy(inbound *model.Inbound, client model.Client, stream map[string]any, extraRemark string) map[string]any { + proxy := map[string]any{ + "name": s.SubService.genRemark(inbound, client.Email, extraRemark), + "server": inbound.Listen, + "port": inbound.Port, + "udp": true, + } + + network, _ := stream["network"].(string) + if !s.applyTransport(proxy, network, stream) { + return nil + } + + switch inbound.Protocol { + case model.VMESS: + proxy["type"] = "vmess" + proxy["uuid"] = client.ID + proxy["alterId"] = 0 + cipher := client.Security + if cipher == "" { + cipher = "auto" + } + proxy["cipher"] = cipher + case model.VLESS: + proxy["type"] = "vless" + proxy["uuid"] = client.ID + if client.Flow != "" && network == "tcp" { + proxy["flow"] = client.Flow + } + var inboundSettings map[string]any + json.Unmarshal([]byte(inbound.Settings), &inboundSettings) + if encryption, ok := inboundSettings["encryption"].(string); ok && encryption != "" { + proxy["packet-encoding"] = encryption + } + case model.Trojan: + proxy["type"] = "trojan" + proxy["password"] = client.Password + case model.Shadowsocks: + proxy["type"] = "ss" + proxy["password"] = client.Password + var inboundSettings map[string]any + json.Unmarshal([]byte(inbound.Settings), &inboundSettings) + method, _ := inboundSettings["method"].(string) + if method == "" { + return nil + } + proxy["cipher"] = method + if strings.HasPrefix(method, "2022") { + if serverPassword, ok := inboundSettings["password"].(string); ok && serverPassword != "" { + proxy["password"] = fmt.Sprintf("%s:%s", serverPassword, client.Password) + } + } + default: + return nil + } + + security, _ := stream["security"].(string) + if !s.applySecurity(proxy, security, stream) { + return nil + } + + return proxy +} + +func (s *SubClashService) applyTransport(proxy map[string]any, network string, stream map[string]any) bool { + switch network { + case "", "tcp": + proxy["network"] = "tcp" + tcp, _ := stream["tcpSettings"].(map[string]any) + if tcp != nil { + header, _ := tcp["header"].(map[string]any) + if header != nil { + typeStr, _ := header["type"].(string) + if typeStr != "" && typeStr != "none" { + return false + } + } + } + return true + case "ws": + proxy["network"] = "ws" + ws, _ := stream["wsSettings"].(map[string]any) + wsOpts := map[string]any{} + if ws != nil { + if path, ok := ws["path"].(string); ok && path != "" { + wsOpts["path"] = path + } + host := "" + if v, ok := ws["host"].(string); ok && v != "" { + host = v + } else if headers, ok := ws["headers"].(map[string]any); ok { + host = searchHost(headers) + } + if host != "" { + wsOpts["headers"] = map[string]any{"Host": host} + } + } + if len(wsOpts) > 0 { + proxy["ws-opts"] = wsOpts + } + return true + case "grpc": + proxy["network"] = "grpc" + grpc, _ := stream["grpcSettings"].(map[string]any) + grpcOpts := map[string]any{} + if grpc != nil { + if serviceName, ok := grpc["serviceName"].(string); ok && serviceName != "" { + grpcOpts["grpc-service-name"] = serviceName + } + } + if len(grpcOpts) > 0 { + proxy["grpc-opts"] = grpcOpts + } + return true + default: + return false + } +} + +func (s *SubClashService) applySecurity(proxy map[string]any, security string, stream map[string]any) bool { + switch security { + case "", "none": + proxy["tls"] = false + return true + case "tls": + proxy["tls"] = true + tlsSettings, _ := stream["tlsSettings"].(map[string]any) + if tlsSettings != nil { + if serverName, ok := tlsSettings["serverName"].(string); ok && serverName != "" { + proxy["servername"] = serverName + switch proxy["type"] { + case "trojan": + proxy["sni"] = serverName + } + } + if fingerprint, ok := tlsSettings["fingerprint"].(string); ok && fingerprint != "" { + proxy["client-fingerprint"] = fingerprint + } + } + return true + case "reality": + proxy["tls"] = true + realitySettings, _ := stream["realitySettings"].(map[string]any) + if realitySettings == nil { + return false + } + if serverName, ok := realitySettings["serverName"].(string); ok && serverName != "" { + proxy["servername"] = serverName + } + realityOpts := map[string]any{} + if publicKey, ok := realitySettings["publicKey"].(string); ok && publicKey != "" { + realityOpts["public-key"] = publicKey + } + if shortID, ok := realitySettings["shortId"].(string); ok && shortID != "" { + realityOpts["short-id"] = shortID + } + if len(realityOpts) > 0 { + proxy["reality-opts"] = realityOpts + } + if fingerprint, ok := realitySettings["fingerprint"].(string); ok && fingerprint != "" { + proxy["client-fingerprint"] = fingerprint + } + return true + default: + return false + } +} + +func (s *SubClashService) streamData(stream string) map[string]any { + var streamSettings map[string]any + json.Unmarshal([]byte(stream), &streamSettings) + security, _ := streamSettings["security"].(string) + switch security { + case "tls": + if tlsSettings, ok := streamSettings["tlsSettings"].(map[string]any); ok { + streamSettings["tlsSettings"] = s.tlsData(tlsSettings) + } + case "reality": + if realitySettings, ok := streamSettings["realitySettings"].(map[string]any); ok { + streamSettings["realitySettings"] = s.realityData(realitySettings) + } + } + delete(streamSettings, "sockopt") + return streamSettings +} + +func (s *SubClashService) tlsData(tData map[string]any) map[string]any { + tlsData := make(map[string]any, 1) + tlsClientSettings, _ := tData["settings"].(map[string]any) + tlsData["serverName"] = tData["serverName"] + tlsData["alpn"] = tData["alpn"] + if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok { + tlsData["fingerprint"] = fingerprint + } + return tlsData +} + +func (s *SubClashService) realityData(rData map[string]any) map[string]any { + rDataOut := make(map[string]any, 1) + realityClientSettings, _ := rData["settings"].(map[string]any) + if publicKey, ok := realityClientSettings["publicKey"].(string); ok { + rDataOut["publicKey"] = publicKey + } + if fingerprint, ok := realityClientSettings["fingerprint"].(string); ok { + rDataOut["fingerprint"] = fingerprint + } + if serverNames, ok := rData["serverNames"].([]any); ok && len(serverNames) > 0 { + rDataOut["serverName"] = fmt.Sprint(serverNames[0]) + } + if shortIDs, ok := rData["shortIds"].([]any); ok && len(shortIDs) > 0 { + rDataOut["shortId"] = fmt.Sprint(shortIDs[0]) + } + return rDataOut +} + +func cloneMap(src map[string]any) map[string]any { + if src == nil { + return nil + } + dst := make(map[string]any, len(src)) + for k, v := range src { + dst[k] = v + } + return dst +}