diff --git a/app/router/command/command.pb.go b/app/router/command/command.pb.go index db55b17e9..4c14c9c18 100644 --- a/app/router/command/command.pb.go +++ b/app/router/command/command.pb.go @@ -186,19 +186,18 @@ func (x *RoutingContext) GetVlessRoute() uint32 { // opened by xray-core. // * FieldSelectors selects a subset of fields in routing statistics to return. // Valid selectors: -// - inbound: Selects connection's inbound tag. -// - network: Selects connection's network. -// - ip: Equivalent as "ip_source" and "ip_target", selects both source and -// target IP. -// - port: Equivalent as "port_source" and "port_target", selects both source -// and target port. -// - domain: Selects target domain. -// - protocol: Select connection's protocol. -// - user: Select connection's inbound user email. -// - attributes: Select connection's additional attributes. -// - outbound: Equivalent as "outbound" and "outbound_group", select both -// outbound tag and outbound group tags. -// +// - inbound: Selects connection's inbound tag. +// - network: Selects connection's network. +// - ip: Equivalent as "ip_source" and "ip_target", selects both source and +// target IP. +// - port: Equivalent as "port_source" and "port_target", selects both source +// and target port. +// - domain: Selects target domain. +// - protocol: Select connection's protocol. +// - user: Select connection's inbound user email. +// - attributes: Select connection's additional attributes. +// - outbound: Equivalent as "outbound" and "outbound_group", select both +// outbound tag and outbound group tags. // * If FieldSelectors is left empty, all fields will be returned. type SubscribeRoutingStatsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` diff --git a/go.mod b/go.mod index 312aac927..c0a08a59c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.27 require ( github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e + github.com/bufbuild/protocompile v0.14.1 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/golang/mock v1.7.0-rc.1 github.com/google/go-cmp v0.7.0 @@ -32,7 +33,7 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard/windows v1.1.1 google.golang.org/grpc v1.84.0 - google.golang.org/protobuf v1.36.12 + google.golang.org/protobuf v1.36.11 h12.io/socks v1.0.3 layeh.com/gopher-luar v1.0.11 lukechampine.com/blake3 v1.4.1 @@ -58,6 +59,12 @@ require ( golang.org/x/text v0.43.0 // indirect golang.org/x/tools v0.51.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect ) + +tool ( + google.golang.org/grpc/cmd/protoc-gen-go-grpc + google.golang.org/protobuf/cmd/protoc-gen-go +) diff --git a/go.sum b/go.sum index 289c2cefa..615a7afbb 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg= github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -152,6 +154,10 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/infra/vprotogen/main.go b/infra/vprotogen/main.go index 2c1e2298a..9233b1185 100644 --- a/infra/vprotogen/main.go +++ b/infra/vprotogen/main.go @@ -1,149 +1,84 @@ package main import ( - "errors" + "bytes" + "context" "flag" "fmt" - "go/build" - "io" - "net/http" + "io/fs" "os" "os/exec" "path/filepath" - "regexp" - "runtime" - "strconv" "strings" + + "github.com/bufbuild/protocompile" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" ) var directory = flag.String("pwd", "", "Working directory of Xray vprotogen.") -// envFile returns the name of the Go environment configuration file. -// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166 -func envFile() (string, error) { - if file := os.Getenv("GOENV"); file != "" { - if file == "off" { - return "", errors.New("GOENV=off") +// topoSort sorts file descriptors so each dependency appears before files importing it. +func topoSort(files []protoreflect.FileDescriptor) []*descriptorpb.FileDescriptorProto { + fileMap := make(map[string]protoreflect.FileDescriptor) + for _, f := range files { + fileMap[f.Path()] = f + } + + visited := make(map[string]bool) + var sorted []*descriptorpb.FileDescriptorProto + + var visit func(fd protoreflect.FileDescriptor) + visit = func(fd protoreflect.FileDescriptor) { + if visited[fd.Path()] { + return } - return file, nil - } - dir, err := os.UserConfigDir() - if err != nil { - return "", err - } - if dir == "" { - return "", errors.New("missing user-config dir") - } - return filepath.Join(dir, "go", "env"), nil -} + visited[fd.Path()] = true -// GetRuntimeEnv returns the value of runtime environment variable, -// that is set by running following command: `go env -w key=value`. -func GetRuntimeEnv(key string) (string, error) { - file, err := envFile() - if err != nil { - return "", err - } - if file == "" { - return "", errors.New("missing runtime env file") - } - var data []byte - var runtimeEnv string - data, readErr := os.ReadFile(file) - if readErr != nil { - return "", readErr - } - envStrings := strings.Split(string(data), "\n") - for _, envItem := range envStrings { - envItem = strings.TrimSuffix(envItem, "\r") - envKeyValue := strings.Split(envItem, "=") - if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) { - runtimeEnv = strings.TrimSpace(envKeyValue[1]) + imports := fd.Imports() + for i := 0; i < imports.Len(); i++ { + if dep, ok := fileMap[imports.Get(i).Path()]; ok { + visit(dep) + } } + sorted = append(sorted, protodesc.ToFileDescriptorProto(fd)) } - return runtimeEnv, nil + + for _, f := range files { + visit(f) + } + return sorted } -// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty. -func GetGOBIN() string { - // The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command` - GOBIN := os.Getenv("GOBIN") - if GOBIN == "" { - var err error - // The one set by user by running `go env -w GOBIN=/path` - GOBIN, err = GetRuntimeEnv("GOBIN") - if err != nil { - // The default one that Golang uses - return filepath.Join(build.Default.GOPATH, "bin") - } - if GOBIN == "" { - return filepath.Join(build.Default.GOPATH, "bin") - } - return GOBIN - } - return GOBIN -} - -func whichProtoc(suffix, targetedVersion string) (string, error) { - protoc := "protoc" + suffix - - path, err := exec.LookPath(protoc) +func runPlugin(pluginName, pwd string, req *pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorResponse, error) { + reqBytes, err := proto.Marshal(req) if err != nil { - return "", fmt.Errorf(` -Command "%s" not found. -Make sure that %s is in your system path or current path. -Download %s v%s or later from https://github.com/protocolbuffers/protobuf/releases -`, protoc, protoc, protoc, targetedVersion) + return nil, err } - return path, nil -} -func getProjectProtocVersion(url string) (string, error) { - resp, err := http.Get(url) - if err != nil { - return "", errors.New("can not get the version of protobuf used in xray project") - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", errors.New("can not read from body") - } - versionRegexp := regexp.MustCompile(`\/\/\s*protoc\s*v\d+\.(\d+\.\d+)`) - matched := versionRegexp.FindStringSubmatch(string(body)) - return matched[1], nil -} + cmd := exec.Command("go", "tool", pluginName) + cmd.Dir = pwd + cmd.Stdin = bytes.NewReader(reqBytes) + cmd.Env = os.Environ() + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr -func getInstalledProtocVersion(protocPath string) (string, error) { - cmd := exec.Command(protocPath, "--version") - cmd.Env = append(cmd.Env, os.Environ()...) - output, cmdErr := cmd.CombinedOutput() - if cmdErr != nil { - return "", cmdErr + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("%v: %s", err, stderr.String()) } - versionRegexp := regexp.MustCompile(`protoc\s*(\d+\.\d+)`) - matched := versionRegexp.FindStringSubmatch(string(output)) - return matched[1], nil -} -func parseVersion(s string, width int) int64 { - strList := strings.Split(s, ".") - format := fmt.Sprintf("%%s%%0%ds", width) - v := "" - for _, value := range strList { - v = fmt.Sprintf(format, v, value) + var resp pluginpb.CodeGeneratorResponse + if err := proto.Unmarshal(stdout.Bytes(), &resp); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) } - var result int64 - var err error - if result, err = strconv.ParseInt(v, 10, 64); err != nil { - return 0 + if resp.GetError() != "" { + return nil, fmt.Errorf("%s", resp.GetError()) } - return result -} - -func needToUpdate(targetedVersion, installedVersion string) bool { - vt := parseVersion(targetedVersion, 4) - vi := parseVersion(installedVersion, 4) - return vt > vi + return &resp, nil } func main() { @@ -154,104 +89,93 @@ func main() { flag.Parse() if !filepath.IsAbs(*directory) { - pwd, wdErr := os.Getwd() - if wdErr != nil { + pwd, err := os.Getwd() + if err != nil { fmt.Println("Can not get current working directory.") os.Exit(1) } *directory = filepath.Join(pwd, *directory) } - pwd := *directory - GOBIN := GetGOBIN() - binPath := os.Getenv("PATH") - pathSlice := []string{pwd, GOBIN, binPath} - binPath = strings.Join(pathSlice, string(os.PathListSeparator)) - os.Setenv("PATH", binPath) - suffix := "" - if runtime.GOOS == "windows" { - suffix = ".exe" - } - - /* - targetedVersion, err := getProjectProtocVersion("https://raw.githubusercontent.com/XTLS/Xray-core/HEAD/core/config.pb.go") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - */ - targetedVersion := "" - - protoc, err := whichProtoc(suffix, targetedVersion) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - installedVersion, err := getInstalledProtocVersion(protoc) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if needToUpdate(targetedVersion, installedVersion) { - fmt.Printf(` -You are using an old protobuf version, please update to v%s or later. -Download it from https://github.com/protocolbuffers/protobuf/releases - - * Protobuf version used in xray project: v%s - * Protobuf version you have installed: v%s - -`, targetedVersion, targetedVersion, installedVersion) - os.Exit(1) - } - - protoFilesMap := make(map[string][]string) - walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error { - if err != nil { - fmt.Println(err) + var protoFiles []string + err := filepath.WalkDir(pwd, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { return err } - - if info.IsDir() { - return nil + if strings.HasSuffix(d.Name(), ".proto") { + rel, _ := filepath.Rel(pwd, path) + protoFiles = append(protoFiles, filepath.ToSlash(rel)) } - - dir := filepath.Dir(path) - filename := filepath.Base(path) - if strings.HasSuffix(filename, ".proto") { - path = path[len(pwd)+1:] - protoFilesMap[dir] = append(protoFilesMap[dir], path) - } - return nil }) - if walkErr != nil { - fmt.Println(walkErr) + if err != nil { + fmt.Println("Walk error:", err) + os.Exit(1) + } + if len(protoFiles) == 0 { + return + } + + compiler := protocompile.Compiler{ + Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{ + ImportPaths: []string{pwd}, + }), + SourceInfoMode: protocompile.SourceInfoStandard, + } + + linkedFiles, err := compiler.Compile(context.Background(), protoFiles...) + if err != nil { + fmt.Fprintf(os.Stderr, "Protobuf compilation failed:\n%v\n", err) os.Exit(1) } - for _, files := range protoFilesMap { - for _, relProtoFile := range files { - args := []string{ - "--go_out", pwd, - "--go_opt", "paths=source_relative", - "--go-grpc_out", pwd, - "--go-grpc_opt", "paths=source_relative", - "--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix), - "--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix), + var fdList []protoreflect.FileDescriptor + for _, f := range linkedFiles { + fdList = append(fdList, f) + } + + req := &pluginpb.CodeGeneratorRequest{ + FileToGenerate: protoFiles, + Parameter: proto.String("paths=source_relative"), + ProtoFile: topoSort(fdList), + // TODO: replace with unknown in the future + CompilerVersion: &pluginpb.Version{ + Major: proto.Int32(6), + Minor: proto.Int32(33), + Patch: proto.Int32(5), + }, + } + + for _, plugin := range []string{"protoc-gen-go", "protoc-gen-go-grpc"} { + resp, err := runPlugin(plugin, pwd, req) + if err != nil { + fmt.Fprintf(os.Stderr, "%s failed: %v\n", plugin, err) + os.Exit(1) + } + + for _, f := range resp.File { + targetPath := filepath.Join(pwd, filepath.FromSlash(f.GetName())) + content := []byte(f.GetContent()) + + if existing, err := os.ReadFile(targetPath); err == nil { + oldNorm := bytes.ReplaceAll(existing, []byte("\r\n"), []byte("\n")) + newNorm := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n")) + if bytes.Equal(oldNorm, newNorm) { + continue + } } - args = append(args, relProtoFile) - cmd := exec.Command(protoc, args...) - cmd.Env = append(cmd.Env, os.Environ()...) - cmd.Dir = pwd - output, cmdErr := cmd.CombinedOutput() - if len(output) > 0 { - fmt.Println(string(output)) + + if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err) + os.Exit(1) } - if cmdErr != nil { - fmt.Println(cmdErr) + mode := os.FileMode(0o644) + if stat, err := os.Stat(targetPath); err == nil { + mode = stat.Mode().Perm() + } + if err := os.WriteFile(targetPath, content, mode); err != nil { + fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", targetPath, err) os.Exit(1) } }