| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | package xray | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							| 
									
										
										
										
											2023-07-17 23:10:22 +00:00
										 |  |  | 	"encoding/json" | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"regexp" | 
					
						
							|  |  |  | 	"time" | 
					
						
							| 
									
										
										
										
											2024-03-10 21:31:24 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-17 23:10:22 +00:00
										 |  |  | 	"x-ui/logger" | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	"x-ui/util/common" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/xtls/xray-core/app/proxyman/command" | 
					
						
							|  |  |  | 	statsService "github.com/xtls/xray-core/app/stats/command" | 
					
						
							|  |  |  | 	"github.com/xtls/xray-core/common/protocol" | 
					
						
							|  |  |  | 	"github.com/xtls/xray-core/common/serial" | 
					
						
							| 
									
										
										
										
											2023-07-17 23:10:22 +00:00
										 |  |  | 	"github.com/xtls/xray-core/infra/conf" | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	"github.com/xtls/xray-core/proxy/shadowsocks" | 
					
						
							| 
									
										
										
										
											2023-07-27 08:04:46 +00:00
										 |  |  | 	"github.com/xtls/xray-core/proxy/shadowsocks_2022" | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	"github.com/xtls/xray-core/proxy/trojan" | 
					
						
							|  |  |  | 	"github.com/xtls/xray-core/proxy/vless" | 
					
						
							|  |  |  | 	"github.com/xtls/xray-core/proxy/vmess" | 
					
						
							|  |  |  | 	"google.golang.org/grpc" | 
					
						
							|  |  |  | 	"google.golang.org/grpc/credentials/insecure" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type XrayAPI struct { | 
					
						
							|  |  |  | 	HandlerServiceClient *command.HandlerServiceClient | 
					
						
							|  |  |  | 	StatsServiceClient   *statsService.StatsServiceClient | 
					
						
							|  |  |  | 	grpcClient           *grpc.ClientConn | 
					
						
							|  |  |  | 	isConnected          bool | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | func (x *XrayAPI) Init(apiPort int) error { | 
					
						
							|  |  |  | 	if apiPort <= 0 { | 
					
						
							|  |  |  | 		return fmt.Errorf("invalid Xray API port: %d", apiPort) | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	addr := fmt.Sprintf("127.0.0.1:%d", apiPort) | 
					
						
							|  |  |  | 	conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 		return fmt.Errorf("failed to connect to Xray API: %w", err) | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-01 17:22:35 +00:00
										 |  |  | 	x.grpcClient = conn | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	x.isConnected = true | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 	hsClient := command.NewHandlerServiceClient(conn) | 
					
						
							|  |  |  | 	ssClient := statsService.NewStatsServiceClient(conn) | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	x.HandlerServiceClient = &hsClient | 
					
						
							|  |  |  | 	x.StatsServiceClient = &ssClient | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 	return nil | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (x *XrayAPI) Close() { | 
					
						
							| 
									
										
										
										
											2024-01-02 09:42:07 +00:00
										 |  |  | 	if x.grpcClient != nil { | 
					
						
							|  |  |  | 		x.grpcClient.Close() | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	x.HandlerServiceClient = nil | 
					
						
							|  |  |  | 	x.StatsServiceClient = nil | 
					
						
							|  |  |  | 	x.isConnected = false | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-17 23:10:22 +00:00
										 |  |  | func (x *XrayAPI) AddInbound(inbound []byte) error { | 
					
						
							|  |  |  | 	client := *x.HandlerServiceClient | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	conf := new(conf.InboundDetourConfig) | 
					
						
							|  |  |  | 	err := json.Unmarshal(inbound, conf) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Debug("Failed to unmarshal inbound:", err) | 
					
						
							| 
									
										
										
										
											2023-07-27 08:28:12 +00:00
										 |  |  | 		return err | 
					
						
							| 
									
										
										
										
											2023-07-17 23:10:22 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	config, err := conf.Build() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		logger.Debug("Failed to build inbound Detur:", err) | 
					
						
							| 
									
										
										
										
											2023-07-27 08:28:12 +00:00
										 |  |  | 		return err | 
					
						
							| 
									
										
										
										
											2023-07-17 23:10:22 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	inboundConfig := command.AddInboundRequest{Inbound: config} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	_, err = client.AddInbound(context.Background(), &inboundConfig) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (x *XrayAPI) DelInbound(tag string) error { | 
					
						
							|  |  |  | 	client := *x.HandlerServiceClient | 
					
						
							|  |  |  | 	_, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{ | 
					
						
							|  |  |  | 		Tag: tag, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	return err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-12 19:13:51 +00:00
										 |  |  | func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error { | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	var account *serial.TypedMessage | 
					
						
							|  |  |  | 	switch Protocol { | 
					
						
							|  |  |  | 	case "vmess": | 
					
						
							|  |  |  | 		account = serial.ToTypedMessage(&vmess.Account{ | 
					
						
							| 
									
										
										
										
											2023-06-13 13:56:50 +00:00
										 |  |  | 			Id: user["id"].(string), | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 		}) | 
					
						
							|  |  |  | 	case "vless": | 
					
						
							|  |  |  | 		account = serial.ToTypedMessage(&vless.Account{ | 
					
						
							|  |  |  | 			Id:   user["id"].(string), | 
					
						
							|  |  |  | 			Flow: user["flow"].(string), | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	case "trojan": | 
					
						
							|  |  |  | 		account = serial.ToTypedMessage(&trojan.Account{ | 
					
						
							|  |  |  | 			Password: user["password"].(string), | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	case "shadowsocks": | 
					
						
							| 
									
										
										
										
											2023-07-27 08:04:46 +00:00
										 |  |  | 		var ssCipherType shadowsocks.CipherType | 
					
						
							|  |  |  | 		switch user["cipher"].(string) { | 
					
						
							|  |  |  | 		case "aes-128-gcm": | 
					
						
							|  |  |  | 			ssCipherType = shadowsocks.CipherType_AES_128_GCM | 
					
						
							|  |  |  | 		case "aes-256-gcm": | 
					
						
							|  |  |  | 			ssCipherType = shadowsocks.CipherType_AES_256_GCM | 
					
						
							| 
									
										
										
										
											2023-08-29 19:02:49 +00:00
										 |  |  | 		case "chacha20-poly1305", "chacha20-ietf-poly1305": | 
					
						
							| 
									
										
										
										
											2023-07-27 08:04:46 +00:00
										 |  |  | 			ssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305 | 
					
						
							| 
									
										
										
										
											2023-08-29 19:02:49 +00:00
										 |  |  | 		case "xchacha20-poly1305", "xchacha20-ietf-poly1305": | 
					
						
							| 
									
										
										
										
											2023-07-27 08:04:46 +00:00
										 |  |  | 			ssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305 | 
					
						
							|  |  |  | 		default: | 
					
						
							|  |  |  | 			ssCipherType = shadowsocks.CipherType_NONE | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if ssCipherType != shadowsocks.CipherType_NONE { | 
					
						
							|  |  |  | 			account = serial.ToTypedMessage(&shadowsocks.Account{ | 
					
						
							|  |  |  | 				Password:   user["password"].(string), | 
					
						
							|  |  |  | 				CipherType: ssCipherType, | 
					
						
							|  |  |  | 			}) | 
					
						
							|  |  |  | 		} else { | 
					
						
							| 
									
										
										
										
											2024-11-13 10:47:09 +00:00
										 |  |  | 			account = serial.ToTypedMessage(&shadowsocks_2022.ServerConfig{ | 
					
						
							| 
									
										
										
										
											2023-07-27 08:04:46 +00:00
										 |  |  | 				Key:   user["password"].(string), | 
					
						
							|  |  |  | 				Email: user["email"].(string), | 
					
						
							|  |  |  | 			}) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	default: | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	client := *x.HandlerServiceClient | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	_, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{ | 
					
						
							|  |  |  | 		Tag: inboundTag, | 
					
						
							|  |  |  | 		Operation: serial.ToTypedMessage(&command.AddUserOperation{ | 
					
						
							|  |  |  | 			User: &protocol.User{ | 
					
						
							|  |  |  | 				Email:   user["email"].(string), | 
					
						
							|  |  |  | 				Account: account, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 		}), | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 	return err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | func (x *XrayAPI) RemoveUser(inboundTag, email string) error { | 
					
						
							|  |  |  | 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | 
					
						
							|  |  |  | 	defer cancel() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	op := &command.RemoveUserOperation{Email: email} | 
					
						
							|  |  |  | 	req := &command.AlterInboundRequest{ | 
					
						
							|  |  |  | 		Tag:       inboundTag, | 
					
						
							|  |  |  | 		Operation: serial.ToTypedMessage(op), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	_, err := (*x.HandlerServiceClient).AlterInbound(ctx, req) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return fmt.Errorf("failed to remove user: %w", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { | 
					
						
							|  |  |  | 	if x.grpcClient == nil { | 
					
						
							|  |  |  | 		return nil, nil, common.NewError("xray api is not initialized") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 	trafficRegex := regexp.MustCompile(`(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)`) | 
					
						
							|  |  |  | 	clientTrafficRegex := regexp.MustCompile(`user>>>([^>]+)>>>traffic>>>(downlink|uplink)`) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) | 
					
						
							|  |  |  | 	defer cancel() | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 09:11:28 +00:00
										 |  |  | 	if x.StatsServiceClient == nil { | 
					
						
							|  |  |  | 		return nil, nil, common.NewError("xray StatusServiceClient is not initialized") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 	resp, err := (*x.StatsServiceClient).QueryStats(ctx, &statsService.QueryStatsRequest{Reset_: reset}) | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2024-07-08 21:08:00 +00:00
										 |  |  | 		logger.Debug("Failed to query Xray stats:", err) | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 		return nil, nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 	tagTrafficMap := make(map[string]*Traffic) | 
					
						
							|  |  |  | 	emailTrafficMap := make(map[string]*ClientTraffic) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	for _, stat := range resp.GetStat() { | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 		if matches := trafficRegex.FindStringSubmatch(stat.Name); len(matches) == 4 { | 
					
						
							|  |  |  | 			processTraffic(matches, stat.Value, tagTrafficMap) | 
					
						
							|  |  |  | 		} else if matches := clientTrafficRegex.FindStringSubmatch(stat.Name); len(matches) == 3 { | 
					
						
							|  |  |  | 			processClientTraffic(matches, stat.Value, emailTrafficMap) | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) { | 
					
						
							|  |  |  | 	isInbound := matches[1] == "inbound" | 
					
						
							|  |  |  | 	tag := matches[2] | 
					
						
							|  |  |  | 	isDown := matches[3] == "downlink" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if tag == "api" { | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	traffic, ok := trafficMap[tag] | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		traffic = &Traffic{ | 
					
						
							|  |  |  | 			IsInbound:  isInbound, | 
					
						
							|  |  |  | 			IsOutbound: !isInbound, | 
					
						
							|  |  |  | 			Tag:        tag, | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 		trafficMap[tag] = traffic | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-03 22:17:28 +00:00
										 |  |  | 	if isDown { | 
					
						
							|  |  |  | 		traffic.Down = value | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		traffic.Up = value | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func processClientTraffic(matches []string, value int64, clientTrafficMap map[string]*ClientTraffic) { | 
					
						
							|  |  |  | 	email := matches[1] | 
					
						
							|  |  |  | 	isDown := matches[2] == "downlink" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	traffic, ok := clientTrafficMap[email] | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		traffic = &ClientTraffic{Email: email} | 
					
						
							|  |  |  | 		clientTrafficMap[email] = traffic | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if isDown { | 
					
						
							|  |  |  | 		traffic.Down = value | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		traffic.Up = value | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func mapToSlice[T any](m map[string]*T) []*T { | 
					
						
							|  |  |  | 	result := make([]*T, 0, len(m)) | 
					
						
							|  |  |  | 	for _, v := range m { | 
					
						
							|  |  |  | 		result = append(result, v) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return result | 
					
						
							| 
									
										
										
										
											2023-06-04 21:02:19 +00:00
										 |  |  | } |