diff --git a/experimental/libbox/build_info.go b/experimental/libbox/build_info.go new file mode 100644 index 00000000..5a02593c --- /dev/null +++ b/experimental/libbox/build_info.go @@ -0,0 +1,234 @@ +//go:build android + +package libbox + +import ( + "archive/zip" + "bytes" + "debug/buildinfo" + "io" + "runtime/debug" + "strings" + + "github.com/sagernet/sing/common" +) + +const ( + androidVPNCoreTypeOpenVPN = "OpenVPN" + androidVPNCoreTypeShadowsocks = "Shadowsocks" + androidVPNCoreTypeClash = "Clash" + androidVPNCoreTypeV2Ray = "V2Ray" + androidVPNCoreTypeWireGuard = "WireGuard" + androidVPNCoreTypeSingBox = "sing-box" + androidVPNCoreTypeUnknown = "Unknown" +) + +type AndroidVPNType struct { + CoreType string + CorePath string + GoVersion string +} + +func ReadAndroidVPNType(publicSourceDirList StringIterator) (*AndroidVPNType, error) { + apkPathList := iteratorToArray[string](publicSourceDirList) + var lastError error + for _, apkPath := range apkPathList { + androidVPNType, err := readAndroidVPNType(apkPath) + if androidVPNType == nil { + if err != nil { + lastError = err + } + continue + } + return androidVPNType, nil + } + return nil, lastError +} + +func readAndroidVPNType(publicSourceDir string) (*AndroidVPNType, error) { + reader, err := zip.OpenReader(publicSourceDir) + if err != nil { + return nil, err + } + defer reader.Close() + var lastError error + for _, file := range reader.File { + if !strings.HasPrefix(file.Name, "lib/") { + continue + } + vpnType, err := readAndroidVPNTypeEntry(file) + if err != nil { + lastError = err + continue + } + return vpnType, nil + } + for _, file := range reader.File { + if !strings.HasPrefix(file.Name, "lib/") { + continue + } + if strings.Contains(file.Name, androidVPNCoreTypeOpenVPN) || strings.Contains(file.Name, "ovpn") { + return &AndroidVPNType{CoreType: androidVPNCoreTypeOpenVPN}, nil + } + if strings.Contains(file.Name, androidVPNCoreTypeShadowsocks) { + return &AndroidVPNType{CoreType: androidVPNCoreTypeShadowsocks}, nil + } + } + return nil, lastError +} + +func readAndroidVPNTypeEntry(zipFile *zip.File) (*AndroidVPNType, error) { + readCloser, err := zipFile.Open() + if err != nil { + return nil, err + } + libContent := make([]byte, zipFile.UncompressedSize64) + _, err = io.ReadFull(readCloser, libContent) + readCloser.Close() + if err != nil { + return nil, err + } + buildInfo, err := buildinfo.Read(bytes.NewReader(libContent)) + if err != nil { + return nil, err + } + var vpnType AndroidVPNType + vpnType.GoVersion = buildInfo.GoVersion + if !strings.HasPrefix(vpnType.GoVersion, "go") { + vpnType.GoVersion = "obfuscated" + } else { + vpnType.GoVersion = vpnType.GoVersion[2:] + } + vpnType.CoreType = androidVPNCoreTypeUnknown + if len(buildInfo.Deps) == 0 { + vpnType.CoreType = "obfuscated" + return &vpnType, nil + } + + dependencies := make(map[string]bool) + dependencies[buildInfo.Path] = true + for _, module := range buildInfo.Deps { + dependencies[module.Path] = true + if module.Replace != nil { + dependencies[module.Replace.Path] = true + } + } + for dependency := range dependencies { + pkgType, loaded := determinePkgType(dependency) + if loaded { + vpnType.CoreType = pkgType + } + } + if vpnType.CoreType == androidVPNCoreTypeUnknown { + for dependency := range dependencies { + pkgType, loaded := determinePkgTypeSecondary(dependency) + if loaded { + vpnType.CoreType = pkgType + return &vpnType, nil + } + } + } + if vpnType.CoreType != androidVPNCoreTypeUnknown { + vpnType.CorePath, _ = determineCorePath(buildInfo, vpnType.CoreType) + return &vpnType, nil + } + if dependencies["github.com/golang/protobuf"] && dependencies["github.com/v2fly/ss-bloomring"] { + vpnType.CoreType = androidVPNCoreTypeV2Ray + return &vpnType, nil + } + return &vpnType, nil +} + +func determinePkgType(pkgName string) (string, bool) { + pkgNameLower := strings.ToLower(pkgName) + if strings.Contains(pkgNameLower, "clash") { + return androidVPNCoreTypeClash, true + } + if strings.Contains(pkgNameLower, "v2ray") || strings.Contains(pkgNameLower, "xray") { + return androidVPNCoreTypeV2Ray, true + } + + if strings.Contains(pkgNameLower, "sing-box") { + return androidVPNCoreTypeSingBox, true + } + return "", false +} + +func determinePkgTypeSecondary(pkgName string) (string, bool) { + pkgNameLower := strings.ToLower(pkgName) + if strings.Contains(pkgNameLower, "wireguard") { + return androidVPNCoreTypeWireGuard, true + } + return "", false +} + +func determineCorePath(pkgInfo *buildinfo.BuildInfo, pkgType string) (string, bool) { + switch pkgType { + case androidVPNCoreTypeClash: + return determineCorePathForPkgs(pkgInfo, []string{"github.com/Dreamacro/clash"}, []string{"clash"}) + case androidVPNCoreTypeV2Ray: + if v2rayVersion, loaded := determineCorePathForPkgs(pkgInfo, []string{ + "github.com/v2fly/v2ray-core", + "github.com/v2fly/v2ray-core/v4", + "github.com/v2fly/v2ray-core/v5", + }, []string{ + "v2ray", + }); loaded { + return v2rayVersion, true + } + if xrayVersion, loaded := determineCorePathForPkgs(pkgInfo, []string{ + "github.com/xtls/xray-core", + }, []string{ + "xray", + }); loaded { + return xrayVersion, true + } + return "", false + case androidVPNCoreTypeSingBox: + return determineCorePathForPkgs(pkgInfo, []string{"github.com/sagernet/sing-box"}, []string{"sing-box"}) + case androidVPNCoreTypeWireGuard: + return determineCorePathForPkgs(pkgInfo, []string{"golang.zx2c4.com/wireguard"}, []string{"wireguard"}) + default: + return "", false + } +} + +func determineCorePathForPkgs(pkgInfo *buildinfo.BuildInfo, pkgs []string, names []string) (string, bool) { + for _, pkg := range pkgs { + if pkgInfo.Path == pkg { + return pkg, true + } + strictDependency := common.Find(pkgInfo.Deps, func(module *debug.Module) bool { + return module.Path == pkg + }) + if strictDependency != nil { + if isValidVersion(strictDependency.Version) { + return strictDependency.Path + " " + strictDependency.Version, true + } else { + return strictDependency.Path, true + } + } + } + for _, name := range names { + if strings.Contains(pkgInfo.Path, name) { + return pkgInfo.Path, true + } + looseDependency := common.Find(pkgInfo.Deps, func(module *debug.Module) bool { + return strings.Contains(module.Path, name) || (module.Replace != nil && strings.Contains(module.Replace.Path, name)) + }) + if looseDependency != nil { + return looseDependency.Path, true + } + } + return "", false +} + +func isValidVersion(version string) bool { + if version == "(devel)" { + return false + } + if strings.Contains(version, "v0.0.0") { + return false + } + return true +}